Android JNI混淆

前言

在上一篇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编译

参考

Hikari

Hikari-LLVM15

obfuscator-llvm

ollvm17

错误排查

Clion集成llvm

LLVM Pass入门导引

相关推荐
帅次6 分钟前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat
Hacker_Nightrain36 分钟前
网络安全CTF比赛规则
网络·安全·web安全
枯骨成佛1 小时前
Android中Crash Debug技巧
android
看山还是山,看水还是。1 小时前
Redis 配置
运维·数据库·redis·安全·缓存·测试覆盖率
学编程的小程1 小时前
【安全通信】告别信息泄露:搭建你的开源视频聊天系统briefing
安全·开源·音视频
网络安全指导员1 小时前
恶意PDF文档分析记录
网络·安全·web安全·pdf
渗透测试老鸟-九青2 小时前
通过投毒Bingbot索引挖掘必应中的存储型XSS
服务器·前端·javascript·安全·web安全·缓存·xss
vortex52 小时前
蓝队基础之网络七层杀伤链:从识别到防御的全方位策略
安全·网络安全·蓝队
白总Server2 小时前
JVM解说
网络·jvm·物联网·安全·web安全·架构·数据库架构
kali-Myon3 小时前
ctfshow-web入门-SSTI(web361-web368)上
前端·python·学习·安全·web安全·web