通过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; } }; }
|
如上是一个 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);} }; }
|
实现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(); 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()); Value *op1 = Builder.CreateAdd(bo->getOperand(0), co); Value *op2 = Builder.CreateAdd(op1, bo->getOperand(1)); Value *result = Builder.CreateSub(op2, co); 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(); 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"; addRand(&I); times--; if (times == 0)goto end; } } } }
} } end: return PreservedAnalyses::all(); }
|
我们来看看混淆结果:
可以发现混淆成功了。
下面预计学习一下 OLLVM 的指令替换的其它混淆并写出对应的方法。
参考文章
- LLVM与代码混淆