抓包全是密文?Frida Hook加密函数与参数解密全链路实战
2026/9/12 11:00:20 网站建设 项目流程

做Android逆向和协议分析的同行,基本都绕不开密文拦截这道坎。Charles抓包拉出来,请求体的data字段是一串无规律的Base64,sign参数看不出任何生成逻辑,想改个参数调试都无从下手。静态反编译dex找加密入口,碰到混淆、加固、Native层加密,耗上一两天也未必能摸清楚完整逻辑。

Frida作为动态插桩工具,最大的价值就是绕开静态逆向的繁琐,直接在运行时给加密函数“搭探针”:函数执行前截获输入的明文、密钥、IV,执行后截获输出的密文,不用管内部算法怎么实现,输入输出直接拿到手。

这篇文章从基础原理到Native层实战,再到主动调用解密,把Frida做加密逆向的完整链路讲透,附常用Hook模板和踩坑总结,覆盖Java层到Native层90%的常规加密场景。

一、Frida Hook加密的核心原理:给黑盒开两个观察窗

加密函数本质上就是一个输入输出的黑盒:输入明文、密钥、偏移量,输出密文。静态逆向要掰开黑盒看内部逻辑,动态Hook只需要在黑盒的入口和出口各插一个探针,就能拿到所有关心的数据。

整个过程完全不影响原程序的正常执行,原函数该怎么跑还是怎么跑,只是在执行前后多执行了我们注入的打印逻辑。这也是动态插桩相比静态逆向最大的优势:不用还原算法,就能拿到输入输出。

核心Hook点位就两个:

  • 函数入口:抓明文参数、密钥、IV、加密模式等所有输入信息
  • 函数出口:抓加密后的密文、哈希值、签名结果

适用场景覆盖从简单到复杂的绝大多数加密逆向需求:Java层标准加密库、第三方SDK加密、Native层自定义算法、协议签名函数、甚至是魔改的国密算法。

二、Java层加密Hook:覆盖绝大多数常规加密场景

Java层的加密基本都遵循JCE标准接口,CipherMessageDigestMac这些基类是所有加密实现的入口。Hook这些基类方法就能通杀大部分常规加密,不用一个个找具体业务类。

2.1 Cipher通用Hook:AES/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汇编,三个常用方法效率从高到低:

  1. 导出函数直筛:先看so的导出表,有没有aes_encryptsm4_cryptrsa_process这类命名的函数,大概率就是目标,直接拿函数名Hook
  2. 字符串检索定位:IDA里搜“encrypt”、“cipher”、“AES”、“key”这些关键词,交叉引用找过去,基本都在加密函数附近
  3. 栈回溯追调用链:先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层用NativeFunction,Java层直接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拦截,然后提取密钥,最后主动调用,一步步推进,再复杂的加密也能拆解开。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询