利用Frida 分析OLLVM字符串加密算法还原
字数 991 2025-08-19 12:41:34
利用Frida分析OLLVM字符串加密算法还原
1. 背景介绍
本文档详细记录了如何利用Frida工具分析并还原OLLVM字符串加密算法的过程。OLLVM(Obfuscator-LLVM)是一个开源的代码混淆工具,常用于Android Native代码的保护,其中字符串加密是其重要功能之一。
2. 目标分析
2.1 Java层分析
目标应用包含一个HelloJni类,关键代码如下:
public class HelloJni extends AppCompatActivity {
TextView tv;
public native String sign1(String str);
public native String stringFromJNI();
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.activity_hello_jni);
this.tv = (TextView) findViewById(R.id.hello_textview);
this.tv.setText(stringFromJNI());
((Button) findViewById(R.id.button_sign1)).setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
HelloJni.this.tv.setText(
HelloJni.this.sign1(RandomStringUtils.randomAscii(16))
);
}
}
);
}
static {
System.loadLibrary("hello-jni");
}
}
关键点:
sign1是native方法,接收一个随机16字节ASCII字符串- 点击按钮时会调用
sign1方法并将结果显示在TextView上
2.2 Native层分析
2.2.1 JNI_OnLoad分析
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
// ... 省略变量声明 ...
v8 = sub_E76C; // sign1函数的地址
v7 = *(_OWORD *)&sign1; // 注册sign1函数
v5 = (*v6)->FindClass(v6, &xmmword_37050);
if (!v5 || (((*v4)->RegisterNatives)(v4, v5, &v7, 1LL) & 0x80000000) != 0) {
return -1;
}
return v2;
}
关键点:
- 使用
RegisterNatives注册native方法 sub_E76C是sign1函数的实现地址- 由于OLLVM字符串加密,直接分析困难
2.2.2 sign1函数分析
jstring __fastcall sign1(JNIEnv *env, __int64 a2, void *a3) {
const char *v5 = (*env)->GetStringUTFChars(env, a3, 0LL); // 获取输入字符串
// ... 省略中间处理代码 ...
sub_103F0(v10, v11, &value); // 关键加密函数
// 将value的每个字节格式化为16进制字符串
sprintf(s, &byte_37040, (unsigned __int8)value);
sprintf(&s[2], &byte_37040, BYTE1(value));
// ... 省略类似的sprintf调用 ...
jstring v12 = (*env)->NewStringUTF(env, s); // 返回结果
return v12;
}
关键点:
- 获取输入字符串后调用
sub_103F0进行加密 - 将加密结果格式化为16进制字符串返回
3. Frida辅助分析
3.1 定位sign1函数地址
使用Frida脚本findSign1.js:
function hook_sign1() {
var module_libart = Process.findModuleByName("libart.so");
var addr_RegisterNatives = null;
// 枚举符号找到RegisterNatives
var symbols = module_libart.enumerateSymbols();
for (var i = 0; i < symbols.length; i++) {
if (symbols[i].name.indexOf("RegisterNatives") > 0) {
addr_RegisterNatives = symbols[i].address;
}
}
// Hook RegisterNatives
Interceptor.attach(addr_RegisterNatives, {
onEnter: function(args) {
var java_class = Java.vm.tryGetEnv().getClassName(args[1]);
var methods = args[2];
var method_count = parseInt(args[3]);
for (var i = 0; i < method_count; i++) {
// 打印方法名和签名
console.log(methods.add(i*Process.pointerSize*3).readPointer().readCString());
console.log(methods.add(i*Process.pointerSize*3+Process.pointerSize).readPointer().readCString());
// 获取函数地址
var fnPtr = methods.add(i*Process.pointerSize*3+Process.pointerSize*2).readPointer();
var module_so = Process.findModuleByAddress(fnPtr);
console.log(module_so.name + " addr: " + fnPtr.sub(module_so.base));
}
}
});
}
运行命令:
frida -U -f 包名 -l findSign1.js
结果:
- 找到sign1函数地址为
0xE76C
3.2 固定输入值
为了便于分析,固定Java层输入:
function hook_java() {
Java.perform(function() {
var hellojni = Java.use("com.example.hellojni.HelloJni");
hellojni.sign1.implementation = function(args) {
return this.sign1("0123456789abcdef"); // 固定输入
}
});
}
3.3 分析加密过程
3.3.1 Hook关键函数
function hook_native() {
var base_hello_jni = Module.findBaseAddress("libhello-jni.so");
// Hook sub_FD90 (结果处理函数)
Interceptor.attach(base_hello_jni.add(0xFD90), {
onEnter: function(args) {
this.arg0 = args[0];
this.arg1 = args[1];
},
onLeave: function(retval) {
console.log("0xFD90:\r\n", hexdump(this.arg0), "\r\n", hexdump(this.arg1));
}
});
// Hook sub_F008 (疑似MD5加密函数)
Interceptor.attach(base_hello_jni.add(0xF008), {
onEnter: function(args) {
this.arg0 = args[0];
this.arg1 = args[1];
},
onLeave: function(retval) {
console.log("0xF008:\r\n", hexdump(this.arg0), "\r\n", hexdump(this.arg1));
console.log("0xF008:", ptr(this.arg1).readCString());
}
});
}
3.3.2 固定随机数
function hook_libc() {
var lrand48 = Module.findExportByName("libc.so", "lrand48");
Interceptor.attach(lrand48, {
onLeave: function(retval) {
retval.replace(0xAAAAAAAA); // 固定随机数返回值
}
});
}
4. 算法还原
通过Frida分析发现:
- 输入字符串为固定值"0123456789abcdef"
- 在加密前会拼接"salt2+"前缀,形成"salt2+0123456789abcdef"
- 对该字符串进行MD5加密
- 将MD5结果格式化为16进制字符串返回
验证:
md5("salt2+0123456789abcdef") = 5a6ecb4b69e035e521bf582135281509
与应用程序输出一致,确认算法还原成功。
5. 总结
完整流程:
- Java层调用native方法
sign1,传入随机16字节字符串 - Native层拼接固定"salt2+"前缀
- 对拼接后的字符串进行MD5加密
- 将MD5结果格式化为16进制字符串返回
关键技术点:
- 使用Frida的
RegisterNativesHook定位native函数地址 - 通过固定输入简化分析过程
- 识别并Hook关键加密函数
- 分析加密前的字符串处理过程
- 验证加密算法
通过这种方法,可以有效分析OLLVM字符串加密保护的Native代码逻辑。