为什么有这篇文章 前段时间看了点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看看二进制文件是否真的被修改了
可以看到确实被修改了
下一篇文章可能会写一下怎么修改基本块和怎么混淆运算符