强化应用程序抵御代码注入攻击
1. 库加载与地址处理机制
当库尚未加载时,全局偏移表(GOT)中的地址会指向用于初始化库的辅助方法。由于该方法的内存地址通常低于 0x10000,比较(CMP)指令会将状态标志设置为“小于”。这会使处理器跳过后续的两条条件或(ORRGE)和条件位清除(BICGE)指令,因为“GE”后缀表示这些指令仅在状态标志为“大于或等于”时执行。之后,寄存器 r9 中的地址会通过四条 BIC 指令进行掩码处理,最终复制到程序计数器(pc)中。
若库已加载,GOT 中的地址会指向加载在 0x4NNNNNNN 地址空间的方法。此时,CMP 指令会将状态标志设置为“大于或等于”,从而允许后续的 ORRGE 和 BICGE 指令执行。这些指令会确保地址的最高四位被设置为 0x4,保证地址始终指向为库分配的内存范围。BICGE 指令会将结果复制到 pc 中。
2. 保护其他代码指针
函数指针的保护与返回地址的保护类似。在跳转到函数指针存储的地址之前,会先通过四次 BIC 操作对其进行掩码处理,以确保指针未被破坏。同样会使用寄存器 r9 进行掩码操作,这能保证攻击者无法干扰掩码过程或跳过掩码操作。
C 语言的长跳转功能在 ARM 架构上通过存储多个寄存器(STM)和加载多个寄存器(LDM)指令实现。长跳转函数的行为与函数的尾声部分非常相似,它会将内存结构的内容加载到多个寄存器中。CPM 以类似处理函数尾声的方式修改了长跳转函数的实现。LDM 指令不再直接将数据加载到程序计数器中,而是添加了四条 BIC 指令进行掩码处理,并跳转到掩码后的位置。
3. 原型的局限性
在某些情况下,CPM 原型在没有额外输入时无法计算掩码。
- 当函数允许返回到库代码时会出现这种情况。例如,库方法将应用程序函数的指针作为参数接收,然后调用该函数,此函数会返回到调用它的库函数。原型编译器通过接受一个不需要进行掩码处理的函数名列表来解决这个问题。该列表是特定于程序的,需要应用程序开发者维护。在 SPEC 基准测试中,只有一个应用程序有一个方法需要避免掩码处理。
- 当应用程序生成代码或从库中获取代码指针并尝试跳转到该指针时,CPM 会阻止应用程序跳转,因为该指针位于可接受的内存区域之外。可以采用与上述类似的解决方案。在 SPEC 基准测试中,没有应用程序出现这种情况。
4. CPM 原型的评估
4.1 兼容性、性能和内存开销
为测试 CPM 对策的兼容性和性能开销,在一台 ARMv7 处理器(运行频率 800MHz,512Mb RAM,运行 Ubuntu Linux 内核 2.6.28)上,对 SPEC 基准测试分别使用和不使用该对策进行了测试。
| 程序 | GCC(秒) | CPM(秒) | 开销 | 平均掩码大小 | 跳转表面 |
|---|---|---|---|---|---|
| 164.gzip | 808 | 824 | +1.98% | 10.4 位 | 2.02% |
| 175.vpr | 2129 | 2167 | +1.78% | 12.3 位 | 1.98% |
| 176.gcc | 561 | 573 | +2.13% | 13.8 位 | 0.94% |
| 181.mcf | 1293 | 1297 | +0.31% | 8.3 位 | 1.21% |
| 186.crafty | 715 | 731 | +2.24% | 13.1 位 | 3.10% |
| 197.parser | 1310 | 1411 | +7.71% | 10.7 位 | 1.18% |
| 253.perlbmk | 809 | 855 | +5.69% | 13.2 位 | 1.51% |
| 254.gap | 626 | 635 | +1.44% | 11.5 位 | 0.57% |
| 256.bzip2 | 870 | 893 | +2.64% | 10.9 位 | 3.37% |
| 300.twolf | 2137 | 2157 | +0.94% | 12.9 位 | 3.17% |
大多数应用程序的性能损失小于几个百分点,这表明 CPM 是一种高效的对策。由于 VORTEX 在 ARM 架构上无法运行,因此没有该程序的测试结果。使用未修改的 GCC 运行此应用程序会导致内存损坏(并崩溃)。
CPM 的内存开销可以忽略不计。它会略微增加应用程序二进制镜像的大小,因为会为应用程序中的每个函数添加一些指令。而且 CPM 在运行时不会分配或使用内存,因此内存开销实际上为 0%。SPEC 基准测试还表明,CPM 与现有代码具有高度兼容性。基准测试中的程序总共包含超过 500,000 行 C 代码,除了一个应用程序需要少量手动干预外,所有程序都与 CPM 完全兼容。
4.2 安全评估
在对 CPM 进行评估时,首先使用原型进行了一些现场测试。将存在漏洞的现有应用程序和库使用新的对策进行编译,CPM 不仅阻止了现有的攻击,还提高了进一步利用这些应用程序的难度。不过,这只是对 CPM 部分特性的展示,并非完整的安全评估。
安全评估分为两部分:
-
与广泛部署的对策比较
:将 CPM 与广泛部署的对策在安全保护方面进行了比较。不同的行代表允许代码注入攻击的不同漏洞,列代表不同的对策。每个单元格包含可用于突破对策安全性的不同(组合)攻击技术,如返回至库(Return-into-libc/Return-oriented programming,RiC)、信息泄露(Information leakage,IL)和堆喷射(Heap spraying,HS)。CPM 是唯一能抵御所有常见攻击技术组合的对策,尽管并非绝对完美。
| 漏洞类型 | ProPolice | ASLR | NX | 组合 |
|---|---|---|---|---|
| 基于栈的缓冲区溢出 | IL | HS, IL | RiC | IL+RiC |
| 基于堆的缓冲区溢出 | N/A | HS, IL | RiC | IL+RiC, HS+RiC |
| 间接指针覆盖 | N/A | HS, IL | RiC | IL+RiC, HS+RiC |
| 悬空指针引用 | N/A | HS, IL | RiC | IL+RiC, HS+RiC |
| 格式字符串漏洞 | N/A | HS, IL | RiC | IL+RiC, HS+RiC |
受三种广泛部署对策保护的应用程序可能会被两种常见攻击技术的组合成功攻击。如果应用程序泄露敏感信息,攻击者可以利用这些信息突破地址空间布局随机化(ASLR)和 ProPolice,然后使用 Return-into-libc 攻击或相关的 Return-oriented Programming 攻击突破不可执行内存(No-Execute)。如果应用程序不泄露敏感数据,攻击者可以使用堆喷射攻击的变体,在堆中填充虚假栈,然后执行 Return-into-libc 或 Return-oriented Programming 攻击。
CPM 通过限制攻击者可返回的返回位点数量来抵御 Return-into-libc 攻击和 Return-oriented Programming 攻击。这两种攻击都依赖于攻击者能够跳转到内存中的某些感兴趣的点并滥用现有代码(无论是库代码内存还是应用程序代码内存)。然而,CPM 掩码很可能不会给攻击者足够的自由来成功实施攻击。具体来说,CPM 不允许返回到库代码,只允许返回到应用程序代码的有限部分。表 1 展示了每个应用程序的跳转表面,即攻击者使用掩码代码指针可以跳转到的程序代码内存的平均表面积(没有 CPM 时,这些值都为 100%)。
CPM 很容易抵御在堆上喷射 shellcode 的攻击,因为掩码永远不会允许攻击者跳转到堆(或任何其他数据结构,如栈),使这种攻击完全无效。攻击者仍然可以喷射虚假栈,但随后必须成功执行 Return-into-libc 或 Return-oriented Programming 攻击,而如前所述,这种可能性非常低。
CPM 也不会受到攻击者通过内存泄露获取的信息的影响,因为它不使用任何秘密信息。编译器计算的掩码不是秘密。即使攻击者知道每个单独掩码的值,也无法帮助他们绕过 CPM 掩码过程。这只能让他们了解哪些内存位置仍然可以返回,但由于掩码范围很窄,这些位置不太可能有利用价值。
与许多基于编译器的对策一样,应用程序使用的所有库也必须使用 CPM 进行编译,否则这些库中的漏洞仍可能被利用。不过,CPM 与未受保护的库完全兼容,支持与可能无法获取源代码的代码进行链接。
CPM 旨在抵御代码注入攻击,但其他类型的攻击仍然可能可行。特别是仅针对数据的攻击(攻击者覆盖应用程序数据而不涉及代码指针),CPM 无法提供保护。
-
CPM 的安全特性
:CPM 的设计基于三个决定其安全性的事实:
- 掩码所有代码指针 :未进行掩码处理的代码指针仍然是潜在的攻击目标。对于 ARM 原型,会对相关论文中描述的所有不同代码指针进行掩码处理。此外,还检查了 GCC 用于发出跳转的所有代码,以验证是否应将其作为 CPM 掩码的目标。
- 掩码不可绕过 :CPM 发出的所有掩码指令都位于只读程序代码中,这保证了攻击者无法修改这些指令本身。同时,攻击者也无法跳过掩码过程。在 ARM 架构上,通过保留寄存器 r9 并使用该寄存器执行所有掩码操作和计算跳转来确保这一点。
- 掩码范围狭窄 :不同应用程序和函数的掩码范围可能不同。调用者较少的函数通常会生成更窄的掩码。统计数据表明,大多数函数的调用者较少。在 SPEC 基准测试的应用程序中,27% 的函数只有一个调用者,55% 的函数有三个或更少的调用者,约 1.20% 的函数有 20 个或更多调用者,这些函数通常是库函数,如 memcpy、strncpy 等。为了改进掩码,编译器会对函数进行重新排列,并在函数之间添加少量填充,以确保返回地址包含尽可能多的 0 位。通过这种技术,可以减少不同函数特定掩码中设置为 1 的位数。在没有 CPM 的情况下,攻击者可以跳转到内存中的任何地址(在 32 位机器上有 2^32 种可能性)。使用这里描述的技术,SPEC 基准测试中应用程序的每个掩码的平均位数可以降低到少于 13 位。这意味着使用 CPM 后,平均每个函数只能返回到应用程序整个内存范围的不到 0.0002%。
CPM 与控制流完整性(CFI)对策具有相似的高级特性,但能抵御更强的攻击模型。特别是,CPM 不需要不可执行的数据内存。如果掩码能够精确到只允许正确的返回位点,受 CPM 保护的应用程序将永远不会偏离预期的控制流,此时 CPM 提供的保证与 CFI 相同。但实际上,掩码不可能完美。因此,CPM 可以看作是 CFI 的一种高效近似。
CPM 对控制流转移的保护强度取决于掩码的精度。攻击者仍然可以跳转到掩码允许的任何位置,对于某些应用程序,这可能仍然存在可利用的攻击机会。因此,CPM 提供的保证比 CFI 少。然而,由于掩码范围非常窄,攻击者很难利用这有限的空间进行攻击。SPEC 基准测试还表明,CPM 的性能比 CFI 好得多。这是因为 CPM 在掩码操作中不访问内存,而 CFI 需要查找存储在内存中的标签。此外,CPM 支持动态链接代码,而 CFI 缺乏这一特性。
5. 讨论与未来工作
CPM 与其他对策部分重叠,但也能抵御其他对策无法覆盖的攻击。反之,也有一些攻击(如仅针对数据的攻击)可能对 CPM 有效,但对其他对策无效。因此,CPM 是对现有安全措施的补充,尤其可以与非可执行内存、栈金丝雀和 ASLR 等流行对策结合使用。将 CPM 添加到现有的保护措施中,可以显著提高攻击者进行代码注入攻击的难度。
当攻击者覆盖某个代码指针时,CPM 不会检测到这种修改,而是会对代码指针进行掩码处理并跳转到清理后的地址。攻击者仍然可以通过在代码指针中写入无效数据来使应用程序崩溃,处理器会跳转到掩码后的无效地址,很可能在某个时刻崩溃。但最重要的是,攻击者无法执行其有效负载。可以对 CPM 进行修改,以检测代码指针的任何更改,并在这种情况下终止应用程序。此功能可以用 7 条 ARM 指令(而不是 4 条指令)实现,但临时需要第二个寄存器进行计算。
CPM 的机制可以移植到其他架构。已经有 x86 架构的第二个原型,但由于页面限制和尚未完成,本文未进行报告。不过,对最大类别的代码指针(返回地址)的保护已经实现,其性能与 ARM 架构上的性能相当。
未来有前景的工作方向是针对特定处理器进行增强。特别是在 ARM 处理器上,可以利用条件执行特性进一步缩小攻击者可以返回的目标地址范围。条件执行允许几乎每条指令根据某些状态位进行条件执行。如果在函数返回时翻转这些状态位,并在应用程序的不同(已知)返回位点再次翻转,攻击者将被迫跳转到其中一个返回地址,否则将落在处理器不会执行的指令上。
6. 相关工作
为了保护程序免受代码注入攻击,已经设计了许多对策。下面简要介绍与其他保护程序免受内存错误漏洞攻击的方法的区别:
-
边界检查器
:边界检查是解决缓冲区溢出问题的较好方法,但在为 C 语言实现时,会对性能产生严重影响,并且可能导致现有代码与经过边界检查的代码不兼容。最近的边界检查器在性能上有所改进,但仍然无法保护免受悬空指针漏洞、格式字符串漏洞等的攻击。
-
概率性对策
:许多对策在保护攻击时会利用随机性。使用随机性进行保护有多种不同的方法。基于金丝雀的对策使用一个秘密随机数,该随机数存储在重要内存位置之前,如果在执行某些操作后随机数发生了变化,则检测到攻击。内存混淆对策使用随机数对重要内存位置进行加密。内存布局随机化器通过在随机地址加载栈和堆,并在对象之间放置随机间隙来随机化内存布局。指令集随机化器在内存中对指令进行加密,并在执行前进行解密。尽管这些方法通常很高效,但它们依赖于保持内存位置的秘密性。存在一些攻击,攻击者可以利用内存泄露读取应用程序的内存,从而绕过这类对策。
-
信息分离和复制
:依赖于信息分离或复制的对策会尝试复制有价值的控制流信息或将该信息与常规数据分离。这些对策很容易被间接指针覆盖攻击绕过,攻击者可以使用栈上的指针覆盖不同的内存位置,而不是返回地址。更高级的技术尝试将所有控制流数据(如返回地址和指针)与常规数据分离,使攻击者更难利用溢出覆盖这类数据。虽然这些技术可以有效地保护免受试图覆盖控制流信息的缓冲区溢出攻击,但无法保护免受攻击者控制作为指针偏移量的整数的攻击。
-
数据和代码内存分离
:另一种广泛部署的对策是区分包含代码的内存和包含数据的内存,将数据内存标记为不可执行。这种简单的对策对直接代码注入攻击(攻击者将代码作为数据注入)有效,但对间接代码注入攻击(如返回至库攻击)无法提供保护。而 CPM 可以同时抵御直接和间接代码注入攻击。
-
软件故障隔离
:软件故障隔离(SFI)并非专门为抵御 C 语言中的代码注入攻击而开发,但它在某些方面也能提供一定的保护。
强化应用程序抵御代码注入攻击
7. 不同对策的对比分析
为了更清晰地了解 CPM 与其他常见对策的差异,我们可以通过以下表格进行对比:
| 对策类型 | 优势 | 劣势 | 对代码注入攻击的防护能力 |
| — | — | — | — |
| 边界检查器 | 理论上可解决缓冲区溢出问题 | 性能影响大,与现有代码兼容性差,无法抵御多种漏洞 | 部分抵御缓冲区溢出 |
| 概率性对策 | 利用随机性,通常较高效 | 依赖内存位置秘密性,易被内存泄露攻击绕过 | 部分抵御攻击 |
| 信息分离和复制 | 可保护控制流信息免受部分溢出攻击 | 易被间接指针覆盖攻击绕过,无法抵御特定整数偏移攻击 | 部分抵御攻击 |
| 数据和代码内存分离 | 对直接代码注入有效 | 无法抵御间接代码注入攻击 | 部分抵御攻击 |
| CPM | 抵御多种常见攻击技术组合,性能高效,内存开销小,与现有代码兼容性高 | 无法抵御数据-only 攻击 | 强,可抵御直接和间接代码注入 |
从这个表格可以看出,CPM 在抵御代码注入攻击方面具有明显的优势,尤其是在面对多种常见攻击技术组合时,表现更为突出。
8. CPM 的工作流程
下面通过 mermaid 格式的流程图来展示 CPM 在处理代码指针时的工作流程:
graph TD;
A[获取代码指针] --> B{库是否已加载};
B -- 未加载 --> C[CMP 指令设置状态标志为小于];
C --> D[跳过 ORRGE 和 BICGE 指令];
D --> E[使用 BIC 指令对 r9 地址掩码];
E --> F[将掩码后地址复制到 pc];
B -- 已加载 --> G[CMP 指令设置状态标志为大于或等于];
G --> H[执行 ORRGE 和 BICGE 指令];
H --> I[确保地址最高四位为 0x4];
I --> J[BICGE 指令复制结果到 pc];
这个流程图清晰地展示了 CPM 根据库的加载状态对代码指针进行不同处理的过程,确保了代码指针的安全性。
9. 总结与展望
综上所述,CPM 是一种高效且强大的抵御代码注入攻击的对策。它在性能、兼容性和安全性方面都表现出色,能够有效地抵御多种常见的代码注入攻击技术组合。与其他常见对策相比,CPM 具有独特的优势,尤其是在面对复杂的攻击场景时。
然而,CPM 也存在一定的局限性,例如无法抵御仅针对数据的攻击。在未来的工作中,可以进一步优化 CPM 的设计,提高其对各种攻击的防护能力。例如,可以结合其他安全技术,如入侵检测系统(IDS),对应用程序进行更全面的保护。
同时,将 CPM 机制移植到更多的架构上也是一个重要的发展方向。目前已经有 x86 架构的原型,未来可以进一步完善并推广到其他架构,扩大 CPM 的应用范围。
另外,针对特定处理器的增强也是值得探索的方向。如在 ARM 处理器上利用条件执行特性进一步缩小攻击者可利用的地址范围,提高系统的安全性。
总之,CPM 为强化应用程序抵御代码注入攻击提供了一种有效的解决方案,未来通过不断的优化和发展,有望在更多的场景中发挥重要作用,保障系统的安全稳定运行。
超级会员免费看
1134

被折叠的 条评论
为什么被折叠?



