引用Bartels, J.,
Stephens, J., & Debray, S.(2020 年 12 月)。对动态代码的表示和推理。第 35 届 IEEE/ACM 自动化软件工程国际会议论文集(第 312-323 页)。
总结
:
动态代码是指在运行时创建或修改的代码,这种代码在当今世界无处不在。动态代码的行为取决于动态代码生成器的逻辑,这种依赖关系具有重大的安全隐患。现有的程序分析方法未能为这种行为关系的推理提供足够的理论支持。本文通过描述程序表示和新依赖关系的概念来解决这个问题,这使我们能够推理动态代码生成器和生成的动态代码之间的依赖关系和信息流关系,从而迈出了解决这个问题的第一步。实验结果表明,通过分析这些概念,我们能够捕捉到传统程序分析无法识别的动态代码特征。
介绍
动态代码出现在许多上下文中,包括 JIT 编译和动态二进制转换。动态代码带来了新的安全挑战,部分原因是包含动态代码的应用程序的行为可能部分依赖于不属于应用程序本身的逻辑,而是依赖于动态代码生成器。解决这个问题的关键是能够推理动态代码生成器和生成的代码之间的依赖关系和信息流,以便我们可以尝试确定生成代码的行为如何受到代码生成器行为的影响。这种端到端分析可以显著加快识别和修复此类问题的过程。
但是,现有的程序分析方法无法完成动态代码修改的推理,并且现有方法没有考虑动态代码修改行为可能产生的依赖关系。本文展示了如何通过捕获动态变化代码的结构和演变的程序表示来解决这个问题。实验结果表明,我们的思想使得对动态代码的推理成为可能。
动态代码推理
动态代码修改会导致程序在执行过程中的不同时间点产生不同版本的程序,并具有不同的指令和行为。适用于动态代码的端到端分析应该能够跟踪动态修改产生的不同版本的代码。有两个问题需要考虑:
(1)何谓“动态代码修改”?
(2) 如何在程序表示中捕获这些修改?
我们解决这些问题的方法如下。首先,一般来说,如果内存写入的目标是程序内存的可执行部分,则可能不够精确,无法将其归类为代码修改,例如,可以在执行过程中更改内存页上的权限,以使内存的不可执行区域可执行。因此,只有当 l 是随后执行的指令的一部分时卡通人物,我们才会考虑写入内存位置 l 作为“代码修改”。其次,即使是很小的动态代码更改也会导致程序的表示和行为发生任意大的变化。例如,在 X86 ISA 中,算术指令“按位或”(操作码:or;代码:0x0c)。可以通过翻转一位来改变它,以控制传输命令“如果相等,则跳跃”(opcode: je;代码:0x0f)。
我们使用动态分析来构建程序的控制流程图(CFG)。在我们遇到修改内存位置的指令之前,我们面临着对程序行为的潜在任意更改。为了捕捉到这一点,我们开始构建一个新的 CFG,我们使用一种特殊类型的边缘(我们称之为“动态边缘”)将其链接到之前构建的 CFG。每个这样的链接CFG对应于程序执行的一个阶段。
概念定义
(1)阶段。阶段的思想是将程序执行分成一系列片段 φ0、φ1,…,φi,…这样,对于每个 φi信息流切片授权是什么意思,用 φi 编写的任何指令都不是 φi 执行的任何指令的一部分。每个φi称为一个“相位”。执行从阶段 φ0 开始,阶段是程序的初始代码。当我们遇到第一个动态指令时,我们切换到φ1。继续在 φ1 中(包括可能在 φ0 中创建或修改的其他指令),直到我们遇到在 φ1 中修改的指令,此时我们切换到 φ2,依此类推。如图 1 所示。程序执行阶段捕获执行过程中动态代码修改产生的代码的不同“版本”。没有动态代码的执行仅包含一个阶段。
图1 阶段
(2)动态控制流程图。我们使用“阶段”的概念来构建动态代码的控制流程图:我们为每个执行阶段构建一个 CFG,使用特殊的边(动态边)链接在一起,这些边代表从一个阶段的最后一条指令到下一阶段的第一条指令的控制流。我们将此 CFG 称为动态控制流程图 (DCFG)。
图2 DCFG示例
图 2 显示了一个简单的 DCFG 示例。所考虑程序的静态CFG如图2(a)所示。当指令 I2 被执行时,它将指令 I1 更改为 J1(由红色虚线箭头表示),其中 J1 是一个条件分支,可能具有后续的 I3 和 I5。
(3) Codegen 依赖。动态代码修改可能会导致执行修改的代码与最终修改的代码之间存在依赖关系。
在此示例中,B 是将即时值 imm 添加到寄存器 r0 的指令;包含 imm 的 B 字节位于地址 loc。因此,如果 loc 包含值 5,则 B 是指令“addi r0,5”。指令 A 通过将寄存器 r1 的内容写入地址 loc 来修改 B。执行 B 时,添加到 r0 的值取决于 A 写入地址的 loc 的值。因此,A 的执行以下列方式影响 B 的行为:动态代码修改行为,与程序中可能存在的任何数据或控件依赖关系无关。我们将以这种方式产生的因动态代码修改而产生的依赖关系称为代码生成依赖关系。
实验我们
构建了一个原型实现来评估我们想法的有效性。我们使用英特尔的 Pin 软件(3.7 版)进行程序检测和指令级执行跟踪的收集;和 XED(版本 8.20.0)用于指令解码。我们遍历指令跟踪并构造 DCFG 以执行。我们识别动态代码如下。在构建 DCFG 时,我们使用自己的污点库来确定代码生成依赖关系:我们将写入内存,每次内存写入都会获得不同的污点标记。对于跟踪中的每条指令,我们检查其任何指令字节是否被污染,在这种情况下,该指令被标记为动态。
(1)精益求精。一个好的切片算法不会排除任何可能影响切片条件的语句。在我们的例子中信息流切片授权是什么意思,在动态代码中观察到的一些行为是执行动态代码生成的代码中某些逻辑的结果,一个好的切片算法应该能够识别出动态生成的代码中观察到的行为与动态代码生成器中的罪魁祸首之间的联系。
(2)综合基准。我们使用了两个动态代码的小示例作为综合基准。这些实现了相对小而简单的动态代码案例,使我们能够专注于我们方法的核心概念。为了评估动态代码分析的当前技术水平,我们使用了三种流行的动态分析工具:PinPlay、angr 和 Triton,并根据这些程序计算的结果计算了反转动态切片。我们的实验表明:(1)这些工具构建的CFG并不代表动态代码修改产生的不同版本的代码;(2)虽然这三个工具都成功地将所有相关的非代码生成相关指令包含在它们计算的切片中,但它们都无法获得执行动态修改的代码。
(3)现实生活中的例子。为了评估我们在使用动态代码的实际软件上的方法,我们考虑了三个示例应用程序:(1)分析涉及JIT代码的漏洞;(2)JIT编译器中的Bug位置;(3)使用动态代码进行基于触发的规避检测。我们的目标是对这些示例进行端到端分析,从相关动态代码开始,然后计算一个向后动态代码段,其中包括产生错误/安全漏洞的动态代码生成器部分。结果如表1所示。
表1
(a) 漏洞利用分析。让我们考虑使用动态代码的三个示例:
(1) 在 Google V8 JavaScript 引擎的 JIT 代码页中写入越界 (OOB) 恶意 shellcode;
(2) V8 的 JIT 编译器中存在转义分析缺陷 (CVE-2017-5121);
(3)用于逃逸LuaJIT沙箱的恶意字节码。
我们按如下方式分析每个漏洞。我们使用概念验证代码从动态生成的漏洞利用代码开始计算 DCFG/反向动态切片。此外,我们检查了每次攻击的编写报告,以确定每次攻击的缺陷,并在每次攻击记录的执行跟踪中识别了有缺陷的代码生成器部分。然后,我们检查切片卡通形象,以确定该切片中是否存在任何有缺陷的生成器代码。
第一个安全漏洞需要 OOB 写入 Google V8 JavaScript 引擎中的 JIT 代码页。该漏洞是由于数组类型不明确所致,我们修改了相同的漏洞,使 shellcode 遇到除以零的异常,以帮助识别执行跟踪中的动态代码。我们使用 D8 从命令行调用 V8 JavaScript 引擎,使用 Pin 获取执行跟踪,然后构造一个 DCFG 和一个向后动态切片,该切片来自 nop 滑槽中的第一个 nop shellcode 指令,该指令紧接在除零异常之前。我们的向后切片成功地包含了导致 V8 中数组类型不明确的错误代码,以及在运行时实际生成 shellcode 的指令
第二个漏洞源于 V8 演进分析中的一个 bug,该 bug 导致 JIT 优化代码中的某些变量初始化在减载期间被错误地优化。提供的概念验证代码会导致 V8 在执行优化的动态代码时因 OOB 读取而崩溃。我们从 Pin 记录的执行跟踪构造了 DCFG,然后在 OOB 读取引发异常之前,根据动态指令计算了向后动态切片。我们发现,在 V8 的 JIT 编译器的转义分析阶段,结果切片正确地包含了 load reducer 的 bug 部分,并且其优化导致了 OOB 读取。
最后一个例子是使用恶意 Lua 字节码来逃避 LuaJIT 中的沙箱。恶意程序会破坏字节码,以便编写打印消息的 shellcode。我们使用的方法类似于编写切片 V8 OOB 的方法,使用 NOP 滑槽的起点开始切片攻击。我们发现,我们的工具计算的向后切片正确地拾取了生成 shellcode 的 Lua 代码。
总之,使用我们的方法,我们能够从一些有问题的动态创建的指令开始,然后向后计算切片,然后向后工作以识别创建指令的 JIT 编译器逻辑。
(b) 错位。我们考虑了 Google V8 JavaScript 引擎中的三个 JIT 编译器 bug,它们被发布并归类为“Bug-Security”。(
1)字节码生成器生成的空跳转表导致越界读取,导致生成的JIT编译崩溃(Issue 794825)。
(2) 类型混淆错误,导致动态代码生成后崩溃(问题 794822)。
(3) 箭头函数范围修复了系统无法处理某些涉及单行箭头函数的结构导致崩溃的错误(问题 807096)。
我们的实验结果如表1所示。我们的端到端分析能够成功找到上面片段中提到的每个 bug 的代码,从而缩小 V8 中涉及的导致崩溃的功能。
(4)性能。表 2 显示了基于 DCFG 的切片实现原型在实际测试输入上的性能。
表2
就绝对时间而言,大多数程序切片大约需要 2-10 分钟,LuaJIT 示例需要 8 秒,V8 转义分析错误需要 2.8 小时。切片能够删除 DCFG 中大约 50%-60% 的指令,多达 71% 的指令被 LuaJIT 攻击删除。通过排除与 JIT 编译器无关的代码,可以进一步改进这些结果。这些数字表明,我们的方法既实用(就时间而言)又有用(就从 DCFG 中删除的代码数量而言)。
可用性的一个重要方面是可伸缩性。这对于动态分析尤为重要,因为使用动态代码的实际软件的复杂性,加上它们通常是多线程的,这意味着指令迹线可能会变得非常大。在这种情况下,我们的原型实现不太关注性能,但具有合理的可扩展性,能够处理大量计算。
DCFG 的简单实现可能会导致 DCFG 组件在多个阶段复制,从而导致大量内存浪费。我们的 DCFG 实现通过将一组“相位”符号与 DCFG 中的每条指令、基础块和边相关联来避免这种重复;我们的切片算法已经过修改,以避免在 DCFG 中遵循无法实现的路径。对于我们测试的实际基准测试,这导致 DCFG 大小节省了约 18%。
(5)重点分析:标注和细分。鉴于将问题本地化为罪魁祸首 JIT 编译器代码的总体目标,检查我们的方法在多大程度上减少了需要考虑的实际 JIT 编译器代码量是很有用的:例如,如果一个代码段排除了大多数前端解析器和解释器,但包含了整个 JIT 编译器,则该片段没有用。为了评估这一点,我们重复了我们的实验,尽可能地将评估重点放在系统的JIT编译器部分。我们在代码中放置了尽可能接近 JIT 编译器调用的标记 – 明确可识别且语义中立的小代码片段。
表3
这些实验的结果如表3所示。DCFG 大小为 35%-85%,切片大小为 26%-84%。JIT类型的混淆误差样本是一个异常值,几乎所有的原始DCFG和切片都被消除了。总体而言,这些结果表明:(a)我们的方法在关注JIT编译器的相关部分方面是有效的;(b) 使用代码标记来标识JIT编译器中的条目,可以帮助锁定所分析代码的有关部分。
(6)结果汇总。实验评估结果表明,DCFG和代码生成器依赖关系对于动态代码的端到端分析是有效的。具体来说,我们展示:
(a) 将代码生成器依赖性的概念扩展和扩展到DCFG的逆向分析,可以有效地从JIT编译代码中的特定行为转变为JIT编译器中导致生成代码的逻辑。
(b) 前向分析(通过代码生成器依赖性的概念增强并扩展到DCFG)可以识别在动态代码生成过程中受环境触发因素影响的动态代码。
(c) 我们对后向动态切片的实现是实用的(分析时间方面)、有用的(从 DCFG 中删除的代码数量)和可扩展的(能够处理涉及复杂现实世界软件的大规模分析)。
总结
动态代码在当今世界无处不在,并带来了一系列安全挑战。现有的程序分析方法不足以推理动态代码的行为。首先,当前的程序表示没有充分捕捉到动态代码修改引起的更改的影响;其次,它们没有考虑动态代码修改行为可能产生的依赖关系。本文讨论如何通过适用于动态代码的程序表示形式和可以捕获动态代码与生成动态代码的代码之间的依赖关系的新概念来应对这些挑战。基于这些想法的向后动态切片原型实现的实验表明,在许多实际示例中,这些想法使得从错误代码回到导致错误生成的 JIT 编译器逻辑成为可能。
确认
本文由南京大学软件学院2021级硕士曹志浩翻译转述。
- 本文固定链接: https://wen.nuanque.com/shouquan/17224.html
- 转载请注明: nuanquewen 于 吉祥物设计/卡通ip设计/卡通人物设计/卡通形象设计/表情包设计 发表
- 文章或作品为作者独立观点不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。本文之内容为用户主动投稿和用户分享产生,如发现内容涉嫌抄袭侵权,请联系在线客服举报,一经查实,本站将立刻删除。本站转载之内容为资源共享、学习交流之目的,请勿使用于商业用途。