基于Frida的OLLVM混淆代码动态分析技术研究
字数 1841 2025-12-23 12:14:29
基于Frida的OLLVM混淆代码动态分析技术教学文档
前言
在移动应用安全领域,代码混淆技术是保护商业应用知识产权、抵御逆向工程的重要手段。OLLVM(Obfuscator-LLVM)作为强大的开源混淆工具,通过字符串加密、控制流程平坦化、指令替换等技术显著增加逆向分析难度。传统静态分析方法难以应对复杂混淆,需要Frida等动态插桩框架进行辅助分析。
一、Frida辅助分析OLLVM字符串加密
1.1 字符串加密原理
字符串加密在编译时对明文字符串进行加密存储,避免以明文形式存在于二进制文件中,增加静态分析难度。
1.2 实例分析
1.2.1 Java层与Native层交互
- Java层调用native函数
sign1,模块在libhello-jni.so - Native层导出表搜索无目标函数,推测为动态注册
- 分析
JNI_OnLoad函数:
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
// 获取JNI环境
if ((*vm)->GetEnv(vm, &env, 65540LL)) return -1;
// 动态注册Native方法
jclass clazz = (*env)->FindClass(env, "com/example/hellojni/HelloJni");
JNINativeMethod methods[] = {{"sign1", "()Ljava/lang/String;", (void*)sub_E76C}};
(*env)->RegisterNatives(env, clazz, methods, 1);
return 65540;
}
1.2.2 字符串解密机制
JNINativeMethod结构体中字符串被加密(显示为"ycmd;"等乱码)- 交叉引用发现
.datadiv_decode解密函数被.init_array段调用 - 程序运行时自动执行解密操作
1.3 Frida动态分析技术
1.3.1 Hook Dump方法
function print_hex_dump(addr) {
var libhello_base = Module.findBaseAddress("libhello-jni.so");
console.log(hexdump(libhello_base.add(addr)));
}
function hooknative() {
print_hex_dump(0x037070); // 解密字符串地址
}
1.3.2 Hook RegisterNatives方法
function hook_libart() {
var module_libart = Process.findModuleByName("libart.so");
var symbols = module_libart.enumerateSymbols();
for (var i = 0; i < symbols.length; i++) {
var name = symbols[i].name;
if (name.indexOf("RegisterNatives") > 0 && name.indexOf("CheckJNI") == -1) {
Interceptor.attach(symbols[i].address, {
onEnter: function(args) {
var java_class = Java.vm.tryGetEnv().getClassName(args[1]);
var method_count = parseInt(args[3]);
console.log("RegisterNatives - Class:", java_class, "Methods:", method_count);
// 遍历并打印所有注册的Native方法
for (var i = 0; i < method_count; i++) {
var method_ptr = args[2].add(i * Process.pointerSize * 3);
console.log("Method Name:", method_ptr.readPointer().readCString());
console.log("Signature:", method_ptr.add(Process.pointerSize).readPointer().readCString());
}
}
});
}
}
}
二、Frida辅助分析OLLVM控制流程平坦化
2.1 控制流程平坦化原理
将函数原有控制流拆散,平铺到while循环和switch结构中,极大增加逆向分析复杂度。
2.1.1 原始代码示例
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0) return 1;
else return 10;
return 0;
}
2.1.2 平坦化后代码
int main(int argc, char** argv) {
int a = atoi(argv[1]);
int b = 0;
while(1) {
switch(b) {
case 0:
if(a == 0) b = 1;
else b = 2;
break;
case 1: return 1;
case 2: return 10;
default: break;
}
}
return 0;
}
2.2 动态分析策略
2.2.1 关键点Hook技术
- 从输入参数开始逆向追踪
- 从输出结果正向追踪
- 交替使用两种分析方式
2.2.2 Hook NewStringUTF获取返回结果
function hook_libart() {
var module_libart = Process.findModuleByName("libart.so");
var symbols = module_libart.enumerateSymbols();
for (var i = 0; i < symbols.length; i++) {
var name = symbols[i].name;
if (name.indexOf("NewStringUTF") > 0) {
Interceptor.attach(symbols[i].address, {
onEnter: function(args) {
console.log("[NewStringUTF Result]:", ptr(args[1]).readCString());
}
});
}
}
}
2.2.3 固定输入参数简化分析
function hook_java() {
Java.perform(function() {
var HelloJni = Java.use("com.example.hellojni.HelloJni");
HelloJni.sign2.implementation = function(arg0) {
// 固定输入参数,排除干扰
var result = this.sign2("adkliowpwkklsap", "0987654321adksoi");
console.log("[Fixed Input Result]:", result);
return result;
};
});
}
2.3 实际案例分析
2.3.1 算法识别过程
- Hook
NewStringUTF得到密文:ea0973cd62f76a7336874be31cfd7540 - 逆向追踪发现base64编码特征
- 进一步分析发现MD5加密常量:
0xD76AA478 - 确认加密流程:输入拼接 → Base64 → MD5
2.3.2 关键函数Hook脚本
function hook_native_addr(addr, idb_addr) {
var base = Module.findBaseAddress("libhello-jni.so");
Interceptor.attach(addr, {
onEnter: function(args) {
this.args = [args[0], args[1], args[2], args[3]];
this.lr = this.context.lr;
},
onLeave: function(retval) {
console.log("Function:", ptr(idb_addr));
console.log("LR:", ptr(this.lr).sub(base));
console.log("Args:", this.args.map(hex_dump));
console.log("Return:", hex_dump(retval));
}
});
}
// 批量Hook关键函数
function hook_critical_functions() {
var base = Module.findBaseAddress("libhello-jni.so");
var functions = [0x15F1C, 0x162B8, 0x154D4, 0x158AC, 0x14844];
functions.forEach(addr => hook_native_addr(base.add(addr), addr));
}
三、Frida辅助分析OLLVM指令替换
3.1 指令替换原理
用功能等价但结构更复杂的指令序列替换原始简单指令,保持逻辑不变但增加理解难度。
3.1.1 常见替换规则
-
加法替换:
a = b + c→a = b - (-c)a = b + c→r = rand(); a = b + r; a = a + c; a = a - r
-
减法替换:
a = b - c→a = b + (-c)a = b - c→r = rand(); a = b + r; a = a - c; a = a - r
-
逻辑运算替换:
- AND:
a = b & c→a = (b ^ ~c) & b - OR:
a = b | c→a = (b & c) | (b ^ c) - XOR:
a = a ^ b→a = (~a & b) | (a & ~b)
- AND:
3.2 动态分析技术
3.2.1 寄存器值提取
针对指令替换,通过Inline Hook提取关键寄存器值:
function hook_instruction_replace() {
var base = Module.findBaseAddress("libhello-jni.so");
var target_addr = base.add(0x12345); // 替换指令地址
Interceptor.attach(target_addr, {
onEnter: function(args) {
// 读取关键寄存器值
var w8 = this.context.x8.toInt32();
var w9 = this.context.x9.toInt32();
console.log("Instruction Replacement Analysis:");
console.log("W8:", w8.toString(16));
console.log("W9:", w9.toString(16));
// 记录计算过程
this.calculation_step = 0;
},
onLeave: function(retval) {
// 分析替换指令的计算结果
var final_result = this.context.x8.toInt32();
console.log("Final Result:", final_result.toString(16));
}
});
}
3.2.2 内存访问监控
监控关键内存写入操作,分析指令替换的实际效果:
function monitor_memory_access() {
var target_range = [0x30000, 0x40000]; // 目标内存范围
var base = Module.findBaseAddress("libhello-jni.so");
// 监控内存写入
MemoryAccessMonitor.enable({
base: base.add(target_range[0]),
size: target_range[1] - target_range[0]
}, {
onAccess: function(details) {
if (details.operation === 'write') {
console.log("Memory Write at:", details.address.sub(base));
console.log("Value:", details.value.toInt32());
console.log("From:", details.from.sub(base));
}
}
});
}
四、综合分析与实战技巧
4.1 动态分析工作流
- 环境准备:安装Frida,配置测试环境
- 初步识别:Hook标准库函数确定输入输出
- 参数固定:稳定输入排除干扰因素
- 关键点定位:通过交叉引用确定关键函数
- 逐层分析:从外到内逐步分析加密逻辑
- 算法识别:通过常量、特征识别标准算法
- 验证确认:对比计算结果验证分析正确性
4.2 高级Hook技巧
4.2.1 条件Hook
function conditional_hook(addr, condition) {
Interceptor.attach(addr, {
onEnter: function(args) {
if (condition(args)) {
this.should_log = true;
console.log("Condition met, starting trace...");
} else {
this.should_log = false;
}
},
onLeave: function(retval) {
if (this.should_log) {
console.log("Function completed, return:", retval);
}
}
});
}
4.2.2 调用栈追踪
function trace_call_stack() {
var base = Module.findBaseAddress("libhello-jni.so");
Interceptor.attach(Module.findExportByName("libc.so", "malloc"), {
onEnter: function(args) {
var backtrace = Thread.backtrace(this.context, Backtracer.ACCURATE);
console.log("Call stack for malloc:");
backtrace.forEach(addr => {
console.log(" ", addr.sub(base));
});
}
});
}
五、总结
通过Frida动态分析OLLVM混淆代码,可以有效应对字符串加密、控制流程平坦化和指令替换等混淆技术。关键成功因素包括:
- 系统化的分析思路:从外到内,从输入输出到中间过程
- 精准的Hook定位:针对关键函数和内存访问点
- 耐心的问题排查:混淆代码需要逐层分析和验证
- 经验积累:熟悉常见加密算法特征和混淆模式
本教学文档提供了完整的技术方案和实战代码,可作为分析OLLVM混淆应用的标准化流程参考。