基本栈溢出

ret2addr和ret2arg这两种利用手法在《黑手缓冲区溢出教程》里有所提及。这两种只是基本的利用手法,如果开启了NX(堆栈代码不可执行)或者ASLR就无用武之地了,需要更高级的利用手法,例如ret2libc,ret2plt,和ROP等高级利用手法,这篇笔记就只说下基本的利用手法及漏洞原理。

了解栈溢出漏洞,需要对汇编里的call指令(相当于push eip和jmp 函数首地址 ),ret指令(相当于pop eip),函数的调用过程有所了解。《加密与解密》的逆向分析技术篇中函数部分说的很清楚。下面也会有所介绍。

简介

栈溢出是向栈中写入超过原本长度限制的数据,使栈中的其他数据被覆盖,常见的是覆盖栈中返回地址,改变程序的执行流程。
栈溢出漏洞成立需要两个条件,其一是:有向栈中写入数据的行为,另一个是:使用了gets,strcpy,strcat等 不限制数据输入长度或者不检查数组长度的函数。

预备知识

函数的调用和返回等过程都是在栈中完成的,栈中也保存着局部变量和函数的参数。
说之前先复习下函数调用的知识。
调用函数前,如果函数有参数,需要先将参数传入栈中(值传递本质是将变量复制一份压入堆栈,而地址传递,是将变量的地址直接压入栈中,通过加上中括号[],直接访问)
一般情况下参数的入栈是从右往左依次入栈的(cdecl调用约定,stdcall,fastcall等)。
值传递模板(非fastcall调用约定,fastcall调用约定前两个参数会直接用寄存器,不用堆栈,后面的参数仍然用堆栈传参)如下:

1
2
3
4
5
mov eax,dword ptr [EBP-xxx]  //push 后无法直接接内存单元,需要先传给寄存器
push eax //压入堆栈传参
mov eaxdword ptr [xxx]
push eax
.....

传完参数后会call 函数(call 会将调用函数即母函数的call指令的下一条要执行的指令的地址压入堆栈,然后再跳转到函数代码段的首地址,这和cpu执行指令的过程有关,cpu执行指令的过程如下:1。读取EIP指向的指令,将其放入指令缓冲器,2。EIP指向下一条指令,3。执行指令缓冲器里的指令,然后返回 1。)

1
call 函数代码首地址

函数内容有个模板(下面为debug版,release版会有所不同)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
push ebp
mov ebp,esp
//这里提升堆栈
sub esp,0x40
//这里是开辟缓冲区,不同编译器开辟的缓冲区大小不同(会根据你所用的变量的多少和大小来开辟)。
push edi
push esi
push ebx
//保留现场
lea edi, dword ptr ss:[ebp-0x40]
mov ecx,0x10
mov eax,0xcccccccc
rep stos dowrd ptr es:[edi]
//填充缓冲区(清除垃圾数据),用于存放局部变量
----------------
这里是写函数的功能
----------------
pop ebx
pop esi
pop edi
//恢复现场
mov esp,ebp
pop ebp
ret

执行完call函数后的堆栈图如下(下面是高址,上面是低址):

此时,EBP的位置就很关键了,使用EBP+xxx,可以访问到传入栈中的参数,向上EBP-xxx,可以访问局部变量。
当函数的代码执行到

1
2
3
mov espebp
pop ebp
ret

此时ESP指向栈中的返回地址,执行ret执行(相当于pop eip)后就会将返回地址赋值给EIP,函数就执行完毕了,此时EIP重新指向母函数。

漏洞原理

漏洞的关键就是利用gets,strcpy,strcat 等函数,输入或者拼接超过字符数组原先规定的长度的字符串
例如原先定义的字符数组 a[8],你使用gets函数,输入了”AAAAAAAAAAAABBBB”,原先编译器编译成汇编时并没有预留足够的空间,例如前面函数模板中的 sub esp,0x40,他只开辟了0x40的空间,那么多输入的字符串就会将堆栈中的其他内容覆盖掉。
调用gets前的堆栈图如下:

调用gets,输入”AAAAAAAAAAAABBBB”后的堆栈图如下:

起始EBP值,和返回地址以及后面的堆栈空间都可以被输入的字符串所覆盖,这就是缓冲区溢出漏洞。

基本利用之ret2addr
ret2addr就是 return to address ,就是将堆栈里的返回地址覆盖为你所编写的shellcode的首地址上,ret2addr特指的是缓冲区里的shellcode。
利用缓冲区溢出后的堆栈图如下:

当ret后 ,EIP就会指向shellcode的首地址,这样就能执行你的shellcode了。如何找到shellcode的首地址,在下篇笔记再提及。

基本利用之ret2arg
与ret2addr不同的之处是 shellcode在返回地址的下面,而不是在栈帧里。同时返回地址被覆盖为JMP ESP这个指令的首地址。利用后的堆栈图如下:

原来的返回地址被覆盖为JMP ESP指令的首地址。
因为ret后 ,ESP加4,则此时ESP指向shellcode的首地址,而EIP指向了JMP ESP指令的首地址,执行JMP ESP后,EIP就指向了shellcode的首地址,这样就会执行你的shellcode了。