栈的缓冲区溢出研究
2015-05-14 21:42
267 查看
1. C程序地址空间布局
2. 函数调用stdcall和cdecl
要理解栈的缓冲区溢出,对栈的结构要非常熟悉。这就需要了解函数调用时,参数是如何传递的。
一般来说,编译器会优先选用寄存器来传递参数,之后才是使用栈来传递。
栈是最通用的传递参数的方法。
如果使用gcc作为编译器,可以加上参数(-mno-accumulate-outgoing-args),来强制使用栈来传递参数。
当使用栈来传递参数时,参数压栈顺序为从右往左。
有如下示例代码:
#include <stdio.h> int max(int a, b){ int c=b; if(a>b){ c=a; } return c; } int main(){ max(3,5); return 0; }
max的汇编代码如下
push ebp
调用max时,汇编指令如下:
push 5 //参数5入栈 push 3 //参数3入栈 call max //跳转到max的地址,隐含动作是将eip入栈,即push eip add esp,0x8 //函数调用完毕,清理堆栈 //balabala
以下内容与溢出联系不大,可以略过。
关于对于第4步,是__cdecl函数调用方式,另外一种是__stdcall。
两者参数入栈顺序都是从右往左,区别在于谁来清理堆栈。
本例中main是调用者,max是被调用者
如果由main来完成清理堆栈动作,则称之为__cdecl方式,
如果由max来完成清理堆栈动作,则称之为__stdcall方式。
如果调用max的函数很多,采用__cdecl方式,清理堆栈的指令就会重复很多次,因此程序体积会变大。而采用__stdcall方式,清理堆栈的指令只在max中出现一次,程序体积相对较小。
但是__cdecl也有优点,就是适用于可变参数的函数,比如printf(format, …)。因为只有调用者才知道它到底传了多少个参数进去,被调用者是不知道的,所以必须要采用__cdecl方式,让调用者来清理堆栈。
3. 函数返回
溢出,就是改变函数返回地址。了解了函数是如何返回的,就很容易理解溢出的原理。
上面讲到,在调用函数max时,
call max指令会将eip入栈。
eip的值就是max的返回地址,即max函数完成后,通过ret指令将eip出栈,并且jmp到eip。
如果在max函数中,”不小心”修改到了栈上保存的eip的值,程序仍然从改动后的eip开始执行代码。
那么,如何才能”不小心”的改变这个值呢?
这就通过一些不安全的函数来做到,比如strcpy,strcat等。
说这些函数不安全,就是说这些函数对用户输入没有做严格的长度检查。
当用户输入比缓冲区的长度更大时。
4. 溢出危害
改变程序流程比如如下代码,当输入以est为结尾的并且长度为4的字符串时,验证通过。
然而,当输入长度为20的字符串,会覆盖掉flag的值。
此时,虽然没有进到
if(!strcmp(pwd,"test")里面去,但是flag的值却已经是非0值了。
因此,最终会输出right password。
#include <stdio.h> #include <string.h> int auth(const char* password){ int flag = 0; char pwd[16]; strcpy(pwd, password); pwd[0] = 't'; if(!strcmp(pwd, "test")){ flag = 1; } return flag; } int main(){ int flag = 0; char password[128]; scanf("%s", password); if(auth(password)){ printf("right password\n"); }else{ printf("wrong password\n"); } return 0; }
远程代码执行
5. 如何写shellcode
写C代码gdb反汇编
写汇编代码,编译连接成可执行文件
dump出二进制代码
6. 防-DEP和Canary
通过上面分析可以看到,缓冲区溢出造成代码执行有两大关键点修改了栈上保存的EIP的值
执行了栈上保存的shellcode
因此,针对第一点,GCC加入了一个对缓冲区溢出进行检测的机制,即在栈上保存一个随机值,函数结束时对该随机值进行验证,当验证不通过时,表明栈被修改,此时EIP的值已经不可信任,因此程序会退出。
针对第二点,人们提出了“栈不可执行”的防护措施,因为可执行代码应该在代码段,因此当EIP指向了栈段时,说明程序在执行非法代码。
7. 攻-Return to libc
针对“栈不可执行”的保护措施,黑客们又想出了一种return to libc的攻击方法。这种方法不执行栈上代码,甚至没有shellcode。仅仅是通过溢出修改栈上的值,覆盖EIP,并且在栈上填充好C语言库函数(比如system)的参数。溢出后程序便会跳转到库函数的位置,通过先前溢出到栈上的参数,来执行C语言的库函数。
整个过程没有执行栈上的代码,因为库函数是在代码段的。
设有代码如下,如何通过return to libc方式来攻击呢?
#include <stdio.h> void handlemsg(char* msg){ char buff[48]; strcpy(buff, msg); printf("\nthe input is [%s]\n\n", buff); } int main(int argc, char** argv){ handlemsg(argv[1]); return 0; }
如下所示,其中0xb7ea78b0是system的地址,0xbffff7a6是参数”/bin/bash”的地址,’ABCD’是
4000
system的返回地址,此处无意义。
如果将abcd设为exit的地址,那么当从溢出后的shell中退出来时,程序就跳到exit处,从而可以“优雅的”退出。
52=48+4,因此,后面的值0xb7ea78b0刚好可以写到eip的地方。
参数”/bin/bash”是从环境变量中得来的,在gdb中可以直接通过
x/s *(char**)environ查看,如果不用环境变量作为参数,而是把参数通过溢出写到栈上,那么就可以执行任意的命令。
./retlib `python -c "print 'A'*52+'\xb0\x78\xea\xb7' + 'ABCD' +'\xa6\xf7\xff\xbf/'"`
如果要更详细的解释,在这里。传送门
但是对于金丝雀的保护机制,该种攻击方法却无能为力。因为溢出时势必要修改金丝雀值,从而导致后面的验证几乎不会通过。而且GCC的这个编译选项默认是开启的,因此攻击成功的情景少了很多。
8. 防-ASLR
通过对Return to libc攻击方式研究可以发现,溢出成功的关键点在于找到了system函数的地址,而程序每次运行该函数的地址都不会变。因此,人们又提出了“地址空间布局随机化”的防护措施,当程序运行时,库文件的加载地址是随机的。这样使得攻击者很难确定库函数的地址,导致无法跳转到库函数。
9. 攻-Return oriented programming
看起来黑客们好像是无计可施了,但是又有牛人推出了一种”Return oriented programming”,中文是“返回导向编程”,通过在代码段中寻找可用的片段,然后在栈上构造返回地址,一步一步的跳转,最终执行完整个shellcode。这种方法暂时还没学会。。。。
10. 程序员能做的事
作为一个lowlevel程序员,在写关于缓冲区相关程序时,为防止出现缓冲区溢出漏洞,可以做到如下几点使用安全的函数,如strclpy,scnprintf等等
严格检查输入长度和缓冲区长度
测试并修改
相关文章推荐
- 缓冲区溢出分析第07课:MS06-040漏洞研究——静态分析
- 缓冲区溢出分析第08课:MS06-040漏洞研究——动态调试
- 缓冲区溢出分析第09课:MS06-040漏洞研究——深入挖掘
- 缓冲区溢出分析第10课:Winamp缓冲区溢出研究
- 互联网盈利模式研究(转)
- 关于Visual Studio 2010 简体中文版上Windows项目ClickOnce发布失败的研究
- 十三个经典算法研究与总结、目录+索引
- 由KIWI数据格式到MapInfo数据格式的转换研究
- 【人体识别系统研究】网摘
- [BZOJ1968] [Ahoi2005]COMMON 约数研究
- iOS GPUImage研究七:动态相册初探(水印)
- JavaScript中的prototype(原型)属性研究[转]
- web spider(网络爬虫)研究
- [转] 程序员面试、算法研究、编程艺术、红黑树4大系列集锦与总结
- oracle AWR深入研究分析,如何使用
- 用户研究(4)教你如何获得用户的芳心
- 我们能通过快速、开放的研究来战胜寨卡病毒吗?
- 深入研究Node.js的底层原理和高级使用
- User Interface Process(UIP) Application Block 2.0 研究总结
- HTML 中各浏览器对A标签中javascript的支持研究