必考,大题,考核方式:代码,c++,为什么溢出,溢出的后果,溢出堆栈长什么样,overflow1,overflow2看懂复现,掌握到代码级别,格式化,串溢出、整数溢出,了解即可,堆溢出不涉及,看看缓冲区溢出如何防御,综合题,怎么攻击?输入什么?如何防御缓冲区溢出?有哪几种防御措施?
基础知识
进程的虚拟内存空间
栈帧空间
❤每个函数独自占用自己的栈帧空间,当前运行的函数的栈帧总在栈顶。
❤寄存器——标识当时运行函数的栈帧
ESP(栈指针寄存器):系统中正在运行函数的栈顶
EBP(基指针寄存器):系统中正在运行函数的栈底
❤当栈帧1对应的函数返回1时,栈帧1的空间被收回。
❤栈帧2变为当前运行函数的栈帧。
❤函数栈中,包含函数运行和函数返回的相关数据
·局部变量。
·栈帧状态信息:保存前一段函数栈帧的栈信 息。
·函数返回地址:函数返回主程序继续执行的指 令地址。
缓冲区溢出原理
通过往程序的缓冲区,写入超出其长度的内容,造成缓冲区的溢出,从而破坏程序的“堆栈”,使程序转而执行其他指令,以达到攻击的目的。
栈溢出
- 栈帧信息由EBP和ESP指针控制,但系统只有一组EBP和ESP指针,只能由正在执行函数的栈帧使用。
- 当被调函数执行完毕返回时,主调函数的ESP自然显露出来,因此不需要保存。
- 所以,编译器将主调函数的EBP存放在栈上。
c语言比较规则
C语言中字符串的比较通常使用
strcmp
函数,其比较规则基于字符串中各个字符的ASCII值。下面是strcmp
函数的基本工作原理:
逐字符比较:
strcmp
会逐个字符地比较两个字符串,从每个字符串的第一个字符开始。ASCII值比较: 比较是基于字符的ASCII值。在ASCII编码表中,每个字符都有一个对应的数值。
返回值:
- 如果第一个不匹配的字符在第一个字符串中的ASCII值小于第二个字符串中对应位置的字符的ASCII值,
strcmp
返回负值。- 如果两个字符串的所有字符都匹配(直到字符串结束符 '\0'),则返回0,表示字符串相等。
- 如果第一个不匹配的字符在第一个字符串中的ASCII值大于第二个字符串中对应位置的字符的ASCII值,
strcmp
返回正值。字符串结束符 '\0' 的作用: 字符串在C语言中以 '\0' 结束。如果一个字符串先达到 '\0',而另一个字符串在该位置的字符不是 '\0',则到达 '\0' 的字符串被认为是较小的。
overflow
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password(char *password)//验证密码函数
{
int authenticated;//定义认证变量
char buffer[8];//定义一个8字节缓冲区
authenticated=strcmp(password,PASSWORD);//比较输入密码与设定密码
strcpy(buffer,password);//将输入密码复制到缓冲区,可能导致溢出
return authenticated;//返回认证结果
}
main(){
int valid_flag=0;//定义验证标志
char password[1024];//定义一个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;//跳出循环
}
}
}
为什么会溢出:
- 'verify_password'中的'buffer'为8字节,'strcpy'在复制的时候没有检查passw的长度,如果超过7字节(加上空字符)会导致buffer溢出到authenticated的位置
输入什么会导致溢出 :
输入大于“1234567”的数会导致溢出,验证通过
输入小于“1234567”的数会导致栈溢出,验证不通过
溢出栈图:
详细解释
在下面这段代码中,password的长度是8,字符串会以“\0”结尾。
输入“1234567”时:
- 输入的字符串正好填满缓冲区(7个字符加上字符串结束符 '\0')。
- 因为输入与预设密码匹配,
strcmp
返回 0,表示认证通过。输入“12345678”时:
- 这个字符串长度超出了缓冲区的大小(8个字符加上 '\0' 结尾)。
- 根据 C 语言的字符串比较规则,"12345678" 确实大于 "1234567",因此
strcmp
返回一个正数(1)。- 但由于缓冲区溢出,字符串结束符 '\0' 被写入到了
authenticated
变量的内存空间,将它的最后一个字节修改为 0。这导致authenticated
的值变成了 0,错误地表示认证通过。输入“11111111”时:
- 同样超出缓冲区大小,但由于 "11111111" 小于 "1234567",
strcmp
返回一个负数。- 在 C 语言中,负数以补码形式表示,因此
authenticated
的值变为 0xFFFFFFFF。- 当缓冲区溢出发生,
authenticated
的最后一个字节变为 0,但它的值变为 0xFFFFFF00。由于这个值不为 0,表示认证失败。
字符串 "1234567" 和 "11111111" 的比较过程
ASCII值对比:
- 字符串 "1234567" 中的字符依次为 '1', '2', '3', '4', '5', '6', '7',它们对应的 ASCII 值分别是 49, 50, 51, 52, 53, 54, 55。
- 字符串 "11111111" 中的字符全都是 '1',对应的 ASCII 值是 49。
逐字符比较:
- 首先比较两个字符串的第一个字符。在这个例子中,两个字符串的第一个字符都是 '1'(ASCII值49),所以它们是相等的,比较继续。
- 接着比较第二个字符。"1234567" 的第二个字符是 '2'(ASCII值50),而 "11111111" 的第二个字符仍然是 '1'(ASCII值49)。因为 49 小于 50,所以
strcmp
会返回一个负值。结果解释
由于 "11111111" 中的第二个字符 '1' 的 ASCII 值小于 "1234567" 中的第二个字符 '2' 的 ASCII 值,
strcmp("1234567", "11111111")
将返回一个负值。这个负值表明第一个字符串在字典顺序上是大于第二个字符串的。在缓冲区溢出的上下文中,这个负值(通常是 -1,但具体值取决于
strcmp
的实现)会以补码形式存储在整数变量中。对于一个32位整数,-1的补码是 0xFFFFFFFF。如果后续发生缓冲区溢出,这个值可能会被部分覆盖,但只要最高有效位保持为1(即值为负),验证就会失败,因为 C 语言中任何非零值都被视为真(true)。
overflow2
#include <stdio.h>
#define PASSWORD "1234567"
int verify_password(char *password)
{
char authenticated;
char buffer[8];
authenticated = strcmp(password,PASSWORD);
strcpy(buffer,password);//会导致缓冲区溢出,没有检查password的长度
return authenticated;
}
main(){
int valid_flag=0;
char password[1024];
FILE *fp;//文件指针
if(!fp=fopen("password.txt","rw+")){
exit(0);
}//以读写模式打开一个名为"password.txt"的文件。如果文件不存在或无法打开,程序会调用'exit'函数立即退出。
fscanf(fp,"%s",password);//从文件中读取一个字符串到"password"缓冲区中,会导致溢出,没有检查文件中字符串的长度。
valid_flag = verify_password(password);
if(valid_flag){
printf("incorrect password!\n\n");
}else{
printf("Congratulation!You have passed the verfication!\n")
fclose(fp);//关闭文件夹
}
要攻击这段代码,攻击者可以在
password.txt
中输入一个超过buffer
数组长度的字符串。这个字符串需要精心构造,以便在内存中覆盖authenticated
变量的存储空间,造成认证绕过。
overflow3
#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);//缓冲区溢出
return authenticated;
}
main(){
int valid_flag=0;
char password[1024];
FILE *fp;//文件指针;
LoadLibrary("user32.dll")//调用'LoadLibrary'函数加载Windows系统库'user.dll'这常用于动态链接库的加载,但在上下文没有明显实验目的。
if(!(fp=fopen("password.txt","rw+"))){
exit(0);
}//尝试以读写模式打开名为"password.txt"的文件。如果文件打开失败,程序将退出。
fscanf(fp,"%s",password);//从文件中读取一个字符串到 password 缓冲区。这里存在潜在的安全风险,因为 fscanf 使用 %s 格式说明符时不会检查目的缓冲区大小,可能导致缓冲区溢出。
valid_flag = verify_password(password);
if(valid_flag){
printf("incorrect password!\n\n");
}else{
printf("Congratulation!You have passed the verification!\n");
flose(fp);
}
攻击这段代码的方法是在
password.txt
中放入超过buffer
的大小(44字节)的字符串。这个字符串必须足够长,以覆盖authenticated
变量,并可能还要覆盖返回地址或其他重要的栈结构。