如何特意制造栈缓冲区溢出?(x86 & ARM)

看了之后,觉得还是蛮有趣的,貌似老早以前的蠕虫病毒就是利用C语言的边界检查的的缺点(现在也存在啊,C语言的哲学思想:相信程序员),使客户端的栈缓冲区溢出,使客户的子程序返回时,返回到自己的病毒或者木马程序上去,而不是原来进去的地方。这在MCU编程里面叫“程序跑飞了”。

不过貌似蛮难发生的,那就构造一个,看看这发生的过程。这样以后如果发生了,心里有个谱。

我们知道,在操作系统中,每个线程都有自己的stack,当然,对RTOS来说,那就是任务。这里为了方便说明,仅仅以裸机程序,即相当于一个线程,整个程序只有一个stack。


先看看MSVC下面的过程:
 visual studio 2008
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>

static void my_func(void)
{
    printf("stack overflow success!");
}

void fun(int a, int b, int c) {
    char buffer1[5];
    int *ret;

    ret = ( int* )(buffer1 + 20);   //找到返回地址ret(ret is short for return)
    *ret = (int)my_func;            //跳过x = 1指令,即使ret返回的是我指定的地址程序
}

void main() {
    int x;

    x = 0;
    fun(1,2,3);
    x = 1;
    printf("%d\n",x);

    getchar();
}
这里的结果就是在fun函数退出的时候,不返回到“x=1”这条指令,而是去执行我指定的程序my_func。

并且因为没有进入此函数的入栈保存,故返回时发生如下错误。

先看看从主函数到进入子函数的过程中,哪些东西需要保存到stack中,以便子函数结束之后可以回到原来的轨道上来。直接上汇编代码看看就晓得了。
Step 1. 再进入子函数之前,先把函数的参数从后至前push到stack中

此时,stack如下所示:
             1     2     3
<------ [    ][    ][    ]
堆栈顶部           堆栈底部

Step 2. 然后去调用函数符号“_fun”,也就是41128Fh这个地址,这个地址上面是一条jmp指令,走你--->

此时,执行跳转指令jmp时,ret地址自动push到stack中,此时,stack如下所示:
            0x00416510 1     2     3
<------ [                ][    ][    ][    ]
堆栈顶部                堆栈底部

Step 3. 再将子函数的局部变量push到stack中(因为编译器就是那么做的),那么我们通过内存看看buffer1存在哪?

我们现在知道的是,返回的地址ret在buffer1之前被放入stack中的,但是具体多少呢?看代码应该可以看出来,这里可以通过直接看内存也可以。先看看buffer1在内存的哪个位置。buffer1的地址是0x0012FE6C,并且在其附近找到了1,2,3,以及0x00416510 ,这里就是stack的区域没错了。

此时的stack如下所示:
             others buffer1  others  0x00416510 1     2     3
<------ [          ][           ][            ][                ][    ][    ][    ]
堆栈顶部                                         ret地址                    堆栈底部
Step 4. 数数吧,差多少,20个!然后就是改变这个地址中的值。也就是下面那两条语句了。

本文木有讲清楚的地方:对x86的汇编不熟悉,step3的那一大段不了解(下面看看ARM的,这个还稍微了解点,而且二者的区别还是蛮大的);还有,为啥叫做栈溢出呢?感觉有点名不副实啊,这里只是通过指针干扰了原来应该是编译器该干的事而已。

再看看ARM MDK的过程有和不同:
代码与MSVC的类似,只需要将20改成12即可。
Step 1. 看看进入子函数之前做些什么?

ARM是直接将子函数的参数保存在寄存器r0,r1,r2中。
Step 2.而后,在进入子函数之后,再将寄存器r2,r3,r4和lr push到stack中。这里的r2-r4将会用来存储局部变量在操作过程中的使用。

比较下push {r2-r4,lr}(先压lr)之后,stack中发生的变化,其中0x20000730是buffer1的地址。

找到返回地址类似的值“0x080003E5”,而返回地址是“ 0x080003E4”,稍微有点不同,咋会多1了呢?原因可能如下:
“尽管PC的LSB总是0(因为代码至少是字对齐的),LR的LSB确实可读可写的。这是历史遗留的产物。在以前,由位0来指示ARM/Thumb状态。因为其他有些ARM处理器支持ARM和Thumb状态并存,为了方便汇编程序移植,CM3需要允许LSB可读可写“——《P28》
”然而,在分支时,无论是直接写PC的值还是使用分支指令,都必须保证加载到PC的数值是奇数(即LSB=1),用以表明这是在Thumb状态下执行。“——《P29》

看看此时寄存器的值,也是如此。

当然,PC值还是字对齐的,只是在PC进入分支的时候,LSB为1且保存在LR中,等待出来的时候返回,并且后面的实验表明,返回的PC也同样是字对齐的。

总结:对于如何管理函数调用时,出栈入栈的参数以及返回地址,没有统一的规定,交给编译器厂商来实现,而且实现方式也大不相同,比如ARM具有较多的通用寄存器(14个),所以ARM的默认函数调用的前4个参数由硬件帮你入栈,可以减少函数调用时的时间。但是我们可以通过看内容的内容和汇编代码可以哨探各个不同编译器的实现方式,我就看看么,O(∩_∩)O~~~

最后还有一个现象,就是在vs2008下回不来,回来的时候就会报错,ARM下可以回来,那是因为ARM的自动入栈保护,它会由硬件来自动保护R0-R3,LR,PSR这些个寄存器,所以可以返回。

参考资料:
 http://laixb.diandian.com/post/2012-09-07/40037152223 (这个链接的代码不可执行,可能是比较老了,编译器的实现变了)
《ARM Cortex-M3权威指南》

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页