Frida实战:3种OLLVM字符串加密的破解姿势与自动化脚本分享
在Android应用安全研究领域,OLLVM(Obfuscator-LLVM)的字符串加密混淆一直是逆向工程师需要面对的常见挑战。当你打开一个被混淆的so文件,发现所有关键字符串都变成了难以理解的byte_xxxx数据块时,那种感觉就像是在黑暗中摸索。但别担心,今天我将分享三种实战中遇到的OLLVM字符串加密场景,以及如何用Frida这把瑞士军刀来优雅地解决它们。
我遇到过不少开发者,他们面对OLLVM字符串混淆时往往感到无从下手。有的尝试手动分析每个解密函数,结果在IDA的伪代码海洋中迷失方向;有的试图静态解密,却发现解密逻辑分散在多个地方;还有的甚至想放弃,直接跳过字符串分析。实际上,只要掌握了正确的方法,这些加密字符串都能被轻松还原。本文将带你深入三种典型场景,从简单的.datadiv特征函数到复杂的运行时解密,提供可直接复用的Frida脚本模板和IDA分析技巧。
1. 基础场景:.datadiv特征函数的快速定位与解密
第一种情况是最经典的OLLVM字符串加密模式,也是新手最容易上手处理的类型。当你用IDA打开so文件,在导出表中看到一堆datadiv_decode开头的函数名时,恭喜你,遇到了最"友好"的混淆版本。
1.1 识别特征与静态分析
在IDA的Exports窗口中按Name排序,你会看到类似这样的函数名:
.datadiv_decode8846988481537047047
.datadiv_decode1234567890123456789
这些函数就是OLLVM自动生成的字符串解密函数。每个加密字符串在内存中都以加密形式存储,使用时通过对应的datadiv_decode函数进行解密。静态分析时,你看到的代码可能是这样的:
// 典型的datadiv_decode函数结构
void __fastcall datadiv_decode_xxxx(char *encrypted_str) {
for (int i = 0; i < strlen(encrypted_str); i++) {
encrypted_str[i] ^= 0xD2; // 简单的异或加密
}
}
注意:实际的解密算法可能比简单的异或复杂,但基本原理相同——在内存中修改加密数据,使其变为可读字符串。
1.2 Frida动态Hook方案
既然解密发生在运行时,我们可以用Frida在内存中直接捕获解密后的字符串。关键是要找到正确的Hook时机和位置。
Hook时机判断:.init_array段是这类解密最常见的发生位置。在Android的so加载过程中,.init_array中的函数会在库初始化时自动执行。我们可以通过以下方式验证:
// 检查.init_array中的解密函数
function analyze_init_array() {
var module = Process.findModuleByName("libtarget.so");
if (!module) return;
// 获取.init_array段
var init_array = module.enumerateSections().filter(function(section) {
return section.name === ".init_array";
});
if (init_array.length > 0) {
var init_array_addr = init_array[0].address;
var init_array_size = init_array[0].size;
console.log(`[+] .init_array found at ${init_array_addr}, size: ${init_array_size}`);
// 遍历.init_array中的函数指针
for (var i = 0; i < init_array_size / Process.pointerSize; i++) {
var func_ptr = init_array_addr.add(i * Process.pointerSize).readPointer();
if (!func_ptr.isNull()) {
console.log(` Function ${i}: ${func_ptr}`);
// 可以进一步分析这些函数
}
}
}
}
通用解密Hook脚本:针对.datadiv_decode类型的解密,我常用的脚本模板如下:
function hook_datadiv_decrypt() {
var target_module = "libtarget.so";
var module_base = Module.findBaseAddress(target_module);
if (!module_base) {
console.log(`[-] Module ${target_module} not loaded yet`);
// 等待模块加载
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function(args) {
var path = ptr(args[0]).readCString();
if (path && path.indexOf(target_module) !== -1) {
console.log(`[+] ${target_module} loaded, hooking...`);
setTimeout(hook_datadiv_decrypt, 100);
}
}
});
return;
}
console.log(`[+] Module base: ${module_base}`);
// 方法1:Hook所有.datadiv_decode函数
var exports = Module.enumerateExportsSync(target_module);
var decode_funcs = exports.filter(function(exp) {
return exp.name.indexOf("datadiv_decode") === 0;
});
console.log(`[+] Found ${decode_funcs.length} datadiv_decode functions`);
decode_funcs.forEach(function(func) {
Interceptor.attach(func.address, {
onEnter: function(args) {
this.encrypted_str = args[0];
console.log(`[+] datadiv_decode called: ${func.name}`);
console.log(` Encrypted string ptr: ${this.encrypted_str}`);
},
onLeave: function(retval) {
// 解密完成后,字符串已经修改为明文
var decrypted = ptr(this.encrypted_str).readCString();
console.log(` Decrypted: ${decrypted}`);
// 可选:将解密结果保存到全局变量供后续使用
if (!global.decrypted_strings) {
global.decrypted_strings = [];
}
global.decrypted_strings.push({
address: this.encrypted_str,
value: decrypted,
function: func.name
});
}
});
});
// 方法2:直接扫描内存中的加密字符串区域
// 通常加密字符串集中在.data或.rodata段
var data_sections = Module.enumerateSectionsSync(target_module)
.filter(function(section) {
return section.name === ".data" || section.name === ".rodata";
});
data_sections.forEach(function(section) {
console.log(`[+] Scanning ${section.name} at ${section.address} (size: ${section.size})`);
// 这里可以添加内存扫描逻辑,识别加密字符串模式
});
}
1.3 实战技巧与注意事项
在实际操作中,我发现有几个细节需要特别注意:
字符串长度识别:加密字符串通常以空字符结尾,但有时解密函数会接收长度参数。你需要观察调用约定:
// 如果解密函数有长度参数
Interceptor.attach(decode_func.address, {
onEnter: function(args) {
this.str_ptr = args[0];
this.str_len = args[1].toInt32();
console.log(`Decrypting ${this.str_len} bytes at ${this.str_ptr}`);
},
onLeave: function(retval) {
// 读取指定长度的字符串
var decrypted = ptr(this.str_ptr).readByteArray(this.str_len);
console.log(hexdump(decrypted));
}
});
多线程环境处理:如果解密函数可能在多线程环境下调用,需要确保Hook代码是线程安全的:
var string_cache = {};
Interceptor.attach(decode_func.address, {
onEnter: function(args) {
this.tid = Process.getCurrentThreadId();
this.str_ptr = args[0];
this.timestamp = Date.now();
},
onLeave: function(retval) {
var key = `${this.str_ptr}-${this.tid}-${this.timestamp}`;
if (!string_cache[key]) {
var decrypted = ptr(this.str_ptr).readCString();
string_cache[key] = decrypted;
console.log(`[Thread ${this.tid}] Decrypted: ${decrypted}`);
}
}
});
性能考虑:频繁的Hook可能会影响应用性能。对于性能敏感的场景,可以考虑批量处理:
// 批量解密模式
var pending_decrypts = [];
function batch_decrypt_handler() {
if (pending_decrypts.length > 0) {
var batch = pending_decrypts.splice(0, 10); // 每次处理10个
batch.forEach(function(item) {
var decrypted = ptr(item.address).readCString();
console.log(`Batch decrypted: ${decrypted}`);
});
}
}
// 设置定时器,每100ms处理一批
setInterval(batch_decrypt_handler, 100);
2. 进阶场景:init_array中的隐式解密函数
第二种情况稍微复杂一些——在64位应用中,你可能在导出表中找不到明显的datadiv_decode函数。这时候,.init_array段就成了我们的突破口。
2.1 识别无特征解密
当你在IDA中搜索不到明显的解密函数时,第一步应该是检查.init_array。按下Ctrl+S打开段视图,找到.init_array段:
| 段名 | 地址范围 | 大小 | 说明 |
|---|---|---|---|
| .init_array | 0x0000F000-0x0000F018 | 24字节 | 初始化函数数组 |
| .fini_array | 0x0000F018-0x0000F020 | 8字节 | 终止函数数组 |
双击.init_array段,你会看到一系列函数指针。这些函数在so加载时自动执行,字符串解密逻辑通常就隐藏在这里。
2.2 动态分析与Hook策略
对于这种情况,我们需要更精细的动态分析。以下是我常用的分析脚本:
function analyze_init_array_decrypt() {
var target_so = "libtarget.so";
var module = Process.findModuleByName(target_so);
if (!module) {
console.log(`[-] ${target_so} not loaded`);
return;
}
// 查找.init_array
var init_array_section = null;
var sections = module.enumerateSections();
for (var i = 0; i < sections.length; i++) {
var section = sections[i];
if (section.name.indexOf(".init_array") !== -1) {
init_array_section = section;
break;
}
}
if (!init_array_section) {
console.log("[-] .init_array section not found");
return;
}
console.log(`[+] .init_array found at ${init_array_section.address}, size: ${init_array_section.size}`);
// Hook .init_array中的所有函数
var pointer_size = Process.pointerSize;
var count = init_array_section.size / pointer_size;
for (var i = 0; i < count; i++) {
var func_ptr = init_array_section.address.add(i * pointer_size).readPointer();
if (!func_ptr.isNull()) {
console.log(`[+] Hook init function ${i} at ${func_ptr}`);
Interceptor.attach(func_ptr, {
onEnter: function(args) {
this.func_index = i;
console.log(`[+] init_array[${this.func_index}] called`);
// 记录寄存器状态,有助于分析解密逻辑
if (Process.arch === 'arm64') {
this.reg_x0 = this.context.x0;
this.reg_x1 = this.context.x1;
// 可以记录更多寄存器
}
},
onLeave: function(retval) {
// 函数执行后,检查常见字符串区域是否被修改
check_string_regions();
}
});
}
}
}
// 监控常见字符串区域的变化
var monitored_regions = [];
function setup_string_monitoring() {
var module = Process.findModuleByName("libtarget.so");
// 通常字符串存储在.data、.rodata、.data.rel.ro等段
var string_sections = [".data", ".rodata", ".data.rel.ro", ".rodata.str"];
string_sections.forEach(function(section_name) {
var section = module.enumerateSections().find(function(s) {
return s.name === section_name;
});
if (section) {
// 创建内存访问监控
MemoryAccessMonitor.enable({
base: section.address,
size: section.size
}, {
onAccess: function(details) {
// 监控写入操作
if (details.operation === 'write') {
cons

595

被折叠的 条评论
为什么被折叠?



