参考:ctf-wiki
Use After Free
漏洞原因
在堆管理机制中,调用free(*ptr)函数后,会将ptr所指向的那个chunk
给释放掉,但是不会将ptr
设置为NULL,这需要程序员编写代码手动设置为NULL
由于程序员的疏忽,没有将ptr
的值设置为NULL,或者没有将ptr
指向其他堆块。这就会导致可以再次利用该指针,对所释放的那个堆块内存进行一些操作
漏洞原理
内存块被释放后,其对应的指针被设置为NULL,然后再次使用,自然程序会崩溃
内存块被释放后,其对应的指针没有被设置为NULL,然后在它下一次被使用之前,没有代码对这块内存进行修改,那么程序很有可能可以正常运转
内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题
故Use After Free 漏洞名称就是字面意思,利用的就是后两种机制。一般称被释放后没有被设置为NULL的内存指针为:dangling pointer
简单实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <stdlib.h> typedef struct name { char *myname; void (*func)(char *str); } NAME; void myprint (char *str) { printf ("%s\n" ,str); } void printmyname () { printf ("call print my name\n" ); } int main () { NAME *a; a = (NAME *)malloc (sizeof (struct name)); a->func = myprint; a->myname = "I can also use it" ; a->func("this is my function" ); free (a); a->func("I can also use it" ); a->func("this is my function" ); a = NULL ; printf ("this pogram will crash...\n" ); a->func("can not be printed..." ); }
1 2 3 4 5 6 ➜ use_after_free git:(use_after_free) ✗ ./use_after_free this is my function I can also use it call print my name this pogram will crash... [1] 38738 segmentation fault (core dumped) ./use_after_free
程序在运行到这块时
屏幕上就会打印出this is my function
然后程序free(a),这时申请的堆被释放了但是再调用结构体里面的内容还是可以有函数操作
1 2 I can also use it call print my name
在将a指针置为NULL时,再访问内存,就会出现段错误了
1 2 this pogram will crash... [1] 38738 segmentation fault (core dumped) ./use_after_free
1 2 3 myheart@aaa ~/C/p/heap_lab> ./a.out this is my function fish: Job 1, './a.out' terminated by signal SIGSEGV (Address boundary error)
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 #include <stdio.h> #include <stdlib.h> #include <unistd.h> struct note { void (*printnote)(); char *content; }; struct note *notelist [5];int count = 0 ;void print_note_content (struct note *this) { puts (this->content); }void add_note () { int i; char buf[8 ]; int size; if (count > 5 ) { puts ("Full" ); return ; } for (i = 0 ; i < 5 ; i++) { if (!notelist[i]) { notelist[i] = (struct note *)malloc (sizeof (struct note)); if (!notelist[i]) { puts ("Alloca Error" ); exit (-1 ); } notelist[i]->printnote = print_note_content; printf ("Note size :" ); read(0 , buf, 8 ); size = atoi(buf); notelist[i]->content = (char *)malloc (size); if (!notelist[i]->content) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read(0 , notelist[i]->content, size); puts ("Success !" ); count++; break ; } } } void del_note () { char buf[4 ]; int idx; printf ("Index :" ); read(0 , buf, 4 ); idx = atoi(buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); _exit(0 ); } if (notelist[idx]) { free (notelist[idx]->content); free (notelist[idx]); puts ("Success" ); } } void print_note () { char buf[4 ]; int idx; printf ("Index :" ); read(0 , buf, 4 ); idx = atoi(buf); if (idx < 0 || idx >= count) { puts ("Out of bound!" ); _exit(0 ); } if (notelist[idx]) { notelist[idx]->printnote(notelist[idx]); } } void magic () { system("cat flag" ); }void menu () { puts ("----------------------" ); puts (" HackNote " ); puts ("----------------------" ); puts (" 1. Add note " ); puts (" 2. Delete note " ); puts (" 3. Print note " ); puts (" 4. Exit " ); puts ("----------------------" ); printf ("Your choice :" ); }; int main () { setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 2 , 0 ); char buf[4 ]; while (1 ) { menu(); read(0 , buf, 4 ); switch (atoi(buf)) { case 1 : add_note(); break ; case 2 : del_note(); break ; case 3 : print_note(); break ; case 4 : exit (0 ); break ; default : puts ("Invalid choice" ); break ; } } return 0 ; }
程序分析
程序菜单
还有程序先定义的结构体
程序定义了note
结构体,里面声明了一个printnote
函数指针,还声明了一个字符类型的指针 content
然后程序声明了 note
类型的指针数组notelist
长度为个
还声明了一个int类型count
全局变量,令其为0。
1 2 3 4 5 6 7 struct note { void (*printnote)(); char *content; }; struct note *notelist [5];int count = 0 ;
程序还定义了函数,其功能为输出结构体的内容content
1 2 3 4 void print_note_content (struct note *this) { puts (this->content); }
1 2 3 4 void magic () { system("cat flag" ); }
add_note
这里面为了更好还原做题环境,就使用反编译后的代码,就不使用源码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 unsigned int add_note () { int v0; int i; int size; char buf[8 ]; unsigned int v5; v5 = __readgsdword(0x14 u); if ( count <= 5 ) { for ( i = 0 ; i<= 4 ; ++i ) { if ( !*(¬elist + i) ) { *(¬elist + i) = malloc (8u ); if ( !*(¬elist + i) ) { puts ("Alloca Error" ); exit (-1 ); } **(¬elist + i) = print_note_content; printf ("Note size :" ); read(0 , buf, 8u ); size = atoi(buf); v0 = *(¬elist + i); *(v0 + 4 ) = malloc (size); if ( !*(*(¬elist + i) + 1 ) ) { puts ("Alloca Error" ); exit (-1 ); } printf ("Content :" ); read(0 , *(*(¬elist + i) + 1 ), size); puts ("Success !" ); ++count; return __readgsdword(0x14 u) ^ v5; } } } else { puts ("Full" ); } return __readgsdword(0x14 u) ^ v5; }
1 2 3 4 5 int v0; int i; int size; char buf[8 ]; unsigned int v5;
然后对全局变量count
做一个判断,count
小于等于5会进入一个for循环。count
大于5会输出Full
,并结束add_note
函数
在for循环里面先判断nodelist
数组里面的值是否为0。nodelist
数组里面为0,就开辟一个node结构体的堆内存。
开辟后,再进行一次判断,查看是否成功开辟了一块堆内存。
接下来,将print_note_content
函数地址,赋值给node
结构体里面的名为printnote
的函数指针
并提示你输入接下来要开辟的堆大小。
开辟输入大小的堆块,然后将开辟的堆地址给前面开辟node
结构体里面的content
变量里面
在判断堆是否开辟成功
判断之后,提示你输入文本的内容,并将内容存储在node
结构体中content
变量所指向的内存地址
提示输入成功后count
自增1,使得下一次addnode
时能够判断conunt
有没有超过5
然后返回到父函数里面去
print_node
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 unsigned int print_note () { int v1; char buf[4 ]; unsigned int v3; v3 = __readgsdword(0x14 u); printf ("Index :" ); read(0 , buf, 4u ); v1 = atoi(buf); if ( v1 < 0 || v1 >= count ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(¬elist + v1) ) (**(¬elist + v1))(*(¬elist + v1)); return __readgsdword(0x14 u) ^ v3; }
提示用户输入索引号,用户输入索引号
判断输入的索引号有没有越界
然后调用输入索引对应的nodelist
数组存储(指针所指)的node
结构体里面的函数指针,并且将索引对应的node
结构体输入进去的content
变量里面的字符串打印出来
delete_node
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 unsigned int del_note () { int v1; char buf[4 ]; unsigned int v3; v3 = __readgsdword(0x14 u); printf ("Index :" ); read(0 , buf, 4u ); v1 = atoi(buf); if ( v1 < 0 || v1 >= count ) { puts ("Out of bound!" ); _exit(0 ); } if ( *(¬elist + v1) ) { free (*(*(¬elist + v1) + 1 )); free (*(¬elist + v1)); puts ("Success" ); } return __readgsdword(0x14 u) ^ v3; }
漏洞分析
堆溢出
堆溢出的简单原理就是程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数。(这里注意是可使用的字节数,而不是用户所申请的字节数 )然后导致数据溢出,溢出到与当前堆块物理地址相邻的高地址的下一个堆块。
题目
例题1
[NISACTF 2022]ezheap | NSSCTF
先打开IDA查看程序内容
发现程序连续开辟了两个chunk,这两个chunk的内存空间是连续的,再加上gets的无限溢出,即可溢出到command
指针所指向的堆块,然后system(command),将溢出内容填为/bin/sh\x00
就能取得shell
然后再通过动态调试或者根据chunk开辟大小的规则确定chunk的真实大小
in_use_size=(用户请求大小+8-4)align to 8 bytes(这里的单位为字节)这里加8是因为需要存储prev_size和size,但又因为向下一个chunk“借”了4个bytes,所以要减去4。但是空闲的chunk和使用中的chunk使用的是同一块空间。要取这两者之间的最大值。所以最终分配的空间为chunk_size=max(in_use_size,16)。
这里申请了0x16个字节,加上0x8-0x4得到0x1A个字节,然后再与8字节(这里是32位的程序,与8字节对齐就行,64位程序要与16字节对齐),最后得到的用户实际可以使用的字节数就是0x20字节
使用动态调试的时候也可以看到Size是0x20字节