这其实是我在大二的时候给微软俱乐部捉虫大赛出的题目之一。代码基本上是写好了的,故意留出了一些bug,只可惜没人尝试来做。
关于/GS编译选项
VS在某一个版本之后添加了/GS编译选项,增强了对溢出检测的能力。随后对Windows XP进行了全面的重新编译,发布了SP2,安全性大大提高。
/GS的原理其实比较简单:
- 程序开始时,在堆的某个位置初始化一个security cookie。cookie的位置是不固定的。
- 存放cookie地址的指针大概在PEB之后的某个位置,地址也不固定,但是有一定的范围。对于这一点的记忆有点模糊了,手头也没有当时看的资料,网上也没搜到……确切查到了如果不对再来更新。
- 构建栈帧时,在所有的局部变量之前压入一个security cookie的副本。
- 函数返回时,检查栈中的security cookie副本与全局的security cookie是否一致,如果不一致则判定为栈溢出,执行相应的动作。
这些步骤之中很多自然需要由编译器来做,不过我们同样可以在源代码的层面模拟此类动作。当然,实际应用的价值也许并不大。
在C语言中模拟/GS编译选项
下面的模拟与VS的/GS编译选项稍有不同。我们在栈中并不保存security cookie,而是保存cookie与当前栈帧中的ebp(即上一个栈帧的栈底)的异或值。由于当前ebp寄存器中保存的是当前栈帧栈底的地址,因此需要间访一次。在函数退出的时候,检查异或值即可。
采用这种机制的原因是,我们没有办法保证security cookie在所有临时变量之下,因此只好变通一下,跟ebp取个异或。
代码中对return做了define,因此所有函数的返回都必须以return的形式显示返回。没想到C语言里面怎么实现ScopeGuard,只能在return上下手了。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
/*
* A SIMPLE Security Cookie Example
* To Examine the Stack Overflow
*/
#define DEFALUT_SECURITY_COOKIE 0xE8CFF7E8
int __security_cookie = DEFALUT_SECURITY_COOKIE;
int __assistant_flag = 0;
int cookie = 0, *ebp = 0;
void __security_init_cookie()
{
__security_cookie ^= time(NULL);
}
void __security_check_fail()
{
printf("Stack Overflow!!n");
exit(0);
}
#define __mainStartup (main)
#define main() _main(int argc, char** argv)
#define ENABLE_SECURITY_CHECK \
int cookie, *ebp, __assistant_flag; \
__asm__("movl %%ebp, %0;":"=g"(ebp)); \
cookie = __security_cookie ^ *ebp; \
__assistant_flag = 1;
#define return if(__assistant_flag){ \
__asm__(" movl %%ebp, %0;":"=g"(ebp)); \
if((cookie ^ (*ebp)) != __security_cookie) \
__security_check_fail(); } \
return
/**
* =======================================
* DO NOT CHANGE THE CODE FROM HERE
*/
int main();
int __mainStartup(int argc, char** argv)
{
__security_init_cookie();
return _main(argc, argv);
}
/* End of Security Cookie Example */
/* ---------------------------------------------
* User Code Begin
* Now Let's See How to Use Our Security Check
* Functions with ENABLE_SECURITY_CHECK MUST use return-statement to return
* DO NOT CHANGE THE CODE
*/
int main()
{
char s[5];
ENABLE_SECURITY_CHECK
strcpy(s, "Welcome to 5th Debug Contest!n");
/* What a Stupid Overflow Example */
return 0;
}
`
上面的代码中main和mainStartup的宏定义只是为了替换掉入口函数。
在不希望使用栈溢出检测的函数中,不添加ENABLE_SECURITY_CHECK即可。为了保证能够正常编译运行,定义了三个同样名称的全局变量。
不足之处
上面的代码如果在溢出时恰好将__assistant_flag设置为0了,也可以绕过后面的检测。这是不足之一;
security cookie的地址固定,不足之二;
重新定义了return,因此所有函数返回的地方需要显示return,不足之三。