利用 LLVM 攻击 VMProtect 代码混淆(上)
2021-09-27 12:50:00 Author: www.4hou.com(查看原文) 阅读量:56 收藏

下图对接下来要描述的所有操作和组件进行了一个简单概述,蓝色块代表输入,黄色块代表运行,白色块代表中间信息,紫色块代表输出。

1.png

LLVM 命名最早源自于底层虚拟机(Low Level Virtual Machine)的缩写,由于命名带来的混乱,目前LLVM就是该项目的全称。LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。能够进行程序语言的编译器优化、链接优化、在线编译优化、代码生成。LLVM的项目是一个模块化和可重复使用的编译器和工具技术的集合。此外,LLVM 还具有强大的优化通道,我们绝对可以利用这些信息来清除执行虚拟化代码后无关紧要的任何不必要的内存存储。

本文所需要的与数据相关的虚拟机组件如下:

30个虚拟寄存器:由虚拟机内部使用,它们的活动范围在VmEnter之后开始(用传入的主机执行上下文初始化它们),在VmExit之前结束(它们的值被复制到传出的主机执行上下文)。因此,它们的状态不应该在虚拟化代码之外持续存在。它们被分配在堆栈上的内存块中,该内存块只能被特定的VmHandlers访问,因此保证无法被虚拟化代码执行的任意堆栈访问访问,它们彼此独立。在虚拟执行期间,它们可以作为一个整体或在子寄存器中访问。这个进程以下被简称为vmregister。

19 个传递槽:VMProtect 用于将执行状态从一个 VmBlock 传递到另一个VmBlock。它们的活动始于VmBlock的结束,止于后续VmBlock的开始。它们被分配到堆栈上,并且处于活动状态时,它们只能通过每个 VmBlock 的结束/开始中的 push/pop 指令访问。它们彼此独立,并且始终作为整个堆栈槽访问这个进程以下被简称为 VmPassingSlots。

16个通用寄存器:在VmEnter期间推入堆栈,通过vmregister加载和操作,在VmExit期间从堆栈弹出,反映虚拟执行期间对它们所做的更改。它们的活动范围在VmEnter之前开始,在VmExit之后结束,因此它们的状态必须在执行虚拟化代码之后持续存在。它们彼此独立,与vmregister相反,通用寄存器总是作为一个整体被访问。标志寄存器也被视为通用寄存器,但它可以被一些VmHandlers直接访问。

4个通用段:FS和GS通用段寄存器的活动范围与通用寄存器匹配,并且保证底层段不与其他内存区域(例如SS,DS)重叠。相反,对 SS 和 DS 段的访问并不总是保证彼此不同。 SS 和 DS 段的活跃度也与通用寄存器匹配。

辅助函数

按照如上所讲的方法获取信息后,我们可以继续定义一些基本的 LLVM-IR 结构,然后这些结构将用于提升各个 VmHandler、VmBlock 和 VmFunction。

当我第一次开始使用 LLVM 时,我生成所需结构或指令链的方法是通过 IRBuilder 类,但我很快意识到我会花更多的时间查看文档以生成所需的类型和指令,而不是真正专注于设计它们。然后,在 SATURN 上工作时,很明显遵循 Remill 的方法是一个成功的策略,至少在最初的高级设计阶段是这样。事实上,他们的想法是在 C++ 中实现结构和语义,将它们编译为 LLVM-IR 并动态加载生成的 bitcode 文件以供 Lifter 使用。

下面是stub函数的最小实现,我们可以使用它作为模板来提升VmStub(VmEnter和一个或多个VmExit之间的虚拟化代码):

2.1.png

2.2.png

VirtualRegister结构表示一个VmRegister,它被分割成更小的子块,这些子块将由 VmHandler 以不一定与对 x64 架构上的子寄存器的访问相匹配的方式进行访问。例如,虚拟化 64 位 bswap 指令将产生 VmHandlers 访问 VmRegister 的所有字子块。 __attribute__((packed)) 旨在生成一个没有填充字节的结构,匹配 VmRegister 使用的确切数据布局。

rref 定义是辅助函数使用的参数定义中采用的一种便利类型,一旦编译为 LLVM-IR,将生成具有 noalias 属性的指针参数。 noalias 属性向编译器暗示,在函数内部发生的任何内存访问都不会取消引用从指针参数派生的指针,保证不会与取消引用从指针参数派生的指针的内存访问别名。

RAM、GS 和 FS 数组定义是方便的零长度数组,我们可以使用它们来生成对通用内存槽(堆栈段、数据段)、GS 段和 FS 段的索引内存访问。访问将以getelementptr指令的形式生成,LLVM 将自动将具有基本 RAM 的指针视为不与具有基本 GS 或 FS 的指针混用,这对我们来说非常方便。

HelperStub 函数原型是一个方便的声明,我们可以在 Lifter 中使用它来表示单个 VmBlock。它接受通用寄存器指针序列、标志寄存器指针、每个 VmEnter 推送的三个关键值(KEY_STUB、RET_ADDR、REL_ADDR)、虚拟堆栈指针、虚拟程序计数器、VmRegister指针和 VmPassingSlots指针作为参数。

HelperFunction 函数被定义为一个方便的模板,我们可以在 Lifter 中使用它来表示单个 VmStub。它接受通用寄存器指针序列、标志寄存器指针和每个 VmEnter 推送的三个关键值(KEY_STUB、RET_ADDR、REL_ADDR)作为参数。对象声明了一个包含 30 个 VmRegisters 的数组、一个包含 19 个 VmPassingSlots 的数组、虚拟堆栈指针和虚拟程序计数器。一旦编译为 LLVM-IR,它们将被转换为 alloca 声明(堆栈帧分配),保证不会与函数中使用的其他指针形成别名,并且会在函数作用域结束时自动释放。为方便起见,我们将 REL_ADDR 设置为 0,但可以根据正在分析的二进制文件的需要,将其动态设置为用户提供的适当 REL_ADDR。最后,我们发出对 HelperStub 函数的调用,传递所有需要的参数并获得更新后的指令指针作为输出,反过来,HelperFunction 也会返回该指针。

全局变量和函数声明被标记为 extern "C", 以避免任何形式的名称修改。事实上,我们希望能够使用 getGlobalVariable 和 getFunction 等函数从动态加载的 LLVM-IR 模块中获取它们。

编译和优化的LLVM-IR代码描述的c++定义如下:

3.png

处理程序的语义

我们现在可以继续实现 VMProtect 使用的处理程序的语义,如前所述,直接在 LLVM-IR 级别实现它们可能是一项不可能完成的任务,因此我们将继续使用与上一节中采用的相同的 C++ 到 LLVM-IR 逻辑。

以下处理程序的选择应该给出实现处理程序语义所采用的逻辑的概念。

STACK_PUSH

为了使用 push 操作访问堆栈,我们定义了一个模板化的辅助函数,它将虚拟堆栈指针和值作为参数进行 push。

4.png

我们可以看到虚拟堆栈指针使用模板参数的字节大小递减,然后继续使用 std::memcpy 函数执行安全类型双关存储操作,以虚拟堆栈指针为索引访问 RAM 数组。 C++ 实现是使用 -O3 优化编译的,因此函数将被内联(如 always_inline 属性所预期的那样)并且 std::memcpy 调用将被转换为正确的指针类型转换和存储指令。

STACK_POP

正如预期的那样,堆栈弹出操作(pop operation)也被定义为一个模板化的辅助函数,它将虚拟堆栈指针作为参数并返回弹出的值作为输出。

5.png

我们可以看到,使用与上面解释的相同的 std::memcpy 逻辑从堆栈中读取值,将未定义的值写入当前堆栈槽,并使用模板参数的字节大小增加虚拟堆栈指针。与前一种情况一样,-O3 优化将负责内联和降低 std::memcpy 调用。

添加

作为堆栈程序,我们知道它将从堆栈顶部弹出两个输入操作数,将它们相加,计算更新的标志并将结果和标志推回堆栈。加法处理程序有四种变体,旨在处理 8/16/32/64 位操作数,其特点是 8 位情况实际上是从堆栈中弹出每个操作数 16 位并将 16 位结果推回堆栈与 x64 push/pop 对齐规则保持一致。

如上所述,我们唯一需要的是虚拟堆栈指针,以便能够访问堆栈。

6.png

我们可以看到,函数定义使用了一个T参数作为模板,该参数在内部用于生成由上面定义的STACK_PUSH和STACK_POP帮助程序执行的大小适当的堆栈访问。此外,我们正在处理截断和零扩展特殊的8位情况。最后,在进行无符号加法之后,依靠Remill的语义验证标志计算来计算新标志,然后再将它们推入堆栈。

其他二进制和算术运算按照相同的结构实现的,具有正确的操作数访问和标志计算。

PUSH_VMREG

此处理程序旨在获取存储在 VmRegister 中的值并将其推送到堆栈中,该值也可以是虚拟寄存器的子块,不一定从 VmRegister 槽的基址开始。因此,函数参数将是虚拟堆栈指针和 VmRegister 的值。该模板还定义了推送值的大小以及与 VmRegister 插槽基数的偏移量。

7.png

我们可以看到如何根据大小和偏移模板参数(例如 vmreg.word.w1、vmreg.qword)访问正确的 VmRegister 子块,以及如何再次使用 std::memcpy 来实现安全的内存写入索引的 RAM 数组,虚拟堆栈指针也照常递减。

POP_VMREG

此处理程序旨在从堆栈中弹出一个值并将其存储到 VmRegister 中。该值也可以是虚拟寄存器的子块,不一定从 VmRegister 槽的基址开始。因此,函数参数将是虚拟堆栈指针和对要更新的 VmRegister 的引用。和之前一样,模板定义了弹出值的大小和到 VmRegister 插槽的偏移量。

8.png

在本例中,我们可以看到VmRegister的子块上的更新操作是通过一些屏蔽、移动和零扩展来完成的。这是为了帮助LLVM尽可能地将较小的整数值合并为更大的整数值。正如我们在STACK_POP操作中看到的,我们正在向当前堆栈槽写入一个未定义的值。最后,我们对虚拟堆栈指针进行递增操作。

加载和LOAD_GS

一般来说,LOAD处理程序意味着从堆栈中弹出一个地址,解除对它的引用以从某个程序段加载一个值,并将检索到的值推到堆栈顶部。

下面的c++代码片段展示了从通用内存指针(例如SS或DS段)和GS段加载内存的实现:

9.1.png

92.png

到目前为止,过程应该是清晰的。唯一的区别是访问的零长度数组将最终作为getelementptr指令的基础,它将直接反映LLVM能够推断出的别名信息。同样的逻辑应用于对不同段的所有读或写内存访问。

DEFINE_SEMANTIC

在本节的代码片段中,你可能已经注意到三个名为 DEFINE_SEMANTIC_64、DEFINE_SEMANTIC_32 和 DEFINE_SEMANTIC 的宏。它们是从 Remill 借来的无数个技巧,旨在生成具有未混淆名称的全局变量,指向专用模板处理程序的函数定义。例如,在LLVM-IR级别上,8/16/32/64位情况的ADD语义定义如下:

10.1.png

102.png

UNDEF

在本节的代码片段中,你可能还注意到了一个名为 UNDEF 的函数的用法。此函数用于在每次从堆栈中弹出后存储一个虚构的 __undef 值。这样做是为了向 LLVM 发出信号,表明在从堆栈中弹出后不再需要弹出的值。

__undef 值被建模为一个全局变量,在优化管道的第一阶段,它会被 DSE 之类的通道用来阻止重叠的post-dominated dead 存储,它会在接近结束时被一个真正的 undef 值替换优化管道,以便相关的存储指令将在最终优化的 LLVM-IR 函数中被删除。

提升基本块(Lifting a basic block)

我们现在有一堆模板、结构和辅助函数,但是我们如何最终提升一些虚拟代码呢?

过程如下:

生成了一个带有HelperStub签名的新的LLVM-IR函数;

函数对象中填充了对VmHandler辅助函数的调用指令,这些辅助函数提供了正确的参数(从HelperStub参数中获得);

优化管道在函数上执行,导致所有辅助函数(标记为 always_inline)的内联以及值的传播;

对 VmRegisters、VmPassingSlots 和存储到段的更新状态进行了优化,删除了 VMProtect 使用的大部分混淆模式;

计算虚拟堆栈指针和虚拟指令指针的更新状态;

下面是一个虚构的基于HelperStub函数的完整管道示例,在 C++ 级别实现并优化以获得传播的 LLVM-IR 代码如下:

11.png

最后一个片段表示与 VmBlock 相关的所有语义计算,如高级概述中所述。如果我们提升的代码捕获了与 VmStub 相关的整个语义,我们可以用 HelperFunction 函数包装 HelperStub 函数,它强制执行活动和别名信息部分中描述的活动属性,使我们能够仅获取计算更新主机执行上下文:

12.1.png

12.2.png

12.3.png

12.4.png

12.5.png

可以看出,该示例只是被推入寄存器 rax 和 rbx 的值,将它们分别加载到 vmregs[0] 和 vmregs[1] 中,将 VmRegisters 推入堆栈,将它们相加,弹出 vmregs 中更新的标志[2],将加法的结果弹出到 vmregs[3],最后将 vmregs[3] 推入堆栈,最后弹出到 rax 寄存器中。 VmRegisters 值的活动随着函数的结束而结束,因此保存在 vmregs[2] 中的更新标志不会反映在主机执行上下文中。查看最后的代码片段,我们可以看到代码的语义已经成功获得。

在下一篇文章中,我们将充分利用所描述的结构和辅助程序,深入研究虚拟化 CFG 探索的细节并介绍 LLVM 优化管道的基础知识。

本文翻译自:https://secret.club/2021/09/08/vmprotect-llvm-lifting-1.html如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/0DR5
如有侵权请联系:admin#unsafe.sh