·约 10 分钟阅读·DeepSeek

如何逆向一个私有 HTTP API:一套方法,而不是一个花招

一套可复用的 HTTP API 逆向方法论:先观察再动手,梳理认证流程,用浏览器开发者工具与 mitmproxy 抓包,用 curl 重放,识别请求签名,最后把它写成状态机与一个小客户端。

reverse-engineeringnetworkingsecurity

你在网上能找到的大多数「逆向 API」建议,都是一堆花招:在开发者工具里嗅到一个 token,复制,粘进脚本,然后祈祷它下周还能用。那不是方法,一旦 token 过期、或者 服务端加了签名,它立刻就坏。下面这套,是我真正在用的 HTTP API 逆向工程方法 ——一个固定的序列:观察、抓包、重放,然后才谈自动化。它刻意做得很无聊。无聊的 东西才能扛过一次服务端更新。

目的从来不是去攻破什么。目的是理解一个你被授权调用的私有接口,好让你用代码 去驱动它,而不是用浏览器。这个区别很重要,我在结尾会回到它。

先观察,再动手

在你抓下第一个数据包之前,先用大白话回答三个问题:

  1. 我到底想做什么——一个动作,不是一个类别。
  2. 浏览器为了完成它,做的最小一件事是什么。
  3. 什么能告诉我这个动作成功了。

API 逆向的第一条规则是:先在真实的浏览器里手工把这个动作做一遍,然后盯着看。 不要猜接口名。不要臆造字段。让应用自己把它的契约展示给你。如果你要的是登录,就 手动登录一次,盯着 Network 面板。如果是搜索,就跑一次搜索,盯着看。

具体来说,观察的循环是这样的:

  • 打开开发者工具、Network 标签页,勾上 Preserve log,这样跳转和重定向不会丢。
  • 当调用是异步的时候打开 Fetch/XHR 过滤器;当接口是服务端渲染并发生重定向时, 就留在 All。
  • 记下方法、URL、Content-Type、请求体、响应状态、响应体——就按这个顺序。
  • 把动作再做一遍,对两次请求做 diff。保持不变的字段是结构;会变的字段是状态。

最后一步就是全局的关键。任何在两次相同动作之间发生变化的东西——session id、 时间戳、nonce、ticket——都属于生命周期,而天真的脚本就死在这里。

梳理认证流程

认证是首先要弄懂的东西,因为其它一切都挂在它上面。私有 HTTP API 几乎总是落在 下面这几种形态之一:

  • Cookie 会话。 登录后服务端设置一个 session cookie;后续每个请求都带着它。 经典的认证门户就是这个样子,我在 逆向锐捷 ePortal 的那篇文章 里详细走过一遍。
  • Bearer token。 登录调用返回一个 token(JWT 或不透明串),客户端放进 Authorization: Bearer ...。
  • 签名请求。 只有 token 还不够;每个请求还带着一个由请求体加一个密钥推导出来的 签名。

要梳理流程,就把登录那次交换单独拎出来,当成它自己的状态机。是什么请求启动了它? 响应里设置(set)了什么?下一个请求里发送了什么、而它此前不可能被发送?那个差值 就是凭据。

接着找出 token 或 cookie 的生命周期:

  • 它在哪里被铸造——哪个响应设置的它,在响应头还是响应体里?
  • 它活多久?盯住 cookie 上的 Expires/Max-Age,或者 JWT 里的 exp 声明,并等到 一个 401 来确认。
  • 它会轮换吗?有些 API 会签发一个短命的 access token 加一个长命的 refresh token; refresh 调用是流程的一部分,不是边角情况。
  • 什么会让它失效——登出、重新登录、IP 变化、服务端重启?

如果一个值没法跨登录复用,就别把它硬编码。每次运行都重新推导。这是第一次尝试里 最常见的错误。

用开发者工具和 mitmproxy 抓包

对浏览器驱动的流程,开发者工具就够了。对任何不在浏览器里的东西——移动 App、嵌入式 设备、原生客户端——你需要在中间放一个代理。mitmproxy 是我的默认选择;Charles 是一个不错的图形界面替代。

把客户端的 HTTP 和 HTTPS 代理设到跑 mitmproxy 的那台机器上,在客户端上安装 mitmproxy 的 CA 证书,然后实时看请求:

# 交互式界面:查看、过滤、重放
mitmproxy

# 或者把抓包落盘成机器可读格式
mitmproxy -w capture.flows
# 之后读回来并过滤
mitmproxy --rfile capture.flows

我在一次抓包里找的东西和开发者工具里一样,但我会把它写成一张表,因为抓包是数据, 不是感觉:

字段为什么重要
方法 + 路径接口的身份
请求头认证、内容类型、自定义签名
请求体形状JSON 的键与嵌套
状态码成功、认证失败、校验失败

一次成功的流程,只留一份干净的抓包。当重放坏掉的时候,它就是你的参照物。

除了显而易见的东西,还要记什么

证书和重定向也是契约的一部分。记下客户端是否校验证书(并且绝不要为了方便抓包而在 生产里关掉它),并把每一个 3xx 跳转逐字记下来。重定向链不是噪声;它是一串你的客户端 将不得不走过的状态转移。

识别 JSON 形状与语义

一旦你能看到流量,就把响应当成一份待描述的 schema:

  • 形状。 哪些字段总是存在,哪些是可选的,哪些在成功时为 null、在失败时被填充。
  • 身份。 哪个字段命名了资源,它和 URL 里的 id 一致吗,还是另一个内部 id?
  • 分页。 找 page/offset/limit,或者 cursor/next 令牌。游标分页没法从页码 猜出来;你得从每个响应里读出 next 的值,再喂回下一个请求。
  • 错误。 这是大家会跳过的一部分。故意发一个坏请求,读它的错误体。这个 API 用的是 HTTP 状态码、还是带数字 code 的信封、还是两者都用?一个带 {"error": ...} 的 200 算失败吗?在你能机械地分辨成功与失败之前,你写不出健壮的客户端。

把 schema 写成注释放在你的客户端旁边。这是你所能产出的最便宜的文档,而 API 会变, 会证明你当初写下它是对的。

用 curl 重放

在写任何代码之前,先证明你能用 curl 重放复现一个请求。这是把「真的懂了」和 「恰好碰对了」分开的现实检验。

# 重放一个抓到的 GET,带齐认证头,和观察到的完全一致
curl -sS 'https://api.example.internal/v1/items?limit=20' \
  -H 'accept: application/json' \
  -H 'authorization: Bearer eyJhbGciOi...' \
  -H 'x-request-id: 3f2c1a' | jq .

# 用一个 JSON 请求体重放一个 POST
curl -sS -X POST 'https://api.example.internal/v1/items' \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer eyJhbGciOi...' \
  --data '{"name":"demo","tags":["a","b"]}'

如果 curl 成功了,你就拿到了契约。如果浏览器成功而 curl 失败,差异就在一个头、一个 cookie、或一个签名里——把两个请求逐字段 diff,直到对上为止。然后按顺序拆解:一次删掉 一个头,直到某处坏掉,并记下到底是什么在承重。

对一个更长的流程,一个「登录、用 jq 提取 token、再调用两个接口」的 shell 脚本, 比一页散文更有价值:

#!/usr/bin/env bash
set -euo pipefail

# 1) 登录,并从响应体里取出 token
TOKEN=$(curl -sS -X POST 'https://api.example.internal/v1/auth/login' \
  -H 'content-type: application/json' \
  --data '{"username":"'"$USER"'","password":"'"$PASS"'"}' \
  | jq -r '.access_token')

# 2) 用它,并证明它有效
curl -sS 'https://api.example.internal/v1/me' \
  -H "authorization: Bearer $TOKEN" | jq .

识别签名、时间戳与 nonce

如果两次完全相同的重放都失败,而浏览器却一直能用,那就怀疑 请求签名。签名会以 一个头的形式出现,它的值在每次请求时都变,即使其它一切都没变。常见的几种是:

  • HMAC:对一个规范字符串(方法、路径、请求体哈希、时间戳)做 HMAC,用一个共享 密钥。
  • 时间戳:被写进签名字符串里,通常带一个小的有效期窗口——这就是为什么抓到的请求 几秒后就过期了。
  • Nonce:服务端记住的一次性值,所以即使在时间窗口内,把同一个请求原样重放两次 也会失败。

要复现签名,你需要两样东西:算法和密钥。算法通常可以从客户端推断——像 x-signature 这样的头名、一个已知的 HMAC-SHA256 形状、一个排过序的参数列表。密钥才是难的部分, 也正是在这里,诚实的答案经常是「这是一个随二进制一起发出的客户端密钥」。如果这个密钥 不是你有权使用的,方法就在这里停下。不要去找提取第三方密钥的偏门;那不是逆向你自己 被授权的访问,那是另一码事。

当它确实是你自己的接口时,一旦你知道了规范字符串,把时间戳和 nonce 的生成搬进你的 客户端就是小事。复现它,然后通过发一个服务端接受、一个服务端拒绝的请求来验证,好让你 确认这个校验是真的在发生。

诚实地处理限流与验证码

服务端会顶回来,而你怎么回应,定义了你做的是工程还是滥用。

  • 限流。 读 429 和任何 Retry-After 头。按指数退避,并设上限。绝不在紧循环里重试。
  • 验证码。 如果出现验证码,停下。验证码是一个明确的信号:这条路不想要自动化。正确的 行为是响亮地失败,让人来介入,而不是去找一个识别器。
  • 账号安全。 一个错误凭据无限重试,正是账号被锁的方式。让这次尝试失败、退避,并把 错误暴露出来。

我把这条规则原样做进了那个认证门户客户端:先探测再动手,被要求验证码时拒绝循环,失败 时指数退避。这种保守不是礼貌——正是它让一个合法的自动化不至于变成一次锁定。

把它写成状态机,再写成客户端

交付物不是一个碰巧能跑的脚本。而是一个你能推理的状态机,外加一个实现它的小客户端。

给状态命名:probe、discover、authenticate、execute、verify。每一次转移都有一个 理由,每个状态都有有界的尝试次数。然后写出满足三条性质的客户端:

  1. 显式失败。 抛出并停止,而不是默默重试。
  2. 重新推导。 每次运行都重新获取 token 和挑战。
  3. 验证。 动作之后,做那个廉价的检查,证明动作确实落地了。

对于会重定向、会协商的接口,理解认证门户到底是什么,帮助很大;我把它写在 认证门户究竟在做什么里,那其实是同一套「请求与 重定向」的推理,只是应用在一个 HTTP API 上。

伦理与授权使用

把这一点说清楚,因为它是真正要紧的部分:

  • 逆向一个你被授权调用的 API,来自动化你自己合法的访问,是正常的工程。
  • 自动化你无权享有的访问、绕过付费/额度/访问控制、或者碰别人的账号,都不是。
  • 不要把这类工具指向你不拥有、也没有书面许可去测试的系统。
  • 如果这个接口要求你去提取别人的密钥、或去攻破验证码,那就停下——那是边界,不是一道 待解的谜题。
  • 遵守你所在之处的服务条款与法律。

上面这套方法,刻意对目标是谁不敏感。它在你自己的网关上、和你自己的 SaaS 账号上一样 好用。把它一直瞄向你有权触碰的东西,不是硬贴在结尾的免责声明;它是整套方法所立足的 前提。

简短版

先观察,再动手。在写下第一行代码之前,先梳理认证流程和 token 生命周期。用开发者工具 抓包,当客户端不是浏览器时就伸手去拿 mitmproxy。描述 JSON 形状、分页和错误语义。用 curl 重放证明一个请求,然后一个个删掉头直到某处坏掉,好让你知道什么在承重。通过把 同一个请求发两次来识别签名。尊重限流,在验证码前停下。然后把这整件事编码成一个状态机, 和一个无聊的、会重新推导自己的凭据、并验证自己工作的小客户端。这就是一套方法。它会比 你所能复制粘贴的任何 token 都活得更久。