一直对代码混淆比较感兴趣,过去的半年刚好有个机会能让我学习并进行了一定的实践,希望将其中的相关经验分享给师傅们,第一篇是对 LLVM NEW Pass 的学习归纳,后续会分享一些混淆的魔改思路与实现思路
实验环境:Ubuntu 24.04 LLVM 17
项目地址:6adK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6*7P5Y4A6U0j5$3y4&6P5i4W2Y4k6$3N6Q4x3V1k6w2L8%4c8G2j5h3#2S2N6s2y4#2K9$3q4E0K9b7`.`.
LLVM 通过一系列的passes来优化IR文件,不同的Pass作用在IR文件不同粒度上,如Function,Module。而Pass中的操作分为两种,Transformation 与 Analysis ,分别用于转化IR文件与收集IR文件中的信息,而一系列的pass则合集则被称为pass pipeline。
LLVM COMPILE AND INSTALL
1 | git clone --depth 1 -b release /17 .x https: //github .com /llvm/llvm-project .git
|
编译相关步骤可参考:253K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9L8s2k6E0i4K6u0W2L8%4u0Y4i4K6u0r3k6r3!0U0M7#2)9J5c8V1y4y4j5h3E0W2i4K6u0W2K9s2c8E0L8l9`.`.
我的编译命令
1 2 | cmake -G Ninja -DLLVM_ENABLE_PROJECTS= "clang;lld" -DLLVM_TARGETS_TO_BUILD= "X86;ARM;AArch64" -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_RTTI=ON -DCMAKE_INSTALL_PREFIX=. /build/ .. /llvm-project/llvm
ninja -j4
|
编译完成后使用以下命令完成安装,会将编译产物安装到DCMAKE_INSTALL_PREFIX
指定的文件夹
这里最好安装一下,不然没有LLVMConfig.cmake
此文件,无法在CMakeLists.txt
中使用find_package()
The LLVM pass manager
为了提升框架的灵活程度,LLVM将源文件->可执行文件的步骤打碎,分为不同的Passes

而控制这些Passes正确运行的,就是Pass Manager,而Pass 往往按照其工作范围分为以下几类:
Module Pass
Module Pass 以整个模块为输入。它在给定模块上执行其工作,可以用于模块内的过程间操作。由于它处理的是整个模块,模块 pass 可以访问和修改模块中的所有函数和全局变量。这使得它特别适合那些需要跨多个函数进行分析或优化的任务。
Call Graph Pass
Call Graph Pass 操作的是调用图的强连通分量(SCCs)。调用图是一个图结构,其中节点表示函数,边表示函数之间的调用关系。SCC 是图中所有节点互相可达的最大子图。调用图 pass 从底向上遍历这些组件,即从被调用函数开始,逐步向上处理调用它们的函数。
Function Pass
Function Pass 以单个函数为输入,只对该函数进行操作。它在函数的级别上执行分析或转换,不涉及其他函数。这使得函数 pass 简单且高效,因为它只需要处理一个函数的代码,而无需考虑模块中其他部分的状态。
Loop Pass
Loop Pass作用于函数内的循环。它只处理特定循环的内容,进行针对性的优化和转换。
如开头所提到,在 LLVM 中,除了用于改变IR代码的Transformation Pass
外 ,还存在着Analysis Pass
用于分析IR
得到其CFG
等相关分析结果 而这些分析结果需要在Pass之间传递,但是当Pass对代码进行改动时,原有的分析结果可能会发生改变,这就需要我们决定是否保存相关分析结果,或是重新分析。所以编写我们的新Pass的时候,需要注意的分析结果的保存与丢弃,尽量减少重复分析。

参考上图,pass 以流水线方式执行。例如,如果有几个函数 pass 应按顺序执行,pass 管理器会在第一个函数上运行每个函数 pass,然后在第二个函数上运行所有函数 pass,依此类推。这种方法的基本思想是通过在有限的数据集(一个 IR 函数)上执行转换,然后移动到下一个有限的数据集,来改善缓存行为。
Implementing a new pass
那么如何实现一个New Pass,首先需要将我们的Pass注册到PassManager中,详情参考代码:b16K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6*7P5Y4A6U0j5$3y4&6P5i4W2Y4k6$3N6Q4x3V1k6w2L8%4c8G2j5h3#2S2N6s2y4#2K9$3q4E0K9g2)9J5c8X3u0D9L8$3u0Q4x3V1k6D9L8s2k6E0i4K6u0V1x3e0N6Q4x3X3c8H3L8s2g2Y4K9h3&6K6i4K6u0r3M7%4u0U0i4K6u0r3f1r3q4K6M7#2m8D9N6h3N6A6L8W2)9J5k6h3y4H3M7l9`.`.
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 60 61 | using namespace llvm;
llvm::PassPluginLibraryInfo getKotoamatsukamiPluginInfo()
{
return {
LLVM_PLUGIN_API_VERSION, "Kotoamatsukami" , LLVM_VERSION_STRING,
[](PassBuilder& PB) {
/ / first way to use the pass
PB.registerPipelineParsingCallback(
[](StringRef Name, ModulePassManager& MPM, ArrayRef<PassBuilder::PipelineElement>) {
if (Name = = "gv-encrypt" ){
MPM.addPass(GVEncrypt());
return true;
}
else if (Name = = "split-basic-block" ) {
MPM.addPass(SplitBasicBlock());
return true;
} else if ……
return false;
});
/ / second way to use the pass
PB.registerPipelineStartEPCallback(
[](ModulePassManager& MPM, OptimizationLevel Level) {
MPM.addPass(AntiDebugPass());
MPM.addPass(SplitBasicBlock());
MPM.addPass(GVEncrypt());
MPM.addPass(BogusControlFlow());
MPM.addPass(AddJunkCodePass());
MPM.addPass(Loopen());
MPM.addPass(ForObsPass());
MPM.addPass(Branch2Call_32());
MPM.addPass(Branch2Call());
MPM.addPass(IndirectCall());
MPM.addPass(IndirectBranch());
MPM.addPass(Flatten());
MPM.addPass(Substitution());
});
}
};
}
__attribute__((visibility( "default" ))) extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo()
{
return getKotoamatsukamiPluginInfo();
}
|
可以看到以上代码有两处的MPM.addPass
,其分别对应着两种对Pass的调用方式,其中第一种方式是通过 opt 主动调用对应的pass,使用示例如下
1 | opt - 17 - - load - pass - plugin = SoPath input_file - - passes = passname - S - o output_file
|
而第二种方式则是将 Pass 插入到Clang、opt等工具提供的 Pipeline 中的某些节点,当这些Pipeline被调用时,我们的Pass就会在Pipeline的对应位置被调用,使用示例如下
1 | clang - fpass - plugin = SoPath input_file - O0 - o output_file
|
1 | opt - 17 - - load - pass - plugin = SoPath input_file - passes = 'default<O0>' - S - o output_file
|
How to make a plugin out of the source code
而调用Pass有两种方式,一种是将其编进LLVM源码树,另一种是将其编译为Pass Plugin,在需要使用的时候加载对应的Plugin,我这里采用的是源码树外构建Pass Plugin的方式,个人感觉比较方便,源码树内构建的话可以参考894K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6T1L8s2g2W2M7$3q4V1K9g2)9J5c8W2m8D9N6i4c8G2
参考CMakeLists.txt如下
cmake_minimum_required(VERSION 3.10)
project(Kotoamatsukami)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g")
set(LLVM_DIR "/home/zzzccc/llvm-17/llvm-project/build/lib/cmake/llvm")
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")
include(FetchContent)
FetchContent_Declare(
json
SOURCE_DIR /home/zzzccc/cxzz/Kotoamatsukami/lib/json
)
FetchContent_MakeAvailable(json)
message(${LLVM_INCLUDE_DIRS})
include_directories(${LLVM_INCLUDE_DIRS})
include_directories("/home/zzzccc/cxzz/Kotoamatsukami/src/include")
include_directories("/home/zzzccc/cxzz/Kotoamatsukami/lib/json/include")
include_directories("/home/zzzccc/cxzz/Kotoamatsukami/lib/json/single_include/nlohmann")
link_directories(${LLVM_LIBRARY_DIRS})
message(STATUS "WORKING_DIRECTORY: ${WORKING_DIRECTORY}")
include(AddLLVM)
include(HandleLLVMOptions)
add_definitions("${LLVM_DEFINITIONS}")
add_llvm_pass_plugin(Kotoamatsukami
src/PassPlugin.cpp
src/pass/AddJunkCodePass.cpp
src/pass/Branch2Call.cpp
src/pass/Branch2Call_32.cpp
src/pass/ForObsPass.cpp
src/pass/Loopen.cpp
src/pass/AntiDebugPass.cpp
src/pass/SplitBasicBlock.cpp
src/pass/IndirectBranch.cpp
src/pass/IndirectCall.cpp
src/pass/BogusControlFlow.cpp
src/pass/Substitution.cpp
src/pass/Flatten.cpp
src/pass/GVEncrypt.cpp
src/utils/utils.cpp
src/utils/config.cpp
src/utils/TaintAnalysis.cpp
)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -rdynamic")
target_link_libraries(Kotoamatsukami nlohmann_json::nlohmann_json)
这里CMakeLists
搞了好一阵子,主要是研究了下find_package
的用法,参考以下两篇文章:d07K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1L8r3!0Y4i4K6u0W2j5%4y4V1L8W2)9J5k6h3&6W2N6q4)9J5c8Y4q4I4i4K6g2X3x3K6V1@1y4U0j5%4y4e0g2Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3k6r3g2@1j5h3W2D9M7#2)9J5c8U0p5K6x3o6V1I4x3U0x3@1y4l9`.`. 、8c0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6*7K9s2g2S2L8X3I4S2L8W2)9J5k6i4A6Z5K9h3S2#2i4K6u0W2j5$3!0E0i4K6u0r3M7q4)9J5c8U0f1H3z5o6t1&6y4e0b7J5
这里首先明确find_package
有两种模式,如下
module模式
在这个模式下会查找一个名为find.cmake的文件,首先去CMAKE_MODULE_PATH指定的路径下去查找,然后去cmake安装提供的查找模块中查找(安装cmake时生成的一些cmake文件)。找到之后会检查版本,生成一些需要的信息。
config模式
在这个模式下会查找一个名为-config.cmake(<小写包名>-config.cmake)或者Config.cmake 的文件,如果指定了版本信息也会搜索名为-config-version.cmake 或者 ConfigVersion.cmake的文件。
而LLVM编译安装后会在/install/lib/llvm下存在相应的LLVMConfig.cmake
,而其主要搜寻路径如下
1 2 3 4 5 | <package>_DIR
CMAKE_PREFIX_PATH
CMAKE_FRAMEWORK_PATH
CMAKE_APPBUNDLE_PATH
PATH
|
故只需要设置LLVM_DIR并将find_package改为搜寻config模式即可
set(LLVM_DIR "/home/zzzccc/llvm-project/build/lib/cmake/llvm")
find_package(LLVM REQUIRED CONFIG)
编译成功后即可使用clang或opt调用
参考
《Learn_LLVM_17_A_beginner's_guide_to_learning_LLVM_2nd》