Published on

Pwn Learning Path #9: Planning My First ret2libc Leak

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

This is the point I have currently reached in my learning path. The lab is ready and its intended path has been verified, but my learner exploit is still deliberately unfinished.

The goal is to understand and implement this full shape:

leak a libc address -> calculate libc base -> call system("/bin/sh")

Why ret2libc

NX is enabled, so executing injected shellcode from the stack is not the intended route. Libc is already mapped as executable code, but ASLR changes its load address between runs.

The plan is therefore:

  1. disclose one real libc function address;
  2. use its known offset to calculate the libc base for this process;
  3. derive the runtime addresses of system and the /bin/sh string;
  4. call the existing libc code with ROP.

PLT and GOT in small pieces

For this lab, the important pair is puts@plt and puts@got:

  • puts@plt is executable code in the main binary that dispatches a call to puts;
  • puts@got is a data slot containing the resolved runtime address of libc's puts.

The binary calls puts before it reads my payload, so the GOT entry has already been resolved. If I call puts@plt with the address of puts@got as its first argument, it prints bytes from that slot and gives me the pointer I need.

Stage 1: disclose puts

The overflow offset is expected to remain 72 bytes. The first ROP chain is:

padding
pop rdi; ret
puts@got
puts@plt
vuln

Each entry has one role:

  • pop rdi; ret prepares the first argument;
  • puts@got is the address passed in rdi;
  • puts@plt performs the output call;
  • vuln restarts the input path so a second payload can be sent in the same process.

Returning to vuln is essential. The leak tells me the base for this process, and stage 2 must use it before the process exits and ASLR gives a new process a different layout.

The stage-one construction I am working toward is:

stage1 = flat(
    b'A' * offset,
    pop_rdi,
    elf.got['puts'],
    elf.plt['puts'],
    elf.symbols['vuln'],
)

Calculate the libc base

Once the leaked bytes are parsed as a 64-bit address, the arithmetic is:

libc.address = leaked_puts - libc.symbols['puts']
system = libc.symbols['system']
bin_sh = next(libc.search(b'/bin/sh\x00'))

libc.symbols['puts'] is the known offset of puts inside that libc file. Subtracting the offset from the leaked runtime address gives the mapping base. Setting libc.address lets pwntools rebase the other symbols and search results.

Stage 2: call system

The final chain should prepare rdi with the address of /bin/sh, then enter system:

padding
pop rdi; ret
address of "/bin/sh"
plain ret, if alignment requires it
system

The previous two labs explain the optional plain ret: it consumes eight bytes and can restore the alignment expected by code inside libc.

My next checkpoint

I am splitting the solve into two tests:

  1. make stage 1 print a plausible puts address and return cleanly to vuln;
  2. only after verifying the leak and base calculation, send stage 2.

That separation should make failures obvious. If the leak is wrong, there is no reason to debug system yet. If the calculated base is page-aligned and the derived symbols match GDB, then I can focus on the final call and stack alignment.

This is where the published series catches up with my current practice. The next entry will come after I complete and explain the two-stage exploit myself.

Series navigation

← Part 8: compiler-generated SSE alignment · Series roadmap