移动安全-Frida动态Hook安卓So层加密函数实战
1. 为什么需要Hook安卓So层加密函数在移动安全领域Hook技术一直扮演着重要角色。特别是在金融、游戏等对安全性要求较高的APP中开发者往往会将核心加密逻辑放在So层即Native层来实现。这种做法主要有三个原因一是So层的代码执行效率更高二是So层的代码更难被逆向分析三是So层可以使用更复杂的加密算法。我遇到过不少案例一些金融APP会把关键的数据加密函数放在So层比如支付密码的加密、通信数据的签名等。这些函数通常会被开发者用各种手段保护起来比如加壳、混淆、反调试等。这时候如果我们需要分析这些加密逻辑或者需要绕过某些安全校验Hook So层函数就成了必不可少的技能。Frida作为目前最流行的动态插桩工具它最大的优势就是可以同时Hook Java层和Native层的代码。相比其他工具Frida的API设计非常友好脚本编写也很灵活。在实际工作中我发现用Frida来Hook So层函数可以实时查看函数的输入输出甚至修改返回值这对安全测试和逆向分析来说简直是神器。2. 准备工作与环境搭建2.1 基础环境配置在开始Hook之前我们需要准备好基础环境。首先是一台已经root的安卓设备或者模拟器我推荐使用Genymotion模拟器它的兼容性比较好。然后需要安装Frida服务端这里要注意版本匹配问题客户端和服务端的版本必须一致否则会出现各种奇怪的问题。安装Frida服务端的命令很简单adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server 在PC端我们需要安装Python环境和Frida客户端pip install frida-tools2.2 目标APP分析工具除了Frida我们还需要一些辅助工具来分析目标APP。IDA Pro是必不可少的它可以帮助我们静态分析So文件。另外JADX或GDA可以用来反编译APK查看Java层的代码逻辑。我通常会先用这些工具静态分析目标APP找到可能的关键函数然后再用Frida进行动态Hook。这样的组合拳效率最高。比如在分析一个游戏APP时我首先用JADX找到了支付相关的Java代码然后顺着调用链找到了So层的加密函数最后用Frida成功Hook了这个函数。3. Hook有导出函数的实战3.1 识别有导出函数有导出函数是指在So文件的导出表中可以找到的函数。这类函数通常具有明显的函数名比如Java开头的JNI函数或者开发者自定义的导出函数。在IDA中我们可以在Exports窗口直接看到这些函数。我最近分析的一个金融APP中就发现了一个名为encryptData的导出函数。通过观察它的调用关系确认这就是用来加密交易数据的函数。这种有明确函数名的导出函数Hook起来是最方便的。3.2 编写Hook脚本对于有导出函数我们可以直接使用Module.findExportByName来获取函数地址。下面是一个典型的Hook脚本示例// 定义要Hook的So文件名和函数名 var soName libencrypt.so; var funcName encryptData; // 获取函数地址 var funcAddr Module.findExportByName(soName, funcName); console.log(函数地址: funcAddr); // 开始Hook Interceptor.attach(funcAddr, { onEnter: function(args) { console.log(进入加密函数); // 打印输入参数 for(var i0; i3; i) { console.log(参数i: args[i]); } }, onLeave: function(retval) { console.log(离开加密函数); // 打印返回值 console.log(返回值: retval); } });这个脚本会在函数进入时打印前三个参数在函数退出时打印返回值。在实际使用中我们可以根据需要修改args和retval的值实现参数的篡改。3.3 常见问题解决在实际操作中有几个常见问题需要注意。首先是So文件的加载时机问题。有些So文件是在APP运行时才动态加载的如果Hook时机过早会找不到目标函数。解决方法是在脚本中添加延迟或者监听模块加载事件// 监听模块加载 Interceptor.attach(Module.findExportByName(null, dlopen), { onEnter: function(args) { var path args[0].readCString(); if(path.indexOf(libencrypt.so) ! -1) { console.log(目标So已加载可以开始Hook); } } });另一个常见问题是函数名粉碎name mangling。对于C编写的函数编译器会对函数名进行修饰。这时候我们需要在IDA中查看实际的函数名或者在脚本中使用模糊匹配// 模糊匹配函数名 var exports Module.enumerateExportsSync(libencrypt.so); for(var i0; iexports.length; i) { if(exports[i].name.indexOf(encrypt) ! -1) { console.log(找到可能的目标函数: exports[i].name); } }4. Hook无导出函数的技巧4.1 定位无导出函数无导出函数是指那些没有出现在So文件导出表中的函数。这类函数通常是被开发者刻意隐藏的比如使用__attribute__((visibility(hidden)))修饰的函数。Hook这类函数的关键是准确定位它们在内存中的地址。我常用的定位方法有三种字符串引用定位、特征码定位和调用链定位。字符串引用定位是最简单有效的如果目标函数中使用了特定的字符串我们可以先在IDA中找到这个字符串然后查看哪些函数引用了它。4.2 计算函数偏移地址找到目标函数后我们需要记录它在So文件中的偏移地址。在IDA中这个地址通常是sub_XXXX的形式。有了这个偏移地址我们就可以在运行时计算出实际的内存地址var soName libsecurity.so; var offset 0x1234; // 在IDA中找到的函数偏移 // 计算实际地址 var baseAddr Module.findBaseAddress(soName); var funcAddr baseAddr.add(offset); console.log(计算出的函数地址: funcAddr);4.3 高级Hook技巧对于特别隐蔽的函数我们可能需要更高级的Hook技巧。比如有些函数会被加壳保护在内存中的代码是加密的。这时候就需要先找到解壳的时机在代码解密后再进行Hook。我曾经遇到过一个游戏APP它的关键校验函数被VMP加壳保护。我的做法是先Hook内存分配函数监控可疑的内存区域等到代码解密完成后再进行Hook。具体代码如下// 监控内存分配 var malloc Module.findExportByName(null, malloc); Interceptor.attach(malloc, { onLeave: function(retval) { var size this.returnValue; if(size 0x1000) { // 监控大内存分配 console.log(分配了大内存块: retval size: size); // 这里可以添加对内存区域的扫描逻辑 } } });另一个有用的技巧是Hook函数调用指令。有些函数虽然本身被隐藏了但它的调用点是可见的。我们可以先找到调用指令的位置然后修改调用指令来实现Hook。5. 实战案例分析5.1 金融APP加密函数Hook让我们来看一个真实的金融APP案例。这个APP在So层实现了一个RSA加密函数用来加密用户的交易数据。首先用IDA分析libsecurity.so发现了一个名为native_rsa_encrypt的导出函数。Hook脚本如下var soName libsecurity.so; var funcName native_rsa_encrypt; var encryptFunc Module.findExportByName(soName, funcName); console.log(加密函数地址: encryptFunc); Interceptor.attach(encryptFunc, { onEnter: function(args) { console.log(\n 进入加密函数 ); // 第一个参数是输入数据 var inputData args[0].readByteArray(256); console.log(输入数据: bytesToHex(inputData)); // 第二个参数是数据长度 var length args[1].toInt32(); console.log(数据长度: length); }, onLeave: function(retval) { // 返回值是加密后的数据 var outputData retval.readByteArray(256); console.log(加密结果: bytesToHex(outputData)); console.log( 离开加密函数 \n); } }); // 辅助函数字节数组转十六进制字符串 function bytesToHex(bytes) { var hex []; for(var i0; ibytes.length; i) { hex.push((0bytes[i].toString(16)).slice(-2)); } return hex.join( ); }通过这个脚本我们成功捕获了APP发送的加密数据并分析出了加密算法的具体实现。5.2 游戏APP校验函数绕过另一个案例是一个游戏APP的So层校验函数。这个函数没有导出但通过字符串引用我们在IDA中找到了它sub_89A10。这个函数负责验证游戏内购的收据是否有效。Hook脚本如下var soName libgame.so; var offset 0x89A10; // 函数偏移 var baseAddr Module.findBaseAddress(soName); var verifyFunc baseAddr.add(offset); Interceptor.attach(verifyFunc, { onLeave: function(retval) { // 原始返回值是校验结果1表示成功0表示失败 console.log(原始校验结果: retval); // 强制修改返回值为1成功 retval.replace(1); console.log(修改后的校验结果: retval); } });这个Hook成功绕过了游戏的付费验证不过需要强调的是这种技术只能用于安全研究和授权测试切勿用于非法用途。6. 高级技巧与注意事项6.1 处理反调试和反Hook很多安全性较高的APP会检测Frida等调试工具的存在。常见的手段包括检查进程名、端口、内存特征等。要绕过这些检测我们可以采取以下措施修改Frida-server的名称和端口# 重命名frida-server adb shell mv /data/local/tmp/frida-server /data/local/tmp/androidsvc # 指定非默认端口 adb shell /data/local/tmp/androidsvc -l 127.0.0.1:8888在Hook脚本中主动绕过检测// 绕过常见的反调试函数 var pthread_create Module.findExportByName(null, pthread_create); Interceptor.attach(pthread_create, { onEnter: function(args) { var funcName args[2].readCString(); if(funcName funcName.indexOf(anti_debug) ! -1) { console.log(检测到反调试线程创建已阻止); this.returnValue 0; // 阻止线程创建 } } });6.2 性能优化技巧Hook So层函数可能会影响APP的性能特别是对那些被频繁调用的函数。以下是一些优化建议尽量减少onEnter和onLeave中的操作特别是避免复杂的字符串处理。对于高频调用的函数可以设置条件Hookvar count 0; Interceptor.attach(targetFunc, { onEnter: function(args) { if(count % 100 0) { // 每100次采样一次 console.log(采样数据: args[0]); } } });使用CModule提升性能const cm new CModule( #include stdint.h void fast_process(uint8_t* data, int len) { // 高性能的C代码处理 } ); Interceptor.attach(targetFunc, { onEnter: function(args) { cm.fast_process(args[0], args[1]); } });6.3 多线程环境下的HookSo层函数常常会在多线程环境下被调用这可能导致Hook脚本出现竞态条件。为了确保线程安全我们可以使用Frida的Thread.backtrace来识别调用线程Interceptor.attach(targetFunc, { onEnter: function(args) { var backtrace Thread.backtrace(this.context, Backtracer.ACCURATE); console.log(调用栈: backtrace.join(, )); } });对共享数据加锁var lock new Lock(); Interceptor.attach(targetFunc, { onEnter: function(args) { lock.acquire(); try { // 处理共享数据 } finally { lock.release(); } } });在实际项目中我遇到过因为线程安全问题导致的Hook脚本崩溃。后来通过添加完善的日志和锁机制成功解决了这个问题。关键是要理解目标函数的调用上下文有针对性地设计Hook策略。