前言
在上一篇Android JNI接口混淆中, 通过gradle编译native代码时, 替换源码中字符串, 来达到混淆接口的目的, 方法总感觉很别扭
后来在看Gradle for Android 中文版时, 感慨gradle的构建系统这么灵活时, 想到ndk的构建系统应该也有同样的, 一番查找后, 了解到ndk也使用了llvm的构建系统, 编译native代码时, 实际上也是使用clang编译器, 结合之前做过llvm pass的经验, 就有了这篇改进版
注: LLVM源码编译确实很蛋疼, 经过各种方法, 各种折腾, 总算找到一条适合自己的路, 环境不一样, 出现的问题也不一样, 本次环境是Mac系统m1芯片, 系统别太老就行, 安装Xcode/brew环境, 尝试过linux出现的问题更少, 应该也适用
编译LLVM源码
命令行编译
这里采用的时完整编译llvm源码, 生成带有混淆功能的clang编译器, 然后替换ndk中的clang. 因为ndk源码也是完全跟踪的llvm官方源码的, 所以替换应该不会有什么问题.
LLVM源码放在 ~/tmp下面, 使用 17 的版本, 跟最新的ndk一致
bash
mkdir ~/tmp && cd ~/tmp
git clone https://github.com/llvm/llvm-project.git -b release/17.x --depth=1
cd llvm-project/llvm
mkdir build && cd build
cmake .. -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang"
# 开始编译, -j8为编译的核心数
cmake --build . --target clang -j8
这里选择ninja是因为编译速度快, 而且后面的断点调试pass, Clion默认也是ninja
如果没有出现问题那么恭喜你
如果编译失败, 参考问题与解决, 还是建议使用Clion编译
不装了, 事实上我在mac上也是通过Clion编译成功的😭
Clion编译源码
使用Clion Open选择 ~/tmp/llvm-project/llvm , 注意是选择子目录llvm
然后添加target, 这里可以直接输入关键词过滤, 选择clang
点击边上那个锤子, 开始编译
编译完成后, 我们测试一下clang
bash
cd ~/tmp/llvm-project/llvm/build
bin/clang --version
# clang version 17.0.6 (https://github.com/llvm/llvm-project.git 6009708b4367171ccdbf4b5905cb6a803753fe18)
# Target: arm64-apple-darwin23.0.0
# Thread model: posix
# InstalledDir: ~/tmp/llvm-project/llvm/build/bin
搞定!!!!
集成混淆代码
混淆的代码来自DreamSoule/ollvm17
使用说明可以去看看, 这里说下集成, 下面任选一种
自动集成
下载patch文件, 保存为ollvm17.patch, 并应用到llvm源码
bash
cd ~/tmp/llvm-project
git apply xxxx/ollvm17.patch
可以跳过手动集成, 直接到编译章节
手动集成
bash
# 拷贝Obfuscation到对应的路径
cp -r ollvm17/llvm-project/llvm/lib/Passes/Obfuscation ~/tmp/llvm-project/llvm/lib/Passes/
修改 ~/tmp/llvm-project/llvm/lib/Passes/PassBuilder.cpp
rust
// 引用 Obfuscation 相关文件
#include "Obfuscation/BogusControlFlow.h" // 虚假控制流
#include "Obfuscation/Flattening.h" // 控制流平坦化
#include "Obfuscation/SplitBasicBlock.h" // 基本块分割
#include "Obfuscation/Substitution.h" // 指令替换
#include "Obfuscation/StringEncryption.h" // 字符串加密
#include "Obfuscation/IndirectGlobalVariable.h" // 间接全局变量
#include "Obfuscation/IndirectBranch.h" // 间接跳转
#include "Obfuscation/IndirectCall.h" // 间接调用
// 添加命令行支持
static cl::opt<bool> s_obf_split("split", cl::init(false), cl::desc("SplitBasicBlock: split_num=3(init)"));
static cl::opt<bool> s_obf_sobf("sobf", cl::init(false), cl::desc("String Obfuscation"));
static cl::opt<bool> s_obf_fla("fla", cl::init(false), cl::desc("Flattening"));
static cl::opt<bool> s_obf_sub("sub", cl::init(false), cl::desc("Substitution: sub_loop"));
static cl::opt<bool> s_obf_bcf("bcf", cl::init(false), cl::desc("BogusControlFlow: application number -bcf_loop=x must be x > 0"));
static cl::opt<bool> s_obf_ibr("ibr", cl::init(false), cl::desc("Indirect Branch"));
static cl::opt<bool> s_obf_igv("igv", cl::init(false), cl::desc("Indirect Global Variable"));
static cl::opt<bool> s_obf_icall("icall", cl::init(false), cl::desc("Indirect Call"));
// 在此函数内直接注册Pipeline回调
PassBuilder::PassBuilder(...) {
...
this->registerPipelineStartEPCallback(
[](llvm::ModulePassManager &MPM,
llvm::OptimizationLevel Level) {
outs() << "[Soule] run.PipelineStartEPCallback\n";
MPM.addPass(StringEncryptionPass(s_obf_sobf));
llvm::FunctionPassManager FPM;
FPM.addPass(IndirectCallPass(s_obf_icall));
FPM.addPass(SplitBasicBlockPass(s_obf_split));
FPM.addPass(FlatteningPass(s_obf_fla));
FPM.addPass(SubstitutionPass(s_obf_sub));
FPM.addPass(BogusControlFlowPass(s_obf_bcf));
MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
MPM.addPass(IndirectBranchPass(s_obf_ibr));
MPM.addPass(IndirectGlobalVariablePass(s_obf_igv));
}
);
}
修改 ~/tmp/llvm-project/llvm/lib/Passes/CMakeLists.txt
scss
# 添加 Obfuscation 相关源码
add_llvm_component_library(LLVMPasses
...
Obfuscation/Utils.cpp
Obfuscation/CryptoUtils.cpp
Obfuscation/ObfuscationOptions.cpp
Obfuscation/BogusControlFlow.cpp
Obfuscation/IPObfuscationContext.cpp
Obfuscation/Flattening.cpp
Obfuscation/StringEncryption.cpp
Obfuscation/SplitBasicBlock.cpp
Obfuscation/Substitution.cpp
Obfuscation/IndirectBranch.cpp
Obfuscation/IndirectCall.cpp
Obfuscation/IndirectGlobalVariable.cpp
...
)
这个时候编译还会出现方法修饰符的问题, 修正一下
css
error: 'getBasicBlockList' is a private member of 'llvm::Function'
NF->getBasicBlockList().splice(NF->begin(), F->getBasicBlockList());
修改 ~/tmp/llvm-project/llvm/include/llvm/IR/Function.h
arduino
private:
...
public:
// 这两行从private下面移过来
const BasicBlockListType &getBasicBlockList() const { return BasicBlocks; }
BasicBlockListType &getBasicBlockList() { return BasicBlocks; }
编译
重新点击锤子编译, 目标还是clang
验证
添加hello.c验证是否混淆
~/tmp/llvm-project/llvm/build/hello.c
arduino
#include <stdio.h>
int main() {
printf("hello");
return 0;
}
防止命令行编译出错, 这里还是使用Clion编译
需要修改的3 个地方分别是
javascript
hello.c -o hello -mllvm -fla -mllvm -split -mllvm -split_num=5
~/tmp/llvm-project/llvm/build
# 添加头文件和链接库
# 解决stdio.h找不到和-l System出错的问题
C_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include;CPLUS_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include;LIBRARY_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk/usr/libd
点击锤子右边的运行按钮
使用ida pro反编译看看~/llvm-project/llvm/build/hello
搞定!!!
NDK集成
由于google编译NDK的源码中也是完全跟踪的llvm源码, 所以这里我们直接替换NDK中的clang
集成很简单, 只需使用新的clang覆盖ndk的clang即可
替换clang
bash
# 使用clang --version查看ndk是否是17
export NDK=~/Library/Android/sdk/ndk/26.0.10792818
cd $NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin
# clang是软连接, 指向clang-17
cp clang-17 clang-17.bak # 先备份
cp -f ~/tmp/llvm-project/llvm/build/bin/clang-17 ./clang-17
验证ndk
使用Android studio创建一个jni项目, 修改build.gradle
arduino
android {
// ...
defaultConfig {
// ...
ndkVersion("26.0.10792818")
externalNativeBuild {
cmake {
// 更多混淆参数, 可以看看上面的链接
cppFlags "-mllvm -fla -mllvm -split -mllvm -split_num=5"
}
}
}
// ...
}
运行assembleRelease, 看看对比, 混淆后和混淆前
./app/build/intermediates/stripped_native_libs/release/out/lib/arm64-v8a/libmyapplication.so
搞定!!!
如果你只想混淆jni, 那么到这里就结束了
调试LLVM Pass
调试的话, 建议使用动态库的方式, 不用编译整个源码, 比如
首先目标设置成opt, 编译得到opt命令
css
====================[ Build | opt | Release ]===================================
/Applications/CLion.app/Contents/bin/cmake/mac/bin/cmake --build /Users/zhao/tmp/llvm-project/llvm/build --target opt -j 8
然后编译官方的Hello示例, 目标为LLVMHello
vbnet
====================[ Build | LLVMHello | Release ]=============================
/Applications/CLion.app/Contents/bin/cmake/mac/bin/cmake --build /Users/zhao/tmp/llvm-project/llvm/build --target LLVMHello -j 8
[3/3] Linking CXX shared module lib/LLVMHello.dylib
ld: warning: -undefined suppress is deprecated
Build finished
这里还需要把hello.c转为IR中间层hello.bc
javascript
export C_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include;
export CPLUS_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include;
export LIBRARY_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk/usr/lib
cd ~/tmp/llvm-project/llvm/build
bin/clang -emit-llvm -S hello.c -o hello.bc
常用的转化命令
- .c -> .ll:
clang -emit-llvm -S a.c -o a.ll
- .c -> .bc:
clang -emit-llvm -c a.c -o a.bc
- .ll -> .bc:
llvm-as a.ll -o a.bc
- .bc -> .ll:
llvm-dis a.bc -o a.ll
- .bc -> .s:
llc a.bc -o a.s
修改opt的配置
需要改的 3 个参数分别为
javascript
-load lib/LLVMHello.dylib -hello -bugpoint-enable-legacy-pm hello.bc
~/tmp/llvm-project/llvm/build
C_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include
-bugpoint-enable-legacy-pm这个是解决插件加载出错的参数
接着就可以调试官方自带的Hello示例
~/tmp/llvm-project/llvm/lib/Transforms/Hello/Hello.cpp
增加JNI接口混淆
原理
这里是Android JNI接口混淆的优化版本
优化的流程是, 使用gradle插件解析mapping.txt, 读取java的混淆结果
比如
css
stringFromJNI -> a
然后使用插件生成对应的mapping.ini
ini
stringFromJNI=a
然后使用pass解析mapping.ini文件, 使用字符串加密的方式, 在IR层, 也把stringFromJNI替换成a, 这样就不需要修改源码.
实现
这里在ollvm17 的源码基础上增加
~/tmp/llvm-project/llvm/lib/Passes/Obfuscation/JNIObfuscation.h
c
namespace llvm {
class JNIObfuscation : public PassInfoMixin<JNIObfuscation> {
public:
bool flag;
JNIObfuscation(bool flag) { this->flag = flag; } // 携带flag的构造函数
std::unordered_map<std::string, std::string> readConfig();
std::vector<GlobalVariable *> filterGlobalVar(Module &M);
void replaceString(std::unordered_map<std::string, std::string> &ConfigMap,
std::vector<GlobalVariable *> GVs, Module &M);
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM); // Pass实现函数
};
} // namespace llvm
#endif
~/tmp/llvm-project/llvm/lib/Passes/Obfuscation/JNIObfuscation.cpp
c
static cl::opt<string> JNIFile("jni_file", cl::desc("specify jni config file"));
char JNIObfuscation::ID = 0;
PreservedAnalyses JNIObfuscation::run(Module &M, ModuleAnalysisManager &AM) {
if (flag) {
unordered_map<string, string> ConfigMap = readConfig();
vector<GlobalVariable *> GVs = filterGlobalVar(M);
replaceString(ConfigMap, GVs, M);
return return PreservedAnalyses::none();
}
return PreservedAnalyses::all();
}
void JNIObfuscation::replaceString(unordered_map<string, string> &ConfigMap,
vector<GlobalVariable *> GVs, Module &M) {
for (GlobalVariable *GVar : GVs) {
string Origin = dyn_cast<ConstantDataArray>(GVar->getInitializer())
->getAsCString()
.str();
if (ConfigMap.find(Origin) != ConfigMap.end()) {
errs() << "replace: " << Origin << "->" << ConfigMap[Origin] << "\n";
Constant *NewStr =
ConstantDataArray::getString(M.getContext(), ConfigMap[Origin], true);
GlobalVariable *NewVar = new GlobalVariable(
M, NewStr->getType(), GVar->isConstant(), GVar->getLinkage(), NewStr,
GVar->getName() + "_new");
GVar->replaceAllUsesWith(NewVar);
GVar->eraseFromParent();
}
}
}
vector<GlobalVariable *> JNIObfuscation::filterGlobalVar(Module &M) {
vector<GlobalVariable *> GVs;
for (GlobalVariable &GVar : M.globals()) {
ConstantDataArray *Arr = dyn_cast<ConstantDataArray>(GVar.getInitializer());
if (Arr != nullptr && Arr->isCString()) {
GVs.push_back(&GVar);
}
}
return GVs;
}
unordered_map<string, string> JNIObfuscation::readConfig() {
if (JNIFile.empty()) {
return unordered_map<string, string>();
}
errs() << "read config: " << JNIFile.getValue() << "\n";
unordered_map<string, string> ConfigMap;
std::ifstream ConfigFile(JNIFile.getValue());
string Line;
while (getline(ConfigFile, Line)) {
size_t Pos = Line.find("=");
if (Pos != string::npos) {
string Key = Line.substr(0, Pos);
string Value = Line.substr(Pos + 1);
ConfigMap[Key] = Value;
}
}
ConfigFile.close();
for (auto Entry : ConfigMap) {
errs() << Entry.first << ":" << Entry.second << "\n";
}
return ConfigMap;
}
添加shell支持
~/tmp/llvm-project/llvm/lib/Passes/PassBuilder.cpp
rust
...
static cl::opt<bool> s_obf_jni("jni", cl::init(false), cl::desc("JNI Obfuscation"));
...
MPM.addPass(JNIObfuscation(s_obf_jni));
集成到ndk后可添加参数进行接口混淆
ini
-mllvm -jni -mllvm jni_file=app/build/outputs/mapping.ini
问题与解决
问题 1
bash
CMake Error at cmake/modules/CheckCompilerVersion.cmake:55 (if):
if given arguments:
"(" "STREQUAL" "MSVC" ")" "AND" "(" "19.24" "VERSION_LESS_EQUAL" ")" "AND" "(" "VERSION_LESS" "19.25" ")"
解决 1
那么需要升级cmake, 其它的cmake问题也建议先升级
bash
cmake --version
# cmake version 3.28.20231228-g158ecdc
# CMake suite maintained and supported by Kitware (kitware.com/cmake).
# 使用 brew install cmake时, 版本一直是2.4, 还是会出现上述问题
# 所以选择源码编译
brew install cmake --HEAD
问题 2
swift
- The C compiler identification is unknown
-- The CXX compiler identification is unknown
CMake Error at CMakeLists.txt:55 (project):
No CMAKE_C_COMPILER could be found.
解决 2
arduino
xcrun -find c++
xcrun -find cc
# 使用cmake -DCMAKE_C_COMPILER=上面的路径
问题 3
头文件或是链接库未找到
解决 3
javascript
export C_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include
export CPLUS_INCLUDE_PATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include
export LIBRARY_PATH=/Library/Developer/CommandLineTools/SDKs/MacOSX11.1.sdk/usr/lib
注: 如果以上都未能解决, 建议使用Clion编译