栈的缓冲区溢出详解

本文介绍了一些栈的缓冲区原理和攻防手段。

1. C程序地址空间布局

先上一张老生常谈的图(来自《Unix环境高级编程》)。


地址空间布局

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 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的值,程序调用max返回之后,就会从改动后的eip开始执行代码。
那么,如何才能"不小心"的改变这个值呢?
这就通过一些不安全的函数来做到,比如strcpy,strcat等。说这些函数不安全,就是说这些函数对用户输入没有做严格的长度检查。当用户输入比缓冲区的长度更大时,会一直往高地址空间进行写入,就会覆盖掉函数调用的现场和返回地址。

4. 溢出危害

  1. 改变程序流程
    比如如下代码,当输入以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;
    }
    
  2. 远程代码执行
    参考上述代码示例,如果输入超过长度,并且输入的是一段汇编指令数据(称之为shellcode),以及经过精心计算的函数返回地址,就会导致auth函数在返回之后,直接跳到了输入的汇编指令开始处,然后就开始执行用户输入的任意汇编指令。
    可以看出来,此时CPU并不是在执行text段的指令,而是在执行栈上的指令,后面会提到针对这种行为的一个保护措施。
    由于函数返回地址刚好指向输入汇编指令的开始这个难度太高,实践中一般会在指令前面写入大量的nop指令,以降低对返回地址的精确度要求,只需要返回地址在nop的指令范围内即可,CPU会自然的跑到shellcode里面。

5. 如何写shellcode

写shellcode的过程分为如下几步:

  1. 写C代码
  2. gdb反汇编
  3. 写汇编代码,编译连接成可执行文件
  4. dump出二进制代码

详细的过程可以参考

  1. writing-your-own-shellcode
  2. Shellcoding for Linux and Windows Tutorial
  3. Shellcoding in Linux

6. 防-DEP和Canary

通过上面分析可以看到,缓冲区溢出造成代码执行有两大关键点

  1. 修改了栈上保存的EIP的值
  2. 执行了栈上保存的shellcode

因此,针对第一点,GCC加入了一个对缓冲区溢出进行检测的机制,即在栈上保存一个随机值(称之为canary),函数结束时对该随机值进行验证,当验证不通过时,表明栈被修改,此时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;
}

首先需要获得system()函数的地址,其次需要获取system()的参数地址,system()函数接收一个字符串作为参数。这里选择"/bin/bash"。参数"/bin/bash"是从环境变量中得来的,在gdb中可以直接通过x/s *(char**)environ查看,system()函数的地址也一样可以通过gdb获得。
攻击演示如下所示,其中0xb7ea78b0是system()函数的地址,0xbffff7a6是参数"/bin/bash"的地址,'ABCD'是system的返回地址,此处无意义。
52=48+4,因此,后面的值0xb7ea78b0刚好可以写到eip的地方。
如果不用"/bin/bash"作为参数,而是把参数通过溢出覆盖到栈上,那么就可以执行任意的命令。

  • 注:如果将'ABCD'设为exit的地址,那么当从溢出后的shell中退出来时,程序就跳到exit处,从而可以“优雅地"退出。
./retlib `python -c "print 'A'*52+'\xb0\x78\xea\xb7' + 'ABCD' +'\xa6\xf7\xff\xbf/'"`

如果要更详细的解释,在这里。传送门

对于canary的保护机制,该种攻击方法却无能为力。因为溢出时势必要修改canary值,从而导致后面的验证几乎不会通过。而且GCC的这个编译选项默认是开启的,因此攻击成功的情景少了很多。

8. 防-ASLR

通过对Return to libc攻击方式研究可以发现,溢出成功的关键点在于找到了system函数的地址,而程序每次运行该函数的地址都不会变。
因此,人们又提出了“地址空间布局随机化”的防护措施,当程序运行时,库文件的加载地址是随机的。这样使得攻击者很难确定库函数的地址,导致无法跳转到库函数。

9. 攻-Return oriented programming

看起来黑客们好像是无计可施了,但是又有牛人推出了一种"Return oriented programming"的方法,中文是“返回导向编程”,通过在代码段中寻找可用的片段(称之为gadgets),然后在栈上构造返回地址,一步一步的跳转,最终执行完整个shellcode。

10. 程序员的自我修养

作为一个lowlevel程序员,在写程序时,为防止出现缓冲区溢出漏洞,可以做到如下2点:

  1. 使用安全的函数,如strclpy,scnprintf等等
  2. 严格检查输入长度和缓冲区长度
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容