为什么有这篇文章 前段时间看了点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的源码
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 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 cd  ./build  cmake -G "Ninja"  ../transforms             cmake --build . cd  ../testg++ 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:Function  和 llvm: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 #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看看二进制文件是否真的被修改了
 可以看到确实被修改了
下一篇文章可能会写一下怎么修改基本块和怎么混淆运算符