参考:ctf-wiki

Use After Free

漏洞原因

  • 在堆管理机制中,调用free(*ptr)函数后,会将ptr所指向的那个chunk给释放掉,但是不会将ptr设置为NULL,这需要程序员编写代码手动设置为NULL
  • 由于程序员的疏忽,没有将ptr的值设置为NULL,或者没有将ptr指向其他堆块。这就会导致可以再次利用该指针,对所释放的那个堆块内存进行一些操作

漏洞原理

  • 内存块被释放后,其对应的指针被设置为NULL,然后再次使用,自然程序会崩溃
  • 内存块被释放后,其对应的指针没有被设置为NULL,然后在它下一次被使用之前,没有代码对这块内存进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

Use After Free漏洞名称就是字面意思,利用的就是后两种机制。一般称被释放后没有被设置为NULL的内存指针为:dangling pointer

简单实验

  • 在Linux上编写如下代码
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");
// 释放堆内存,但是没有置NULL
free(a);
a->func("I can also use it");
a->func("this is my function");
// 将堆指针置NULL
a = NULL;
printf("this pogram will crash...\n");
a->func("can not be printed...");
}
  • 在比较老的Linux上运行结果如下
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
  1. 程序在运行到这块时

image-20240604133436631

  • 屏幕上就会打印出this is my function
  1. 然后程序free(a),这时申请的堆被释放了但是再调用结构体里面的内容还是可以有函数操作

image-20240604133614652

  • 屏幕上还是会打印出来内容
1
2
I can also use it
call print my name
  1. 在将a指针置为NULL时,再访问内存,就会出现段错误了

image-20240604133747725

  • 屏幕上就会出现以下内容
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)

例子

  • 以ctf-wik上对应内容给出的链接题目为例子:lab 10 hacknote

  • 源码如下

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;
}

程序分析

程序菜单

  • 运行一下该程序,该程序首先出现的是一个菜单

image-20240604134728924

  • 还有程序先定义的结构体
    • 程序定义了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);
}
  • 还有一个名为magic的函数
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; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i<= 4; ++i )
{
if ( !*(&notelist + i) )
{
*(&notelist + i) = malloc(8u);
if ( !*(&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
**(&notelist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = *(&notelist + i);
*(v0 + 4) = malloc(size);
if ( !*(*(&notelist + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *(*(&notelist + i) + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}
  • 对反编译的代码进行分析
  • 代码声明了如下的变量
1
2
3
4
5
int v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]
  • 然后对全局变量count做一个判断,count小于等于5会进入一个for循环。count大于5会输出Full,并结束add_note函数

  • 在for循环里面先判断nodelist数组里面的值是否为0。nodelist数组里面为0,就开辟一个node结构体的堆内存。

  • 开辟后,再进行一次判断,查看是否成功开辟了一块堆内存。

image-20240604191953761

  • 接下来,将print_note_content函数地址,赋值给node结构体里面的名为printnote的函数指针
  • 并提示你输入接下来要开辟的堆大小。
  • 开辟输入大小的堆块,然后将开辟的堆地址给前面开辟node结构体里面的content变量里面
  • 在判断堆是否开辟成功

image-20240604193028811

  • 判断之后,提示你输入文本的内容,并将内容存储在node结构体中content变量所指向的内存地址
  • 提示输入成功后count自增1,使得下一次addnode时能够判断conunt有没有超过5
  • 然后返回到父函数里面去

image-20240604193444258

  • IDA反编译后的代码如下
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; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&notelist + v1) )
(**(&notelist + v1))(*(&notelist + v1));
return __readgsdword(0x14u) ^ v3;
}
  • 首先定义了三个局部变量

image-20240604193634817

  • 提示用户输入索引号,用户输入索引号
  • 判断输入的索引号有没有越界

image-20240604193845969

  • 然后调用输入索引对应的nodelist数组存储(指针所指)的node结构体里面的函数指针,并且将索引对应的node结构体输入进去的content变量里面的字符串打印出来

image-20240604194421973

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; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&notelist + v1) )
{
free(*(*(&notelist + v1) + 1));
free(*(&notelist + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}
  • 先定义几个局部变量

image-20240604194541863

  • 提示输入索引,然后让用户输入指定索引号
  • 判断索引号有没有在指定范围内
  • 接下来实现对应索引堆内存的释放,释放两个地方,先释放content所指向的堆内存,再释放指定索引的用来存放node结构体的堆

  • 注意:这里free只是单纯的free了,并没有将对应的索引里面的指针置NULL

image-20240604194944672

漏洞分析

堆溢出

  • 堆溢出的简单原理就是程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数。(这里注意是可使用的字节数,而不是用户所申请的字节数)然后导致数据溢出,溢出到与当前堆块物理地址相邻的高地址的下一个堆块。

题目

例题1

[NISACTF 2022]ezheap | NSSCTF

  • 先打开IDA查看程序内容
  • 发现程序连续开辟了两个chunk,这两个chunk的内存空间是连续的,再加上gets的无限溢出,即可溢出到command指针所指向的堆块,然后system(command),将溢出内容填为/bin/sh\x00就能取得shell

image-20240619113717648

  • 然后再通过动态调试或者根据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字节

image-20240619115704284