frida分析ollvm字符串混淆
字数 2276 2025-08-29 08:29:58
Frida分析OLLVM字符串混淆教学文档
前言
本教程将详细介绍如何使用Frida分析经过OLLVM字符串混淆处理的Android原生库(so文件)。我们将分析三个不同版本的APK,它们具有相同的Java层逻辑,但SO库采用了不同的OLLVM字符串混淆技术。
准备工作
示例APK下载:
- 链接: https://pan.baidu.com/s/1zTCqXvG9PHxOQAqfoJS-1g
- 提取码: ttmm
工具准备:
- Frida
- IDA Pro或其他反编译工具
- Android设备或模拟器
Java层分析
三个APK的Java层逻辑一致,主要功能:
- 初始界面显示欢迎字符串(通过
stringFromJNI函数生成) - 点击sign1按钮会更新显示类似hash的字符串(通过
sign1函数生成)
这两个函数都是JNI函数,因此我们需要转向SO层分析。
SO层分析
版本0分析(hellojni_2.0.0.apk)
静态注册与动态注册
stringFromJNI:静态注册,可在导出表中直接搜索到sign1:动态注册,导出表中无法直接搜索到
stringFromJNI函数分析
- 函数直接将
stru_37010指向的内容转换为UTF-8编码字符串返回给Java层 stru_37010以64位双字对形式定义,初始值可能是返回字符串的原始数据- 众多
byte_开头的变量(每个占1字节)参与数据处理 - 通过
DATA XREF注释可知这些数据在多处被引用,参与如datadiv_decode8846988481537047047等函数的复杂处理流程
字符串解密特征
- 导出表中可搜索到
datadiv_decode函数 - 解密函数对字符串的每一位进行异或操作
- 手动还原较麻烦,可使用Frida从内存中dump解码后的字符串
解密函数加载时机
- 使用IDA快捷键
Ctrl+S跳转到.init_array段 - 发现三个decode函数都在
.init_array中加载 - 因此可以直接hook
37010地址获取解密后的字符串
sign1函数分析
- 最终结果存储在v19中返回
- 进入
37040同样可以看到datadiv_decode函数 - 同样可以dump解密后的字符串
版本0特征总结
- 导出表中出现
.datadiv_decode是OLLVM字符串混淆的特征 - 解密发生在初始化时(
.init_array段)
版本1分析(hellojni_2.0.1.apk)
主要区别
- 从
stringFromJNI里的37010进入未发现datadiv_decode函数 - 导出表中也没有
datadiv_decode - 发现类似
std__string___4921590060622252445的交叉引用
解密函数分析
std__string___4921590060622252445实际上就是之前的datadiv_decode函数- 加载时机仍在
.init_array中 - 可以直接打印字符串获取明文
版本1特征总结
- 无法在导出表中看到OLLVM字符串混淆的明显特征
- 解密函数名称变化,但功能相同
- 解密仍在
.init_array中加载 - 这种情况在64位架构中更常见
版本2分析(hellojni_2.0.2.apk)
主要变化
.init_array中没有解密函数的踪迹- 解密时机改为运行时
stringFromJNI函数分析
- 将
3E160数组转换为Java字符串返回 - 解密逻辑:
byte_22E80[i + 21 + -18 * (i / 0x12)] ^ byte_22E80[i + 39] byte_22E80是映射表- 打印
0x3E160可获取欢迎字符串
sign1函数分析
- 解密代码不在函数内部
- 需要在
JNI_OnLoad中查找解密逻辑 - 发现映射表出现在
JNI_OnLoad中 - 解密发生在
JNI_OnLoad中
版本2特征总结
- 解密时机改为运行时(
JNI_OnLoad) - 没有明显的
.datadiv_decode特征 - 需要分析运行时解密逻辑
Frida Hook脚本示例
版本0和1的Hook脚本
// Hook .init_array中的解密函数
Interceptor.attach(Module.findBaseAddress('libhello-jni.so').add(0x37010), {
onLeave: function(retval) {
console.log("Decrypted string: " + Memory.readUtf8String(retval));
}
});
// Hook sign1函数的解密结果
Interceptor.attach(Module.findBaseAddress('libhello-jni.so').add(0x37040), {
onLeave: function(retval) {
console.log("Sign1 result: " + Memory.readUtf8String(retval));
}
});
版本2的Hook脚本
// Hook JNI_OnLoad中的解密过程
Interceptor.attach(Module.findExportByName('libhello-jni.so', 'JNI_OnLoad'), {
onLeave: function(retval) {
var decryptedStr = Memory.readUtf8String(Module.findBaseAddress('libhello-jni.so').add(0x3E160));
console.log("Decrypted welcome string: " + decryptedStr);
}
});
// Hook sign1函数的解密结果
Interceptor.attach(Module.findBaseAddress('libhello-jni.so').add(0x3E160), {
onLeave: function(retval) {
console.log("Sign1 result: " + Memory.readUtf8String(retval));
}
});
总结与识别特征
OLLVM字符串混淆的识别特征
-
版本0:
- 导出表中可见
.datadiv_decode函数 - 解密发生在
.init_array段 - 字符串以加密形式存储在数据段
- 导出表中可见
-
版本1:
- 导出表中无
.datadiv_decode函数 - 解密函数名称变化(如
std__string_前缀) - 解密仍在
.init_array中 - 64位架构中更常见
- 导出表中无
-
版本2:
- 无
.init_array中的解密函数 - 解密逻辑在
JNI_OnLoad或运行时 - 需要分析运行时解密算法
- 无
分析方法总结
- 首先确定解密时机(
.init_array或JNI_OnLoad) - 查找解密函数或解密逻辑
- 使用Frida在内存中dump解密后的字符串
- 对于运行时解密,需要分析解密算法
应对策略
-
对于初始化时解密:
- Hook
.init_array中的解密函数 - 或直接Hook加密字符串的内存地址
- Hook
-
对于运行时解密:
- 分析
JNI_OnLoad函数 - 跟踪字符串使用过程,找到解密点
- Hook解密后的内存位置
- 分析
通过本教程,您应该能够掌握使用Frida分析不同OLLVM字符串混淆技术的方法,并能够根据不同的混淆特征选择合适的分析策略。