尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python实现phantom-token签名逆向:从抓包分析到算法还原

Python实现phantom-token签名逆向:从抓包分析到算法还原 先说清楚一件事拿到一个陌生接口发现请求里藏着个叫phantom-token的参数研究它的生成逻辑再用 Python 把这个逻辑原样实现出来——这种活儿干多了之后整个过程其实是有固定套路可循的。这篇复盘就记录一次我针对某 OTA 平台标题里说的是携程就按这个场景讲酒店搜索场景的完整实战过程涉及抓包分析、参数定位、调用栈回溯、算法还原和 Python 纯算实现五个环节适合已经会基础抓包、正准备往参数逆向深挖的朋友参考。整个过程不算复杂但里面的判断思路和排查技巧比单点技术本身更值钱。1. 项目背景与整体思路设计1.1 这个 token 是什么为什么要逆向它先解释一下phantom-token是个什么东西。在浏览器里正常打开酒店搜索页页面会向后端发一堆异步请求其中一个请求的请求头或者请求参数里会带上一段看起来像乱码的字符串名字就叫phantom-token。它本质上是一个由前端 JS 动态生成的签名值后端拿到后会按照同一套规则重新计算一遍比对一致才把数据返回给你。我为什么要去逆向它起因是我想做酒店价格监控和比价方面的技术验证需要在本地脚本里重新构造搜索请求。直接拿浏览器 Cookie 去请求很快就会发现光有 Cookie 不够缺少这个 token服务器直接返回 403 或者一段“参数错误”的提示。也就是说只有把 token 的生成逻辑搞清楚才能在脱离浏览器的环境下构造出合法请求。这个场景其实非常典型做接口调试、价格采集、数据研究的人大概率都撞上过类似的东西。从技术角度看这个东西值得拆解的原因有几点一是它出现在高频核心搜索接口上普适性比较强二是它的生成往往依赖多个变量路径、参数、时间戳、盐值等涉及前端签名算法设计的一般思路三是它能很好地串起“抓包 - 定位 - 还原 - 复现”这条完整的学习链条。这篇文章不讨论任何绕过风控批量抓数据的事只讲纯技术上的分析方法和通用实现路径。1.2 逆向目标与整体流程拆解在动手之前我把目标定得很明确写一段 Python 代码传入和浏览器相同的请求参数能在本地算出和浏览器一致的phantom-token并且用这个 token 能成功拿到接口返回的数据。这个目标可以拆成三条验收标准算法确定性相同的入参必须产出相同的 token。环境无关性纯 Python 计算不依赖浏览器环境不需要执行 JS。请求可用性生成的 token 配合正常 Cookie能拿到和浏览器一致的数据。整体流程我分成了六个阶段每条线上都有对应的产出物阶段核心动作产出物抓包分析定位携带 phantom-token 的请求记录请求头、参数、Cookie完整请求快照特征判断分析 token 长度、字符集、依赖变量token 特征画像定位生成逻辑搜索 JS 关键字、Hook 关键函数可疑函数清单断点验证打断点确认参数来源和拼装顺序参数拼接规则算法还原用 Python 实现签名算法跑通一致性可运行 Python 代码真实请求验证带 token 发请求比对返回结果最终验证截图这个流程看起来好像每个点都有人讲过但真正执行的时候每一步都有容易卡住的小地方。下面我按实际推进顺序把每个环节的关键细节和踩坑记录展开说。2. 抓包分析与 token 特征判断2.1 从网络面板到请求定位我习惯先用 Chrome DevTools 的 Network 面板做第一轮分析因为浏览器环境里能直接看到完整请求上下文。打开酒店搜索页输入一个城市和日期点搜索等列表加载完切到 Network 面板按Fetch/XHR筛选就能看到一批异步请求。这个时候不要急着翻先在过滤框输入hotel或者search缩小范围逐个点开看响应内容找到那个返回酒店列表数据的接口。确认接口之后把请求头从头到尾过一遍。我发现请求头里有一个自定义 Header名字就叫phantom-token后面跟着一串 64 位的小写十六进制字符串。类似这种phantom-token: 8f3a2b1c9e4d5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b这个发现很重要因为它决定了后续的逆向路线。64 位 hex 字符串第一反应就是 SHA-256 的输出但也不排除是两次 MD5 拼接或者自定义哈希。第二反应是去对比不同请求之间的 token 变化规律看看它到底跟哪些变量绑定。顺便说一句抓包工具有很多选择Charles、Fiddler、Burp Suite 各有各的适用场景。浏览器 DevTools 适合快速定位但如果你想改包重放建议上 Fiddler 或 Charles配合本地代理能方便地截断、修改、重发请求。我这次主要用 DevTools 做定位用 Fiddler 做改包验证。2.2 三个特征决定逆向路线拿到 token 样本之后我总结了三类特征基本决定了后面怎么走第一是长度和字符集。64 位、纯小写 hex大概率是某种摘要算法的产物。如果字符串里同时有大写字母、小写字母和数字还有、/、这种字符那就要考虑 Base64 编码的哈希值如果还有下划线和中划线那就可能是自定义编码。特征判断能帮你排除错误方向避免在一个 Base64 字符串上硬套 MD5 逻辑。第二是变化规律。我刷新了几次页面发现每次请求的 token 都不一样。然后我把搜索条件从“北京”改成“上海”token 也变了。再然后我清掉部分 Cookie 重新刷新token 还是会变但把时间前后差了五分钟的两次请求对比看不出哪里有关联性。这说明 token 至少依赖当前请求参数和某个时间因子。第三是依赖关系。这一点要靠改包验证。我用 Fiddler 把请求里的 query 参数改掉一个字符不重新生成 token直接放行后端返回参数校验失败。这说明 query 参数是参与签名计算的。接着我改动了请求头里的 Cookie 中的一个关键值具体哪个值下面会讲同样不改 token也报了签名错误。这就说明 token 的计算输入里还有 Cookie 相关的因子。这三类特征一出来逆向路线基本就清晰了先找生成函数再确认拼装规则最后写 Python 复现。没有特征判断直接去翻 JS很容易被混淆代码带偏浪费大量时间。2.3 参数的联动观察在正式打开 JS 文件之前我还做了一步操作用 Fiddler 的自动响应AutoResponder功能把某些参数改成固定值观察 token 的输出变化。比如我把请求 URL 里的checkin日期固定成2025-06-01连续请求几次发现 token 的中间段有肉眼可见的规律性变化但整体每次都不同说明计算里掺杂了时间戳或者随机数。这里有一个很重要的判断如果 token 每次都不同但差异只集中在尾部几个字符那大概率是末尾拼了一个随机字符串如果差异出现在整段字符串上那可能是入口参数里带了一个随机值比如 UUID 或设备 ID。这个细节很影响后续的断点调试策略因为你要重点观察的变量完全不一样。我用一个简单办法验证随机因子在 Console 里手动执行页面暴露的加密函数如果碰巧挂在了 window 上连续调用两次如果输出完全一样说明随机因子要么不参与要么被缓存了如果输出不一样说明每次调用都会取新的随机值。我这次踩到的平台函数名不是直接暴露的所以这步没走通但不代表这个思路没用反而提醒我要用更底层的方式去定位。3. Hook 定位与调用栈回溯3.1 用 Hook 快速锁定生成函数既然 token 是一段 64 位 hex那生成它的函数八成是哈希函数或者是先做序列化再做哈希。我打开 Sources 面板在全局搜索里输入phantom大概率能搜到一堆结果可能是变量名、注释、字符串也可能就是生成逻辑本体。但直接搜关键字有个问题混淆压缩后的 JS 里很多变量名都被重命名了关键字不一定能搜到。更可靠的办法是 Hook。Hook 的核心思路是在目标函数执行前后插入一段我们自己的代码把入参和返回值打印出来。常见哈希函数包括md5、sha256、sha1它们的实现通常会挂在某个工具对象上比如CryptoJS.MD5、window.md5等。我一般会先写一个通用 Hook 脚本在页面加载前注入拦截常见的哈希入口。以 MD5 为例Hook 代码可以这样写(function () { // 假设页面把 md5 挂载在 window 上常见于引入某个加密库之后 const originalMd5 window.md5; if (!originalMd5) { console.log([Hook] window.md5 不存在尝试其他入口); return; } window.md5 function (...args) { const result originalMd5.apply(this, args); console.log([Hook MD5] 入参:, JSON.stringify(args), 输出:, result); return result; }; })();这段脚本通过 Chrome DevTools 的 Overrides 功能或者油猴脚本注入跑起来之后刷新页面Console 里就会不断打印所有 MD5 调用的入参和输出。如果发现某个输出的值正好等于请求头里那个phantom-token那就直接锁定目标了。不过这里有个坑你提前不知道它是用 MD5 还是 SHA-256甚至可能是自定义的哈希算法所以光 Hook 一个函数不够。我最后的做法是同时 Hook 了CryptoJS.SHA256、CryptoJS.MD5、window.btoa、JSON.stringify这几个高频入口从结果里筛。实际执行下来Hook 输出里还真有一条哈希调用的输出和请求包的 token 完全一致这就算锁定成功了。3.2 调用栈回溯从出口找入口锁定了哈希函数的调用点之后下一个问题是这个哈希函数的入参是从哪拼出来的要回答这个问题就得看调用栈。在刚才的 Hook 脚本里加一行console.trace()重新刷新页面Console 里会打印出完整的调用栈console.log([Hook SHA256] 入参:, JSON.stringify(args), 输出:, result); console.trace();调用栈里会显示一层层的函数调用关系比如at Object.sha256、at generatePhantomToken、at requestInterceptors之类的函数名。看到类似generatePhantomToken这种名字基本就接近真相了。有的压缩代码里函数名会被替换成单个字母这时候就要靠堆栈里的文件 URL 和行号来定位。找到之后我在 Sources 面板里跳转到对应行打上断点然后重新触发搜索请求。断点命中后Watch 面板里查看当前作用域的变量能直接看到某几个变量名带着time、query、token、salt之类的关键词。我本地的经验是签名函数一般不会超过五十行核心逻辑就是“拼字符串 - 做哈希 - 返回结果”。3.3 断点验证参数从哪来断点验证是逆向过程中最容易出细节错误的一步。我第一次还原算法的时候以为签名输入只有请求路径和时间戳结果跑出来怎么都对不上。后来在断点里仔细对比才发现在哈希之前还拼了一个固定的盐值字符串以及 Cookie 里的deviceId。我建议到了这一步先不要急着关掉断点而是手动把作用域里的关键变量抄下来包括路径、排序后的参数串、时间戳、盐值、设备 ID、随机数等。然后回到 Console手动执行一次拼接和哈希如果算出来的结果和请求头里的 token 完全一致就说明你已经完整还原了生成逻辑。这一步通过了后面写 Python 就只是翻译工作而已。有个细节值得提一下有的平台会把时间戳放到签名串的中间而不是开头参数排序方式也可能是按照 ASCII 码排序而不是浏览器里看到的顺序。这些都是需要通过断点反复对比变量值来确认的不能想当然。4. 核心算法还原与 Python 纯算实现4.1 参数拼接规则复现通过断点验证我最终还原出的签名输入规则大致是这样的平台细节做过脱敏处理但结构是可信的sign_input request_path sorted_query_string device_id timestamp salt其中sorted_query_string是对 URL 上的查询参数按照参数名做字典序排序之后拼成keyvaluekeyvalue的格式timestamp是毫秒级时间戳salt是从 JS 里抠出来的一个固定字符串。最后对sign_input做 SHA-256输出 64 位 hex 就是phantom-token。这个结构很典型很多平台的签名都是类似的套路路径 排序参数 设备标识 时间戳 盐值然后做一次或多次摘要。区别只在于拼接顺序和盐值内容。还原的时候要特别留意参数拼接顺序因为哪怕少一个号算出来的值就完全不一样。查询参数排序这一步我踩过一个坑JS 里的URLSearchParams排序规则和 Python 里的urlencode不完全一致。JS 对某些字符的编码方式跟 Python 默认行为不同比如空格在 JS 里会编码成%20但 Python 的urlencode默认会编码成。解决方法是统一用urllib.parse.quote手动处理并且显式指定quote_viaquote保证两边行为一致。4.2 哈希算法的选择与验证还原逻辑之后我先不急着写完整代码而是先用 Python 在交互环境里验证一次哈希算法选型是否正确。根据 token 长度是 64 位 hexSHA-256 是头号候选但也有可能是 SHA-512 截断或者两次 MD5 拼接。验证方法很简单把从断点抄下来的输入串依次用几种常见哈希算一遍看哪个输出和浏览器里的 token 相等。我用的验证代码如下import hashlib sign_input /api/hotel/search?checkin2025-06-01checkout2025-06-02citybeijingdevice_id_xxx1718000000000a1b2c3d4 # 注意这行是演示用实际请从断点里抄完整输入串 print(MD5 :, hashlib.md5(sign_input.encode()).hexdigest()) print(SHA1 :, hashlib.sha1(sign_input.encode()).hexdigest()) print(SHA256 :, hashlib.sha256(sign_input.encode()).hexdigest()) print(SHA512 :, hashlib.sha512(sign_input.encode()).hexdigest())跑完之后SHA-256 的输出正好和浏览器抓到的 token 一致哈希选型就定下来了。这里有个小技巧如果你在断点里拿到的输入串是完整的可以直接在 Python 里算完和抓包值比对这一步在任何环境里都能做不需要依赖浏览器。4.3 引入随机因子后的适配我这次遇到的平台生成 token 时还掺杂了一个随机字符串导致每次请求的 token 都不完全一样。起初我以为随机字符串也会参与签名后来在断点里观察发现随机字符串只在入口参数中短暂存在并不会进入最终哈希输入。换句话说签名对随机值并不敏感真正参与计算的只有路径、参数、设备 ID、时间戳和盐值。但我也处理过另一种情况某个平台的 token 生成逻辑里确实拼了Math.random().toString(36)的结果导致同一秒内发多次请求token 也是不同的。遇到这种随机因子参与签名的场景调试的时候可以把 JS 里的随机函数替换成固定值比如Math.random () 0.123456789再做对照实验就能判断随机值是否真的影响输出。Python 侧如果也要带随机性用secrets.token_hex(8)之类的方式生成即可。这个适配步骤比较考验细节因为随机因子出现的位置和生命周期都不同。建议在恢复逻辑的时候先确认随机值参与不参与签名再决定是否在 Python 代码里模拟它。4.4 完整 Python 实现示例整个签名逻辑还原完成后我把它封装成了一个简单的客户端类。这里给出一个可运行的示例参数名和盐值都做了脱敏重点看思路和结构import hashlib import secrets import time from urllib.parse import urlencode, quote class PhantomClient: def __init__(self, device_id: str , salt: str ): self.device_id device_id self.salt salt staticmethod def _sort_query(params: dict) - str: return urlencode(sorted(params.items()), quote_viaquote) staticmethod def _timestamp() - str: return str(int(time.time() * 1000)) def generate_token(self, path: str, params: dict) - str: sorted_query self._sort_query(params) raw f{path}{sorted_query}{self.device_id}{self._timestamp()}{self.salt} return hashlib.sha256(raw.encode()).hexdigest() def build_headers(self, path: str, params: dict) - dict: return { User-Agent: Mozilla/5.0 ..., phantom-token: self.generate_token(path, params), Referer: https://example.com/hotel/list, } if __name__ __main__: client PhantomClient( device_iddevice_id_xxx, salta1b2c3d4e5f67890 ) params { city: beijing, checkin: 2025-06-01, checkout: 2025-06-02, } token client.generate_token(/api/hotel/search, params) print(phantom-token:, token)这段代码的核心逻辑只有四行排序参数、拼字符串、算时间戳、做哈希。剩下的都是工程封装。真实使用的时候设备 ID 需要从 Cookie 或者请求头里取盐值要提前从 JS 里分析出来路径也要和你抓包时看到的接口路径保持一致。4.5 用真实请求验证复现结果代码写完不算完必须用真实请求验证。我在 Python 里用requests库组装了一个完整请求头信息包括 User-Agent、Cookie、Referer以及刚才生成的phantom-token请求参数和浏览器一致。发送之后服务端返回的响应状态码是 200返回体里是正常的酒店列表数据。到这里整个逆向闭环就算打通了。但这里我想强调一点验证阶段用的请求频率一定要低间隔拉长一些定位是“技术可行性验证”不是“压测”。我在实践里通常每次请求间隔五秒以上连续验证几次没问题就不再发了。这种验证方式的优点很明显它证明你的 Python 实现和浏览器里的算法完全一致后续如果要调整参数只需要重新生成 token 即可不需要再依赖浏览器环境。至于请求频率、代理策略这些东西属于另一个层面的问题不在这次讨论范围内。5. 踩坑记录与问题排查5.1 token 一直失效先检查这三点我在还原过程中遇到过几次“明明算法对了但请求还是失败”的情况。复盘下来绝大多数问题出在以下三点建议按顺序排查时间戳是否一致。签名里的时间戳是生成 token 那一刻的毫秒值如果脚本运行环境的时间和服务器时间偏差过大哪怕算法完全正确后端也会判定签名过期。我习惯在请求前用网络时间协议校正本地时钟或者从服务器响应头里读时间再做偏移修正。设备 ID 是否和 Cookie 对应。很多签名会把设备 ID 和 Cookie 绑定生成 token 时用了 A 设备 ID请求时却带着 B 设备的 Cookie必然失败。写代码时要从同一个来源取这两个值不要手工拼接。参数顺序和编码是否一致。URL 查询参数的排序和编码方式必须和 JS 里保持一致尤其是特殊字符的转义。我遇到过因为一个和%20的差异导致签名对不上的情况这类问题最隐蔽也最花时间。5.2 调试时误改环境导致的偏差用 Chrome DevTools 的 Local Overrides 做本地覆盖时很容易因为修改了 JS 文件导致页面行为异常。我踩过一次坑想给哈希函数加日志结果改错了闭合括号整个文件语法错误页面直接白屏。排查了半天才发现是 Overrides 文件的问题而不是平台的服务端出 bug。建议把原本的 JS 文件先备份一份修改时只加日志不改逻辑改完先在浏览器里确认页面正常再做断点验证。这个习惯看起来简单但能省掉大量排查时间。5.3 浏览器指纹与自动化检测有些平台的风控不止看 token还会校验浏览器的指纹信息包括navigator.webdriver标志、Canvas 指纹、WebGL 渲染信息等。如果 token 是在浏览器里通过自动化工具生成的这些指纹信息很可能会被服务端识别出来。但纯 Python 实现则不存在这个问题因为你是在本地计算 token不依赖浏览器环境服务的接收方看到的就是一个普通请求。不过纯 Python 实现也有自己的弱点比如 TLS 指纹和 HTTP/2 指纹和真实浏览器不同。如果遇到特别严格的风控还需要进一步处理但这就属于另一个层面的对抗了。我的态度是学习技术原理可以但实际应用时要注意边界不要在未经授权的情况下对线上服务做高频请求。5.4 频率控制与合规边界这部分我必须强调一下逆向技术本身是中性的它的价值在于帮助你理解接口签名机制、研究加密算法原理、验证数据完整性和做安全研究。比如你可以拿这套方法去分析自己负责的接口是否存在签名缺陷或者用于自动化测试场景下构造合法请求。这些都是正当用途。但反向使用就有问题了比如绕过平台风控批量抓取用户隐私数据、恶意刷接口、干扰服务正常运行。这些行为既违反平台规则也可能触碰法律红线。这篇博文只讨论纯技术的分析方法和通用实现路径不鼓励任何违规使用。合规的边界很简单你要么是平台方授权的研究人员要么是在自己的测试环境里复现要么只是学习算法原理。超出这个范围的使用请务必仔细评估风险。5.5 常见问题速查表最后把这次实战中可能遇到的问题整理成一张速查表方便以后排查现象可能原因解决思路请求返回 403token 生成时间距请求时间过长缩短生成与发送之间的间隔校正系统时间返回“参数错误”query 参数排序或编码不一致抓包对比参数串确认排序规则返回“签名无效”盐值或拼接顺序还原错误重新断点验证输入串逐字符比对返回“设备异常”设备 ID 与 Cookie 不对应从同一上下文中提取设备 ID 与 Cookie请求频率过高被限流触发服务端频控降低请求频率增加随机延时页面白屏Local Overrides 改坏了 JS恢复备份文件重新添加日志这张表是我多次逆向实践中沉淀下来的通用排查模板遇到具体问题的时候照着查能少走不少弯路。6. 一点延伸思考这次携程酒店phantom-token的逆向本质上是一道“给定输入输出反推函数逻辑”的题目。互联网产品的前端签名方案五花八门有的是 MD5 加盐有的是 SHA-256 拼接有的还会套一层对称加密但分析方法都是一样的先定位入口再还原数据流最后用目标语言重写。我个人的经验是这个领域最值钱的不是某个具体平台的算法而是那一套从抓包到断点验证的方法论。它能平滑迁移到很多类似的场景无论是 Web 端还是 App 端核心思维都一样——只是具体工具不同而已。比如 App 端把浏览器 DevTools 换成抓包工具把 JS Hook 换成 Frida流程骨架是通用的。最后分享一个小习惯做这种逆向一定要把每一步的验证动作保留下来尤其是断点里确认过的参数拼接串和输出值。我碰到过几次调试的时候是对的过了几天再跑突然不对翻日志才发现是平台改了盐值或者签名规则。工具链会过期但方法论不会过期——把当时的关键输入输出记下来下次算法一变你可以很快定位到变的是哪一段。
返回列表