SEEDLab-堆与UAF漏洞

SEEDLab-堆与UAF漏洞

实验介绍

本实验旨在深入理解UAF(Use-After-Free)漏洞的成因、利用原理及其在实际程序中的表现形式。通过分析两个典型场景——普通结构体中的函数指针UAF和C++对象中虚函数表(vtable)相关的UAF,掌握堆内存分配与释放的基本机制,熟悉攻击者如何通过精心构造输入覆盖已释放内存中的关键数据(如函数指针或虚表指针),从而劫持程序控制流并执行任意代码。同时,借助GDB等调试工具,观察内存布局变化,验证漏洞触发过程,提升对内存安全问题的分析与防护意识。

实验内容

任务一 Use After Free漏洞

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
#include <stdio.h>

typedef struct s{
int id;
char name[20];
void (*clean)(void *);
}VULNSTRUCT;

void *cleanMemory(void *mem){
free(mem);
}
int main(int argc, char *argv[]){
void *ptr1;
VULNSTRUCT *vuln=malloc(256);

fflush(stdin);
printf("Enter id num: ");
scanf("%d", &vuln->id);
printf("Enter your name: ");
scanf("%s", vuln->name);

vuln->clean=cleanMemory;

if(vuln->id>100){
vuln->clean(vuln);
}

ptr1=malloc(256);
strcpy(ptr1, argv[1]);

free(ptr1);
vuln->clean(vuln);

return 0;
}

首先,观察程序代码,发现总体上调用了两次vuln结构体中的clean函数,clean函数执行free操作,因此程序中存在可利用的UAF漏洞,在if条件中调用clean视为free,在此之后,又为ptr1分配相同大小的内存块,可以猜想这次分配得到的块与被释放内存块相同。下面通过GDB调试验证这一点。

图1 反汇编获取分配内存位置 图2 获取分配内存位置 如图1,反汇编程序,在调用malloc函数之后,返回值(分配内存的地址)存放在eax寄存器中,在此设置断点。如图二,运行后查看eax中的内容即可获取申请内存的位置。

图3 使用GDB调试程序 图4 使用GDB查看填充的输入 图5 编写程序输入 如图3,查看分配内存块的内容,可以看到写入到ptr1所指区域的“AAAA”确实写入到了被释放的vuln区域中,可以确定是存在UAF漏洞。如图5,编写程序输入,,将shellcode放到ptr1的后半部分,将clean函数指针覆盖为shellcode的地址,考虑到误差,采用slide方式,使得程序可以正确运行到shellcode。

图6 攻击成功 如图6,运行漏洞程序,攻击成功。

任务二 UAF和VPTR

创建badfile,你可以用下面的框架来创建。

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
#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std;

class Human{
private:
virtual void give_shell(){
setuid(geteuid());
system("/bin/sh");
}
protected:
int age;
string name;
public:
virtual void introduce(){
cout << "My name is " << name << endl;
cout << "I am " << age << " years old" << endl;
}
};

class Man: public Human{
public:
Man(string name, int age){
this->name = name;
this->age = age;
}
virtual void introduce(){
Human::introduce();
cout << "I am a nice guy!" << endl;
}
};

class Woman: public Human{
public:
Woman(string name, int age){
this->name = name;
this->age = age;
}
virtual void introduce(){
Human::introduce();
cout << "I am a cute girl!" << endl;
}
};

int main(int argc, char* argv[]){
Human* m = new Man("Jack", 25);
Human* w = new Woman("Jill", 21);

size_t len;
char* data;
unsigned int op;
while(1){
cout << "1. use\n2. after\n3. free\n";
cin >> op;

switch(op){
case 1:
m->introduce();
w->introduce();
break;
case 2:
len = atoi(argv[1]);
data = new char[len];
read(open(argv[2], O_RDONLY), data, len);
cout << "your data is allocated" << endl;
break;
case 3:
delete m;
delete w;
break;
default:
break;
}
}
return 0;
}

观察程序代码,这是一个菜单程序,其中选项可以选择释放内存、申请内存或者调用函数,存在UAF漏洞,同时类中存在虚函数,即可利用虚函数表进行攻击。

图7 反汇编程序 如图7所示,为了观察内存中堆的情况,在程序中调用函数的部分和read读取文件的部分以及释放内存的部分设置断点。

图8 查看分配内存中的内容 如图8,运行程序到free之前可以查看分配区域的内容,先查看区域地址,再查看堆中的内容,可以看到w和m对象的内容,其中就有虚函数表地址。

图9 申请内存后查看内存内容 如图9,先选择free后再选择after申请内存,输入AAAA,可以看到虽然申请的内存在之前释放的区域,但并非是m的区域而是w的区域,但由于在use选项中先调用的是m中的函数,因此这么覆盖无法攻击成功。

图10 两次申请内存后查看内存内容 如图9,由于先释放m再释放w,所以会先占用w,再申请一次就会占用m的空间,用这种方式就可以覆盖m的虚函数表地址。

图11 查看程序中的函数地址 图12 查看虚函数表内容 如图11和图12,使用GDB查看程序中各个函数的地址,再查看虚函数表的内容,可以确定giveshell地址在函数表中的位置,下面只要将虚函数表指针覆盖成giveshell的位置,再选择use选项即可运行giveshell,攻击成功。

图13 编写程序输入 图14 攻击成功 如图13,编写程序输入,将虚函数表地址覆盖为giveshell的地址的地址。如图14,攻击成功。

任务三 问题

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
// g++ -z execstack -o vtable vtable.cpp
#include <iostream>
#include <cstring>
#include <unistd.h>
class Number
{
public:
Number(int x) : number(x) {
}
void setAnnotation(char *a) {
memcpy(annotation, a, strlen(a));
}

virtual int operator+(Number &r){
return number + r.number;
}
private:
char annotation[100];
int number;
};
int main(int argc, char **argv)
{
if(argc < 2) _exit(1);

Number *x = new Number(5);
Number *y = new Number(6);
Number &five = *x, &six = *y;

five.setAnnotation(argv[1]);

return six + five;
}

首先观察程序代码,发现类中存在虚函数调用,同时存在无限制的缓冲区复制,可以造成溢出,也有溢出的条件。可以将shellcode放在five的annotation+4的位置,将shellcode的地址放在annotation,再将six的虚函数指针覆盖为annotaion。 图15 反汇编main() 首先对程序进行调试,首先反编译main(),可以发现setAnnotation方法的调用,在调用该方法时,寄存器eax中存放的应当是对象five的地址。

图16 查看对象中的内容 图17 攻击成功 查看对象five中的内容,可以看到两个一模一样的地址值,可以确定这就是虚函数地址只需将对象six的虚函数指针覆盖为shellcode地址的地址即可。因此构造payload,在five的annotation中写入shellcode的地址,并将six的虚函数指针覆盖为five的annotation。如图12所示,攻击成功。


SEEDLab-堆与UAF漏洞
https://eleco.top/2025/11/21/SEEDLab-堆与UAF漏洞/
作者
Eleco
发布于
2025年11月21日
许可协议