3 对抗措施

3.1 应用安全工程原则

  • 使用强类型语言,例如 Java、C#,以及其他。使用这些语言,可以避免缓冲区溢出。

  • 使用安全的库函数

    • 可能拥有缓冲区溢出问题的函数:getsstrcpystrcatsprintf
    • 这些函数更加安全:fgetsstrncpystrncat以及snprintf

3.2 系统化代码修改

  • StackShield:分离控制(返回地址)和数据。

    • 它是保护返回地址的 GCC 编译器扩展。
    • 当函数调用时,StackShield 将返回地址复制到不能覆盖的区域。
    • 从函数返回时,返回地址被存储。因此,即使栈上的返回地址发生改变,也没有效果,因为原始的返回地址在返回地址用于跳转之前复制了回来。
  • StackGuard:标记缓冲区的边界

    • 观察:一个人需要覆盖返回地址之前的内存,来覆盖返回地址。换句话说,攻击者很难治修改返回地址,而不修改返回地址之前的栈内存。
    • 无论函数什么时候调用,都可以将一个哨兵值放在返回地址的旁边。
    • 如果函数返回值,哨兵值发生改变,就代表发生了缓冲区溢出。
    • StackGuard 也内建于 GCC。
    • 我们可以理解 StackGuard 如何工作,通过下面的程序(我们模拟了编译器,手动将保护代码添加到函数中)。处于明显的原因,我们在这个例子中使用整数作为哨兵值,它还不够强大。我们可以使用多个整数作为哨兵值。
    /* This program has a buffer overflow vulnerability. */
    /* However, it is protected by StackGuard */ #include <stdlib.h> #include <stdio.h> #include <string.h>int func (char *str) {int canaryWord = secret; char buffer[12];/* The following statement has a buffer overflow problem */ strcpy(buffer, str);if (canaryWord == secret) // Return address is not modified return 1; else { // Return address is potentially modified ... error handling ... }
    }
    static int secret; // a global variable
    int main(int argc, char **argv) { // getRandomNumber will return a random number secret = getRandomNumber();char str[517]; FILE *badfile;badfile = fopen("badfile", "r"); fread(str, sizeof(char), 517, badfile); func (str); printf("Returned Properly\n"); return 1;
    }

3.3 操作系统方法

  • 地址空间随机化(ASLR):猜测恶意代码的地址空间是一个缓冲区溢出的关键步骤。如果我们可以使恶意代码的地址难以预测,攻击就能变得更困难。多种 Linux 发行版都已经使用了 ASLR 来随机化堆和栈的起始地址。这使得猜测准确地址变得困难。下面的命令(只能由 Root 运行)开启或禁用 ASLR:

    
    # sysctl -w kernel.randomize_va_space=2 // Enable Randomization # sysctl -w kernel.randomize_va_space=0 // Disable Randomization
    

    不幸的是,在 32 位机器上,即使地址空间随即化了,熵依然不是非常大,来放置猜测。实际上,如果你尝试多次,你的成功率就会非常高。我们的经验表明,几分钟的尝试足以成功利用 Intel 2GHz 的机器。

  • 不可执行栈:从攻击中,我们可以观察到,攻击者将恶意代码放置在栈上,并跳转到它。由于栈是数据而不是代码的地方,我们可以将栈配置为不可执行,因此防止了恶意代码的执行。

    这个保护机制叫做 ExecShield,多种 Linux 发行版已经实现了该机制。ExecShield 本质上禁止了储存在栈上的任意代码的执行。下面的代码(只能由 Root 执行)开启或禁用了 ExecShield。

    
    # sysctl -w kernel.exec-shield=1 // Enable ExecShield # sysctl -w kernel.exec-shield=0 // Disable ExecShield
    

    在下一节中,我们可以看到,这种保护模式并没有解决缓冲区溢出问题,因为另一种类型的攻击,叫做 Return-to-Libc 攻击不需要可执行的栈。

4 不可执行栈和 Return-to-Libc 攻击

为了利用基于栈的缓冲区溢出漏洞,攻击者需要将代码段注入到用户的栈上,之后执行栈上的代码。如果我们使栈的内存段不可执行,即使代码注入到了栈中,代码也不能够执行。这样,我们就可以放置缓冲区溢出攻击。严格来说,这易于实现,因为现代 CPU 架构(例如 x86)的确允许操作系统来将一块内存变为不可执行。但是,还是没有那么简单:许多操作系统,例如 Linux,将代码保存到栈中,因此需要可执行的栈。例如,Linux 为了处理信号,需要在用户栈中放置代码序列。这个序列会在处理信号时执行。

新版本的 Linux 已经使栈只存放数据了。因此,栈可以配置成不可执行。在 Fedora 中,我们可以执行下列命令来使栈不可执行:

# /sbin/sysctl -w kernel.exec-shield=1

不幸的是,使栈不可执行不能完全放置缓冲区溢出。它使运行栈上的代码变得不可能,但是还有其它方法来利用缓冲区溢出漏洞,不需要执行栈上的任意代码。Return-to-Libc 攻击就是这种攻击。

为了理解这种新型攻击,让我们回忆从栈中执行恶意代码的主要目的。我们知道它为了调用 Shell。问题就是,我们是否能够不实用输入的代码来调用 Shell?这实际上是可行的:我们可以使用操作系统自身的代码来调用 Shell。更加具体来讲,我们可以使用操作系统的库函数来完成我们的目标。在类 Unix 系统中,叫做 Libc 的共享库提供了 C 运行时。这个库是多数 C 程序的基础,因为它定义了系统调用,以及其他基本的设施,例如openmallocprintfsystem,以及其他。Libc 的代码已经作为共享运行时库在内存中了,并且他可以被所有应用访问。

函数system是 Libc 中的函数之一。如果我们可以使用参数/bin/sh调用这个函数,我们就可以获得 Shell。这是 Return-to-Libc 攻击的基本原理。攻击的第一部分类似于使用 Shellcode 的攻击,它溢出了缓冲区,并修改了栈上的返回地址。第二部分所有不同。不像 Shellcode 方式,返回地址不指向任何注入的代码。它指向 Libc 中函数system的入口。如果我们执行正确,我们就可以强迫目标程序执行system("/bin/sh"),它会加载 Shell。

挑战:为了完成 Return-to-Libc 攻击,我们需要客服如下困难:

  • 如何寻找system的位置?
  • 如何寻找字符串/bin/sh的位置?
  • 如何将字符串/bin/sh的地址传递给system函数?

4.1 寻找system函数的位置

在多数 Unix 操作系统中, Libc 库始终加载到固定内存地址中。为了寻找 Libc 函数的地址,我们可以使用下面的 GDB 命令(假设a.out是任意程序):

$ gdb a.out
(gdb) b main
(gdb) r
(gdb) p system $1 = {<text variable, no debug info>} 0x9b4550 <system>
(gdb) p exit $2 = {<text variable, no debug info>} 0x9a9b70 <exit>

从上面的 GDB 命令,我们可以发现,system函数的地址是0x9b4550,函数exit的返回地址是0x9a9b70。你系统中的实际地址可能不同。

我们也可以调用函数dlopendlsym来寻找 Libc 中函数的地址。

#include <dlfcn.h>#define LIBCPATH "/lib/libc.so.6" /* on Fedora */void *libh, *sys;if ((libh = dlopen(LIBCPATH, RTLD_NOW)) == NULL){ // report error
}if (( sys = dlsym (libh, "system")) == NULL){ // report error
}
printf("system @ %p\n", sys);

4.2 寻找/bin/sh的地址

有几种方式来寻找这种字符串的地址:

  • 使用缓冲区溢出问题,直接将地址插入栈中,之后猜测它的地址。

  • 在执行漏洞程序之前,创建环境变量,值为/bin/sh。当 C 程序从 Shell 执行时,它就会从 Shell 继承所有环境变量。下面,我们定义了新的 Shell 变量MYSHELL,并使它的值为/bin/sh

    $ export MYSHELL=/bin/sh
  • 我们使用这个变量的地址作为system调用的参数。这个变量在内存中的位置可以使用下面的程序轻易在内存中找到:

    void main() { char* shell = getenv("MYSHELL"); if (shell) printf("%x\n", shell);
    }

    如果站地址没有随机化,我们会发现打印出了相同地址。但是,当我们运行另一个程序时,环境变量的地址可能和你刚刚运行的程序不一样。这种地址在你修改程序名称时就可能改变(因为文件名称的字符数量不同了)。好消息是,Shell 的地址会很接近你是用上一个程序打印出来的东西。因此,你可能需要尝试几次直到成功。

  • 我们也知道,函数system在自己的代码中使用/bin/sh。因此,字符串必然存在于 Libc。如果我们能够寻找字符串的位置,我们就可以直接使用这个字符串。你可以在 LIBC 库文件(/lib/libc.so.6)中搜索字符串rodata

    $ readelf -S /lib/lib.so.6 | egrep ’rodata’
    [15] .rodata PROGBITS 009320e0 124030 ......

    上面命令的结果表明,.rodata段起始于0x009320e0.rodata段用于储存不变数据,字符串常量/bin/sh应该储存在这一段内。你可以编写程序来在起始于0x00932030的内存中搜索字符串。

4.3 将/bin/sh的地址传给system

为了让system执行命令/bin/sh,我们需要将命令字符串的地址作为参数传给system。就像调用任何函数那样,我们需要通过栈传递参数。因此,我们需要将参数放到栈上的正确位置。为了执行正确,我们需要清晰理解调用函数的时候,函数的栈帧如何构建。我们使用小型的 C 程序来理解函数调用在栈上的影响:

/* foobar.c */
#include<stdio.h>
void foo(int x) { printf("Hello world: %d\n", x);
}
int main() { foo(1); return 0;
}

我们可以使用gcc -S foobar.c来将这个程序编译为汇编代码。产生的文件foobar.s像这样:

   ...... 8 foo: 9 pushl %ebp
10 movl %esp, %ebp
11 subl $8, %esp
12 movl 8(%ebp), %eax
13 movl %eax, 4(%esp)
14 movl $.LC0, (%esp) : string "Hello world: %d\n"
15 call printf
16 leave
17 ret ......
21 main:
22 leal 4(%esp), %ecx
23 andl $-16, %esp
24 pushl -4(%ecx)
25 pushl %ebp
26 movl %esp, %ebp
27 pushl %ecx
28 subl $4, %esp
29 movl $1, (%esp)
30 call foo
31 movl $0, %eax
32 addl $4, %esp
33 popl %ecx
34 popl %ebp
35 leal -4(%ecx), %esp
36 ret

调用和进入foo。让我们专注于调用foo时的栈。我们可以忽略之前的栈。要注意,行号而不是指令地址用于解释。

  • 28~29 行:两个语句将值 1,也就是foo的参数压入栈。这个操作使%esp增加了 4。两个豫剧之后的栈由图 3(a) 描述:

    图 3:foo的进入的返回

  • 30 行:call foo:这个语句将call语句的下一条语句的地址压入了栈(也就是返回地址),并跳到foo的代码。当前的栈由图 3(b) 描述。

  • 9~10 行:函数foo的第一行将%ebp压入了栈,来保存上一个帧指针。第二行让%ebp指向当前的帧。当前的栈由图 3(c) 描述。

  • 11 行:subl $8, %esp:栈指针发生改变,来为局部变量和两个传给printf的参数分配空间(8 个字节)。所以函数foo中没有局部变量,8 字节全部用于参数。请见图 3(d)。

离开foo:现在控制流传给了函数foo。让我们看看当函数返回时,栈上发生了什么。

  • 16 行:leave:这个指令隐式执行两条指令(在早期 x86 发行版中它是一个宏,但是后来做成了一个指令):

    mov %ebp, %esp
    pop %ebp

    第一条语句释放了为函数分配的栈空间,之后跳到了返回地址。当前的栈由图 3(e) 描述。

  • 17 行:ret:这个指令只是弹出栈的返回地址,之后跳到返回地址,当前的栈图 3(f) 描述。

4.4 /bin/bash中的保护

如果/bin/sh指向了/bin/bash,即使我们可以在权限的 Set-UID 程序中调用 Shell,我们也不能获得 Root 权限。这是因为 Bash 会自动降低它的权限,如果它执行在 Set-UID Root 上下文中。

但是,有几种方式来绕过这个保护模式。虽然/bin/bash限制了 Set-UID 程序的运行,它的确允许以真实的 Root 权限运行 Shell。所以,如果我们可以将当前的 Set-UID 进程转换为真实的 Root 进程,在我们调用/bin/bash之前,我们就可以绕过这个 Bash 的限制。setuid(0)系统调用可以帮助你实现它。因此,我们首先需要调用setuid(0),之后调用system("/bin/sh")。所有这些可以使用 Return-to-Libc 机制来实现。

基本上,我们需要两次 Return-to-Libc。我们首先让目标程序返回到 Libc 的setuid函数。当这个函数返回时,它会从栈上抓取返回地址,并跳到该地址。如果我们可以让这个返回地址指向system,我们就可以让函数setuid强制返回到system的入口。在执行这个过程时,我们需要十分小心,因为我们需要将合理的参数放到栈的正确位置。

雪城大学信息安全讲义 4.3~4.4相关推荐

  1. 雪城大学信息安全讲义 4.1~4.2

    四.缓冲区溢出漏洞和攻击 原文:Buffer-Overflow Vulnerabilities and Attacks 译者:飞龙 1 内存 这个讲义的"区域"(Area)和&quo ...

  2. 雪城大学信息安全讲义 3.1 Set-UID 机制如何工作

    三.Set-UID 特权程序 原文:Set-UID Programs and Vulnerabilities 译者:飞龙 这个讲义的主要目标就是来讨论特权程序,为什么需要他们,他们如何工作,以及它们有 ...

  3. 雪城大学信息安全讲义 3.3 提升 Set-UID 程序的安全性

    3 提升 Set-UID 程序的安全性 exec函数 exec函数系列通过将当前进程映像包装为新的,来运行紫禁城.有许多exec函数的版本,工作方式不同.它们可以归类为: 使用/不适用 Shell 来 ...

  4. 雪城大学信息安全讲义 七、格式化字符串漏洞

    七.格式化字符串漏洞 原文:Format String Vulnerability 译者:飞龙 printf ( user_input ); 上面的代码在 C 程序中十分常见.这一章中,我们会发现如果 ...

  5. 雪城大学信息安全讲义 六、输入校验

    六.输入校验 原文:Input Validation 译者:飞龙 1 环境变量(隐藏的输入) 环境变量是隐藏的输入.它们存在并影响程序行为.在编程中忽略它们的存在可能导致安全隐患. PATH 在 Sh ...

  6. 雪城大学信息安全讲义 五、竞态条件

    五.竞态条件 原文:Race Condition Vulnerability 译者:飞龙 1 竞态条件漏洞 下面的代码段属于某个特权程序(即 Set-UID 程序),它使用 Root 权限运行. 1: ...

  7. 雪城大学信息安全讲义 4.5

    5 堆或 BSS 的缓冲区溢出 堆或 BSS 的内容 字符串常量 全局变量 静态变量 动态分配的内存 示例:覆盖文件指针 /* The following variables are stored i ...

  8. 雪城大学信息安全讲义 3.2 Set-UID 程序的漏洞

    2 Set-UID 程序的漏洞 2.1 隐藏的输入:环境变量 特权程序必须对所有输入进行安全检查.输入检查实际上是访问控制的一部分,特权程序必须这么做,来确保程序的安全.很多安全问题都是输入检查的错误 ...

  9. 雪城大学信息安全讲义 二、Unix 安全概览

    二.Unix 安全概览 原文:Unix Security Basics 译者:飞龙 1 用户和用户组 用户 root:超极用户(UID = 0) daemon:处理网络. nobody:不拥有文件,用 ...

最新文章

  1. 002_Redis安装和卸载
  2. python数组随机分组_Python实用黑科技——以某个字段进行分组
  3. VTK:相互作用之MoveAVertexUnstructuredGrid
  4. 移动端 c++ 开发_这 10 点值得移动端开发重点学习
  5. RESTful API接口文档规范小坑
  6. LeetCode 1340. 跳跃游戏 V(DP)
  7. 【狂转】某个N人的访谈记录
  8. (Ruby)Ruby中区块用的一些潜藏关键字
  9. pt5 mysql预处理_技术分享 | MySQL 监控利器之 Pt-Stalk
  10. linux asp.net 性能优化,ASP.NET性能优化之减少请求
  11. 最新版ins安装包下载
  12. 数据挖掘项目---航空公司客户价值分析
  13. mysql中as是什么意思_数据库中as是什么意思
  14. 打开计算机文件反应慢怎么解决方法,word文档打开速度慢的几个原因和有效解决方法...
  15. 基于Python的geopandas库处理矢量几何的教程
  16. codeforces 1553B Reverse String
  17. 其他:Pycharm 常用快捷键
  18. Count bits set in parallel(查找32位整形数中置1的个数)
  19. kali中信息收集相关的命令
  20. python tkinter出牌洗牌

热门文章

  1. mysql负载均衡分区_分区和负载均衡让MySQL更大更好
  2. 7004.vue脚手架快速生成项目
  3. 【蓝桥杯嵌入式】【STM32】3_Buzzer之将JTAG功能相关引脚映射为普通IO、AFIO的使用
  4. 【AD】如何将喜欢的图案导出成为丝印层和PCB形状
  5. STM32f407与STM32F103 串口采用DMA收发数据配置方法的异同
  6. ARM系统中断产生流程
  7. 进程间通信(匿名管道、命名管道、共享内存)
  8. arctime工程文件怎么打开_微信dat后缀的文件怎么打开
  9. 正则的简单学习与应用
  10. 互联网系统性能优化方向