OLLVM学习成果展示 - SLLVM
本帖最后由 lichao 于 2026-3-16 15:58 编辑笔者于2024年8月起间断研究OLLVM至今已有1年多, 在学习初期发过如下如下帖子
* (https://www.0xaa55.com/thread-27506-1-1.html)
* (https://www.0xaa55.com/thread-27509-1-1.html)
* (https://www.0xaa55.com/thread-27510-1-1.html)
* (https://www.0xaa55.com/thread-27511-1-1.html)
* (https://www.0xaa55.com/thread-27512-1-1.html)
* (https://www.0xaa55.com/thread-27513-1-1.html)
笔者在2025年初开始开发自己的SLLVM并在实际项目中使用, 实际使用已有1年多了, 期间不断优化增强并修复bug已经到第三个版本, 这里仅作展示, 后续完善后会开源部分源码:
[https://github.com/lich4/sllvm](https://github.com/lich4/sllvm/blob/main/README_zh.md)
## 支持的系统和架构
* 计划支持的宿主系统为`macOS`/`Linux`, 暂无计划支持`Windows`
* 计划支持的目标系统为`macOS`/`iOS`/`Android`
* 计划支持的宿主架构和目标架构均为`X64`/`ARM64`
* 计划支持`C`/`C++`/`Objective-C`/`Objective-C++`/`Swift`/`Rust`/`Golang`等语言
## 混淆能力
* 数据加密
* (#字符串加密) 只设计字符串混淆
* 控制流混淆
* (#控制流平坦化) 控制流平坦化
* (#控制流伪造) 控制流伪造
* (#控制流另类混淆) 控制流另类混淆
* 函数级混淆
* (#函数嵌套) 函数嵌套
* (#调用约定混淆) 调用约定混淆
* 指令级混淆
* (#间接跳转) 间接跳转
* (#间接调用) 间接调用
* (#间接全局变量) 间接数据引用
* (#系统调用混淆) 系统调用混淆
* (#拆分指令) 拆分指令
* 其他
* (#内联子函数) 内联子函数
* (#安全防护) 反调试
---
### SLLVM的特点
目前活跃开源`OLLVM`项目有: 原版OLLVM, Hikari, Hikari-LLVM15, Pluto, Polaris-Obfuscator, goron, Arkari, o-mvll, ...
`SLLVM`和其他`OLLVM`()的区别主要在以下方面:
* 支持Release编译, 在Release下编译时混淆不会被编译器还原
* 抗'改数据段属性只读'攻击, 在`IDA`中设置数据段为只读后混淆不会被还原
* 对已有的混淆功能加固+去特征, 由于`OLLVM`已经被广泛使用, 很容易被特定脚本/Angr/AI针对(笔者也自行开发过通用还原脚本)
* 在混淆大工程和使用header-only头文件时不会内存爆和长时间编译卡死
---
## 配置
在很多实际项目中, 由于以下原因无法对整个项目完全混淆:
* 项目较大, 依赖较多, 或使用了很多`header-only`的库, 混淆了很多不需要混淆的代码, 导致编译出来的二进制过大
* 项目较大, 依赖较多, 使用了平坦化(或其他方式)混淆了很多不需要混淆的代码, 导致编译时间过久甚至卡死
* 混淆了复杂算法, 导致运行时耗时比正常大很多, 一般使用平坦化后耗时会增加10%以上
* 混淆过多可能不允许上架`AppStore`/`GooglePlay`等
实际操作时, 常常需要根据模块/函数的重要性使用不同程度的混淆, 因此需要配置策略来指定哪些模块/函数需要用哪种混淆, 而开源的`OLLVM`常见设置策略的方式如下:
* 对需要混淆的模块单独指定命令行参数, 如`-llvm -fla`, 这种方式兼容所有支持`LLVM`命令行参数的编译器前端
* 使用环境变量指定混淆参数
* 对需要混淆的函数指定注解, 如`__attribute((__annotate__(("fla"))))`(新式语法`[]`), 这种方式仅支持`C/C++`, `Objective-C`和其他语言均不支持
* 对需要混淆的函数指定标记函数, 如下所示, 这种方式支持`Objective-C`
```objc
extern void hikari_fla(void);
@implementation foo2:NSObject
+(void)foo{
hikari_fla();
NSLog(@"FOOOO2");
}
@end
```
以上方式均有局限性, 或对代码改动太大, 或无法控制到函数粒度, 或只支持特定语言。本项目使用配置文件(`sllvm.json`)来指定需要混淆的函数和模块, 兼容大部分编译器前端及开发语言。
### 一级/二级字段
* `log_level` 全局日志等级, 字符串类型, 可选字段, 默认无日志, 可选值: `info`|`debug`
* `src_root` 源文件路径, 字符串类型, 可选字段, 默认为当前目录, 一般无需指定
* `policies` 策略列表, 分为 模块级策略 和 函数级策略, 模块级策略有`module`,`policy`字段而无`func`字段, 函数级策略同时有`module`,`func`,`policy`字段
* `module` 正则表达式, 用于匹配模块路径
* `func` 正则表达式, 用于匹配函数名, 对于C++函数会Demangling再匹配
* `policy` 字符串类型, 值为`policy_map`的某个key
* `policy_map`策略索引, 用于在`policies`指定, 每个策略名对应一个字典, 字典字段如下:
* `base` 继承的基策略名, 模块级或函数级策略, 字符串类型, 可选字段, 值为`policy_map`的某个key
* `dump` 输出中间代码类型(`sllvm_dump`目录下), 模块级或函数级策略, 字符串数组类型, 默认空, 可选值: `ir`, `mmd`, `asm`
* `enable_std`启用C++标准库函数混淆, 模块级策略, 布尔类型, 可选字段, 默认禁用以减少兼容问题
### 函数级策略
* `enable_ce` 启用`CE`混淆
* `ce_size_min` 最小字符串长度
* `ce_size_max` 最大字符串长度
* `ce_algo` 加解密算法
* `ce_mode_stack` 字符串基于栈解密
* `enable_fla` 启用`FLA`混淆
* `fla_prob` BasicBlock混淆比例
* `fla_force_reg` 增加混淆强度
* `fla_use_igv` 增加混淆强度
* `fla_use_dyn` 增加混淆强度
* `fla_use_rcf` 增加混淆强度
* `fla_blk_size` 增加混淆强度
* `fla_invoke_op` 兼容异常处理方式
* `enable_bcf` 启用`BCF`混淆
* `bcf_prob` BasicBlock混淆比例
* `bcf_complex` 表达式复杂度
* `bcf_use_var` 使用变量构造表达式
* `enable_ecf` 启用`ECF`混淆
* `ecf_prob` BasicBlock混淆比例
* `enable_fw` 启用`FW`混淆
* `fw_loop_min` 函数嵌套层数最小值
* `fw_loop_max` 函数嵌套层数最大值
* `fw_exclude` 排除的子函数
* `enable_fcc` 启用`FCC`混淆
* `fcc_num` 调用约定数量(模块级)
* `fcc_type` 调用约定类型(模块级)
* `fcc_narg_reg` 寄存器传参个数(模块级)
* `enable_ibr` 启用`IBR`混淆
* `ibr_prob` BasicBlock混淆比例
* `ibr_use_igv` 增加混淆强度
* `ibr_use_dyn` 增加混淆强度
* `enable_icall` 启用`ICALL`混淆
* `icall_use_igv` 增加混淆强度, 默认开启
* `icall_use_dyn` 增加混淆强度
* `enable_igv` 启用`IGV`混淆
* `igv_use_dyn` 增加混淆强度
* `enable_svc` 启用`SVC`混淆
* `svc_usr_ir` 增加混淆强度
* `enable_split` 启用`SPLIT`混淆
* `split_maxsize` 拆分指令数
* `enable_inline` 启用`INLINE`混淆
* `enable_sec` 启用`SEC`混淆
* `sec_ad_prob` Function反调试插入比例
* `sec_usr_ir` 增加混淆强度
一个典型的`SLLVM`配置文件`sllvm.json`如下:
```json
{
"log_level": "info",
"policy_map": {
"mod_pol": {
"dump": ["ir"],
},
"func_pol": {
"enable_ce": true,
"ce_size_min": 5,
"ce_size_max": 128,
"ce_algo": 0
}
},
"policies": [
{
"desc": "模块级策略",
"module": ".*",
"policy": "mod_pol"
},
{
"desc": "函数级策略",
"module": ".*",
"func": ".*",
"policy": "func_pol"
}
]
}
```
## 支持的混淆方式
### 字符串加密
目前`SLLVM`的`CE`支持`arm64`/`arm64e`, 支持`Objective-C`常量字符串, 支持栈解密`ce_mode_stack`, `CE`是函数级混淆而非`Hikari`的模块级, 因此可以对指定函数中的所有字符串做混淆, 这样在处理`header-only`更方便.
与`Hikari`的区别:
* 支持XOR以外的加密算法
* 支持栈解密(`ce_mode_stack`)
#### `ce_algo`
用于设置加解密算法, 目前支持30种算法, 复杂度介于XOR-AES之间, 取100则随机算法
#### `ce_mode_stack`
用于控制是否在栈上解密字符串. 若`ce_mode_stack`未开启, 则采取和`Hikari`同样的方式处理. `Hikari`字符串混淆, `SLLVM`的`ce_mode_stack`(S)模式, 基于C++模板元的字符串混淆, 这三种方式对比如下:
|项 |`Hikari` |`SLLVM`S模式|C++模板元|
|- |- |- |- |
|加密位置 |静态区 |静态区 |静态区/立即数|
|解密时机 |函数头 |函数头 |引用前 |
|解密位置 |静态区 |栈区 |栈区 |
|需要改源码 |N |N |Y |
|复杂算法 |支持 |支持 |不适合 |
说明:
* 解密时机, `Hikari`在函数头处解密且解密一次, 其他`OLLVM系`项目有的是在初始化函数解密的, 这种方式的缺点是解密一次即在静态区出现明文
* C++模板元方式只支持C++, 其他语言如Rust也有针对性的第三方库实现类似功能, 都需要对源码做变化
注意:
* 目前`OLLVM系`没有实现对字符串混淆进行栈解密的, 因为字符串常量的本质是静态数据, 难以在IR层判断字符串是否可能发生逃逸.
* 在`SLLVM`中启用`ce_mode_stack`后字符串会从静态数据降级为栈数据, 因此有特殊使用方式.
#### 展示
```c
int main(int argc, char** argv) {
printf("hello sllvm\n");
return 0;
}
```
CE静态区
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_ce_static_algo.png)
CE栈区
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_ce_stack_algo.png)
### 控制流平坦化
将控制流从顺序执行转换为switch-case循环执行, 与`Hikari`的区别:
* 弱化了大量`FLA`特征(状态变量/入度/分发块/单循环)
* `Hikari`无法处理有异常处理的函数, 而`SLLVM`可以通过指定`fla_invoke_op`选择处理方式
#### 展示
```c
int main(int argc, char** argv) {
if (argc <= 0) {
printf("not possible\n");
} else if (argc == 1) {
printf("no args\n");
} else {
printf("%d args\n", argc - 1);
}
return 0;
}
```
FLA全开
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_fla_full.png)
### 控制流伪造
向顺序执行的控制流插入条件恒假的条件分支, 与`Hikari`的区别:
* 混淆不会在Release下编译时被编译器还原
* 混淆不会因为在`IDA`中设置数据段只读而被还原(依赖`bcf_use_var`)
#### 展示
```c
int main(int argc, char** argv) {
if (argc <= 0) {
printf("not possible\n");
} else if (argc == 1) {
printf("no args\n");
} else {
printf("%d args\n", argc - 1);
}
return 0;
}
```
BCF常量
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_bcf_const.png)
BCF变量
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_bcf_var.png)
### 控制流另类混淆
全新的混淆思路, 可以对抗`Angr`等工具跟踪
### 函数嵌套
对指定函数直接调用的子函数执行嵌套, 与`Hikari`的区别:
* 混淆不会在Release下编译时被编译器还原
### 调用约定混淆
对常规C调用约定转换为随机调用约定, 改变参数和返回值所用寄存器, 目前只实现ARM64的部分
* `fcc_num` 指定随机CallingConv的数量
* `fcc_type` 指定自定义CallingConv的类型, 取值如下
* 0 仅使用`X0~X8`
* 1 仅使用整数寄存器
* 2 仅使用浮点寄存器
* 10 使用任意寄存器
* `fcc_narg_reg` 寄存器最大传参个数, 其余参数用栈传递
#### 展示
```c
static int test(int a0, int a1, int a2, int a3, int a4) {
printf("a0=%d\n", a0);
printf("a1=%d\n", a1);
printf("a2=%d\n", a2);
printf("a3=%d\n", a3);
printf("a4=%d\n", a4);
return a0 + a1 + a2 + a3 + a4;
}
int main(int argc, char** argv) {
test(argv, argv, argv, argv, argv);
return 0;
}
```
FCC使用`X8(X8,X1,X6,...)`
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_fcc_base.png)
FCC使用`D26(D26,D2,X15,...)`
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_fcc_full.png)
### 间接跳转
与`Hikari`的区别:
* 混淆不会在Release下编译时被编译器还原
* `ibr_use_igv`和`Hikari`的`indibran-enc-jump-target`类似, 结合`ibr_use_dyn`可以进一步增加混淆强度
#### 展示
```c
int main(int argc, char** argv) {
if (argc <= 0) {
printf("not possible\n");
} else if (argc == 1) {
printf("no args\n");
} else {
printf("%d args\n", argc - 1);
}
return 0;
}
```
IBR全开
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_ibr_full.png)
### 系统调用混淆
将系统调用函数转SVC
#### 展示
```c
int main(int argc, char** argv) {
int r = access("/tmp/1.txt", 0);
printf("r=%d\n", r);
return 0;
}
```
SVC基础
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_svc_base.png)
SVC自定义1
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_svc_usr1.png)
### 拆分指令
将函数指令拆分到整个模块的随机地址
#### 展示
```c
void test(int argc) {
if (argc <= 0) {
printf("not possible\n");
} else if (argc == 1) {
printf("no arg\n");
} else {
printf("%d args\n", argc - 1);
}
}
int main(int argc, const char** argv) {
if (argc <= 0) {
printf("not possible\n");
} else if (argc == 1) {
printf("no arg\n");
} else if (argc == 2) {
printf("1 arg\n");
} else {
printf("%d args\n", argc - 1);
}
return 0;
}
```
SPLIT全开
!(https://raw.githubusercontent.com/lich4/sllvm/refs/heads/main/screenshots/sllvm_split_full.png)
### 内联子函数
将指定函数的所有子函数内联到当前函数中
### 安全防护
将反调试逻辑插入到函数中
页:
[1]