Ghost_Diary
Problem
Try writing in this ghost diary. Its also found in /problems/ghost-diary_5_7e39864bc6dc6e66a1ac8f4632e5ffba on the shell server.
Solution
Stage 1: Analysis
Analysis commands (run on shell server):
The libc version is 2.27 which implies the use of tcache with very little security checks. All protections are enabled, implying a heap only exploit.
Test run:
After running, we are given a menu with the following options: New page, Talk with ghost, Listen to ghost, Burn the page, and Go to sleep. We suspect that these operations correspond to a fairly standard heap problem with create, edit, read, delete, and exit.
Reverse the binary file using Ghidra (cheat sheet).
main()
function:create_page()
function:edit_page()
function:edit_page_input()
function (FUN_00100a5a()
):print_page()
function:delete_page()
function:menu()
function:Ghidra failed to decompile
main()
, but this should be okay since it probably only handles the logic that calls the above functions.Findings:
The
delete_page()
method correctly sets the pointer to NULL after freeing, so any use-after-free exploit will have to work non-trivially.There is a null byte overflow in the edit function.
In addition, we can print any chunk, regardless of if it’s freed or not, which we will use to get a libc leak.
Stage 2: Leak Libc (using a poison null byte)
tcache
bins prevent us from getting a leak normally by freeing unsorted bin. However, eachtcache
bin can only hold a maximum of 7 chunks, letting us easily overflow it. We create 8 chunks in the unsorted bin range, and free them all. The last freed chunk will have a libc address, which we can leak, since it will be freed to a regular bin.Throughout this exploit, we will need to ensure that the
tcache
is filled, or else our freed chunks will go into the tcache and not unsorted bins.This section is somewhat similar to the "zero_to_hero" challenge since we overflow a poison null byte into the next chunk to change the size of the next chunk.
In this case, in order to perform null byte poisoning we need 3 chunks (size
0x118
but value is0x119
because ofPREV_INUSE
bit).We eventually want to shrink chunk B by overwriting the least significant byte of the size header with
0x00
. Thus, the new size of B becomes0x100
. However, we need to change theprev_size
value of the chunk following B or elsemalloc
will error when it is called. This can be done by writing a fake block header into block B that is0x100-0x10
bytes after block B's header. Remember, all blocks overlap with the next block by 8 bytes in 64-bit programs (4 bytes in 32-bit). This overlap contains the last 8 bytes of the block if the block is allocated, but if it is unallocated it contains the size of the previous block. We write the size of0x100
to block_b_header+0xf0
and then free B (to the unsorted list) to setup the backwards consolidation later. We do not write a size to the fake header becausemalloc
does not check the size value. Freeing block B changes thePREV_INUSE
byte of block C, making block C's size value change from0x121
to0x120
, to mark the previous block as not in use. It alsoWe then abuse the null byte overflow to change B’s size. We fill block 7 (block A) with
A
s and the single null byte overflows to change block 8's (block B) size from0x120
to0x100
When
malloc()
is called, it looks on its list for a piece of memory that is big enough. If it finds one, then it removes that memory from the linked list and returns it to the user. Whenfree()
is called, the memory is put back on the linked list. Now, to be efficient, if there is a chunk of memory on the free list that much bigger than what is requested, then it breaks up that chunk into two chunks: one which is the size of the request (padded to a multiple of 8), and the remainder. The remainder is put on the free list and the one the size of the request is returned to the user. Therefore, runningalloc(0x88)
to create a block of size0x88
will split the chunk in the unsorted list, which in our case is chunk B of size0x118
, into0x88
, as requested by the program, and0x90
which is the leftover amount.When the block of size
0x88
is requestedmalloc
simply returns a pointer to the block of memory. It does not null out the values or set them to zero likecalloc
does. When block B was initially freed to the unsorted list the backwards pointer was overwritten with the address of the unsorted bin list in libc'smain_arena
. Thus, when it is split and the top part is returned we get a block of memory containing a libc address. We can calculate the offset and obtain the libc base address. Fore more information about howmalloc
determines which address to return visit the MallocInternals glibc wiki page (Archive) and read the "Malloc Algorithm" section (although the whole page is great and I recommend reading it all if you are unfamiliar). The image below shows the state of the heap aftermalloc(0x88)
is called:
Stage 3: Overlap chunks and overwrite an address
We can call
malloc
again to get our chunk that we’ll collapse over. Specifically, to request a chunk of size0x30
, leaving a remainder of0x40
since B2 is being split. The heap now looks like this:We then free B1 to ensure that a proper
prev_size
header is written:Now, we can free chunk C to backwards consolidate over B2, resulting in overlapping chunks (this ctf-wiki page might be helpful in understanding overlapping chunks). Since we have overlapping chunks, we control chunk B2 we can free it so it goes in the tcache. Then, we create a chunk of size
0x130
to allocate our consolidated, overlapping chunk into page 0 of the "diary".Next, we overwrite the forward address of chunk B2 with the address of
free_hook
by using our overlapped chunks and writing directly into B2. Also, specify the argument tosystem()
to be/bin/sh
. We request the next block from the tcache list, leaving the head of the list pointing at the forward pointer of the chunk we just freed. We overwrote this forward pointer tofree_hook
. Then, we request the next chunk from the same tcache list. This will return a pointer tofree_hook
. After that, we set the address that page 2 points to, which is actually free_hook, tosystem
by editting the page that points to__free_hook
.We overwrote the
__free_hook
pointer to thesystem('/bin/sh')
function. So, now we actually have to use our overwritten memory by callingfree()
, which redirects its actions to whatever function__free_hook
happens to point to. Fore more info about__free_hook
see step 2 of "Stage 2: Overwrite an Address" of the "zero_to_hero" writeup.Run the script.py and get the flag
python script.py USER=<username> PASSWORD=<password>
:
Flag
picoCTF{nu11_byt3_Gh05T_abf74d12}
Last updated