Frida实时Hook实战:深度还原某支付平台HMAC-SHA256签名机制,Native密钥抓取全流程
2026/7/24 19:58:19 网站建设 项目流程

在移动端接口安全评估与业务对接场景中,客户端签名校验始终是绕不开的核心环节。近期团队在对某第三方支付平台做合规性安全测试时,遇到了典型的「Java层封装+Native层实现」签名防护方案:核心的HMAC-SHA256密钥与计算逻辑全部下沉到so库,常规静态逆向很难提取完整密钥,且应用内置了多维度反调试与环境检测。

经过一周的逆向分析与动态调试,我们通过Frida实现了全链路实时Hook,完整还原了从参数排序、密钥派生到最终签名生成的全流程,脱离原APP环境实现了100%一致的签名输出。本文从特征分析、环境搭建、函数定位到Hook实战、算法复现,完整复盘整个落地过程,分享一线逆向工程的实操经验与踩坑细节。

一、目标样本分析与签名特征初判

动手逆向之前,先从外部建立对签名机制的宏观认知,避免一上来就扎进代码里迷失方向。

1.1 抓包层面的签名特征

通过Charles抓取正常支付请求,我们先对sign参数做基础特征分析:

  • 签名值固定64位小写十六进制字符串,初步对应SHA-256算法的输出长度
  • 相同参数重复请求,sign值保持不变,排除随机盐干扰
  • 修改请求体任意一个字节,sign值完全变化,符合摘要算法雪崩效应
  • 请求头中同步携带timestamp、nonce、deviceId三个参数,推测参与签名运算

初步判断这是基于哈希的消息认证码方案,大概率是HMAC-SHA256,但密钥和拼接规则未知。

1.2 防护体系初判

安装目标APK后做初步检测,发现几个典型的防护特征:

  • 应用整体加壳,直接解压无法拿到完整dex文件,需先脱壳处理
  • 核心支付逻辑独立打包在libpaysecure.so中,存在Native层加密调用
  • 内置基础反调试,IDA附加后进程立即退出
  • 存在模拟器、Root环境检测,常规测试环境直接闪退

这是非常典型的支付类应用防护等级,纯静态逆向成本很高,动态Hook是性价比最高的方案。

抓包获取请求样本

分析sign参数特征

脱壳后Jadx静态分析Java层

核心逻辑是否在Java层

直接Hook Java签名方法

定位对应Native so库

IDA分析导出函数与逻辑

Frida动态Hook Native层

捕获密钥与待签明文

本地算法复现与一致性验证

工程化落地与稳定性优化

二、逆向工具链与环境搭建

工欲善其事,必先利其器。移动端Native层逆向的工具链相对固定,但版本匹配和环境细节很容易踩坑。

2.1 核心工具选型

  • Frida 16.1.3:动态插桩工具,负责运行时Hook函数、捕获参数
  • Jadx 1.4.7:Java层反编译,做静态代码检索与调用链分析
  • IDA Pro 7.7:so库静态分析,定位Native函数偏移与逻辑
  • Charles 4.6.3:网络抓包,获取请求样本做结果验证
  • Pixel 4 测试机:Root环境,Android 11系统,ARM64架构

2.2 环境部署避坑指南

这部分新手最容易翻车,我们踩过的坑统一列出来:

  1. Frida版本严格对齐:PC端frida-tools版本必须和手机端frida-server主版本号完全一致,差一个小版本都可能出现连接失败或注入崩溃。
  2. 架构对应:frida-server要根据手机CPU架构下载,ARM64设备不能用ARM32版本,否则无法运行。
  3. 端口转发:默认27042端口,执行adb forward tcp:27042 tcp:27042做端口映射,很多人漏这一步导致连不上设备。
  4. 临时关闭SELinux:部分高版本Android系统SELinux限制严格,会导致Frida注入失败,执行setenforce 0临时关闭可解决大部分问题。

三、全链路定位:从Java层到Native层

定位签名函数是逆向的第一步,找错了入口后面全是无用功。我们采用「自上而下、逐层收敛」的定位思路。

3.1 Java层入口:静态检索+动态验证

先将脱壳后的dex导入Jadx,全局搜索关键词:signhmacsha256signature,同时结合抓包得到的请求参数名做检索。
很快定位到一个名为PaySignUtils的工具类,其中有一个native方法:

publicnativeStringnativeGetSign(Stringparams,StringdeviceId,longtimestamp);

方法返回值正好是64位字符串,和抓包的sign格式完全匹配。进一步查看调用链,确认所有支付请求的签名都经过该方法生成。

到这里只完成了一半——Java层只是个壳,真正的计算逻辑在so库里。

3.2 Native层:so库函数定位

从APK中解压出libpaysecure.so,导入IDA Pro做静态分析。
首先看导出函数表,找到对应的JNI函数:Java_com_xxx_pay_PaySignUtils_nativeGetSign。这是JNI的入口函数,但通常只是做参数转换,真正的核心算法会在内部调用其他子函数。

顺着JNI函数的调用链往下追,经过几层封装后,定位到一个内部函数,根据IDA的交叉引用和字符串特征,判断这就是HMAC-SHA256的核心计算函数,相对偏移为0x12F4C

3.3 完整调用链路梳理

至此我们梳理出了签名生成的完整调用链路:

业务请求模块

参数按键名排序拼接

Java层PaySignUtils封装

JNI调用Native入口

设备指纹与密钥派生

HMAC-SHA256核心计算

结果转Hex字符串返回

拼接进请求参数发往服务端

这里有个容易忽略的细节:密钥并不是硬编码在so里的常量,而是由设备ID、APP证书签名等信息动态派生而来。这也是很多人静态逆向拿不到有效密钥的原因——密钥只存在于运行时内存中。

四、Frida动态Hook核心实战

定位到目标函数后,就进入核心的Hook环节。我们分三步推进:先绕过反调试,再Hook Java层验证入口,最后Hook Native层抓取核心密钥与明文。

4.1 前置操作:绕过基础反调试

目标应用内置了ptrace反调试,直接附加Frida会导致进程闪退。我们先写一个基础的反调试绕过脚本,Hook libc的ptrace函数,强制返回成功:

// 绕过ptrace反调试functionbypassPtrace(){varptraceAddr=Module.findExportByName("libc.so","ptrace");if(!ptraceAddr)return;Interceptor.attach(ptraceAddr,{onEnter:function(args){// 拦截PTRACE_TRACEME调用if(args[0].toInt32()===0){console.log("[*] 拦截ptrace反调试调用");args[0]=ptr(-1);}},onLeave:function(retval){retval.replace(0);}});}

除了ptrace,还要处理进程名检测、Frida端口检测等。我们的策略是先改frida-server的二进制文件名,再配合脚本Hook相关检测函数,基本可以绕过常规检测。

4.2 Java层Hook:验证入口正确性

先从Java层入手,验证我们定位的native方法是否真的是签名入口。写一个简单的Frida脚本:

Java.perform(function(){varPaySignUtils=Java.use("com.xxx.pay.PaySignUtils");PaySignUtils.nativeGetSign.implementation=function(params,deviceId,timestamp){console.log("[+] 输入参数:",params);console.log("[+] 设备ID:",deviceId);console.log("[+] 时间戳:",timestamp);varresult=this.nativeGetSign(params,deviceId,timestamp);console.log("[+] 签名结果:",result);returnresult;}});

触发一次支付请求,成功捕获到输入参数和输出签名,和抓包结果完全一致,入口确认无误。

4.3 Native层Hook:抓取密钥与明文

Java层只能拿到输入和输出,拿不到核心密钥。接下来我们Hook Native层的HMAC核心函数,直接从内存中读取密钥和待签明文。

这里有个关键知识点:ARM64架构下,函数前8个参数通过X0-X7寄存器传递,超出部分才走栈。我们分析IDA中的函数定义,确认目标函数的参数顺序是:

  • X0:密钥内存指针
  • X1:密钥长度
  • X2:待签明文指针
  • X3:明文长度
  • X4:输出结果缓冲区指针

对应的Hook脚本核心片段如下:

functionhookNativeHmacCore(){varlibBase=Module.findBaseAddress("libpaysecure.so");if(!libBase){console.log("[-] so库未加载,等待中...");return;}// 目标函数相对偏移,ARM64直接使用偏移即可varfuncOffset=0x12F4C;vartargetFunc=libBase.add(funcOffset);Interceptor.attach(targetFunc,{onEnter:function(args){// 读取密钥varkeyLen=args[1].toInt32();varkeyBytes=Memory.readByteArray(args[0],keyLen);console.log("[+] 密钥(Hex):",bytesToHex(keyBytes));// 读取待签明文varinputLen=args[3].toInt32();varinputStr=Memory.readUtf8String(args[2],inputLen);console.log("[+] 待签明文:",inputStr);// 保存结果指针,onLeave中读取this.resultPtr=args[4];},onLeave:function(retval){// SHA256输出固定32字节varresultBytes=Memory.readByteArray(this.resultPtr,32);console.log("[+] Native计算结果(Hex):",bytesToHex(resultBytes));}});}

执行脚本后触发请求,成功捕获到了完整的密钥和待签明文。和我们之前的推测一致:密钥是32字节的二进制值,由设备信息派生而来,同一个设备每次启动密钥固定。

4.4 全量样本验证

我们捕获了50+不同场景的请求,涵盖正常支付、退款、查询等接口,每一组都记录了密钥、明文、签名结果。初步比对发现,所有场景共用同一套密钥和算法,只是待签参数的拼接规则略有差异。

五、算法完整还原与一致性验证

抓到密钥和明文只是第一步,必须脱离原APP环境,用独立代码复现签名结果,才算真正还原算法。

5.1 算法结构拆解

结合Hook到的信息与静态分析,我们完整拆解了签名算法的四层结构:

  1. 参数标准化:所有请求参数按键名ASCII码升序排序,空值参数剔除
  2. 明文拼接:按timestamp + nonce + deviceId + 排序后参数字符串的固定格式拼接成待签明文
  3. 密钥派生:基于设备ID与APP签名,通过一次AES-ECB加密派生32字节HMAC密钥
  4. 核心计算:使用派生密钥对待签明文做HMAC-SHA256运算,结果转小写Hex即为最终sign

5.2 本地代码复现

我们用Python实现了完整的签名逻辑,核心片段如下:

importhmacimporthashlibdefgenerate_sign(params,device_id,timestamp,nonce,root_key):# 1. 参数排序sorted_items=sorted(params.items(),key=lambdax:x[0])param_str="&".join([f"{k}={v}"fork,vinsorted_itemsifv!=""])# 2. 拼接待签明文plain_text=f"{timestamp}{nonce}{device_id}{param_str}"# 3. 派生HMAC密钥hmac_key=derive_hmac_key(device_id,root_key)# 4. HMAC-SHA256计算sign=hmac.new(hmac_key,plain_text.encode(),hashlib.sha256).hexdigest()returnsign

5.3 100%一致性验证

我们将Hook捕获的50组样本全部输入本地算法,输出结果与原APP生成的sign完全一致,逐字节无偏差。
这也验证了一个结论:核心算法就是标准HMAC-SHA256,防护的核心在于密钥的隐藏与动态派生,以及Native层的反调试保护。

六、工程化落地与对抗优化

算法跑通只是实验室结果,要做到稳定、隐蔽、可复用,还需要工程化优化。

6.1 自动化Hook脚本封装

我们将反调试绕过、函数Hook、结果输出封装成完整的Frida脚本,支持一键启动、日志持久化、批量样本导出。配合objection使用,可以快速完成不同版本APP的签名验证。

6.2 降低Hook特征,规避检测

原生Frida的特征很明显,长期运行容易被风控检测到。我们做了几处优化:

  • 不使用默认frida-server,改用gadget方式注入,减少进程特征
  • Hook时尽量不修改函数序言,使用更隐蔽的Hook点
  • 避免高频打印日志,降低系统调用异常特征
  • 对Frida自身的内存特征做隐藏,绕过内存扫描检测

6.3 性能与并发考量

如果只是做逆向验证,单线程Hook足够;如果用于批量测试,可以将还原后的算法做成独立的签名服务,单进程每秒可处理上万次签名请求,性能远超Hook原APP的方案。

七、踩坑实录与经验总结

整个过程踩了不少坑,很多问题都是教程里不会写的实战细节,这里统一整理出来。

7.1 印象最深的几个坑

坑一:ARM/Thumb指令集偏移错误
一开始分析so时没注意指令集,直接用了IDA里的地址作为偏移,结果Hook后程序直接崩溃。排查了很久才发现,目标函数是Thumb指令集,Hook时偏移需要+1。ARM64架构不存在这个问题,但32位so一定要注意。

坑二:密钥是动态派生的,不是硬编码
早期静态逆向时在so里搜了很久常量字符串,始终找不到密钥。后来Hook了内存拷贝函数才发现,密钥是运行时动态生成的,每次启动都会重新计算,静态分析根本拿不到。这也是动态Hook的价值所在。

坑三:参数顺序搞反,结果始终对不上
刚开始复现算法时,待签串的参数顺序和实际相反,导致签名始终不一致。后来逐字节对比明文才发现,对方用的是ASCII码降序排序,不是常见的升序。细节决定成败。

坑四:高版本Android的权限限制
Android 12以上对进程注入限制更严,普通的frida-server注入会被系统拦截。最后是通过Magisk的Frida模块,以系统级权限注入才解决问题。

7.2 移动端签名逆向的通用思路

总结下来,面对任何移动端签名问题,都可以遵循这个通用流程:

  1. 先抓包分析签名特征,初步判断算法类型
  2. 静态反编译定位入口,区分Java层还是Native层
  3. 动态Hook捕获输入输出,验证算法逻辑
  4. 本地复现算法,做一致性验证
  5. 工程化封装,处理对抗与稳定性

7.3 对抗升级的一点思考

现在的APP防护已经从单纯的代码混淆,升级到了运行时环境检测、行为风控、云端校验的多层体系。Frida Hook也不是万能的,面对VMP、虚拟机保护等强防护方案,同样需要更深入的对抗技术。

但无论防护怎么升级,底层的思路是不变的:找到核心逻辑、获取运行时数据、验证算法一致性。工具只是手段,系统化的分析能力才是核心。

合规声明

本文所述技术仅用于合法的安全研究、合规性测试与自有系统接口对接场景。任何技术都有其边界,读者在实际应用中请严格遵守《网络安全法》《数据安全法》等相关法律法规,尊重平台方的服务协议与知识产权,不得用于非法破解、恶意攻击、盗取数据等违规场景。技术本身是中性的,合理使用、守住边界,是每个安全从业者的基本职业素养。

移动端的攻防对抗是一场持续的军备竞赛,今天的方法可能明天就会失效。但相比于掌握某个具体平台的破解方法,更重要的是建立起系统化的分析思路和解决问题的工程能力。本文完整复盘了从特征分析到算法落地的全流程,其中的通用思路与踩坑经验,同样适用于大多数移动端签名逆向场景。

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

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

立即咨询