【软件与系统安全笔记】五、内存破坏防御

【软件与系统安全】五、内存破坏防御

Image

这是《【软件与系统安全】笔记与期末复习》系列中的一篇

虽然对缓冲区溢出的认知已超过 40 年之久, 但缓冲区溢出仍未被消除。部分原因在于存在大量的利用选项:

  • 多样的目标: 不仅仅可以利用返回地址, 实际上可以利用任意代码地址或数据
  • 多样的使用: 可以利用“读”或“写”操作
  • 多样的利用方式: 对代码可以“注入”或“重用”
  • 当前对溢出的防御措施多样, 但不完全

防御可以在不同的时机进行

  • 编程前
  • 开发过程中 (防御性编程)
  • 测试时 (fuzzing, . . .)
  • 代码运行时 (检测和缓解: stack canaries, DEP, . . .)

Stack Canaries 栈 金丝雀

是栈溢出的检测机制, 又称“栈 cookies”,由 gcc 的 StackGuard 实现

原理:将一个 dummy 值(或随机值)写到栈上的返回地址之前,并在函数返回时检查该值。不小心构造的栈溢出(假定是顺序栈粉碎)会覆写该“canary”单元, 该行为将被探测到。

攻破 StackGuard 的基本方法

对 canary 单元, 用正确的值覆写

  • 如果 canary 所使用的随机值范围很小, 则枚举每种可能性
  • 或先实施一个 memory disclosure 攻击, 获知 canary 的值

无法抵御 disclosure 攻击是 StackGuard 的最大局限性

  • disclosure 攻击通过对缓冲区的“overread”实现

  • 著名例子: 对 SSL 的 Heartbleed 攻击

  • 以下程序为什么会对 Stackguard 的 canaries 造成威胁?

    • 通过对缓冲区的“overread”, 攻击者读取超出栈缓冲区之外的值, 从而获取 canary 的值
char packet[10];// suppose len is adversary controlled
strncpy(buf, packet, len);
send(fd, buf, len);

image.png

有时不需要覆写返回地址, 可以溢出:

  • 安全敏感的局部变量
  • 堆数据
  • 全局数据
  • · · ·
  • 本质上, 攻击者只需要劫持一个函数指针

劫持函数指针

void foo () {...}
void bar () {...}
int main() {
    char buf [16];
    void (*f) () = &foo;
    gets(buf);
    f();
}

假定我们没有机会溢出返回地址

可溢出缓冲区, 使得函数指针被修改为 bar 的地址, 然后函数调用将调用 bar 而非 foo

劫持函数指针的其他方法

  • 使用堆溢出,对堆上的函数指针进行劫持
  • 劫持全局函数指针
  • 劫持全局偏移量表(GOT)中的函数指针, 被动态链接函数所使用

攻破 StackGuard 的其他方法

有时不需要覆写返回地址, 可以溢出:

  • 安全敏感的局部变量

  • 堆数据

  • 全局数据

    • 全局数据溢出: 攻击位于全局数据区的缓冲区
  • · · ·

如何防御?

  • 让函数指针位于其他类型数据的下方(更低地址)
  • 在全局数据区和其他管理表结构之间使用守卫页

守卫页(Guard Pages)

也是一种运行时检测方法, 可以看作StackGuard的扩展

在一个进程地址空间中关键内存区域之间放置守卫页 (像一些gaps)

  • 需借助CPU内存管理单元(MMU)的管理功能将它们标记为非法地址
  • 任何对其的访问尝试都导致进程被终止

效果: 能失效缓冲区溢出攻击, 特别是对全局数据区的溢出攻击

甚至可以在栈帧之间、或者堆缓冲区之间放置守卫页

  • 可以提供更进一步的保护, 防止栈溢出和堆溢出攻击
  • 会导致执行时间和内存的很大开销, 因为要支持大量页映射

数据执行保护(DEP)

冯诺依曼体系结构

  • 将代码作为数据存储
  • 使得攻击者可以向栈或堆注入代码, 而栈和堆原本只应该存储数据

哈佛架构

  • 虚拟地址空间切分为一个数据区和一个代码区
  • 代码区可读且可执行
  • 数据区可读且可写
  • 没有区域是既可写又可执行的

Data Execution Prevention (数据执行保护): 是一种运行时缓解技术

DEP又称作Nx-bit (non executable bit), WX

能够阻止代码注入攻击

很多缓冲区溢出攻击涉及将机器码复制到目标缓冲区, 然后将执行转移到这些缓冲区

  • 一种防御方法就是阻止在栈/堆/全局数据区中执行代码, 并假定可执行代码只能出现在进程地址空间中除这些位置外的其他位置需要CPU内存管理单元(MMU)提供支持, 将虚拟内存的对应页标记为不可执行
  • 对于每一个被映射的虚拟内存页, 都有这样额外的1个no-executebit, 置位时, 表示该页的数据不能作为代码执行, 一旦程序控制流到达该页, CPU会产生陷入

DEP 被绝大多数操作系统和指令集体系结构支持

  • 一些CPU早有支持(如Solaris的SPARC), 只需修改Solaris内核参数即可启用
  • x86系列后来才向MMU中加入no-execute位
  • Linux/Unix类系统, Windows均已提供相应扩展, 支持使用DEP特性

对 Nx-bit 的不同叫法

  • Intel: XD (eXecute Disable)
  • AMD: Enhanced Virus Protection
  • ARM: XN (eXecute Never)

如果CPU硬件支持, DEP可作为操作系统更新, 通过更改对进程虚拟地址空间的内存管理, 提供对现有漏洞程序的保护

DEP将栈和堆置为不可执行, 对多种缓冲区溢出攻击提供了一种高度的保护

但有一些合法程序需要将可执行代码放在栈上:

  • 如Java运行时系统、运行时代码生成、Linux信号处理程序等
  • 需要针对这些需求制定一些专门条款(Special provisions)

攻击 DEP——代码重用攻击

思路: 重用程序自身的代码

Return-to-libc: 用危险的库函数的地址替换返回地址

代码重用攻击: Return to libc

危险库函数如 system()

攻击者构造合适的参数(在栈上, 返回指令指针的上方)

  • 在x64架构上,还需要更多的工作:设置参数传递寄存器的值

函数返回,库函数得到执行

  • 例如:execve(“/bin/sh”)

甚至可以链接两个库函数调用

image.png

具体地

  • 攻击者用一个溢出填充buffer:

    • 更改栈上保存的ebp为一个合适地址
    • 更改返回指令指针为一个欲执行的库函数的地址
    • 写一个占位符值(库函数会认为其是返回地址,如果想利用它调用第二个库函数, 应写入第二个库函数的地址)
    • 写一个或多个要传递给此库函数的参数
  • 当被攻击的函数返回时, 恢复(更改过的)ebp, 然后pop更改后的返回地址到eip, 从而开始执行库函数代码

  • 因为库函数相信它已被调用, 故会将栈顶当前值(占位符)作为它自己栈帧的返回指令指针, 之上是参数

  • 最终会在占位符位置的下方创建起一个新的栈帧 (对应于库函数的执行)

  • 根据库函数参数类型以及库函数对参数的解释方式, 攻击者可能需要准确地知道参数地址以做溢出写

代码注入 vs 代码重用

image.png

代码重用与代码注入的协同

在很多攻击中, 代码重用攻击用来作为禁用DEP的第一步

  • 目标是允许对栈内存进行执行

  • 有一个系统调用可以更改栈的读/写/执行属性

    • int mprotect(void *addr, size_t len, int prot);
  • 设置对于起始于addr的内存区域的保护

  • 调用此系统调用, 允许在栈上的“执行”属性, 然后开始执行被注入的代码

ROP 面向返回的编程

面向返回的编程

  • 执行任意行为, 不需要注入代码
  • 联合现有的代码片段 (gadgets)
  • 一系列图灵完全的 gadgets, 及一种串联这些 gadgets 的方法
  • 现有的展示已能针对小程序(如16KB)找到图灵完全的 gadgets 集合

image.png

正常机器指令序列

image.png

ROP执行

image.png

TODO

用ROP我们能做什么?

Turing completeness

一种语言是Turing complete的,如果其具有

  • 条件分支(Conditional branching)
  • 可以任意修改内存

这两点在ROP中均能实现

针对ROP的保护

ROP的工作基于对程序控制流的修改

控制流完整性 (Control-flow integrity, CFI)

  • 预先决定被攻击程序的控制流图

  • 向该程序中插入检测, 使得在程序运行时发生非法控制流跳转时,终止程序

    • 通过编译器或二进制重写进行插入

ROP 的运行时缓解: 随机化

ROP利用要求攻击者对代码/数据地址的知识,例如

  • 缓冲区的起始地址
  • 库函数的地址

思路: 引入人为的多样性(随机化)

  • 使得地址对于攻击者而言难以预测
  • 如果攻击者不知道一段代码(或数据)在内存的什么位置,
  • 他就没办法在攻击中重用它们

有很多方法能够实现随机化

  • 对栈的位置进行随机化, 对堆上的关键数据结构进行随机化,
  • 对库函数的位置进行随机化
  • 随机地填充栈帧
  • 在编译时, 随机化代码生成, 以抵御 ROP

实现随机化的时机

  • 编译时
  • 链接时
  • 运行时(通过动态二进制重写, dynamic binary rewriting)

地址空间随机化的挑战

  • 信息泄露(如通过边信道)
  • 暴力破解秘密值
  • 对于长时间运行的进程, 如何“再次随机化”

地址空间随机化的有效性取决于

  • 每个被随机出的位置的熵值
  • 随机化的完备性(completeness), 例如是否所有的对象都被随机化?
  • 信息泄露的避免程度

ASLR Linux 的地址空间布局随机化

ASLR(Address space layout randomization)

  • 对于位置无关的可执行程序(PIE), 随机化该可执行程序的基地址

    • 所有库都是 PIE, 因此它们的基地址被随机化
    • 主可执行程序可能不是 PIE, 故可能无法被 ASLR 保护
  • 关注的是内存块的随机化

  • ASLR 是一种粗粒度的随机化形式

    • 只有基地址被随机化
    • 在内存对象之间的相对距离不变

攻破 ASLR 的方法

  • 如果随机地址空间很小, 可以进行一个穷举搜索

    • 例如, Linux 提供 16 位的随机化强度, 可以在约 200 秒以内被穷举搜索攻破
  • ASLR 经常被 memory disclosure (内存泄漏) 攻破

    • 例如, 如果攻击者可以读取指向栈的指针值, 他就可以使用该指针值发现栈在哪里

防御性编程

使用更安全的编程语言

代码评审

费根检查(fagan inspection)

检查表

编译时防御

编写时

危险的 C 库函数

输入验证

所有输入都是恶意的

最小化攻击面

识别攻击面

防御性编程总结

  • 好的实践
    • 使用更安全的编程语言
    • 进行代码评审
    • 使用编译器的机制, 如 StackGuard
    • 编写内存安全的代码
      • 使用边界检查库函数,使用更安全的库
  • 输入验证
    • 识别攻击面: 程序从信道获得输入
    • 最小化攻击面
    • 将所有输入都看作潜在恶意的

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

框架主义者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值