【学习笔记Ⅰ】Chapter2 栈溢出原理与实践(实操)
0x10 栈溢出实践
0x11 写在前面
接基础知识之后,继续实操的部分。
书中分为以下三部分:
- 修改邻接变量
- 修改函数返回地址
- 代码植入
其实代码注入攻击属于非常早期的攻击手段,在Data Execution Prevention, DEP(又称“栈不可执行”技术)广泛应用之后就成为了历史。但谁让咱是小白呢,还是得从零体验缓冲区溢出攻击啊...
0x12 修改邻接变量
实验代码:
#include <stdio.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[8];// add local buff authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; char password[1024]; 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; } } }
是一段实现密码确认的代码,其中 char buffer[8] 变量为缓冲区溢出提供可能,以下是 verify_password 函数的栈帧布局,看到 buffer 正好与确认密码是否正确的 authenticated 变量相邻:
思路:
修改邻接变量,顾名思义就是冲刷掉相邻变量的内容,因此只要计算输入的 buffer,使其刚好可以覆盖或者修改所要绕过的值。
工具:
OllyDbg(看雪学院下载)
如何使用推荐B站视频(动态调试工具-OllyDbg教程)
栈帧分析:
通过OD的单步调试,找到verify_password 的入口地址和让栈状态发生变化的strcpy函数:
当输入密码为qqqqqqq时,栈帧数据分布如下:
在内存地址为 0x12FB20 处为变量 authenticated 的值,因此只要延长输入的密码长度,溢出并填充authenticated的值为0时,即可绕过验证(又由于字符串结尾都有NULL,因此直接输入任意八个字符)。
0x13 修改函数返回地址
作为修改邻接变量的继续,修改函数返回地址更近一步,由于多数时候我们无法锁定且也无法保证一定有 authenticated 这样一个值,因此,将数组溢出至 返回地址retn 或 EBP 更有价值。
结合实例,当输入qqqqqqq时 EBP 和 retn 数值如下:
思路:可以自然地想到,如果我们修改返回地址,使其绕到另一个验证成功的分支不就OK了!
准备工作:在OD中可找到验证成功的分支相应的内存地址位置。
为了输入更多的ASCII码,程序改为从文件中读取 password.txt:
#include <stdio.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[8]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; char password[1024]; 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); }
栈帧分析:
验证成功的分支其内存地址为 0x00401122,通过分析可按照如下表格构造password.txt中的数据。
变量 | 内存地址 | 数值 |
buffer[0-3] | 0x12FB18 | '1234' |
buffer[4-7] | 0x12FB1C | '1234' |
authenticated | 0x12FB20 | '1234' |
EBP | 0x12FB24 | '1234' |
retn | 0x12FB28 | 0x00401122 |
工具:
UltrEdit(看雪学院下载)
选择16进制修改,四组‘1234’后依次 逆序填入 22 11 40 00
补充!“内存数据”和‘数值数据’的区别:
- Win32系统在内存中由低位向高位存储一个4字节的DWORD;
- 但作为“数值”应用时,却是按照由高位向低位字节解释。
因此,要让内存正确解释这样一串数值 00 40 11 22,必须要按照逆序 22 11 40 00 存储。
结果(崩溃由EBP覆盖为无效值,导致栈帧无法平衡引起,但已经成功转到验证成功的分支):
0x14 代码植入
与修改返回地址不同,另一种方式是让程序执行我们想让程序执行的机器代码(已经有了shellcode的味道!),即第三个实验——代码植入。
环境:WinXP 32位 虚拟机,书写得比较早,但主要在于入门起步,还是装个虚机省力。
思路:通过调用的机器码,调用 Windows 动态链接库 user32.dll 中的API MessageBoxA,最终弹出消息框,并显示 Aha! 字样。
- 获取淹没返回地址的偏移
- 获取 buffer 的起始位置,用来冲刷返回地址
- 将可执行机器码填充至buffer顶部(password.txt),用来调用MFC的MessageBoxA
首先再次修改代码:
#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } main() { int valid_flag=0; char password[1024]; FILE * fp; LoadLibrary("user32.dll");//prepare for messagebox 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); }
三处变更:
- 增加头文件windows.h,以便程序能够顺利调用 LoadLibrary 函数去装载 user32.dll;
- verify_password 函数的局部变量 buffer 由 8 字节增加到 44 字节,这样做是为了有足够的空间来“承载”我们植入的代码;
- main 函数中增加了 LoadLibrary("user32.dll")用于初始化装载 user32.dll,以便在植入代码中调用 MessageBox。
栈帧分析:
OD调试获得淹没返回地址的偏移(方法同前文,只是 buffer 变为44字节),即需要淹没内存地址为0x12FB24上的内容,使其填充为 buffer 的起始地址 0x12FAF0。
下表比较直观:
变量 | 内存地址 | 数值 | 修改后 |
buffer[0-3] | 0x12FAF0 | 31323334 | 机器代码 |
... | ... | 31323334 | 机器代码 |
buffer[40-43] | 0x12FB18 | 31323334 | 机器代码 |
authenticated [44-47] | 0x12FB1C | 0x000000 | 机器代码 |
EBP [48-51] | 0x12FB20 | 0x12FF80 | 机器代码 |
retn [52-55] | 0x12FB24 | 0x401118 | 0x12FAF0 |
根据上述逻辑,淹没返回地址后会重新跳到 password.txt 的开头取指执行。因此,下一步是构造调用 MessageBox 的机器代码。
构造机器代码:
先照搬对MessageBox函数的解释:
思路:先用汇编代码调用 MessageBoxA ,再翻译成机器代码,用 UltraEdit 写入 password.txt
- 装载动态链接库 user32.dll,程序中已经实现
- 调用 MessageBoxA 的入口地址
- 在调用前从右向左压入4个参数
工具:VC6.0自带的“Dependency Walker”
打开任意一个带图形界面的MFC程序,获取 USER32.DLL 的基地址 0x77D10000 和 MessageBoxA 的偏移地址 0x000407EA ,得到 MessageBoxA 在内存中的入口地址 0x77D507EA(注意:由于采用的实验环境不同,地址必须要重新确定)。
最后一步!千难万难卡在了汇编上,确实没有接触过这些指令,可谓基础不牢地动山摇,不过一时半会这基础也补不上,占坑再说。
将上述汇编指令对应的机器代码按照上一节介绍的方法以十六进制形式逐字写入password.txt,第 53~56 字节填入 buffer 的起址 0x0012FAF0,其余的字节用 0x90(nop 指令)填充。(还没搞懂)
我所做的只是在16进制编辑器中替换掉函数的入口地址(狗头):
结果:本来想修改弹出的内容,不懂汇编没改成,还是先failwest吧...