llvm 浅探

写在前面

这篇文章的前半部分包括写的环境搭建都是基于 LLVM 源码的构建方式,重点在于帮助了解 llvm 的结构,如果目的只是编写 Pass 的话,直接跳到 自己编写 LLVM Pass 部分即可

LLVM 环境搭建

由于我的 Ubuntu 版本不够,无法直接 apt install llvm-20,所以就去 https://github.com/llvm/llvm-project.git 里面下载了源码,解压到 wsl 下

更新包列表
sudo apt-get update

安装基础编译工具(包含g++, make等)
sudo apt-get install build-essential

安装 CMake
sudo apt-get install cmake

安装 Ninja
sudo apt-get install ninja-build

安装 Clang
sudo apt-get install clang

安装其他依赖
sudo apt-get install python3 zlib1g-dev libtinfo-dev

进入 llvm 源码目录

1
cd  llvm-project-llvmorg-20.1.8

配置并生成构建文件

1
cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang"

进入 build 目录,并行使用 8 个线程进行构建

1
2
cd build
ninja -j8

把 llvm 源码导入 CLion 中,找到 llvm-project-llvmorg-20.1.8\llvm 目录下的 CMakeLists.txt,打开
在设置里的 构造、执行、部署 中点开 CMake,点 + 添加,在 CMake 选项中输入之前的命令
cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang",
确认后发现多了两个文件夹 cmake-build-debug 和 cmake-build-release,然后 ninja -j8 编译,但是我这里好像是因为当前执行命令的目录路径 (/home/eleven/…) 和它在 CMakeCache.txt 文件中记录的路径 (//wsl.localhost/Ubuntu/home/eleven/…) 不一致,编译失败了,然后就删了重新构建一次

1
2
3
4
cd ~/llvm-project-llvmorg-20.1.8/llvm/cmake-build-release
rm -rf CMakeCache.txt CMakeFiles/
cmake .. -G Ninja -DCMAKE_BUILD_TYPE=Release
ninja -j8

LLVM tool

https://llvm.org/docs/GettingStarted.html#llvm-tools
先编写一个简单的 c 语言脚本

1
2
3
4
5
6
7
// ez.c
#include <stdio.h>

int main() {
printf("Hello, LLVM!\n");
return 0;
}

用 clang 编译
clang ez.c -o ez

clang 默认方式与 GCC 相同,标准 -S 和 -c 参数分别生成 .s 和 .o 文件

把 c 源代码编译成 LLVM IR
clang -emit-llvm -S ez.c -o ez.ll

-emit-llvm 告诉编译器不要生成机器码(如 .o 或可执行文件),而是生成 LLVM IR
-S 告诉编译器,生成的 LLVM IR 应该以文本格式输出,而不是默认的二进制字节码格式
.ll 文件是人类可读的文本文件

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
; ModuleID = 'ez.c'
source_filename = "ez.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@.str = private unnamed_addr constant [14 x i8] c"Hello, LLVM!\0A\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @main() #0 {
%1 = alloca i32, align 4
store i32 0, ptr %1, align 4
%2 = call i32 (ptr, ...) @printf(ptr noundef @.str)
ret i32 0
}

declare i32 @printf(ptr noundef, ...) #1

attributes #0 = { noinline nounwind optnone uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
attributes #1 = { "frame-pointer"="all" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }

!llvm.module.flags = !{!0, !1, !2, !3, !4}
!llvm.ident = !{!5}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{i32 8, !"PIC Level", i32 2}
!2 = !{i32 7, !"PIE Level", i32 2}
!3 = !{i32 7, !"uwtable", i32 2}
!4 = !{i32 7, !"frame-pointer", i32 2}
!5 = !{!"Ubuntu clang version 18.1.3 (1ubuntu1)"}

执行 .ll 文件 : lli ez.ll

lli 是 LLVM Interpreter(LLVM 解释器) 的缩写,是一个命令行工具,能够直接执行 LLVM IR代码

把 c 源码编译成 Bitcode
clang -O3 -emit-llvm ez.c -c -o ez.bc

-O 表示优化,3表示这是最高级别的优化
-emit-llvm 作用同上,告诉编译器生成 LLVM IR
-c 告诉编译器只进行编译,不进行链接,即生成一个目标文件而不是可执行文件

.bc 文件是 LLVM 字节码(Bitcode)的缩写,它与之前的 .ll 文件都属于 LLVM IR,但是

.ll 文件是 LLVM IR 的文本格式,人类可读,并且可以使用文本编译器查看内容
.bc 文件是 LLVM IR 的二进制格式,人类不可读,但文件体积更小,加载和处理速度更快,更适合作为编译器后端的输入

llvm 中提供了 llvm-as (汇编器) 和 llvm -dis (反汇编器),让两种形式的文件能相互转化
把 .ll 文件转化为 .bc 文件
llvm-as ez.ll -o ez.bc
把 .bc 文件转化为 .ll 文件
llvm-dis ez.bc -o ez.ll

llvm-dis < ez.bc | less

llvm-dis 是 LLVM 的反汇编工具,用于将 .bc 文件转换为 .ll 文件
< 是一个重定向操作符,告诉 llvm-dis 命令,从标准输入(stdin)而不是从命令行参数中读取数据
| 是 管道 符号,它将左边命令的标准输出(stdout)作为右边命令的标准输入
less 是一个分页查看器,能够让用户逐页查看文本内容

把 llvm 字节码文件转化为汇编
llc ez.bc -o ez.s

llc 是 LLVM Static Compiler,即LLVM 编译流程中的后端代码生成器

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
	.text
.file "ez.c"
.globl main # -- Begin function main
.p2align 4, 0x90
.type main,@function
main: # @main
.cfi_startproc
# %bb.0:
pushq %rax
.cfi_def_cfa_offset 16
movl $.Lstr, %edi
callq puts@PLT
xorl %eax, %eax
popq %rcx
.cfi_def_cfa_offset 8
retq
.Lfunc_end0:
.size main, .Lfunc_end0-main
.cfi_endproc
# -- End function
.type .Lstr,@object # @str
.section .rodata.str1.1,"aMS",@progbits,1
.Lstr:
.asciz "Hello, LLVM!"
.size .Lstr, 13

.ident "Ubuntu clang version 18.1.3 (1ubuntu1)"
.section ".note.GNU-stack","",@progbits

把汇编转化成可执行文件
clang ez.s -o ez_asm
我直接用这个失败了,因为汇编代码使用了绝对地址引用(movl $.Lstr, %edi),与 PIE 不兼容
禁用pie:
clang -no-pie ez.s -o ez_asm

使用 opt 来执行 pass

在典型的编译流程中,源代码首先被编译成 LLVM IR,然后 opt 会介入,对这个 IR 进行一系列的分析和转换,优化代码
opt --help 获取 LLVM 中可用程序转换列表

LLVM Pass

LLVM Pass 框架是 LLVM 系统的一个重要组成部分,Pass 执行构成编译器的各种转换和优化

Pass 的核心特征:

  • 模块化:每个 Pass 都被设计成一个独立的、自包含的单元,它只专注于完成一个特定的任务
  • 可组合:LLVM 允许通过 Pass 管理器将多个 Pass 串联起来,形成一个完整的优化管道,开发者可以根据需求自由组合不同的 Pass
  • 统一接口:与旧版 Pass 管理器中通过继承定义 Pass 接口不同,新版 Pass 管理器中的 Pass 都遵循一个基于概念的多态接口

Pass 的分类

  • FunctionPass:在函数上运行,这是最常见的 Pass 类型,通常用于函数内部的优化或分析。
  • ModulePass:在整个模块(通常是一个源文件)上运行,它适用于需要在函数间进行分析或优化的场景
  • LoopPass:在每个循环上运行

根据官方文档具体了解 pass
https://llvm.org/docs/WritingAnLLVMNewPMPass.html

基于源码的 LLVM Pass

上文已经为新 pass 构建好了环境
以 llvm-project 目录下创建好了的这个为例 llvm/lib/Transforms/Utils/HelloWorld.cpp

首先需要在一个头文件中定义这个 pass,创建文件 llvm/include/llvm/Transforms/Utils/HelloWorld.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
#define LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H

#include "llvm/IR/PassManager.h"

namespace llvm {

class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {
public:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);
};

} // namespace llvm

#endif // LLVM_TRANSFORMS_HELLONEW_HELLOWORLD_H
1
class HelloWorldPass : public PassInfoMixin<HelloWorldPass> {

定义了一个 HelloWorldPass 类,它继承自 PassInfoMixin,这是 LLVM 新版 Pass 管理器的一个基类
PassInfoMixin 是一个 CRTP(奇异递归模板模式)模板,提供了 Pass 的基本信息和接口

1
2
public:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM);

run 方法是 Pass 的核心执行函数
Function &F:当前正在处理的函数对象的引用
FunctionAnalysisManager &AM:函数分析管理器,用于获取和管理分析结果
PreservedAnalyses:返回值,告诉 Pass 管理器哪些分析结果在此 Pass 执行后仍然有效

然后看llvm/lib/Transforms/Utils/HelloWorld.cpp

1
2
3
4
5
6
7
8
9
10
#include "llvm/Transforms/Utils/HelloWorld.h"
#include "llvm/IR/Function.h"

using namespace llvm;

PreservedAnalyses HelloWorldPass::run(Function &F,
FunctionAnalysisManager &AM) {
errs() << F.getName() << "\n";
return PreservedAnalyses::all();
}

头文件中包含了刚刚定义的 HelloWorld.h 以及 LLVM 的 Function 类定义,提供函数相关的 API

pass 的核心逻辑

1
errs() << F.getName() << "\n";

errs():LLVM 的错误输出流
F.getName():获取当前函数的名称
整体作用:将函数名打印到标准错误输出

1
return PreservedAnalyses::all();

告诉 Pass 管理器所有的分析结果都被保留

以上就是 Pass 本身的代码,接下来需要在几个地方添加一些内容来注册这个 pass

llvm/lib/Passes/PassRegistry.def 文件的 FUNCTION_PASS 部分添加

1
FUNCTION_PASS("helloworld", HelloWorldPass())

让这个 pass 名为 “helloworld”

llvm/lib/Passes/PassBuilder.cpp 中添加正确的 #include:

1
#include "llvm/Transforms/Utils/HelloWorld.h"

然后就可以编译运行了,但是我按照这样操作之后再 ninja 编译时出错了,大概意思是刚刚的 HelloWorld.cpp 中缺少头文件
添加了下面这一行之后就能顺利编译了

1
#include "llvm/Passes/PassBuilder.h"

build 目录下执行 ninja
回显

1
[59/59] Creating library symlink lib/libclang-cpp.so

使用 opt 来运行一个 pass

1
2
3
opt -passes=helloworld ez.ll -o /dev/null

#main

-passes=helloworld: 告诉 opt 运行一个名为 helloworld 的 Pass
-o /dev/null: 将 opt 的输出结果重定向到空设备,不保存修改后的 IR 文件

以上是旧的注册方法,LLVM 还提供了一种新的注册方式 –以 插件 的形式注册 Pass
首先,移除之前按照旧方法而做的修改
然后在 llvm/lib/Transforms/Utils/HelloWorld.cpp 中修改如下:

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
#include "llvm/Transforms/Utils/HelloWorld.h"
#include "llvm/IR/Function.h"
#include "llvm/Passes/PassPlugin.h" //关键
#include "llvm/Passes/PassBuilder.h"

using namespace llvm;

PreservedAnalyses HelloWorldPass::run(Function &F,
FunctionAnalysisManager &AM) {
errs() << "Hello from HelloWorldPass: " << F.getName() << "\n";
return PreservedAnalyses::all();
}

// 这是 Pass 插件的注册入口点
extern "C" LLVM_ATTRIBUTE_WEAK PassPluginLibraryInfo llvmGetPassPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "HelloWorldPass", "v1.0",
[](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "hello-world") { //hello-world 即为定义的 pass 的名字
FPM.addPass(HelloWorldPass());
return true;
}
return false;
});
}};
}

工作流程:

用户运行:opt -passes=hello-world input.ll
LLVM 加载插件并调用 llvmGetPassPluginInfo()
插件注册解析回调到 PassBuilder
PassBuilder 解析 “hello-world” 名称
回调函数将 HelloWorldPass 添加到管道
Pass 管理器执行 HelloWorldPass::run() 方法
输出函数名并完成处理

接下来需要修改 CMakeLists.txt 文件,添加插件的构建配置
llvm/lib/Transforms/Utils/CMakeLists.txt 中添加 Passes 和 PARTIAL_SOURCES_INTENDED

1
2
3
4
5
6
7
8
9
# 已有代码...
LINK_COMPONENTS
Analysis
Core
Support
TargetParser
Passes # <- 链接Passes组件库
PARTIAL_SOURCES_INTENDED # <- 允许部分源文件不在此目标中
)

然后进入 build 目录,重新运行 cmake,让 Cmake 读取修改后的 CMakeLists.txt;再执行 ninja

1
2
3
cd /home/eleven/llvm-project-llvmorg-20.1.8/build
cmake -G Ninja ../llvm
ninja

自己编写 LLVM Pass

先安装 LLVM 的软件包,我这里安装的是 LLVM 19

1
sudo apt install llvm-19

创建一个新的目录 myllvm
Function Pass

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
//Hello.cpp
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"

using namespace llvm;

struct HelloPass : public PassInfoMixin<HelloPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM) {
errs() << "Hello from LLVM 19: " << F.getName() << "\n";
return PreservedAnalyses::all();
}

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

extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "HelloPass",
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "hello") {
FPM.addPass(HelloPass{});
return true;
}
return false;
});
}
};
}

一些注释

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
#include "llvm/Passes/PassBuilder.h"  //引入 PassBuilder 类,它是新 PM 的核心组件
#include "llvm/Passes/PassPlugin.h" //编写 LLVM 插件的必备头文件
#include "llvm/Support/row_ostream.h" //引入了输出流库

using namespace llvm;
struct HelloPass : public PassInfoMixin<HelloPass>
{
// 定义了一个名为 HelloPass 的结构体,继承自 PassInfoMixin ,能自动提供一些基础功能
PreservedAnalyses run(Function &F, FunctionAnalysisManager &AM)
{
// Pass 核心函数
errs() << "Hello from LLVM 19:" << F.getName() << "\n";
// F.getName() 获取当前函数的名称,并输出到错误流
return PreservedAnalyses::all();
}
static bool isRequired() { return true; }
// 表明这个函数是必需的,无论优化级别如何都不会被跳过
};

// 插件注册
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
// LLVM_ATTRIBUTE_WEAK 标记该函数为一个“弱符号”,在链接时允许多个库定义同名的函数而不会产生冲突
llvmGetPassPluginInfo()
{
// LLVM 插件的入口函数
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "HelloPass", // 插件名称
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB)
{
PB.registerPipelineParstringCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>)
{
if (Name == "hello")
{ // passname
FPM.addPass(HelloPass{});
return true; // 如果名称匹配,添加 HelloPass 到 FunctionPassManager
}
return false;
});
}};
}

编译 LLVM Pass

1
clang++-18 -shared -fPIC `llvm-config-19 --cxxflags` -o HelloPass.so Hello.cpp

clang++ 是 C++ 编译器,18是 clang 版本
-shared 告诉编译器和连接器创建一个共享库(动态链接库)
-fPIC Position-Independent Code,位置无关代码,创建共享库是个标志这是必需的
`llvm-config-19 –cxxflags` 确保编译器可以找到LLVM pass所依赖的所有头文件

前半部分查询 LLVM 配置信息,19 是LLVM 版本
后半部分的–cxxflags 参数让 llvm-config-19 输出编译C++代码时需要的标志
-o HelloPass.so 指定生成共享库的名字

准备一个测试文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//test1.c
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
return 0;
}
int add(int a, int b)
{
return a + b;
}

int subtract(int a, int b)
{
return a - b;
}

编译成 .ll 文件

1
clang -emit-llvm -S test1.c -o test1.ll

用 opt 执行 Pass

1
opt -load-pass-plugin=./HelloPass.so -passes="hello" ./test1.ll -disable-output

HelloPass.so 是共享库文件 hello 是 pass 名称
在默认情况下,opt 会将经过pass处理后的LLVM IR输出到标准输出,-disable-output 能禁止该行为
输出

1
2
3
Hello from LLVM 19: main
Hello from LLVM 19: add
Hello from LLVM 19: subtract

混淆–自实现 OLLVM

符号混淆

写了一个实现混淆函数名的 pass,把自定义的函数名都变成随机生成的名字

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//obf.cpp
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
#include <random>
#include <string>
#include <set>

using namespace llvm;

struct SymbolReplacePass : public PassInfoMixin<SymbolReplacePass>
{
private:
std::string generateRandomName()
{
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> dis(0, 25);

std::string name;
for (int i = 0; i < 8; ++i)
{
name += ('a' + dis(gen));
}
return name;
}

bool shouldObf(const Function &F)
{
if (F.isDeclaration())
return false;

StringRef name = F.getName();

std::set<std::string> preservedNames = {
"main",
"_start",
"__libc_start_main",
"printf",
"scanf",
"malloc",
"free",
"exit"};

if (name.starts_with("_") && name != "main")
return false;

for (const auto &preserved : preservedNames)
{
if (name == preserved)
return false;
}
return true;
}

public:
PreservedAnalyses run(Module &M, ModuleAnalysisManager &)
{
bool modified = false;
std::vector<Function *> functionsToObf;
for (Function &F : M)
{
if (shouldObf(F))
{
functionsToObf.push_back(&F);
}
}

for (Function *F : functionsToObf)
{
std::string origName = F->getName().str();
std::string newName = generateRandomName();

F->setName(newName);
errs() << "[SymbolReplace] obfuscated : " << origName << "->" << newName << "\n";
modified = true;
}

if (modified)
{
errs() << "Total functions obfuscated:" << functionsToObf.size() << "\n";
}

return modified ? PreservedAnalyses::none() : PreservedAnalyses::all();
}
};

extern "C" LLVM_ATTRIBUTE_WEAK::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo()
{
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "SymbolReplacePass",
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB)
{
PB.registerPipelineParsingCallback(
[](StringRef Name, ModulePassManager &MPM,
ArrayRef<PassBuilder::PipelineElement>)
{
if (Name == "symbol_replace")
{
MPM.addPass(SymbolReplacePass());
return true;
}
return false;
});
}

};
}

准备的测试文件

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
//test2.c
# include <stdio.h>
int func1(int a, int b)
{
return a + b;
}

int func2(int a, int b)
{
return a - b;
}

int func3(int a, int b)
{
return a * b;
}

int main(){
func1(7,11);
func2(7,11);
func3(7,11);
printf("func1: %d\n", func1(7, 11));
printf("func2: %d\n", func2(7, 11));
printf("func3: %d\n", func3(7, 11));
return 0;
}

编译插件
clang++-18 -shared -fPICllvm-config-19 –cxxflags -o SymbolReplacePass.so obf.cpp

生成 .ll 文件
clang -emit-llvm -S test2.c -o test2.ll

进行全函数混淆
opt -load-pass-plugin=./SymbolReplacePass.so -passes=symbol_replace test2.ll -S -o afterObf_test2.ll
可以看到输出

1
2
3
4
[SymbolReplace] obfuscated : func1->sgwvbkay
[SymbolReplace] obfuscated : func2->lwhtbjkx
[SymbolReplace] obfuscated : func3->cfjkyqtx
Total functions obfuscated:3

编译最终程序
clang afterObf_test2.ll -o afterObf_test2

用 ida 查看混淆前后的程序,对比

混淆前:

混淆后:

指令替换

自实现指令混淆的 pass

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//sub.cpp
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Instructions.h"
#include "llvm/Support/raw_ostream.h"
#include <random>
#include <vector>

using namespace llvm;

struct InstructionSubstitutionPass : public PassInfoMixin<InstructionSubstitutionPass>
{
private:
std::mt19937 rng;

int getRandomInt(int max)
{
std::uniform_int_distribution<int> dist(0, max - 1);
return dist(rng);
}

// a + b => (a ^ b) + 2 * (a & b)
void substituteAdd(BinaryOperator *addInst)
{
IRBuilder<> builder(addInst);
Value *op1 = addInst->getOperand(0);
Value *op2 = addInst->getOperand(1);

//虚假计算
Value *dummy = builder.CreateMul(op1, ConstantInt::get(op1->getType(), 1), "dummy");

// a ^ b
Value *xorResult = builder.CreateXor(op1, op2, "xor_tmp");
// a & b
Value *andResult = builder.CreateAnd(op1, op2, "and_tmp");
// 2 * ( a & b)
Value *shiftResult = builder.CreateShl(andResult, 1, "shift_tmp");
//(a + b) + 2 * (a & b)
Value *addResult = builder.CreateAdd(xorResult, shiftResult, "add_subst");

//异或 0 的无用操作
Value *finalResult = builder.CreateXor(addResult, ConstantInt::get(op1->getType(), 0), "add_subst");

addInst->replaceAllUsesWith(finalResult);
errs() << "[InstSubst] Replaced ADD instruction\n";
}

// a - b => a + ()
void substituteSub(BinaryOperator *subInst)
{
IRBuilder<> builder(subInst);
Value *op1 = subInst->getOperand(0);
Value *op2 = subInst->getOperand(1);

//~b
Value *notOp2 = builder.CreateNot(op2, "not_tmp");
//~b + 1
Value *negOp2 = builder.CreateAdd(notOp2,
ConstantInt::get(op2->getType(), 1), "neg_tmp");
// a + (~b + 1)
Value *finalResult = builder.CreateAdd(op1, negOp2, "final_tmp");

subInst->replaceAllUsesWith(finalResult);
errs() << "[InstSubst] Replaced SUB instruction\n";
}

// a ^ b => (a | b) - (a & b)
void substituteXor(BinaryOperator *xorInst)
{
IRBuilder<> builder(xorInst);
Value *op1 = xorInst->getOperand(0);
Value *op2 = xorInst->getOperand(1);

// a | b
Value *orResult = builder.CreateOr(op1, op2, "or_tmp");
// a & b
Value *andResult = builder.CreateAnd(op1, op2, "and_tmp");
// (a | b) - (a & b)
Value *finalResult = builder.CreateSub(orResult, andResult, "final_tmp");

xorInst->replaceAllUsesWith(finalResult);
errs() << "[InstSubst] Replaced XOR instruction\n";
}

// a * b =>
void substituteMul(BinaryOperator *mulInst)
{
if (!isa<ConstantInt>(mulInst->getOperand(1)))
{
return;
}
ConstantInt *constOp = cast<ConstantInt>(mulInst->getOperand(1));
uint64_t multiplier = constOp->getZExtValue();

if (multiplier > 0 && (multiplier & (multiplier - 1)) == 0)
{
IRBuilder<> builder(mulInst);
Value *op1 = mulInst->getOperand(0);

unsigned shiftAmount = 0;
uint64_t temp = multiplier;
while (temp > 1)
{
temp >>= 1;
shiftAmount++;
}

Value *shiftResult = builder.CreateShl(op1, shiftAmount, "mul_subst");
mulInst->replaceAllUsesWith(shiftResult);
errs() << "[InstSubst] Replaced MUL instruction\n";
}
}

// cmp 混淆
void substituteICmp(ICmpInst *cmpInst)
{
IRBuilder<> builder(cmpInst);
Value *op1 = cmpInst->getOperand(0);
Value *op2 = cmpInst->getOperand(1);

ICmpInst::Predicate pred = cmpInst->getPredicate();

// a == b => (a ^ b) == 0
if (pred == ICmpInst::ICMP_EQ)
{
Value *xorResult = builder.CreateXor(op1, op2, "xor_tmp");
Value *zero = ConstantInt::get(op1->getType(), 0);
Value *newCmp = builder.CreateICmpEQ(xorResult, zero, "eq_subst");

cmpInst->replaceAllUsesWith(newCmp);
errs() << "[InstSubst] Replace ICMP_EQ instruction\n";
}
}

public:
InstructionSubstitutionPass() : rng(std::random_device{}()) {}

PreservedAnalyses run(Function &F, FunctionAnalysisManager &)
{
bool modified = false;
std::vector<Instruction *> toReplace;

for (BasicBlock &BB : F)
{
for (Instruction &I : BB)
{
if (auto *binOp = dyn_cast<BinaryOperator>(&I))
{
switch (binOp->getOpcode())
{
case Instruction::Add:
case Instruction::Sub:
case Instruction::Xor:
case Instruction::Mul:
toReplace.push_back(&I);
break;
default:
break;
}
}
else if (auto *cmpInst = dyn_cast<ICmpInst>(&I))
{
toReplace.push_back(&I);
}
}
}

for (Instruction *inst : toReplace)
{
if (auto *binOp = dyn_cast<BinaryOperator>(inst))
{
switch (binOp->getOpcode())
{
case Instruction::Add:
substituteAdd(binOp);
modified = true;
break;
case Instruction::Sub:
substituteSub(binOp);
modified = true;
break;
case Instruction::Xor:
substituteXor(binOp);
modified = true;
break;
case Instruction::Mul:
substituteMul(binOp);
modified = true;
break;
default:
break;
}
}
else if (auto *cmpInst = dyn_cast<ICmpInst>(inst))
{
substituteICmp(cmpInst);
modified = true;
}
}

for (Instruction *inst : toReplace)
{
if (inst->use_empty())
{
inst->eraseFromParent();
}
}

if (modified)
{
errs() << "[InstSubst] Modified function: " << F.getName() << "\n";
}

return modified ? PreservedAnalyses::none() : PreservedAnalyses::all();
}
};

extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo()
{
return {
.APIVersion = LLVM_PLUGIN_API_VERSION,
.PluginName = "InstructionSubstitutionPass",
.PluginVersion = "v0.1",
.RegisterPassBuilderCallbacks = [](PassBuilder &PB)
{
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>)
{
if (Name == "instruction_sub")
{
FPM.addPass(InstructionSubstitutionPass());
return true;
}
return false;
});
}};
}

测试文件

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
//test3.c
#include <stdio.h>

int calc(int a, int b)
{
int c = 18;
int res = 0;
int sum = a + b;
int diff = a - b;
int xor_res = diff ^ b;
int mul_res = xor_res * 4;

if (mul_res == c)
{
return 1;
}
else
{
return 0;
}
}

void instsub()
{
int x = 10;
int y = 20;

int res = x + y;
res = res - 5;
res = res ^ 15;
res = res * 2;
printf("Result: %d\n", res);
if (res == 50)
{
printf("right\n");
}
else
{
printf("wrong\n");
}
}

int main()
{
printf("Hello instruction substitute!\n");
printf("calc result: %d\n", calc(5, 3));
instsub();
}

编译 LLVM Pass 插件
clang++-18 -shared -fPIC llvm-config-19 –cxxflags -o InstructionSubstitutionPass.so sub.cpp

将 C 源码编译为 LLVM IR
clang -O0 -emit-llvm -c test3.c -o test3.ll

运行自定义 Pass 对 IR 进行转换
opt -load-pass-plugin=./InstructionSubstitutionPass.so -passes=instruction_sub test3.ll -S -o afterSub_test3.ll

将混淆后的 IR 编译为可执行文件
clang afterSub_test3.ll -o afterSub_test3


llvm 浅探
http://example.com/2025/08/01/llvm/
作者
Eleven
发布于
2025年8月1日
许可协议