This project is based on LLVM NewPass and turns original OLLVM and Hikari into standalone Pass, with the following goals:
- Verify that
IRlevel obfuscation can be implemented using independentPass - Assist in validating another project:
ida_mcp
This project has been tested on macOS 15 with LLVM 15–19. Because it preserves the original code as much as possible (with minimal modifications), it does not provide enhancements in obfuscation strength. If you’re interested in stronger obfuscation capabilities, you can follow my other project: SLLVM.
Pros and cons of implementing obfuscation as independent Pass:
Pro #1:If you already installedLLVMviaHomebrew(macOS),apt(Debian), etc., you don’t need to buildLLVMyourself—faster development iterationPro #2:Passcan be compiled quickly and the output binaries are smallCon #1:You must use a matchingClang—at minimum, the majorLLVMversion must match
In many real-world projects, it’s often impossible to obfuscate the entire project due to reasons such as:
- The project is large, has many dependencies, or uses many
header-onlylibraries; obfuscating too much unnecessary code makes the final binary too large - The project is large with many dependencies; using flattening (or other techniques) on unnecessary code makes compile time too long or even causes the build to hang
- Obfuscating complex algorithms increases runtime overhead significantly; flattening typically increases runtime by
10%+ - Excessive obfuscation may violate
AppStore/GooglePlaypolicies (e.g., may prevent publishing)
In practice, you often need different obfuscation levels based on the importance of modules/functions. Therefore, a policy is needed to specify which modules/functions use which obfuscation options. Common strategy configuration approaches in open-source OLLVM include:
- Add command-line options only to modules that need obfuscation (e.g.,
-llvm -fla). This works with any compiler frontend that supports LLVM command-line options. - Use environment variables to specify obfuscation options
- Annotate functions that should be obfuscated, e.g.
__attribute((__annotate__(("fla"))))(new syntax:[[clang::annotate("fla")]]). This only supportsC/C++;Objective-Cand other languages do not support it. - Use a marker function inside functions that should be obfuscated, as shown below (this supports
Objective-C):
extern void hikari_fla(void);
@implementation foo2:NSObject
+(void)foo{
hikari_fla();
NSLog(@"FOOOO2");
}
@endAll of the approaches above have limitations: they either require too much code modification, cannot control at function granularity, or only support specific languages. This project uses a configuration file to specify which functions and modules should be obfuscated, making it compatible with most compiler frontends and development languages.
The policy file is policy.json in the working directory, provided by the user. Fields are:
| Type | Meaning | Required | Note | |
|---|---|---|---|---|
globals |
dictionary | Global options | No | |
policy_map |
dictionary | Mapping of policy name -> option set |
Yes | |
policies |
array | All policy rules | Yes | |
policies.module |
string | Regex match for module name | Yes | |
policies.func |
string | Regex match for function name | No | Used to distinguish module-level vs function-level policies |
policies.policy |
string | Policy name, refers to policy_map |
Yes |
Rules:
- A policy that specifies only the
modulefield is amodule-levelpolicy; a policy that specifies bothmoduleandfuncis afunction-levelpolicy - Forward override: For
function-levelpolicies in thepoliciesarray, if a later item’s matchedmodule/funcset is a subset of an earlier item’s matches, it overrides the earlier policy. The same applies tomodule-levelpolicies. - Comments supported: Any optional sub-field supports
#comments, e.g."#enable-strcry": true - Name demangling supported: My other project
SLLVMsupports matchingmodule/funceven under name obfuscation for languages likeC++/Swift - The
enable-dumpfield in a policy is used to print module IR / function IR (depending on the policy type)
{
"globals": {
"aesSeed": 4919
},
"policy_map": {
"my_mod_pol": {
"acd-use-initialize": true,
...
},
"my_func_pol": {
"enable-strcry": true,
"enable-splitobf": false,
"split_num": 2,
...
}
},
"policies": [
{
"module": ".*",
"policy": "my_mod_pol" // module level policy
},
{
"module": ".*",
"func": ".*",
"policy": "my_func_pol" // function level policy
}
]
}In the test directory of each subproject (original OLLVM / Hikari / ...), you can find a policy.json that contains all configurable fields.
If you can locate LLVM’s build directory (for example, if you installed LLVM via Homebrew (macOS) or apt (Debian), you can find ./cmake/AddLLVM.cmake), then you can skip building LLVM:
# optional: -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Debug
cmake -S llvm -G Ninja -B llvm_build_dir
cmake --build llvm_build_dirExample: macOS + original OLLVM
git clone https://github.com/lich4/llvm-pass-hikari
cd llvm-pass-hikari
export LLVM_DIR=/path/to/llvm_build_dir
cmake -S obfuscator -G Ninja -B obfuscator/build
cmake --build obfuscator/buildExample: macOS + original OLLVM. Notes:
- The
Passmust match the major version of the correspondingLLVM/Clang - If you get header-related errors when testing
objc++, ensure the open-sourceclangversion matches theclangversion used byXcode
cd test
# test c
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib test.c -o test
# test cpp
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib -std=c++11 -stdlib=libc++ -lc++ test.cpp -o test
# test objc
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib -framework Foundation test.m -o test
# test objc++
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib -framework Foundation -std=c++11 -stdlib=libc++ -lc++ test.mm -o testBecause the open-source LLVM Clang differs from Xcode’s Clang, a dynamic Pass cannot be used directly in Xcode. You can consider the following approaches:
- In Xcode, set the
CCvariable to an open-sourceclang(e.g.,brew install llvm@15), and setOther C Flagsto include-fpass-pluginpointing to the Pass path - In Xcode, set
CCto a build script. The script logic is: “useclang -emit-llvmto generate bitcode first, then runoptto execute the Pass, and finally runclang -cto produce the object file as usual.” This approach can use Xcode’s built-inApple clang, and has better compatibility with thearm64earchitecture. (This repo includes my own script:xcode_cc.sh.) - Develop a dynamic Pass specifically for Xcode’s built-in
Apple clang, and setOther C Flagsto include-fpass-pluginpointing to the Pass. This is more complex (you must handle many symbol conflicts) and is only suitable for developers highly proficient with LLVM. This approach can also use Apple clang directly and has better compatibility witharm64e.
本项目基于LLVM NewPass实现原版OLLVM和Hikari的Pass化,有以下目标:
- 验证可以用独立
Pass实现IR层混淆 - 用于辅助验证另一个项目
ida_mcp
本项目在MacOS15+LLVM15-19上测试,因为最大限度保留原始代码未改动因此不会有混淆功能方面的增强。对更强的混淆功能有兴趣的可以关注我的另一个项目SLLVM
使用独立Pass实现混淆的优缺点:
- 优点1:如果已经用
Homebrew(Mac)/apt(Debian)等安装过LLVM则无需编译LLVM,开发速度快 - 优点2:
Pass本身编译速度快,编译出的文件小 - 缺点1:搭配对应的
Clang一起使用,至少保证LLVM大版本匹配
在很多实际项目中,由于以下原因无法对整个项目完全混淆:
- 项目较大,依赖较多,或使用了很多
header-only的库,混淆了很多不需要混淆的代码,导致编译出来的二进制过大 - 项目较大,依赖较多,使用了平坦化(或其他方式)混淆了很多不需要混淆的代码,导致编译时间过久甚至卡死
- 混淆了复杂算法,导致运行时耗时比正常大很多,一般使用平坦化后耗时会增加10%以上
- 混淆过多可能不允许上架
AppStore/GooglePlay等
实际操作时, 常常需要根据模块/函数的重要性使用不同程度的混淆,因此需要配置策略来指定哪些模块/函数需要用哪种混淆,而开源的OLLVM常见设置策略的方式如下:
- 对需要混淆的模块单独指定命令行参数,如
-llvm -fla,这种方式兼容所有支持LLVM命令行参数的编译器前端 - 使用环境变量指定混淆参数
- 对需要混淆的函数指定注解,如
__attribute((__annotate__(("fla"))))(新式语法[[clang::annotate("fla")]]),这种方式仅支持C/C++,Objective-C和其他语言均不支持 - 对需要混淆的函数指定标记函数,如下所示,这种方式支持
Objective-C
extern void hikari_fla(void);
@implementation foo2:NSObject
+(void)foo{
hikari_fla();
NSLog(@"FOOOO2");
}
@end以上方式均有局限性,或对代码改动太大,或无法控制到函数粒度,或只支持特定语言。本项目使用配置文件来指定需要混淆的函数和模块,兼容大部分编译器前端及开发语言。
策略文件为工作目录下名为policy.json的文件,需用户提供,字段如下:
| 字段类型 | 字段含义 | 必须 | 特殊说明 | |
|---|---|---|---|---|
| policy_map | 字典 | 策略名 - 混淆选项集合的映射 |
是 | |
| policies | 数组 | 所有策略 | 是 | |
| policies.module | 字符串 | 正则匹配模块名 | 是 | |
| policies.func | 字符串 | 正则匹配函数名 | 否 | 用以区分模块级/函数级策略 |
| policies.policy | 字符串 | 策略名,对应policies |
是 |
语法如下:
- 仅指定
module字段的策略为模块级策略,同时指定module/func字段的策略为函数级策略 - 前向覆盖:对于
policies数组中的函数级策略,如果后面的项匹配的module/func是在其之前项匹配的子集,则覆盖之前的策略;模块级策略同理 - 支持注释:非必须的子字段,都支持
#注释,如"#enable-strcry": true - 支持名称混淆:本人另一个项目
SLLVM支持c++/swift等语言名称混淆情况下的module/func匹配 - 策略中
enable-dump字段用于打印模块IR/函数IR(取决于策略类型)
{
"globals": {
"aesSeed": 4919
},
"policy_map": {
"my_mod_pol": {
"acd-use-initialize": true,
...
},
"my_func_pol": {
"enable-strcry": true,
"enable-splitobf": false,
"split_num": 2,
...
}
},
"policies": [
{
"module": ".*",
"policy": "my_mod_pol" // 模块级策略
},
{
"module": ".*",
"func": ".*",
"policy": "my_func_pol" // 函数级策略
}
]
}在每个子工程(原版OLLVM/Hikari/...)中的test目录可找到policy.json,其中包含了所有可配置字段
若能找到LLVM对应的编译目录位置(如已经用Homebrew(Mac)/apt(Debian)等安装过LLVM,可定位到./cmake/AddLLVM.cmake)则可跳过编译
# optional: -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Debug
cmake -S llvm -G Ninja -B llvm_build_dir
cmake --build llvm_build_dir以Mac系统+原版OLLVM为例
git clone https://github.com/lich4/llvm-pass-hikari
cd llvm-pass-hikari
export LLVM_DIR=/path/to/llvm_build_dir
cmake -S obfuscator -G Ninja -B obfuscator/build
cmake --build obfuscator/build以Mac系统+原版OLLVM为例,注意:
Pass要匹配对应的LLVM/Clang大版本- 如果测试
objc++时报头文件相关错误则需要开源clang版本和Xcode对应clang版本一致
cd test
# test c
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib test.c -o test
# test cpp
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib -std=c++11 -stdlib=libc++ -lc++ test.cpp -o test
# test objc
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib -framework Foundation test.m -o test
# test objc++
/path/to/llvm_build_dir/bin/clang -isysroot `xcrun --sdk macosx --show-sdk-path` -fpass-plugin=../build/Hikari.dylib -framework Foundation -std=c++11 -stdlib=libc++ -lc++ test.mm -o test由于开源LLVM的Clang不同于Xcode的Clang,因此动态Pass不能直接用于Xcode,可以考虑以下方式:
- 在
Xcode中指定CC变量为开源clang(如brew install llvm@15),且指定Other C Flags的-fpass-plugin为对应Pass路径 - 在
Xcode中指定CC变量为编译脚本,脚本逻辑为"先用clang -emit-llvm参数生成bitcode,然后运行opt执行Pass,最后用clang -c生成原本要生成的obj文件"。此种方式可以直接使用Xcode自带的Apple clang,能比较好的兼容arm64e架构. (本项目中公开本人自用的xcode_cc.sh) - 直接针对
Xcode自带的Apple clang开发动态Pass,在Xcode中指定Other C Flags指定-fpass-plugin为Pass路径。此种方式复杂度较高,需要处理大量符号冲突,只适合精通LLVM的开发者。此种方式可以直接使用Xcode自带的Apple clang,能比较好的兼容arm64e架构