前言
本篇旨在通过简单的例子来学习32位栈利用,随手散记。
一、函数栈帧
每一个函数独占自己的栈帧空间。当前正在运行的函数栈帧总是在栈顶。Win32 系统提供两个特殊的寄存器用于表示位于系统栈顶端的栈帧:ESP(栈指针寄存器)和EBP(基址指针寄存器)。
在函数栈帧中,一般包括以下几类重要信息:
(1)局部变量:为函数局部变量开辟的内存空间。
(2)栈帧状态值:保存前栈帧底部,用于在本栈帧被弹出后恢复上一个栈帧。
(3)函数返回地址:保存函数调用前的指令位置。
---------------- 0x00001234
/------- | ebp |
| ----------------
| | ret_addr |
| ----------------
| | data |
| ----------------
| | data |
| ----------------
\------>| ebp |-----\
---------------- |
| ret_addr | |
---------------- |
| data | |
---------------- |
| data | |
---------------- |
| ebp |<----/
----------------
| ret_addr |
---------------- 0x0000125c
函数调用时的系统栈结构如上所示。
二、修改邻接变量
1.原理
如上,我们知道了函数调用时栈中数据分布情况。函数的局部变量在栈中一个挨一个排列(上图中两data部分)。如果这些局部变量中有数组之类的缓冲区,并且程序中存在数组越界的缺陷,那么越界的数组元素就有可能破坏栈中相邻变量的值,甚至破坏战阵中的保存的EBP值、返回地址等重要数据。利用简单的实验验证上述可能。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
int verify_password(char* password)
{
int authenticated;
char buffer[8];
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password);
return authenticated;
}
int 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 verificaction!\n");
break;
}
}
return 0;
}
实验环境:
Win10 + VS2019 + IDA + x86dbg
代码分析:
通过的strcmp比较输入的密码与设置的密码,若两者相同则 authenticated = 0,反之authenticated = 1 。
实验目的:
通过修改临界变量来突破密码验证。为了达到这一目的,在子函数中在 authenticated 之后设置了数组buffer 。
2.测试
假如输入7个"q",按照字符串的序关系 "qqqqqqq" > "1234567",strcmp应返回1,既 authenticated = 1。x86dbg动态调试的实际内存情况如下:
(1)主函数:
(2)步入子函数verify_password:
(3)strcmp执行结束:
此时可以看出 "qqqqqqq" 和 "1234567" 经strcmp比较后为1,保存在eax中。
(4)authenticated变量在栈中被分配:
注:0x004115FA处: mov dword ptr ss:[ebp-4], eax
此时eax中存放的既是 authenticated = strcmp(password, PASSWORD) 的值。
栈中0x0019FA04既是authenticated在栈中的地址,也是我们要修改的邻接变量的地址。
(5)strcpy执行结束
此时eax中存放的地址0x0019F9FC,在栈中可以观察到如上所示,即为变量char buffer[8];
如此是否可以通过strcpy函数造成缓冲区溢出,使得缓冲区buffer的邻接变量authenticated被覆盖为0,从而突破密码验证?
字符串数据最后都有作为结束的NULL(0),当输入8个"q"时,按上述分析,buffer所拥有的8个字节将全部被 'q' 填满,而结尾的NULL将写入内存0x0019FA04。
三、修改返回地址
如上可以掌握函数栈帧中变量的位置。那么通过输入超长的字符串,从而覆盖函数返回地址便是有可能的。既然返回地址是手动覆盖的,那么我们所给出的返回地址从键盘不方便键入,改为从txt文件中读取。更改代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <string.h>
#include <iostream>
#define PASSWORD "1234567"
int verify_password(char* password)
{
int authenticated;
char buffer[8];
authenticated = strcmp(password, PASSWORD);
strcpy(buffer, password);
return authenticated;
}
int main()
{
int valid_flag = 0;
char password[1024];
FILE* fp;
if (!(fp = fopen("C:\\password.txt", "r+")))
{
printf("1");
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 verificaction!\n");
}
return 0;
}
c盘下txt文件内容如下:
在x32dbg中观察 verify_password 函数 strcpy执行后的栈帧情况:
思路:
(1)若以 "4321" 为一个单位,则第三个 "4321" 将覆盖掉 authenticated,第四个将覆盖掉前栈帧ebp,第五个则将覆盖掉返回地址;
(2)返回地址的选择问题,既然我们已经可以控制函数的执行流程,不妨在函数返回时直接跳转到验证成功的分支。
实验验证
在x32dbg中可以观察到,0x0041184c处为将输出的验证成功字符串压栈,0x00411851处为调用printf函数。则txt文件中第五个单元即为 "0x0041184c"。
运行程序结果如下:
由于栈内 EBP 等被覆盖为无效地址,从而导致程序在退出时堆栈无法平衡,导致崩溃。但我们成功地覆盖了返回地址并控制了执行流程。
结束
本篇复现《0day安全》中的简单实验,也是希望加深自己的理解。下一篇讨论x64栈帧以及x64缓冲区溢出:https://blog.csdn.net/weixin_41487541/article/details/119960765