资讯动态

某手App请求签名sig与__NS_sig3逆向分析:从抓包到算法复现

发布时间:2026/9/15 15:16:22 来源:尧图企业网站定制
做客户端请求分析的同学对某手 App 里的签名参数应该都不陌生。每次翻请求总能在 query 或者 body 里看到sig、__NS_sig3这类字段值看上去就是一串随机字符串但服务端每一次都校验得极严。之前已经把这个版本的签名框架整体过了一遍这次单独把sig和__NS_sig3拎出来说清楚它俩到底怎么生成、在哪里生成、调试时怎么确认自己的算法写对了。文章主要针对 v8.x 的 iOS 端分析思路Android 端部分结论也通用。适合已经会抓包、想从“抄参数”进阶到“理解签名逻辑”的朋友内容偏实操不搞云里雾里的理论堆砌。1. 签名体系整体认知sig 与 __NS_sig3 是什么关系1.1 一次业务请求里究竟带了哪些签名参数很多人第一次看某手的请求会被一堆参数吓到。除了正常的业务字段还会看到__NStokensig、sig、__NS_sig3甚至还有__NS_sigTime、__NS_sigValidity这种看起来就很“安全风控”的字段。它们不是同一个东西也不是每次都会全部出现但大部分核心接口都会强制校验。简单分类sig最常见的请求签名覆盖 URL path、排序后的 query、body 内容以及时间戳等用来保证请求在传输过程中没有被篡改。__NS_sig3更偏风控和设备因子长字符串经常出现在 body 末尾可能同时参与了请求合法性校验。其他前缀为__NS的字段属于同一套签名体系的辅助参数比如时间戳、随机数、过期时间等。以 v8.x 为例一次典型 GET 请求里query 部分会有sigxxxbody 如果是 POST则末尾经常能看到__NS_sig3xxx。这两个参数的取值都没有固定规律每次请求都不同直接写死肯定不行。1.2 生成位置与调用链要理解sig和__NS_sig3的关系先得搞清楚它们是在哪里生成的。从逆向角度看某手 iOS 端的签名逻辑并没有完全放在独立 SDK 里而是深度融入了网络层。v8.x 里请求会先经过一个统一的协议层在请求发出前业务方把 path、query、body 塞给某一个加密类由这个类完成签名参数拼接再塞回原来的请求。从调用链上看大概是业务请求 - 公共请求封装层 - 签名/加密模块 - 生成 sig 与 __NS_sig3 - 回填请求参数 - 发出网络请求听起来很简单但实际操作下来会发现签名模块做了大量字符串拼接和字节运算而且很多中间量被编译到了二进制里用静态分析看到的是大量寄存器操作没有明显的字符串“加盐点”。所以动态调试在签名分析里几乎是必须的。1.3 为什么需要单独区分 __NS_sig3很多人会问既然已经有了sig校验整个请求为什么还要多一个__NS_sig3我的理解是sig负责的是“当前这个请求的内容没被人改过”它的输入更多是请求层面的数据而__NS_sig3负责的是“这个请求是不是来自一个可信的设备环境”所以它的输入大概率包含设备 ID、安装时间、本地存储的一些风控因子、甚至是键盘输入习惯这类采集数据。换个生活化的说法sig相当于你寄快递时填的单号用来确认包裹没被拆过__NS_sig3相当于快递员看一眼你本人和身份证确认你确实住在寄件地址。两者服务端都会验少一个都不行。这样设计既防止请求被抓包后随意重放也能拦截模拟器、多开、群控这类异常环境。2. 抓包与参数定位最快拿到请求全貌2.1 环境准备与抓包方案分析签名前先把“看到请求、改请求、重发请求”的基础设施准备好。推荐两套组合iOS越狱手机 Frida Charles / StreamAndroidRoot 手机或模拟器 Frida Charles / HttpCanary抓包工具用来观察签名参数长什么样Frida 用来定位签名函数、打印入参和返回值。需要注意的是某手客户端有证书校验直接配置 Charles 证书大概率抓不到 HTTPS 明文。这种情况下要先用 Frida 或者 objection 绕过 SSL Pinning然后再抓包。关于证书校验绕过v8.x 上常见做法是 HookSecTrustEvaluateWithError或NSURLSession的didReceiveChallenge把服务端信任逻辑强制改为接受任意证书。不过这类 hook 代码在网上一搜一大把真正要花时间的是确认 App 有没有做二次校验以及抓包后是不是所有接口都能看到明文。2.2 从流量里识别签名参数抓包后不要急着看参数内容先做“对照实验”。打开 App随便点几个列表页找同一个接口的多次请求把 query 和 body 里的字段逐个对比。以http://api.xxx.com/rest/photo/list?page1sigxxx为例连续刷新几次你会发现业务字段可能不变但sig一直在变。如果同一时间戳下请求里多了__NS_sig3也大概率是签名模块生成的。重点看两个东西哪些字段会随请求内容变化而变化大概率是签名的一部分哪些字段不随请求内容变化但随设备变化大概率是设备因子实际操作时我习惯先导出几次请求的原始 body放到本地 diff能快速定位sig、__NS_sig3和普通参数的区别。要注意 Charles 里展示的 body 可能是格式化后的建议看 Raw 标签页保持原始文本。2.3 用 Hook 定位生成函数抓包只能验证“有这些参数”要找生成位置得动 Frida。最简单的思路是 Hook 网络请求对象看某个字段何时被赋值。iOS 端可以先 HookNSMutableURLRequest的setValue:forHTTPHeaderField:和setHTTPBody:再看有没有自定义分类对 request 做二次修改。但 v8.x 可能不走系统网络类签名是在更底层完成的所以更通用的办法是搜索字符串。Frida 里可以直接搜内存中映射的字符串比如搜__NS_sig3// hook.js function findStr(s) { var ranges Process.enumerateRanges(r--); var result []; ranges.forEach(function(range) { try { Memory.scanSync(range.base, range.size, s).forEach(function(m) { result.push(m.address); }); } catch (e) {} }); return result; } findStr(__NS_sig3).forEach(function(addr) { console.log(found at:, addr); });找到字符串地址后用Memory.readCString确认上下文再通过交叉引用或者NativePointer下访问断点就能看到哪些代码在调用这个字符串。这一步之后基本能锁定签名函数所在的模块和偏移方便后续静态砸壳分析。3. 签名生成原理拆解从字符串构造到哈希/加密3.1 签名材料构成签名计算不是简单的“参数 盐 md5”某手这类大型 App 的签名材料通常会包括以下部分请求方法GET/POSTURL path比如/rest/photo/list排序后的 query 参数HTTP body 明文POST 时客户端时间戳随机数一些固定盐值这听起来很像普通签名算法但实际坑点在于“排序规则”和“编码方式”。比如 query 参数是按字典序排序还是按 ASCII 码排序body 是原始 JSON 还是序列化后的格式空值参数字段会不会拼接拼进去的是key还是只拼key这些都决定了最终签名结果。我见过一种通用做法把所有参与签名的参数拼成一个字符串中间用连接最后追加一个动态时间戳和固定盐值再整体做一次 md5。但某手 v8.x 不一定是单次摘要很可能是“摘要 - hex - 再摘要”的多重计算这也是为什么静态分析容易看乱。3.2 sig 的核心计算过程因为涉及商业因素这里不能直接给真实算法但可以把分析思路拆成伪代码方便你对照自己的逆向结果import hashlib import time def gen_sig(path, params, body, salt): # 1. 过滤空值按 key 排序 sorted_params sorted((k, v) for k, v in params.items() if v is not None) # 2. 拼接请求串具体拼接格式以抓包/实际逆向为准 raw_string path ? .join([f{k}{v} for k, v in sorted_params]) if body: raw_string body # 3. 加入时间戳和盐 raw_string str(int(time.time() * 1000)) raw_string salt # 4. 哈希 return hashlib.md5(raw_string.encode(utf-8)).hexdigest()这个伪代码的作用是提供一个排查框架。你可以先按自己的抓包数据尝试构造raw_string再和线上sig比对不一致就逐段拆分对比直到定位到具体的拼接差异。3.3 __NS_sig3 的特殊之处__NS_sig3和sig最大的不同在于它更像一个“加密结果”而不是“摘要结果”。从长度和字符集来看__NS_sig3通常比sig更长而且可能包含大小写字母和数字说明它大概率是某种加密后的密文或者经过 Base64 编码的二进制。我分析 v8.x 时观察到__NS_sig3的输入很可能不止请求参数还把设备相关信息和本地文件内容都带进去了。判断方法很简单在同一 WiFi、同一账号下换一个设备发同一个请求sig可能因为时间戳不同而不同但__NS_sig3的差别会更大反过来在同一设备上修改本地时间、切换网络__NS_sig3也可能跟着变。如果要把__NS_sig3逆清楚建议先定位到生成它的函数在动态调试时打印它的入参。通常这个函数的参数里会有几个 NSString其中一个特别长里面包含设备 ID、应用版本、系统版本等拼接信息。把这个字符串结构理清楚比直接去逆一个未知加密算法容易得多。3.4 参数计算中的常见坑签名算不对90% 是细节问题不是算法方向问题。URL 编码不一致客户端可能对参数做了encodeURIComponent但编码后的%20和在不同平台表现不一样签名用到的究竟是哪种值。时间戳单位有的接口用秒级有的用毫秒级还有的干脆把时间戳拆成了高低位一不留意就会差一整片。空值处理某个参数为空时签名里可能直接不拼也可能拼了空字符串。body 格式签名用的是原始 JSON 还是经过自定义序列化的字符串直接影响结果。固定盐值有些版本里盐值会分渠道不同渠道包盐不一样不要拿一个包的结果套另一个包。这些坑都是我在实际调试中一个个踩过的。最靠谱的办法还是动态打印原始字符串拿原始字符串去对比自己的拼接逻辑而不是拿最终 md5 去反推。4. 实操复现如何验证你算出的签名4.1 用 Python 复现校验流程有了签名逻辑的假想结构后就可以开始用 Python 做离线复现。先把抓包得到的请求保存下来原样写进测试脚本然后按自己的算法生成一个签名和抓包里的签名比对。下面是一个最小可用的测试脚本骨架import hashlib import json import time import requests # 假设从 Charles 导出的一次请求 url https://api.xxx.com/rest/photo/list params { page: 1, count: 20, sig: REPLACE_ME, } body { __NS_sig3: REPLACE_ME, } def load_original(): # 请替换成你抓到的真实请求参数 return url, params, body def calc_sign(req_path, sorted_params, body_text, timestamp_ms, salt): raw req_path ? .join(f{k}{v} for k, v in sorted_params) raw body_text raw str(timestamp_ms) raw salt return hashlib.md5(raw.encode(utf-8)).hexdigest() # 用自己算出来的签名替换到请求中 # 然后通过 Charles 重放或者直接 requests 发送观察返回是否正常比较重要的不是这个脚本能直接跑通而是它能帮你把“拼接规则”和“哈希方式”拆成独立变量。每次改一个变量重新生成一次签名看能不能匹配上抓包值。如果匹配上说明这个变量找对了如果没匹配上就换下一个变量。4.2 动态调试时如何定位失败原因离线比对总是对不上时不要继续猜回到动态调试。最有效的方法是 Hook 签名生成函数的返回值同时打印入参拿到客户端自己计算的原始字符串。以 Frida 为例var signFunc Module.findExportByName(null, some_sign_method); Interceptor.attach(signFunc, { onEnter: function(args) { console.log(arg0:, ObjC.Object(args[0])); console.log(arg1:, ObjC.Object(args[1])); }, onLeave: function(retval) { console.log(ret:, ObjC.Object(retval)); } });打印出来的原始字符串就是“正确答案”。把这个原始字符串和你自己拼的字符串做逐字符对比差异点一眼就能看出来。很多时候差异就出在一个看不见的换行符或空格上肉眼看不出来建议写个脚本做 diff。另外不要只盯着返回值还要留意函数有没有修改 buffer 内容有些签名接口会先返回一个临时值再在后续流程里二次加工直接 Hook 返回点可能拿到的是中间态。4.3 自查清单与常见错误把我在排查签名问题时遇到的问题整理成了速查表现象可能原因排查方向签名一直对不上URL 编码不一致检查%20、、%2F等差异换了时间戳签名就错时间戳单位不对确认是秒还是毫秒只有 __NS_sig3 算不对设备因子缺失检查本地存储的 deviceId请求返回 403签名过期 / 重放次数过多确认时间戳是否新鲜iOS 能过但 Android 过不了两套算法不同不要跨端复用签名逻辑实操里最容易被忽略的是“Header 参与签名”。很多签名算法不光拼 body还会把User-Agent、Cookie、自定义 Header 里的值一起带进去。一旦你发现 body 拼对了签名还是不对马上把 Header 里所有字段也加进去试试。5. 常见问题与排查技巧实录5.1 签名生成后服务端仍然拒绝有次我按既定流程算出了sig服务端照样返回 403后来发现是请求里的__NS_sigTime和本地时间差太多了。原来某手服务端会校验签名里的时间戳和实际请求到达时间的差值超过一定范围直接拒绝。所以本地调试时尽量避免手动修改手机时间最好让系统时间保持自动同步。还有一种情况是请求被重放过。你刚抓包时用的那个sig可能已经被服务端标记为失效再重放自然过不了。遇到这种情况不要怀疑算法重新生成一次新请求用新参数再试。5.2 拿到原始字符串后怎么判断哪些字段参与拼接动态调试打印出的字符串往往很长里面混着 URL、body、设备信息、时间戳和盐值。我会把原始字符串按、拆开然后跟抓包请求里的字段逐一对应。对应不上的那几段大概率就是盐值或者本地动态生成的随机数。注意千万别只盯着明文部分。有些参与计算的字段是二进制数据打印出来是乱码但签名确实把它算进去了。所以最好的做法是打印字节长度和 hex 表示不要直接看字符串。5.3 几个亲测好用的调试技巧不要只 Hook 一个函数把整个调用链上的可疑函数都打上日志一旦入口参数变了能更早定位到源头。用logcat或Console看客户端的日志很多加密模块会带调试输出只是默认被关掉了。静态分析时优先看NSData相关的类方法比如dataUsingEncoding:、base64EncodedStringWithOptions:签名和数据编码强相关。遇到非常规长度结果先怀疑 Base64再怀疑 Hex最后才是标准哈希。这些技巧也许不能保证每个版本都有效但大多数商业化 App 的签名分析都逃不出这个框架。真到了卡住的时候退回来重新从抓包字段差异入手往往比硬啃汇编更快。我在实际跟进 v8.x 时最大的感受是签名调试是个体力活拼字符串的逻辑一旦差一个字节结果就是天壤之别。所以强烈建议每次拿到新版本先把抓包、Hook、字符串打印这些基本功练熟再往深了挖。另外再提醒一句这类分析最好只用于个人学习、测试自己的账号和开发调试不要拿去做大规模爬取或骚扰式抓取别把技术路走窄了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价