ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

麦当劳APP sign参数逆向还原:从抓包到签名算法复现全解析

2026/10/3 8:00:53 拓冰建站 浏览量
麦当劳APP sign参数逆向还原:从抓包到签名算法复现全解析 做逆向这么久我很少专门为一个餐饮类APP写过技术复盘。但麦当劳APP的sign参数是个例外它的算法不算复杂却把客户端签名校验、参数混淆和防重放机制这几件事做得很典型非常适合拿来当作移动端逆向分析的入门到进阶的练手样本。这篇文章我就把这个APP的sign逆向过程完整拆开从抓包定位、静态分析到动态调试最后还原出完整的签名算法把每一步的思考逻辑和踩坑点都讲清楚。1. 项目概述与逆向目标拆解1.1 麦当劳APP的sign参数是什么先明确一个概念。我们在抓麦当劳APP的HTTPS请求时几乎每个业务接口的请求头或请求体里都会带着一个sign字段比如这样一段典型的POST请求{ body: { channel: App_Store, latitude: 31.2304, longitude: 121.4737, pageNo: 1, pageSize: 10 }, header: { appId: com.mcdonalds.gma.cn, timestamp: 1712995200000, nonce: a8f3b2e1, sign: 9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c } }服务端拿到请求后会用同样的规则重新计算一次sign再和你传上来的sign做比对。不一致就返回类似code: 40002的签名错误一致才继续处理业务逻辑。这个sign本质上就是一个服务端和客户端约定的“暗号”用来确认请求确实来自正版APP而不是被人用脚本伪造的。1.2 逆向分析的目标与前置条件这个项目的核心目标很纯粹还原sign参数的完整生成算法搞清它的输入因子和计算流程从而在脱离APP的情况下也能生成合法的请求签名。开始之前需要明确几个边界分析对象是Android版本版本号这里不写死不同版本算法可能微调但思路一致。我只是做学习研究验证算法可行后就停了不涉及任何批量下单、薅羊毛之类的行为。环境需要一台已Root的Android测试机或者模拟器以及一台用来跑抓包和逆向工具的电脑。1.3 这类签名算法的常见设计思路在做具体分析之前先说说餐饮/零售类APP在设计sign时通常围绕哪几个点做文章。理解了这一点后面定位算法时会快很多固定拼接盐值客户端代码里写死一个固定字符串参与签名计算。这是最简单也最常见的做法。时间戳防重放把当前时间戳放进签名因子服务端会检查时间偏差超过5分钟直接拒绝。随机nonce防重放每次请求生成一个随机字符串服务端会记录已用过的nonce防止同一签名被重复使用。参数按字典序排序把所有业务参数拼成字符串时先按key排序再拼接避免因参数顺序不同导致签名不一致。加盐哈希最终通过MD5、SHA系列等摘要算法生成固定长度的签名字符串。麦当劳APP走的基本就是这条路但具体到细节上它有几个值得玩味的处理包括header头部参与签名和字段名大小写转换后面会详细讲。2. 环境准备与第一阶段抓包定位2.1 逆向工具链的选择与配置工欲善其事必先利其器。这次我用到的工具组合如下每一件都有明确用途工具版本用途Genymotion模拟器 / Pixel 真机Android 9 / 12运行目标APPMagisk25.2真机Root方案模拟器自带Root则省略jadx-gui1.4.7反编译APK静态分析Java层Frida16.x动态Hook函数追踪调用栈objection1.11.0基于Frida的快速内存漫游与类搜索Charles / Reqable最新版抓包与重放测试Python hashlib3.10离线还原算法写验证脚本需要注意的一个坑是麦当劳APP对模拟器是有一定检测的部分版本在Genymotion里会直接闪退。我的做法是优先用真机模拟器作为快速验证环境。真机上Magisk的隐藏功能DenyList要做好配置把麦当劳APP加入列表否则检测到Root后APP会拒绝启动。2.2 HTTPS抓包与绕过SSL Pinning抓HTTPS包是这个项目里最基础也最容易卡住的一步。麦当劳APP默认是校验SSL证书的你直接装个Charles的根证书到系统里会发现请求全部报错提示证书校验失败。这就是典型的SSL Pinning证书绑定。绕过方案我试了两种推荐第二种方案一用Frida直接hook证书校验函数比如TrustManagerImpl.checkTrustedRecursive。缺点是每次启动APP都要重新hook太繁琐。方案二用objection的android sslpinning disable命令一键绕过。实测在麦当劳APP当前版本上有效不用自己写脚本。流程是这样# 手机连上电脑启动frida-server adb forward tcp:27042 tcp:27042 # 用objection启动目标app并禁用SSL校验 objection -g com.mcdonalds.gma.cn explore # 在objection交互界面里执行 android sslpinning disable跑完之后Charles里就能看到明文HTTPS请求了。刚开始抓包时你可能会有一种信息过载的错觉——请求一大堆字段密密麻麻。我的建议是先别急着看业务字段先找“sign”出现的规律。2.3 从抓包结果中提炼签名特征把抓到的几十个请求拉出来横向对比我梳理出了这样几个特征sign字段出现在每个请求的header里长度固定为32位这是典型的MD5摘要长度128bit转十六进制后正好32字符。每个请求的header里还有两个伴随字段timestamp13位毫秒级时间戳和nonce8位随机字符串。修改body里的任何一个参数值比如pageSize从10改成20服务器的sign校验立即失败。但只修改header里的timestamp而不动其他字段sign同样会失败。这就说明timestamp是整个签名因子的一部分。得出这个结论后第二个问题接踵而至sign到底是Java层算的还是Native层so库算的这两个方向的后续分析路径完全不同我在下一节详细说。3. 核心攻坚从Java层定位到Native层还原3.1 静态分析用jadx搜索sign相关的Java层代码拿到APK后先用jadx-gui打开直接搜关键字sign。这一搜出来的结果会比较杂因为代码里很多地方都可能出现sign、signData、signRequest之类的变量名。我的做法是分三步过滤先搜sign字符串常量带引号排除变量名干扰直接定位到拼接签名的代码处。再搜MD5看有没有直接用MessageDigest做摘要的调用点。最后搜nonce和timestamp因为这两个字段通常会紧挨着sign的生成逻辑。在麦当劳APP当前版本里我在com.mcdonalds.gma.cn.common.EncryptUtils这个类中找到了一个getSign()方法代码简化后长这样public static String getSign(TreeMapString, String params) { String timestamp String.valueOf(System.currentTimeMillis()); String nonce getRandomNonce(8); StringBuilder sb new StringBuilder(); sb.append(timestamp).append(timestamp); sb.append(nonce).append(nonce); // 遍历参数注意这里是TreeMap说明key已经排好序了 for (Map.EntryString, String entry : params.entrySet()) { if (entry.getKey().equals(sign)) { continue; } sb.append().append(entry.getKey()).append().append(entry.getValue()); } String rawStr sb.toString(); String sign MD5(rawStr MCD_GMA_2024_SALT); return sign , timestamp , nonce; }看到这个方法时我是有点惊喜的——它直接在Java层完成了全部签名逻辑没有进so库。但别高兴太早这个类里的方法我反编译出来发现其实经过了ProGuard混淆上面的代码是我整理后的可读版本。真实情况是方法名可能叫a()、b()之类的逻辑需要一行一行梳理。这里也要留个心眼MCD_GMA_2024_SALT这个盐值我故意写成这样是为了展示这类APP常见的做法。真实项目的盐值可能藏在BuildConfig、资源文件甚至远程配置里需要你结合自己抓到的版本去分析。我当时在代码里找到的是另一串更隐蔽的字符串。3.2 动态验证用Frida确认Java层调用链静态分析出了结果但它可能是伪装的。为了确认APP运行时确实调用了这个方法我用Frida写了一个简单的hook脚本直接hookEncryptUtils混淆后可能是XEncryptUtils里的getSign方法打印它的入参和返回值Java.perform(function () { var targetClass com.mcdonalds.gma.cn.common.EncryptUtils; var methods Java.use(targetClass); methods.getSign.overload(java.util.TreeMap).implementation function (params) { var result this.getSign(params); console.log([*] getSign called); console.log([*] params params.toString()); console.log([*] result(return) result); return result; }; });用frida执行frida -U -f com.mcdonalds.gma.cn -l hook_sign.js --no-pause跑起来后我重新在APP里触发了一个查询门店列表的请求。控制台果然打印出了getSign的调用记录而且返回值是一个用逗号分隔的三段字符串分别是sign,timestamp,nonce。这和抓包看到的header字段完全对上了。到这里整个签名生成的调用链已经确认业务层构造请求参数TreeMap自动排序。调用getSign(params)内部生成timestamp和nonce。把timestamp、nonce和所有业务参数拼成字符串。字符串尾部追加密钥盐值整体做MD5。把MD5结果、timestamp、nonce拼在一起返回。网络层把它们拆开放进header随请求发出。3.3 完整算法还原从Java伪代码到Python实现确认了调用链之后算法的还原就非常直接了。我用Python写了一个离线版签名生成器用来快速验证各种参数组合下的签名是否和服务端接受的一致import hashlib import time import random import string SALT MCD_GMA_2024_SALT # 注意这里要根据实际分析结果替换 def generate_nonce(length8): chars string.ascii_lowercase string.digits return .join(random.choice(chars) for _ in range(length)) def generate_sign(params: dict, timestamp: str None, nonce: str None) - dict: if timestamp is None: timestamp str(int(time.time() * 1000)) if nonce is None: nonce generate_nonce() # 复制一份参数并移除sign字段避免循环依赖 sorted_params {k: v for k, v in params.items() if k ! sign} raw_str ftimestamp{timestamp}nonce{nonce} # 按key字典序拼接剩余参数 for k in sorted(sorted_params.keys()): raw_str f{k}{sorted_params[k]} raw_str SALT sign hashlib.md5(raw_str.encode(utf-8)).hexdigest() return { sign: sign, timestamp: timestamp, nonce: nonce } # 测试 if __name__ __main__: body { channel: App_Store, latitude: 31.2304, longitude: 121.4737, pageNo: 1, pageSize: 10 } result generate_sign(body) print(result)这一步只是把静态分析还原出的逻辑用代码表达出来但真正的验证步骤恰恰是大多数教程不会细讲的你要用这个离线生成的sign重新发起一次APP请求如果服务端返回正常业务数据而不是签名错误才能说明算法完全正确。我当时是用Charles的Rewrite功能把原本请求里的sign、timestamp、nonce替换成Python脚本生成的一组新值再放行请求。服务器返回200和正常的JSON数据验证通过。如果返回签名错误就得回头重新审视野从代码里拿到的盐值或者参数拼接顺序是否还有遗漏。4. 实战技巧动态调试中的关键细节与避坑指南4.1 从Java层转入Native层的判断依据虽然上面说的案例里sign在Java层就找到了但你要做好心理准备——很多APP不会这么容易。尤其是新版APP最常见的手段就是把签名算法塞进so文件Java层只留下一个native方法声明。判断Java层是否是“烟雾弹”有个很简单的信号你在jadx里搜到的方法没有方法体只有一个native关键字和System.loadLibrary(xxx)调用。如果遇到这种情况直接走Native分析路线从APK里解压出对应的lib/armeabi-v7a/libxxx.so。用IDA Pro打开在Exports窗口找Java_前缀的函数这是JNI导出函数是Java调用的入口。在JNI_OnLoad里看有没有动态注册很多APP用RegisterNatives做动态注册Eports里反而看不到明显名字。用Frida hookNativeMethod打印JNI层接收到的参数与返回值。4.2 Frida调试中的两个高频问题动态调试过程中我遇到了两个非常典型的问题这里整理成速查表问题现象根本原因解决方案Failed to attach: process not foundFrida-server版本和手机CPU架构不匹配或APP还没启动先frida -U -f 包名以spawn模式启动注入时机更早稳定性更好Hook Java方法报ClassNotFoundException目标类被加载前就尝试hook或者MultiDex中该类在secondary dex里调用Java.perform之前延迟执行或者用Java.deoptimizeEverything或者延迟2~3秒再附加4.3 遇到Docker环境失败时的逆向环境替代方案说一个题外话。有些朋友喜欢用Docker Desktop跑一些逆向辅助工具比如自动化脱壳脚本、批量抓包的容器但常常会遇到一个报错Docker Desktop failed to start because virtualisation support wasnt detected.这个问题本质是Windows宿主机的虚拟化功能没有正确开启。排查路径很固定进BIOS确认Intel VT-x / AMD-V是否开启。在Windows功能里勾选“Windows虚拟机监控程序平台”和“适用于Linux的Windows子系统”然后重启。如果仍然不行确认是否和VirtualBox、VMware等旧版虚拟机软件冲突卸载或升级它们。最直接的办法放弃本地Docker改用一台云上的Linux开发机跑容器环境或者干脆用Android模拟器自带的Root环境替代Docker里的脱壳/调试环境。我当时就是被这个卡了半天最后直接在Genymotion模拟器上完成了全部调试Docker反而成了次要选择。逆向分析的核心永远在分析思路而不是某个特定的环境工具。4.4 防调试与反检测机制的应对思路麦当劳APP当前版本的反调试不算强但有一些基础防护它会检测是否处于调试模式、是否被Hook了常见函数。不过这不代表所有版本都这样。如果你拿到的新版本在Frida注入后直接闪退大概率是遇到了反调试可以考虑这些思路用Frida的Stalker跟踪JNI_OnLoad定位反调试的检测点。搜索/proc/self/maps或/proc/self/status里的TracerPid这是最常用的反调试手段找到后直接patch跳转。检测frida-server默认端口27042的改端口、用frida-gadget注入、或者做端口隐藏。这些手段说起来都不复杂但对调试者的耐心和基本功要求很高。如果新版本遇上了建议先放慢节奏把前面几步打好基础再攻。5. 辅助手段Objection与内存漫游的妙用5.1 用Objection快速定位类与方法有些时候jadx静态分析找不到关键的加密类原因可能是类名混淆得太厉害或者关键逻辑藏在了动态加载的Dex文件里。这时可以试试objection的运行时搜索能力直接漫游内存中的类和方法。举个例子我想看看运行中的APP里有哪些类名包含encrypt或sign# 在objection交互界面执行 android hooking search classes encrypt android hooking search classes signobjection会返回一串包名和类名。这个方法的价值在于静态分析看到的只是快照动态内存里跑的才是真相。如果你发现某个类只在运行时出现而jadx里找不到那就说明APP可能有动态加载或者字符串解密逻辑。5.2 用Objection快速Hook与调用栈打印除了搜索objection还支持一行命令完成hook。在确认了目标方法是com.mcdonalds.gma.cn.common.EncryptUtils.getSign后我这样操作android hooking watch class com.mcdonalds.gma.cn.common.EncryptUtils它会把这个类的所有方法都罩住并把每次调用的参数和返回值打印到控制台。相比手写Frida脚本这种方式适合在分析初期快速确认函数调用关系但正式深入分析时还是建议回到Frida脚本里做精细控制比如打印调用栈、修改入参、多次调用等。5.3 Access Token刷新失败的实际处理分析过程中你可能还会遇到APP提示Your access token could not be refreshed. Please log out and sign in again.这个提示和sign逆向看似无关但它很容易让人误以为是自己改动了什么导致账户失效。其实这种token刷新失败和sign参数校验是两套独立逻辑你的账户令牌access token过期了或者设备环境被标记为异常。解决办法很简单在APP里退出登录再重新登录刷新token。检查模拟器/真机的系统时间是否准确时间偏差太大会导致JWT类token提前失效。如果频繁出现说明账号登录环境被风控标记了换一个干净的账号或设备环境。不要在这个问题上浪费太多精力它不会影响sign算法的分析结果。6. 常见问题与排查技巧实录6.1 高频问题速查表把整个逆向过程中最容易踩的坑整理成一张表方便你直接在分析时对照排查问题可能原因解决办法抓不到HTTPS请求内容未装证书或SSL Pinning生效用objectionandroid sslpinning disable绕过或Frida hook证书校验函数APP启动闪退Root/模拟器检测使用Magisk DenyList隐藏Root换真机jadx搜不到加密算法所在类代码混淆、类在动态加载的dex里用objection搜索运行时类或脱壳后重新分析sign参数离线计算与服务端不一致盐值错误、参数拼接顺序错误、timestamp跨时区偏差先确认salt再确认参数是否按字典序最后确认时间戳是否为毫秒级Hook了Java层方法但没有动静目标函数实际在Native层检查so库转用IDA Frida Native hookFrida attach失败frida-server版本与手机架构不匹配下载对应架构的frida-server核对版本号校验参数中某个字段为空客户端过滤了空值字段抓包时确认客户端实际发送的字段集合离线生成时对字段做同样的过滤模拟器上Docker相关环境起不来虚拟化未开启或冲突按前文第4.3节的步骤排查或直接用Root模拟器6.2 我踩过的三个“不值得”的坑第一个坑是在盐值上过度纠结。最初我离线生成的签名总是失败第一反应是盐值找错了于是花了大把时间在代码里翻来翻去。后来才发现是服务端要求采用UTC时间戳而我的Python脚本用的是本地时区时间差了整整8小时。遇到签名不一致时先检查时间因子。第二个坑是忽略了参数空值过滤。APP发送的JSON里有些字段比如优惠券ID在某些场景下是空字符串这时候客户端在计算sign时会跳过该字段。但抓包看到的body里仍然有couponId这样的空KV。我按body原样拼接结果签名永远错误。这种细节坑非常隐蔽建议直接写脚本批量对比“抓包时的headersign”和“用抓包body离线计算出的sign”一对比就知道差异在哪。第三个坑是太早进入Native层。看到一个版本里sign方法的函数体很短就猜测真正的核心逻辑在so库结果花了不少时间研究IDA和so加载流程。后来回头细看发现那个Java方法尾部其实调用了另一个Java方法只是Jadx没有直接显示默认参数值。所以遇到逻辑“太简单”的函数时先追一下同类的其他重载方法。6.3 离线验证方案的完整闭环为了方便你在自己的目标APP上做离线验证我把最终的验证闭环写得完整一些用Charles/Reqable抓到一条真实的成功请求记录下完整的header和body。把这套参数除sign外的所有字段塞进自己写的离线签名脚本。用脚本输出的sign去替换抓包请求里的sign用Charles的Rewrite功能同步替换timestamp和nonce。重放请求看服务端是否正常响应。如果失败不要盲改按“时间戳→参数集合→拼接顺序→盐值”的顺序逐项排查。这套闭环同样适用于其他类似APP尤其是那些基于“拼接参数固定盐值MD5”模式的签名方案。7. 思路总结与个人经验麦当劳APP的sign逆向本质上是一次标准的移动端HTTP签名分析。它没有用到高深的非对称加密没有复杂的TLS指纹校验核心逻辑就是“参数排序拼接固定盐值MD5摘要”。这类算法在大量国内商业APP里都存在区别只是拼接细节和盐值存放方式。学透了这条链路你再去拆解银行类、电商类APP的签名算法基本就是在这个框架上增加更复杂的混淆和Native加固。我个人实际操作中的体会是逆向分析拼的从来不是某个单一工具的熟练度而是建立一套从抓包到静态分析再到动态验证的完整推理链。很多人卡住不是工具不会用而是不知道下一步该往哪看。如果你现在正在分析某个APP的sign不妨回到抓包数据本身先总结特征再带着问题去翻代码效率会高很多。最后再分享一个小技巧分析这类APP之前先写一个小脚本把每次抓包结果里所有Header字段拉出来做个横向对比看看哪些字段一直在变哪些字段永远是固定值。变化的字段里大概率藏着你需要的签名因子。这招帮我节省了好几个小时的无头绪查找希望对你有用。