Function Peekaboo: 使用LLVM构建自掩码函数
引言
LLVM编译器基础设施因其模块化设计、灵活性和丰富的中介表示而功能强大。与传统编译器不同,LLVM将前端(语言解析)与后端(代码生成)分离,使开发者能够以最小的重复支持多种语言和目标平台。其中介表示与语言无关,专为优化而设计,非常适合构建静态分析器、自定义代码生成器、混淆器和JIT编译器。
Clang/Clang++是基于LLVM构建的前端,用于解析和编译用C、C++、Objective-C和Objective-C++编写的源代码。它将代码转换为LLVM IR,然后由LLVM的后端处理。Clang和LLVM共同形成了一个灵活可扩展的工具链,能够支持广泛的语言、平台和自定义编译器功能。
在本文中,我们将自定义LLVM编译器基础设施,构建一个解决方案,为C++源文件中的普通用户定义函数启用自掩码功能。自掩码意味着函数在被调用之前保持在掩码(混淆或加密)状态。一旦执行进入函数,它会被临时解除掩码,返回时恢复为掩码状态。
实现方案
函数注册
我们的自定义LLVM实现不会不加选择地掩码编译单元中的每个函数,而是仅对用户明确选择的函数有选择地应用修改。我们提供了一个专门的宏函数REGISTER_FUNCTION(),它将函数元数据插入到.funcmeta部分。
|
|
自定义Prologue和Epilogue
为了实现自掩码功能,我们需要在编译期间通过注入自定义代码来操作程序的控制流。关键的是这些修改不能改变原始控制流或干扰函数参数和返回值的处理。
我们为编译单元中的每个函数实现了自定义prologue和epilogue存根。这些存根将管理执行流并调用专用的掩码处理程序,负责执行掩码和解除掩码操作。
Prologue代码工作流程:
- 检索函数起始地址并将其传递给处理程序
- 检查是否处于初始化阶段
- 如果在初始化阶段,跳转到epilogue存根
- 如果在正常执行流程,调用处理程序解除掩码函数体
Epilogue代码工作流程:
- 检查是否处于初始化阶段
- 如果在初始化阶段,直接调用处理程序
- 如果在正常执行流程,跳转到处理程序代码
掩码处理程序
处理程序代码负责:
- 在执行处理程序逻辑之前保存寄存器值
- 解析
.funcmeta部分中的函数元数据 - 在初始化阶段更新每个注册函数的函数体大小
- 使用简单的XOR编码对函数体和epilogue存根进行掩码
- 在掩码前后调用VirtualProtect来更改内存保护
重定向入口点和初始化阶段
在编译完成后,我们注入一个名为.stub的新部分到最终的PE文件中。这个部分包含负责处理入口点重定向和预CRT执行逻辑的汇编代码。入口点存根执行初始化阶段,计算所有注册函数的总大小,并在正常执行恢复之前对每个函数进行掩码。
LLVM编译器基础设施修改
选择正确的阶段
对于这个项目,我们不与LLVM的IR级代码交互,而是在后端阶段进行修改。我们专注于在每个注册函数的开头和结尾附加自定义prologue和epilogue,并将处理程序存根嵌入到.text部分。
创建自定义机器函数传递
我们创建了一个自定义机器函数传递X86RetModPass,它继承自MachineFunctionPass类。我们重写了超类中的特殊LLVM例程runOnMachineFunction。
这个传递的逻辑很简单:如果返回指令位于函数块的末尾,我们只需删除它。如果在其他地方找到返回,我们将其替换为跳转指令,将控制重定向到我们的处理程序存根。
修改后端组件
我们修改了X86AsmPrinter类中的三个关键函数:
emitFunctionBodyStart():为每个注册函数发出自定义prologue代码emitFunctionBodyEnd():为每个注册函数发出自定义epilogue代码emitEndOfAsmFile():将处理程序逻辑的汇编存根发出到.text部分
注入入口点存根
我们使用Python脚本将名为.stub的新部分注入到最终PE文件中。该部分嵌入了负责处理入口点重定向和预CRT执行逻辑的汇编代码。
测试程序
我们创建了一个测试程序来验证自掩码功能:
|
|
测试结果显示,在初始化阶段之前,REG_foo函数体保持未掩码状态,可以清楚地观察到附加到函数体的自定义prologue和epilogue代码存根。初始化阶段后,REG_foo函数及其epilogue代码被掩码,只有prologue保持可见,这反映了预期的运行时状态。
总结
通过修改LLVM编译器基础设施,我们成功实现了一个能够为C++函数添加自掩码功能的解决方案。这种方法通过自定义prologue和epilogue代码段,在运行时动态加解密函数体,有效增强了二进制保护能力,而不改变原始控制流或影响参数和返回值的处理。