Windows 平台搞 LLVM

警告
本文最后更新于 2023-07-13,文中内容可能已过时。

LLVM 在 Linux 和 Windows 平台下的具体区分不是很大,困难在如果需要对驱动或者 shellcode 加密,需要调用 MSVC 的链接器,在编译的时候很麻烦

编译

下载比较新的 llvm-project,虽然只用得到的 clang 和 llvm

1
git clone https://github.com/llvm/llvm-project.git

在项目根目录下新建Directory.build.props

1
2
3
4
5
6
<Project>
    <PropertyGroup>
         <LLVMInstallDir>/Your/project/path</LLVMInstallDir>
         <LLVMToolsVersion>16.0.5</LLVMToolsVersion>
    </PropertyGroup>
</Project>

打开 VS 2022 的命令行

1
2
3
4
5
6
cd BronyaObfus
mkdir build
mkdir install
cd build
cmake -G "Ninja" -DCMAKE_CXX_FLAGS="/utf-8" -DLLVM_ENABLE_PROJECTS="clang" -DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" -DLLVM_ENABLE_PLUGINS=On -DLLVM_ENABLE_RTTI=ON -DBUILD_SHARED_LIBS=off -DLLVM_INCLUDE_TESTS=OFF -DCMAKE_INSTALL_PREFIX="../install" ../llvm
ninja

等待编译完成即可

修改 CMakeLists.txt 和部分源码

因为基本上是迁移和重写 Pluto-Obfuscator 的工作,所以就不直接从 OLLVM 搬了

llvm/include/llvm/Transforms以及llvm/lib/Transforms目录下创建Obfuscation文件夹

修改llvm/lib/Transforms下的CMakeLists.txt,添加

1
2
3
4
5
add_subdirectory(Utils)
add_subdirectory(Instrumentation)
add_subdirectory(AggressiveInstCombine)
...
add_subdirectory(Obfuscation) # add here

以字符串混淆为例,在新创建的Obfuscation中分别添加StringObfuscation.hStringObfuscation.cpp

然后在lib目录下的Obfuscation中添加CMakeLists.txt,内容如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
add_llvm_component_library(LLVMObfuscation
  Hello.cpp
  StringObfuscation.cpp

  DEPENDS
  intrinsics_gen

  LINK_COMPONENTS
  Core
  Support
  Analysis
  TransformUtils
)

然后注册宏,需要先在StringObfuscation.h中编写好 PassManager 继承类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#ifndef LLVM_STRING_OBFUSCATION_H
#define LLVM_STRING_OBFUSCATION_H

#include "llvm/IR/LLVMContext.h"
#include <llvm/Transforms/Obfuscation/PassRegistry.h>

namespace llvm {

class StringObfuscationPass : public PassInfoMixin<StringObfuscationPass> {
public:
  PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);

  static bool isRequired() { return true; }
};

} // namespace llvm

#endif

然后在llvm/lib/Tranforms/Passes中,对PassBuilder.cppPassRegistry.defCMakeLists.txt进行修改

  • PassBuilder.cpp中添加对StringObfuscation.h头文件的引用

  • PassRegistry.def中对应的 Pass 中添加对应内容,比如字符串混淆是 MODULE 层的 Pass,则添加MODULE_PASS("string-obfus", StringObfuscationPass()),如果是 FUNCTION 层的 Pass,添加对应的FUNCTION_PASS即可

    • 这里可能会因为名字过短而无法实现使用?不太清楚
  • CMakeLists.txt中添加对Obfuscation的链接

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    add_llvm_component_library(LLVMPasses
    OptimizationLevel.cpp
    PassBuilder.cpp
    PassBuilderBindings.cpp
    PassBuilderPipelines.cpp
    PassPlugin.cpp
    StandardInstrumentations.cpp
    
    ADDITIONAL_HEADER_DIRS
    ${LLVM_MAIN_INCLUDE_DIR}/llvm
    ${LLVM_MAIN_INCLUDE_DIR}/llvm/Passes
    
    DEPENDS
    intrinsics_gen
    
    LINK_COMPONENTS
    AggressiveInstCombine
    Analysis
    ...
    Obfuscation # add here
    )
    

最后在StringObfuscation.cpp编写 Pass,完成之后ninja编译即可,一个简单的例子如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using namespace llvm;

static cl::opt<bool>
    RunStringObfuscationPass("string-obfus", cl::init(false),
                             cl::desc("BronyaObfus - StringObfuscationPass"));

namespace {

// ...

bool runStringObfuscation(Module &M) {
  auto GlobalStrings = encodeGlobalStrings(M);

  auto DecodeFunc = createDecodeFunc(M);

  auto DecodeStub = createDecodeStubFunc(M, GlobalStrings, DecodeFunc);

  Function *MainFunc = M.getFunction("main");

  insertDecodeStubBasicBlock(MainFunc, DecodeStub);
  return false;
}

} // namespace

PreservedAnalyses StringObfuscationPass::run(Module &M,
                                             ModuleAnalysisManager &AM) {
  if (!runStringObfuscation(M))
    return PreservedAnalyses::all();
  return PreservedAnalyses::none();
}

// New pass manager register

llvm::PassPluginLibraryInfo getStringObfuscationPluginInfo() {
  return {LLVM_PLUGIN_API_VERSION, "StringObfuscation", "v0.1",
          [](PassBuilder &PB) {
            // Register ModulePass
            PB.registerPipelineStartEPCallback(
                [](llvm::ModulePassManager &PM, OptimizationLevel Level) {
                  PM.addPass(StringObfuscationPass());
                });
          }};
}

#ifndef LLVM_STRING_OBFUSCATION_LINK_INTO_TOOLS
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
  return getStringObfuscationPluginInfo();
}
#endif

可以通过opt.exe --help来查看优化选项,如果成功注册可以看到选项中多出了刚写的 Pass

如果需要用到 OLLVM 中的CryptoUtils.cpp,需要修改CryptoUtils.h中的一个宏定义

1
2
#if defined(__i386) || defined(__i386__) || defined(_M_IX86) ||                \
    defined(INTEL_CC) || defined(_WIN64) || defined(_WIN32)

在 VS2022 中使用

因为编译 PE 文件,需要调用 MSVC 的链接器,这里需要一个插件 LLVM2019,在项目属性中选择 LLVM

然后在新增的 LLVM 中将Use lld-link关闭

关闭优化,关闭 C/C++ 命令行中的从父级或项目默认设计继承,如果使用 Plugin 来加载 Pass ,前面 clang-cl.exe 的目录设置为编译 Plugin 的 LLVM 目录

这样在编译导出的dll插件时就能顺利编译通过了

混淆

其实这些在以前的博客中也写过,单独拿出来再写一次(

字符串混淆

Pass 处理在 IR 中要找到所有的字符串常量是比较容易的,直接遍历 module->globals 即可,但是问题的关键是,加密范围,解密时机和解密存储位置,目前来说开源方案的几种方案:

  • 加密范围:
    • 指令中直接引用的 CDS
    • 指令中间接引用的 CDS
  • 解密时机:
    • ctor 解密
    • Function 入口解密
    • Instruction 前解密
  • 解密存储位置:
    • 解密到段上
    • 解密到栈上

这里采取的是 ctor 解密,尽管 Windows 的 PE 加载与 ELF 不同,但在进入代码的入口函数之前都要经过 CRT 的全局初始化,所以将解密函数插入到初始化的过程中就能实现解密了

这样实现的就是编译期加密,运行期解密的字符串混淆,只能对抗静态分析,动态运行之后无法对抗 DUMP 手段,优点是编写方便且不容易导致程序崩溃,并且对性能的损耗很小

(暂时还不支持std::string,会报错,很迷惑)

虚假控制流

虚假控制流将正常的控制流拆分,通过不透明谓词的特性,使得静态分析无法通过程序分析来计算条件的真假

一个典型的虚假控制流如下图,Opaque 表示不透明谓词,是一个永真式

Opaque 是永真式,通常是某个不能被静态程序分析得到的条件式(谓词),从上面的控制流图来看,真正的控制流只有 EntryBlock’->Body->End,而 Altered 永远不会跳转到,也就不会影响正常的程序执行

整个 Pass 根据某个概率阈值确定是否对 BasicBlock 进行拆分,然后根据混淆次数对整个 Function 中的 BasicBlocks 进行不断拆分,从而实现虚假控制流混淆

这里用到的不透明谓词有两个,第一个是 prime1 * (((x & 0x3FF) | any1)^2) != prime2 * (((y & 0x3FF) | any2)^2),其中xy分别是程序中可用的变量,prime1prime2是随机素数,any1any2是随机整数

注意
这里存在一个问题,在选取程序中的变量的时候,因为设置了跳转所以很容易选取到非支配基本块中的变量,导致无法通过opt优化

第二个是y < 10 || x * (x + 1) % 2 == 0xy均为全局变量

控制流平坦化

控制流平坦化同样也是修改正常的控制流,将正常的控制流修改为一个如下伪代码和控制流图所示的结构

1
2
3
4
5
while(1) {
  switch (Var) {
    //...
  }
}

根据原本控制流的基本块跳转顺序来设定Var,从而确定switch结构下一个要跳转的case,即下一个需要跳转的 BasicBlock

Pass 为每一个 BasicBlock 设置一个随机的SwitchVar,并在结尾根据后继基本块的跳转在基本块的末尾设置对应的SwitchVar,进入 Return 块之后跳转到 Dispatch 块,Dispatch 块中根据当前SwitchVar的值选择对应的case

多项式 MBA 指令替换

简单来说,Mixed Boolean-Arithmetic(MBA) 是将普通的计算表达式替换成由布尔运算构成的等价计算表达式,形如

而一个线性 MBA 构造形如

其中 $e_i(x_1, …, x_t)$ 表示的是由变量 $(x_1, …, x_t)$ 构成的布尔运算表达式,$a_i$ 表示的是系数

先从线性 MBA 开始,如果我们想混淆一个x + y表达式,可以计算出相对应的混合布尔运算表达式(x ^ y) + 2 * (x & y),通过 z3 可以得到验证

我们可以通过以下的方式来构造一个线性 MBA 表达式:

对正整数 $n, s, t$ , $n$ 为整数位数, $s$ 为布尔表达式个数, $t$ 为变量个数, $x_i, i \in [1,t]$ 为 $B^n$ 上的变量, $e_j, j \in [0, s-1]$ 为 $x_i$ 组成的按位运算表达式,得到线性 MBA 表达式

$$ E = \sum^{s - 1}_{j = 0} a_j e_j $$

共有 $2^t - 1$ 种状态,令

$$ f_j = (v_{0, j}, …, v_{i, j}, v_{2^t - 1, j})^{\top} $$

表示真值表的列向量,令 $A = (v_{i, j})_{2^t \times s}$ 为 $\mathrm{Z}/2^n$ 环上的 0-1 真值表矩阵

若要 $E = 0$ 有解,当且仅当 $AY = 0$ 在 $\mathrm{Z}/2^n$ 环上有解

注意

Proof:

若 $E = 0$ ,$(a_0, a_1, …, a_{s-1})^{\top}$ 为 $e = 0$ 的一种解

假设解存在,令 $z_{j,i}$ 表示 $e_j$ 的二进制位值,即

$$ e_j = \sum^{n - 1}_{i = 0} z_{j, i} 2^i $$

则 0-1 真值表矩阵的行的第 i 位向量可表示为 $(z_{0, i}, …, z_{s - 1, i})$

通过上面的假设可以得到,

$$ \sum^{s - 1}_{j = 0} a_j z_{j, i} = 0 $$

因此有

$$ E = \sum^{s - 1}_{j = 0} a_j e_j = \sum^{n - 1}_{i = 0}\sum^{s - 1}_{j = 0}a_j z_{j, i} 2^i = \sum^{n - 1}_{i = 0} 2^i \left(\sum^{s - 1}_{j = 0} a_j z_{j, i}\right) = 0 $$

得证

通过上面的数学推导,我们只需要构造出一个 0-1 真值表矩阵 $A$ ,并在 $\mathrm{Z}/2^n$ 环上求出 $AY = 0$ 的解,最后再加上原本表达式的操作数,就能得到一个等价的、经过混淆的线性 MBA 表达式

比如现在有五个布尔表达式, $f_0(x,y) = x$ 、 $f_1(x,y) = y$ 、 $f_2(x,y) = x \lor y$ 、 $f_3(x,y) = \neg(x \land y)$ 、 $f_4(x,y) = -1$

得到 0-1 真值表矩阵:

$$ A = \begin{pmatrix} 0&0&0&1&1 \\ 0&1&1&1&1 \\ 1&0&1&1&1 \\ 1&1&1&0&1 \\ \end{pmatrix} $$

相当于

x y x ∨ y ¬(x∧y) -1
0 0 0 1 1
0 1 1 1 1
1 0 1 1 1
1 1 1 0 1

得到 $Y = (1, 1, -1, 1, -1)^\top$ ,即 $x + y - (x \lor y) + (\neg(x \land y)) - (-1) = 0$

得到线性 MBA 表达式后,可以进一步扩展到多项式上

定义群 $(P_m(\mathrm{Z}/2^n), \circ)$ , $\circ$ 为函数复合运算,即 $f(x) \circ g(x) = f(g(x))$ ,由群的性质可得,在群 $(P_m(\mathrm{Z}/2^n), \circ)$ 上一定存在 $g(x)$ 使得 $f(g(x)) = x$

如果我们构造两个互为逆元的多项式 $f(x)$ 、 $f^{-1}(x)$ ,可以得到 $f(x) \circ f^{-1}(x) = x$ ,即 $f(f^{-1}(x)) = x$ ,将 $x$ 替换为需要混淆的线性 MBA 表达式,就能得到一个等价的多项式 MBA 表达式

为了简化编写过程,令多项式为 $f(x) = a \cdot x + b$ ,则 $f^{-1}(x) = a^{-1} \cdot x + (- a^{-1} \cdot b)$ ,替换 $x$ 为上面得到的线性 MBA 表达式即可

混淆间接调用

通过枚举每个函数中直接调用指令,对收集到的每个直接调用(CallInstInvokeInst)生成一个间接调用数组,比如对于一份源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void foo() {
    printf("foo\n");
}

int bar(int a) {
  if (a < 0) {
    return 1;
  }
  return 2;
}

int main() {
    
    foo();

    int input;

    scanf("%d", &input);

    int result = bar(input);

    printf("%d\n", result);

    return 0;
}

main()中调用了foo()scanf()bar(input),生成一个指针数组(IndGV = &GV[EncKey]),内容是对应调用地址+随机数

1
@main_IndirectCallees = private global [4 x ptr] [ptr getelementptr (i8, ptr @_Z3foov, i64 -3610769465783198784), ptr getelementptr (i8, ptr @__isoc23_scanf, i64 -3610769465783198784), ptr getelementptr (i8, ptr @_Z3bari, i64 -3610769465783198784), ptr getelementptr (i8, ptr @printf, i64 -3610769465783198784)]

然后再次遍历一遍当前函数中所有的直接调用指令,将指令替换为上述指针数组的值-随机数(GV = IndGV[-EncKey]),一加一减就相当于直接调用函数并且修改成了间接调用

Reference

0%