跳转至

内存越界引用和缓冲区溢出

在C语言中,对于数组引用不进行任何的边界检查,而且局部变量和状态信息都存放在栈中。这两种情况结合到一起就会导致严重的程序错误,对于越界的数组元素的写操作会破坏存储在栈中的状态信息。当程序使用这个被破坏的状态了,试图重新加载寄存器或执行ret指令时,就会出现严重的错误

缓冲区溢出

  • 通常,在栈中分配某个字符数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间
    /*Implementation of library function gets() */
    char *gets(char *s){
        int c;
        char *dest = s;
        while((c = getchar()) != '\n' && c != EOF)
            *dest++ = c;
        if(c == EOF && dest == s)
            return NULL;
        *dest ++ = '\0';
        return s;
    }
    
    /*Read input line and write it back*/
    void echo(){
        char buf[8];
        gets(buf);
        puts(buf);
    }
    
  • gets函数的问题:无法确定是否为保存整个字符串分配了足够的空间,可能导致缓冲区溢出
    //echo产生的汇编代码(需要使用命令行选项`-fno-stack-protector`)
    echo: 
    subq $24, %rsp          //Allocate 24 bytes on stack
    movq %rsp, %rdi         //Compute buf as %rsp
    call gets               //Call gets
    movq %rsp, %rdi         //Compute buf as %rsp
    call puts               //Call puts
    addq $24, %rsp          //Deallocate stack space
    ret                     //Return
    
  • echo的栈结构:
  • 根据输入的字符长度,栈被破坏的情况: | 输入的字符数量 | 附加的被破坏的状态 | | -------------- | ------------------ | | 0~7 | 无 | | 9~23 | 未被使用的栈空间 | | 24~31 | 返回地址 | | 32+ | caller中保存的状态 |
  • 字符串到23个字符之前都没有严重的后果,但是超过以后,返回指针的值以及更多可能的保存状态会被破坏

    • 如果存储返回地址的值被破坏了,那么ret指令会导致程序跳转到一个完全意想不到的位置

      如果只看C代码,是不会发现这个问题的,只有通过研究机器代码级别的程序才能理解gets这样函数的进行的内存越界写的影响

  • 缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数,这是一种最常见的通过计算机网络共计系统安全的方法

    • 通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为攻击代码
    • 一些字节会用一个只想共计代码的指针覆盖返回地址,执行ret指令的时候就回跳转到攻击代码上

对抗缓冲区溢出攻击

栈随机化

[!引入] 为了在系统中插入攻击代码,攻击者需要插入代码,也需要插入指向这段攻击代码的指针,这个指针也是攻击字符串的一部分。 产生这个指针需要知道这个字符串放置的栈地址 - 安全单一化: - 在过去,程序的栈地址非常容易预测: - 对于所有运行同样程序和操作系统版本的系统来说,在不同的机器之间,栈的位置是固定的 - 如果攻击者可以确定一个常见的Web服务器所使用的的栈空间,就可以设计一个在许多机器上都能实施的攻击 - 类似于传染病

  • 栈随机化:使栈的位置在程序每次运行时都有变化
    • 机器运行同一段代码的时候,其栈地址不同
  • 实现的方法:
    • 在程序开始时,在栈上分配一段0~n字节之间的随机大小的空间
    • 程序不使用这段空间,但是它会导致程序每次执行后续的栈位置发生变化
    • 分配的范围n必须足够大,才能获得足够多的栈地址变化
    • 但是又要足够小,不至于浪费太多的空间
  • 一种寻找栈地址的方法:
    int main(){
        long local;
        printf("local at %p\n", &local);
        return 0;
    }
    

[!地址空间布局随机化Address-Space Layout Randomization] 采用ASLR,每次运行时程序的不同部分,包括程序代码、库代码、栈、全局变量和堆数据,都会被加载到内存的不同区域。这就意味着在一台机器上运行一个程序,与在其他机器上运行同样的程序,他们的地址映射大相径庭。这样能够对抗一些形式的攻击

  • 暴力破解:反复的用不同的地址进行攻击

    [!空操作雪橇nop sled] 一种常见的把戏就是在实际的攻击代码之前插入很长的nop指令。执行这种指令除了对程序计数器加1,使之只想下一条指令之外,没有任何效果。只要攻击者能够猜中这段序列中的某个地址,程序就回经过这个序列到达攻击代码。这个序列常用的术语是:空操作雪橇,意思是程序会“滑过“这个序列。 如果我们建立一个256字节的nop sled,那么枚举\(2^{15} = 32768\)个起始地址,就能破解\(n=2^{23}\)随机化

栈破坏检测

[!引入] 计算机的第二道防线是能够检测到何时栈已经被破坏。我们在echo函数实例中看到,破坏尝尝发生在当超越局部缓冲区的边界时。在C语言中,没有可靠的方法来防止对数组的越界写。但是,我们能够在发生了越界写的时候,在造成任何有害结果之前,尝试检测到它。 - 栈保护者机制: - 思想:在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值 [!金丝雀值] 金丝雀值,也被称为哨兵值,是在程序每次运行时随机产生的,因此,攻击者没有简单的办法能够知道他是什么。 在回复寄存器状态和从函数返回之前,程序检查这个金丝雀值是否被该函数的某个操作或者该函数调用的某个操作改变了。如果是,则程序异常终止。

  • 包含金丝雀值的代码
    • 指令参数%fs:40致命金丝雀值使用段寻址的方式从内存中读入的
    • 将金丝雀值存放在一个特殊的断中,标志为“只读”,这样攻击者就不能覆盖存储的金丝雀值
    • 在回复寄存器状态和返回之前,函数将存储在栈位置出的值与金丝雀值作比较
      • 相同:函数正常完成
      • 不相同:调用一个错误处理的例程

限制可执行代码区域

  • 消除攻击者向系统中插入可执行代码的能力
    • 限制哪些内存区域能够存放代码
      • 在典型的程序中,只有保存编译器产生的代码的那部分内存才需要是可执行的
      • 其余部分可以被限制为只读写