栈溢出---《0day2安全》
# 基础知识## 二进制文件概述
### PE文件格式
PE(Portable Executable)是 Win32 平台下的可执行文件(如:"\*.exe","\*.dll"),PE文件规定了所有信息(二进制机器代码、字符串、菜单、图标、位图、字体等)在可执行文件中如何组织。
PE 文件格式把可执行文件分成若干个数据节(section):
- .text 二进制的机器代码
- .data 初始化的数据块
- .idata 动态链接库
- .rsrc 程序的资源
# 系统栈的工作原理
## 内存的不同用途
缓冲区溢出:大缓冲区向小缓冲区复制,撑爆了小缓冲区,从而冲掉了和小缓冲区相邻内存区域的其它数据而引起的内存问题。
进程使用的内存划分:
1.代码区
2.数据区
3.堆区
4.栈区
## 函数调用过程
同一文件不同函数的代码在内存代码区中是散乱无关的,但都在同一个 PE 文件的代码所映射的一个 “节” 里。
```c
intfunc_B(int arg_B1, int arg_B2)
{
int var_B1, var_B2;
var_B1=arg_B1+arg_B2;
var_B2=arg_B1-arg_B2;
return var_B1*var_B2;
}
intfunc_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1,arg_A2) + arg_A1;
return var_A;
}
int main(int argc, char **argv, char **envp)
{
int var_main;
var_main=func_A(4,3);
return var_main;
}
```
当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧的内存空间被它所属的函数独占。当函数返回时,系统栈会弹出该函数所对应的栈帧。
函数调用时,栈中的变化:
## 函数调用相关约定
如果要明确使用某一种调用约定,在函数前加上调用约定的声名即可。默认调用是__stdcall 调用方式,从右向左将参数入栈。
> 特例:C++类成员中的 this 指针,一般用 ECX 寄存器传递。用GCC编译器编译,他会作为最后一个参数压栈。
**函数调用步骤:**
1.参数入栈
2.返回地址入栈
3.代码区跳转
4.栈帧调整:
保存当前栈帧状态值,已备后面恢复本栈帧时使用( EBP 入栈);
将当前栈帧切换到新栈帧(将 ESP 值装入 EBP,更新栈帧底部);
给新栈帧分配空间(把 ESP 减去所需空间的大小,抬高栈顶);
__stdcall 调用约定,函数调用指令:
```asm
;调用前
push 参数 3 ;假设该函数有 3 个参数,将从右向左依次入栈
push 参数 2
push 参数 1
call 函数地址;call 指令将同时完成两项工作:
;a)向栈中压入当前指令在内存中的位置,即保存返回地址。
;b)跳转到所调用函数的入口地址函数入口处
push ebp ;保存旧栈帧的底部
mov ebp, esp ;设置新栈帧的底部(栈帧切换)
sub esp, xxx ;设置新栈帧的顶部(抬高栈顶,为新栈帧开辟空间)
```
**函数返回的步骤:**
1.保存返回值:通常保存在 EAX 中。
2.弹出当前栈帧,恢复上一个栈帧。
具体操作:
1.在堆栈平衡的基础上,给 ESP 加上栈帧的大小,降低栈顶,回收当前栈帧的空间
2.将当前栈帧底部保存的前栈帧 EBP 值弹入 EBP 寄存器,恢复出上一个栈帧。
3.将函数返回地址弹给 EIP 寄存器。
3.跳转
函数返回时,相关指令:
```asm
add esp, xxx ;降低栈顶,回收当前的栈帧
pop ebp;将上一个栈帧底部位置恢复到 ebp,
retn;这条指令有两个功能:
;a)弹出当前栈顶元素,即弹出栈帧中的返回地址。
;至此,栈帧恢复工作完成。
;b)让处理器跳转到弹出的返回地址,恢复调用前的代码区
```
# 修改邻接变量
## 修改邻接变量原理
函数的局部变量在栈中相邻排列。如果局部变量有数组之类的缓冲区,并且程序中存在数组越界缺陷,那么越界的数组就能破坏相邻变量,甚至能破坏 EBP 、返回地址。
```c
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer;// add local buffto be overflowed
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password;
while(1)
{
printf("please input password: ");
scanf("%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n\n");
}
else
{
printf("Congratulation! You have passed the
verification!\n");
break;
}
}
}
```
当程序执行到 int verify_password(char *password)时,栈帧状态如下图:
**改变程序流程思路:**
可以发现,authenticated 变量来源于 strcmp 函数的返回值,它被返回给main函数作为验证标志。当 authenticated 为 0 时,标识验证成功;反之,验证不成功。
当我们输入超过 7 个字符的密码(注意:字符截断符 NULL 将占用一个字节),就有机会把 authenticated 覆盖为 0,从而绕过密码验证。
### 突破密码验证程序
| | 推荐使用的环境 | 备 注 |
| ---------- | -------------- | --------------------------------------------------- |
| 操作系统 | Windows XP SP3 | 其他 Win32 操作系统也可进行本实验 |
| 编译器 | Visual C++ 6.0 | 如使用其他编译器,需重新调试 |
| 编译选项 | 默认编译选项 | VS2003 和 VS2005 中的 GS 编译选项会使栈溢出实验失败 |
| build 版本 | debug 版本 | 如使用 release 版本,则需要重新调试 |
> 说明: 如果完全采用实验指导所推荐的实验环境,将精确地重现指导中所有的细节;否则需要根据具体情况重新调试。
(1)先验证一下正确密码,输入“1234567”,通过验证,结果如下图所示:
(2)再来分析一下具体覆盖时,栈中的情况,输入“qqqqqqq”,因为“qqqqqqq”>“1234567”,所以 strcmp 应该返回 1,即 authenticated 为 1。
| 局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 处的值 | 偏移 1 处的值 | 偏移 0 处的值 |
| ------------- | ---------- | ------------- | ------------- | ------------- | ------------- |
| buffer| 0x0012FB18 | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| buffer| 0x0012FB1C | NULL | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| authenticated | 0x0012FB20 | 0x00 | 0x00 | 0x00 | 0x01 |
> 观察内存时,注意 “内存数据” 与 “数值数据” 的区别。Win32 系统在内存中由低位向高位存储一个 4 字节的双字(DWORD),但在作为 ”数值“ 应用的时候,却是按照由高位字节向低位字节进行解释。“内存数据” 中的 DWORD 和我们逻辑上使用的 “数值数据” 是按字节序逆序过的。
(3)输入超过 7 个字符,“qqqqqqqqrst”,结果如下图:
| 局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 处的值 | 偏移 1 处的值 | 偏移 0 处的值 |
| ---------------------- | ---------- | ------------- | ------------- | ------------- | ------------- |
| buffer | 0x0012FB18 | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| | 0x0012FB1C | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| authenticated 被覆盖前 | 0x0012FB20 | 0x00 | 0x00 | 0x00 | 0x01 |
| authenticated 被覆盖后 | 0x0012FB20 | NULL | 0x74 (‘t’) | 0x73 (‘s’) | 0x72(‘r’) |
我们已经知道,通过溢出 buffer 我们能修改 authenticated 的值,若要改变程序流程,就需要把 authenticated 覆盖为 0,而我们的字符截断符 NULL,就刚好能实现,当我们输入 8 个 ‘q' 时,buffer所拥有的 8 个字节将全部被 ’q‘ 填充,而 NULL 则刚好写入内存 0x0012FB20 出,即下一个双字的低位字节,恰好能把 authenticated 从 0x 00 00 00 01 改成 0x 00 00 00 00,如下图所示:
| 局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 处的值 | 偏移 1 处的值 | 偏移 0 处的值 |
| ---------------------- | ---------- | ------------- | ------------- | ------------- | ------------- |
| buffer | 0x0012FB18 | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| | 0x0012FB1C | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| authenticated 被覆盖前 | 0x0012FB20 | | | | 0x01 |
| authenticated 被覆盖后 | 0x0012FB20 | | | | 0x00 (NULL) |
经上述分析,我们只要输入 8 个**(大于 ”1234567“)** 字符的字符串,那么最后的 NULL 就能将 authenticated 低字节中的 1 覆盖为 0,从而绕过验证程序。
> authenticated = strcmp( password, PASSWORD ),
> 当输入的字符串大于 ”1234567“时,返回1(0x 00 00 00 01),这时可以用NULL 淹没 authenticated 的低位字节从而突破验证;
> 当输入的字符串小于 ”1234567“时,返回 -1(0x FF FF FF FF),这时如果任然用上述方法淹没,其值变为 0xFF FF FF 00,所以这时是不能冲破验证程序的。
# 修改函数返回地址
## 返回地址与程序流程
更改邻接变量对环境要求很苛刻。而更改 EBP 和函数返回地址,往往更通用,更强大。
上节实验输入 7 个 “q“ ,程序栈状态:
| 局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 处的值 | 偏移 1 处的值 | 偏移 0 处的值 |
| ------------- | ---------- | ------------- | ------------- | ------------- | ------------- |
| buffer | 0x0012FB18 | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| | 0x0012FB1C | NULL | 0x71 (‘q’) | 0x71 (‘q’) | 0x71 (‘q’) |
| authenticated | 0x0012FB20 | 0x00 | 0x00 | 0x00 | 0x01 |
| 前栈帧 EBP | 0x0012FB24 | 0x00 | 0x12 | 0xFF | 0x80 |
| 返回地址 | 0x0012FB28 | 0x00 | 0x40 | 0x10 | 0xEB |
如果继续增加输入的字符,我们就能让字符串中相应位置字符的 ASCII 码覆盖掉这些栈帧状态值。
这里用 19 个字符作为输入,看看淹没返回地址会对程序产生什么影响。出于双字对齐的目的,我们输入的字符串按照 “ 4321 ” 为一个单元进行组织,最后输入的字符串为“ 4321432143214321432”。
| 局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 字节 | 偏移 1 字节 | 偏移 0 字节 |
| ------------------------- | ---------- | ------------- | ----------- | ----------- | ----------- |
| buffer | 0x0012FB18 | 0x31 (‘1’) | 0x32 (‘2’)| 0x33 (‘3’)| 0x34 (‘4’)|
| buffer | 0x0012FBIC | 0x31 (‘1’) | 0x32 (‘2’)| 0x33 (‘3’)| 0x34 (‘4’)|
| authenticated(被覆盖前) | 0x0012FB20 | 0x00 | 0x00 | 0x00 | 0x01 |
| authenticated(被覆盖后) | 0x0012FB20 | 0x31 (‘1’) | 0x32 (‘2’)| 0x33 (‘3’)| 0x34 (‘4’)|
| 前栈帧 EBP(被覆盖前) | 0x0012FB24 | 0x00 | 0x12 | 0xFF | 0x80 |
| 前栈帧 EBP(被覆盖后) | 0x0012FB24 | 0x31 (‘1’) | 0x32 (‘2’)| 0x33 (‘3’)| 0x34 (‘4’)|
| 返回地址(被覆盖前) | 0x0012FB28 | 0x00 | 0x40 | 0x10 | 0xEB |
| 返回地址(被覆盖后) | 0x0012FB28 | 0x00(NULL) | 0x32 (‘2’)| 0x33 (‘3’)| 0x34 (‘4’)|
返回地址用于在当前函数返回时重定向程序的代码。在函数返回的“ retn” 指令执行时,栈顶元素恰好是这个返回地址。“retn”指令会把这个返回地址弹入 EIP 寄存器,之后跳转到这个地址去执行。
返回地址本来是 0x004010EB,对应的是 main 函数代码区的指令,现在我们通过溢出 buff 覆盖返回地址为 0x00323334,函数返回时,将 0x00323334 装入 EIP 寄存器,从内存 0x00323334 处取址,由于此处没有合法指令,处理器不知如何处理,报错。
但如果这里是一个有效的指令地址,就能让处理器跳转到任意指令区去执行,我们可以通过淹没返回地址而控制程序的执行流程。
## 控制程序的执行流程
用键盘输入字符的 ASCII 表示范围有限,很多值(如 0x11、 0x12 等符号)无法直接用键盘输入,所以我们将程序的输入由键盘改为**从文件中读取字符串**。
```c
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer;
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main()
{
int valid_flag=0;
char password;
FILE * fp;
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
```
程序的基本逻辑和上一节中的代码大体相同,只是现在将从同目录下的 password.txt 文件中读取字符串。
| | 推荐使用的环境 | 备 注 |
| ---------- | -------------- | --------------------------------------------------- |
| 操作系统 | Windows XP SP3 | 其他 Win32 操作系统也可进行本实验 |
| 编译器 | Visual C++ 6.0 | 如使用其他编译器,需重新调试 |
| 编译选项 | 默认编译选项 | VS2003 和 VS2005 中的 GS 编译选项会使栈溢出实验失败 |
| build 版本 | debug 版本 | 如使用 release 版本,则需要重新调试 |
> 用 VC6.0 将上述代码编译链接(使用默认编译选项, Build 成 debug 版本),在与 PE 文件同目录下建立 password.txt 并写入测试用的密码之后,就可以用 OllyDbg 加载调试了。
动态调试时,需要我们做的工作:
(1)摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。
(2)得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行。
(3)在 password.txt 文件的相应偏移处填上这个地址。
这样 verify_password 函数返回后就能直接跳转到验证通过的分支执行了。
用OllyDbg 加载 可执行文件,【找到验证的程序分支的指令地址为】按G调出程序执行的流程图,分析一下程序执行流程。
从上面的流程图中,可以发现,在`401111`处的指令进行了程序验证。
`0x00401102` 调用了 verify_password 函数,之后在 `0x0040110A` 处将EAX中的返回值取出,在 `0x0040110D`处与0比较,然后决定跳转到提示验证通过的分支或是提示验证失败的分支。
提示验证通过的分支从 `0x00401122`处的参数压栈开始。如果我们把返回地址覆盖成这个地址,那么在 `0x00401102`处的函数调用返回后,程序将跳转到验证通过的分支,而不是进入分支判断代码。
通过动态调试,发现栈帧中的变量分布情况基本没变。这样我们按如下方法构造 password.txt 中的数据。
构造思路:用2个 “4321”来填充 buffer,第3个“4321”来覆盖 authenticated,第4个“4321”覆盖前栈帧 EBP,第5个“4321” 的 ASCII码值 0x34333231 修改成验证通过分支的指令地址 0x00401122。
在构造 password.txt 时,我们需要用到一个软件 Ultraedit,通过它来编辑十六进制。
构造步骤:
1. 创建一个 password.txt文件,写入5个“4321”,放在实验程序的目录中。
2. 用 Ultraedit32 打开 password.txt
3. 切换至十六进制编辑模式。
4. 将最后4个字节修改为新的返回地址 0x00401122,注意:由于“大顶端”,我们需要逆序输入这4个字节
将 password.txt 保存后,用 OllyDbg 加载程序并调试,可以看到最终的栈状态如表所示。
| 局部变量名 | 内存地址 | 偏移 3 处的值 | 偏移 2 处的值 | 偏移 1 处的值 | 偏移 0 处的值 |
| ------------------------- | ---------- | ------------- | ------------- | ------------- | ------------- |
| buffer | 0x0012FB14 | 0x31 (‘1’) | 0x32 (‘2’) | 0x33 (‘3’) | 0x34 (‘4’) |
| buffer | 0x0012FB18 | 0x31 (‘1’) | 0x32 (‘2’) | 0x33 (‘3’) | 0x34 (‘4’) |
| authenticated(被覆盖前) | 0x0012FB1C | 0x00 | 0x00 | 0x00 | 0x01 |
| authenticated(被覆盖后) | 0x0012FB1C | 0x31 (‘1’) | 0x32 (‘2’) | 0x33 (‘3’) | 0x34 (‘4’) |
| 前栈帧 EBP(被覆盖前) | 0x0012FB20 | 0x00 | 0x12 | 0xFF | 0x80 |
| 前栈帧 EBP(被覆盖后) | 0x0012FB20 | 0x31 (‘1’) | 0x32 (‘2’) | 0x33 (‘3’) | 0x34 (‘4’) |
| 返回地址(被覆盖前) | 0x0012FB24 | 0x00 | 0x40 | 0x11 | 0x07 |
| 返回地址(被覆盖后) | 0x0012FB24 | 0x00 | 0x40 | 0x11 | 0x22 |
程序执行状态如下图所示。
由于站内EBP被覆盖为无效值,使得程序在退出时堆栈无法平衡,导致崩溃。
跟着大佬一起学习 牛逼牛逼! 看不懂啊 虽然目前看不懂,但是还是要谢谢大佬的付出 学习了,感谢大佬 写的好,有时间我也试下 大佬,学习了 谢谢 正好学习一下 感谢楼主分享