在学习C语言中,使用得最多的输入输出便是scanf和printf,可惜在vs2017/2019等一些版本提示函数不安全:
严重性 代码 说明 项目 文件 行 禁止显示状态
错误 C4996 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
需要添加一句预编译命令:
#pragma warning(disable:4996)
为何scanf不安全?
C语言字符串以'\0'作为结束符,长度为n的字符串至少需要在长度为n+1的char数组中才能安全保存,C语言字符串引起的错误往往是缺少'\0'或长度超出数组范围。
若字符串长度>=数组长度
很多旧的库函数在对字符串进行操作时都不会检查字符串长度,例如:scanf,strcpy,strcat等。若字符串过长,这些函数往往并不会报错,也不会有任何提示,超出的部分会覆盖数组后面的内存,导致后面的变量被修改了,后面将会举一个这样的例子。
若字符串缺少'\0'结尾符
这种错误往往出现在对字符串进行处理的时候导致的,例如自己写函数来实现字符串复制的功能。这时字符串后面往往会多出一些乱七八糟的内容,例如"烫烫烫......"、"屯屯屯......",直到遇到保存0的字节。如果在实际开发中出现这个问题,有可能会将字符数组后面的重要内容给展示出来了,比如字符数组后面跟着保存密码的变量,结果在运营时出现这种问题把密码都输出出来了,这个项目估计就凉了。
黑客常用攻击方式之一——缓冲区溢出
此标题看起来高大上,其实原理很简单,就是字符串长度超出数组长度,导致后面的变量被覆盖了。这里举一个简单的例子:
#include <stdio.h>
#include <malloc.h>
#pragma warning(disable:4996)
#define SIZE 4
int main()
{
int gold = 10; //初始用户默认10金币
char s[SIZE]; //字符数组用于存放用户名
char* p1 = s, * p2 = (char*)&gold; //p1:数组s的首地址 p2:变量gold的首地址
printf("地址差%d\n", p2 - p1); //输出两个变量的地址差
printf("输入用户名:\n");
scanf("%s", s);
printf("%s,你有%d金币\n", s, gold);
}
/*
debug编译方式
SIZE 地址差
1 9
2 12
3 12
4 12
5 16
6 16
release编译方式
SIZE 地址差
1 -7
2 -4
3 -4
4 4
5 -4
6 -4
*/
在release编译方式下运行,如果地址差输出4,说明该例子能够达到效果。
为何必须要输出4?栈区是从高地址开始向下增长,数组首地址是数组中地址最低的那个字节的地址,当地址差为4时,int变量刚好在数组的后面(如果是debug方式的话两者中间会有额外内容,地址差大于4,如果修改了就会弹出错误提示,因此必须在release方式),而x86是小端模式,如果访问s[4],访问到的是变量gold的32位中的最低8位。至于其他的数组大小导致的奇怪的地址差,主要是编译器优化导致的,尤其是对齐优化,这里就不多介绍了。
如果看得混乱了,只需要知道:修改了s[4]~s[7]就是修改gold变量的内容。
这里字符数组很小,只能存下3个字符(还要一个字节要存'\0'),如果输入的字符串超过3个字符,gold变量就会被修改,
如果输入4个字节,比如abcd,输出的金币数量就是0(想一想为什么)。如果输入超过4个字节,比如abcde,输出的金币数量是101('e'的ASCII码是101)。因此这个贼简单的游戏已经被我黑了嘿嘿(只是输入了一个名字)。