通过LLVM简单学习一下指令混淆。

注册Pass

添加文件

以LLVMHello项目为起点,在上面做改动,首先找到 llvm-project/llvm/lib/transforms/hello 文件夹,添加一个头文件和一个 CPP 源文件,并向 Cmakelists 添加 cpp 源文件,重新生成就可以发现源文件出现在了项目中。

我们让 Hello.cpp 仅仅注册 pass 即可,要写新的 pass 最好加文件,看起来条理清晰。

pass定义

1
2
3
4
5
6
7
8
9
10
11
12
#include "llvm/IR/PassManager.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/IRBuilder.h"

namespace llvm {
class XPass : public PassInfoMixin<XPass> {
public:
PreservedAnalyses run(Module &m, ModuleAnalysisManager &AM);
static bool isRequired() { return true; }
};
} // namespace llvm

如上是一个 pass 的基本定义,使用类继承 PassInfoMixin<XPass>,实现两个 public 的方法,分别是 run 和 isRequired。曾经的 runOnfunction 似乎已经不能被注册了(有可能是我的问题),不过我们依然可以用一些方法实现类似的 runOnfunction。

pass注册

在 hello.cpp 中添加如下代码,上篇文章写到过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>

#include "Xpass.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;

void myCallback(llvm::ModulePassManager &PM,
OptimizationLevel Level) {
PM.addPass(XPass());
}
extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_WEAK
llvmGetPassPluginInfo() {
return {
LLVM_PLUGIN_API_VERSION, "XPass", LLVM_VERSION_STRING,
[](PassBuilder &PB) { PB.registerPipelineStartEPCallback(myCallback);}//仅注册 clang 的回调
};
}

实现Pass

实现 Pass 主要是对 run 函数的实现,这里每个pass都会遍历一遍模块,我们可以把模块(Module)类比成文件,每个文件或多或少会定义一些函数(Function),每个函数又会由若干个基本块(BasicBlock)构成,每个基本快又由若干个指令(Instruction)构成。

模块

是 LLVM pass 处理的应该算是最大的一个单元了,run 函数的每个参数就是一个模块,一个模块可以理解成一个 C 语言源文件。通过一个接口可以获取模块中定义的函数 auto &list=m.getFunctionList()

然后直接用强for循环就可以遍历函数。

1
2
3
4
5
6
7
8
9
PreservedAnalyses XPass::run(Module &m, ModuleAnalysisManager &AM) {
errs() << "[Xpass] Load Successful" << "\n";
auto &list= m.getFunctionList();
for (auto &f : list) {
std::string s= (std::string)f.getName();
errs()<<"Function found:" << s << "\n";
}
return PreservedAnalyses::all();
}

函数

前面我们看到了,可以调用 getName 方法获取函数名,这里我们可以仅仅对 main 函数进行操作,就进行一下字符比较,然后遍历main的基本块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PreservedAnalyses XPass::run(Module &m, ModuleAnalysisManager &AM) {
int times = 100;
errs() << "[Xpass] Load Successful" << "\n";
G.randomize();
//Sleep(10000);
auto &list= m.getFunctionList();
for (auto &f : list) {
std::string s= (std::string)f.getName();
if (!s.compare("main")) {
for (llvm::BasicBlock& BB : f){

}
}
}
return PreservedAnalyses::all();
}

基本块

基本块中,可以继续遍历其中的指令,在基本的指令替换中,我们不需要考虑基本块和函数,我们仅仅考虑指令即可。遍历指令,并判断加法指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PreservedAnalyses XPass::run(Module &m, ModuleAnalysisManager &AM) {
errs() << "[Xpass] Load Successful" << "\n";
auto &list= m.getFunctionList();
for (auto &f : list) {
std::string s= (std::string)f.getName();
if (!s.compare("main")) {
for (llvm::BasicBlock& BB : f) {
for (llvm::Instruction& I : BB) {
if (I.isBinaryOp()) {//二元运算符判断
switch (I.getOpcode()) {
case BinaryOperator::Add://加法
errs() << "add operator found" << "\n";
}
}
}
}

}
}
return PreservedAnalyses::all();
}

写一个C语言测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>

int main(){
printf("add test\n");
int x=5;
int y=4;
int k=x+y;
y=k+x;
x=k+y;
k=x+y;
printf("%d\n",k);
}

结果:

指令

那么下面我们主要讲对指令的操作,将加法指令进行简单的混淆。原理十分简单:

1
2
3
4
5
6
7
//混淆前
int c=a+b;
//混淆后
int r=rand();
int c=a+r;
c+=b;
c-=r;

就是生成一个随机数,然后先加,再减。

先写一个随机数生成器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Random {
public:
void randomize() {
srand(time(NULL));
srand(GetInt32());
}
char GetInt8() {
return rand() & 0xFF;
}
short GetInt16() {
return rand();
}
int GetInt32() {
return 1ll * rand() << 16 | rand();
}
long long GetInt64() {
return 1ll * GetInt32() << 32 | GetInt32();
}
}G;

通过 IRBuilder 进行指令构造和替换,不用Create是因为@Qfrost师傅告诉我 Create 有被淘汰的趋势。

这里识别到 add 指令之后直接传指令指针进来,使用方法 getOperand 获取操作数,0 为第一个,1为第二个,前面我们已经判断是二元操作符且是 add 指令,所以直接获取就好。

用下面的函数处理:

1
2
3
4
5
6
7
8
9
10
11
void addRand(Instruction *bo) {
IRBuilder<> Builder(bo);
if (bo->getOpcode() == Instruction::Add) {
Type *ty = bo->getType();
ConstantInt *co = (ConstantInt *)ConstantInt::get(ty, G.GetInt64());//co等同于随机数r
Value *op1 = Builder.CreateAdd(bo->getOperand(0), co); //a+r
Value *op2 = Builder.CreateAdd(op1, bo->getOperand(1)); //a+r+b
Value *result = Builder.CreateSub(op2, co); //a+r+b-r
bo->replaceAllUsesWith(result);
}
}

处理函数如下:

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
PreservedAnalyses XPass::run(Module &m, ModuleAnalysisManager &AM) {
int times = 100;
errs() << "[Xpass] Load Successful" << "\n";
G.randomize();
//Sleep(10000);
auto &list= m.getFunctionList();
for (auto &f : list) {
std::string s= (std::string)f.getName();
if (!s.compare("main")) {
for (llvm::BasicBlock& BB : f) {
for (llvm::Instruction& I : BB) {
//errs() << I << "\n";
if (I.isBinaryOp()) {//二元运算符判断
switch (I.getOpcode()) {
case BinaryOperator::Add://加法
errs() << "add operator found" << "\n";
addRand(&I);
times--;
if (times == 0)goto end;
}
}
}
}

}
}
end:
return PreservedAnalyses::all();
}

我们来看看混淆结果:

可以发现混淆成功了。

下面预计学习一下 OLLVM 的指令替换的其它混淆并写出对应的方法。

参考文章

  1. LLVM与代码混淆