
做Android逆向和协议分析的同行基本都绕不开密文拦截这道坎。Charles抓包拉出来请求体的data字段是一串无规律的Base64sign参数看不出任何生成逻辑想改个参数调试都无从下手。静态反编译dex找加密入口碰到混淆、加固、Native层加密耗上一两天也未必能摸清楚完整逻辑。Frida作为动态插桩工具最大的价值就是绕开静态逆向的繁琐直接在运行时给加密函数“搭探针”函数执行前截获输入的明文、密钥、IV执行后截获输出的密文不用管内部算法怎么实现输入输出直接拿到手。这篇文章从基础原理到Native层实战再到主动调用解密把Frida做加密逆向的完整链路讲透附常用Hook模板和踩坑总结覆盖Java层到Native层90%的常规加密场景。一、Frida Hook加密的核心原理给黑盒开两个观察窗加密函数本质上就是一个输入输出的黑盒输入明文、密钥、偏移量输出密文。静态逆向要掰开黑盒看内部逻辑动态Hook只需要在黑盒的入口和出口各插一个探针就能拿到所有关心的数据。整个过程完全不影响原程序的正常执行原函数该怎么跑还是怎么跑只是在执行前后多执行了我们注入的打印逻辑。这也是动态插桩相比静态逆向最大的优势不用还原算法就能拿到输入输出。核心Hook点位就两个函数入口抓明文参数、密钥、IV、加密模式等所有输入信息函数出口抓加密后的密文、哈希值、签名结果适用场景覆盖从简单到复杂的绝大多数加密逆向需求Java层标准加密库、第三方SDK加密、Native层自定义算法、协议签名函数、甚至是魔改的国密算法。二、Java层加密Hook覆盖绝大多数常规加密场景Java层的加密基本都遵循JCE标准接口Cipher、MessageDigest、Mac这些基类是所有加密实现的入口。Hook这些基类方法就能通杀大部分常规加密不用一个个找具体业务类。2.1 Cipher通用HookAES/DES/RSA通杀javax.crypto.Cipher是所有对称、非对称加密的统一入口只要是标准实现的AES、DES、RSA、SM4最终都会走到doFinal方法。核心Hook脚本function byte2hex(bytes) { let hex []; for (let i 0; i bytes.length; i) { hex.push((0 (bytes[i] 0xFF).toString(16)).slice(-2)); } return hex.join(); } Java.use(javax.crypto.Cipher).doFinal.overload([B).implementation function(input) { let result this.doFinal(input); let algo this.getAlgorithm(); let opmode this.getOpMode() 1 ? 加密 : 解密; console.log(\n Cipher ${opmode} | ${algo} ); console.log(输入长度: ${input.length} 字节); console.log(输入数据: ${byte2hex(input)}); console.log(输出结果: ${byte2hex(result)}); return result; }这段脚本可以拦截所有标准Cipher的加解密操作自动打印算法类型、操作模式、输入输出十六进制数据。拿到十六进制之后配合密钥就能直接还原明文。2.2 哈希与HMAC签名Hook协议里的sign参数绝大多数是MD5、SHA256或者HMAC-SHA1生成的。Hook对应的标准类就能直接拿到签名前的原始参数字符串。// Hook MessageDigest 通用哈希 Java.use(java.security.MessageDigest).digest.overload([B).implementation function(input) { let result this.digest(input); console.log(\n 哈希计算 | ${this.getAlgorithm()} ); console.log(原文: ${Java.use(java.lang.String).$new(input)}); console.log(结果: ${byte2hex(result)}); return result; } // Hook Mac 类 HMAC 签名 Java.use(javax.crypto.Mac).doFinal.overload([B).implementation function(input) { let result this.doFinal(input); console.log(\n HMAC签名 | ${this.getAlgorithm()} ); console.log(签名原文: ${Java.use(java.lang.String).$new(input)}); console.log(签名结果: ${byte2hex(result)}); return result; }很多人找sign签名找半天其实挂上这两段脚本跑一遍请求签名原文和结果直接就打印出来了。2.3 Java层踩坑总结这几个坑是新手百分百会踩的提前避开能省很多时间重载函数必须指定参数类型doFinal有多个重载不写overload大概率Hook不到正确的方法二进制数据不要直接转字符串加密输入输出都是byte数组直接转UTF-8会乱码统一用十六进制输出区分加密和解密模式getOpMode()返回1是加密、2是解密混在一起日志会完全没法看多线程加线程标识高并发场景下日志会乱打印前加上Thread.currentThread().getId()区分调用来源三、Native层加密Hook搞定深层加固的加密逻辑加固过的APP基本都会把核心加密逻辑挪到so库里Java层只留一个空的native接口。这种情况Java层Hook只能拿到调用native的入参和出参想深入分析内部逻辑就要Hook Native层函数。3.1 加密函数定位三招Hook之前先定位函数不用上来就硬扒IDA汇编三个常用方法效率从高到低导出函数直筛先看so的导出表有没有aes_encrypt、sm4_crypt、rsa_process这类命名的函数大概率就是目标直接拿函数名Hook字符串检索定位IDA里搜“encrypt”、“cipher”、“AES”、“key”这些关键词交叉引用找过去基本都在加密函数附近栈回溯追调用链先Hook Java层的native方法打印调用栈顺着地址往下找Native层的入口适合函数名完全被混淆的场景3.2 Native函数Hook模板定位到函数之后用Interceptor.attach挂载Hook读取内存中的参数和返回值。let soName libencrypt.so; let funcAddr Module.findExportByName(soName, aes_encrypt); Interceptor.attach(funcAddr, { onEnter: function(args) { this.inputPtr args[0]; this.inputLen args[1].toInt32(); this.keyPtr args[2]; console.log(\n Native AES 加密 ); console.log(输入长度: ${this.inputLen}); console.log(输入明文:); console.log(hexdump(this.inputPtr, { length: this.inputLen })); console.log(密钥:); console.log(hexdump(this.keyPtr, { length: 16 })); }, onLeave: function(retval) { console.log(输出密文:); console.log(hexdump(this.inputPtr, { length: this.inputLen })); } });注意一个非常容易踩的点很多Native加密函数是原地覆盖输入缓冲区返回值只是状态码密文结果还存在原来的输入指针里。这种情况不要去读retval要回头读入参指针的内存。3.3 Native层常见坑点ARM64参数传递规则前8个整型/指针参数走X0-X7寄存器浮点参数走V寄存器Frida会自动映射args但要注意参数顺序和类型C符号名修饰导出函数名会被修饰成_Z11aes_encryptPhjS_这种形式不要直接猜名字用readelf或者IDA看准确的导出符号内联函数Hook不到如果加密逻辑被编译器内联到调用函数里直接Hook加密函数是没效果的要往上找外层调用不要在Hook回调里做重操作Native层对时序敏感打印太多或者做复杂逻辑很容易触发ANR或者崩溃四、进阶玩法从被动拦截到主动加解密Hook拦截只是第一步只能看不能改。拿到函数入口和密钥之后就可以更进一步主动调用加密解密函数实现批量解密、构造请求等更复杂的需求。4.1 提取密钥一劳永逸的方案能拿到固定密钥的场景优先提取密钥。Hook Cipher的init方法把SecretKey转成字节数组打印出来拿到密钥之后自己写Python脚本就能加解密不用一直挂着Frida。对于每次启动生成的动态会话密钥就Hook密钥协商的接口把协商过程的参数全部打出来还原密钥派生逻辑。只要密钥生成逻辑是固定的就能自己算出每一次的会话密钥。4.2 主动调用加密函数算法魔改严重、拿不到密钥的场景就把APP里的加密函数直接封装成可调用的接口。Native层用NativeFunctionJava层直接Java.use调用自己构造参数传进去直接得到结果。// 封装Native加密函数为可调用方法 let aesEncrypt new NativeFunction( Module.findExportByName(libencrypt.so, aes_encrypt), void, [pointer, int, pointer] ); // 构造输入数据主动调用 let input Memory.allocUtf8String(test_data_123); let key Memory.alloc(16); key.writeByteArray([0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10]); aesEncrypt(input, 12, key); console.log(加密结果: byte2hex(input.readByteArray(16)));这种方式相当于直接复用APP里的加密逻辑不管算法怎么魔改、怎么加混淆结果和APP完全一致用来构造请求、批量生成签名准确率100%。4.3 批量解密抓包数据抓包拿到一堆密文不用一个个对照写个循环主动调用解密函数批量还原成明文。尤其是协议字段多、加密逻辑复杂的场景比自己逆向还原算法效率高太多。五、工程化落地与对抗建议5.1 通用扫描脚本前期快速定位加密位置的时候不用针对性写脚本直接挂通用加密扫描脚本自动Hook所有标准加密类和常见Native函数跑一遍操作所有加密调用一目了然。5.2 Frida检测对抗思路现在多数主流APP都会做Frida检测常见的对抗手段包括改默认27042端口用自定义端口启动frida-server用frida-gadget注入模式代替frida-server的服务端模式用Magisk模块隐藏Frida进程、maps里的so路径和相关特征没有万能的隐藏方案检测和对抗永远是相对的根据目标的强度调整策略即可。5.3 定位不到加密函数怎么办如果加密逻辑藏得特别深不要死磕函数本身从调用链往上倒推。从网络请求发送的地方开始Hook一层层往上追调用栈总能追到加密的入口点。栈回溯是定位混淆代码最有效的手段没有之一。六、总结Frida做加密逆向核心思路从来不是“逆向算法”而是“复用逻辑”。不用费劲去抠so里的汇编代码不用去还原魔改的AES轮数只要能Hook到函数入口出口能拿到输入输出能主动调用就解决了99%的协议分析问题。工具只是手段思路才是关键。先定位入口再Hook拦截然后提取密钥最后主动调用一步步推进再复杂的加密也能拆解开。