tosser

        最近一个朋友学习信息安全方面的知识,然后发来一题和我一起讨论,虽然觉得简单,但是实际还是有点意思的,就拿出来一起看看。题目如下:

        从图中可以看到一段C语言的代码,还能看到3个问题。这里我把代码提出来,代码如下:

 1 #include <stdio.h>
 2 
 3 int main(int argc, char* argv[])
 4 {
 5     int apple;
 6 
 7     char buf[9];
 8 
 9     gets(buf);
10 
11     if (apple == 0x64636261) 
12     {
13         printf("hello world!");
14     }
15 
16     return 0;
17 }

我把问题也写出来,问题有三个:

(1)分析是哪种溢出类型

(2)给出题目的变量 apple 的地址,例如 0x0012ff44,给出 buf 各字符的地址

(3)a、b、c、d 的 ASCII 码值分别为0x61、0x62、0x63 和 0x64 ,给出 buf 输入方式,使得程序可以输出 hello world

 

什么是缓冲区

        简单说,缓冲区就是一块存放数据的内存区域。根据存放数据的内存的分配方式,可以把内存分为栈内存和堆内存。

        栈内存,用于存放局部变量、函数的参数等,对于函数调用时现场的保护,也会用到栈内存,比如保存函数的返回地址。栈内存,由 CPU 的来维护,在 32 位操作系统下,由 CPU 的 EBP 和 ESP 两个寄存器来维护。

        堆内存,是程序员通过特定的函数来申请的,比如 malloc 和 new 等函数。堆内存申请后由程序员来释放。而 栈内存 随着函数的返回 栈内存 也会被自动的回收。

 

什么是缓冲区溢出

        通常就是内存的覆盖,由于缓冲区分为 栈 和 堆,因此缓冲区溢出分为 栈溢出 和 堆溢出。因为 C/C++ 很多函数早期都不检查内存边界,所有的内存边界检查都由程序员自己去完成。这样就有可能因为疏忽造成缓冲区的溢出。而现在,大部分操作内存的函数,都在之前函数的基础上增加了安全检查,也就比以前安全了。

        有些安全书籍认为,避免缓冲区溢出,不要使用栈内存,而是去使用堆内存,这样的认识是错误的。因为堆内存的使用不当也会造成溢出,也是存在安全隐患的。

 

缓冲区溢出攻击

        缓冲区溢出攻击的本质是数据当作代码运行。在有存在缓冲区溢出攻击的程序中,攻击者将可执行的代码当作数据植入内存,再通过特定的方式使植入的数据运行,从而达到攻击的目的。

 

题目解析

        有了上面的铺垫,就来说说题目中的内容。

        第一题,上面的代码是哪种类型的溢出。在代码中可以看出,数组 buf[9] 是一块缓冲区,而 buf 是一个局部变量。局部变量是在栈中保存。代码中的 gets() 函数是接收用户输入的函数,但是它不对内存边界进行检查。buf[9] 的长度为 9 个字节,但是当使用 gets() 函数获取用户输入时,当超过 9 个字节时,也会全部接收。这样就造成了缓冲区溢出,更具体的说,就是栈溢出。这点是 C/C++ 语言的特点,数组越界是被允许的,因为在很多程序设计中,为了存储不定长数据,就会使用数组越界的方式。

 

        第二题,假如 apple 的内存地址是 0x0012ff44,那么给出 buf 中各个字符的地址。变量相当于给某个内存首地址起了一个名字,变量的类型限制了该变量的内存长度,比如 0x0012ff44 这个是一个内存的地址,给这个内存的地址起一个名字叫 apple,另外 变量 apple 的类型是 int,那么限制该变量的长度占用 4 个字节。

         第二题的题目,是给出我们 apple 的地址,然后让写出 buf 变量的地址。这里就又需要了解两个知识。首先,局部变量是在栈地址中这个是已知的,而栈地址的增长方向是由高到低的。第二,在 C 语言中,函数内部定义的局部变量,会按照变量定义的先后顺序来分配栈中的内存地址。那么,在代码中,先定义的 apple ,后定义的 buf 变量。那么,apple 的地址就比 buf 的地址要高(大、上),如图。

        知道上面两点以后,那么 buf 的地址到底是多少呢?还是先来说说 apple 实际占用的地址,apple 变量的地址是 0x0012ff44,这个地址其实是 apple 变量的首地址,因为 0x0012ff44 只代表一个字节的内存空间,而 apple 是 int 类型的变量,占用 4 个字节,那么 apple 实际占用的是 0x0012ff44、0x0012ff45、0x0012ff46 和 0x0012ff47 四个内存空间,也就是 4 个字节。而 apple 就是首地址就是 0x0012ff44。

        再说 buf 变量,buf 的定义为 char buf[9],则说明 buf 占 9 个字节,而 buf 在 apple 之后定义的,那么 buf 在栈内存中的地址一定是小于 apple 的地址的。那是不是只要用 apple 的地址减去 9 就是 buf 的地址呢?其实还不是。虽然 buf 占 9 个字节,但是在 32 位的 CPU 中,内存中的数据一般是按照 4 个字节对齐的(32 位刚好 4 个字节)。那么,也就是通过 0x0012ff44 - 0xC 就是 buf 的首地址。内存结构如下图。

        在上图中,标注为红色的部分,就是 buf 变量的内存,标注为绿色的部分,则是 apple 变量的内存。其中的白色内存,就是被用来对齐的内存。这样是不是浪费了内存。是的!在 32 位系统下,内存按 4 字节对齐,CPU 访问速度是最快的。因此,浪费 3 个字节去进行内存对齐,从而换取 CPU 读取的速度更快,是划得来的。在计算机算法中,经常提到两句话,“用空间换时间”和“用时间换空间”,这显然是“用空间换时间”的情况。从上面的图可以看出,buf 的起始地址是 0x0012ff38。

 

        第三题,是要让程序输出“hello world”这个字符串。但是从代码中来看,只有在 apple 等于 0x64636261 的时候,才会输出"hello world"字符串。而整个代码中就没有对 apple 进行赋值的代码。而且 0x64636261 又是什么?在第三题的题目中给出提示,0x61 代表小写字母 a 的 ASCII 码,0x62 代表小写字母 b 的 ASCII 码。那么,也就是说让 apple 中填充为字母 abcd 即可。看下图。

        只要我们在给 buf 通过 gets 赋值时,输入的内容超过 9 个字符,去覆盖其后面的内存即可。那么要输入多少个字符呢?buf 的长度是 9 个字节,对齐的字节是 3 个字节,apple 的长度是 4,那么一共输入 16 个字符即可,前 12 个随便输入,最后 4 个输入 abcd 即可。

        等等,代码中 apple == 0x64636261,看起来 apple 比较的是 dcba,但是为什么输入的是 abcd 呢?这个是字节顺序的问题,这里不展开讨论,只要了解了字节序的问题,就可以理解了,而字节序在开发网络程序和进行逆向分析时,也算是基础的基础。

 

演示

        这个程序,我使用 XP + VC6 来进行演示。为什么使用 VC6,因为在新版的 VS 中,已经没有 gets 函数了,因为它不安全,所以被丢弃了。

        把上面的代码录入 VC6 中,然后使用 DEBUG 进行编译(Release编译的话,生成的二进制会被优化,内存结构不明显,溢出的方式也不同,由于是试题,用最简单的方式表明问题即可)。

        编译后,在 gets() 的位置设置断点,然后打开“watch”窗口,来看一下 apple 和 buf 的内存地址,如下图。

        可以看出,apple 的地址是 0x0012ff7c,buf 的内存地址是 0x0012ff70。是不是有疑惑?跟题目中的地址不同!别急!相同的程序在不同的操作系统(比如,XP 和 Win7)上变量的内存地址是不同的,甚至在补丁不同的系统(XP SP2 和 XP SP3)上也可能是不同的。但是,我们注意两点,第一,apple 的地址比 buf 的地址大,第二,apple 的地址和 buf 的地址差 0xC。只要凭这两点来看,和我们前面分析的是相同的。

 

        接着打开“memory”窗口,来看内存,如下图。

        接着,在 if 的位置处下断点,然后让程序运行起来,我们就可以进行输入了,如下图。

        我这里输入了 12 个 1,因为前 12 个字符随便输入,然后输入了 abcd,输入完成后按下回车,我们在 if 位置处设置的断点被断住了,此时观察内存,如下图。

        从上图可以看到,在 0x0012ff7c 的位置处,也就是 apple 所在的栈空间中,被填充了0x61、0x62、0x63 和 0x64。虽然程序中没有任何位置给 apple 变量赋值,但是我们通过溢出的方式覆盖了 apple 的内存地址,成功的对它进行了赋值。让程序运行起来,观察程序的运行,如下图。

        可以看到,字符串“hello world”被输出了。

 

总结

        上面把整个题目分析了一下,没有难度,只是一些基础知识。这种题目有什么实际的意义呢?就拿这个题目的代码来举个例子,如果 gets 接收的是一串密码,只有在密码正确的情况下,才会执行特定的功能,而密码的对与否可能有一个标志位。那么及时不知道正确的密码,只要通过溢出去覆盖标志位是不是就可以执行特定的功能了?当然这只是一个简单的例子。对于缓冲区溢出、SQL 注入、XSS 等攻击,它们的问题都是检查不严格而导致 外部输入的数据被当作代码 执行了,从而产生安全的问题。因此,它们的本质是相同的。因此,对于程序员而言,就是不能相信任何的外部输入,一定要对外部输入做严格的检查。

 

相关文章: