Liveleak

Before diving into any rop-fu or leaking magic, we started with the basics: "What even is this file?"

file chall

file libc.so.6

checksec --file=chall

This to identify what protections are active. The result:

· No PIE = addresses are fixed, makes ROP easier

· No stack canary = overflow possible

· NX enabled = we can’t run shellcode, so we must use ROP

Before we could run any meaningful function like system(), we first needed to find the base address of the loaded libc. Since ASLR randomizes this base address, we could not hardcode it, so we used a classic ret2plt technique to leak the real address of puts() at runtime.

This stage focuses on building and triggering a ROP chain to call puts(puts@got) and leak the actual address of puts()from the Global Offset Table (GOT).

We opened the binary in GDB to see which function gets called from main():

Output showed:

call 0x40125c <vuln>

So main() calls a function named vuln(), which likely holds the input logic. Disassembling that next:

Inside vuln():

· The stack allocates 0x40 bytes (64 bytes)

· But uses fgets() to read 0x80 bytes (128 bytes)

That’s a buffer overflow; we just confirmed this binary is vulnerable. The total overflow offset is 0x40 (buffer) + 0x8 (saved RBP) = 72 bytes

We used the following tools to find the addresses needed for our ROP chain:

Result: 0x404018

Result: 0x401090

Found gadget: 0x4012bd : pop rdi ; ret

We also needed to return to main() after leaking, to send our next payload:

Found: main = 0x401292

We now have everything we needed to build our first-stage ROP chain:

This payload does:

1. Sets rdi = puts@got

2. Calls puts(), which prints the actual address of puts() in libc

3. Returns to main() to allow a clean second input

We used a short Python script to run the binary and send our payload:

Output: [+] Raw leaked data: b' \n'

This showed that the ROP chain reached puts(puts@got) successfully, even though the leak was too short in our simulation, it still proved the logic was sound.

After successfully leaking the address of puts() in Step 1 (via the ROP chain: puts(puts@got)), we proceeded to simulate the continuation of our exploit logic by calculating the base address of libc.so.6, followed by resolving the actual addresses of system() and the string "/bin/sh". This step forms the core of our final payload construction, which will execute system("/bin/sh") to spawn a shell.

We first loaded the custom libc.so.6 provided with the challenge into pwntools:

This confirmed that the binary was 64-bit and PIE-enabled, and more importantly, that debug symbols were present, which made it easier for us to extract symbols like puts, system, and string references.

We assumed a simulated leak from Step 1:

leaked_puts = 0x7ffff7e0d000 # simulated address from puts@got leak

This is the value we would normally receive from the output of puts(puts@got).

We then subtracted the known offset of puts (as resolved from the custom libc) from the leaked address to compute the libc base address:

Output:

[+] Simulated puts() leak: 0x7ffff7e0d000

[+] Calculated libc base: 0x7ffff7d8c1b0

This libc base address would now allow us to access any other function or string inside libc.

Using the calculated libc base, we resolved the actual address of system() and located the "/bin/sh"string inside the libc memory space:

This confirmed that we now had everything needed to build our second-stage payload and complete the exploit. From this point, we are ready to construct the final payload:

→ pop rdi ; ret → "/bin/sh" → system()

to spawn a shell and complete the challenge.

After leaking the address of puts() and calculating the base address of libc + resolving system() and "/bin/sh", we were ready for the final attack: crafting a ret2libc payload that spawns a shell. This stage is where everything comes together.

The payload layout was designed to do this:

In the final script flag.py, this section builds the stage 2 payload using the calculated libc_base, then adds the gadgets:

This will call system("/bin/sh") once executed.

We then sent the stage 2 payload:

Full flag.py:

Once this runs, we're dropped into a remote shell.

After sending the second-stage ROP payload, we successfully triggered the system("/bin/sh") call. This dropped us into a live shell on the remote server hosted at 34.133.69.112:10007.

Here’s the moment of truth:

The presence of flag_copy looked promising, so we followed up with:

This confirmed that our entire exploit chain, from leaking puts@GLIBC, computing the libc base, resolving system() and "/bin/sh", to finally building the second-stage ROP payload, was functioning as intended.

Flag: umcs{GOT_PLT_8f925fb19309045dac4db4572435441d}

Last updated