River5tone

关于gets()的危险(不安全)性

字数统计: 2k阅读时长: 7 min
2018/04/08 Share

//2018.3.22&2018.3.29&2018.4.6


DJ’s question:
s question]
3.23凌晨给DJ的简略回答:

每次看DJ的说说都能感受到自己有多菜嘤,但有所收获不管多小还是很开心hiahiahia

以下也算不上回答2333
(1)之前完全没有想过这个问题orz,完全想不到,于是去百度了下orz:”由于gets()无法知道字符串的大小,必须遇到换行字符或文件尾才会结束输入,因此容易造成缓存溢出的安全性问题。“ gets()函数使用指针不断读取直到遇到EOF或换行符,无限读取有溢出的可能。
(2)会覆盖某些内存;会使程序发生异常而崩溃;so,黑客利用这个达到某些目的,使对方程序崩溃
(3)保护的话,自己写一个gets函数,指定分配一定的内存空间,考虑越界问题;改使用fgets()或get_s()

DJ晚安

更新回答:

(1)&(2)gets()函数的不安全性及其原因:

gets()函数不检查目标数组是否能够容纳输入,分配空间不足会导致数组越界,会造成缓冲区溢出的问题,内存中原代码被覆盖等问题。缓存溢出有导致程序崩溃的可能性,或原有数据被恶意代码覆盖从而执行攻击者的指令。著名的“蠕虫”病毒的原理就是用很长的数据覆盖原有数据导致崩溃(?没有查到相关资料证明)。

由于数据在内存中是以高地址向低地址压栈的方式储存的,所以先于所存数组定义的数据有被覆盖改写的可能性,见下面例子

(3)针对其安全问题的保护:

  1. 尽量避免gets()函数这类不安全函数的使用

  2. 使用替代品:

    • fgets():

      会指定检测大小,但会将超出字符串的部分放入缓冲区,下一次调用时又会出现;

      不会将换行符转为空字符,而是保留换行符并添加空字符’\0’,这会意味着字符串比原应有大1。

      以下看不懂的东西:

      可是,虽然代码变复杂了,但是还是存在一个隐藏问题,该问题会导致程序崩溃,或者有安全隐患。当程序执行时,如果标准输入流已经得到了所有可用的字符,但是还没有遇到文件结束符( EOF), fgets() 函数将会通过将 input[0] 标记为 NULL 字符的形式,直接返回一个 NULL 字符串。此时, strlen(intput) 的返回值为0, 因此导致 last 指针指向 input 数组之前的那个字符。因为不能确定这个字符到底是什么,这段代码的行为将因此无法判断。
      • gets_s():

        原型如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        char *gets_s(char * str, int num)
        {
        if (fgets(str, int, stdin) != 0)
        {
        size_t len = strlen(str);
        if (len > 0 && buffer[len-1] == 'n')
        buffer[len-1] = '';
        return buffer;
        }
        return 0;
        }
    • 自己写一个函数

  3. 引自靳爷:

    (ii)栈随机化

    使得栈的位置在程序每次运行时都有变化。为了在系统插入攻击代码,攻击者不但要插入代码,还需要插入指向这段代码的指针(指向攻击代码的首地址/栈地址),这个指针也是攻击字符串的一部分。产生这个指针需要知道这个字符串放置的栈地址。老的系统版本,如果在相同的系统运行相同的程序,栈的位置是相当固定的。所以黑客可以在一台机器上研究透系统上的栈是如何分配地址的,就可以入侵其它主机。

    实现的方式:程序开始时,在栈上分配一段0~n字节之间的随机大小的空间。分配的范围n必须足够大,才能获得足够多样的栈地址变化,但是又要足够小,不至于浪费程序太多的空间。

    (iii)栈破坏检测(主要受GCC版本的限制,老的GCC版本不支持栈破坏检测):

    检测到栈何时被破坏。破坏通常发生在当超越局部缓冲区的边界区的时候。C语言中,有可靠的方法防止对数组的越界。但是,可以在发生越界的时候,并且,在其还没有造成任何有害成果之前,尝试检测到它,并且把程序终止。

    实现方法:金丝雀,加入一种栈保护机制。在栈帧中,紧接着局部缓冲区的位置放置一个哨兵(金丝雀),哨兵值是随机产生的,攻击者没有简单的方法能够知道它是什么。在恢复寄存器状态和函数返回之前,程序检查金丝雀的值是否发生了变化,如果变化立即终止程序。

    (iiii)限制可执行代码区域(主要受硬件版本的限制,需要硬件的支持)

    消除攻击者向系统插入可执行代码的能力,一种方法是:限制那些能够存放可执行代码的存储器区域。在典型的系统中,只有保存编译器产生的代码的那部分存储器才需要是可执行的,其它部分可以被限制为只允许读和写。

    一般的系统允许三种访问的形式:读(从存储器读数据)、写(存储数据到存储器)和执行(将存储器的内容看作是机器级代码)。以前,x86体系结构将读和执行访问控制合并为1位的标志,这样任何被标记为可读的页都是可执行的。栈又要求必须是既可以读又可以写的,所以x86体系结构栈上的字节都是可执行的。也有一些体制,能够限制一些页是可读但是不可执行,但是这些体制一般都会带来严重的性能损失。

    实现的方式:AMD为它的64位存储器的内容保护引入了“NX”(No-eXecute,不执行)位,将读和执行访问模式分开,intel也跟进了。从这开始,栈可以被标记为可读、可写,但是不可执行。检查页是否可执行由硬件来完成,效率上没有损失。

附加:

  • windows:程序崩溃,覆盖,如下

  • 引用俊鸿大佬的实践

关于栈帧:

计算机内存储数据是以高地址向低地址压栈的方式进行的。

缓冲区溢出:

缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,使得溢出的数据覆盖在合法数据上,通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,造成程序崩溃或使程序转而执行其它指令,以达到攻击的目的。

网上资料

例子(栗子):

Windows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main()
{
char a;
char name[3];
char b;
scanf("%c",&a);
printf("a:%c\n",a); //第一次输出a的值
getchar();
gets(name);
scanf("%c",&b);
printf("name:%s;\na:%c;\nb:%c;\n",name,a,b); //输出所有的值,注意a
}
gets()函数原型
1
2
3
4
5
char* gets(char* s)
{
...
return s;
}

gets()有两种可能的返回值类型:

  1. 当程序正常输入字符串时:返回读入字符串也就是其存放数组的首地址
  2. 当程序出现错误或者遇到文件结尾时:返回空指针NULL(注意区分空指针与空字符)
其他相关函数:

fgets()

gets_s()

CATALOG
  1. 1. DJ’s question:
  2. 2. 3.23凌晨给DJ的简略回答:
  3. 3. 更新回答:
  4. 4. 关于栈帧:
  5. 5. 缓冲区溢出:
  6. 6. 网上资料:
  7. 7. 例子(栗子):
  8. 8. gets()函数原型
  9. 9. 其他相关函数: