数据库安全漏洞浅析之缓冲区溢出漏洞(2)
作者:安华金和 发布时间:2017-01-25

前文中我们对数据库安全漏洞中最为常见的一种——缓冲区溢出漏洞进行了简单的概念介绍。在静态数据溢出、栈溢出和堆溢出三种类型中,栈溢出和堆溢出相对来说更复杂,且危害范围较大,在前文概念介绍的基础上,本文对windows下的栈结构进行更直观的介绍,通过构造一段代码示例,更清晰的演示栈结构下的缓冲区溢出漏洞原理。

下面我们将构造一段代码,完成3个任务:

1.演示WIN下栈的结构

2.演示缓冲区溢出改变函数控制流程

3.演示缓冲区溢出覆盖返回地址(劫持函数)

下面的程序包含一个主函数main和另外一个子函数re_choose。re_choose函数用于把从main函数中取得的输入字符串和存储的字符串liusicheng做对比。如果输入的字符串和存储的字符串一致则返回0。如果不一致则可能返回1或者-1。同时还人为制造了一个缓冲区溢出点strcpy(buffer,input)。input有1024的空间,而buffer只有44的空间。只要input超过44则就会引发缓冲区溢出。main函数取re_choose返回值如果返回1或-1走if。如果返回0则走else。将用缓冲区溢出来让返回1或者-1也走else。

#include <stdio.h>

#include <string.h>

#define ture_password "liusicheng"

int re_choose (char *input)

{

int result;

char buffer[44];

result = strcmp(input,ture_password);       

strcpy(buffer,input);                 //缓冲注入点

return result;

}

 void main()

{

int choose=0;        

char input[1024];

scanf("%s",input);

choose=re_choose(input);

if (choose == 1 || choose == -1)

{

printf("error\n");

else

{

printf("ture\n");

}

}

     

编译出上面代码的release版,放入IDA pro中得到反编译代码。下图是MAIN函数的流程结构。清楚的看到main函数的整个控制流程和main函数的栈从建立到销毁的全过程。栈主要用在函数调用上。进程调用的开始会调用大量系统函数,其中大量函数的地址是固定不变的(只和操作系统版本有关系),这些固定的函数将成为以后用于跳转的平台 。本文先不涉及这些函数。直接跳到main函数开始介绍。栈的结构是4个字节为一层。如果超过4个字节。按照4个整数倍存储。不足4个字节按照4个字节存储。栈的主要操作只有2种push和POP。push是把寄存器的内容压入到栈中,pop是把栈中的内容释放掉。ebp是当前栈帧的栈底,esp是当前栈帧的栈顶。(注意由于栈是顺序执行的所以同一时间只有一个栈顶和一个栈底。但栈底一般不是整个系统栈的栈底,而只是当前这个栈帧的栈底)。栈的结构采用先进先出,后进后出的原则。所以当创建一个栈的时候会遵循如下步骤:

1把上一个栈帧的栈底的指针压入当前栈保存起来(push ebp)。这一步其实是2步:第一步压入返回地址,第二步压入当上一个栈帧的ebp。

2把上一个栈帧的栈底移动到上一个栈帧的栈顶(mov ebp,esp)。从此这个栈的栈底就确定且不会发生任何改变。栈顶esp会一直发生变化。

3接着分配局部函数(sub esp,404h)。本程序中2个变量1个是4字节1个是1024字节。加一起正好是0x404个字节。需要栈顶上移0x404。注意栈的方向和内存相反。数据进入内存是从低地址向高地址写,而栈则是从高地址向低地址写。正是这种结构,给了后来数据改写之前数据的机会。栈顶的值会随着栈中数据随时进行调整。 


          20170125-1.jpg
注意:上图中var_404= -404h、str1= -400h

  同样栈撤销的时候基本可以按照栈建立的逆操作进行。首先把栈底值覆盖栈顶(mov esp,ebp)。接着栈中弹出当前ebp的值(pop ebp)。然后跳转EIP中存储的上个函数的返回地址(retn),回到前一个栈帧中(上一个函数中)删除返回地址行(add esp 4)。到此栈被完全撤销。至此一个栈从建立到撤销的全部过程已经完成。我们除了关心一个栈的创建和消亡,更关心的就是栈是如何传递返回值和参数的。下图是re_choose的反汇编图:清楚的解释了,在栈中是如何传参和返回值的。

20170125-3.png            

注上图中 var_4= -4 、str1 = 8

main函数从 call sub_401000这句开始,创建子函数re_choose的栈帧,开始也是和main一样的栈创建过程。直到执行到 mov eax,[ebp+str1],这句就是大家最关心的传参。在栈中固定不动的是栈底(ebp)。利用栈底为坐标向高位内存移动8个字节取值。取到存在main中的input。放入eax寄存器中传入re_choose用于计算。同样的机制看后半段 从re_choose中 (mov eax,[ebp+var_4])取栈底向低地址偏移4个字节的内容。存在eax中,main把eax值存入ebp-404(mov [ebp+var_404],eax)这个地址中用于后续的判断。至此栈的基本结构基本操作已经介绍完毕。栈缓冲区溢出的根源和栈的自身结构密切相关。正是由于栈中数据是先存入的在内存高地址,后入的在内存低地址。所以给了后入的机会,一旦超过栈原本分配的长度则会直接覆盖原先存在内存高地址中的数据或指令。从而带来不可预知的结果。

至于函数的参数传入的顺序是从左到右还是从右到左(局部变量int a,b 是先压a还是先压b),函数返回时恢复栈平衡是让母函数作还是子函数作。这部分和函数调用约定相关,主要的调用约定分为,_cdecl、_fastcall和_stdcall。一般VS默认采用 _stdcall和windows api保持一致。stdcall规则要求:参数从右向左压。(int a,b 先压b)。函数退出的时候自己清理栈中的参数。(图中经常会看到一个参数后面没用直接被add esp 4了)

至此,通过上面缓冲区溢出漏洞的实例演示,你应该已经了解了栈溢出的漏洞原理。后续,我们将基于此实例,向大家介绍利用栈溢出漏洞的攻击方式,以及随之带来的数据库安全风险。通过这样的实例演示,让大家对此类漏洞原理产生深刻理解,并梳理出防护思路。