Ghost_Diary
Last updated
Was this helpful?
Last updated
Was this helpful?
Try writing in this ghost diary. Its also found in /problems/ghost-diary_5_7e39864bc6dc6e66a1ac8f4632e5ffba on the shell server.
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 ().
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.
tcache
bins prevent us from getting a leak normally by freeing unsorted bin. However, each tcache
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 is 0x119
because of PREV_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 becomes 0x100
. However, we need to change the prev_size
value of the chunk following B or else malloc
will error when it is called. This can be done by writing a fake block header into block B that is 0x100-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 of 0x100
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 because malloc
does not check the size value. Freeing block B changes the PREV_INUSE
byte of block C, making block C's size value change from 0x121
to 0x120
, to mark the previous block as not in use. It also
We 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 from 0x120
to 0x100
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. When free()
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, running alloc(0x88)
to create a block of size 0x88
will split the chunk in the unsorted list, which in our case is chunk B of size 0x118
, into 0x88
, as requested by the program, and 0x90
which is the leftover amount.
We can call malloc
again to get our chunk that we’ll collapse over. Specifically, to request a chunk of size 0x30
, leaving a remainder of 0x40
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:
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 to system()
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 to free_hook
. Then, we request the next chunk from the same tcache list. This will return a pointer to free_hook
. After that, we set the address that page 2 points to, which is actually free_hook, to system
by editting the page that points to __free_hook
.
We overwrote the __free_hook
pointer to the system('/bin/sh')
function. So, now we actually have to use our overwritten memory by calling free()
, 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.
picoCTF{nu11_byt3_Gh05T_abf74d12}
When the block of size 0x88
is requested malloc
simply returns a pointer to the block of memory. It does not null out the values or set them to zero like calloc
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's main_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 how malloc
determines which address to return visit () 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 after malloc(0x88)
is called:
Now, we can free chunk C to backwards consolidate over B2, resulting in overlapping chunks ( 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".
Run the and get the flag python script.py USER=<username> PASSWORD=<password>
: