覆盖邻接变量的方法利用条件太过苛刻,需要源代码的结构符合漏洞利用才能实行。直接修改EBP或者函数返回地址的攻击则更为通用。
0x00 源码
由于键盘能够直接输入的字符ASCII范围有限,无法表达0x11
、0x12
等值,所以对代码稍作修改,通过读取文本文件输入。
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[8];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password); // Overflow
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);
}
使用Visual C++ 6.0编译,编译选项默认,Build版本为Debug。务必避开Visual Studio系列的GS编译选项。
0x01 分析
溢出工作包括:
- 了解栈中的情况,如函数地址距离缓冲区的偏移量等。虽然可以通过分析代码得到,但最好还是通过动态调试挖掘;
- 得到程序通过验证的指令地址,以便使程序直接跳转到这个分支;
- 在password.txt中的相应偏移处填写该地址。
首先反汇编得出验证通过分支的指令地址为0x00401122
,函数verify_password
在0x00401102
处被调用,在0x0040110A
处将EAX中函数返回的值取出,在0x0040110D
处与0比较,再决定跳转到哪个分支。
验证通过的分支从0x00401122
处的参数压栈开始,如果把返回地址覆盖成该地址,那么0x00401102
处的函数调用返回后,程序无论如何将跳转到验证通过的分支。
0x02 溢出
以4个字节为单位进行输入,为方便查看,每个单位都为“abcd”。
缓冲区buffer
容量为8个字节,需要2个单位填充。按照栈帧结构,向下为变量authenticated
,需要1个单位填充。再向下为前栈帧EBP,需要1个单位填充。再向下为函数返回地址,需要1个单位填充。
通过十六进制编辑方式制作password.txt:
注意:动态调试时显示的地址是经过转换而便于阅读的,其实际存储方式为小端存储。
之后通过OllyDbg调试运行该程序,最终的栈情况为:
局部变量名 | 内存地址 | 偏移3处的值 | 偏移2处的值 | 偏移1处的值 | 偏移0处的值 |
---|---|---|---|---|---|
buffer[0~3] | 0x0012FB14 |
0x61 ('a') |
0x62 ('b') |
0x63 ('c') |
0x64 ('d') |
buffer[4~7] | 0x0012FB18 |
0x61 ('a') |
0x62 ('b') |
0x63 ('c') |
0x64 ('d') |
authenticated(修改前) | 0x0012FB1C |
0x00 |
0x00 |
0x00 |
0x01 |
authenticated(修改后) | 0x0012FB1C |
0x61 ('a') |
0x62 ('b') |
0x63 ('c') |
0x64 ('d') |
前栈帧EBP(修改前) | 0x0012FB20 |
0x00 |
0x12 |
0xFF |
0x80 |
前栈帧EBP(修改后) | 0x0012FB20 |
0x61 ('a') |
0x62 ('b') |
0x63 ('c') |
0x64 ('d') |
返回地址(被覆盖前) | 0x0012FB24 |
0x00 |
0x40 |
0x11 |
0x07 |
返回地址(被覆盖后) | 0x0012FB24 |
0x00 |
0x40 |
0x11 |
0x22 |
执行情况:
由于EBP传入了无效值,程序崩溃,但成功到达验证通过的分支。