深入GS保护机制
大多数溢出漏洞产生的原因是由于数组越界,导致溢出。首先要明白溢出漏洞这个我在很早前就写过烂大街的文章了
我们知道大部分的溢出攻击主要是覆盖程序函数的返回地址那么看完再讲讲GS的工作流程=检测某些覆盖函数的返回地址、异常处理程序地址(SEH)或者类型参数的缓冲区溢出。在执行缓冲区溢出时会有安全检查GS 缓冲区。 GS 缓冲区可以是下列之一:
+++++++++++++++++++++++++++++
char buffer[20];
int buffer[20];
struct { int a; int b; int c; int d; } myStruct;
struct { int a; char buf[20]; };
但是,以下语句不声明 GS 缓冲区。 前两个声明包含指针类型的元素。 第三个和第四个语句声明的数组太小。 第五个语句声明一个结构,其大小在 x86 平台不超过 8 个字节。
+++++++++++++++++++++++++++++
char *pBuf[20];
void *pv[20];
char buf[4];
int buf[2];
struct { int a; int b; };
一个数组,大于 4 个字节,超过两个元素,并且具有不是指针类型的元素类型。一种数据结构,其大小超过 8 个字节,不包含指针。通过使用分配的缓冲区_alloca函数。任何类或结构,其中包含 GS 缓冲区。
例如,以下语句声明 GS 缓冲区。
从MSDN的官宣可以知道什么情况下可以申明GS,什么情况下是不可以。
GS编译器选项要求使用 COOKIE 的任何函数运行之前初始化安全 COOKIE。 在进入 EXE 或 DLL,必须立即初始化安全 COOKIE。如果使用默认 VCRuntime 入口点会自动完成: mainCRTStartup,wmainCRTStartup,WinMainCRTStartup,wWinMainCRTStartup,或 _DllMainCRTStartup。如果使用备用的入口点,必须通过调用手动初始化的安全 cookie __security_init_cookie这个就是后话了,当科普。
那么为了进一步了解GS,动手测试一下吧。
新建项目那么编译之前我们先关闭其他的一些机制,只开启GS的安全检查。
开启GS
+++++++++++++++++++++++++++++
void fun(const char * D)
{
char buf[10];
strcpy_s(buf, D);
}
int _tmain(int argc, _TCHAR* argv[])
{
fun("aaaaaaaaaaaaaaa");
return 0;
}
+++++++++++++++++++++++++++++
运行崩溃,载入Debugger。
查看堆栈空间
载入的15个字节的A,但是会发现真正入栈的只有9个A而多余都被舍去。
用可以看到有这么一行。
用OD分析Security_Check_Cookie流程
可以看到在执行我们的函数之前先CALL了一个地址。
CALL正是Security-cookie,下面分析流程。
00412EA0 > 55 push ebp
00412EA1 8BEC mov ebp,esp
00412EA3 83EC 14 sub esp,0x14
00412EA6 C745 F4 0000000>mov dword ptr ss:[ebp-0xC],0x0
00412EAD C745 F8 0000000>mov dword ptr ss:[ebp-0x8],0x0
00412EB4 813D 00804100 4>cmp dword ptr ds:[__security_cookie],0xBB40E64E
00412EBE 74 1F je short 00412EDF
00412EC0 A1 00804100 mov eax,dword ptr ds:[__security_cookie]
00412EC5 25 0000FFFF and eax,0xFFFF0000
00412ECA 74 13 je short 00412EDF
00412ECC 8B0D 00804100 mov ecx,dword ptr ds:[__security_cookie]
00412ED2 F7D1 not ecx
00412ED4 890D 04804100 mov dword ptr ds:[__security_cookie_complement],ecx
00412EDA E9 9A000000 jmp 00412F79
00412EDF 8D55 F4 lea edx,dword ptr ss:[ebp-0xC]
00412EE2 52 push edx
从反汇编窗口可以看出进行了一个比较然后取COOKIE之后存储然后生成一个新的COOKIE那么要生成新的COOKIE,函数首先会查询当前系统的时间也就是GetSystemTimeAsFileTime然后XOR生成新的保存至eax这是第一部分要做的事
第二部分GetCurrentThreadId开始获取了进程标识符然后XOR作为COOKIE的第二部分。
TEB第一个指向SHE,第二个指向了自身ID。
第三部分组成就是GetCurrentProcessId由线程标识符XOR了。
第四部分通过GetTickCount获取执行的毫秒数XOR作为COOKIE的第四部分。
003115D1 |. 8945 FC mov [local.1],eax
003115D4 |. FF15 04203100 call dword ptr ds:[<&KERNEL32.GetCurrentThreadId>] ; [GetCurrentThreadId
003115DA |. 3145 FC xor [local.1],eax
003115DD |. FF15 08203100 call dword ptr ds:[<&KERNEL32.GetCurrentProcessId>] ; [GetCurrentProcessId
003115E3 |. 3145 FC xor [local.1],eax
003115E6 |. 8D45 EC lea eax,[local.5]
003115E9 |. 50 push eax ; /pPerformanceCount
003115EA |. FF15 0C203100 call dword ptr ds:[<&KERNEL32.QueryPerformanceCounter>] ; \QueryPerformanceCounter
最后通过QueryPerformanceCounter获取性能计算器值异或XOR为第五部分的COOKIE。
当strcpy函数跑完后会进行一个COOKIE值比较,不相等就跳到了IsProcessorFeaturePresent。
跟完后发现他在函数执行之前就把COOKIE取出来PUSH到了堆栈当中,用于函数执行后的比较。
基本上门清了一下流程,当然GS不是万能的在某种情况下是不受保护的。官方也说明了:
什么是不受保护
/GS编译器选项不能防止所有缓冲区溢出安全攻击。 例如,如果在对象中有一个缓冲区和 vtable,缓冲区溢出可能会损坏 vtable。即使使用 /GS,始终应该尝试编写安全代码没有缓冲区溢出。
MSDN已经告诉我们了,一个虚表函数就可以绕过GS.
从现存的资料有这么几种方式:
1、攻击S.E.H
1、通过猜测cookies值绕过
2、通过覆盖虚函数指针绕过
3、等
这里我先拿简单的通过覆盖虚函数指针来举例,剩下的几种方法我会从下篇文章中一一列举。
那么就定义一个虚表函数来尝试绕过GS保护机制
// GS.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <string.h>
#include <windows.h>
class TestGS
{
public:
void test(char* src)
{
char buf[8];
strcpy(buf, src);
test2(); //调用
}
void test2()//虚函数
{
}
};
int main()
{
TestGS test;
test.test("AAAAAAAAAAAAAAAAAAAA");
return 0;
}
shellcode
/*LPVOID Memory = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(Memory, buf, sizeof(buf));
((void(*)())Memory)();*/
//unsigned char buf[] =
//"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
//"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
//"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
//"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
//"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
//"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
//"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
//"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
//"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
//"\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f"
//"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5"
//"\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
//"\x00\x53\xff\xd5\x6e\x6f\x74\x65\x70\x61\x64\x2e\x65\x78\x65"
//"\x00";
编译后运行崩溃,用ICE进行调试。
第一个CALL 生成Security-cookie
跟第二个CALL进去,从中会遇到很多函数别去管他找到入口。
再跟进到Strcpy函数。
使用JE指令来依次压栈,这里插入个题外话可以去跟一下各种复制字符串到地址空间的汇编代码,收获会有很多。这里发一下我自己跟的。
strcpy
00401000 /$ 55 push ebp
00401001 |. 8BEC mov ebp,esp
00401003 |. 83EC 08 sub esp,0x8
00401006 |. 33C0 xor eax,eax
00401008 |. EB 06 jmp short 00401010
0040100A | 8D9B 00000000 lea ebx,dword ptr ds:[ebx]
00401010 |> 8A88 20214000 /mov cl,byte ptr ds:[eax+0x402120]
00401016 |. 8D40 01 |lea eax,dword ptr ds:[eax+0x1]
00401019 |. 84C9 |test cl,cl
0040101B |.^ 75 F3 \jnz short 00401010
0040101D |. 8BE5 mov esp,ebp
0040101F |. 5D pop ebp
00401020 \. C2 0400 retn 0x4
scanf
706AEA32 > 55 push ebp
706AEA33 8BEC mov ebp, esp
706AEA35 8B4D 08 mov ecx, dword ptr [ebp+8]
706AEA38 FF49 04 dec dword ptr [ecx+4]
706AEA3B 78 0C js short 706AEA49
706AEA3D 8B01 mov eax, dword ptr [ecx]
706AEA3F 0FB610 movzx edx, byte ptr [eax]
706AEA42 40 inc eax
706AEA43 8901 mov dword ptr [ecx], eax
706AEA45 8BC2 mov eax, edx
还有像fopen等等。
当我们进入到Strcpy函数后单步走的注意力看着堆栈的变化
然后当我们Strcpy函数执行完后可以看到我们的虚函数TEST2的CALL
当mov edx,dword ptr ds:[eax] 获取到EAX的地址后我们就可以覆盖到虚函数指针的地址然后JMP我们的shellcode。具体计算我们可以使用:
虚表指针地址-字符串堆栈地址=要覆盖的字数
从中的COOKIE在虚表函数后再进行校验,而我们的SHELLCODE早就执行完了。
但这种方式在实际过程中对使用者技术难度相对要求稍微高一点。就到这吧动手的活留给大家琢磨了,关于shellcode的生成搞web就不陌生了,可以使用自己写的代码然后取机器码无非就是自己动手累一点了,我比较喜欢用现成的metasploit了。
剩下的几种方式会在下节文章中列举,Thanks。