从零开始的LLVM-pass (一) 环境搭建和第一个demo

为什么有这篇文章

前段时间看了点LLVM的博客,学的非常痛苦,所以打算写一篇文章记录一下基本的框架搭建过程,省的一段时间后又忘了

Demo

实现一个 FunctionPass ,遍历所有函数,如果函数不是main函数就修改混淆函数的名字

关于环境

开发环境是win,至于为什么不选linux,主要是没有物理机实在不方便,后续如果被win恶心到了可能会迁移到linux

win-gnu-llvm下载

非常神奇的找到了兼容win-gnu ABI的llvm工具链,试了下能跑,索性先这样
g++用的是MinGW,网上随便下一个新一点的都行

直接下载完就是编译完的二进制文件,把bin加到环境目录就能识别clang和opt了

框架搭建

目录结构如图所示

build是Cmake的输出路径,最终编译好的pass就存在里面
test里是测试文件,用来测试pass的混淆效果
transforms里是pass的源码

/transforms/CMakelists.txt

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
# /transforms/CMakelists.txt
cmake_minimum_required(VERSION 3.13)
project(MyPass)
set(CMAKE_C_COMPILER "gcc")
set(CMAKE_CXX_COMPILER "g++")
set(LLVM_DIR "C://llvm-19.1.6-1/lib/cmake/llvm")
find_package(LLVM REQUIRED CONFIG)


include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
add_definitions(${LLVM_DEFINITIONS})


add_library(MyPass MODULE MyPass.cpp
)

target_link_libraries(MyPass
LLVMCore
LLVMSupport
LLVMIRReader
LLVMPasses
LLVMAnalysis
LLVMTransformUtils
)

CMakelist如上设置,要手动导入LLVM的cmake路径,然后中间这些宏都是LLVM的.cmake文件里自带的,直接抄就行
之后就和正常cmake项目一样,设置链接库源文件,输出和依赖

test.sh

1
2
3
4
5
6
7
8
9
10
11
12
# test.sh
cd ./build
cmake -G "Ninja" ../transforms
cmake --build .
cd ../test
g++ test.cpp -o beforeLLVM_test
clang++ -S -emit-llvm test.cpp -o test.ll
opt -load-pass-plugin=../build/libMyPass.dll -passes=encode-func -S test.ll -o test.out.ll
llc test.out.ll -filetype=obj -o test.o
g++ test.o -o test
./test
cd ..

使用g++编译一份未加pass的二进制文件方便以后对比,使用clang++配合-S -emit-llvm参数输出llvm-IR文件,这是llvm的中间语言文件,之后pass所有的处理都在该文件上进行
-load-pass-plugin=${filePath} 是opt新版的api,我们生成的是类似于插件库的dll,之后还要加上 -passes={passName} 指定具体用哪个pass,之后讲如何注册pas时会具体讲这个passName是怎么来的

处理好后用llc把中间文件编译成目标文件,再用g++把中间文件编译成可执行文件就完事了

MyPass.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
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
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
#include <string>
using namespace llvm;
namespace
{
class EncodeFunctionName : public PassInfoMixin<EncodeFunctionName>
{
private:
static int functionCnt;

public:
PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM)
{
if (F.getName() != "main")
{
errs() << "Old name: " << F.getName() << "\n";
F.setName("114514func" + std::to_string(++functionCnt));
errs() << "New name: " << F.getName() << "\n";
}
else
{
errs() << "function is " << F.getName() << "\n";
}
return PreservedAnalyses::all();
}

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

int EncodeFunctionName::functionCnt = 0;

extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo()
{
return {
LLVM_PLUGIN_API_VERSION,
"encode-func",
LLVM_VERSION_STRING,
[](PassBuilder &PB)
{
errs() << "\n=== Registering EncodeFunctionName Pass ===\n";
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>)
{
if (Name == "encode-func")
{
errs() << "Adding EncodeFunctionName pass to manager\n";
FPM.addPass(EncodeFunctionName());
return true;
}
return false;
});
}};
}

llvm的所有实现都定义在llvm空间中,因为是个demo所以干脆直接using namespace llvm
要实现一个自己的pass,我们要从 PassInfoMixin<> 这个基类模板继承,这是LLVM的新版API,区别于旧版的是我们不用指定pass的类型,而是依靠下面的 run 方法的实现区分pass类型,我们要实现一个functionPAss,所以 run 的参数就是 llvm:Functionllvm:FunctionAnalysisManager

run 是pass中最关键的方法,一个pass所有的业务都是在run中完成的,这是一个回调,会对所有的函数执行,我们直接用getName获取名字,然后用setName重设名字就行,返回 PreservedAnalyses::all() 表示这个pass不会对其他任何pass产生影响,反正我们也只跑这一个pass,返回all即可

isRequired() 编译器可能会跳过我们的pass,因为实际上pass没做优化,所以要实现 isRequired 返回ture强制要求编译器执行我们的pass

extern “C” LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo llvmGetPassPluginInfo()
这是新版LLVM注册pass的惯用约定,这部分基本没什么好改的,返回一个四元组 {LLVM插件API版本号,插件名,插件版本号,注册回调函数} ,其中插件名就是我们用-pass时传递的名字,opt会解析这个名字并并调用相关回调,插件版本号随便写就行

回调会传入一个PassBuilder,我们往里面注册一个解析回调,每次opt解析我们的命令时都会执行这个回调,这个回调的格式也基本是固定的,最重要的是 Name ,这是解析得到的 passName ,我们调用 FPM.FPM.addPass(EncodeFunctionName()) 来完成注册

test.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
// test.cpp
#include <cstdio>
#include <cstring>
#include <iostream>

void testFunctionA()
{
std::cout << "testA";
}
void testFunctionB()
{
std::cout << "testB";
}
void testFunctionC()
{
std::cout << "testC";
}
int main()
{
testFunctionA();
testFunctionB();
testFunctionC();
return 0;
}

我们声明了三个函数,预期这三个函数的名字都会被改成114514funcxxx

输出

根据pass输出的调试信息可以发现opt按照从上到下的顺序对每个函数执行了pass

然后再打开ida看看二进制文件是否真的被修改了



可以看到确实被修改了

下一篇文章可能会写一下怎么修改基本块和怎么混淆运算符

从零开始的LLVM-pass (一) 环境搭建和第一个demo

https://sgsgsama.github.io/ctf/llvm/llvm0x1/

Author

SGSG

Posted on

2025-03-06

Updated on

2025-04-18

Licensed under