协议分析学堂
从零开始 · 从底层到实战 · 图解版

把网络安全与协议分析
从头到尾讲成一条线

这不是一堆名词的堆砌。全站按数据真实的流动顺序组织:从一根网线里的电平,到 TLS 握手里的每一个字节,再到 App 里的签名、风控包与设备指纹。每一层都回答三个问题:它解决什么问题、长什么样、怎么被看懂。

36页系统教程
106张矢量图解
330条术语解释
306道自测题
536分钟通读
人话版 这站就是一本能装在手机里的图文教材:左边是目录,点进去就是一页一页的图和解释。看不懂的名词可以去「术语词典」搜,学完一章可以做页尾的自测题。飞机上没网也能看。
离线阅读 本站是单文件零依赖页面,不加载任何字体、图片或第三方脚本。点右上角「离线保存」会下载一个完整 HTML,飞机上双击就能看,导航、搜索、自测全部可用;在线访问时浏览器还会自动缓存整站,断网后直接打开域名也能读。

0.1先建立一张地图:数据到底怎么走

很多人学安全,卡在"知识点都会,但串不起来"。原因是没建立分层直觉。先看这张图:你点一下"登录",数据从手指到服务器,再回来,中间穿了五层衣服。

一次登录请求经过的五层 你的手指 ① 业务层 账号 / 密码 / 验证码 ② 签名层 参数排序 + 哈希 + 密钥 ③ TLS 握手 + 加密 ④ TCP / IP 分段、编号、重传 ⑤ 物理链路 Wi-Fi / 蜂窝 / 光纤 服务器 逐层拆开,反向验一遍 中间任何一层被看懂,整条链路就没有秘密 协议分析的本质:定位"秘密藏在哪一层",再逐层剥开
图 0-1 一次登录要穿过五层;分析的顺序通常是自下而上定位,自上而下验证

0.2三条学习路线,按你的底子选

路线 A · 零基础

先把地基打牢

不懂网络也不懂编程。按 01 → 02 → 03 → 04 → 05 → 06 → 07 顺序读,每章做完自测再走。

预计 6 到 8 天
路线 B · 有开发基础

直接进协议与逆向

会写代码、懂 HTTP。从 04 → 07 → 08 → 09 → 10 → 11 切入,遇到密码学名词回 03 补。

预计 3 到 4 天
路线 C · 安全从业

补移动端与风控

熟悉 Web 安全。重点读 11 → 13 → 14 → 15 → 16 → 17,再做 20 到 24 的对照。

预计 2 到 3 天

0.3全站地图

七个分组,36 页。每一页都是独立完整的,可以从任意一页开始;页尾的"上一页 / 下一页"会把线接回去。

入门 · 00–05

零基础预备课

第一次接触这些概念,先读这一页:用寄快递和门牌号讲清客户端、服务器、域名、Token。

先读这一页 →
入门 · 02–05

底层地基

网络分层与封装、二进制与编码、密码学、数字签名与证书。看懂后面所有章节的前提。

从 01 开始 →
协议 · 05–10

协议与抓包

TCP/UDP、DNS/HTTP、TLS、序列化格式、抓包工具链、逆向方法论。把"看得见"变成"看得懂"。

从 05 开始 →
移动端 · 11–15

App 逆向与指纹

Android/iOS 逆向、加固混淆、SSL Pinning、设备指纹与风控体系。移动端分析的主战场。

从 11 开始 →
实战 · 16–19

完整案例与工程化

纯协议登录全链路、签名与风控包构造原理、自动化稳定性、排错手册。把知识变成可复现的流程。

从 16 开始 →
Web / API · 20–23

服务端与云

OWASP、认证授权、API/GraphQL/WebSocket、云原生与容器。补齐"另一侧"的视角。

从 20 开始 →
防御 · 24–25

检测与合规

风控如何识别异常、日志与告警怎么建、法律边界在哪里。做技术的人必须懂的另一半。

从 24 开始 →
实战 · 21

真实案例集

三个脱敏案例:电商 App 协议分析、企业信息平台风控、批量执照识别。看方法怎么落到真实项目上。

看案例 →
附录 · 28–35

词典与题库

330 条术语、工具与命令速查、面试题库、学习路线、速记卡、常见误区。

随时查阅 →
附录 · 33

关于本站

版本、更新日志、内容边界、离线使用与反馈方式。

查看 →

0.4怎么用这套教程才有效

方法 1

先猜,再看

每张图出现前,先在心里猜"下一步会发生什么"。猜错的地方就是你真正的知识缺口,比从头读到尾有效得多。

方法 2

用自己的话复述

读完一节,合上页面,用三句话讲给一个不懂技术的人听。讲不下去的地方,说明你只是记住了词,没记住因果。

方法 3

对拍,不靠感觉

凡是"我算出来了"的结论,都要拿真实流量或真实设备对一遍。对不上就是没懂,不要用"应该是环境问题"糊过去。

方法 4

间隔重复

当天读、隔天看速记卡、一周后做面试题库。术语表按主题分组,适合一次刷一组。

0.5快捷键与阅读功能

功能怎么用说明
全文搜索Ctrl / ⌘ + K,手机点底部「搜索」跨 36 页搜索正文,回车直接跳到命中段落
翻页← →按全站顺序上一页 / 下一页
本页目录右下角圆形按钮 / 手机底部「目录」列出当前页所有小节,点击平滑滚动
字号右上角 A- A+四档字号,自动记住选择
离线保存右上角「离线保存」下载完整单文件,断网可读
看图手机上左右滑动 / 点「放大」图解保持字号不缩小,可全屏查看
翻页手机左右滑动屏幕在正文区域横向滑动切换上一页 / 下一页
阅读进度自动侧栏打绿点的页面表示已读过
关于无脚本环境 本站的页面切换使用纯 CSS 的 :target 实现,不依赖 JavaScript。即使预览器禁用了脚本,左侧导航、折叠问答、字号之外的所有阅读功能都正常;只有全文搜索、进度条和离线保存按钮会失效。

0.6内容边界:先把红线说清楚

必须遵守 本站所有技术内容仅用于自有设备、书面授权的目标、公开教学靶场。只讲原理、检测思路与防护方法,不提供针对具体平台的绕过、过检、批量注册操作步骤。未经授权对他人系统进行抓包、逆向、渗透,可能触犯《网络安全法》《刑法》相关条款。

这不是一句免责套话。真正做这行的人都明白:技术能力的边界,和授权文件的边界是重合的。第 25 章会把法律条款、授权范围怎么写、哪些行为绝对不要碰,一条条讲清楚。

开始之前 你不需要先成为程序员,也不需要先背完所有术语。只需要保持一个习惯:看到任何"加密""签名""验证",都问一句——它在防谁? 这一个问题会带你走完全站。
第 0 章 · 开课之前

零基础预备课:先搞懂这 8 个问题

如果你没学过网络、没写过代码,直接看后面的 TLS、逆向、风控会像看天书。这一页不涉及任何技术细节,只用寄快递、门牌号、身份证这些日常经验,把后面 35 页要用到的底层概念先铺一遍。看完这页,你就能看懂全站 80% 的图。

客户端 / 服务器IP / 域名 / 端口请求 / 响应 加密与签名Token抓包 / 逆向
先记住这一句 手机上的每一次点击,本质都是一句话从你的手机寄到远处的一台电脑,再把回信寄回来。所谓网络安全,就是研究这句话在路上会不会被偷看、被调包、被冒充;所谓协议分析,就是把这句话拆开看清楚。
人话版 这一页就讲一件事:你点一下"登录",手机和服务器之间到底发生了什么。搞懂这条线,后面所有章节都是在这条线上加细节。

0.1问题一:点一下按钮,发生了什么

你在 App 里输入账号密码,点"登录"。这一瞬间发生了四件事,顺序固定:

一次请求与响应的完整往返 你的手机 客户端 Client 远方的电脑 服务器 Server ① 请求:账号 + 密码 + 签名 ② 响应:登录成功,给你一个凭证 ③ 后续请求:每次带上这个凭证 ④ 服务器确认凭证有效,返回你的数据 这四步就是后面所有章节反复出现的基本骨架
图 0-1 一次登录的四个动作:请求、响应、带凭证、取数据

把这四步想成寄快递:

  • 请求:你写一张单子(账号密码),装进信封寄出去;
  • 响应:对方收到后回一张单子(登录成功),还附了一张"取件卡";
  • 带凭证:以后每次取件都出示这张卡,对方就知道是你;
  • 取数据:对方核对卡片,把包裹给你。
术语对照 单子叫请求报文(Request),回信叫响应报文(Response),取件卡叫 Token(令牌),寄快递的动作叫发起 HTTP 请求。

0.2问题二:手机怎么找到那台电脑

互联网上有几十亿台设备,你的手机怎么知道该把请求发给哪一台?靠三层地址,缺一不可。

域名、IP、端口三层寻址 域名 shop.example.com 像"某某大厦" DNS IP 地址 203.0.113.10 像"街道门牌" 端口 443 像"房间号" 服务 程序 三步合起来:先查通讯录拿到门牌,再按门牌找到楼,最后按房间号找到具体的人 常见端口:80 是普通网页,443 是加密网页,53 是域名查询,22 是远程登录 端口号只是"程序的门牌",同一台服务器上可以同时开很多个服务
图 0-2 域名负责好记,IP 负责找到机器,端口负责找到机器上的程序
概念生活类比作用
域名大厦名字给人看的,好记
DNS通讯录把名字翻译成 IP
IP 地址街道门牌号给机器看的,能找到具体设备
端口房间号找到这台机器上的哪个程序

0.3问题三:寄过去的那句话长什么样

这句话不是随便写的,它有固定格式。最常见的是 HTTP 报文,分成三块:

请求行:方法 + 路径 + 版本
POST /api/login HTTP/1.1

请求头:一堆"附件说明"
Host: shop.example.com
Content-Type: application/json
User-Agent: MyApp/8.2.1 (Android 14)

请求体:真正的业务内容
{"username":"demo","password":"******","ts":1729900000,"sign":"9f2c..."}

三块各管一件事:

  • 起始行:我要干什么(GET 取数据 / POST 提交数据);
  • 头(Header):附件说明,告诉对方"我是什么客户端、内容是什么格式、我带没带凭证";
  • 体(Body):正文,通常是 JSON。

JSON 就是一种"用花括号和冒号写清单"的格式,你可以把它当成一张填写好的表格:

{
  "code": 0,                 // 0 表示成功
  "msg": "ok",
  "data": {
    "uid": 10086,
    "nickname": "小明",
    "token": "eyJhbGciOiJIUzI1NiIs..."
  }
}
注意 后面章节里会反复出现"请求头"和"请求体"。记住一句话:头是说明,体是内容。签名、设备信息通常放头里,业务参数放体里,但不同公司习惯不同。

0.4问题四:为什么这句话要加密

因为这句话一路上要经过很多台你不认识的机器:你家的路由器、运营商的基站、机房的交换机。任何一环都能偷看。看下面这张对比图:

明文与加密传输的对比 你的手机 发送方 中间人看得一清二楚 账号 abc 密码 123 服务器 接收方 不加密:内容全程裸奔 你的手机 先加密 中间人只看到乱码 x9F2aC7e1B4... 服务器 用密钥解开 加密后:只有拿钥匙的人能看懂
图 0-3 加密不改变"寄快递"这件事,只是把信封里的字变成只有收件人能读的乱码

但光加密还不够。攻击者虽然看不懂内容,却可以把整句话原样再寄一遍(重放),或者把信封上的名字换掉(冒充)。所以还需要:

  • 加密:防偷看;
  • 签名:防篡改、防冒充;
  • 时间戳 + 随机数:防重放;
  • 证书:证明"对面那台机器真的是它"。
先分清 加密解决"别人看不懂",签名解决"别人改不了、冒充不了"。这是两件事,第 4、5 章会分别讲透。很多人把它们混为一谈,这是初学者最大的坑。

0.5问题五:登录到底在干什么

登录不是"把密码发给服务器然后它记住你"。真实过程更像办一张临时门禁卡:

登录与令牌的流程 ① 输入账号密码 密码只在这里用一次 ② 服务器核对 比对密码哈希,不存明文 ③ 发一张 Token 有有效期,可被吊销 ④ 手机保存 Token 以后每个请求都带上它 ⑤ 请求业务数据 Header: Authorization ⑥ 服务器验证 Token 有效才返回数据 Token 就像临时门禁卡:丢了可以挂失,到期自动失效 所以抓到 Token 往往比猜到密码容易,这也是风控重点
图 0-4 登录的核心不是密码,而是登录成功后拿到的那张"临时门禁卡"

这也解释了一个常见疑问:为什么改密码之后,别的设备会被踢下线?因为服务器把旧 Token 作废了。Token 不是密码,它只是一张有期限的通行证。

0.6问题六:抓包到底在抓什么

"抓包"不是抓一个包裹,而是把手机和服务器之间来回的每一句话都录下来。录下来之后你能看到:

  • 请求发到了哪个域名、哪个路径;
  • 带了哪些头、哪些参数;
  • 服务器回了什么状态码、什么内容;
  • 一次操作背后其实发了好几个请求。

但抓包有个前提:你得能看懂它。如果流量是加密的,你只能看到一堆乱码和域名。于是就有了后面两条路:一条是让手机信任你的代理证书(第 10 章),另一条是直接分析 App 里的加密逻辑(第 12 到 15 章)。

你看到的说明对应章节
域名 + 路径请求发往哪里第 7 章
请求头客户端身份、签名、设备信息第 5、18 章
请求体业务参数第 9 章
乱码加密了,需要密钥或 hook第 8、12 章
证书错误证书固定(pinning)第 15 章

0.7问题七:逆向到底在逆向什么

App 是一个打包好的程序。你手上没有它的源代码,只有安装包。逆向就是把打包好的东西拆开,还原出它的逻辑。拆的东西主要有三类:

第一类

Java / Kotlin 代码

打包成 DEX 字节码。用 jadx 能看到接近源代码的 Java,用 apktool 能看到 smali 汇编。

第二类

Native 库

用 C/C++ 编译的 .so 文件。签名、加密等关键算法常藏在这里,要用 IDA 或 Ghidra 看汇编。

第三类

运行时行为

代码可以加密、可以混淆,但运行时必须解密。用 Frida 在内存里拦函数,看真实入参和返回值。

三者是递进关系:先静态看结构,再动态看行为,最后用真实流量对拍验证。只做其中一步,很容易得出错误结论。

0.8问题八:我需要先会什么

不需要先成为程序员,但下面这些东西会反复用到,建议边学边补:

技能要到什么程度大概需要多久
命令行会 cd、ls、运行程序、看输出半天
十六进制知道 0x41 是 A,能看懂 00 01 FF一小时
JSON能读懂嵌套的花括号和数组一小时
Python 基础会写循环、函数、发 HTTP 请求三到五天
Java 阅读能看懂类、方法、if/for,不用会写两到三天
HTTP 常识知道 GET/POST、状态码、Header两小时
给零基础的建议 不要等"学完 Python 再开始"。先跟着第 1 到第 10 章把概念看懂,遇到代码再回头补。看懂和会写是两件事,前期只需要看懂。

0.9接下来的学习顺序

从零到实战的五段学习路径 ① 地基 分层 编码 第 2–3 章 ② 密码学 加密 签名 第 4–5 章 ③ 协议 TCP TLS 第 6–11 章 ④ 移动端 逆向 指纹 第 12–16 章 ⑤ 实战 17–21 每一段都建立在前一段之上:没有分层概念,看不懂 TLS;没有密码学,看不懂签名 急的话可以跳到实战章节,但遇到不懂的词一定要回到对应章节补
图 0-5 五段式学习路径:地基、密码学、协议、移动端、实战
动手练(不需要任何工具)
  • 打开手机上的任意一个 App,点一个按钮,猜它背后发了几个请求。
  • 在浏览器地址栏输入一个网址,观察它最后变成了什么(有没有 www、有没有路径)。
  • 用一句话向家人解释"Token 是什么",不许用"令牌"这个词。
自测:域名、IP、端口,分别解决什么问题?
域名解决"好记",IP 解决"找到哪台机器",端口解决"找到机器上的哪个程序"。DNS 负责把域名翻译成 IP。三者缺一不可。
自测:加密和签名,哪个负责防偷看?
加密负责防偷看(机密性),签名负责防篡改和防冒充(完整性、身份认证)。它们解决的是不同问题,不能互相替代。
自测:为什么抓到 Token 不等于拿到密码?
Token 是登录成功后服务器发的临时凭证,有有效期、可被吊销,通常也不包含密码。拿到它能在有效期内冒充你,但改不了密码,也拿不到你的原始密码。所以风控会把 Token 泄露当成重点风险。
自测:抓包抓到一堆乱码,最可能是什么原因?
流量被 TLS 加密了,而你的抓包工具没有被客户端信任,拿不到会话密钥。下一步要么让客户端信任代理证书,要么面对证书固定(pinning)的问题。不是"抓包工具坏了"。
客户端Client发起请求的一方,通常是你手机上的 App 或浏览器。
服务器Server接收请求、处理后返回结果的一方。
请求 / 响应Request / Response一问一答,构成一次完整的网络交互。
报文Message网络上传输的一份完整数据,包含头和体。
Token令牌登录后发放的临时凭证,替代密码用于后续请求。
抓包Packet Capture把网络上来回的数据原样记录下来。
逆向Reverse Engineering从打包好的程序还原出它的逻辑。
哈希Hash把任意内容压成固定长度的指纹,不可逆。
你现在应该能回答 一次登录经过哪四步?域名/IP/端口各管什么?加密和签名分别防什么?Token 是什么?抓包为什么会出现乱码?如果这五个问题你能用自己的话讲出来,后面的章节就不会卡住。
第 2 章 · 网络基础

网络分层:数据是怎么一层层穿上衣服的

你在抓包软件里点开一个包,会看到 Ethernet、IP、TCP、HTTP 一层套一层,像俄罗斯套娃。这一页用寄快递要套好几层包装讲清为什么要分层、每层加了什么,以及为什么改一个 HTTP 头根本不影响数据怎么走路。

分层思想OSI 七层封装 / 解封装 MTU 与分片帧头 / IP 头抓包
先记住这一句 分层不是把网络切成几段互不相干的东西,而是把"发一句话"拆成几件互不干扰的事:每一层只加自己那一小段头,只关心自己那一小段地址。
人话版 你要寄的东西被套了四层包装,每层只写自己关心的信息;收件方从外往里一层层拆。网络就是这么干的。

2.1先看一次真实的寄快递

你给朋友寄一部手机。你不会把手机裸着塞进邮筒,而是分四步:

  • 先用泡沫裹一层,防摔;
  • 装进纸箱,保证装得下、不散架;
  • 箱子上贴一张面单,写清收件人地址;
  • 交给快递员,由他决定这一站送到哪个分拣中心。

这四步各管一件事。泡沫管"别摔坏",纸箱管"装得下",面单管"送到哪",快递员管"这一站交给谁"。

关键在这里:分拣中心只看面单,不会拆开箱子研究你寄的是什么。它也不需要知道。

网络用的是同一套思路。你要发的一句话,也会被裹上好几层"包装",每一层只写自己关心的信息。这套做法叫分层(Layering)。

快递四层包装与网络四层一一对应 快递的四层包装 对应的网络层 ① 你要寄的手机 物品本身 应用层数据 HTTP 请求内容 ② 防震泡沫 防止摔坏、散架 传输层 TCP 加端口,保证不丢不乱 ③ 纸箱 装下所有东西 网络层 IP 加 IP 地址,负责找路 ④ 快递面单 写清收件地址 链路层帧头 写给下一站看 每一层只关心自己那一段,不关心别人装了什么
图 2-1 快递的四层包装,和网络的四层头一一对应
术语对照 每一层加的那段"包装信息"叫头(Header);层与层之间传递的整块数据,统称协议数据单元(Protocol Data Unit,PDU),不同层还有各自的小名。

2.2七层和四层:两套说法,说的是同一件事

教科书上最常见的是 OSI(Open Systems Interconnection,开放系统互连)七层模型,由国际标准化组织在 1980 年代提出。

但真实互联网跑的不是 OSI,而是 TCP/IP 四层模型。OSI 更像一张教学挂图:分得细,方便描述问题;TCP/IP 是施工图:能落地,大家都在用。

OSI 七层TCP/IP 四层这一层管什么常见协议
应用层应用层业务内容HTTP、DNS、SMTP
表示层应用层编码、压缩、格式TLS 常被放在这里讨论
会话层应用层建立和维持对话登录会话、Cookie
传输层传输层端到端可靠传输TCP、UDP
网络层网际层寻址与路由IP、ICMP
数据链路层网络接口层同一段链路上传输以太网、Wi-Fi
物理层网络接口层电信号、光、无线电网线、光纤

注意两个细节:

  • 表示层和会话层在 TCP/IP 里被并进了应用层,所以"七层"和"四层"不矛盾,只是切法不同。
  • 很多教材说"五层",是把网络接口层再拆成数据链路层和物理层。三种说法都对,看你跟谁聊。

从下往上的记忆口诀:物、数、网、传、会、表、应。背下来,看任何协议文档都能对上号。

2.3每层到底在干什么

从上往下走一遍,每层一句话说清职责,再配一个真实例子。

  • 应用层:你和程序打交道的地方。浏览器发出 GET /index.html HTTP/1.1,这就是应用层的内容。
  • 传输层:负责"这一头到那一头",用端口号区分程序。TCP 还会编号、确认、重传。头最少 20 字节。
  • 网络层:负责"从这台机器到那台机器",加源 IP 和目的 IP。头也是 20 字节起。
  • 数据链路层:负责"这一跳到下一跳",加以太网帧头,里面是目的 MAC、源 MAC 和类型,共 14 字节。
  • 物理层:把 0 和 1 变成电压、光脉冲或无线电波。
一句话记法 应用层管"说什么",传输层管"说全没说全",网络层管"送到哪台机器",链路层管"下一站交给谁",物理层管"怎么变成信号"。

2.4封装:一层层穿衣服

数据从应用层往下走的时候,每经过一层,就在前面加一个头。这个动作叫封装(Encapsulation)。

反过来,服务器收到电信号,从物理层往上走,每层检查并撕掉属于自己的头,把剩下的交给上一层。这叫解封装(Decapsulation)。

数据从应用层到物理层逐层加头,服务器再逐层拆头 发送方:从应用层往下,每层加一个头 应用层 · HTTP 请求内容 DATA 传输层 · 加 TCP 头(端口、序号、确认号) TCP DATA 网络层 · 加 IP 头(源 IP、目的 IP、TTL) IP TCP DATA 链路层 · 加以太网帧头(目的 MAC、源 MAC、类型) ETH IP TCP DATA 物理层 · 变成 0 和 1,走网线或无线电 0101 0110 0011 ... 逐层加头 接收方:从物理层往上,一层层拆掉属于自己的头 0101 0110 0011 ... 网卡收到电信号 IP TCP DATA 撕掉帧头,剩下 IP 包 TCP DATA 撕掉 IP 头,剩下 TCP 段 DATA 撕掉 TCP 头,剩下应用数据 逐层拆头 ④ 交给应用:GET /api/user HTTP/1.1
图 2-2 封装是逐层加头,解封装是逐层撕头,两边的顺序正好相反

每一层的数据单位有专门的名字,看文档时经常遇到:

层数据单位典型头长度
应用层报文(Message)不固定
传输层段(Segment)/ 数据报(Datagram)20 字节起
网络层包(Packet)20 字节起
链路层帧(Frame)14 字节 + 4 字节帧尾
物理层比特(Bit)没有头

为什么要这么麻烦?因为每一层可以独立替换。Wi-Fi 换成 4G,只是链路层和物理层变了,上面的 HTTP 一行都不用改。

2.5四个头的真实样子

光说概念容易飘,直接看字节。下面是一个 HTTPS 请求在网线上被拆开后的样子,十六进制,空格只是为了好读。

以太网帧头(14 字节)
3c 22 fb 11 08 00   目的 MAC
a4 5e 60 9c 31 7d   源 MAC
08 00               类型 = IPv4

IP 头(20 字节,节选)
45 00 05 dc         版本 4、头长 5、总长 1500
40 06 ...           TTL 64、协议 6 表示 TCP
c0 a8 01 0a         源 IP 192.168.1.10

TCP 头(20 字节,节选)
1f 90 01 bb         源端口 8080、目的端口 443
00 00 00 00         序号
50 18 ...           头长 5、标志位 PSH + ACK

HTTP 请求(应用层,纯文本)
GET /api/user HTTP/1.1
Host: shop.example.com
User-Agent: MyApp/8.2.1

逐条解释,这些字段以后会反复出现:

  • 帧头最后的 08 00 是类型字段,告诉接收方"里面装的是 IPv4"。如果是 86 dd,就是 IPv6。
  • IP 头的第一个字节 45,高 4 位是版本号 4,低 4 位是头长度 5,单位是 4 字节,所以 5 乘 4 等于 20 字节。
  • IP 头的 40 是 TTL(Time To Live,生存时间),每过一个路由器减 1,减到 0 就丢弃,防止包在网络里永远转圈。
  • TCP 端口是 16 位,范围 0 到 65535。443 是 HTTPS 的默认端口,80 是 HTTP 的默认端口。
  • HTTP 头是纯文本,一行一个字段。Host 告诉服务器你要访问哪个站点,一台服务器可以靠它区分很多网站。
注意 上面的数字是为了讲结构,不是某一次真实抓包。你自己抓到的包,MAC、IP、端口、序号都会不一样,别拿这些值去硬套。

2.6MTU 1500:超重必须拆包

网线不是想传多大就传多大。以太网一帧最多扛 1500 字节的 IP 数据,这个上限叫 MTU(Maximum Transmission Unit,最大传输单元)。

为什么是 1500?这是早期以太网定下的工程折中:太小,头部开销占比高;太大,一个包出错要重传的代价高,而且别的设备可能收不下。这个数字一直沿用至今。

超过 1500 怎么办?IP 层会把大包切成几片,每片都带自己的 IP 头,头里记着"我是第几片、从原始数据的第几个字节开始、后面还有没有"。这个过程叫分片(Fragmentation)。

4000 字节的 IP 包被切成三个分片 一个 4000 字节的 IP 包,遇到 MTU 1500 20 字节 IP 头 + 3980 字节数据 超过 1500,必须切 1480 字节 分片 1 · 偏移 0 · 后面还有(MF=1) 1480 字节 分片 2 · 偏移 185 · 后面还有(MF=1) 1020 字节 分片 3 · 偏移 370 · 最后一片(MF=0) 每片都有自己的 IP 头;接收方按偏移拼回去,丢任何一片整包作废 所以 TCP 会先探测路径 MTU,尽量让每个包都不超重
图 2-3 MTU 就是每件包裹的重量上限,超了必须拆成几件,编号不能丢

用快递打比方:一件超重货物必须拆成三个箱子,每个箱子贴同一张运单的编号,收件方按编号拼回去。少一个箱子,整件货就不完整。

分片的代价很实在:

  • 丢任何一片,整个原始包都得重传。IP 层自己不管重传,靠上层 TCP 补。
  • 每片都要重复 20 字节 IP 头,开销变大。
  • 中间设备重组策略不一致时会出现安全漏洞:攻击者用重叠分片,让防火墙看到无害内容,让目标主机拼出恶意内容。

所以现代做法是尽量避免分片:TCP 握手时会根据路径 MTU 算出 MSS(Maximum Segment Size,最大报文段长度),让每个 TCP 段正好装进一个帧。IPv6 更进一步,路由器不再分片,只允许发送方自己分。

2.7抓包软件里看到的是什么

用 Wireshark 抓一个 HTTPS 请求,点开其中一个包,会看到这样的层次,从上到下:

Frame 1: 1514 bytes on wire      物理层信息:帧长、到达时间
Ethernet II: 源 MAC 到 目的 MAC    链路层:类型 IPv4
Internet Protocol Version 4      网络层:源 IP 到 目的 IP,TTL 64
Transmission Control Protocol    传输层:51234 到 443,Seq / Ack
Transport Layer Security         表示层 / 会话层:TLS 记录
Hypertext Transfer Protocol      应用层:GET /api/user

注意顺序:抓包工具把"最外层"放在最上面,正好和发送时的封装顺序相反。你点开三角,就是在做一次解封装。

如果某一层显示"加密的应用数据",说明 TLS 把上面几层的内容包住了。你只能看到 TLS 这一层,再往上要拿到密钥。

2.8常见误区:分层不是物理隔离

很多人以为七层是七个独立的程序,甚至以为是七段不同的网线。其实一台手机里,从浏览器到网卡驱动,所有层都在同一台设备上完成,只是代码分工不同。

分层是逻辑约定,核心只有一句话:每一层只使用下一层提供的服务,不关心下一层怎么实现。

  • 所以底层换掉,上层不用改。Wi-Fi 换 4G、IPv4 换 IPv6,HTTP 依然照跑。
  • 所以同一层可以有很多实现。TCP 在 Windows、Linux、手机里写法不同,但对上层的行为一致。

也别把分层当法律。现实里有大量"不守规矩"的用法:

  • 隧道:把整个 IP 包装进另一个 IP 包里传,VPN 常这么做,等于套了两层网络层。
  • QUIC:把可靠传输做在 UDP 之上,绕开了传统 TCP。
  • TLS:横跨表示层和会话层,还顺手做了身份认证。

分层是描述工具,不是限制。判断一个协议属于哪层,看它解决的是哪类问题,而不是看它叫什么名字。

2.9回到那个问题:改 HTTP 头为什么不影响路由

现在可以回答了。路由器转发一个包时,只看 IP 头里的目的 IP 和路由表,然后决定从哪个口发出去。

HTTP 头在 IP 层眼里只是"数据",被完整地包在 IP 包的载荷里。路由器不拆、不解析、也不关心。

快递分拣中心只看面单上的城市,不会拆开箱子读你写的信。同一个道理。

但这句话有个重要边界:不影响的只是"IP 路由"。下面这些设备会故意往上拆一层看。

设备看到哪一层会受什么影响
普通路由器IP 头只看目的 IP
NAT 网关IP 头 + 端口看端口做地址转换
HTTP 代理 / WAFHTTP 头看 Host、User-Agent、Cookie
CDNHTTP 头按 Host 决定回源到哪台服务器

所以"改 User-Agent 能骗过某些检测"和"改 User-Agent 能改变路由"是两件事。前者是应用层的事,后者根本不可能,除非中间有应用层设备插手。

顺带说清一个常见混淆:CDN 按 Host 头把请求分到不同源站,这叫应用层负载均衡,不是 IP 路由。它的决策依据是 HTTP 头,不是路由表。

自测:数据从应用到网线,依次加了哪些头?
应用数据先加 TCP 头变成段,再加 IP 头变成包,再加以太网帧头变成帧,最后变成比特。顺序是应用、传输、网络、链路、物理,接收方反过来撕。
自测:MTU 1500 是什么,超了怎么办?
它是以太网一帧能承载的 IP 数据上限,1500 字节。超了由 IP 层分片,每片带自己的 IP 头并记录偏移。丢一片整包重传,所以现代协议尽量用 MSS 探测避免分片。
自测:为什么路由器不看你 HTTP 头?
因为路由决策发生在网络层,只看 IP 头里的目的 IP 和路由表。HTTP 头属于应用层,对 IP 层来说只是载荷内容。只有代理、WAF、CDN 这类应用层设备才会拆开看。
自测:OSI 七层和 TCP/IP 四层是什么关系?
OSI 是教学参考模型,分得细;TCP/IP 是实际在用的模型,把表示层和会话层并进应用层,把链路层和物理层并成网络接口层。两者描述的是同一套东西,只是切法不同。
动手练
  • 打开手机上的抓包工具(或电脑的 Wireshark),随便抓一个网页请求,数一数它有几层,从外到内念一遍每层的名字。
  • 在命令行执行 ping -l 2000 192.168.1.1,看提示是否要求分片;再改成 ping -l 1400 对比。
  • 拿一张纸,自己画一遍封装和解封装两个方向,标出每层加了几个字节。
分层Layering把通信拆成多层,每层只解决一类问题。
封装Encapsulation往下走时每层加一个头。
解封装Decapsulation往上走时每层撕掉自己的头。
协议数据单元PDU每层传输的数据块的统称。
MTUMaximum Transmission Unit一帧能承载的最大 IP 数据量,以太网通常 1500。
分片Fragmentation包太大时切成多片,每片带偏移信息。
MAC 地址MAC Address网卡的硬件地址,链路层用来标识下一跳。
帧Frame链路层的数据单位,等于帧头加 IP 包加帧尾。
你现在应该能回答 数据从上到下加了哪几个头?MTU 超了会怎样?为什么改 HTTP 头不影响 IP 路由,却可能影响 CDN 回源?如果这三问答得出来,后面看 TLS 握手和抓包就不会迷路。
第 3 章 · 数据表示

字节、编码与字节序:所有协议的底层语言

抓包看到一串 48 54 54 50,逆向看到 0x12345678,网页里看到 %E4%B8%AD。这些不是天书,是同一种东西的不同写法。这一页把"0 和 1 怎么变成人能读的字"讲透,让你以后看到字节先能判断它是什么,而不是直接喊加密。

位与字节十六进制大端 / 小端 补码varintBase64 / UTF-8
先记住这一句 所有协议最终都是一串比特。你看到的所有"乱码""密文""编码",先要判断它是哪一种表示法,再谈它是不是加密。
人话版 这一页就教你一件事:看到一串十六进制,先别喊加密,先按字节拆开看它到底是什么。

3.1为什么安全人要懂字节

你在抓包工具里看到的是十六进制,在逆向工具里看到的是汇编和内存,在网页源码里看到的是百分号编码。它们说的是同一件事:一段字节。

不懂字节,会出现三种典型误判:

  • 把 Base64 当成加密,以为解不开,直接放弃。
  • 把 UTF-8 的中文当成乱码,以为数据坏了。
  • 把大端当小端读,长度算错一位,整个包解析全错。

这一页不要求你会写代码,只要求你能"看懂并手算"。手算过一次,比看十遍定义都管用。

3.2位、字节、十六进制

计算机最小的单位是位(Bit),只有 0 和 1 两种状态,像一盏灯的开关。

8 个位组成一个字节(Byte)。一个字节能表示 0 到 255,共 256 种值。

写二进制太长,于是用十六进制(Hexadecimal):每 4 位合成一个十六进制数字,一个字节正好写成两位。比如 0100 0001 写成 41。

十六进制用 0 到 9 和 A 到 F 共 16 个符号。A 代表 10,F 代表 15。前面加 0x 表示"这是十六进制"。

一个字节的八位权重,以及二进制、十进制、十六进制、ASCII 四种写法 一个字节 = 8 个二进制位,每一位有固定权重 128 0 64 1 32 0 16 0 8 0 4 0 2 0 1 1 把这 8 位看成 8 个开关 只有 64 和 1 这两位是开的 64 + 1 = 65 0100 0001 二进制 65 十进制 0x41 十六进制 A ASCII 字符 同一个字节,四种写法,意思完全一样
图 3-1 0x41 就是 65,就是二进制 0100 0001,就是字符 A

所以 0x41 等于十进制 65,等于二进制 0100 0001,等于 ASCII 字符 A。这四个写法完全等价。

手算方法:把十六进制每一位乘 16 的幂。

0x41  = 4 乘 16 + 1  = 65
0xFF  = 15 乘 16 + 15 = 255
0x100 = 1 乘 256      = 256

反过来,十进制转十六进制就是不断除以 16 取余数。初学阶段记住几个锚点就够:

  • 0x20 是空格,0x30 是字符 0,0x39 是字符 9。
  • 0x41 是大写 A,0x61 是小写 a,两者差 32。
  • 0x7F 是 ASCII 的上限,超过它就是非 ASCII 内容。

3.3大端与小端:一个数字,两种排法

一个整数如果超过 1 个字节,就要决定"高位字节先放还是低位字节先放"。这就是字节序(Endianness)。

以 0x12345678 为例,它占 4 个字节:12、34、56、78。

  • 大端序(Big-endian):高位在前,内存里是 12 34 56 78,符合人的阅读习惯。
  • 小端序(Little-endian):低位在前,内存里是 78 56 34 12。x86 和 ARM 默认用这种。
同一个整数在大端和小端内存中的字节排列 同一个数 0x12345678,占 4 个字节,从地址 1000 开始 大端 高位在前 12 1000 34 1001 56 1002 78 1003 小端 低位在前 78 1000 56 1001 34 1002 12 1003 两种排法表示的数值完全一样,只是字节顺序不同 网络传输统一用大端,叫网络字节序
图 3-2 同一个数,大端从高位写起,小端从低位写起,读错就是灾难

网络传输统一用大端,这叫网络字节序(Network Byte Order)。所以写代码时经常要调用"主机序转网络序"的函数,就是在做这个转换。

为什么这事和安全有关?因为协议解析错误经常出在这里。你把小端的长度字段按大端读,可能把 1000 读成 3891200,然后按这个长度去读缓冲区,就是经典的越界漏洞。

判断方法:看到 00 00 03 e8,按大端读是 1000;看到 e8 03 00 00,按小端读也是 1000。真实协议里长度字段通常是 2 或 4 字节,先确认字节序,再算长度。

3.4整数编码:无符号、有符号、补码

同样 1 个字节,可以表示 0 到 255(无符号,Unsigned),也可以表示 -128 到 127(有符号,Signed)。区别在于最高位是不是被当成符号位。

有符号数用补码(Two's Complement)表示。规则很简单:正数不变,负数等于取反再加 1。

 5 = 0000 0101
-5 = 1111 1010 + 1 = 1111 1011 = 0xFB

用温度计理解最直观:0 度在中间,往上是正数,往下是负数。走到最底下再往下走一格,就绕回最上面,因为一个字节只有 256 个格子,溢出会"绕圈"。

这个"绕圈"是很多漏洞的来源:

  • 一个无符号长度减去一个更大的数,不会变成负数,而是变成一个巨大的正数。
  • 用这个巨大数字去分配缓冲区,要么分配失败,要么后续拷贝直接越界。

反过来,有符号数溢出在 C 语言里是未定义行为,编译器可以做任何事。所以安全代码里做长度计算,一律用无符号并检查上界。

3.5变长整数 varint:省空间的艺术

固定 4 字节存一个小数字很浪费。比如用户 ID 是 300,也要占 4 字节。Protobuf(Protocol Buffers)用变长整数(varint)解决这个问题。

规则:每个字节只用低 7 位存数据,最高位是"还有没有下一个字节"的标志。1 表示后面还有,0 表示这是最后一个。

300 = 0000 0001 0010 1100
按 7 位一组,从低位开始切:010 1100、000 0010
每组补上继续位:1010 1100、0000 0010
写成十六进制:ac 02

所以 300 只占 2 字节,而不是 4 字节。数字越小占得越少,超过 127 才需要第二个字节。

代价是解析要循环:一直读到最高位为 0 为止。这也给了攻击者空间,构造一个超长的 varint,让解析器一直读下去,就是拒绝服务。所以实现里必须限制最大字节数,常见上限是 10 字节。

3.6Base64:3 个字节变 4 个字符

有些通道只能传文本,不能传二进制,比如老式邮件、URL 参数、JSON 字段。Base64 把任意字节换成 64 个安全字符:A 到 Z、a 到 z、0 到 9,再加 + 和 /。

原理只有一步:把 3 个字节共 24 位,重新切成 4 组,每组 6 位。6 位能表示 0 到 63,正好对应 64 个字符。

Base64 把三个字节重新分成四组六位 3 个字节拆成 4 组 6 位,再查表变成 4 个字符 01001101 M 01100001 a 01101110 n 重新分组 010011 T 010110 W 000101 F 101110 u 每 6 位取值 0 到 63,查表换成字符;3 字节变 4 字符,体积涨约 33% Base64 只是换了一种写法,任何人都能还原,不是加密
图 3-3 Base64 的核心就是"24 位重新切成 4 份,每份 6 位"

以 "Man" 为例,完整过程是:

M = 01001101    a = 01100001    n = 01101110
连起来:010011 010110 000101 101110
查表:   T      W      F      u
结果:TWFu

3 字节变 4 字符,体积涨约 33%。如果原文不是 3 的倍数,用 = 补齐,所以你会看到结尾有 = 或 ==。

重点:Base64 不是加密。它没有密钥,任何人拿到都能还原。它只是换了一种写法,作用是让二进制能安全穿过文本通道。

红线 把密码、Token 做一次 Base64 就以为安全了,是极常见的错误。Base64 只是换了个信封,谁都能拆开。

3.7UTF-8:一个汉字通常 3 字节

计算机只认字节,要把"字"变成字节,需要字符编码(Character Encoding)。

UTF-8 是目前最通用的编码。它的规则是变长:

字符类型字节数例子
英文、数字、符号1A 等于 41
拉丁扩展、希腊文2é 等于 c3 a9
中文、日文、韩文3中 等于 e4 b8 ad
部分表情、生僻字4少数符号

所以"中"这个字在 UTF-8 里是 e4 b8 ad 三个字节。你在抓包里看到这三个字节,不是乱码,是正常的中文。

判断技巧:UTF-8 的多字节序列,第一个字节高位有几个 1,就表示总共几个字节。e4 是 1110 0100,开头三个 1,所以是三字节字符。后面两个字节都以 10 开头,是续字节。

GBK 是另一套中文编码,一个汉字 2 字节。同一段中文用 UTF-8 和 GBK 编码,字节完全不同。看到"锟斤拷"这种乱码,通常是编码用错了,不是数据损坏。

3.8URL 编码:%E4%B8%AD 是怎么来的

URL 里有一批字符不能直接出现,比如空格、中文、&、=、#。解决办法是把每个字节写成百分号加两位十六进制。

"中"的 UTF-8 是 e4 b8 ad,所以 URL 编码后就是 %E4%B8%AD。大小写都合法,习惯上写大写。

常见对照:

  • 空格变成 %20,在查询参数里也可能写成 +。
  • / 变成 %2F,. 变成 %2E。
  • 一个中文变成三个 %XX。

这里有个经典安全问题:如果服务器只解码一次,而中间某层解码了两次,攻击者就能用 %252e%252e%252f 绕过路径检查,解码两次后变成 ../。这叫双重编码(Double Encoding)。防御方法是对输入只解码一次,然后做规范化再校验。

3.9文件头:看前几个字节就认出文件

很多文件格式在开头放了固定字节,叫魔数(Magic Number)。改扩展名骗不了它。

格式开头字节说明
PNG89 50 4E 47 0D 0A 1A 0A图片
ZIP50 4B 03 04压缩包,APK 也是 ZIP
PDF25 50 44 46文档,即 %PDF
ELF7F 45 4C 46Linux 可执行文件
DEX64 65 78 0AAndroid 字节码

用法:拿到一个可疑文件,先看前 16 字节,比看扩展名可靠得多。攻击者也常用"改文件头"来绕过检测,所以只看魔数也不够,要结合结构解析。

3.10工具:两条命令看字节

Linux 和 macOS 自带 xxd 和 hexdump,Windows 可以用 Git Bash,或者 PowerShell 的 Format-Hex。

# 看前 64 字节:左边偏移,中间十六进制,右边字符
xxd -l 64 sample.bin

# 经典十六进制视图,-C 表示同时显示字符
hexdump -C sample.bin | head

# 看一个汉字在 UTF-8 下的字节
printf '中' | xxd

输出长这样:

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR

左边 00000000 是偏移,也就是第几个字节。中间每 2 个十六进制数字是 1 个字节。右边是能打印的字符,不能打印的显示为点。

3.11误区:看到十六进制就以为加密

这是初学者最常见的误判。判断顺序应该是:

  1. 先看是不是文本编码。能按 UTF-8 正常读出中文英文,就是编码问题。
  2. 再看是不是 Base64。全是 A 到 Z、a 到 z、0 到 9、加号、斜杠,长度是 4 的倍数,结尾可能有等号,大概率是 Base64。
  3. 再看是不是压缩。开头 1F 8B 是 gzip,50 4B 是 zip。
  4. 最后才考虑加密。加密后的字节应该接近随机:每个值出现频率差不多,没有明显的重复结构。

一个实用判据:把字节画成直方图。文本和 Base64 的分布很不均匀,压缩和加密的分布接近均匀。这就是"熵"的直观含义,下一章会讲。

另一条:如果一段数据你能用 Base64 解开,得到可读内容,那它从一开始就不是加密。

自测:0x41 是什么?三种写法怎么换算?
0x41 是十六进制 41,等于十进制 65,等于二进制 0100 0001,对应 ASCII 字符 A。一个字节写两位十六进制,是因为 4 位二进制正好对应 1 位十六进制。
自测:0x12345678 在小端机器内存里怎么排?
小端是低位在前,所以是 78 56 34 12。大端则是 12 34 56 78。网络传输统一用大端,本机内存多半是小端,所以代码里要做转换。
自测:Base64 是加密吗?为什么?
不是。Base64 只是把 3 个字节重新分成 4 组 6 位,再查表换成字符,没有任何密钥。任何人拿到都能一步还原,它解决的是"二进制怎么穿过文本通道",不是"怎么不让别人看"。
自测:抓包里看到 e4 b8 ad,该怎么判断?
先看第一个字节 e4 是 1110 0100,开头三个 1,说明这是一个三字节 UTF-8 字符;后两个字节都以 10 开头,符合续字节规则。查表可知是"中"。不是乱码,也不是加密。
动手练
  • 找一个 PNG 图片,用 xxd -l 16 看开头,对上表里的 89 50 4E 47。
  • 把"中"字手写成 UTF-8 三个字节,再写成 URL 编码,和 %E4%B8%AD 对照。
  • 把数字 300 按 7 位一组手写成 varint,看是不是 ac 02。
  • 把字符串 "Man" 手算一遍 Base64,看是不是 TWFu。
位Bit最小单位,只有 0 和 1。
字节Byte8 个位,取值 0 到 255。
十六进制Hexadecimal每 4 位二进制写成一个字符,前缀 0x。
字节序Endianness多字节整数在内存里的排列顺序。
补码Two's Complement用取反加一表示负数的方法。
varintVariable Integer每字节 7 位数据加 1 位继续标志的变长整数。
Base64把 3 字节换成 4 个可打印字符的编码,不是加密。
魔数Magic Number文件开头用来标识格式的固定字节。
你现在应该能回答 0x41 为什么等于 A?0x12345678 在小端里怎么排?Base64 为什么不安全?看到 e4 b8 ad 该想到什么?如果这四问答得出来,后面解析任何协议字段都不会被"乱码"吓住。
第 4 章 · 密码学

密码学基础:四把锁和一台榨汁机

加密、哈希、签名、证书,这四个词经常被混着用。这一页用四把锁和一台榨汁机,把它们各自解决什么问题、不能解决什么问题分清楚。看完你会明白:为什么 TLS 不直接用非对称加密传数据,为什么同一个 IV 用两次会出大事。

机密性完整性对称 / 非对称 AES 模式哈希 / HMAC随机数
先记住这一句 密码学不是"把数据变乱",而是用数学解决四个不同的问题:别人看不懂、别人改不了、确认对面是谁、事后赖不掉。四件事各有各的工具,不能互相替代。
人话版 锁管"不让看",封条管"不许改",身份证管"你是谁",签字管"别赖账"。四样东西,四种用途,别拿锁去当身份证。

4.1先分清四个需求

在选工具之前,先问自己要解决哪个问题。这四个词以后会反复出现,必须一次分清。

密码学的四个需求与对应工具 机密性 别人看不懂 工具:加密 完整性 别人改不了 工具:哈希 / HMAC 身份认证 确认对面是谁 工具:证书 / 签名 不可否认 事后赖不掉 工具:数字签名 对应四类攻击:偷看、篡改、冒充、抵赖
图 4-1 四个需求对应四种工具,混用就会留下缺口

逐个说清:

  • 机密性(Confidentiality):内容不能让中间人看懂。工具是加密。
  • 完整性(Integrity):内容不能被悄悄改动。工具是哈希、消息认证码。
  • 认证(Authentication):确认对方真的是它。工具是证书、签名。
  • 不可否认(Non-repudiation):事后不能否认自己做过。工具是数字签名。

现实系统通常四个都要。TLS 就是把这四件事打包:用非对称加密协商密钥并认证服务器,用对称加密传数据,用认证标签保证没被改。

4.2对称加密:一把钥匙开一把锁

对称加密(Symmetric Encryption)只有一把钥匙,加密和解密用同一个。像家里的门锁,同一把钥匙进和出。

优点是快。AES(Advanced Encryption Standard,高级加密标准)在普通笔记本上能跑到每秒几个 GB,加密一整块硬盘也不费劲。

缺点是钥匙怎么给对方。你们没见过面,怎么安全地把钥匙送过去?这就是后面非对称加密要解决的问题。

AES 是分组密码(Block Cipher):把数据切成固定大小的块,每块 16 字节,逐块加密。密钥长度有 128、192、256 位三种,目前常用 128 或 256。

但分组密码只解决了"一块怎么加密",还需要一个工作模式(Mode of Operation)来决定"多块怎么串起来"。

4.3四种常见模式:ECB、CBC、CTR、GCM

模式怎么工作优点问题
ECB每块独立加密最简单,可并行相同明文块产生相同密文,泄露图案
CBC前一块密文异或进当前块隐藏图案需要 IV,不能并行加密
CTR计数器生成密钥流再异或可并行,可随机访问计数器绝不能重复
GCMCTR 加认证标签同时加密和防篡改nonce 重复会泄露密钥流

重点说 ECB。它把每一块单独加密,完全不管上下文。结果就是:明文里相同的块,密文里也相同。

最著名的演示是"ECB 企鹅":一张图片用 ECB 加密后,企鹅的轮廓依然清晰可见,因为大面积的同色区域产生了相同的密文块。

ECB 保留图案,CBC 与 CTR 打散图案 原始图案 ECB 加密后 CBC / CTR 加密后 看得出形状 形状完全一样 只剩色块 ECB 对每个块独立加密,相同明文块必然得到相同密文块 所以 ECB 不该用于任何真实数据,只适合教学和单个随机块
图 4-2 ECB 就像给每个格子盖同一个章,图案原样保留;CBC 和 CTR 每格都不一样

所以 ECB 不该用于任何真实数据。它只适合加密单个随机块,或者当教学例子。

CBC 的做法是:先让明文块和"上一块密文"异或,再加密。第一块没有上一块,就用一个初始向量(Initialization Vector,IV)顶上。这样相同的明文块,位置不同,密文就不同。

CTR 换了个思路:不加密数据,而是用"密钥加计数器"生成一串密钥流,再和数据异或。加密解密是同一个操作,可以并行,也可以随机读某一块。

GCM 在 CTR 基础上加了一个认证标签(Authentication Tag),解密时先校验标签,不对就拒绝。这类"加密加认证"一体的模式叫 AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)。

4.4IV 和 nonce:一次性,绝不能重复

IV 和 nonce(Number used once,一次性数字)的作用是让同样的明文每次加密出来不一样。

为什么必须不一样?以 CTR 和 GCM 为例:密钥流等于加密后的计数器。如果同一个密钥配同一个计数器,生成的密钥流就完全一样。

后果很严重:

  • 两段密文异或,密钥流会互相抵消,直接得到两段明文的异或。英文文本的异或很容易被统计分析还原。
  • GCM 下重复 nonce 更糟,攻击者可以伪造认证标签,等于完整性保护失效。

规则很简单:

  • CBC 的 IV 要随机,且不可预测;不需要保密,但要每次不同。
  • CTR 和 GCM 的 nonce 绝对不能在同一密钥下重复。常用做法是随机 96 位,或者用递增计数器。

现实中出过很多事故,根源都是"随机数生成器坏了"或"计数器重启归零"。这类 bug 从外部看不出来,但加密等于没做。

4.5非对称加密:两把钥匙

非对称加密(Asymmetric Encryption)有一对钥匙:公钥(Public Key)和私钥(Private Key)。

公钥可以公开给任何人,私钥只有自己知道。用公钥加密,只有私钥能解;用私钥签名,任何人都能用公钥验证。

像邮箱:投信口是公钥,谁都能往里塞信;取信口的钥匙是私钥,只有你能打开。

两种主流算法:

  • RSA:安全性建立在大整数分解难题上。把两个大素数乘起来容易,反过来分解极难。
  • ECC(Elliptic Curve Cryptography,椭圆曲线密码):建立在椭圆曲线离散对数难题上。同样安全强度下,密钥短得多。
安全强度RSA 密钥长度ECC 密钥长度
约 112 位2048 位224 位
约 128 位3072 位256 位
约 192 位7680 位384 位

所以你会看到"RSA 2048 约等于 ECC 256"。密钥短的好处是握手快、证书小、手机省电。

非对称加密的代价是慢。RSA 加密比 AES 慢几个数量级,所以它不用来加密大量数据。

4.6混合加密:真正干活的是对称密钥

现实中的做法是混合加密(Hybrid Encryption):用非对称加密只传一把临时对称密钥,之后所有数据都用对称加密。

TLS 风格的混合加密流程 你的手机 客户端 服务器 持有私钥 ① 请求建立安全连接 ② 出示证书,证书里有公钥 ③ 客户端验证证书,本地生成随机会话密钥 K ④ 用公钥加密 K,发给服务器 ⑤ 之后双方用 K 做 AES-GCM 加密,快 非对称只用来传几十字节的钥匙,真正干活的是对称加密
图 4-3 先用慢的非对称算法安全地换一把钥匙,再用快的对称算法搬数据

步骤拆开看:

  1. 客户端连上服务器,服务器出示证书,证书里含公钥。
  2. 客户端验证证书,确认公钥真的是这家网站的。
  3. 客户端生成一个随机的对称密钥,叫会话密钥。
  4. 客户端用服务器公钥加密这个会话密钥,发过去。
  5. 服务器用私钥解开,拿到会话密钥。
  6. 之后双方用会话密钥做 AES-GCM 加密通信。

为什么这么设计?因为非对称加密慢,只用来传 32 字节的密钥很划算;对称加密快,用来传几 MB 的网页刚好。

这也是为什么"破解 TLS"实际上不是破解 AES,而是想办法拿到会话密钥:要么拿到服务器私钥,要么在客户端装证书,要么直接读进程内存。

4.7哈希:一台不可逆的榨汁机

哈希(Hash)把任意长度的输入,压成固定长度的输出。SHA-256 的输出永远是 256 位,也就是 32 字节。

最好的类比是榨汁机:苹果进去,果汁出来。你没法从果汁还原出苹果。

哈希有三个关键性质:

  • 单向:给定输出,反推输入在计算上不可行。
  • 抗碰撞:很难找到两个不同输入产生相同输出。
  • 雪崩:输入改 1 个比特,输出大约一半的比特会变。
echo -n "hello" | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

echo -n "hellp" | sha256sum
输出完全不同,哪怕只改了一个字母

常见算法现状:

算法输出长度现状
MD5128 位已破解,能人为构造碰撞,不能用于安全
SHA-1160 位已破解,浏览器和证书早已弃用
SHA-256256 位目前安全,广泛使用
SHA-3可变新标准,结构不同,作为备选

"已破解"的意思是能找到碰撞,不是能反推原文。但对签名、证书来说,能碰撞就足够伪造,所以必须换掉。

4.8HMAC:带密钥的哈希

哈希有个问题:谁都能算。攻击者改了数据,自己算一个新哈希附上,接收方照样验证通过。

解决办法是 HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码):把密钥混进哈希计算里。

HMAC(K, M) = Hash((K 异或 opad) 拼接 Hash((K 异或 ipad) 拼接 M))

不用记公式,只要知道:没有密钥就算不出正确的 HMAC。所以它同时提供完整性和认证。

和哈希的区别一句话:哈希防"意外损坏",HMAC 防"有人故意改"。

注意:不要把加密和 MAC 混用同一个密钥,也不要用"哈希(密钥加消息)"这种土办法,长度扩展攻击可以绕过它。用标准 HMAC。

4.9KDF:为什么存密码不能只做一次 SHA-256

如果数据库存的是"密码的 SHA-256",攻击者拿到库之后,可以每秒试几十亿个候选密码。因为 SHA-256 的设计目标就是快。

正确做法是用密钥派生函数(Key Derivation Function,KDF),故意把计算变慢、变占内存:

算法特点适用
PBKDF2迭代很多次哈希,实现简单兼容性好,参数要够大
scrypt消耗大量内存抵抗显卡和专用芯片
Argon2可调内存、时间、并行度目前推荐,密码哈希比赛冠军

除了慢,还要加盐(Salt):每个用户一个随机值,和密码一起哈希。这样同样的密码,存下来也不同,攻击者不能预先算一张大表。

参数选择的原则:让一次验证在你服务器上耗时约 100 毫秒,攻击者要试十亿次就得花几年。

4.10随机数:最容易被忽视的地基

所有密钥都来自随机数。随机数一旦可预测,密钥就等于公开。

两种随机数要分清:

  • 普通伪随机:比如 C 语言的 rand(),种子固定就序列固定,只适合游戏和模拟,绝不能用来生成密钥。
  • 密码学安全伪随机(Cryptographically Secure Pseudo-Random Number Generator,CSPRNG):输出不可预测,即使知道前面的输出也推不出下一个。

历史教训很多:路由器用开机时间当种子,导致密钥可枚举;虚拟机克隆后随机数状态相同,两台机器生成同一对密钥;物联网设备第一次开机没有足够熵,密钥几乎是固定的。

实践规则:密钥、IV、盐、Token,一律用操作系统提供的 CSPRNG,比如 /dev/urandom、getrandom()、secrets 模块。不要自己写随机算法,不要用时间戳。

4.11国密算法:一句话认识

国内有一套自己的密码标准,统称国密(ShangMi,SM 系列):

  • SM2:非对称算法,基于椭圆曲线,用于签名和密钥交换,对标 ECDSA 和 ECDH。
  • SM3:哈希算法,输出 256 位,对标 SHA-256。
  • SM4:分组密码,块长和密钥都是 128 位,对标 AES-128。

在金融、政务、部分国内 App 里会见到。分析流量时如果看到 32 字节的摘要、128 位的分组,别默认是 SHA-256 和 AES,也可能是 SM3 和 SM4。

4.12三个必须记住的误区

第一个:Base64 不是加密。它没有密钥,只是编码,任何人可还原。上一章已经讲过原理。

第二个:加密不等于签名。加密只保证别人看不懂,不保证是谁发的,也不保证没被改。攻击者可以整段替换你的密文,接收方照样能解开,只是内容变了。

第三个:哈希不等于加密。哈希不可逆,加密可逆。用哈希存密码是对的,用哈希"加密"需要还原的数据是错的。

还有一个隐藏误区 自己设计加密方案。把 AES 和 Base64 拼一拼、加个异或、换个顺序,看起来复杂,实际上通常比标准方案弱得多。用成熟的库和协议,不要自创。

4.13一张表收尾

你要解决用什么一句话
机密性AES-GCM一把钥匙,快,还顺手防篡改
完整性HMAC、SHA-256改一个字节就露馅
身份认证证书、签名证明对面真的是它
不可否认数字签名私钥签的,赖不掉

记住这张表,遇到"这个场景该用什么"的问题,先想它属于哪一行。

自测:对称加密和非对称加密,各自的问题是什么?
对称加密快,但钥匙怎么安全送到对方手里是个难题;非对称加密解决了送钥匙的问题,但慢几个数量级,不适合传大量数据。所以现实里用混合加密:非对称传钥匙,对称传数据。
自测:为什么 ECB 模式不能用?
ECB 对每块独立加密,相同的明文块会产生相同的密文块,图案和结构原样泄露。加密一张图片,轮廓依然可见。真实数据要用 CBC、CTR 或 GCM。
自测:同一个密钥下重复使用同一个 IV 或 nonce 会怎样?
CTR 和 GCM 会生成相同的密钥流,两段密文异或后密钥流抵消,直接暴露明文的异或关系,容易被统计分析还原;GCM 下还能伪造认证标签。所以 nonce 必须每次不同。
自测:哈希和 HMAC 差在哪?
哈希谁都能算,只能防意外损坏;HMAC 把密钥混进计算,没有密钥就算不出正确结果,能防有人故意篡改。需要防篡改时用 HMAC,不要只用哈希。
自测:为什么存密码要用 Argon2 而不是一次 SHA-256?
SHA-256 太快,攻击者拿到库后每秒能试几十亿次。Argon2 故意消耗大量内存和时间,把单次验证拖到几十毫秒,再配合每个用户不同的盐,暴力破解的成本就高到不可行。慢,在这里是优点。
动手练
  • 用命令行算一次 echo -n "hello" | sha256sum,再把 hello 改成 hellp,对比两个结果差多少位。
  • 找一段 Base64 字符串,用任意在线工具或命令行解开,确认它不需要密钥。
  • 打开浏览器的证书面板,看一个网站的证书用的是 RSA 还是 ECC,密钥多少位。
  • 用一句话向家人解释"加密"和"签名"的区别,不许用专业词。
对称加密Symmetric Encryption加密解密同一把钥匙,快。
非对称加密Asymmetric Encryption公钥加密、私钥解密,慢但能换钥匙。
分组密码Block Cipher把数据切块逐块加密,AES 是代表。
初始向量IV让相同明文每次加密结果不同的随机值。
AEAD加密同时带认证,GCM 属于这一类。
哈希Hash把任意输入压成固定长度指纹,不可逆。
HMAC带密钥的哈希,能防篡改和冒充。
KDF故意变慢的密钥派生函数,用来存密码和生成密钥。
你现在应该能回答 四个需求分别用什么工具?为什么 TLS 要混合加密?ECB 泄露什么?重复 IV 会怎样?哈希和 HMAC 差在哪?存密码为什么要慢?这六问答得出来,后面看 TLS 握手、JWT 签名、App 加固都不会被术语卡住。
第 5 章 · 密码学基础

数字签名与证书:谁在证明这句话是你说的

加密能挡住偷看,却挡不住"改一个字"和"对面是假货"。这一页用蜡封、印模、身份证把签名、证书、证书链、吊销一路讲通,最后说清为什么"地址栏有锁"不等于"这个网站可信"。

哈希私钥 / 公钥X.509 证书 证书链OCSP代码签名
先记住这一句 数字签名只做两件事:证明这段话确实出自某个私钥的持有者,并且一个字都没被改过。证书则是把"一把公钥"和"一个身份"绑在一起的那张纸,而这张纸本身,也是被上一级签过名的。
人话版 私钥像你独有的印章,盖上去就是签名;公钥像名片上的印模,谁都能拿来比对。证书是公证处开的证明:这枚印模确实属于这家公司。整页就讲这三样东西怎么互相作证。

5.1先分清:加密、哈希、签名是三件事

初学者最容易把这三个词混着用。它们解决的是完全不同的问题,用错地方就会得出完全错误的结论。

打个比方:加密是把信锁进保险箱,只有拿钥匙的人能打开;哈希是给信算一个指纹,指纹不能还原出信;签名是在指纹上盖一个只有你能盖的章。

手段用什么解决什么能否还原原文
对称加密(AES)一把钥匙,收发双方都有防偷看能,有钥匙就行
哈希(SHA-256)不需要钥匙做指纹、做比对永远不能
数字签名私钥签,公钥验防篡改、防冒充不能,也不负责保密

一句话记住分工:加密管"看不懂",哈希管"变了没有",签名管"谁说的"。这三件事经常一起出现,但不能互相替代。

5.2蜡封:把签名的动作拆开看

中世纪的信封会用火漆封口,再盖上家族印章。收信人看到蜡封完整、印章对得上,就能得出两个结论:信没被拆过,而且确实出自那个家族。

数字签名就是把这件事搬到电脑上。私钥是你的印章,只有你有;公钥是印在名片上的印模,谁都能拿到。用私钥盖一次,全世界都能用公钥验,但没人能伪造。

数字签名的生成与验证流程 签名方:只有他手里有私钥 原文 订单 100 元 算哈希 SHA-256 摘要 私钥运算 只有你能做 签名 一串固定长度字节 验证方:只要有原文、签名和公钥 原文和签名 两样一起给 公钥解出摘要 谁都能做 自己算哈希 对收到的原文算 比对 一致即通过 签名不藏内容:原文照样明文可读,签名只是附在旁边的一段证据
图 5-1 上半部分是"签",下半部分是"验":签要私钥,验只要公钥

注意图里两行:上面那行是签名方做的事,下面那行是任何拿到数据的人都能做的事。签名本身是一串字节,通常和原文一起发出去,或者单独放在请求头里。

长度要有概念:RSA-2048 的签名是 256 字节;Ed25519 只有 64 字节;ECDSA P-256 大约 70 到 72 字节。签名会明显增加报文体积,抓包时看到一段很长的 base64,往往就是它。

补充 公钥可以随便公开,私钥一旦泄露,别人就能以你的名义签任何东西。所以私钥通常放在硬件里:手机的安全芯片、银行的 U 盾、服务器的硬件安全模块(HSM)。

5.3为什么必须先哈希再签名

直接拿整份文件去做非对称运算,有两个硬伤。

  • 太慢:非对称运算比哈希慢好几个数量级,签一个 1GB 的安装包要等到天荒地老。
  • 装不下:RSA 一次能处理的数据长度受密钥长度限制,2048 位密钥只能处理几百字节以内的内容。

所以标准做法是:先对原文算一个哈希(比如 SHA-256,固定输出 32 字节),再对这 32 字节的摘要签名。验证时,对方也对原文算一次哈希,再和"用公钥解出来的摘要"比对。

做法运算量能签多大文件
直接签原文与文件大小成正比受密钥长度限制
先哈希再签固定一次运算任意大小

哈希还有一个好处:把任意长度的内容压成固定长度,签名算法就不用关心原文有多长,实现起来简单,验证也快。

注意 哈希本身不防篡改。攻击者把文件改掉、再重新算一个哈希给你,你比对的是"他给的哈希",照样对得上。只有让哈希经过私钥盖章,篡改才会暴露。

这也解释了为什么哈希算法必须选强的。MD5 和 SHA-1 已经被证明能人为制造碰撞(两份不同内容算出同一个值),现在做签名普遍要求 SHA-256 及以上。

5.4四种常见签名算法

你不需要懂数学,只要记住它们的取舍。四种算法的共同点只有一句:私钥签,公钥验。

算法签名长度特点常见场景
RSA PKCS#1 v1.5256 字节确定性,同样输入永远同样签名老证书、部分安装包
RSA-PSS256 字节随机化填充,每次签名都不同TLS 1.3、新系统
ECDSA约 70 字节短、快,极度依赖随机数质量证书、JWT、Android
Ed2551964 字节快、确定、实现简单不易出错SSH 密钥、新协议

"确定性"是指同样的原文和私钥,每次算出来的签名一模一样;"随机化"是指每次都不同,但都能验证通过。两者都安全,只是用途和兼容性不同。

选哪个?一般不用你选。协议和平台会规定好,比如 TLS 1.3 里 RSA 证书要求用 PSS,很多新系统默认 Ed25519。

红线 ECDSA 如果随机数被重复使用,私钥可以被直接算出来。历史上真出过这种重大事故。不要自己实现签名算法,用成熟库,把随机数交给系统。

5.5证书:把公钥和身份钉在一起

光有公钥还不够。攻击者完全可以把自己的公钥发给你,说"我是银行"。你怎么知道这把公钥真的属于银行?

答案是找一个大家都信的第三方,给你的公钥签个字。这张"签过字的纸"就是证书(Certificate),格式标准叫 X.509。

字段内容示例作用
版本v3目前基本都是 v3
序列号一串十六进制CA 分配的唯一编号,吊销时用它指认
颁发者某某 CA谁签的这张证书
主体example.com 及公司名证书写给谁
公钥RSA 2048 位或 ECDSA真正要绑定的东西
有效期起止时间过期即失效,现在普遍不超过一年
SANDNS:example.com浏览器实际校验的域名列表
扩展CA:TRUE、用途限制限定它能干什么、能不能再签下级

这里有个过时知识要纠正:很多老教程说"看 CN 字段里的域名"。现在浏览器只认 SAN(Subject Alternative Name,主体备用名)扩展里的域名列表,CN 早就不参与域名匹配了。抓包看证书时,重点看 SAN。

技巧 通配符证书 *.example.com 只匹配一级子域:a.example.com 可以,a.b.example.com 不行。看到证书里写着通配符,先确认它覆盖的范围。

5.6证书链:浏览器一级一级往上验

证书不是凭空签出来的,它本身也是被上一级 CA 签的,于是一条接一条,形成证书链(Certificate Chain)。

根 CA 到中间 CA 到服务器证书的信任链 根 CA 证书 自签名,预装在系统或浏览器里 信任的起点 中间 CA 证书 由根 CA 签名,可能不止一层 可被替换和吊销 服务器证书 由中间 CA 签名,含域名和公钥 有效期通常更短 浏览器从下往上验:签名、有效期、域名、用途、吊销状态,最后看根是否可信 任何一环断了,地址栏就会报警
图 5-2 信任是"从根往下传"的:根证书是起点,中间 CA 是二传手

浏览器从服务器证书开始,逐级用上一级的公钥验证签名,一直验到本机信任库里的根证书。根证书是自签名的,靠操作系统和浏览器预装来"无条件信任"。这也解释了为什么根证书数量很少:它是整个体系的锚点。

完整验证清单,缺一不可:

  • 每一级签名都对得上,用上一级的公钥验下一级;
  • 当前时间在有效期内,没有过期也没到生效时间;
  • 访问的域名出现在 SAN 里,通配符范围匹配;
  • 用途正确,服务器证书不能拿来签代码;
  • 没有被吊销,这一步经常是"尽力而为";
  • 根证书在本机信任库里,并且没有被用户手动移除。
注意 自签名证书报警,不是因为加密弱,而是因为没人给它的身份作证。加密强度照样够,但你不知道对面到底是谁。

5.7吊销、私有 CA 与"合法中间人"

证书有有效期,但私钥可能提前泄露,需要"提前作废"。业界有三种做法。

方式谁去查代价
CRL 证书吊销列表客户端下载整张名单名单越来越大,更新有延迟
OCSP 在线状态查询客户端实时问 CA多一次请求,且暴露你在访问谁
OCSP Stapling服务器代查,随握手下发需要服务器支持并定期更新

CRL 是 CA 定期发布一张"作废名单",客户端把整张表下载下来自己比对。证书多了以后,这张表会非常大。

OCSP 是拿着证书序列号实时问 CA"这张还有效吗"。快,但多一次网络往返,而且等于告诉 CA"我正在访问这个网站",隐私上有代价。

OCSP Stapling 是服务器替你去问,把 CA 的答复(带时间戳)在 TLS 握手时一起发给你。省一次请求,也不泄露你的访问记录。代价是服务器要自己维护这份答复。

补充 现实里很多客户端查不到吊销状态时会选择放行(业内叫 soft-fail)。所以"吊销"不是百分之百可靠,短有效期、证书固定才是更硬的约束。

5.7.1 自签名、企业私有 CA 和抓包代理

这三种场景经常被混为一谈,其实动机完全不同:

  • 自签名:自己给自己签,测试环境常见,浏览器会红,但开发时够用。
  • 企业私有 CA:公司自建根证书,装进员工电脑和手机,用于内部系统,方便统一管理。
  • 抓包代理:Charles、Fiddler、mitmproxy 这类工具会生成自己的 CA。你把它装进手机并设为信任,它就能给任何网站现场签一张证书。
抓包代理作为合法中间人的两条加密连接 代理自建 CA 你手动装进手机 手机 / App 只信任系统根证书 抓包代理 现场签一张证书 真实服务器 不知道中间有人 手机到代理是一条 TLS,代理到服务器是另一条 TLS 两条连接各自都"合法",中间人就被合法化了 前提只有一个:你主动把那把根证书装进了手机
图 5-3 抓包代理不是"破解"了 TLS,而是让两端各自都信任了它

原理不复杂:手机以为自己在和服务器说话,其实是在和代理说话;代理再用真实身份去连服务器。中间人之所以能看懂内容,是因为你把信任交给了它。

平台差异很关键:Android 7 以后,App 默认不信任用户自己安装的证书;iOS 需要在"关于本机"里找到"证书信任设置",手动打开完全信任。这就是为什么"证书装了,包还是抓不到"。

反过来,App 也会用证书固定(pinning)来防这一手:只认自己写死的那张证书或那把公钥,换了就不连。攻防就在这一层反复拉锯。

5.8代码签名:安装包也要盖章

你下载的 APK、IPA、EXE 本质上都是"代码",同样需要证明"没被改过、是谁发的"。否则任何人往里塞一段恶意代码,用户根本看不出来。

平台方案签的是什么
Android v1JAR 签名逐个文件算摘要,不解压整包
Android v2整包签名对整个 APK 二进制签名,改一字节就失效
Android v3支持换密钥带密钥轮换证明,换完旧版本仍能升级
iOS苹果签发证书加描述文件绑定 Bundle ID、权限和设备
WindowsAuthenticode对 PE 文件签名,并附时间戳

Android 的 v1 方案只签每个文件的摘要,安装时逐个校验,历史上出过漏洞;v2 改成对整个安装包做一次签名,校验快、覆盖全;v3 增加了换密钥的能力,方便密钥泄露后平滑过渡。

iOS 更严格:证书由苹果签发,还要配描述文件,绑定 Bundle ID、权限和设备列表。所以"重签名"在 iOS 上是个专门的技术活,不是改个文件就行。

Windows 的 Authenticode 会给签名加一个时间戳。这样即使证书过期,之前签过的程序仍然有效,不会因为证书到期就全部失效。

注意 重打包必然要重新签名,签名者就变了。所以很多 App 会校验自己的签名指纹,指纹不对就直接退出。这也是"改包"经常闪退的原因之一。

5.9四个常见误区

  • 以为签名能保密:签名不隐藏内容,明文照样能读。要保密得另外加密。
  • 以为有证书就可信:钓鱼站也能申请到合法证书。证书只证明域名归属,不证明内容善意。
  • 以为签名能防重放:签名不绑定时间,原样再发一次照样验通过。要配合时间戳、随机数和服务端去重。
  • 以为哈希就是签名:哈希谁都能算,没有身份含义,谁都能伪造一个"看起来对"的哈希。

把这四点想清楚,你在抓包里看到签名、证书、指纹时,就不会再把它们当成同一件事。

自测:为什么签名之前一定要先算哈希?
两个原因:性能(非对称运算太慢,不能对整份大文件做)和长度限制(RSA 一次只能处理几百字节)。哈希把任意长度压成固定 32 字节,签名就变成一次固定开销。哈希本身不防篡改,必须经过私钥盖章。
自测:既然根 CA 可以直接签服务器证书,为什么还要中间 CA?
为了安全和运维。根证书的私钥应该离线保管,尽量不动;日常签发交给中间 CA,万一中间 CA 出问题,吊销它就行,不用动根。中间 CA 是一层可牺牲的缓冲。
自测:抓包代理为什么能看懂 HTTPS 内容?
因为它把自己的根证书装进了你的手机,手机于是信任了它现场签发的证书。手机和代理之间是一条 TLS,代理和服务器之间是另一条 TLS,两段各自都合法。本质是你主动交出了信任,不是算法被攻破。
自测:地址栏有小锁,是不是就说明网站安全?
不是。锁只说明"这条连接是加密的,并且证书链验证通过",也就是传输这一段没被偷看和篡改。网站本身有没有漏洞、是不是钓鱼站、会不会卖你的数据,证书一概不管。
动手练
  • 在电脑浏览器里点地址栏的锁图标,打开证书,找到 SAN 字段,看它到底覆盖了哪些域名。
  • 用 openssl x509 -in cert.pem -text -noout 打印一张证书,对照本页表格找字段。
  • 用 openssl s_client -connect example.com:443 看一次握手,观察服务器发了几张证书。
  • 在自己的手机上装一次抓包代理的证书,然后访问一个银行 App,观察它是拒绝连接还是正常打开。
数字签名Digital Signature用私钥对摘要盖章,任何人可用公钥验证。
哈希Hash把任意内容压成固定长度指纹,不可逆。
公钥 / 私钥Public / Private Key一对数学上关联的密钥,公钥公开,私钥保密。
X.509证书的标准格式,规定字段和扩展怎么排。
CACertificate Authority签发并担保证书的第三方机构。
SANSubject Alternative Name证书里真正被校验的域名列表。
OCSP Stapling服务器代查吊销状态,随握手一起下发。
代码签名Code Signing给安装包盖章,证明来源且未被篡改。
你现在应该能回答 签名和加密分别解决什么?为什么先哈希再签名?证书里哪个字段才决定域名匹配?浏览器怎么一级级验到根?抓包代理凭什么能解密?有证书为什么不等于可信?这六个问题答得出来,第 8 章的 TLS 就不会卡。
第 6 章 · 网络协议

TCP 与 UDP:可靠到底是怎么做到的

很多人背过"TCP 可靠、UDP 不可靠",但说不出可靠是靠什么换来的。这一页用打电话、寄明信片、编号回执、路口限流,把握手、挥手、重传、粘包、NAT 一次讲透,最后告诉你为什么抓包里会出现 RST 和零窗口。

三次握手四次挥手序列号与窗口 重传粘包QUIC
先记住这一句 TCP 的"可靠"不是魔法,而是编号、确认、重传、排序四件事叠加的结果,代价是延迟和开销。UDP 把这些全砍掉,只保留"把包发出去",可靠性交回给应用自己决定。
人话版 TCP 像打电话:先接通、确认对方在听,说漏了让对方再说一遍。UDP 像寄明信片:写完就投,丢了不管。这一页就讲打电话这套流程的每个细节。

6.1打电话和寄明信片

传输层要解决的是"两台机器上的两个程序怎么对话"。最常用的两套方案,思路完全相反。

TCP 像打电话:先拨号,等对方接起来,确认双方都能听见,再开始说;说的时候对方会"嗯"一声表示收到;没听清就让他重复。挂电话也要互相确认。

UDP 像寄明信片:写好地址投进邮筒,不确认对方收没收到,也不保证顺序。便宜、快,但可能丢、可能后发的先到。

对比项TCPUDP
要不要先建立连接要,三次握手不要,直接发
丢包怎么办自动重传不管,交给应用
顺序保证按发送顺序交付不保证
典型用途网页、文件、登录DNS、视频、游戏、QUIC

注意最后一行:QUIC 跑在 UDP 上,但它自己在应用层把可靠性做回来了。所以"UDP 不可靠"说的是协议本身不负责,不是说用了它就一定丢数据。

6.2三次握手:为什么不能是两次

建立连接要交换三句话。核心目标只有一个:双方都要确认"我发的能到、你发的能到"。

TCP 三次握手的过程 客户端 服务器 SYN_SENT ① SYN,我的起始编号是 x SYN_RCVD ② SYN-ACK,收到 x,我的编号是 y ③ ACK,确认收到 y,连接建立 ESTABLISHED ESTABLISHED 两次不够:服务器发完 SYN-ACK 就以为连上了,却不知道它有没有到 三次之后,双方都确认了双向可达,并同步了各自的起始序号
图 6-1 三次握手的本质是"双向各确认一次",少一次就会留下不确定

为什么两次不行?服务器发完 SYN-ACK 就单方面认为连接建立了,但它并不知道这个包有没有送到客户端。如果客户端根本没收到,服务器就白建了一条连接,还占着内存。

更麻烦的是旧包。网络里可能残留很久以前发出的重复 SYN,如果只握两次,服务器会被这些过期请求骗出一堆无效连接。三次握手能让这种老包自然失效。

还有一个细节:起始序号(ISN)是随机选的,不是从 0 开始。这样攻击者更难猜到下一个序号,也更难伪造报文。

抓包里看到 SYN、SYN-ACK、ACK 三行,就是一次成功的握手;只看到 SYN 后面跟着 RST,通常是端口没开,或者被防火墙直接拒绝。

6.3四次挥手与 TIME_WAIT

关闭连接要四句话,因为 TCP 是全双工的:两个方向要各自关一次。你说"我不发了",不代表对方也发完了。

TCP 四次挥手与 TIME_WAIT 客户端 服务器 ① FIN,我没有数据要发了 ② ACK,知道了,但我可能还有数据 ③ FIN,我也发完了 ④ ACK,好,都结束 TIME_WAIT 主动关闭方要等 2MSL,确保最后那个 ACK 丢了还能补发 通常几十秒到几分钟,Linux 默认约 60 秒
图 6-2 中间的 ACK 和 FIN 有时会合并,变成三步,取决于服务器还有没有数据

主动关闭的一方最后要进入 TIME_WAIT,等 2 倍报文最大生存时间。这个等待不是浪费,它有两个明确作用:

  • 万一最后那个 ACK 丢了,对方重发 FIN 时,还能再回一次,不至于让对方一直卡着;
  • 让这条连接的旧报文在网络里彻底消失,避免污染下一条恰好用了相同端口的新连接。

服务器上出现大量 TIME_WAIT 是正常现象,不是"连接泄漏"。真正需要担心的是本地端口被占满,导致新连接建不起来。

6.4编号、回执和窗口

TCP 把数据看成一条很长的字节流,每个字节都有编号。发送方说"我从 1001 号开始给你 500 字节",接收方回"1001 到 1500 收到,下一条我要 1501"。

这个"下一条我要第几号"就是确认号(ACK)。它是累积的:确认了 1501,就代表 1501 之前的全部收到,不用一条条确认。

窗口(Window)是接收方的缓冲区还剩多少空间。窗口变成 0,发送方就必须停下来等,这就是抓包里的"零窗口"。

字段含义生活类比
序列号这一段的第一个字节编号这是第几页
确认号我下一条想要第几号回执写"1 到 5 页已收到"
窗口我还能接多少字节桌子只能再放三摞
补充 TCP 头里的窗口字段只有 16 位,最大 65535 字节。现代网络靠"窗口缩放"选项把它按倍数放大,否则高延迟链路上根本跑不满带宽。

这三样合起来就是可靠传输的骨架:编号让接收方能排序和去重,确认让发送方知道哪些到了,窗口让发送方别把接收方撑爆。丢包、乱序、重复,都靠这套机制兜住。

6.5重传与拥塞控制

丢包了怎么办?等。等多久?根据往返时间动态估算,这个等待时间叫 RTO。超时还没收到确认就重传,而且每次超时等待时间翻倍,避免把已经堵死的链路砸得更烂。

还有一种更快的办法:如果连续收到三个重复的确认,说明中间缺了一段,不用等超时,立刻重传缺的那段。这叫快重传,能让恢复从"秒级"降到"毫秒级"。

拥塞控制像路口限流。发送方维护一个"拥塞窗口",开始时很小,每收到确认就涨一点(慢启动阶段实际是每轮翻倍),摸到上限后改成缓慢增长(拥塞避免);一旦检测到丢包,就把窗口砍半,退回去重新试探。

现代系统常用 CUBIC 或 BBR 这类算法。你不需要记公式,只要记住一句话:TCP 会主动试探带宽上限,一丢包就退让。

技巧 抓包里同一段数据出现两次、序号相同,就是重传。少量重传正常;比例超过百分之几,就要查链路质量、查是不是拥塞、查中间设备有没有乱丢包。

6.6粘包与拆包:TCP 是字节流,不是消息流

这是初学者最容易被坑的地方。你调了两次发送,对方不一定读两次;你发了一条完整消息,对方可能只读到一半。

TCP 粘包与拆包的形成原因 应用层调了两次发送 第一次写 HELLO 第二次写 WORLD TCP 字节流 HELLOWORLD 没有消息边界 一次读到 HELLOWORLD 两段粘在一起:粘包 先读到 HEL 剩下的下次读:拆包 TCP 只保证字节顺序,不保证"一次读多少" 解决办法:定长、分隔符、长度前缀,三者选一 这不是 TCP 出错,是"流"本来就没有消息边界
图 6-3 两次写入变成一条字节流,读几次、读到哪,完全由接收方决定

你写了两次,可能被合成一次读到(粘包);也可能一次写入被拆成两半(拆包)。这跟"TCP 可靠"不矛盾:字节一个没少,只是边界没了。

解决办法都在应用层,常见三种:

  • 定长:每条消息固定长度,不够就补齐,简单但浪费;
  • 分隔符:用换行或特殊字符分隔,HTTP 头就是用空行结束的;
  • 长度前缀:先发 4 字节表示后面有多长,再发内容,最通用,很多私有协议都这么干。
注意 不要用 sleep 来"解决"粘包。网络抖动一大就失效,而且会拖慢整个程序。正确做法是解析长度或分隔符,而不是猜时间。

6.7选项、UDP 与 QUIC

TCP 头里有一块"选项"区域,用来协商一些增强能力。抓包时展开它,经常能看到问题所在。

选项作用为什么重要
MSS告诉对方我一次最多收多少字节避免 IP 分片,典型值 1460
窗口缩放把 16 位窗口按倍数放大高延迟高带宽链路必备
SACK告诉对方我收到了哪些不连续段只重传真正丢的那一段
时间戳更精确地算往返时间、识别旧包提升重传判断的准确度

UDP 的头只有 8 字节,没有握手、没有确认、没有重传。它把"要不要可靠"的决定权交给应用。DNS 查询、视频通话、游戏同步都用它,因为丢一帧比卡一秒更能接受。

QUIC 是把可靠性做进用户态的一次重做:它跑在 UDP 上,自己实现重传、流控、加密(内置 TLS 1.3)和多路复用。好处是升级不用等操作系统和中间设备,坏处是很多网络设备看到的就是一堆 UDP 包,可能被限速或拦截。

QUIC 还顺手解决了 TCP 的一个老毛病:一条连接里所有数据共享同一个字节流,丢一个包会卡住后面所有数据(队头阻塞)。QUIC 的每条流独立,丢包只影响自己那条。

6.8五元组、NAT 与抓包排错

一条 TCP 连接由五元组唯一确定:源 IP、源端口、目的 IP、目的端口、协议。抓包工具就是靠这五项,把杂乱的包归到不同连接里。

你家所有设备共用一个公网 IP,靠 NAT 区分:路由器改写源端口,维护一张"内网地址端口"到"公网端口"的对照表。表项有超时,太久没流量就被回收,回收后再来包就对不上了。

抓包时三种信号最值得看:

现象通常意味着下一步查什么
RST端口没开、被防火墙拒绝,或应用主动重置服务是否在听、中间设备是否拦截
大量重传链路丢包或拥塞丢包率、是否集中在某一跳
零窗口接收方应用没在读,缓冲区满了应用是否卡住、处理是否太慢
红线 最后一个误区:TCP 可靠,不等于你的业务可靠。数据被内核确认,只代表"收到了",不代表"处理成功了"。应用崩溃、写库失败、逻辑报错,TCP 一概不管。所以业务层仍然需要自己的确认、重试和幂等。

把这句话记牢,你在排查"用户说没收到"这类问题时,就不会只盯着网络层了。

自测:为什么握手是三次而不是两次?
两次的话,服务器发完 SYN-ACK 就认为连接建立,但它不知道这个包有没有到客户端,也不知道客户端是否还在。三次之后,双方都确认了双向可达,同时同步了各自的起始序号,还能让网络里残留的旧 SYN 自然失效。核心是"双向各确认一次"。
自测:TIME_WAIT 等的是什么?
等 2 倍报文最大生存时间。作用有两个:最后那个 ACK 丢了还能补发;让旧连接的报文在网络里彻底消失,不污染复用相同端口的新连接。它是保护机制,不是故障。
自测:为什么会粘包?
因为 TCP 是字节流,只保证字节顺序,不保留"你写了几次"的边界。两次写入可能被合并读到,一次写入也可能被拆开读。解决办法在应用层:定长、分隔符或长度前缀。
自测:TCP 已经保证可靠了,为什么应用还要重试?
TCP 只保证"字节到达对端内核",不保证"业务处理成功"。应用可能崩溃、写库可能失败、逻辑可能报错,这些 TCP 都看不见。业务层需要自己的确认、超时、重试和幂等键。
动手练
  • 用 ss -tan 看本机的连接状态,找找有没有 TIME_WAIT。
  • 用 Wireshark 抓一次打开网页的过程,找到 SYN、SYN-ACK、ACK 三行,展开看序列号。
  • 用 nc 连一个服务,连续发两条短消息,观察对端是不是一次读到。
  • 抓一次大文件下载,看窗口大小怎么变化,有没有出现重传。
三次握手Three-way HandshakeSYN、SYN-ACK、ACK,建立双向确认。
序列号Sequence Number给每个字节编号,用于排序和去重。
确认号Acknowledgment告诉对方"下一条我要第几号"。
窗口Window接收方还能接多少字节,用于流控。
MSS单个 TCP 段能携带的最大数据量,典型 1460 字节。
TIME_WAIT主动关闭方最后的等待状态,约 2MSL。
粘包Sticky Packet多条消息被合并成一次读到,边界丢失。
QUIC基于 UDP 的可靠传输,内置加密和多路复用。
你现在应该能回答 握手为什么是三次?TIME_WAIT 保护了什么?序列号、确认号、窗口各管什么?丢包怎么恢复?为什么会粘包?RST 和零窗口分别说明什么?这六个问题答得出来,第 7 章的 HTTP 和后面的抓包排错就顺了。
第 7 章 · 网络协议

DNS 与 HTTP 家族:从域名到页面的每一跳

你在地址栏敲下域名到看见页面,中间至少经过一次域名查询、一次 TCP 握手、一次 HTTP 请求、若干次缓存判断。这一页把这条链路上的每一跳拆开,告诉你 301 和 302 差在哪、304 为什么没有正文、HTTP/2 到底解决了什么又没解决什么。

DNS 解析TTLHTTP 报文 状态码CookieHTTP/2 与 HTTP/3
先记住这一句 打开一个网页不是"发一个请求",而是一条先查地址、再建连接、再要资源、再判断缓存的流水线。任何一环慢,页面就慢;任何一环被篡改,你看到的东西就可能不是服务器原本想给你的。
人话版 DNS 是通讯录,负责把名字翻译成门牌号;HTTP 是说话的方式,规定怎么问、怎么答。这一页就是把"查地址"和"问内容"这两件事的规矩讲清楚。

7.1第一跳:把域名换成 IP

你在地址栏输入 example.com 回车。第一件事不是发 HTTP,而是问"这个域名对应哪个 IP"。因为网络只认 IP,不认名字。

DNS 查询从缓存到权威服务器的全过程 ① 浏览器缓存 查过就有 ② 系统缓存 hosts 和本机缓存 ③ 递归解析器 替你跑完全程 ④ 根服务器 回答:谁管 .com ⑤ 顶级域服务器 回答:example.com ⑥ 权威服务器 给出 A 记录 结果原路返回并缓存:IP 203.0.113.10(TTL 决定缓存多久) 任何一级命中就直接返回,后面的都不用问
图 7-1 DNS 是逐级委托的:根只知道顶级域在哪,顶级域只知道权威在哪

查询顺序是逐级往上的:浏览器缓存、系统缓存(含 hosts 文件)、递归解析器、根服务器、顶级域服务器、权威服务器。任何一级命中就直接返回,后面的都不用问。

递归解析器通常是运营商或公共 DNS 提供的。它替你把整条链路跑完,然后把结果缓存一段时间,这个时间就是 TTL。所以同一个域名第二次访问,往往一个包都不用发。

7.2记录类型、TTL 与 DNS 安全

DNS 不是只有"域名换 IP"一种记录。不同类型的记录,管的是不同的事。

类型作用例子
A域名对应的 IPv4 地址203.0.113.10
AAAA域名对应的 IPv6 地址2400:cb00::1
CNAME别名,指向另一个域名www 指向主域
MX收邮件的服务器10 mail.example.com
TXT任意文本,常用于验证和反垃圾SPF 记录
NS这个域由哪台服务器负责ns1.example.com
SOA区域起始信息,含主 NS 和序列号管理信息
SRV某类服务的主机和端口语音服务地址

TTL 是"这条记录能缓存多少秒"。TTL 设得很长,改 IP 后要等很久才全网生效;设得很短,解析压力大。常见做法是切换前先把 TTL 调短,切完再调回去。

DNS 天生是明文的,早期也没有验证机制,所以能被污染:中间人抢先返回一个假 IP,你的请求就去了别人的服务器。DNSSEC 用签名保证"这条记录确实来自权威服务器",但它不加密,别人仍然能看到你在查什么。

DoH(DNS over HTTPS)和 DoT(DNS over TLS)把查询塞进加密通道,解决"被偷看、被篡改"的问题。代价是:企业内网的 DNS 审计会失效,所以很多公司会主动禁用它。

补充 分清三件事:DNSSEC 管"真假",DoH 和 DoT 管"隐私"。它们经常一起用,但解决的不是同一个问题。

7.3第二跳:HTTP 报文长什么样

拿到 IP、建好连接之后,才轮到 HTTP 出场。HTTP 是"一问一答"的文本协议,请求分三块:起始行、头、空行、体。

起始行:方法 + 路径 + 版本
GET /index.html HTTP/1.1

请求头:一堆说明
Host: example.com
User-Agent: MyApp/8.2.1 (Android 14)
Accept: text/html
Cookie: session=abc123

空行之后是请求体,GET 一般没有体

响应也一样:状态行、头、空行、体。

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 132
Cache-Control: max-age=600
ETag: "a1b2c3"

空行之后是正文,这里是 132 字节的 HTML

那个空行不是装饰,它是"头结束、体开始"的分界。少了它,对方会一直等更多的头,最后超时。

请求头作用例子
Host要访问哪个站点,一台服务器可能挂很多站example.com
User-Agent客户端身份,服务端常用来做适配和风控MyApp/8.2.1
Cookie浏览器自动带上的凭证session=abc123
Referer从哪个页面跳过来的搜索结果页
Authorization显式凭证,常见 Bearer 令牌Bearer 加 token
Content-Type体的格式application/json
Content-Length体有多少字节132
Transfer-Encoding分块传输,事先不知道总长时用chunked

Content-Length 和 Transfer-Encoding 是二选一的关系。分块传输适合"边生成边发"的场景,比如大文件下载、流式输出,因为发送前根本不知道总长度。

7.4状态码:三位数里的信息

状态码的第一位决定大类。记住大类,再看具体数字,就不会慌。

大类含义常见
1xx中间状态,还在处理100、101
2xx成功200、204
3xx重定向或走缓存301、302、304
4xx客户端的问题400、401、403、404、429
5xx服务端的问题500、502、503、504

重点记这几个,够用八成场景:

  • 200:成功,响应体里有业务数据;
  • 301:永久重定向,会被缓存,搜索引擎会更新地址;302 是临时重定向,不缓存;
  • 304:内容没改过,用你本地的缓存,响应没有体;
  • 400:请求格式错了;401 没认证或凭证失效;403 认证了但没权限;404 找不到;
  • 429:请求太频繁,通常带 Retry-After 告诉你等多久;
  • 500:服务端代码出错;502 网关拿到上游的坏响应;503 服务暂时不可用;504 网关等上游超时。
注意 502 和 504 是"代理或网关"报的错,不是源站直接报的。看到它们,先查反向代理和上游服务之间的链路,而不是盯着业务代码。

7.5Cookie 和 Session 到底谁存什么

HTTP 本身是无状态的:服务器不记得上一次是谁来过。要让"登录状态"延续,需要额外的东西。

Cookie 是存在浏览器里的一小段文本,每次请求自动带上。Session 是存在服务器上的一份数据,浏览器只保存一个编号(Session ID)。

对比CookieSession
存在哪浏览器服务器
存什么编号或少量数据用户状态、权限
谁控制过期浏览器按 Max-Age服务器说了算

Cookie 有几个关键属性要认识:

  • HttpOnly:脚本读不到,降低被注入脚本窃取的风险;
  • Secure:只在 HTTPS 下发送,防止明文链路泄露;
  • SameSite:限制跨站携带,防跨站请求伪造,常见值 Lax 和 Strict;
  • Domain 和 Path:决定哪些请求会带上它。
补充 把用户信息直接塞进 Cookie 是常见错误。Cookie 在客户端,可以被改;服务端必须校验签名,或者干脆改用服务端 Session。

7.6HTTP/2:一条连接跑多个请求

HTTP/1.1 最大的问题是:一条连接同一时刻只能处理一个请求。浏览器只能开六条左右的连接来并行,开太多又浪费资源。

HTTP/1.1 排队与 HTTP/2 多路复用的对比 HTTP/1.1:一条连接,请求排队 请求 1 处理中 请求 2 排队 请求 3 排队 大响应卡住全队 HTTP/2:一条连接,多路复用 A1 B1 C1 A2 B2 C2 每个小格是一帧,按流编号交错发送,接收方按编号重组 但底层还是 TCP:丢一个包,所有流一起等它重传 所以 HTTP/2 解决了 HTTP 层排队,没解决 TCP 层队头阻塞
图 7-2 上半部分是一个接一个,下半部分是你一段我一段地交错发送

HTTP/2 把报文改成二进制分帧:把每个请求和响应切成小帧,打上"流编号",然后在一条连接上交错发送。接收方按流编号重组。这就是多路复用。

它还做了两件事:HPACK 压缩头部(把重复的头字段放进表里,只发索引),以及服务器推送(服务端主动推资源,实际用得很少,主流浏览器已经移除)。

但 HTTP/2 仍然跑在 TCP 上。TCP 丢一个包,后面所有流的数据都要等它重传,这叫 TCP 层队头阻塞。HTTP/2 只解决了 HTTP 层的排队问题。

注意 常见误区是"上了 HTTP/2 一定更快"。小站点、单请求场景几乎没差别;网络丢包严重时,一条 TCP 上的所有流会一起被卡,反而不如多条连接。协议升级不等于体验升级。

7.7HTTP/3 与缓存控制

HTTP/3 直接换成 QUIC(跑在 UDP 上)。它把 TLS 1.3 内置进握手,首次连接一个往返,恢复连接可以做到 0-RTT:客户端拿着上次的票据,第一个包就能带数据。

0-RTT 有代价:这些早期数据可能被重放。所以只应该用于幂等请求,比如读取,不能用于"转账"这种操作。

QUIC 用连接 ID 而不是四元组标识连接。你从 Wi-Fi 切到移动网络,IP 变了,连接还能继续,这叫连接迁移。

缓存这块,重点是三个响应头:

响应头作用例子
Cache-Control缓存多久、谁能缓存max-age=600
ETag内容的版本指纹"a1b2c3"
Last-Modified最后修改时间某个时间点

浏览器再次请求时会带 If-None-Match(对应 ETag)或 If-Modified-Since。服务器发现内容没变,就回 304,不传正文,省流量也省时间。

no-store 是完全不缓存;no-cache 是"可以存,但每次必须回源确认"。这两个词经常被搞反,配置错会导致该更新的不更新、该省流量的不省。

7.8代理、CDN 与三个误区

正向代理代表客户端,反向代理代表服务器。CDN 是部署在各地机房的反向代理集群,把静态资源放在离用户最近的地方。

CDN 边缘节点在链路中的位置 DNS 先指路 解析到最近的边缘 手机 发起请求 CDN 边缘节点 缓存静态内容 也可能兼做代理 源站服务器 只处理回源请求 静态资源就近取,动态请求回源,这就是 CDN 的基本分工 代理和 CDN 会让源站看到代理 IP,于是有了 X-Forwarded-For 这个头可以被伪造,不能无条件当成真实客户端 IP
图 7-3 用户连的是边缘节点,不是源站;边缘没命中才回源

用了代理和 CDN 之后,源站看到的 IP 往往是代理的。所以会有 X-Forwarded-For 这类头来传递真实客户端 IP。这个头由客户端和代理逐跳追加,可以被伪造,做风控时不能单独信它。

最后是三个必须纠正的误区:

  • HTTP/2 一定比 1.1 快:不一定,见上一节,丢包严重时反而更糟;
  • 200 就代表业务成功:很多系统把错误放在响应体里,HTTP 照样回 200,判断业务要看体里的 code 字段;
  • HTTPS 就安全:HTTPS 只保证传输这一段,服务端漏洞、越权、注入它一概不管。
自测:输入域名后,DNS 的查询顺序是什么?
浏览器缓存、系统缓存(含 hosts)、递归解析器、根服务器、顶级域服务器、权威服务器。任何一级命中就直接返回。递归解析器替客户端跑完整条链路,并按 TTL 缓存结果。
自测:301 和 302 有什么区别?
301 是永久重定向,会被浏览器和搜索引擎长期缓存,地址正式换了;302 是临时重定向,不缓存,下次还会问原地址。选错会导致换域名后流量回不来,或者临时活动把缓存写死。
自测:Cookie 和 Session 分别存在哪?
Cookie 存在浏览器,是一小段文本,每次请求自动带上;Session 存在服务器,浏览器只保存一个编号。Cookie 可以被用户修改,所以服务端必须校验,或者用 HttpOnly 降低被脚本读取的风险。敏感状态放服务器,别放客户端。
自测:HTTP/2 用了多路复用,为什么还会有队头阻塞?
因为 HTTP/2 仍然跑在一条 TCP 连接上。TCP 保证字节按序交付,前面丢了一个包,后面的数据即使到了也要等它重传。HTTP/2 解决的是 HTTP 层的排队,TCP 层的队头阻塞要靠 HTTP/3(QUIC)解决。
动手练
  • 用 dig example.com A 查一次 A 记录,再用 dig example.com MX 看邮件记录。
  • 用 curl -I 只看响应头,找 Cache-Control、ETag、Content-Length。
  • 在浏览器开发者工具的 Network 面板里刷新页面,找出哪些请求返回了 304。
  • 看一个常用网站用的是 HTTP/2 还是 HTTP/3,观察协议列里的标识。
DNSDomain Name System把域名翻译成 IP 的分布式数据库。
TTLTime To Live一条 DNS 记录能被缓存多少秒。
DoHDNS over HTTPS把 DNS 查询放进加密的 HTTPS 通道。
状态码Status Code三位数,第一位表示成功、重定向或错误大类。
Cookie存在浏览器里、每次请求自动携带的一小段文本。
Session存在服务器上的会话数据,浏览器只保存编号。
HPACKHTTP/2 的头部压缩方案,靠索引表减少重复字段。
0-RTT恢复连接时第一个包就能带数据,但存在重放风险。
你现在应该能回答 DNS 的查询顺序是什么?301 和 302 差在哪?Cookie 和 Session 各存什么?HTTP/2 解决了什么、没解决什么?304 为什么没有正文?200 为什么不一定代表成功?这六个问题答得出来,第 8 章的 TLS 和第 10 章的抓包就接得上了。
第 8 章 · 协议

TLS 深入:握手、密钥与加密记录

浏览器地址栏那把小锁,背后是一套跑了三十年的协议。这一页把它拆成四件事:怎么谈、怎么算钥匙、怎么装信封、怎么证明身份。看懂之后,抓包里那些看不懂的握手包就有了名字。

握手 Handshake三个随机数密钥派生 AEAD证书链TLS 1.3
先记住这一句 TLS 的全部工作可以压成一句话:先用非对称密码安全地商量出一把只有双方知道的对称钥匙,再用这把钥匙加密后面的所有对话。前者很贵,但只用一次;后者很便宜,却要用一整场。
人话版 手机和网站先对暗号,再一起算出一把临时钥匙,之后所有内容都锁进这把钥匙的保险箱。锁只保路上,不保对面是好人。

8.1它到底在防谁

先想一个场景:你在咖啡店连公共 WiFi,打开网页输入账号密码。这条链路上有多少人能复制你的数据?

至少五六个:你的手机、店里的路由器、运营商、中间经过的机房。任何一环都能录一份。

TLS(Transport Layer Security,传输层安全)做的事,就是在这条路上临时修一条管道。管道外面的人只能看到"有数据在流",看不到里面是什么。

它只解决三个问题,不多不少:

  • 机密(Confidentiality):别人看不懂。靠加密。
  • 完整(Integrity):别人改不了。改一个字节,接收方就会发现。
  • 身份(Authentication):对面真的是它。靠证书。

拿寄合同打比方:

  • 不加密,等于寄明信片,路上谁都能读;
  • 加密,等于装进保险箱;
  • 完整性,等于保险箱贴封条,被撬过就有痕迹;
  • 身份,等于对方出示营业执照,确认不是隔壁老王冒充。
攻击者想干的事具体手段TLS 的对策
偷看在 WiFi 上嗅探数据包对称加密,看到的是密文
篡改往网页里插广告、改转账金额认证标签,改一个字节就失败
冒充假基站、假网站、DNS 污染证书链验证
重放把旧请求原样再发一遍序列号与一次性随机数
补充 TLS 不判断网站是不是骗子。钓鱼站也能申请免费证书、也能挂上小锁。小锁只说明"这条管道是加密的",不说明"对面是好人"。

8.2TLS 1.2 握手:两个来回

握手(Handshake)就是开场谈判。双方要在这几步里完成四件事:交换随机数、确认算法、验证身份、算出钥匙。

RTT(Round Trip Time,往返时延)指"一句话发出去再收到回复"要多久。手机 4G 下通常 30 到 80 毫秒,跨洋可能 200 毫秒以上。

TLS 1.2 的完整握手需要 2 个 RTT。也就是说,在你看到网页之前,光握手就多花了两个来回。

TLS 1.2 完整握手时序图 客户端 你的手机 / 浏览器 服务器 example.com ① ClientHello:随机数 A、套件清单、SNI ② ServerHello:随机数 B、选定套件 ③ Certificate:服务器证书链 ④ ServerKeyExchange:临时公钥 + 签名 ⑤ ServerHelloDone:服务器说完了 ⑥ ClientKeyExchange:客户端临时公钥 ⑦ ChangeCipherSpec:之后我说的话都加密 ⑧ Finished:握手摘要的校验值(已加密) ⑨ ChangeCipherSpec ⑩ Finished(已加密),握手完成 前 5 条是第 1 个 RTT,后 5 条是第 2 个 RTT 握手结束后,双方用同一套会话密钥收发应用数据
图 8-1 TLS 1.2 握手:两个来回、十条消息,先谈条件再算钥匙

逐条看这十条消息在说什么:

  • ① ClientHello:客户端说"我支持这些算法,这是我的随机数 A,我要访问 example.com"。
  • ② ServerHello:服务器从清单里挑一个双方都支持的算法,给出随机数 B。
  • ③ Certificate:服务器把证书链发过来,证明"我是 example.com"。
  • ④ ServerKeyExchange:用 ECDHE 时,服务器给出一个临时公钥,并用证书私钥签名。
  • ⑤ ServerHelloDone:服务器表示"我这边说完了,轮到你"。
  • ⑥ ClientKeyExchange:客户端给出自己的临时公钥。双方各自算出同一个共享秘密。
  • ⑦⑧:客户端发 ChangeCipherSpec 和 Finished,表示"接下来我加密,这是校验值"。
  • ⑨⑩:服务器同样回两条,握手结束,开始传业务数据。

Finished 里装的是"前面所有握手消息的摘要"。任何一条被改过,摘要就对不上,握手立刻失败。这是防篡改的最后一道闸。

注意 在 TLS 1.2 里,证书是明文传输的。抓包时你能看到服务器证书,也能看到 SNI 里的域名。TLS 1.3 把证书也加密了,能看到的就只剩 IP 和端口。

8.3三个随机数与密钥派生

很多人以为 TLS 只用一把钥匙。实际上,握手阶段产生了三份关键材料:

材料谁生成作用
客户端随机数
Client Random,32 字节
客户端给本次会话加盐,让每次的钥匙都不同
服务器随机数
Server Random,32 字节
服务器同上,并防止旧握手被重放
预主密钥
Pre-Master Secret,48 字节
双方协商真正的秘密来源,只有双方知道

为什么需要两个随机数,而不是一个?

因为随机数的作用是"加盐"。假如只有预主密钥,同一对客户端和服务器每次算出来的会话密钥都一样。攻击者录下今天的密文,明天偷到钥匙,就能解开昨天的全部记录。

加入两个随机数之后,每一次连接算出来的钥匙都不同。昨天的密文,今天解不开。

那为什么还要预主密钥?因为随机数是公开传的,光有它没有秘密。预主密钥才是只有双方知道的那部分。

协商预主密钥有两种方式,差别很大:

  • RSA 密钥交换(老):客户端生成预主密钥,用服务器证书里的公钥加密后发过去。缺点是谁录下了流量,将来只要拿到服务器私钥,就能解开全部历史记录。这叫没有前向保密。
  • ECDHE(椭圆曲线临时迪菲-赫尔曼):双方各生成一对临时密钥,交换公钥,各自算出同一个共享秘密。临时私钥用完就扔。即使服务器私钥泄露,也解不开过去的流量。这就是前向保密(Forward Secrecy)。

现在几乎所有正规站点都用 ECDHE。看到"没有前向保密"这个词,基本等于"这套配置已经过时"。

从预主密钥到会话密钥的派生链 预主密钥 48 字节,只有双方知道 PRF(秘密, 随机数 A + 随机数 B) 主密钥 48 字节,整场会话的总根 PRF(主密钥, 密钥扩展标签) 密钥块 一长串,按顺序切给各个用途 MAC 密钥 算校验值用,各方向一份 加密密钥 真正锁数据用,各方向一份 初始向量 IV 给加密算法一个起点
图 8-2 密钥派生:一份预主密钥,经过两次搅拌,切成多条各管一段的钥匙

PRF(Pseudo-Random Function,伪随机函数)像一个搅拌机:把秘密、标签、随机数一起打碎,输出指定长度的字节。它单向、不可预测,同样的输入永远得到同样的输出。

为什么要分"客户端一份、服务器一份"?

因为如果两个方向用同一把钥匙,攻击者可以把客户端发出去的密文原样发回给客户端,让它自己解密自己。这叫反射攻击。分开之后,方向不对就解不开。

技巧 记住这条链:预主密钥 → 主密钥 → 密钥块 → 几条具体钥匙。抓包里每一段加密数据,用的都是这条链末端的东西。

8.4TLS 1.3 改了什么

TLS 1.3 不是小修小补,是一次大扫除。它做了四件事:

改动一

握手降到 1 个 RTT

客户端在 ClientHello 里就带上自己的临时公钥(key_share),服务器算完直接把后续消息一起发回来。

改动二

砍掉一批老算法

删掉 RSA 密钥交换、静态 DH、CBC 模式、RC4、压缩、重协商。只保留带前向保密的 ECDHE 和预共享密钥。

改动三

握手本身也加密

服务器一回完 ServerHello,后面的证书、签名、Finished 全部加密。中间人看不到你访问的是哪个证书。

改动四

密钥派生换成 HKDF

握手消息本身也参与计算。任何一条被篡改,派生出的钥匙就不同,降级攻击更难做。

0-RTT 是 1.3 最诱人也最危险的功能。客户端如果之前连过,可以拿着上次的票据(ticket),在第一个包里就带上"早期数据"(Early Data)。省掉一整个往返,代价是:这些早期数据可以被攻击者原样重放。

所以规范要求:0-RTT 只能用于幂等请求,比如 GET 查询。不能用于转账、下单、改密码。

项目TLS 1.2TLS 1.3
完整握手2 个 RTT1 个 RTT
证书明文可见加密
密钥交换RSA 或 ECDHE只有 ECDHE / PSK
加密模式CBC、GCM 等只有 AEAD

8.5记录层:AEAD、nonce 与重放

握手只是开场。真正传数据的是记录层(Record Layer)。它把上层的数据切成一块块,每块加上头和认证标签。

TLS 1.3 的记录结构非常简单:

TLS 记录结构与 nonce 的构造 类型 1 字节 版本 2 字节 长度 2 字节 AEAD 密文 + 16 字节认证标签 长度等于明文长度加 16 前 5 个字节是明文头,抓包能看见;后面全是密文 静态 IV 12 字节 异或 序列号 从 0 递增,不上线 本次 nonce 12 字节,绝不重复 同一条连接里序列号只增不减,所以 nonce 不会撞车 接收方用同样规则算出 nonce,对不上就丢弃这一块
图 8-3 记录层:明文头加 AEAD 密文;nonce 由静态 IV 和序列号异或得到

AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)是一类算法,一次调用同时完成"加密"和"防篡改"。常见的有 AES-GCM 和 ChaCha20-Poly1305。

它需要三样输入:明文、密钥、nonce。nonce 是"一次性数字",同一把钥匙下绝不能重复。

TLS 1.3 的 nonce 怎么来?把静态 IV 和序列号做异或。

  • 静态 IV 是握手时派生出来的 12 字节,双方都知道;
  • 序列号从 0 开始,每发一条记录加一,不放在网络上,双方各自维护;
  • 异或之后得到本次使用的 nonce。

为什么这样能防重放?

因为同一条连接里序列号只增不减。攻击者把第 5 条记录再发一次,接收方按第 8 条去解,nonce 对不上,认证标签校验失败,直接丢弃。跨连接呢?每条连接的钥匙都是新算的,旧密文在新连接里根本解不开。

注意 TLS 1.2 的 CBC 模式不是这样。它先算 MAC 再加密,早期版本的 IV 还可预测,这直接导致了 BEAST 和 POODLE 两个著名漏洞。TLS 1.3 干脆把 CBC 整个删了。

8.6证书怎么验、SNI、ALPN、ECH 与会话恢复

拿到证书不等于信任。客户端至少要做四道检查:

检查项在查什么不过会怎样
链能否一路签到受信任的根证书报"证书不受信任"
签名每一环的签名是否用上一环公钥验过报"证书链损坏"
有效期当前时间是否在起止时间内报"证书已过期"
主机名SAN 里是否包含你访问的域名报"名称不匹配"

还有两件常被忽略的事:

  • 用途:证书要标着"可用于服务器认证"(EKU serverAuth),否则不能拿来当网站证书。
  • 吊销:证书可能被提前作废。检查方式有 CRL(下载黑名单)和 OCSP(在线问一句)。现实中很多客户端采用"软失败",查不到就放行,所以吊销并不总是可靠。

接下来三个缩写,各自解决一个具体麻烦。

SNI(Server Name Indication,服务器名称指示)解决"一个 IP 上放很多网站"的问题。客户端在 ClientHello 里明说"我要访问 example.com",服务器据此挑对应的证书。代价是:SNI 是明文,旁观者能看出你访问了哪个站点。

ALPN(Application-Layer Protocol Negotiation,应用层协议协商)解决"谈完 TLS 之后说什么协议"。浏览器列出 h2、http/1.1,服务器挑一个。没有它,双方只能退回 HTTP/1.1。

ECH(Encrypted Client Hello,加密客户端问候)解决 SNI 泄露。做法是把真正的 ClientHello 包在一个外层 ClientHello 里加密,公钥从 DNS 的 HTTPS 记录里取。中间人只能看到外层那个假域名。

会话恢复是为了省掉重复的握手。老办法有两种:

  • Session ID:服务器记住会话状态,给客户端一个编号。缺点是服务器要存东西,机器一多就难同步。
  • Session Ticket:服务器把状态加密后交给客户端保管,下次带回来。服务器不用存,但票据本身要保护好。

TLS 1.3 统一用 PSK 票据,0-RTT 就是建立在它之上的。

8.7攻击面与抓包可见性

下面只讲原理,不讲操作。

攻击原理防御
降级攻击篡改握手包,逼双方退回老版本再打老漏洞版本号参与 Finished,服务器放降级哨兵
BEASTTLS 1.0 的 CBC 初始向量可预测TLS 1.1 起改用显式 IV
POODLE利用 CBC 填充错误做"填充预言"彻底关闭 SSL 3.0
心脏出血心跳扩展没检查长度,可读服务器内存升级 OpenSSL

中间人攻击(Man-in-the-Middle)稍微不同:攻击者站在中间,两边各建一条 TLS。它能成功的前提只有一个——客户端信任了攻击者的证书。

那抓包到底能看到什么?

能看到看不到
目标 IP 和端口请求路径和参数
SNI 里的域名请求头、Cookie
证书内容(TLS 1.2 明文)响应正文
数据包大小、时间、方向具体业务字段

如果拿到会话密钥,比如浏览器导出的 keylog 文件,Wireshark 就能解密。这就是第 10 章要讲的事。

8.8三个常见误区

误区一:HTTPS 就一定安全。

小锁只代表"这条连接是加密的、证书链是通的"。钓鱼站、诈骗站一样能用免费证书。安全与否要看对面是谁、你输出了什么。

误区二:TLS 1.3 的 0-RTT 没有风险。

0-RTT 数据可以被重放。同一个"下单"请求被重放十次,可能就下十单。所以服务端必须只对幂等操作开放 0-RTT。

误区三:证书有效就代表网站正规。

域名验证型(DV)证书几分钟就能自动签发,只证明"你控制了这个域名",不证明公司真实存在。要判断主体,得看组织验证(OV)或扩展验证(EV)证书,而这两类现在也很少被浏览器特殊展示。

自测:TLS 为什么需要两个随机数,一个不够吗?
不够。随机数的作用是给密钥加盐,保证每次连接算出的钥匙都不同。只有一个随机数时,熵变少,而且无法同时绑定双方。两个随机数各由一方生成,任何一方都无法单独控制最终结果,旧握手也无法被原样重放。
自测:为什么 ECDHE 比 RSA 密钥交换更安全?
RSA 密钥交换把预主密钥用服务器公钥加密后发过去。谁录了流量,将来拿到服务器私钥就能全解。ECDHE 用临时密钥对,用完即弃,服务器私钥泄露也解不开历史流量,这就是前向保密。
自测:TLS 1.3 的 nonce 为什么不会重复?
nonce 等于静态 IV 异或序列号,而序列号在同一条连接里每发一条记录就加一,只增不减。跨连接时密钥是重新派生的,所以旧密文在新连接里解不开。重放会在认证标签校验时失败。
自测:抓到 TLS 流量,为什么还能看到域名?
因为 SNI 在 ClientHello 里,TLS 1.2 和 1.3 默认都是明文。此外目标 IP、端口、包大小和时间也可见。要看具体内容,必须拿到会话密钥,或者让客户端信任你的代理证书。ECH 正是为了把 SNI 也藏起来。
自测:会话票据(Session Ticket)解决了什么问题,代价是什么?
解决服务器要保存大量会话状态的问题:状态被加密后交给客户端保管,服务器无状态。代价是票据本身成了敏感数据,泄露或被伪造就可能被用来恢复会话,所以票据必须加密并绑定有效期。
动手练
  • 用浏览器开发者工具的"安全"面板,点开证书,找到 SAN、有效期、签发者三项。
  • 打开一个只支持 TLS 1.3 的站点,对比另一个老站点,观察握手耗时差异。
  • 用一句话向别人解释:为什么装了代理证书就能看到 HTTPS 内容。
  • 列出你手机上三个必须登录的 App,猜它们有没有做证书固定。
握手Handshake建立连接前的谈判:换随机数、验身份、算钥匙。
前向保密Forward Secrecy长期私钥泄露也解不开历史流量。
预主密钥Pre-Master Secret双方协商出的 48 字节秘密,密钥派生的起点。
AEAD认证加密一次调用同时完成加密和防篡改的算法族。
nonce一次性数字同一把钥匙下只能用一次,重复就会失去安全性。
SNI服务器名称指示握手时明说访问哪个域名,默认不加密。
ALPN应用层协议协商在 TLS 里谈好之后用 h2 还是 http/1.1。
0-RTT零往返复用旧票据提前发数据,快但可被重放。
你现在应该能回答 TLS 防的是哪三件事?握手为什么要两个 RTT?三个随机数各起什么作用?密钥从预主密钥怎么一步步变成加密钥匙?TLS 1.3 为什么能压到 1 个 RTT?nonce 为什么不会重复?如果这几个问题你能用自己的话讲出来,抓包时看到握手包就不会发懵了。
第 9 章 · 协议

序列化格式:JSON 之外的二进制世界

同样一句"张三二十岁",可以写成一张表、一封信,也可以写成一段只有约定好的人才能读的密码。这三种写法就是序列化的三个流派。看懂它们的取舍,你才能在抓包里认出那堆字节到底是什么。

JSONXML / XXEProtobuf MessagePack表单编码自定义协议
先记住这一句 序列化就是把内存里的结构,按一套约定翻译成字节。可读性、体积、解析速度三样东西永远互相拉扯,没有全能选手。选格式,本质是选你愿意在哪一项上吃亏。
人话版 同一份信息有好几种写法:写给人看的、写给机器看的、写得最省地方的。这一页教你怎么一眼认出对方用的是哪一种。

9.1序列化:把一张表变成一串字节

先做一个思想实验。你要把"张三,20 岁,VIP"这条信息发给另一台电脑,有几种写法?

  • 写成表格:一行三列,谁都看得懂,但占地方。
  • 写成一封信:有称呼、有格式、有落款,啰嗦但清楚。
  • 写成一段密码:最短,但只有事先约好暗号的人能读。

这三种写法,分别对应今天要讲的三个流派:文本格式、标记语言格式、二进制格式。

为什么不能直接把内存里的东西发过去?两个原因:

  • 不同机器存同一个数字的字节顺序可能相反,这叫字节序(Endianness)。
  • 内存里有"指针"这种东西,它记录的是本机地址,换台机器就完全失效。

所以必须先翻译成一套双方都认的中间形式,这个过程就叫序列化(Serialization),反过来叫反序列化(Deserialization)。

同一份信息的三种序列化写法 一份信息 张三 20 岁,VIP JSON:写成表格 {"name":"张三","age":20} XML:写成一封信 <name>张三</name> 二进制:写成密码 0A 03 E5 BC A0 10 14 好读,占地方 更啰嗦,但能表达结构 最省,但必须约定 同一份内容,三种体积:约 30 字节、约 60 字节、约 7 字节
图 9-1 同一份信息的三种写法,差别在可读性和体积之间怎么取舍

评价一种格式,看四个指标:可读性、体积、解析速度、跨语言支持。它们没法同时最优。

格式可读性体积典型场景
JSON高中网页接口、开放平台
XML高大企业系统、配置文件
Protobuf低小App 接口、微服务
MessagePack低较小缓存、消息队列

9.2JSON:最好读,也最容易踩坑

JSON(JavaScript Object Notation)只有六种值:对象 {}、数组 []、字符串 ""、数字、true / false、null。就这么点,所以谁都能在十分钟内学会。

{
  "user": {
    "name": "张三",
    "age": 20,
    "tags": ["vip", "new"]
  },
  "ok": true
}

好读是优点,但下面四个坑,几乎每个新手都会踩一次。

坑一:数字精度

JSON 规范没有规定数字用几位存。绝大多数语言的解析器把它读成 IEEE 754 双精度浮点数。这种格式能精确表示的整数上限是 2 的 53 次方,也就是 9007199254740992。

超过这个范围,末尾就开始丢。比如 9007199254740993 读进来会变成 9007199254740992。

小数更明显。0.1 加 0.2,在 JSON 里序列化出来是 0.30000000000000004。这不是 JSON 的错,是浮点数的错,但 JSON 把它暴露在你面前。

注意 用户 ID、订单号、金额、时间戳这类不能丢精度的字段,一律用字符串传。看到接口里 ID 是 "1234567890123456789" 而不是数字,不是对方偷懒,是在保护精度。

坑二:重复键

{"a":1,"a":2} 是合法 JSON。不同解析器的行为不一样:多数保留最后一个,有的保留第一个,有的直接报错。

这个差异可以被利用。安全设备看到的是第一个值,后端业务代码用的是最后一个值,两边判断不一致,这叫参数污染。写接口时,遇到重复键应该直接拒绝,而不是默默取一个。

坑三:注释和尾逗号

标准 JSON 不允许注释,也不允许最后一项后面多一个逗号。JSON5、JSONC 是扩展语法,配置文件里常见,但接口里别混用。

坑四:看起来像数字的字符串

"1e3" 带引号是字符串,1e3 不带引号是数字 1000。很多解析器对科学计数法的处理还有细微差别。跨语言传数据时,能写清楚就别用简写。

9.3XML 与 XXE

XML(Extensible Markup Language,可扩展标记语言)用标签嵌套表达结构。它比 JSON 啰嗦,但多出三样东西:属性、命名空间、DTD。

<order id="1001">
  <item sku="A-01">键盘</item>
  <price currency="CNY">199</price>
</order>

命名空间(Namespace)解决"两个不同的 title 打架"的问题。用 xmlns 声明前缀,前缀不同的标签就是不同东西。这在拼接多个系统时非常必要。

DTD(Document Type Definition,文档类型定义)是更老的一套约束语法,可以声明元素、属性,也可以声明实体。问题就出在实体上。

XXE(XML External Entity,XML 外部实体)的原理只有一句话:DTD 里的实体可以指向外部资源,解析器会主动去取。

<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>

解析器读到 &xxe; 时,会去读那个文件,把内容填进来。如果解析结果被回显,攻击者就读到了服务器文件。把 file 换成 http 地址,还能让服务器替你发请求,也就是 SSRF。

防护很直接:解析时禁用 DTD、禁用外部实体、禁用外部 DTD;用带安全默认值的解析库;不要用 XML 承载不可信输入。三条里做到第一条,绝大多数 XXE 就没了。

9.4Protobuf:字段编号才是身份

Protobuf(Protocol Buffers)是 Google 的二进制格式。核心思想:双方先约定一份 .proto 文件,写清字段名和编号,编译成代码。传输时只传编号和值,不传字段名。

message Login {
  string username = 1;
  int32  age      = 2;
  bool   vip      = 3;
}

编码规则一:varint。每个字节用低 7 位存数据,最高位表示"后面还有没有"。

  • 150 的二进制是 10010110,拆成 7 位一组:0010110 和 0000001。
  • 低位组在前,每个字节补上延续位:0x96 0x01。
  • 所以 150 只占 2 字节,而不是 4 字节。小数字特别省。

编码规则二:tag。每个字段前面有一个 tag 字节,算法是:tag = 字段编号 左移 3 位,再或上线类型(wire type)。

线类型用于后面跟什么
0int32 / int64 / bool / enumvarint 值
1fixed64 / double固定 8 字节
2string / bytes / 嵌套消息长度 + 内容
5fixed32 / float固定 4 字节

拿 username="abc"、age=150 举例:

  • 字段 1 是字符串,tag = (1 左移 3) 或 2 = 0x0A;长度 0x03;内容是 61 62 63。
  • 字段 2 是 int32,tag = (2 左移 3) 或 0 = 0x10;值是 96 01。
  • 完整字节:0A 03 61 62 63 10 96 01,一共 8 字节。
Protobuf 字节布局与 varint 编码 0A 字段 1 的 tag 03 长度 3 61 62 63 abc 的 ASCII 10 字段 2 的 tag 96 01 150 的 varint 同样的内容写成 JSON 要 30 多字节,这里只用 8 字节 150 = 1001 0110 原始二进制 0000001 / 0010110 拆成 7 位一组 0x96 0x01 低位组在前 varint 的妙处:数字越小占的字节越少,1 字节到 10 字节都可能
图 9-2 Protobuf 的 tag 加 varint:字段编号决定"这是什么",varint 决定"占几个字节"

向后兼容规则,这几条必须背下来:

  • 字段编号一旦发布就不能改,也不能复用。删掉的编号要写进 reserved,防止后人误用。
  • 可以新增字段,老程序会忽略不认识的编号,不会报错。
  • 不能把 int32 改成 string,线类型变了就解不出来。
  • 1 到 15 号字段的 tag 只占 1 字节,留给最常用的字段。
补充 Protobuf 的字段名不上线,改名不影响兼容。真正决定身份的是编号。这也是为什么逆向时看到 0A 03 这样的字节,必须拿到 .proto 才能还原出含义。

9.5MessagePack 与 CBOR

这两个可以理解为"二进制版 JSON":不需要 .proto,自己带类型信息,所以拿到字节就能解,但体积比 Protobuf 大一点。

对比项JSONMessagePackCBOR
可读性人直接能读需要工具需要工具
体积大小小
是否要 schema不要不要不要
二进制数据要 Base64原生支持原生支持

CBOR(Concise Binary Object Representation)是 IETF 的标准,物联网和 COSE 体系里常见。MessagePack 在缓存、消息队列、游戏后端里用得多。

9.6Thrift、Avro 与两种表单编码

Thrift:Facebook 出身,自带 RPC 框架,用 IDL 定义服务和数据结构,一次生成多语言代码。定位是"协议加远程调用一整套"。

Avro:Hadoop 生态出身,特点是 schema 随数据一起存,读的时候用写入方的 schema 解释。适合大数据批处理和历史数据兼容。

再说网页表单。它有两种编码,很多人只知道第一种。

  • application/x-www-form-urlencoded:拼成 a=1&b=2,特殊字符做百分号编码,空格变成加号。只能传文本。
  • multipart/form-data:用一段 boundary 分隔多个部分,每部分有自己的头,可以传二进制。文件上传必须用它。

抓包时看到 Content-Type 是哪一个,就知道该用哪种方式去解析请求体。

9.7自定义二进制协议怎么设计

很多 App 不用现成格式,自己定一套。典型结构长这样:

自定义二进制协议的字节布局 magic 4 字节 版本 1 标志 1 类型 1 长度 4 序列号 4 payload N 字节 校验和 4 一条消息的全部字段,按固定顺序排列 4D 59 50 54 01 00 02 00 00 00 1A 00 00 00 07 magic 是 M Y P T 四个字母,长度写 0x1A 表示后面还有 26 字节 长度字段必须有:TCP 是字节流,没有边界,不写长度就不知道一条消息在哪结束 字节序、校验范围、字段对齐方式,都要在文档里写死
图 9-3 自定义协议的常见骨架:magic 认门、长度划界、校验和兜底

每个字段都有理由:

  • magic:固定几个字节,用来一眼判断"这是不是我的包"。抓错端口时能立刻发现。
  • 版本:以后改协议时,老客户端还能被识别出来。
  • 标志位:一位表示是否压缩,一位表示是否加密,省得再加字段。
  • 类型:这条消息是登录、心跳还是拉数据。
  • 长度:告诉对方 payload 有多少字节。
  • 序列号:对应答、防重放、排查丢包。
  • 校验和:CRC32 之类,快速发现传输错误。
注意 长度字段是这类协议最容易出问题的地方。如果解析代码信任了长度却不去核对实际剩余字节,攻击者发一个"长度写成 10 亿"的包,就能让程序申请巨大内存或越界读取。先校验,再分配。

9.8怎么认出一个未知格式

拿到一段来路不明的字节,按这四步走,通常十分钟内能定性。

STEP 1

看可打印字符比例

把字节按 ASCII 看一遍。文本格式通常九成以上是可打印字符;二进制格式里会夹杂大量 00 到 1F 的控制字符。

STEP 2

算熵

熵衡量"有多乱"。压缩或加密的数据接近每字节 8 比特;结构化二进制通常在 3 到 6 之间;纯文本更低。

STEP 3

找 magic bytes

看开头 16 字节。很多格式有固定文件头,比如 PNG 开头是 89 50 4E 47。对不上已知表,就说明是私有格式。

STEP 4

试长度字段

找连续的 4 字节,看它的值是否约等于"剩余长度"。抓多个不同大小的包对比,如果这个位置的数值随包长同步变化,基本就是长度字段。

还有两个小技巧:看有没有规律重复的字节(可能是分隔符),看末尾是不是固定长度的一段(可能是校验值)。多个包一起看,比盯着一个包有效得多。

9.9两个必须记住的误区

误区一:JSON 字段顺序不重要,所以签名时也不用管顺序。

JSON 对象在语义上确实无序,但签名是对字节做的。序列化时字段顺序一变,字节就变,签名就对不上。所以签名前必须约定规范化规则:键按字典序排、去掉多余空格、数字格式统一、Unicode 转义统一。这类约定通常叫规范化(Canonicalization)。

误区二:Protobuf 字段名改了没关系,反正传的是编号。

前半句对,后半句对,但结论危险。字段名确实不上线,改名不影响兼容。可是字段编号绝对不能改、不能复用。改了编号,老客户端会把新字段当成另一个字段解析,数据直接错位,而且不会报错,只会静默出错。

自测:为什么用户 ID 常常用字符串传,而不是数字?
因为 JSON 数字在多数语言里被解析成双精度浮点,能精确表示的整数上限是 2 的 53 次方。19 位的用户 ID 会丢末尾几位,导致查错人。用字符串传是保护精度,不是格式洁癖。
自测:Protobuf 的 tag 字节是怎么算出来的?
tag = 字段编号左移 3 位,再或上线类型。比如字段 1 是字符串,线类型 2,tag 就是 (1 左移 3) 或 2 = 0x0A。线类型决定后面怎么读,编号决定这是哪个字段。
自测:XXE 能读到服务器文件,根因是什么?
XML 的 DTD 允许定义指向外部资源的实体,解析器会主动去取并展开。根因是解析器默认信任了文档里的声明。防护就是禁用 DTD 和外部实体。
自测:自定义协议为什么一定要有长度字段?
因为 TCP 是字节流,没有消息边界。没有长度字段,接收方就不知道一条消息在哪结束,只能靠分隔符猜,而二进制数据里任何字节都可能出现。长度字段把"流"切成"消息"。
自测:熵很高说明什么?
说明数据分布接近随机,通常是压缩过或加密过。熵接近每字节 8 比特时,基本可以判断不是明文结构。但要注意:熵高只能说明"乱",不能说明"用了什么算法"。
动手练
  • 把一段 JSON 手动改成一个重复键,用两种不同的解析器读,看结果是否一致。
  • 把数字 300 手算成 Protobuf 的 varint 字节,再对照 0xAC 0x02 验证。
  • 用十六进制编辑器打开一张 PNG,找到开头 8 个字节,记下它们。
  • 抓一个上传文件的请求,看 Content-Type 是不是 multipart,找到 boundary 那一行。
序列化Serialization把内存结构翻译成可传输、可保存的字节。
varint变长整数小数字占 1 字节、大数字才变长的编码方式。
线类型Wire TypeProtobuf 里表示"后面这段怎么读"的 3 位标记。
XXEXML 外部实体利用 DTD 实体读取服务器文件或发起请求。
magic bytes魔数文件或协议开头固定的几个字节,用来快速识别格式。
熵Entropy衡量数据有多乱,压缩和加密数据接近每字节 8 比特。
规范化Canonicalization签名前把数据整理成唯一确定的字节形式。
boundary分隔串multipart 表单里用来分隔各部分的随机字符串。
你现在应该能回答 序列化到底在解决什么问题?JSON 的数字精度坑在哪?XXE 的根因是什么?Protobuf 为什么只传编号不传字段名?字段编号为什么不能改?自定义协议里长度字段为什么必须有?这六个问题答得出来,你看到任何一段陌生字节都不会慌。
第 10 章 · 协议

抓包与工具链:先看得见,才谈得上分析

分析协议的第一步不是读代码,是先把流量完整地看见。这一页讲清楚抓包发生在哪三个位置、每件工具适合干什么、为什么装了证书还是抓不到,以及一条从"打不开网页"到"证书固定"的排查路线。

tcpdumpWireshark代理与证书 mitmproxyAndroid 证书排查决策树
先记住这一句 抓包不是"装个软件就完事",而是在链路上选一个位置,让流量必须经过你。位置选错,工具再好也只能抓到空气;证书没被信任,抓到的全是乱码。
人话版 想看清别人寄的信,你得站在邮局门口,还得让他愿意把信交给你拆。这一页讲的就是:站哪儿、怎么让他愿意、拆不开时先查哪一步。

10.1抓包的三个位置

流量从手机到服务器,一路上有很多个"可以下手"的点。按距离远近,分成三类。

抓包的三个位置:设备内、代理、链路 ① 设备内抓包 抓包 App / VPN 模式 ② 代理抓包 中间人,可解 TLS ③ 链路抓包 网卡 / 路由器镜像 手机 App 内部 最贴近源头 代理 中间人 能看到明文 服务器 终点 不归你管 链路抓包看到的是线路上真实跑的东西,加密的就是加密的 代理抓包能解密,但前提是客户端愿意信任代理的证书 三者互补,不是替代关系
图 10-1 三个抓包位置:越靠近设备越容易看到明文,越靠近链路越原始
位置能看到什么代价
设备内App 发出的请求、部分本地日志受系统限制,非 root 权限有限
代理解密后的完整请求和响应必须让客户端信任代理证书
链路原始数据包、时间、大小加密内容仍然看不懂

10.2tcpdump:最基础的命令行抓包

tcpdump 几乎每台 Linux 都自带。它没有界面,输出是一行行文本,但正因为轻,服务器上排查问题首选它。

参数作用
-i any指定网卡,any 表示全部网卡
-n / -nn不把 IP 反解成域名;两个 n 连端口也不反解
-s 0抓完整包,不截断
-w cap.pcap写入文件,留给 Wireshark 慢慢看
-c 100抓满 100 个包就停
-A / -X以文本 / 十六进制加文本显示内容

过滤表达式用的是 BPF 语法,写在命令最后,用单引号包住:

# 只抓和某台服务器有关的 443 流量,存成文件
tcpdump -i any -nn -s 0 -w cap.pcap 'tcp port 443 and host 203.0.113.10'

# 排除噪音:不要 SSH,不要 DNS
tcpdump -i any -nn 'not port 22 and not port 53'

# 只看带数据的包,过滤掉纯 ACK
tcpdump -i any -nn -A 'tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)'

最后那条看着吓人,其实只做一件事:算出这个包里实际有多少字节的应用数据,大于 0 才显示。纯确认包不带数据,就被过滤掉了。

技巧 先用 -w 存文件,再用 Wireshark 分析。直接在终端里看,滚动一快就什么都抓不住,而且没法回溯。

10.3Wireshark:把包拆开看

Wireshark 是图形化的协议分析器。它的核心不是"抓",而是"拆"和"筛"。

先分清两套过滤器:抓包过滤器(Capture Filter)在抓之前生效,语法和 tcpdump 一样;显示过滤器(Display Filter)在抓之后生效,功能强得多,能按协议字段筛。

显示过滤器含义
ip.addr == 203.0.113.10只显示和这个 IP 有关的包
tcp.port == 443只显示 443 端口
http.request.method == "POST"只看 POST 请求
tls.handshake.type == 1只看 ClientHello

三个必须会用的操作:

  • Follow TCP Stream:右键任意一个包,选它,就能把这条连接的双向数据按顺序拼起来。看 HTTP 请求响应最快的方法。
  • Statistics 里的 Conversations:一眼看出谁和谁通信最多、各传了多少字节。找"可疑外连"从这里开始。
  • Expert Information:自动标出重传、乱序、握手失败。很多"网络慢"的问题,答案在这里。

Wireshark 还能解密 TLS,前提是你有会话密钥。浏览器和部分程序支持把密钥写进一个环境变量指定的文件(常叫 SSLKEYLOGFILE),在 Wireshark 的 TLS 协议设置里填上这个文件路径,就能看到明文。

注意 这个 keylog 文件等于万能钥匙:拿到它就能解开对应会话的全部内容。它只在你能控制客户端时才有用。别人的 App 不会给你写这个文件,这正是证书固定要防的事。

10.4代理为什么能解密

代理(Proxy)的本质是中间人:客户端以为在跟服务器说话,其实在跟代理说话;服务器以为在跟客户端说话,其实也在跟代理说话。

HTTPS 代理的 CONNECT 隧道与证书签发 客户端 代理 真服务器 ① CONNECT 目标:443 ② 隧道建立成功 ③ 客户端发起 TLS 握手 ④ 代理现场签一张证书 ⑤ 代理另建一条 TLS ⑥ 拿到真实响应 两段加密在代理处交汇,代理看到的是明文 前提只有一个:客户端信任代理的根证书
图 10-2 代理解密的两段式结构:客户端到代理一段,代理到服务器一段

把过程拆成六步:

  • 客户端配置了代理地址,比如 127.0.0.1:8080。
  • 访问 HTTPS 前,客户端先发一条 CONNECT example.com:443,请求开一条隧道。
  • 代理回 200,隧道建立。此时还只是裸 TCP。
  • 客户端在隧道里发起 TLS 握手,它以为对面是 example.com。
  • 代理当场生成一张 example.com 的证书,用自己 CA 的私钥签,发给客户端。
  • 客户端验证签发者:如果用户装过代理的 CA,就通过。握手完成,代理拿到明文。

代理再和真服务器建一条独立的 TLS。于是链路上有两段加密,而代理手里同时握着两把钥匙。

关键点只有一句:客户端必须信任代理的 CA。否则第 5 步就会报证书错误,连接直接断开。

10.5mitmproxy、Charles、Fiddler 怎么选

工具定位特点
mitmproxy命令行加 Python 脚本可编程,适合自动化、批量改包
Charles图形界面上手快,查看和重发方便,收费
FiddlerWindows 老牌规则脚本强,Classic 版免费

选型不用纠结:手工看几个请求,Charles 最快;要写脚本批量处理、做自动化,mitmproxy 更合适;只在 Windows 上临时用,Fiddler 够。

10.6Android 的两套证书,以及 Android 7 的分水岭

Android 里有两个证书库,地位完全不同:

  • 系统证书:预装在系统分区里,出厂就有,普通应用改不了。
  • 用户证书:用户自己安装的,存在用户数据区。

Android 7(API 24)之前,应用默认两者都信任。所以那个年代,装上代理的 CA 就能抓包。

Android 7 之后,规则变了:应用默认只信任系统证书,用户证书被忽略。除非应用在自己的网络安全配置里显式声明信任用户证书,或者这个应用是可调试的。

补充 这就是"装了证书还是抓不到"的最常见原因。不是你装错了,是系统按设计忽略了它。至于把证书放进系统分区的具体做法,涉及修改系统,这里只讲原理,不展开操作。

10.7抓不到包,按这个顺序查

不要一上来就怀疑工具。按下面的顺序,一步一停,能省掉大量时间。

抓不到包的排查决策树 ① 手机本身能上网吗 打开网页试试 先修网络 WiFi、DNS、代理地址 不能 能 ② 请求到代理了吗 看代理窗口有没有记录 App 可能忽略系统代理 试透明代理或改抓包位置 没有 有 ③ 证书被信任了吗 看有没有 TLS 握手失败 Android 7 以上要放系统证书 或改应用的信任配置 失败 通过 ④ 是不是证书固定 / QUIC 握手秒断,或走 UDP 443 固定要 hook,QUIC 要关 非 HTTP 协议另找入口 是 都不是 应该已经抓到了
图 10-3 抓不到包时按这个顺序查,每一步都能排除掉一大类原因

再补两句容易忽略的:

  • QUIC:HTTP/3 走 UDP 443,普通 HTTP 代理不接管 UDP,所以流量直接绕过去了。表现是"网页能开,但代理里一条记录都没有"。
  • 非 HTTP 协议:WebSocket、gRPC、自定义 TCP,代理不一定认得。得回到链路抓包,或者直接看代码。

10.8无线抓包与 USB 抓包

无线抓包:网卡要支持监听模式(Monitor Mode),才能收到空口上所有帧,而不只是发给自己的。WPA2 加密的网络,需要抓到四次握手并且知道密码,才能解出内容。只能用于自己拥有或获授权的网络。

USB 抓包:用 USBPcap 之类的工具,看手机和电脑之间的 USB 通信。主要用于分析 iOS 的备份、同步协议,以及某些走 USB 的外设通信。

10.9合法性:这一节比技术更重要

红线 只抓自己的设备,只抓书面授权的目标。抓包文件里往往包含他人的账号、位置、聊天记录,属于个人信息。最小必要,能不留就不留,能哈希就不存明文,不要上传到公共平台。

技术能力不等于许可。同样的工具,用在自家 App 上是调试,用在别人系统上就是入侵。这条线不因为"我只是看看"而移动。

10.10一个最大的误区

装上证书就能抓到所有包。

证书只解决"信任"这一环。它管不了下面这些:

  • 应用自己做了证书固定,只认特定证书,代理的证书直接被拒;
  • 应用走 QUIC,代理根本不接管;
  • 应用用的是非 HTTP 协议,代理不认识;
  • 应用在应用层又加了一层自己的加密,解开 TLS 之后还是密文。

所以"抓不到"是个多层问题,得按 10.7 的顺序一层层排。

自测:代理为什么能解密 HTTPS,而链路抓包不能?
代理站在中间,和客户端、服务器各建一条独立的 TLS,两边都握过手,所以它同时握着两把会话密钥。链路抓包只是被动复制数据包,没有参与握手,拿不到密钥。解密的前提是"参与",不是"看见"。
自测:Android 7 之后,为什么用户证书默认不生效?
系统把"信任用户证书"改成了需要应用显式声明的选项,默认只信任系统预装的 CA。这样即使用户被诱导安装了恶意 CA,也不会影响所有应用。安全默认值提高了,抓包成本也就上去了。
自测:代理里一条记录都没有,最先怀疑什么?
先怀疑请求根本没走代理:应用可能忽略系统代理设置,或者走了 QUIC(UDP 443)让代理无法接管。先确认"流量到没到",再谈"能不能解密"。
自测:显示过滤器和抓包过滤器有什么区别?
抓包过滤器在抓之前生效,只能按链路层、IP、端口这类粗粒度条件筛,语法是 BPF;显示过滤器在抓之后生效,能按任意协议字段筛,改条件不用重抓。先用抓包过滤器减量,再用显示过滤器精筛。
自测:为什么说 keylog 文件等于万能钥匙?
它记录了每条 TLS 会话派生出的密钥材料。有了它,任何一份同期抓到的密文都能被解开,而且不需要再参与握手。所以它必须像私钥一样保管,用完就删。
动手练
  • 在自己电脑上跑一次 tcpdump -i any -nn -c 20,认出每行里的源、目的、端口。
  • 用 Wireshark 打开任意一个 pcap,试一次 Follow TCP Stream。
  • 访问一个网站,用显示过滤器只看 ClientHello,找到里面的 SNI。
  • 把自己手机连上自己的代理,装好证书,抓一次自己的请求,再卸载证书。
抓包Packet Capture把链路上来回的数据原样记录下来。
BPF过滤语法tcpdump 和抓包过滤器用的表达式语言。
显示过滤器Display FilterWireshark 抓完之后按协议字段筛选的语法。
CONNECT隧道请求让代理开一条裸 TCP 隧道,HTTPS 走它。
中间人MITM站在两端之间,各建一条连接,从而看到明文。
keylog密钥日志记录 TLS 会话密钥的文件,可用来解密抓包。
QUIC快速 UDP 传输HTTP/3 的底层,走 UDP,普通代理不接管。
证书固定Pinning应用只认指定证书,代理证书会被拒绝。
你现在应该能回答 抓包有哪三个位置,各自能看见什么?tcpdump 的 -w 和 -s 0 各是什么意思?代理为什么能解密?Android 7 改了什么?抓不到包时第一步查什么?如果这五个问题你能顺下来,遇到"抓不到"就不会乱试了。
第 11 章 · 逆向方法论

协议逆向方法论:从黑盒到可复现

你抓到一条登录请求,里面有二十多个字段,sign 是一串看不懂的十六进制,ts 每次都变。凭感觉试了三天,偶尔能通一次,但说不清为什么。问题不在技术,在于没有方法。这一页给一套可以照着做的实验流程:怎么拆、怎么比、怎么验证、怎么记。

黑盒 / 灰盒 / 白盒八步工作流输入输出清单 差异对比对拍 Oracle实验记录
先记住这一句 协议逆向不是"猜中一次",而是用最少的实验,把一条请求里的每个字段都变成可解释、可预测、可复现的东西。判断你有没有学会,标准只有一个:给你一个没见过的输入,你能不能算出正确输出。
人话版 这一页教你把拆请求当成做实验:先数清有几个变量,再一次只改一个,记下变化,最后能自己把签名算出来。

11.1三种盒子:你手上有什么,决定你从哪下手

同样一个 App,三个人手里的东西可能完全不同:一个人只有一段抓包,一个人有安装包,一个人有源码。这三种起点,做法差得很远。用透明度打个比方,就是黑盒、灰盒、白盒。

  • 黑盒(Black Box):只有一个输入口和一个输出口,里面完全看不见。你只能发请求、看响应,靠对比猜规律。
  • 灰盒(Grey Box):外壳是半透明的。你有安装包,能反编译看到类名、字符串、调用关系,但看不到完整源码,也看不懂所有细节。
  • 白盒(White Box):全透明。你有源码,或者有带完整符号表的二进制,逻辑直接摆在眼前。
黑盒、灰盒、白盒三种起点 黑盒 只有流量 抓到什么就看什么 从字段对比开始猜 灰盒 有安装包 能反编译看结构 能看到字符串和调用 白盒 有源码或符号 逻辑直接可读 重点变成确认版本 盒子越白,越省时间;但白盒也会骗人,源码和线上包常常不是同一版 不管从哪个盒子出发,最后一步都必须用真实流量对拍
图 11-1 三种起点的区别不在难度,而在"你能直接看到多少",以及"还有多少必须靠实验补"
盒子你手上有什么典型起点常见卡点
黑盒一段抓包,没有安装包字段清单 + 差异对比字段多、没有参照,容易瞎猜
灰盒APK 或 IPA,能反编译先看字符串和类名,再回到流量代码被混淆,关键逻辑在 native 层
白盒源码,或带符号的二进制直接读签名函数版本不一致,或线上走了另一条分支

真实项目里,九成情况是灰盒:有安装包,但代码被混淆过,关键算法还塞进了 .so。所以这一页的方法默认按灰盒写,黑盒和白盒都能套用。

补充 盒子颜色只决定起点,不决定终点。黑盒也能做到完全复现,只是要多做几十次实验;白盒也可能做不出来,因为你读的那份代码不是线上跑的版本。

11.2八步工作流:顺序错了,全是返工

下面这八步是一条固定流水线。它的核心不是"步骤多",而是前四步只观察、不动手。绝大多数人失败,是因为第一步还没定清楚,就直接跳到第五步去脱壳了。

协议逆向的八步工作流 ① 定目标 要复现哪一步 ② 抓样本 同一操作抓三次 ③ 拆请求 头、体、路径分开 ④ 找变量 哪个字段在变 ⑤ 定位算法 静态加动态 ⑥ 复现 自己算一遍 ⑦ 对拍 和真机逐字节比 ⑧ 固化 脚本加记录 前四步只做观察,第五步才允许动手;顺序颠倒是最常见的返工原因 每一步都要留下可复查的产物,否则下次只能从头再来
图 11-2 八步流水线:前四步把问题缩小,后四步才把答案做出来

每一步的产出物写清楚,你就知道自己在哪:

STEP 1

定目标:缩到一条请求

把"搞懂这个 App 的登录"改成"复现 /api/login 请求体里的 sign"。产出:一句可验收的话。

STEP 2

抓样本:同一操作抓三次

一次不够。三次才能区分"固定值"和"每次变"。产出:三份完整请求,含头和体。

STEP 3

拆请求:分块归档

路径、方法、头、体分开列。产出:一张字段清单,见 11.3。

STEP 4

找变量:标出会变的部分

哪些每次变、哪些跟着别的字段变。产出:变量分类表,见 11.5。

后四步是另一套节奏:定位算法靠静态看结构、动态看入参;复现是自己写代码算一遍;对拍是拿真机结果逐字节比对;固化是把结论写成脚本和文档,让别人也能跑。

注意 "定目标"这一步最常见的错误是目标太大。目标越大,实验越没法设计。一条请求里的一个字段,就是合适的目标粒度。

11.3输入输出清单法:把请求变成一张可实验的表

把一次请求想成一排开关。你要做的是:先数清有多少个开关,然后一次只拉一个,看灯怎么变。一次拉三个,灯变了你也不知道是谁干的。

先把所有字段抄成表。抄的时候不要漏:路径、方法、每个头、每个体字段,一个都不能少。

字段示例值长度三次抓包的变化
uid100865 位数字固定
ts172990000010 位数字每次加约 8
noncea7f3c9e18 位十六进制每次不同,无规律
sign9f2c...(32 位)32 位十六进制每次不同,随 ts 变

列完之后做四件事:

  1. 给每个字段标上"固定 / 时间 / 随机 / 依赖别人"四类之一;
  2. 挑一个字段,只改它,其他一个字节都不动,发出去;
  3. 记录响应的状态码、错误码、提示语;
  4. 把这次实验写进记录表(模板在 11.8)。

发出去之后,失败通常有三种,必须分清,否则你会把"签名错"当成"参数错":

  • 签名失败:服务端先说签名不对。说明这个字段参与了签名计算,你改它等于改了原文。
  • 业务失败:签名过了,但业务规则不满足,比如余额不足、验证码过期。
  • 风控失败:签名和业务都过,但请求被判定为异常行为而拒绝。

三者的错误码通常不同,提示语也不同。第一次遇到时就把它们记下来,后面能省大量时间。

技巧 改之前先把原始请求原样存一份,命名为"基线"。之后每次实验都和基线比,而不是和上一次比。和上一次比,误差会累积;和基线比,误差不会。

11.4差异对比法:让两次请求自己说出关系

人眼找规律很不可靠,但机器做 diff 很可靠。方法很简单:抓两次请求,让它们只差一个参数,然后把两份文本逐行对齐。

举个具体例子。同一个账号连续登录两次,除了 ts 之外完全一样。diff 的结果是:

第一次
ts=1729900000  sign=9f2c1a...
第二次(8 秒后)
ts=1729900008  sign=4b81ee...
只有 ts 和 sign 两行不同

这条 diff 直接告诉你两件事:sign 依赖 ts;sign 不是固定值。如果两次之间你还改了 uid,那就分不清是 ts 还是 uid 影响了 sign,这次实验就废了。

把这种关系一条条记下来,最后会得到一张依赖清单,长这样:

  • sign 依赖 ts(改 ts,sign 变);
  • sign 依赖 uid(改 uid,sign 变);
  • sign 不依赖 nonce(改 nonce,sign 不变);
  • 改 nonce 服务端也不报错,说明它可能只是日志用途。

这张清单就是算法的骨架。剩下的问题只是"用什么函数把这些字段拼起来"。

注意 "改了没反应"不等于"这个字段没用"。服务端可能宽容(不校验),也可能把它当可选字段。只能得出"当前这条路径不校验它",不能得出"它永远不参与计算"。

11.5时间戳与随机数剥离:先把"必然会变"的拿掉

很多人对拍永远对不上,不是算法错了,而是把一个时间戳当成常量算了。所以第三步就要把"每次都会变"的字段挑出来,单独处理。

常见的四种"会变"的字段,判断方法各不相同:

类型怎么判断典型样子
时间戳连抓三次,差值稳定,且接近真实时间间隔10 位十进制(秒)或 13 位(毫秒)
随机数长度固定,多次采样不重复,没有单调性8 到 32 位十六进制或字母数字
计数器严格递增,每次加 1 或固定步长小整数,如 1、2、3
会话标识同一会话内不变,重新登录后变长串字母数字,常见 32 位以上

时间戳的判定最实用,记住两个数字就够:

  • 10 位十进制,大致是"秒"级时间戳。2026 年前后,这个值在 17 亿左右(1.7 乘 10 的 9 次方)。
  • 13 位十进制,是"毫秒"级时间戳,就是秒级乘 1000。

把它除以 1000 再转成日期,如果落在这几天,基本可以确认。反过来,一个 10 位数字如果不落在合理时间范围,那它可能只是流水号。

"剥离"的意思是:对拍时把这些字段固定成同一个值。比如你在脚本里写死 ts=1729900000,然后用真机在同一秒抓到的请求做对比。两边输入完全一致,输出才有可比性。

补充 随机数字段要特别注意:如果它真的参与了签名,你的复现脚本必须能生成"服务端也认可"的随机数。多数情况下服务端只要求"格式合法、没被用过",不要求特定值,这时自己生成即可。

11.6从字段倒推算法:长度、字符集、分组边界都是线索

字段本身就是一张名片。看一眼长度和字符集,就能把候选算法从几十种缩到三五种。

你看到的可能的算法依据
32 位,只有 0-9a-fMD5,或 16 字节数据的十六进制16 字节乘 2 个字符
40 位,只有 0-9a-fSHA-120 字节乘 2
64 位,只有 0-9a-fSHA-25632 字节乘 2
44 位,末尾常有等号Base64 编码的 32 字节每 3 字节变 4 字符

字符集还能再细分一层:

  • 只出现 0-9 和 a-f,是小写十六进制;出现 A-F 就是大写十六进制,说明代码里做了 toUpperCase。
  • 出现 + 和 /,是标准 Base64;出现 - 和 _,是 URL 安全变体(URL-safe Base64)。
  • 固定前缀如 v1_、sig:,说明是自定义拼接格式,前缀通常不参与计算。

长度还能暴露"分组加密":如果一个字段的长度总是 16 的整数倍,而且明文长度不同、密文长度会跳变,那多半是 AES 这类分组密码(Block Cipher),块大小 16 字节。反过来,哈希的输出长度是固定的,无论输入多长都一样长。

最后一个关键实验是"雪崩":把输入改动一个字符,看输出变化多少。

  • 输出整段都变,约一半的位翻转:这是哈希或加密的特征。
  • 输出只有一小段变:更像编码、拼接或简单替换,比如 Base64、异或。
  • 输出完全不变:这个字段根本没进计算,或者被服务端忽略了。
注意 长度和字符集只能给出候选,不能给出结论。32 位十六进制也可能是"16 字节随机数",不一定是 MD5。必须用雪崩实验和真实对拍确认。

11.7对拍方法学:把服务器当成你的判卷老师

"对拍"这个词来自算法竞赛:写完程序,用一个肯定正确的慢程序跑同一组输入,比输出是否一致。在协议分析里,那个"肯定正确的慢程序"就是真实客户端加真实服务器。

对拍的核心概念叫 Oracle(预言机,这里指判定器):一个你能反复问"这个答案对不对"的东西。服务器就是最好的 Oracle,因为它会明确告诉你签名通过还是失败。

对拍有三种做法,解决的问题不一样:

方法一

同一输入,多次运行

输入完全不变,跑五次。输出每次一样,说明没有随机数;输出每次不同,说明有随机数或时间参与。

方法二

多设备、多账号运行

换一台手机、换一个账号,同样输入。结果变了,说明字段绑定了设备或账号。

方法三

逐字节比对

把两个结果按位置对齐,标出第几位开始不同。差异位置往往对应拼接顺序或字段长度。

这三种方法要配合"假设-验证"循环用。假设必须写成可以被证伪的句子,不能写成"应该是某种加密"这种模糊判断。

假设与验证的循环 提出假设 写成一句话 只改一个变量 其他全部不动 预测结果 先说会怎样变 做实验看结果 与预测比对 对不上就换假设 不要打补丁 连续三次对不上,说明假设错了,回到起点重提,而不是在错的假设上继续加条件
图 11-3 假设-验证循环:每次只改一个变量,先预测再实验,对不上就换假设

一个合格的假设长这样:

不好的假设
sign 应该是 MD5 之类的东西
好的假设
sign = MD5(uid + ts + key) 的小写十六进制
可证伪的预测:改 uid,sign 整段变化;改 nonce,sign 不变

写完预测,再去做实验。如果结果和预测一致,假设暂时成立;连续三次不一致,说明假设错了,回到起点重新提,不要在错的假设上继续加条件。

红线 对拍只在你自己的账号、你自己的设备、或书面授权的目标上进行。批量注册、绕过他人平台风控、对未授权系统发请求,都不属于方法讨论范围,也可能触犯法律。

11.8实验记录、常见死胡同与最大的误区

方法论里最不起眼、但最能救命的是记录。人的短期记忆只能装三四个变量,第五个开始就全靠猜。一张表就能解决:

时间改动预期实际与结论
10:20基线,不改登录成功成功,记录 sign 原值
10:26ts 加 1sign 整段变确实变了,ts 参与计算
10:33nonce 改一位sign 不变不变,nonce 不参与
10:41uid 改成别人签名失败报错 1003,uid 参与

四列就够。写的时候注意两点:一是"预期"必须写,不写预期的实验等于没做;二是"实际"要抄原文错误码,不要写"失败了"。

下面这些死胡同,几乎每个人都会掉进去一次:

  • 一上来就脱壳。还不知道要找什么,就先花两天脱壳,脱完发现关键逻辑根本不在被脱的那一层。
  • 只看静态。反编译出来的代码可能被混淆、可能走不到、可能线上根本不执行。静态给结构,动态给事实。
  • 把巧合当规律。改了 A 之后成功了一次,就认定 A 是关键。一次成功可能只是缓存、可能只是重试。
  • 同时改多个变量。这是最浪费时间的做法,实验做完什么也证明不了。
  • 不做记录。三天后回来,连自己改过什么都想不起来。
  • 用"环境问题"解释对不上。对不上就是没懂,先把差异定位到字节,再谈环境。

最后说这一页最重要的误区:很多人以为"能复现一次"就等于"搞懂了"。

能复现一次,可能只是照抄了一次成功请求,或者运气好碰上了。真正的标准有三个:

  1. 给你一个没见过的输入,你能算出正确输出;
  2. 你能解释每个字段为什么这么算,包括顺序、编码、大小写;
  3. 别人拿着你的记录,不用问你也能重跑一遍。

三条都满足,才叫可复现。差一条,都还停在"试出来了"的阶段。

技巧 每次收工前花五分钟做一件事:把今天的结论写成三句话,附上对应的实验编号。第二天开工先读这三句话,能省掉半小时的重新回忆。
自测:黑盒、灰盒、白盒,做法的差别到底在哪?
差别在"你能直接看到多少"。黑盒只能靠请求响应做对比,实验次数最多;灰盒能反编译看结构和字符串,可以用静态信息缩小范围;白盒直接读逻辑。但三种盒子最后都要用真实流量对拍,白盒也可能因为版本不一致而骗人。
自测:一个 32 位、只有 0-9a-f 的字段,能不能直接判定是 MD5?
不能。32 位十六进制只说明它是 16 字节数据的十六进制表示,可能是 MD5,也可能是 16 字节随机数、截断的 SHA-256、或某种自定义编码。必须做雪崩实验(改一位看输出是否整段变化)并用真实请求对拍,才能确认。
自测:两次请求只有 ts 不同,sign 也跟着变了,能得出什么结论?
能得出"sign 依赖 ts"。但得不出"sign 只依赖 ts",因为其他字段这两次没变,无法判断它们是否也参与。要确认依赖关系,必须逐个字段单独改,一次只改一个。
自测:为什么强调"一次只改一个变量"?
因为输出变化必须能归因。同时改三个字段,输出变了也不知道是哪个造成的,这次实验等于白做。一次一个变量,是把"猜"变成"证明"的唯一办法。
自测:能复现一次,算不算搞懂了?
不算。一次成功可能是照抄或巧合。真正的标准是:能对没见过的输入算出正确输出、能解释每个字段的算法、别人能照你的记录重跑。三条都满足才叫可复现。
动手练(只用你自己的账号和请求)
  • 抓同一个操作的请求三次,列一张四列表:字段、示例值、长度、三次的变化。
  • 挑一个字段单独改一位,记录服务端返回的错误码和提示语,判断它属于签名失败还是业务失败。
  • 把一个 10 位数字字段换算成日期,确认它是不是时间戳。
  • 按 11.8 的模板,为你的三次实验各写一行记录,包括"预期"那一列。
黑盒Black Box只能看到输入和输出,内部逻辑完全不可见。
灰盒Grey Box能看到部分内部结构,如有安装包但无源码。
白盒White Box能看到完整源码或带符号的二进制。
Oracle判定器能反复回答"这个结果对不对"的黑箱,服务器就是最常用的。
差异对比Diff把两次请求逐行对齐,找出唯一不同的地方。
Nonce随机数一次性的随机值,用来防重放,通常不重复。
雪崩效应Avalanche输入改一位,输出约一半的位发生变化,是哈希的特征。
基线Baseline未经改动的原始请求,所有实验都拿它做参照。
你现在应该能回答 你手上属于哪种盒子、该从哪一步开始?一条请求怎么列成可实验的字段表?两次请求怎么 diff 出依赖关系?怎么判断一个字段是时间、随机数还是计数器?为什么必须一次只改一个变量?如果这五个问题你能答上来,你已经比"凭感觉试"快十倍。
第 12 章 · 移动端

Android 逆向基础:APK、DEX 与 Frida

你下载了一个 APK,双击解压,看到一堆不认识的文件夹;用工具反编译,出来几千个文件,不知道从哪看起。这一页把 Android 拆成四层:安装包、字节码、原生库、运行时,每一层告诉你它是什么、用什么工具看、能看到什么。

APK 结构DEX / smaliManifest JNI / .soFridaroot 与加固
先记住这一句 Android 逆向不是"把 APK 变成一个能读的工程",而是按层定位:先用 Manifest 找入口,再用 DEX 找逻辑,逻辑不在 DEX 里就去 .so,最后用运行时观察真实入参。层次搞错了,看再多代码也没用。
人话版 这一页把 APK 当成一个快递箱:先看箱子里的清单(Manifest),再看菜谱(DEX),菜谱被锁就去后厨(.so)看,最后在运行时盯着它做菜。

12.1APK 的本质:一个改了后缀的 zip

APK(Android Package,安卓安装包)不是什么神秘格式。把 app.apk 改名成 app.zip,用任何解压工具都能打开。它就是一个压缩包,里面按固定目录放了几类文件。

这一点很重要:你不需要特殊工具就能看到它的骨架。解压出来先看一眼目录,比直接上反编译工具更快建立整体印象。

APK 内部结构 app-release.apk(本质上是一个 zip 压缩包) AndroidManifest 二进制 XML:身份与入口 classes.dex 主程序字节码 classes2.dex 方法数超限时拆分 lib/ 各 CPU 架构的 .so res/ 布局、图片、字符串 META-INF/ 签名文件,防止被改 assets/ 原样打包的资源,可加密 resources.arsc 资源索引表 真实项目还会有 lib 下多个架构目录、assets 里加密的配置、以及厂商自己加的壳文件
图 12-1 APK 的七个固定位置:清单、字节码、原生库、资源、签名

每个位置对应一个问题:

  • AndroidManifest.xml:这个 App 叫什么、能干什么、从哪个界面启动、申请了哪些权限。
  • classes.dex:Java 和 Kotlin 代码编译后的字节码,业务逻辑主要在这里。
  • lib/:C 和 C++ 编译出来的原生库,加密、签名、反调试常藏在这里。
  • res/ 与 assets/:界面、图片、字符串、配置文件。
  • META-INF/:签名信息,用来证明这个包没被人改过。
补充 Manifest 在 APK 里是二进制 XML,直接打开是乱码。这是为了省空间和加快解析,不是加密。用 apktool 或 jadx 打开就能还原成可读的 XML。

12.2DEX 与 smali:为什么必须反编译

你写的 Java 代码,编译器不会把它原样放进 APK。它先被编译成字节码(Bytecode),再打包成 DEX(Dalvik Executable,安卓可执行格式)。

打个比方:源码是"人话",字节码是"电报码"。电报码也是信息,但没人能直接读。反编译就是把这封电报翻译回人话。

DEX 有两个绕不开的限制,直接影响了它的样子:

  • 方法数上限:单个 DEX 最多引用约 65536 个方法。超过就要拆成 classes2.dex、classes3.dex,这叫多 DEX(MultiDex)。
  • 寄存器架构:DEX 用的是寄存器指令,不是栈指令。所以反编译出来的中间表示和源码不完全一样,会有很多临时变量。

于是有两个层次的"可读版本":

形态长什么样什么时候用
smali类汇编,一行一条指令,寄存器用 v0、v1 表示要改一个判断、改一个返回值时
Java 近似源码接近原始 Java,有 if、for、类名要读懂整体逻辑时
字节码二进制,一串操作码加参数基本不用人眼看,工具读

smali 之所以重要,是因为它一一对应字节码,改起来不会失真。你想把"签名不对就退出"改成"签名不对也继续",改 Java 是没用的(改完要重新编译整个工程),改 smali 只需要动一两行,再重新打包签名。

一段典型的 smali 长这样,含义是"如果 v0 等于 0,就跳转到标签":

# 比较 v0 和 0
if-eqz v0, :cond_1
# 相等时执行的代码
const-string v1, "ok"
return-object v1
:cond_1
# 不相等时走到这里
const/4 v0, 0x0
return-object v0

看不懂没关系。你现在只需要记住:smali 是"可以精确修改"的那一层,Java 是"方便阅读"的那一层。读用 Java,改用 smali。

12.3Manifest 与四大组件:先找到入口

反编译之后不要急着搜 sign。先看 Manifest,它相当于这个 App 的身份证加目录:谁开发的、能做什么、从哪个界面启动、能被谁调用。

Android 有四种基本组件,合称四大组件:

  • Activity:一个界面。你看到的每一屏都是一个 Activity。
  • Service:后台干活的,没有界面,比如上传、播放音乐。
  • Broadcast Receiver:接收系统或别的应用发来的广播消息。
  • Content Provider:对外提供数据,比如通讯录、相册。

Manifest 里最该看的几个字段:

字段含义为什么重要
package包名,全局唯一后面所有类名都以它为前缀
android:exported是否允许外部应用调用为 true 的组件是攻击面,也是调试入口
intent-filter声明能响应哪些意图能看到启动入口和深链(Deep Link)
uses-permission申请了哪些权限权限多说明功能重,也提示数据敏感度

其中 android:exported 值得单独说。它决定"别的 App 能不能直接唤起这个组件"。为 true 时,任何应用都能发一个意图(Intent)把它叫起来,并带上参数。这既是功能,也是风险:参数没校验就可能被注入。

找入口的顺序建议是:先在 Manifest 里找带 MAIN 和 LAUNCHER 的 Activity(那就是桌面图标的入口),再从它的代码里看它启动了哪些界面、调用了哪些网络类。

技巧 在 Manifest 里搜 exported="true",把所有对外暴露的组件列出来。这张表比盲目翻代码有用得多,也是做安全评估时的第一张表。

12.4工具分工:jadx、apktool、IDA 各管一段

新手最容易犯的错,是拿一个工具硬啃所有问题。这三个工具的分工其实很清楚:

工具擅长不擅长
jadx把 DEX 反编译成接近 Java 的代码,支持全局搜索、跳转定义改完不能直接打包;对混淆代码可读性差
apktool完整解包资源,输出 smali 和可读 Manifest,能重新打包反编译出的 Java 不可读,只能看 smali
IDA / Ghidra分析 .so 里的机器码,看汇编和伪代码对 Java 层无能为力,学习成本高

实际流程通常是:jadx 读逻辑,apktool 改代码,IDA 看 native。

  • 先用 jadx 打开 APK,全局搜关键字(比如 sign、encrypt、接口路径),快速定位到可疑类。
  • 如果发现关键方法是 native 声明,就转去 lib/ 找对应的 .so,用 IDA 打开。
  • 如果确认要改行为,用 apktool 解包,改 smali,再 apktool b 回编译、重新签名。

回编译之后必须重新签名,否则装不上。这一步用的是你自己的调试证书,只对你自己测试的设备有意义。

注意 改包重签会改变签名指纹。很多 App 会校验自己的签名,发现不一致就拒绝运行或走异常分支。这不是"加固",是普通的完整性校验,但足以让改包版跑不起来。

12.5native 库与 JNI:Java 怎么落到 .so

Java 代码跑在虚拟机里,碰不到硬件,也做不了高性能计算。需要时,它会调用 C 或 C++ 写的函数,这些函数编译成 .so(Shared Object,共享库)放在 lib/ 下。

连接两边的桥叫 JNI(Java Native Interface,Java 原生接口)。可以把它想成翻译:Java 说"我要调用 sign",JNI 负责把这句话翻译成 C 函数能听懂的参数格式。

Java 通过 JNI 调用 .so 的过程 Java 层 native String sign() 只有声明,没有方法体 JNI 边界 按名字查函数 .so 里的机器码 JNI_OnLoad 注册函数表 真正的算法在这里 ① 找类和方法名 拼成固定格式的字符串 ② 查函数指针表 拿到内存地址 ③ 跳到机器码 开始执行 名字有两种绑定方式:按约定命名,或在 JNI_OnLoad 里动态注册;后者更隐蔽
图 12-2 Java 的 native 方法只是一个声明,真正的代码在 .so 里,中间隔着 JNI 这层翻译

名字绑定有两种方式,这决定了你在 .so 里怎么找函数:

  • 静态注册:函数名按 Java_包名_类名_方法名 拼出来。在 IDA 的导出表里直接搜方法名就能找到。
  • 动态注册:在 JNI_OnLoad 里用 RegisterNatives 把名字和函数地址绑起来。导出表里看不到业务函数名,只能从 JNI_OnLoad 顺藤摸瓜。

看到动态注册不用慌。它只是"把电话本换成了另一张纸",函数本体还在,只是名字不写在导出表上。顺着 JNI_OnLoad 里的那张表,一个个对过去就行。

补充 lib/ 下常按 CPU 架构分目录,比如 arm64-v8a、armeabi-v7a、x86_64。分析时优先看 arm64-v8a,因为现在的手机基本都是这个架构。

12.6Frida 原理:在运行时看真实入参

静态分析有个天生的短板:代码可以加密、可以混淆、可以动态加载。但程序要运行,就必须把真实的参数交给真实的函数。Frida 就是在这个时刻插一脚。

它的原理分三步:

  1. 往目标进程里注入一个 agent(代理模块),让目标进程多出一段外来代码;
  2. 这段代码里带一个 JavaScript 运行时,你用 JS 写逻辑;
  3. 用 Interceptor 把目标函数的开头改成跳转,先跳到你的回调,再跳回原函数。
Frida 的注入与拦截结构 你的电脑 Python 或 CLI 脚本 通过 USB 转发命令 手机(Android 设备) frida-server:以 root 权限常驻后台 目标 App 进程(被注入了 agent) JS 运行时 Interceptor 挂钩 只读观察的脚本不修改任何逻辑,只打印入参和返回值
图 12-3 Frida 由三部分组成:电脑上的控制端、手机上的 server、被注入的目标进程

下面两个脚本都只做观察,不改任何逻辑。第一个挂在 Java 层,打印方法入参和返回值:

// 只读观察:打印 sign 方法的入参与返回
Java.perform(function () {
  var cls = Java.use("com.example.app.SignUtil");
  cls.sign.implementation = function (a, b) {
    console.log("[入参] a=" + a + " b=" + b);
    var r = this.sign(a, b);
    console.log("[返回] " + r);
    return r;
  };
});

第二个挂在原生函数上,只打印参数和返回值,不读内存内容,避免踩到指针:

// 只读观察:打印 .so 里某个导出函数的调用
var addr = Module.findExportByName("libsign.so", "sign_init");
Interceptor.attach(addr, {
  onEnter: function (args) {
    console.log("[调用] 第 1 个参数: " + args[0]);
  },
  onLeave: function (retval) {
    console.log("[返回] " + retval);
  }
});

这两段代码的价值不在语法,而在思路:与其猜算法,不如让程序自己告诉你它收到了什么、算出了什么。拿到真实入参之后,再回到静态代码里对照,比纯看汇编快得多。

红线 Frida 需要注入进程,只在你自己的设备、你自己的应用、或书面授权的测试目标上使用。给别人的 App 注入、抓取他人数据、绕过付费或风控,都不属于技术学习范围。

12.7adb:连接手机的那根线

adb(Android Debug Bridge,安卓调试桥)是电脑和手机之间的命令行通道。它做的事情很朴素:把命令送进手机,把文件取出来。

最常用的几条命令,建议背下来:

# 看设备有没有连上
adb devices
# 安装(-r 表示覆盖安装,保留数据)
adb install -r app.apk
# 看指定应用的日志
adb logcat -s MyApp
# 列出所有包名,找目标应用
adb shell pm list packages
# 启动某个界面
adb shell am start -n com.example.app/.MainActivity
# 端口转发:把手机的 27042 映射到电脑
adb forward tcp:27042 tcp:27042
# 从手机取文件出来
adb pull /data/local/tmp/trace.txt

其中端口转发是 Frida 的关键一步:frida-server 在手机上监听一个端口,电脑通过转发连上去,看起来就像连本机一样。

看日志也有技巧。logcat 默认输出所有应用的日志,噪音极大。用 -s 标签 只留你关心的那一个,或者用 --pid 只留某个进程的日志,效率会高很多。

技巧 遇到崩溃或异常,先看日志再猜代码。日志里的异常栈会直接告诉你类名、方法名、行号,比在几千个文件里搜索快一个数量级。

12.8root、Magisk 与 App 的检测点

root 的本质是拿到系统的最高权限。用大楼打比方:普通用户只有自己房间的钥匙,root 有整栋楼的总钥匙,能进任何房间、改任何门锁。

Magisk 是目前最常见的 root 方案。它的特点是"系统分区不改动",通过启动时挂载的方式把修改叠加上去,所以能比较容易地撤销。它还提供模块机制,可以往系统里加东西而不碰原始分区。

代价也很直接:

  • 系统完整性被破坏,部分银行、支付、企业应用会拒绝运行;
  • 系统更新可能失败,或者更新后 root 失效;
  • 安全边界降低,任何拿到 root 的恶意程序都能读全部数据。

App 检测 root,靠的是一组"痕迹"。这些痕迹的原理值得了解,因为它们是移动端风控的基础知识:

检测点原理说明
su 文件检查常见路径下有没有 su 可执行文件最古老也最容易误判的方法
Magisk 包名查是否安装了管理工具可以被改名或隐藏
系统属性读调试、解锁相关属性需要读系统配置的权限
分区可写尝试以写方式打开系统目录正常设备是只读的

从防守方角度看,这些检测都有同一个弱点:它们依赖"环境看起来正常",而环境是可以被伪装的。所以现代风控不会只靠单一检测,而是把 root 痕迹、设备指纹、行为特征放在一起打分。

从学习角度看,你要记住的是结论:root 不是目的,只是让你能观察系统的一种手段。绝大多数协议分析,用一台普通测试机加抓包就够了,不一定需要 root。

12.9动态调试 smali、加固脱壳与两个误区

改 smali 之后怎么验证?最直接的办法是让程序停在你想看的那一行。思路是:在 smali 里插入一条调试指令(本质是一条特殊的指令,会让调试器接管),然后用调试器附加到进程,单步执行,看寄存器的值。

这条路的门槛在于:你得先让应用以可调试的方式运行,而正式包默认是不可调试的。所以实践中更多用 Frida 做观察,用 smali 打补丁做验证,两者配合。

再往上就是加固。加固厂商做的事情,本质是三层:

  • 藏:把真正的 DEX 加密后放进 assets 或 .so,启动时在内存里解密,磁盘上永远没有明文 DEX。
  • 骗:改 Manifest、加壳入口,让静态工具看到的类列表是空的或假的。
  • 防:检测调试器、检测注入、校验自身完整性,发现异常就退出。

对应的脱壳思路,原理上只有一句话:DEX 要在内存里变成可执行的形式,就一定有一个时刻是明文。所以方向是在那个时刻把内存里的 DEX 抓出来,再修复头部。具体工具和步骤不在本页展开,因为不同壳差异极大,且容易踩到授权边界。

最后说两个流传很广的误区。

误区一:反编译出来就是源代码。不是。反编译是"根据字节码推测源码",变量名、注释、部分语法结构都丢了。混淆之后,类名会变成 a、b,方法名变成 a()、b(),可读性接近零。你看到的是"能编译回去的近似版本",不是作者写的那份。

误区二:混淆等于加密。也不是。混淆只改变名字和结构,逻辑完全没变,运行时该怎么做还是怎么做。加密是让内容在没钥匙时不可读。混淆挡的是"人眼",加密挡的是"没有钥匙的人"。所以混淆过的代码照样能被 Frida 挂钩,只是读起来更费劲。

注意 看到"反编译失败""类列表是空的""Manifest 里只有一个 Application 类",先别怀疑工具。这通常说明遇到了加固,而不是你的操作错了。先确认有没有壳,再决定要不要往下走。
自测:APK 里最该先看哪个文件,为什么?
先看 AndroidManifest.xml。它给出包名、启动入口、对外暴露的组件和申请的权限,相当于一张目录。先有目录再翻代码,能少走很多弯路。
自测:为什么读代码用 jadx,改代码却要用 apktool?
jadx 把 DEX 还原成接近 Java 的代码,适合阅读和搜索,但改完不能直接打包。apktool 输出的是 smali,与字节码一一对应,改完能重新编译打包,所以修改要用它。读和改是两件事。
自测:Java 里的 native 方法,真正的代码在哪里?
在 lib/ 目录下对应架构的 .so 文件里。Java 层只有一个声明,运行时通过 JNI 按名字或注册表找到函数地址再跳过去执行。所以看到 native 就要转去分析 .so。
自测:混淆和加密的区别是什么?
混淆改的是名字和结构,逻辑不变,程序照常运行,挡的是人眼;加密让内容在没钥匙时不可读,挡的是没有密钥的人。混淆过的代码仍然能被动态挂钩,只是更难读。
自测:为什么说 root 不是协议分析的必要条件?
多数协议分析只需要抓包和观察请求,普通测试机加代理就能做。root 主要用于需要注入进程、读其他应用数据、改系统环境的场景。它是手段之一,不是前提。
动手练(用你自己的应用或开源示例)
  • 把一个 APK 改名成 zip 解压,画出它的一级目录,标出每个目录放什么。
  • 用 jadx 打开一个开源 App,在 Manifest 里找出所有 exported="true" 的组件。
  • 找一个带 native 方法的类,去 lib/arm64-v8a/ 里找对应 .so,看它是静态注册还是动态注册。
  • 用 adb 连上你自己的测试机,跑一遍 adb devices、pm list packages、logcat -s 三条命令。
APKAndroid Package安卓安装包,本质是一个 zip,内含清单、字节码、资源与签名。
DEXDalvik ExecutableJava 代码编译后的字节码格式,单个文件方法数有上限。
smaliDEX 的反汇编形式,一行一条指令,可精确修改后重新打包。
JNIJava Native InterfaceJava 与 C/C++ 之间的调用约定,负责参数翻译。
Frida运行时注入框架,用 JS 挂钩函数,观察真实入参与返回值。
adbAndroid Debug Bridge电脑与安卓设备之间的命令行通道,可装包、看日志、转发端口。
Magisk主流 root 方案,不改系统分区,通过启动时挂载实现。
加固Packing把真实代码加密藏起、运行时再解密加载的保护手段。
你现在应该能回答 APK 里哪几个位置分别放什么?为什么读用 jadx、改用 smali?native 方法怎么落到 .so?Frida 的三步原理是什么?混淆和加密差在哪?如果这五个问题你能讲清楚,你已经能独立拆开一个普通 APK 并找到它的网络逻辑入口。
第 13 章 · 移动端

iOS 逆向基础:Mach-O、签名与运行时

iOS 的安装包不像 APK 那样一眼能看穿:它没有 DEX,代码是编译好的机器码;它还有一整套签名与授权机制,让"装不上"和"跑不起来"成为常态。这一页讲清四件事:IPA 里有什么、Mach-O 怎么读、签名在防谁、运行时怎么观察。

IPA 结构Mach-O代码签名 越狱ObjC 运行时Keychain
先记住这一句 iOS 逆向的门槛不在"看不懂代码",而在三层门:装得上、解得密、连得上。IPA 是箱子,签名决定谁能装,Mach-O 是真正的内容,运行时工具负责在它跑起来之后观察。
人话版 这一页把 iOS 应用拆成四层:箱子(IPA)、书(Mach-O)、封条(签名)、读书的人(运行时)。四层都认识,才知道卡在哪一层。

13.1IPA 的结构:也是一个 zip

IPA(iOS App Store Package,iOS 应用包)和 APK 一样,本质是 zip。把它改名成 .zip 就能解压。但它的目录结构只有一个固定入口:Payload/。

用寄快递打比方:Payload 是外箱,MyApp.app 是里面那个贴着标签的包裹,包裹里才是真正的东西。

IPA 的内部结构 MyApp.ipa(zip 压缩包) Payload/ MyApp.app MyApp(Mach-O 可执行文件) Info.plist embedded.mobileprovision Frameworks/ Mach-O:真正的程序代码 CPU 指令,没有源码 Info.plist:应用身份证 包名、版本、入口类 描述文件:允许谁安装 含证书、设备列表、权限 Frameworks/:内嵌的第三方库 每个都是独立的 Mach-O 从 App Store 下载的 IPA 里,可执行文件是加密的;解密后的 IPA 才能直接分析
图 13-1 IPA 只有一条主线:Payload 里一个 .app,.app 里四样关键东西

四样关键东西,各管一件事:

  • 可执行文件:和 .app 同名的那个文件(没有后缀),就是 Mach-O 格式的程序本体。
  • Info.plist:应用的身份证,写着包名(Bundle ID)、版本、支持的设备、启动配置。
  • embedded.mobileprovision:描述文件,写着"哪个证书签的、允许哪些设备装、能用哪些能力"。
  • Frameworks/:内嵌的动态库,第三方 SDK 通常以这种形式存在,每个都是独立的 Mach-O。
补充 从 App Store 下载的 IPA,可执行文件是加密的(行业里叫"砸壳"指的就是解密这一步)。加密的是 Mach-O 里的代码段,不是整个包。所以直接拖进反汇编工具,看到的可能是一堆无意义的字节。

13.2Mach-O:iOS 程序的真实形状

Mach-O(Mach Object,Mach 目标文件格式)是苹果系统的可执行文件格式。它的结构像一本印刷书:先有封面和目录,再有正文,正文按章节分块。

三大部分,从前往后:

  • Header(头部):开头一小段,说明"我是什么架构、有多少条目录项"。头 4 个字节是魔数,用来确认这是 Mach-O。
  • Load Commands(加载命令):一串目录项,每一项说"某一段在文件的哪个位置、多大、加载到内存哪里"。
  • Segments 与 Sections(段与节):真正的数据。段是大块(代码段、数据段),节是段里的细分(代码段里的指令、常量字符串)。
Mach-O 的三层结构 Header 魔数、CPU 类型 Load Commands 目录:每段在哪 Segments __TEXT / __DATA 魔数 0xFEEDFACF:确认这是 64 位 Mach-O 后面跟着 CPU 架构、文件类型、命令条数 LC_SEGMENT_64:记录一段的偏移与大小 还有 LC_LOAD_DYLIB 等,说明依赖哪些库 Section:段内更细的划分,如 __text __cstring 里是常量字符串,最常被搜 有的文件是"胖二进制",一个文件里打包了多种 CPU 架构
图 13-2 Mach-O 像一本书:头部是封面,加载命令是目录,段与节是正文

实际分析时,最常打交道的不是头部,而是两个地方:

  • __TEXT 段里的 __text 节:所有机器指令都在这里。反汇编工具默认打开的就是它。
  • __cstring 节:常量字符串。接口路径、错误提示、密钥片段、加密算法名,常常在这里露出来。

还有一类文件叫胖二进制(Fat Binary)或通用二进制:一个文件里同时包含 arm64 和 x86_64 等多份代码,方便在不同设备上运行。分析前要先用工具"瘦身",只留你关心的架构,否则地址会对不上。

技巧 拿到 Mach-O 先做三件事:看架构、搜字符串、列依赖库。字符串能告诉你"这个 App 用了什么协议、连了哪些域名",依赖库能告诉你"它用了什么网络框架和加密库"。

13.3签名机制:不是加密,是封条

很多人把 iOS 签名理解成"给应用加密"。不是。签名更像火漆印:它不隐藏内容,只证明"这封信是谁写的、路上有没有被拆过"。

一套完整的签名,涉及四样东西:

iOS 签名的链条 根证书 苹果自己持有 开发者证书 证明"是谁" 描述文件 证明"能装哪" App 签名 证明"没被改" Entitlements:这个 App 能用哪些能力 推送、钥匙串共享、后台运行都在这里声明 设备:核对描述文件里的设备列表 不在列表里的设备,装不上 签名只保证"来源可信、内容未改",它不隐藏任何代码
图 13-3 签名链四环:根证书、开发者证书、描述文件、App 签名,缺一环都装不上
环节作用失效会怎样
证书证明签名者身份系统不认识签名者,直接拒绝
描述文件限定设备、权限、有效期设备不在名单或已过期,装不上
Entitlements声明能用的系统能力用了未声明的能力,运行即崩
代码签名逐页哈希,防内容被改改一个字节,签名校验失败

这解释了一个常见现象:你改了一个字节想验证猜想,结果应用直接闪退。不是你的改动逻辑错了,是签名对不上了。要验证改动,必须重新签名,而重签需要证书和描述文件。

也解释了一个常见误解:签名不加密。任何人拿到 IPA 都能解压、都能看到资源文件、都能读字符串。签名防的是"篡改"和"冒充",不是"偷看"。真正防止偷看代码的是 App Store 下发时的加密(对应 13.1 提到的"砸壳")。

13.4越狱:作用与代价

越狱(Jailbreak)就是绕过系统的签名与沙盒限制,拿到最高权限。用大楼打比方:正常应用各自住在自己的房间里,越狱相当于拿到总钥匙,能进配电房、能改门禁。

它能带来三件事:

  • 安装未经 App Store 签名的应用;
  • 读写系统目录和其他应用的数据;
  • 往其他进程注入代码,这是动态分析的前提之一。

代价同样明确:

  • 安全性下降:沙盒失效,恶意程序拿到权限后能做更多事;
  • 应用拒绝运行:银行、支付、部分游戏会检测越狱并直接退出;
  • 系统不稳定:越狱依赖系统漏洞,更新后经常失效,甚至无法开机;
  • 保修与支持:官方通常不对此类设备提供支持。

需要澄清一点:越狱不是分析的必要条件。很多工作可以在未越狱设备上完成,比如用开发者证书自签一个测试应用、用官方提供的调试通道、或者直接分析已经解密好的 IPA。越狱只是让"注入其他进程"这件事变简单。

注意 越狱设备不适合放任何真实账号。分析用的机器应该是专门的测试机,不登录个人账号、不存私人照片、不装支付应用。

13.5工具分工:谁看结构,谁看代码

iOS 这边工具不多,但分工比 Android 更细。核心区别是:有的工具只读结构,有的工具读指令。

工具擅长输出什么
class-dump从 Mach-O 里提取 Objective-C 类和方法声明一堆 .h 头文件,只有接口没有实现
Hopper反汇编并给出可读性较好的伪代码汇编加流程图,适合快速定位
IDA深度反汇编、交叉引用、类型恢复最完整的分析视图,学习成本最高
Ghidra免费开源的反汇编与反编译伪代码质量不错,适合入门和批量分析

实际顺序通常是:class-dump 看有哪些类和方法,Hopper 或 Ghidra 定位到具体函数,IDA 做深度确认。

class-dump 的价值常被低估。它不能给你实现,但能给你地图:类名、方法名、继承关系。有了地图,你才知道该在哪个函数上下断点。方法名往往还很直白,比如 signWithParameters:、encryptData:,一眼就能看出用途。

补充 Swift 写的应用,class-dump 的效果会差很多,因为 Swift 的类信息不在 Objective-C 的运行时结构里。这时要靠符号表(如果有)或者直接反汇编。

13.6Objective-C 运行时:消息发送与替换

Objective-C(简称 ObjC)是 iOS 的主力语言之一。它和 Java 最大的不同在于:方法调用不是"直接跳过去",而是"发一条消息"。

打个比方:Java 是"我知道你工位在哪,直接走过去找你";ObjC 是"我打总机,报一个名字,总机帮我转接"。这个"总机"就是运行时(Runtime)。

由此产生三个关键概念:

  • 类对象(Class):运行时里真实存在的一个结构,里面有一张方法表,记录"名字到函数地址"的映射。
  • 选择器(Selector):方法的名字,编译后变成一个唯一的标识。发消息时传的就是它。
  • 消息发送(objc_msgSend):所有方法调用的统一入口。它拿到对象和选择器,去方法表里查地址,再跳过去。

因为是"查表再跳",方法表在运行时是可以被改的。这就带来了 method swizzling(方法交换):把两个方法的实现地址互换,调用 A 的时候实际执行 B。

它的用途很广:统计埋点、功能开关、热修复,都是这个原理。对分析者来说,它的意义是一个天然的观察点:交换前后,你可以在自己的实现里打印参数,再调用原实现。

这也解释了为什么 ObjC 应用比 Swift 应用更容易做动态分析:类名、方法名、参数类型都还在运行时结构里,工具能直接读出来。

注意 方法交换是"改行为",不是"看行为"。做只读观察时,应该在回调里打印完就调用原实现,不要改变返回值,也不要吞掉异常。观察和篡改是两条线,不要混在一起。

13.7Swift 符号与 mangling:名字里藏着类型

Swift 为了让"同名方法但参数类型不同"能共存,把类型信息编码进了符号名。这个编码规则叫 mangling(名称修饰)。

结果就是:你在符号表里看到的不是 sign(ts:),而是一串以 $s 开头、中间夹着数字和字母的东西。比如模块名、类型名、参数类型,全被压进了这一串。

这串东西不是加密,只是编码,可以还原。苹果工具链自带还原命令(swift-demangle),很多反汇编工具也会自动还原。

需要记住的只有三点:

  • Swift 符号可还原,不是乱码;
  • 还原后能看到模块名、类型名、参数类型,对定位很有帮助;
  • Swift 的类信息不在 ObjC 运行时里,所以 class-dump 看不到,得靠符号或反汇编。

13.8Frida 在 iOS 上:同样的思路,不同的入口

Frida 在 iOS 上的原理和在 Android 上一致:注入进程、跑 JS、挂钩函数。差别在挂钩的写法,因为 iOS 的方法是用选择器(Selector)标识的。

下面这段只做观察:打印入参对象,然后照常调用原实现,不改任何返回值。

// 只读观察:打印某个 ObjC 类方法的入参
var method = ObjC.classes.SignUtil["+ signWith:ts:"];
Interceptor.attach(method.implementation, {
  onEnter: function (args) {
    var first = new ObjC.Object(args[2]);
    var second = new ObjC.Object(args[3]);
    console.log("[入参] " + first + " / " + second);
  },
  onLeave: function (retval) {
    console.log("[返回] " + new ObjC.Object(retval));
  }
});

这里 args[0] 是对象自身,args[1] 是选择器,真正的参数从 args[2] 开始。这个偏移是 ObjC 消息调用约定决定的,记错就会读到一个奇怪的指针。

在 iOS 上用 Frida,通常需要满足一个前提:设备越狱并装了 frida-server,或者应用本身以可调试方式运行并加载了对应的动态库。这不是"绕过",而是调试通道是否打开的问题。

红线 只在自己的测试设备、自己开发的应用、或书面授权的目标上做动态分析。给他人应用注入、窃取数据、绕过付费,既超出技术讨论范围,也可能违法。

13.9Keychain、数据保护与抓包的前提

iOS 把敏感数据放在 Keychain(钥匙串)里。它不是一个普通文件,而是系统管理的一个加密数据库,应用只能通过接口存取,拿不到原始文件。

钥匙串的条目可以设置"可访问条件",决定它在什么状态下能被读:

保护级别什么时候可读典型用途
设备解锁后可读只要解锁过就一直可读登录凭证、Token
仅解锁时可读锁屏后不可读较高敏感度的密钥
仅本机、不解锁不可迁移锁屏不可读,且不能备份到新机设备绑定的密钥

这些级别的实现依赖硬件里的安全区域(Secure Enclave)和设备的锁屏密码。所以"拿到手机就能读出所有密钥"是不成立的:锁屏状态下,高保护级别的条目根本解不开。

再说抓包。iOS 对证书的信任分两层:

  1. 把代理工具的根证书装进设备(作为描述文件安装);
  2. 到"证书信任设置"里手动打开完全信任。

只做第一步是不够的,这是 iOS 和很多 Android 设备最大的操作差异。很多人卡在"证书装了但还是抓不到",原因就是第二步没开。

即使两步都做了,仍可能看不到明文。因为应用可以做证书固定(Certificate Pinning):不信任系统证书库,只认自己内置的那张证书或公钥。这时握手会失败,流量依然加密。

从防守方角度,证书固定是有效手段;从分析角度,它是一个明确的信号:看到握手失败而不是乱码,就该怀疑固定,而不是怀疑抓包工具坏了。

13.10两个必须纠正的误区

误区一:不越狱就没法分析 iOS 应用。

不对。越狱解决的是"往别人的进程里注入代码"这一件事。而下面这些工作都不需要越狱:

  • 解压 IPA、读 Info.plist、看资源文件;
  • 对已解密的 Mach-O 做静态反汇编、搜字符串、看依赖库;
  • 用开发者证书给自己编译的测试应用做动态调试;
  • 在信任了证书的前提下抓自己应用的流量。

越狱只是让"动态注入"这一步更省事。把越狱当成前置条件,会让很多人还没开始就放弃。

误区二:iOS 一定比 Android 安全。

这句话混淆了"恶意软件少"和"不可被分析"。iOS 的封闭生态确实降低了普通用户中招的概率:应用必须经过审核、不能随意侧载、沙盒更严格。但这不等于系统不可分析,也不等于没有漏洞。

更准确的说法是:两者的威胁模型不同。

维度iOSAndroid
应用来源以官方商店为主,侧载受限渠道多,侧载容易
权限模型粒度较粗,但审核严格粒度细,用户可控性高
分析难度签名与加密门槛高包结构开放,工具链成熟

对做安全的人来说,结论是:没有"更安全的平台",只有"不同的攻击面"。iOS 把门槛放在了签名和加密上,Android 把门槛放在了混淆和加固上,两边都要学。

自测:IPA 解压后,真正可分析的目录是哪一个?
是 Payload/。里面只有一个 .app 目录,可执行文件、Info.plist、描述文件、Frameworks 都在它里面。其他位置基本是打包工具留下的附属文件。
自测:签名是加密吗?它到底防什么?
不是加密。签名不隐藏内容,任何人拿到 IPA 都能解压看资源。它防的是两件事:内容被篡改(逐页哈希校验)、签名者被冒充(证书链)。防偷看靠的是下发时的加密,不是签名。
自测:为什么 ObjC 应用比 Swift 应用更容易做动态分析?
因为 ObjC 的类名、方法名、参数类型都保留在运行时结构里,工具能直接读出来并挂钩;Swift 的类型信息不在 ObjC 运行时里,只能靠符号表或反汇编还原。可读性来自运行时结构,不是语言本身的安全性。
自测:iOS 抓包时证书装了还是抓不到,最可能是什么原因?
两种可能:一是没有在"证书信任设置"里手动开启完全信任;二是应用做了证书固定,只认自己内置的证书。前者是操作问题,后者是设计问题,要分开判断。
自测:不越狱能不能分析 iOS 应用?
能。解包、读 plist、静态反汇编、搜字符串、分析自己编译的应用、抓自己应用的包,都不需要越狱。越狱主要解决"往其他进程注入代码"这一件事。它不是分析的前置条件。
动手练(只用自己的设备与自建应用)
  • 把一个 IPA 改名成 zip 解压,画出 Payload 到 .app 的目录树,标出四样关键文件。
  • 用文本编辑器打开 Info.plist(或先转成 XML),找出 Bundle ID 和版本号。
  • 找一个 Mach-O 文件,用工具查看它的架构、依赖库和 __cstring 里的字符串。
  • 在自己的测试机上装一次代理证书,然后到证书信任设置里打开完全信任,对比开与不开的区别。
IPAiOS App Store PackageiOS 应用安装包,本质是 zip,内容都在 Payload 目录下。
Mach-OMach Object苹果系统的可执行文件格式,由头部、加载命令、段与节组成。
描述文件Provisioning Profile限定应用由谁签名、能装到哪些设备、能用哪些能力。
Entitlements应用被授权使用的系统能力清单,写在签名里。
越狱Jailbreak绕过签名与沙盒限制拿到最高权限,代价是安全性与稳定性。
选择器SelectorObjC 方法的名字标识,发消息时用它去方法表里查地址。
方法交换Swizzling运行时互换两个方法的实现地址,常用于埋点与热修复。
钥匙串Keychain系统管理的加密数据库,按保护级别控制何时可读。
你现在应该能回答 IPA 里哪四样东西最关键?Mach-O 的三层各放什么?签名防的是篡改还是偷看?越狱解决了哪个具体问题、代价是什么?ObjC 为什么能被动态挂钩?如果这五个问题你能讲清楚,iOS 这扇门你已经知道该从哪一侧推。
第 14 章 · 移动端对抗

加固、混淆与反调试:对抗的成本

很多人第一次拆一个商业 App,打开反编译工具会愣住:类名全是 a.b.c,字符串全是乱码,调试器一附加程序就退出。这不是工具坏了,而是对方花钱买了对抗。这一页讲清楚加固到底做了什么、为什么有效、边界在哪里。

混淆DEX 壳VMP / OLLVM 完整性校验反调试反 Hook
先记住这一句 加固不是"打不开的锁",而是把开锁时间从十分钟拉到十天。它改变的是成本曲线,不是可能性。
人话版 加固就像给快递套了五层泡沫箱:货还是那批货,但你每拆一层都要多花时间,拆错还会触发报警。

14.1先给结论:加固是把成本抬高

把一个 App 装进保险柜,和把它装进纸箱,区别不在"能不能打开",而在"打开要多久、要什么工具、要懂多少东西"。加固(Hardening)做的正是这件事。

它抬高三类成本,缺一不可:

  • 时间成本:原来十分钟能读完的逻辑,现在要三天。
  • 工具成本:原来一个免费反编译器就够,现在要会写脚本、会看汇编、会搭动态环境。
  • 知识成本:原来会 Java 就行,现在要懂链接器、内存布局、进程状态。

对防守方来说,这是一笔很划算的买卖:一次投入,长期生效。对分析方来说,这是一笔持续支出:每出一个新版本,之前的经验要重来一遍。

所以评估一个 App 的加固,不要问"能不能破",要问"破它值不值"。这句话是整章的主线。

补充 加固和"漏洞"是两件事。加固不影响业务逻辑是否正确,也不修复越权、注入这类问题。它只保护"代码不容易被读懂"。

14.2混淆:让代码能跑但读不懂

混淆(Obfuscation)的生活比喻是:把一篇文章里所有人名换成"甲、乙、丙",再把段落顺序打乱。文章照样能读通,但你已经不知道谁是谁。

它在编译阶段完成,代码的行为完全不变,变的只是"名字"和"结构"。常见四种手法:

手法一

改名(R8 / ProGuard)

把 checkLicense 改成 a,把 LicenseManager 改成 b。名字里不再有任何线索。

手法二

字符串加密

代码里出现的网址、密钥提示、错误文案,全部变成密文,运行时由一个小函数现场解出来。

手法三

控制流平坦化

把顺序执行的语句塞进一个巨大的 switch,靠一个状态变量决定下一步跳哪。读起来像迷宫。

手法四

无效分支

插入大量永远不成立的判断和死代码,让真正的逻辑淹没在噪声里。

原始代码与混淆后代码的对比 原始代码:名字会说话 class LicenseChecker { boolean verify(String key) { return check(key); } } 混淆后:名字没有信息量 class a { boolean b(String c) { return d(c); } } 改名 R8 / ProGuard 字符串加密 明文变密文 控制流平坦化 if 变 switch 无效分支 插入死代码
图 14-1 混淆只改"名字和结构",不改"功能":它挡得住阅读,挡不住运行
手法改了什么分析方的代价
改名类名、方法名、字段名要顺着调用关系重新起名
字符串加密常量变成密文要先找到解密函数并还原
控制流平坦化执行顺序被打散要重建状态机才能读懂
无效分支插入大量死代码要逐条判断哪些分支有用

关键判断:混淆完全不改变程序语义。所以它只对"静态阅读"有效,对"动态观察"几乎无效。程序运行时该调用的函数照样调用,该传的参数照样传。

注意 改名会影响反射和序列化。所以混淆工具必须留一份"保护规则"(keep 规则),告诉它哪些类名不能动。这份规则本身就是一个信息泄露点:被保留的类,往往就是关键类。

14.3加壳:外面套一层,运行时才解开

加壳(Packing)的比喻是快递里的泡沫箱:你收到的是一个箱子,拆开泡沫才见货。App 也一样,磁盘上那份文件里装的是"壳",不是真正的代码。

常见三层,保护强度递增:

壳、运行时解密、真实代码的三层结构 第一层:壳(DEX 壳 / VMP / OLLVM) 磁盘上能看到的只有壳,真实代码被加密或被改写成别的形式 第二层:运行时解密 App 启动后,壳在内存里把真实字节码解出来,或把指令还原 第三层:真实代码 只有到了这一步,内存里才有可读的原始逻辑 静态分析只能看到第一层,动态分析要抢在第二层到第三层之间 「解密点」是所有壳共同的软肋,也是所有防守方的必守之地
图 14-2 壳保护的是磁盘文件,不是运行中的内存

14.3.1 DEX 壳

Android 的可执行代码叫 DEX(Dalvik Executable)。壳的做法是:把真实 DEX 加密后藏进安装包的资源里,安装包里只留一个很小的"启动器"。

App 启动时,启动器把真实 DEX 解到内存,再交给系统加载。所以你在磁盘上翻来翻去,只能翻到壳。

14.3.2 VMP:把指令翻译成方言

虚拟机保护(Virtual Machine Protection,VMP)更狠:它把原始指令翻译成一套自定义字节码,再配一个解释器循环执行。

比喻:把中文合同翻译成只有某个翻译官懂的手语。你拿到了手语录像,但没有词典。

代价也很明显:解释执行比原生执行慢,耗电高。所以 VMP 通常只用在最核心的一两个函数上,不会全量使用。

14.3.3 OLLVM:在编译期就把代码搅乱

OLLVM 是一套编译期插件,在生成机器码之前做三件事:控制流平坦化、虚假控制流、指令替换。

它和前面混淆的区别是:混淆作用在"名字"层面,OLLVM 作用在"指令"层面。即使你拿到汇编,也要花很大力气才能还原。

壳的类型保护对象静态难度
DEX 壳整个 DEX 文件中,找到解密点即可
OLLVM关键函数的指令高,要重建控制流
VMP核心算法很高,要还原解释器
共同的软肋 无论哪种壳,程序最终必须执行明文逻辑。CPU 不认识密文。所以壳一定有一个"还原"的瞬间,这个瞬间就是所有分析工作的焦点。

14.4完整性校验:改了就要被发现

完整性校验(Integrity Check)的比喻是门上的封条:内容不重要,重要的是"有没有被撕开过"。

它有四种常见强度,从弱到强:

方式强度常见时机
CRC 校验弱,改一个字节就变资源文件加载前
文件哈希中,比对 SHA-256启动时、关键功能前
签名校验强,比对签名证书摘要启动时、登录时
分段校验很强,多点分散分散在多个业务流程里

签名校验值得单独说。Android 安装包必须签名,重打包必然换证书。客户端只要读出自己的签名摘要,和内置的期望值比一下,就知道有没有被改过。

分段校验是"分散投资":不在一处检查全部,而是把校验点撒在启动、登录、下单、支付前。改一处只能过一关,下一关还会拦。

注意 校验点越多,绕过越麻烦,但校验代码本身也在被保护的代码里。这是一个自指结构:保护者必须暴露自己。所以校验永远不是不可破的,只是让成本继续上升。

14.5反调试:让你连不上

反调试(Anti-Debugging)的目标不是"让你看不懂",而是"让你没法停下来看"。调试的核心动作是"暂停程序、检查状态",反调试就围绕这一点做文章。

反调试的自检流程与四类检测手段 App 启动 初始化阶段 自检代码执行 读状态 / 测时间 / 扫端口 没发现异常,继续运行 发现异常,静默退出或崩溃 四类最常见的检测手段 ptrace 占坑 自己先附加自己 调试器就挤不进来 读 TracerPid 看进程状态文件 非 0 就是被跟踪 时间差检测 两段代码之间 耗时突然变长 端口与标志 扫常见调试端口 看可调试标志 检测代码本身也是代码,可以被定位和观察,这是反调试的边界
图 14-3 反调试的判断逻辑:自检、发现、处置三步

14.5.1 ptrace 占坑

Linux 里一个进程同一时刻只能被一个调试器跟踪。App 可以在启动时自己调用 ptrace 附加自己,把"名额"占掉。调试器再来就附加失败。

比喻:自己先把车位停了,别人就没地方停。

14.5.2 读进程状态

系统会给每个进程维护一份状态文件,里面有一个 TracerPid 字段,表示"谁在跟踪我"。正常情况是 0,被调试就不是 0。App 读一下就知道有没有人挂在上面。

14.5.3 时间差检测

在代码里埋两处时间戳,正常执行可能只隔几微秒。如果中间被下了断点,人会停下来看,时间差就会变成几百毫秒甚至几秒。

比喻:掐表看一段路走了多久,走得太慢说明有人停下来看风景了。

14.5.4 端口与标志检测

调试器通常会在本机监听一个端口。App 试探性地连一下这些常见端口,能连上就说明环境可疑。另外,安装包里如果声明了"可调试",也是明显信号。

边界 本章只讲检测原理和防守方思路。具体的规避操作、针对某个平台的过检步骤,不在教学范围内,也不应对外提供。

14.6反 Hook:找注入框架的痕迹

Hook 是动态分析的主力手段:把一个函数"劫持"掉,看它的入参和返回值。反 Hook 的思路是:不管你用什么框架,它都要在进程里留下痕迹。

痕迹主要有四类:

检测面观察对象说明
端口本机监听端口注入框架默认会开固定端口
内存映射进程加载的库列表多出来的陌生库很可疑
线程名运行中的线程框架的线程名有固定特征
进程与文件运行中的进程、临时文件服务端进程名是公开信息

这四类检测的共同点是:它们观察的是"环境",不是"代码"。所以加固厂商会把它们打包成一个"环境体检"模块,在启动、登录、支付前各跑一次。

但反过来,环境检测也有代价:扫描内存映射、遍历线程都要时间,做得太频繁会拖慢启动,做得太粗会误伤正常用户(比如某些安全软件、无障碍服务、输入法也会注入进程)。

补充 误报是反 Hook 最大的工程难题。真实产品里,检测到异常通常不是直接崩溃,而是降级:限制功能、要求二次验证、上报风控。这样既不影响正常用户,又能提高攻击成本。

14.7多进程与双进程保护

单进程的保护有个天然弱点:所有判断都在一个进程里,只要控制了这个进程,判断就全废了。于是有了多进程方案。

比喻:两个保安互相盯着,一个被放倒,另一个立刻报警。

常见做法有三种:

  • 守护进程:主进程和一个轻量守护进程互相心跳,一方消失另一方重启它。
  • 校验分工:把签名校验、环境检测放到独立进程,主进程只接收结论。
  • 数据分散:密钥分片存放在不同进程,单独一个进程拿不到完整密钥。

但这条路在近年的 Android 上越来越难走。系统对后台进程限制很严,守护进程被杀后不一定能拉起来,反而导致崩溃率上升、耗电变差。所以现在的产品更倾向于"多进程分工",而不是"互相拉活"。

注意 多进程会显著提高崩溃率和排查难度。用户看到的是"这个 App 老闪退",而不是"这个 App 很安全"。这是加固最常见的副作用。

14.8怎么评估一个 App 的加固强度

不要凭"打开反编译器看到什么"下结论。用四个维度打分,才比较靠谱。

维度观察方法弱的表现
静态可读性反编译后有多少有意义的类名和字符串能看到业务词汇、接口路径
动态可观察性能否附加、能否看到关键函数入参一附加就退出,或毫无阻碍
校验点分布几个校验、在哪些时机触发只有启动时一次
崩溃行为被改动后是静默失败还是立即崩溃改了也没反应,说明没校验

第三项最容易被忽略。一个只在启动时校验一次的 App,和分别在启动、登录、下单、支付前校验四次的 App,防护强度差一个数量级。

第四项是"诚实度测试":故意改一个无关紧要的字节,看它有没有反应。反应越干脆(立刻退出、功能不可用),说明校验越硬;毫无反应,说明基本没做完整性保护。

14.9攻防的经济学:谁更划算

把加固看成一场成本竞赛,很多现象就解释得通了。

环节防守方投入攻击方每次成本结果
改名混淆几乎为零,编译开关低,读慢一点防守方划算
字符串加密低,改构建脚本中,要找解密函数防守方划算
DEX 壳中,买或自研中高,要抓解密点看版本迭代速度
VMP高,性能与人力很高,要还原解释器只保护核心算法

关键洞察:防守方一次投入长期有效,攻击方每个版本都要重来。所以加固真正的价值不在"挡住一次",而在"让每次都要重新付出"。

反过来说,如果一个 App 一年只发一个版本,加固的边际收益就低;如果两周发一版,加固的收益会成倍放大。这也解释了为什么大厂更愿意在加固上花钱。

14.10两个最常见的误区

误区一:混淆等于加密。很多人看到乱码字符串,就以为内容被加密了、拿不到了。其实混淆后的代码必须能正常运行,而运行就必须解密。解密函数就在同一个文件里,只是名字难看。加密保护的是"传输和存储",混淆保护的是"阅读体验",两件事。

误区二:加壳等于安全。壳只保护磁盘上的文件,程序跑起来之后内存里一定是明文。而且壳本身是一个复杂的加载器,历史上不少壳自己就有漏洞,反而扩大了攻击面。

还有一个变体误区:以为加固能防住"有授权的人"。加固防的是批量和自动化,防不住一个愿意投入几周时间、并且有合法授权做安全评估的团队。这是设计目标决定的,不是实现缺陷。

判断口诀 看到乱码字符串,先问"解密函数在哪";看到一附加就退出,先问"它在检测什么";看到很多校验,先问"它们分别在哪一步"。三句话能拆掉大部分加固的"神秘感"。

14.11合规提醒

加固分析在合法场景里非常常见:安全公司做渗透评估、厂商做兼容性排查、研究机构分析恶意样本、开发者检查自家 App 有没有被二次打包。

这些场景有一个共同点:要么是自己的 App,要么有书面授权。

  • 只分析自己开发的、或客户书面授权的 App。
  • 分析他人 App 的付费、授权、会员逻辑并加以利用,可能构成侵权甚至犯罪。
  • 不要把分析结果、密钥、绕过方法公开传播。
  • 涉及用户数据时,遵守最小必要原则,能不留存就不留存。
自测:混淆和加密,最本质的区别是什么?
混淆只改名字和结构,程序行为完全不变,运行时不需要"解密";加密会改变数据本身,必须有密钥才能还原。混淆挡阅读,加密挡获取。
自测:为什么说"解密点"是所有壳的共同软肋?
因为 CPU 只能执行明文指令,壳无论怎么加密,最终必须在内存里把真实代码还原出来。这个还原的瞬间,明文一定存在过。保护得再好,也只能推迟这个瞬间,不能取消它。
自测:一个 App 只在启动时校验一次签名,这个设计的问题在哪?
校验点太集中。启动时校验通过之后,整个运行期都不再检查,等于只守了大门,屋里没有巡逻。合理做法是把校验分散到登录、下单、支付等多个关键节点。
自测:为什么加固厂商更愿意"降级"而不是"直接崩溃"?
因为环境检测一定会误报,正常用户的无障碍服务、安全软件、输入法都可能被误判。直接崩溃会让大量正常用户用不了 App。降级(限制功能、二次验证、上报风控)既能提高攻击成本,又不伤害正常用户。
动手练
  • 找一个自己开发的或开源的 Android 示例 App,开启代码混淆编译一次,对比混淆前后反编译结果,数一数有多少个类名变成了单个字母。
  • 把同一个 App 的安装包复制一份,用十六进制编辑器改动资源文件里的一个字节,观察它启动时是否有异常反应,记录你的判断依据。
  • 用一张纸画出"壳、解密、真实代码"三层,标注哪一层是静态分析能看到的,哪一层必须动态观察。
加固Hardening提高逆向成本的一系列工程手段的总称。
混淆Obfuscation改变名字与结构,不改变程序行为。
壳Packer包在真实代码外面的一层加载器,运行时才解密。
VMPVirtual Machine Protection把指令翻译成自定义字节码,由解释器执行。
OLLVMObfuscator-LLVM编译期打乱控制流的工具链。
完整性校验Integrity Check检查文件或代码有没有被改动过。
反调试Anti-Debugging检测是否被调试器附加,并作出反应。
反 HookAnti-Hooking检测进程里是否存在注入框架的痕迹。
你现在应该能回答 加固改变的是"可能性"还是"成本"?混淆为什么不影响程序运行?壳的软肋在哪一层?完整性校验为什么要分散?反调试和反 Hook 分别观察什么?如果这五个问题你能用自己的话讲清楚,看到任何"加固"宣传都不会被唬住。
第 15 章 · 传输层信任

SSL Pinning 与中间人:证书信任的边界

很多人第一次抓包会遇到同一种挫败:代理装好了,证书也装好了,浏览器能抓,偏偏这个 App 一点流量都没有,或者干脆报错退出。原因几乎总是同一个:这个 App 不信任"系统里所有受信任的证书",它只信任自己认定的那一张。

中间人证书链证书固定 SPKI 哈希网络安全配置私有 CA
先记住这一句 TLS 保证的是"你在和证书对应的那一方说话",而不是"你在和真正的服务器说话"。证书固定(Pinning)补的就是这个缺口:提前把"应该是谁"写死在客户端里。
人话版 普通 HTTPS 像门卫只看身份证是不是派出所发的;证书固定像门卫手里有一张照片,必须长得一模一样才放行。

15.1中间人是怎么成立的

先说清楚一件事:中间人(Man-in-the-Middle,MITM)不是"破解"了 TLS,而是把一条 TLS 拆成了两条。

生活比喻:你以为在跟房东打电话,其实中间有个中介。中介一边跟你说"我是房东",一边跟真房东说"我是租客"。两边都以为在跟对方说话。

技术上,抓包代理干的就是中介的活:

中间人代理造成的两条独立 TLS 握手 你的手机 客户端 抓包代理 中间人 目标服务器 真实站点 ① 客户端与代理握手 ② 代理与服务器握手 ③ 服务器返回真数据 ④ 代理换证书重发 两条完全独立的 TLS 握手,客户端只看到代理的证书 代理两边都握着会话密钥,所以它看得见明文 这就是中间人能看见流量的全部原因
图 15-1 中间人不是破解加密,而是把一条加密通道拆成两条,自己站在中间

所以问题变成:客户端为什么会接受"代理的证书"?因为代理的证书也是某个受信任的机构签的——只要你在手机上装了代理的根证书,系统就认为它是合法的。

这正是抓包工具的标准流程:生成一张自己的根证书,让你装进系统信任库。从系统角度看,它和正规证书颁发机构没有区别。

15.2证书校验的三个层次

客户端验证一张证书,通常分三步,像机场安检的三道关:

证书校验的三个层次 第一层:系统信任库 这张证书是不是某个受信任机构签发的 第二层:主机名校验 证书上的域名是不是我要访问的那一个 第三层:证书固定 是不是我预先记下的那一个 不通过就断开 不通过就断开 不通过就断开 装一个自己的根证书,就能骗过第一层和第二层 但骗不过第三层:代理没有服务器私钥,也复制不出那个指纹
图 15-2 前两层是"通用规则",第三层是"这个 App 自己加的规矩"

第一层,系统信任库。手机里预装了一百多个受信任机构的根证书,操作系统和浏览器都认这份名单。你在设置里装的证书,也会进这份名单。

第二层,主机名校验。证书上写着"这个证书属于哪些域名"。如果证书是给 api.example.com 的,你却拿它去访问 pay.example.com,就要报错。

第三层,证书固定。客户端不看"谁签的",只看"是不是那一张"。这一层是应用自己实现的,不是 TLS 协议强制的。

为什么第三层有效 代理可以伪造签名(因为客户端信任了它的根证书),但代理拿不到服务器的私钥。而证书固定的比对对象,正是与私钥绑定的那部分内容。所以它能挡住绝大多数中间人。

15.3证书固定的三种类型

"固定"(Pinning)的意思是:把某个值提前写进客户端,运行时比对。固定的对象不同,严格程度和可维护性也不同。

三种证书固定方式的对比 整证书固定 对整张证书做哈希 续期换证书就失效 最严格也最脆 公钥固定 只比对公钥本身 换证书可复用公钥 要先解析证书 SPKI 哈希固定 对公钥信息算哈希 只存一串短字符串 移动端最常见 前两种要拿到证书或公钥本身,第三种只比对一个哈希值 所以主流网络库默认都用 SPKI 哈希 SPKI 指证书里的公钥信息部分
图 15-3 固定得越细越安全,但越难维护;工程上通常选 SPKI 哈希

15.3.1 整证书固定

把整张证书的字节做一次哈希,客户端存这个哈希。服务器换证书(哪怕只是续期),哈希就变了,客户端立刻连不上。

比喻:门卫手里拿着你的整张身份证复印件,你换了张新身份证(号码没变),他也不认。

所以这种方式几乎只用在极少数场景,比如固定硬件的物联网设备。

15.3.2 公钥固定

只比对证书里的公钥。证书可以换,只要公钥不变就能通过。

但公钥是一段很长的二进制,要在客户端里解析证书、取出公钥再比对,实现成本高,也容易因为解析差异出问题。

15.3.3 SPKI 哈希固定

SPKI(Subject Public Key Info)是证书里"公钥加算法标识"那一段。把它单独取出来做 SHA-256,得到一串 32 字节的值,编码成 Base64 就是一串短文本。

客户端只需要算一次哈希,和存好的字符串比一下。简单、稳定、体积小,所以 OkHttp、AFNetworking 这类库默认都走这条路。

实务提醒 正经产品一定会同时固定"当前证书"和"备用证书"(备份 pin)。否则证书一到期,全网 App 同时连不上,这是一次典型的线上事故。

15.4Android 上怎么实现

Android 有两条路:系统级配置和代码级实现。

15.4.1 网络安全配置

从 Android 7 开始,应用可以用一份 XML 声明"我信任哪些证书"。这份文件叫网络安全配置(Network Security Configuration)。

# network_security_config.xml 片段(示意)
<domain-config>
  <domain>api.example.com</domain>
  <pin-set expiration="2027-01-01">
    <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</pin>
    <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB</pin>
  </pin-set>
</domain-config>

几个要点:

  • domain 指定对哪个域名生效,可以写多个。
  • pin 写的是 SPKI 哈希的 Base64,一条当前、一条备用。
  • expiration 是保险丝:到期后固定自动失效,避免把自己锁死。
  • 还可以用 debug-overrides 只在调试包里放开限制,正式包保持严格。

15.4.2 代码级固定

OkHttp 提供了 CertificatePinner,在构造客户端时挂上去,写法大致是"域名 + sha256/哈希值"。

它的好处是粒度细:可以只对支付相关域名严格固定,对日志上报域名放宽。坏处是绕过了系统配置,排查问题时不容易一眼看出来。

注意 网络安全配置只对部分网络栈生效。有些自行实现的网络库、Native 层自己写的 TLS 代码,可能完全不读这份配置。所以"看到配置里没写 pin"不等于"没有固定"。

15.5iOS 上怎么实现

iOS 的思路类似,但实现位置不同。

  • AFSecurityPolicy:AFNetworking 提供的策略对象,可以设置"只接受指定证书"或"只接受指定公钥",并可选是否校验域名。
  • URLSession 代理校验:实现挑战回调,在服务器要求客户端验证证书时,自己读取证书链、算哈希、和内置值比对,然后决定接受还是拒绝。
  • 系统级固定:把证书放进应用的配置描述里,由系统在建立连接时强制校验。

要注意的是,iOS 的信任评估比 Android 更集中,一旦应用自己接管了校验回调,就必须自己处理证书链、有效期、域名匹配等全部细节。写得粗糙反而会引入漏洞,比如只比对了域名却没验证签名。

15.6为什么抓包会失败:错误码对照

失败信息本身就是最好的线索。不同错误对应不同的关卡。

现象或错误卡在哪一层含义
证书不受信任第一层根证书没装进系统信任库
主机名不匹配第二层证书域名和你访问的域名不一致
证书链校验失败第一、二层之间中间证书缺失或顺序错误
连接被主动关闭第三层固定校验不通过,应用直接断开

最后一行最容易被误判。应用不会告诉你"pin 校验失败",它只会默默关掉连接,或者返回一个笼统的网络错误。所以你看到"请求超时""网络异常"时,也可能是被固定挡了。

判断方法:同一台设备、同一个代理,用浏览器访问同一个域名。浏览器能通、App 不通,基本可以确定是应用层做了额外校验。

15.7企业场景下的合法中间人

中间人不等于攻击。很多正规场景必须做中间人:

  • 企业设备管理(Mobile Device Management,MDM):公司发的手机,公司需要检查有没有恶意流量。
  • 私有证书颁发机构(私有 CA):企业内网服务用自己的 CA 签发证书,员工设备预装根证书。
  • 安全网关:办公网出口做流量审计、防数据外泄。
  • 开发调试:开发阶段用代理查看自己 App 的请求。

这些场景能成立,靠的不是技术,而是知情与授权:设备是公司的、员工被明确告知、证书由公司统一管理、离职时收回。

红线 在他人设备上私自安装根证书、解密他人流量,无论技术多简单,都是违法的。企业做流量审计也必须履行告知义务,并限定在必要范围内。

15.8检测思路:双方各自怎么看

这一节讲"怎么发现问题",不讲"怎么绕过"。两者方向相反,但原理是同一套。

15.8.1 服务端能看出什么

  • 客户端证书指纹异常:如果业务用了双向认证,客户端证书就是身份,代理拿不到。
  • TLS 指纹异常:不同客户端库的握手参数有固定组合,代理转发的握手往往和真实客户端不一致。
  • 请求特征突变:同一个账号,平时用移动网络,突然所有请求都来自一个机房 IP,且头部顺序变了。
  • 时间与顺序异常:代理会引入额外延迟,某些强时序校验会对不上。

15.8.2 客户端能自检什么

  • 证书链是否来自预期签发者:不只看"是否受信任",还看"是谁签的"。
  • 是否存在多余的根证书:检查用户证书库里有没有不该有的条目。
  • 连接是否被转发:检查系统代理设置、检查网络接口是否异常。
  • 自身完整性:包名、签名、代码段是否被改动。

注意最后一条:客户端的自检代码本身也在被保护的范围内,所以这是一个不断加码的循环。检测越细,误报越多;误报越多,越需要"降级"而不是"直接拒绝"。

15.9两个常见误区

误区一:pinning 能防住所有分析。固定只能保证"客户端只和持有特定私钥的服务器通信",它管不了内存里的明文。请求发出去之前,参数一定是明文;响应回来之后,也一定是明文。所以动态分析依然能看到业务数据,只是看不到"线上那一份原始报文"。

误区二:关掉校验就万事大吉。很多应用不只在一处校验。关掉网络库的固定,可能还有 Native 层的校验、还有服务端的证书指纹检查、还有请求签名里绑定的通道信息。改一处往往只是让错误换了个样子。

还有一个更隐蔽的误区:以为"能抓到包"就等于"看懂了协议"。抓包只给你原料,字段含义、签名算法、加密参数,仍然要靠分析和验证。

15.10合规提醒

证书固定是保护用户的技术,不是攻击工具。使用它的场景应该是:保护自己的 App 不被中间人劫持,尤其是在公共 Wi-Fi、恶意热点环境下。

  • 只对自己开发的 App 或书面授权的目标做抓包分析。
  • 不要在他人设备上安装根证书。
  • 抓到的流量里可能包含个人信息,按最小必要原则处理,用完即删。
  • 企业内部审计必须告知员工,并限定范围与保存期限。
自测:中间人为什么能"看见"加密流量?
因为它把一条 TLS 拆成了两条:客户端和代理一条,代理和服务器一条。代理在两条通道里都是合法的通信方,两边都握着会话密钥,所以两边的内容它都能解密。它不是破解了加密,而是站在了两段加密的中间。
自测:为什么证书固定能挡住中间人,即使客户端已经装了代理的根证书?
因为固定比对的是与服务器私钥绑定的内容(整证书或公钥信息的哈希)。代理没有服务器私钥,签不出同样的公钥,也复制不出那个哈希。装了根证书只能骗过"谁签的"这一层,骗不过"是不是那一张"这一层。
自测:SPKI 哈希固定为什么比整证书固定更适合移动端?
整证书固定一换证书就失效,续期都可能造成全量用户断网;SPKI 哈希只绑定公钥信息,换证书时公钥可以复用,而且客户端只需要比对一个短字符串,实现简单、体积小。工程上还会配一条备用 pin 兜底。
自测:App 里所有请求都超时,浏览器访问同一域名却正常,最可能是什么原因?
应用层做了额外校验,最典型的就是证书固定。浏览器只做前两层校验,App 多了第三层,代理的证书过不了。这不是网络问题,是信任边界问题。
动手练
  • 用自己的电脑搭一个本地 HTTPS 服务(自签证书),用浏览器访问一次,记录报错文字,判断它卡在第几层。
  • 查一份公开的证书(比如任意一个网站),找出它里面的公钥信息部分,理解"整证书"和"公钥"的区别。
  • 画一张图,标出"浏览器抓包能成功、App 抓包失败"时,两条路径分别经过了哪几层校验。
中间人Man-in-the-Middle夹在通信双方之间,分别与两边建立连接。
证书链Certificate Chain从站点证书逐级上溯到根证书的信任路径。
证书固定Certificate Pinning客户端只信任预先写死的那一张证书或公钥。
SPKISubject Public Key Info证书中承载公钥及算法标识的那一段。
根证书Root Certificate信任链的起点,系统或用户手动信任。
私有 CAPrivate CA企业自建、只在内网使用的证书颁发机构。
双向认证Mutual TLS客户端和服务器互相出示证书验证身份。
网络安全配置Network Security ConfigAndroid 用 XML 声明信任规则的文件。
你现在应该能回答 中间人为什么能看见明文?证书校验分哪三层?三种固定方式各固定了什么?为什么抓包失败要先看错误文字?企业做中间人靠什么才合法?这五个问题答得出来,你就不会再被"抓不到包"卡住。
第 16 章 · 风控与指纹

设备指纹与风控:八类参数与一致性

你有没有想过:为什么换了个手机号注册,平台还是知道"又是你"?为什么刚注册的新号,第一次下单就被要求人脸验证?答案通常不是某个神秘的"唯一编号",而是一堆普通参数拼出来的画像,再加上它们之间的一致性。

稳定性 / 唯一性八类参数JA3 / JA4 一致性校验关系图谱最小必要
先记住这一句 指纹不是"一个编号",而是一组参数的组合,加上这些参数彼此是否自洽。真正难伪造的不是单个值,而是一整套互相对得上的细节。
人话版 单看身高认不出人,但身高加口音加笔迹加走路姿势,就很难装成别人了。指纹就是把这套"细节组合"搬到设备上。

16.1什么是指纹:稳定和唯一的拉扯

设备指纹(Device Fingerprint)的定义很朴素:从一台设备上采集一组参数,用来在多次访问之间认出"这是同一台"。

它有两个目标,而这两个目标天然冲突:

  • 稳定性:同一个用户下次来,这些值最好别变。
  • 唯一性:不同用户之间,这些值最好别撞。

比喻:用"身高"当指纹,很稳定(成年后基本不变),但一点都不唯一(一米七的人太多了);用"今天穿的衣服"当指纹,很唯一,但明天就失效了。

稳定性与唯一性的二维权衡 稳定性高 唯一性高 IMEI Android ID GAID Canvas IP 地址 时区 越靠右上,越适合当风控依据;越靠左下,越没有参考价值
图 16-1 没有完美的单点,所以真实系统永远用组合

一个好的指纹要同时满足四个条件:稳定、唯一、难伪造、可采集。现实中没有任何一项能全占,所以工程上用的是"多项加权":单个值只贡献一点分数,组合起来才有判断力。

16.2八类参数:从"机器是什么"问到"它和谁有关系"

把常见参数归归类,一共八类。前四类描述设备本身,后四类描述"这台设备在做什么、和谁有关"。

设备指纹的八类参数 从「这台机器是什么」一直问到「它和谁有关系」 硬件 CPU 核数 内存大小 传感器列表 系统 系统版本 内核版本 安全补丁 网络 IP 与运营商 DNS 与代理 Wi-Fi 名称 应用 包名与版本 安装来源 已装应用列表 行为 点击节奏 操作路径 页面停留 环境 时区与语言 屏幕与亮度 电量与充电 账号 手机号邮箱 注册时间 历史登录 关系 账号与设备 设备与 IP IP 与支付 单看任何一类都可能被伪造,交叉起来就很难同时自洽
图 16-2 前四类回答"它是什么",后四类回答"它在干什么、和谁有关"

注意最后两类。很多初学者以为指纹就是"设备参数",其实行为和环境才是最难伪造的部分。设备参数可以改,但一个人的操作习惯、作息时间、常驻地点,改起来成本高得多。

16.3Android 标识的变迁:为什么一直在换

Android 的设备标识换过好几代,每一次都是隐私压力和商业需求拉扯的结果。

标识范围怎么变
IMEI整机唯一硬件写死,普通应用已读不到
Android ID应用 + 用户 + 设备恢复出厂会变,同签名重装不变
GAID整机,可重置用户可手动重置,关闭个性化广告后失效
OAID整机,可重置国内厂商联合方案,系统设置里可重置

IMEI 是硬件串号,最稳定也最敏感。从 Android 10 起,普通应用已经拿不到它,只有系统级应用或设备所有者才有权限。

Android ID(也叫 SSAID)改成按"应用签名 + 用户 + 设备"派生。这意味着同一台手机上的两个不同 App,读到的值是不一样的,横向串联被切断了。

GAID(Google 广告 ID)是给广告场景用的,用户可以随时重置,也可以关闭个性化广告。关闭后返回的是一串全零,等于"我不给你"。

OAID 是国内厂商联合推出的匿名设备标识,思路一样:可重置、可关闭,用匿名性换可用性。

为什么一直在变 因为"跨应用追踪用户"这件事,商业上很有价值,隐私上很有争议。监管一收紧,平台就换一个更"匿名"的方案。结果是指标越来越不稳定,风控不得不转向组合判断。

16.4iOS:IDFV、IDFA 和钥匙串

iOS 的思路和 Android 类似,但更早收紧。

  • IDFV(Identifier for Vendor):同一个开发商的所有 App 共享一个值。删掉该开发商全部 App 后重置。
  • IDFA(Identifier for Advertisers):广告标识。iOS 14.5 之后必须用户明确授权才能读取,未授权返回全零。
  • 钥匙串(Keychain):系统级的加密存储。删除 App 后数据可能仍然保留,所以常被用来存"持久标识"。

钥匙串这一点值得多说一句。它的设计初衷是"让同一开发商的 App 安全共享密码",但被拿来当持久标识之后,就变成了一个隐私争议点:用户以为卸载干净了,实际上痕迹还在。

16.5浏览器指纹:不用任何 ID 也能认出你

浏览器没有 IMEI,也没有 GAID,但依然能被认出来。方法是采集一堆"渲染细节"。

项目采集方式稳定性
Canvas画一段文字和图形,读像素较高,换显卡会变
WebGL读显卡型号与渲染器字符串高,硬件绑定
Audio跑一段音频处理,读输出较高
字体列表测量各种字体的渲染宽度中,装软件会变

原理是:同样的代码,在不同的显卡、驱动、字体、操作系统上,渲染出来的像素会有微小但稳定的差异。这个差异就是签名。

比喻:同一句话让不同的人手写,字迹不同;同一段代码让不同的机器渲染,像素也不同。

再叠加时区、语言、屏幕分辨率、是否装了某些插件,几十个维度组合起来,碰撞概率就非常低。有研究指出,仅 Canvas 一项在普通用户群体里的区分度就相当高,加上其他维度后接近唯一。

16.6TLS 指纹:握手姿势也会暴露你

就算你把浏览器指纹全改了,还有一个地方很难改:TLS 握手时发送的那一堆参数。

TLS 客户端第一次说话时,会发一个"你好"消息(ClientHello),里面包含:支持的版本、一串密码套件、一串扩展、支持的椭圆曲线、曲线点格式。

不同的客户端库(不同版本的浏览器、不同语言的网络库)会给出不同的组合。把这些字段按固定顺序拼成一个字符串,再做哈希,就得到一个短标识。

TLS 指纹的生成过程 ClientHello 客户端的第一句话 含五个关键字段 ① 版本 ② 密码套件列表 ③ 扩展列表 ④ 曲线 ⑤ 点格式 拼接 用逗号连起来 哈希 32 位十六进制 这个字符串和"你用的是哪个网络库"高度相关,和"你是谁"无关 早期方案用 MD5,只取上面五个字段,业界习惯叫 JA3 更新的方案结构更细、抗误判更强,习惯叫 JA4 它能识别"这是不是一个真实的手机 App 在说话" 但对"这是哪一个人"帮助有限,所以要和别的参数一起看
图 16-3 TLS 指纹识别的是一类客户端,不是某一个用户

它的价值在于识别"客户端类型"。比如服务端发现:同一个接口,九成请求的 TLS 指纹都属于某 App 的官方库,只有一小撮请求的指纹属于某个脚本库——这一小撮就值得重点看。

但它不能单独用来认人。同一个版本的 App,几百万用户共享同一个指纹。把它当"唯一 ID"用,是典型误用。

16.7HTTP 头的顺序和大小写

还有一个更细的层面:HTTP 请求头的顺序、大小写、有没有多余空格。

正规的网络库通常按固定顺序输出头,大小写也统一。而用脚本手工拼出来的请求,顺序往往和官方库不一致,甚至会出现不该有的头。

比喻:伪造一封信,内容可以抄得一字不差,但信封的折法、邮票贴的位置、笔迹的力度,往往会露馅。

服务端把"头顺序 + 头名大小写 + 头集合"作为一个整体特征,和账号、设备、IP 一起看,就能发现"这个请求不是这个 App 发的"。

16.8一致性校验:矛盾才是信号

前面讲的都是"采集"。真正决定风控效果的,是参数之间对不对得上。

单点异常很常见(用户换了网络、装了新输入法),所以不会直接判定。但多个维度同时矛盾,就很难解释了。

三类典型的一致性矛盾 声明与实际不符 自称低端机 却每秒两百次请求 时区与 IP 不符 系统时区东八区 出口 IP 在另一洲 设备与行为不符 全新设备 直接大额下单 综合评分:单点可疑不定罪,多点矛盾才拦截 这是所有风控系统的基本判断方式 一条矛盾通常只降权,三条以上互相印证的矛盾才触发验证或拒绝
图 16-4 风控不是找"坏人特征",而是找"解释不通的组合"

常见的矛盾类型有四组:

  • 性能与行为:参数显示是低端机,行为却是高频并发。
  • 时间与地点:时区、语言、IP 归属地三者互相打架。
  • 新与重:设备是全新的,但操作路径像老手,直接找到最深的功能。
  • 账号与设备:一个新设备,登录了一堆注册时间接近、行为相似的账号。

这些矛盾单看都不致命,组合起来就有说服力。风控的本质是概率判断,不是事实认定,所以它给出的通常是"分数"和"动作",而不是"结论"。

16.9关系图谱:从单点走向网络

单台设备的信息再多,也有上限。真正的识别能力来自关系。

把账号、设备、IP、支付方式、收货地址都当成"点",把它们之间的使用关系当成"线",就得到一张图。风控在这张图上找"团伙":

  • 多个账号共用同一台设备;
  • 多个设备共用同一个支付账号;
  • 一批账号在短时间内从同一批 IP 登录;
  • 多个账号填写的收货地址高度重合。

比喻:单看一个人可能毫无问题,但一看他的人际关系图,全是同一伙人,那就值得注意了。

图谱的价值在于抗伪造:你可以改设备参数,但改不了"你的支付账号和另外二十个账号绑在一起"这个事实。

16.10隐私合规:能哈希就不存明文

指纹技术天然涉及个人信息,所以合规不是可选项。

  • 告知同意:采集哪些参数、用来做什么、保存多久,要说清楚。
  • 最小必要:能不用 IMEI 就不用,能用匿名标识就不用硬件号。
  • 能哈希不存明文:设备号、手机号这类值,存哈希比存原文安全得多。
  • 可撤回、可删除:用户要求删除时,要能真正删掉。

哈希也有讲究。手机号只有十一位,直接做一次 SHA-256 是可以被穷举的——攻击者把所有可能的号码都算一遍,就能反查。所以正规做法是加盐(在原文里混入一段随机值)或用带密钥的哈希。

注意 "匿名化"不等于"去标识化"。把名字去掉但保留了设备号,仍然可以重新识别到人。真正的匿名化要求无法通过任何合理手段复原。

16.11两个最常见的误区

误区一:改一个参数就能换身份。很多人以为改掉设备号就"变成新设备"了。但风控看的是组合:设备号变了,硬件参数没变、行为习惯没变、IP 没变、支付方式没变,几十个维度里只有一个变了,反而更可疑——正常用户换手机,是整机参数一起换。

误区二:指纹等于唯一 ID。指纹是一组概率性的特征,不是数据库主键。它会有碰撞(两台设备算出一样的组合),也会有漂移(系统升级后特征变了)。所以工程上用的是"置信度",不是"是或否"。

还有一个相关误区:以为指纹只在客户端采集。实际上服务端能看到的东西同样多,甚至更多——TLS 指纹、请求头顺序、时序特征,这些客户端根本改不了。

判断口诀 看到"设备指纹",先问三句:采了哪几类?哪几个是稳定的?它们之间对得上吗?三句问完,你就知道这套系统的强度在哪。
自测:为什么没有"完美的单一设备标识"?
因为稳定性和唯一性天然冲突,而且还要考虑隐私与可采集性。越稳定的标识(如硬件串号)越敏感、越受限;越开放的标识(如广告 ID)越容易被重置。所以工程上只能用组合,用概率判断。
自测:Android ID 为什么不能用来跨应用追踪同一台手机?
因为它按"应用签名 + 用户 + 设备"派生。同一台手机上的两个不同 App,读到的值不一样。这是系统层面对横向串联的切断。
自测:TLS 指纹能识别"人"吗?
不能单独识别。它反映的是客户端库的类型和版本,同版本的几百万用户共享同一个指纹。它的价值是识别"这是不是官方客户端",需要和设备、账号、行为等维度一起用。
自测:为什么"只改一个参数"反而更容易被识别?
因为风控看的是组合的一致性。正常用户换设备时,硬件、系统、应用列表会整体变化;只有一个维度突变、其余全部不变,属于典型的矛盾特征。矛盾比异常更值得警惕。
动手练
  • 列一张表,写出你手机上能想到的十项参数,标注每一项"多久会变一次"。
  • 用浏览器打开一个检测指纹的公开演示页(自己搜索即可),看看它报出了几项,哪几项你觉得最稳定。
  • 画一张三列的关系图:账号、设备、支付方式,用线连出你自己的生活场景,体会"关系比单点更有辨识度"。
设备指纹Device Fingerprint一组设备参数的组合,用来在多次访问间识别同一台设备。
稳定性Stability同一设备在不同时间采集,值保持不变的程度。
唯一性Uniqueness不同设备之间取值不重复的程度。
SSAIDAndroid ID按应用签名、用户、设备共同派生的 Android 标识。
GAIDGoogle Advertising ID可重置的广告标识,关闭个性化广告后失效。
IDFVIdentifier for Vendor同一开发商应用共享的 iOS 标识。
JA3 / JA4把 TLS 握手字段拼接后哈希得到的客户端指纹方案。
一致性校验Consistency Check检查多个参数之间是否互相矛盾。
你现在应该能回答 指纹为什么不是"一个编号"?稳定性和唯一性为什么冲突?Android 的四个标识各有什么限制?TLS 指纹能说明什么、不能说明什么?为什么"多处矛盾"比"单点异常"更值得警惕?这五个问题想清楚,你就理解了风控的骨架。
第 17 章 · 实战链路

纯协议登录:一个完整案例的全链路

不启动 App,只用代码把登录过程重做一遍,到底要发几个请求、带哪些字段、在哪一步判断成败?这一页用一个脱敏的电商类 App 做样本,把六个请求串成一条可以逐步验证的链路,并告诉你什么才算真正跑通。

纯协议设备注册预登录 签名Token端到端判据
先记住这一句 纯协议登录的难点从来不是"把请求发出去",而是让服务端相信这个请求来自一台真实、连续、没被改过的设备;判断成功的标准也不是 HTTP 200,而是拿到 Token 之后能取到非空的业务数据。
人话版 这一页把登录拆成六次寄信:报户口、领题目、交答卷、拿门禁卡、查自己资料、办正事。少一步或答错一题,后面全不通。

17.1什么叫"纯协议"

"纯协议"这三个字,指的是不启动 App、不点界面,直接用代码构造 HTTP 请求,把 App 在背后做的事情在命令行里重做一遍。它不需要手机,也不需要模拟器,只需要一台能联网的电脑。

你可以把它想成不去柜台排队,而是照着柜台的表单格式,把材料直接寄过去。表单的栏目、顺序、盖章位置都必须对,错一项就被退回。

要跑通它,至少需要三样东西:

  • 看得懂请求:知道每一步发到哪个路径、带哪些字段,这来自抓包;
  • 算得出签名:请求里通常有一个随时间变化的签名字段,这来自逆向或算法还原;
  • 保得住会话:登录成功后拿到的 Token 要能存下来、带上去、及时换。
常见误区 很多人以为纯协议就是"把抓到的请求复制成 curl,然后点运行"。实际上抓到的请求里通常有一个几分钟就过期的时间戳、一个一次性的随机数,原样重发十有八九会被拒。纯协议不是复制,而是重建。

17.2案例背景:一条被拆成六步的登录链

样本是一个电商类 App 的账号密码登录。为了避免指向任何真实系统,下文域名统一写成 api.example.com,路径和字段名都做了替换,数值只作示例。

这个 App 的登录不是"一个请求搞定",而是六个请求前后依赖,像一条流水线:

  1. 设备注册:先告诉服务端"我是一台什么设备",换回一个设备编号;
  2. 预登录:领取本次登录专用的盐值和挑战码;
  3. 登录:提交账号、密码派生值、挑战答案和签名;
  4. 换取 Token:用登录结果换访问凭证;
  5. 拉用户信息:验证这张凭证是不是真的能用;
  6. 业务接口:取购物车、订单等真实数据,确认整条链真的通了。

依赖关系很清楚:第 1 步的 deviceId 是后面每个请求的必需参数;第 2 步的盐值和挑战码决定第 3 步的签名内容;第 4 步的 Token 决定第 5、6 步能不能过。

这条链有个很坑的特点:任何一步失败,后面都会以"参数缺失"或"未授权"结束,而不是告诉你真正的原因。所以排错必须从第一步开始,不能从报错的那一步猜。

17.3全链路时序:六个请求的先后关系

纯协议登录六个请求的时序 你的脚本 模拟客户端 服务端 API 网关 ① 设备注册:上报机型与系统,返回 deviceId ② 预登录:请求盐值与挑战码 ③ 登录:提交账号、签名、挑战答案 ④ 拿 Token:服务端校验后发放凭证 ⑤ 拉用户信息:验证 Token 是否真能用 ⑥ 业务接口:取购物车等真实业务数据 六条箭头都由客户端发起,服务端在每条之后返回结果;第 1 步的 deviceId 后面每步都要带
图 17-1 登录是一条链,不是一次请求;任何一环断开,后面都只会报"未授权"

为什么第一步必须是设备注册?因为服务端需要给你发一个稳定的编号,用来关联后续的登录、风控和行为记录。没有这个编号,登录请求会被当成"来路不明",很多系统会直接返回一个含糊的错误。

为什么要有预登录?如果每次登录都用同一个固定签名,别人抓一次就能无限重放。预登录下发一次性的盐值和挑战码,让每次的签名都不一样。这就像考试前先领一张带编号的答题卡,抄上次的答案没有用。

17.4每一跳的请求结构

下面这张表只列决定成败的字段。真实请求还会有一些通用头,例如内容类型、客户端标识,它们不参与核心判断,但缺失时同样可能被拒。

步骤方法与路径关键字段作用
① 注册POST /v1/device/registermodel、osVer、installId、ts、sign建立设备档案,换 deviceId
② 预登录POST /v1/auth/preloginaccount、deviceId、ts、sign领取 salt 与 challenge
③ 登录POST /v1/auth/loginaccount、pwdHash、answer、nonce、sign提交凭证,服务端校验
④ 换 TokenPOST /v1/auth/tokenticket、deviceId、sign换 accessToken 与 refreshToken
⑤ 用户信息GET /v1/user/profileAuthorization、deviceId确认 Token 可正常使用
⑥ 业务数据GET /v1/cart/listAuthorization、deviceId、page取真实数据,判断链路真通
补充 第 ④ 步不一定单独存在。有些系统在登录响应里直接返回 Token,有些则先给一张短期的 ticket,再换正式 Token。看接口文档不如看真实响应,以抓到的返回结构为准。

17.5签名参数从哪来

签名的本质,是给这一堆参数按一个只有双方知道的规则算一个"指纹"。服务端收到后按同样规则再算一遍,两边一致才认。所以签名的输入必须双方都能独立算出来。

这个案例里,签名用到四类输入:

  • 时间戳 ts:10 位秒级数字。服务端只接受与服务器时间相差 300 秒以内的请求,过期直接拒,用来压缩重放的窗口;
  • 随机数 nonce:每次请求重新生成。服务端短期缓存用过的值,重复即拒,用来防止同一时间窗内的重复提交;
  • 设备标识 did:注册时拿到。它把签名和具体设备绑在一起,换设备后旧签名就失效;
  • 请求体哈希 bodyHash:把请求体按固定规则序列化后做 SHA-256。任何一个参数被改,哈希就变,签名随之失效。
签名的四类输入如何合成一个签名字段 时间戳 ts 10 位秒级数字 随机数 nonce 每次重新生成 设备标识 did 注册时获得 请求体哈希 body 的 SHA-256 按参数名字典序排序,再用固定分隔符拼成一条字符串 排序规则、分隔符、编码方式必须与服务端完全一致 HMAC-SHA256(密钥, 串) 密钥来自本地或服务端下发 sign = 十六进制串 放进请求头或请求体 任何一处与服务端不同,签名就对不上;逆向时这些细节比算法本身更容易出错
图 17-2 签名的四个输入缺一不可,拼接规则比算法更容易踩坑

用伪代码写出来大概是这样,注意这里只是示意,真实项目里参数名和规则都不一样:

# 伪代码:算一次签名
params = {
    "account": user,
    "deviceId": did,
    "nonce": rand_hex(16),
    "ts": now_seconds(),
    "bodyHash": sha256(canonical_json(body)),
}
raw = "&".join(k + "=" + params[k] for k in sorted(params))
sign = hmac_sha256(secret, raw).hex()

# 结果放进请求,body 原样发送
headers["X-Sign"] = sign
headers["X-Ts"] = params["ts"]

密钥从哪来?三种常见情况:编译在本地库文件里、由服务端在预登录时下发、或者设备注册时协商。算法版本号也会随 App 升级变化,所以签名逻辑通常需要持续维护,而不是一次写好就永久有效。

17.6会话保持:Token 放哪里,什么时候刷新

登录成功后拿到的 Token,就是后面所有请求的通行证。它通常放在请求头里,最常见的形式是 Authorization: Bearer 一串字符,也有放在自定义头或 Cookie 里的。

有效期各家不同,常见是 2 小时到 7 天。系统一般还会给一个有效期更长的刷新凭证,用来在不重新输密码的情况下换新 Token。

  • 主动刷新:在到期前 5 分钟换一次,避免请求打到一半过期;
  • 被动刷新:收到 401 时先刷新一次,刷新也失败才重新登录;
  • 单会话限制:很多系统同一账号只允许一个有效 Token,重复登录会把旧的顶掉。
注意 批量任务共用同一个账号时,最容易遇到"互相踢下线":A 登录后 B 登录,A 的 Token 立刻失效,A 又去重登,循环往复。一个账号一个会话,会话状态集中管理,不要每个任务各自登录。

17.7端到端判据:不是 HTTP 200

初学者最容易在这里误判:看到状态码 200、看到返回里有一个 code: 0,就认为登录成功。其实网关只要把请求转给后端就会回 200,业务失败也常常包在 200 里,用响应体里的业务码表示。

端到端成功的三道门 ① HTTP 200 只说明请求被收到 不代表业务成功 ② 拿到 Token 字段存在 长度与格式合理 ③ 业务数据非空 用户信息或列表 有真实内容 停在这里:假成功 到这里:端到端通过
图 17-3 三道门都过了才算成功;只过第一道是最常见的自欺

为什么"有 Token"也还不算?因为 Token 可能是空串、可能是过期占位符、也可能权限不足。真正的判据是:调一个必须登录才能访问的业务接口,返回的数据字段非空且结构符合预期。

把判据写成断言,程序才能自己判断成败,而不是靠人看日志:

  • 状态码等于 200,且响应体能正常解析;
  • Token 字段存在,长度大于 20,字符集符合预期;
  • 用户信息里 uid 非空,昵称或手机号后四位存在;
  • 购物车列表能解析成数组,且元素数量与账号状态相符。

17.8失败分支与错误码

排错的第一步不是改代码,而是把失败分类。同样是"登录失败",原因可能完全不同,处理方式也相反。

现象可能原因该怎么做别做什么
HTTP 401Token 过期或被顶掉刷新一次,失败再重登无限重试
HTTP 403风控拦截或权限不足停下,核对设备与频率换地址硬撞
HTTP 429触发限流指数退避,降低并发加机器加压
code 1001账号或密码错误记录并跳过该账号反复试密码
code 1002要求图形或短信验证转人工处理尝试自动识别
code 1003设备信息异常逐项对照注册字段只改一个字段

这张表的价值在于:它把"重试"和"不重试"分开了。401 值得刷新一次,429 值得等一会儿,403 和 1002 继续重试只会让情况更糟。

17.9和"自动化点界面"比,差在哪

两种方式都能完成登录,但适用的场景完全不同。纯协议不是"更高级",它只是把变量换了一个位置。

维度纯协议自动化点界面
单次耗时0.1 到 0.5 秒3 到 10 秒
稳定性只依赖接口是否变化依赖界面、分辨率、弹窗
可观测性每个字段都能记录只能看截图和录屏
资源占用一个进程可跑多个任务一台设备或一个模拟器
维护成本接口变了改代码界面改版就要改脚本
适合场景自有或授权目标的批量任务一次性人工操作

纯协议的优势是快、可观测、省资源;代价是你必须完全理解签名和会话,任何一个字段算错都跑不通。界面自动化相反:不用懂协议,但慢、脆、难排查。

17.10合规边界:这条链路只能用在哪里

这一页讲的是原理和工程方法,落到实践必须满足三个前提:

  • 目标自有或书面授权:自己的系统、客户明确授权的系统、公开教学靶场;
  • 遵守平台规则与法律:不碰他人账号,不采集超出范围的数据,不做影响服务稳定的事;
  • 数据最小必要:日志里不存明文密码,手机号、地址等个人信息脱敏或哈希后再保存。
红线 没有授权就去分析别人的系统、拿别人的账号做实验、把抓到的个人信息存下来,这些都不是"技术问题",而是法律问题。技术能力越强,越要先确认边界。
自测:为什么登录前通常要先做设备注册?
因为服务端需要一个稳定的设备编号,把后续的登录、风控和行为记录关联起来。没有这个编号,登录请求会被当成来路不明。设备注册是整条链的地基,不是可选步骤。
自测:为什么不能把抓到的请求原样重发?
因为请求里通常有时间戳和一次性随机数。时间戳超出服务端允许的偏差就被拒,随机数用过一次就进黑名单。原样重发在几分钟内就会失效。要重发,必须能重新算出签名。
自测:返回 HTTP 200 且 code 为 0,为什么还不能算成功?
200 只表示请求被服务器收到并处理过,业务失败也常常包在 200 里。code 为 0 也可能只是"流程受理",Token 为空或权限不足。真正的判据是调用需要登录的业务接口,能取到非空且结构正确的数据。
自测:遇到 403 和 429,处理方式有什么不同?
429 是限流,说明请求太快,应该指数退避、降低并发,等一等通常能恢复。403 是拒绝,可能是风控或权限问题,继续重试只会加重怀疑,应该停下来核对设备和频率。一个该等,一个该停。
动手练
  • 用本地起一个模拟服务,按这一页的六步写出六个接口,先用固定返回值跑通顺序。
  • 给自己的模拟服务加一个时间戳校验,超过 60 秒的请求返回 401,观察脚本怎么失败。
  • 把六个请求画成一张时序图,标出每一步依赖上一步的哪个字段。
  • 为这张图配一张错误码表,写清哪些错误重试、哪些直接停。
纯协议Protocol-only不启动 App,直接用代码构造请求完成交互。
设备注册Device Register首次使用时上报设备信息,换回唯一设备编号。
预登录Pre-login正式登录前领取一次性盐值与挑战码的步骤。
盐值Salt参与运算的随机值,让每次结果都不同。
随机数Nonce一次性使用的随机串,服务端记住用过的值防重复。
签名Signature对参数按约定规则算出的指纹,用于校验内容未被改动。
会话Session登录后由 Token 维持的一段可信状态。
端到端End-to-end从发起请求到拿到真实业务结果的完整验证。
你现在应该能回答 六个请求的顺序和依赖是什么?签名由哪四类输入组成?Token 该放在哪里、什么时候刷新?为什么 200 不算成功?403 和 429 分别该怎么处理?这五个问题能讲清楚,你就能自己拆解一条真实的登录链。
第 18 章 · 实战链路

签名与风控包:构造原理与一致性

服务端看不见你的手机,它凭什么判断一个请求"像人发的"?答案藏在客户端随请求附带的那一包字段里。这一页讲清风控包的分层结构、签名链路,以及为什么单点伪装没有用。

风控包设备指纹签名链路 一致性时间戳 / nonce降级验证
先记住这一句 风控包的价值不在某一个字段,而在字段之间的相互印证;签名保证内容没被改,一致性保证这份"体检报告"不像临时编的。两者叠加,伪造的成本才会真正上升。
人话版 风控包像酒店入住登记表:姓名、证件、电话、同行人。单看每项都普通,几项一对不上,前台立刻起疑。

18.1风控包是什么:一份客户端自报的体检报告

你打开 App,服务端其实什么也看不见。它不知道你的手机是什么型号、屏幕多大、在哪个时区、刚才点得快不快。这些信息只能由客户端自己报上去。

客户端上报的这组信息,通常塞在请求头的一个 JSON 字段里,或者放在请求体的固定位置。业内叫法不统一,有叫设备信息、有叫风险参数、有叫指纹包,这一页统称"风控包"。

它不是 HTTP 标准的一部分。每个公司自己定义字段名、编码方式和加密方式,所以你在不同 App 上看到的形态完全不一样。这也意味着没有通用模板,只能一个目标一个目标地看。

为什么要有这个东西?因为"自报"天然不可信。服务端能做的,是让自报的细节足够多、彼此足够自洽,从而提高伪造的代价。它不追求"证明你是好人",只追求"让造假变得不划算"。

18.2风控包一般包含哪几类字段

虽然字段名千差万别,但按用途归类,基本逃不出六类。理解分类比记住字段名有用,因为字段会变,分类不会。

风控包的六类字段 设备 机型、分辨率、传感器 环境 时区、语言、亮度 行为 点击、滑动、停留 风控包 随请求一起上报 应用 版本、渠道、安装来源 网络 运营商、Wi-Fi、IP 账号 注册时长、历史设备
图 18-1 六类字段从不同角度描述同一个客户端,服务端做的是交叉比对
类别典型内容服务端用它判断
设备机型、屏幕分辨率、CPU 核数、传感器数量是不是真实设备,是否模拟器
环境时区、语言、系统版本、亮度、字体列表环境是否自洽,是否被改动
行为点击间隔、滑动轨迹、页面停留、输入节奏操作像人还是像脚本
应用版本号、渠道、安装来源、首次安装时间是否官方包,是否刚装上
网络运营商、Wi-Fi 标识哈希、IP、代理特征地理位置,是否经过代理
账号注册时长、历史登录设备数、近期操作这个账号本身是否异常

18.3为什么字段这么多:细节密度就是成本

一个字段很好编,十个字段就难了,几十个彼此关联的字段几乎编不圆。这就是风控包设计的核心逻辑:用细节密度换伪造成本。

举个具体例子。一台真实手机上报的字段是相互约束的:

  • 机型决定了屏幕分辨率的合理范围;
  • 分辨率决定了像素密度和屏幕尺寸;
  • 系统版本决定了可用的接口和字体集合;
  • 时区决定了语言习惯和当前时间;
  • 传感器数量和型号决定了这台设备能不能"感知"移动。

如果你只改机型,分辨率却还是五年前小屏机的值,传感器数量是 0,系统版本又是最新的,这三条放在一起就互相打架。服务端不需要知道哪一条是假的,只要发现"这个组合在真实设备里不存在",就可以打低分。

补充 字段不是越多越好。采集受法律和平台政策约束,要遵循最小必要原则;字段太多还会增加传输体积和解析成本。真正有效的不是数量,而是字段之间的约束关系。

18.4签名与加密:六个层次别混在一起

很多人把编码、哈希、加密、签名混成一件事。它们在链路里各占一层,作用完全不同。先看一条完整的处理链:

从明文参数到传输的六个处理层次 ① 明文参数 账号、设备、业务字段 ② 排序 按参数名固定顺序 ③ 拼接 固定分隔符连成串 ④ 哈希 / HMAC 单向,改一字节结果全变 ⑤ 加密 可逆,需要密钥 ⑥ 经 TLS 传输 编码不是加密:Base64 谁都能还原 签名不保密内容,只证明内容没被改
图 18-2 编码、排序、哈希、加密各司其职,混为一谈就会算错签名

三件事必须分清:

  • 编码(如 Base64):只是换一种写法,任何人可还原,不提供任何安全性;
  • 哈希(如 SHA-256):单向,改一个字节结果全变,用来防篡改;
  • 加密(如 AES):可逆,需要密钥,用来防偷看。

常见组合是:参数排序拼接后做 HMAC 得到 sign,请求体整体加密,最外层再走 TLS。签名本身是公开传输的,这没问题——它只需要服务端能验证,不需要保密,只要密钥不泄露。

18.5时间戳、nonce、序列号,各自防什么

这三个字段经常一起出现,初学者容易觉得重复。其实它们防的是三种不同的攻击。

字段防什么失效条件
时间戳 ts重放很久以前的请求超出允许时间窗,常见正负 300 秒
随机数 nonce同一时间窗内重复提交服务端已缓存过这个值
序列号 seq乱序、丢包后重放小于等于服务端记录的上次值

打个比方:时间戳是门票上的日期,过期作废;nonce 是每张票的座位号,同一座位不能坐两次;序列号是排队号,叫过号的不能再插队。三者叠加,重放的窗口被压到极小。

反过来说,如果你只带时间戳不带 nonce,那么在 300 秒的窗口内,同一个请求可以被重复使用成百上千次。这就是为什么很多系统宁可多传一个字段,也要把窗口堵死。

18.6三条构造路径:录制、参数化、纯算法

想让程序自动发出"看起来合法"的请求,历史上只有三条路。它们的稳定性差了一个数量级。

路径做法稳定性主要代价
录制回放原样重发抓到的请求很低,几分钟后失效几乎为零
参数化重放替换可变字段,其余照抄中等,取决于能否重算签名要定位签名输入
纯算法复现本地实现签名与加密高,可长期使用需要逆向并持续维护

第一条路只能验证"请求长什么样",不能用于持续运行。第二条路听起来聪明,但有个陷阱:只要改了参与签名的字段,签名就必须重算。所以"参数化"最终还是会撞上算法。

第三条路才是工程上可用的方案,代价是要把算法、密钥、拼接规则全部还原出来,并且随着 App 升级持续跟进。这也是为什么协议分析里,最花时间的往往不是抓包,而是搞清签名。

18.7一致性:字段之间会互相"打脸"

假设你要编一份体检报告。身高写 190,体重写 45,肺活量写 2000,医生一眼就知道有问题。风控包也一样,单看每个字段都正常,放在一起就露馅。

常见的矛盾组合:

  • 分辨率是 1080×2400,机型却是五年前的小屏机;
  • 时区是东八区,IP 归属地却在另一个大洲;
  • 系统语言是简体中文,字体列表却全是英文;
  • 应用版本写 8.2,客户端标识里却是 7.9;
  • 首次安装时间晚于账号注册时间;
  • 机型是真实手机,传感器数量却是 0。

这些矛盾不需要每一条都被检测到。服务端只要发现其中一两条,就足以把这次请求标记为"可疑",然后降级处理:加验证码、限流、要求二次验证,而不是直接拒绝。

注意 一致性问题没法靠"再多加一个字段"解决。每加一个字段,就多一条需要自洽的约束。这就是为什么单点伪装在真实系统里几乎无效。

18.8服务端可能怎么校验

服务端的校验通常不是一道门,而是五层筛子。任何一层不过,结果不一定是拒绝,更常见的是"降级"。

服务端校验的五层筛子 ① 验签:按同样规则重算,比对是否一致 ② 时间窗:ts 是否在允许偏差内 ③ 频率:同设备、同地址的请求速率 ④ 画像:字段组合是否符合历史与机型约束 ⑤ 序列:请求顺序是否符合正常业务 任一层不过 不一定是拒绝 更常见是降级 加验证码 限流排队 降低权限 转人工审核 直接拒绝会误伤真人
图 18-3 五层筛子输出的是分数,不是开关;低分的典型处理是降级而不是封死

把五层拆开看:

  • 验签:重算签名,不一致说明参数被改过或密钥不对;
  • 时间窗:时间戳偏差过大,说明请求是旧的或时钟被改过;
  • 频率:同一设备或同一地址在短时间内请求过多;
  • 画像:字段组合与历史记录、机型约束是否匹配;
  • 序列:请求顺序是否像正常业务,例如没浏览商品就直接下单。

这五层不是每次都全跑。低风险请求可能只过前两层,高风险请求才会走完整流程。所以你在外部看到的现象经常是"有时成功有时失败",而不是稳定的通过或拒绝。

18.9检测与防护视角:为什么单点伪装没用

安全上有个木桶效应:水位由最短的那块板决定。风控包也是,整体可信度由最不自洽的那个字段决定。你把机型改得天衣无缝,只要时区和地址对不上,前面的努力全部作废。

从防守方角度看,一套好的校验体系要满足四点:

  • 多维度:不依赖单一字段,任何单点被攻破都不致命;
  • 可解释:每个低分要能说清是哪条规则触发的,方便运营复核;
  • 可运营:阈值能调、规则能灰度、误伤能回滚;
  • 可对抗:定期看新型伪造手法,更新约束关系。

反过来,从分析者角度看,这也解释了为什么"找一个能过检的字段"是伪命题:系统看的不是某个字段的值,而是整份报告的内部逻辑。这一页只讲原理,不针对任何具体平台提供操作步骤。

18.10合规提醒

风控包里大量字段与个人信息相关:设备标识、位置、网络、行为轨迹。处理这些数据必须守住几条线:

  • 授权范围:只对自有系统或书面授权的目标做分析;
  • 最小必要:只采集完成目标所需的最少字段,不扩大范围;
  • 告知与同意:涉及真实用户数据时,必须有明确的告知与同意基础;
  • 脱敏存储:能哈希就不存明文,日志里的手机号、地址要打码;
  • 可删除:任务结束后按约定清理原始数据。
红线 不针对具体平台编写过检、绕过或批量注册的操作步骤;不采集他人个人信息;不把分析结果用于干扰他人服务。技术原理可以学,落地必须先有授权。
自测:风控包的字段是不是越多越安全?
不是。真正起作用的是字段之间的约束关系,而不是数量。字段太多会增加采集合规风险、传输体积和解析成本。少量但彼此强关联的字段,往往比一大堆互不校验的字段更有效。
自测:哈希和加密,最关键的区别是什么?
哈希是单向的,算完无法还原,用来防篡改;加密是可逆的,有密钥就能还原,用来防偷看。Base64 只是编码,既不是哈希也不是加密,任何人都能还原。
自测:为什么"录制回放"很快就不能用了?
因为请求里通常带时间戳和一次性随机数。时间戳超出允许窗口会被拒,随机数用过一次就进缓存。录制下来的请求在几分钟内就会失效。要持续使用,必须能重新计算签名。
自测:举一个字段互相矛盾的例子,并说明服务端会怎么处理。
例如上报的机型是五年前的小屏机,分辨率却是 1080×2400,传感器数量还是 0。这三项在真实设备里不会同时成立。服务端不需要判断哪项是假的,只要发现组合不存在,就会打低分,然后降级为验证码、限流或人工审核,而不是直接拒绝。
动手练
  • 列一张表,把你能想到的风控字段按六类归档,每类写三个例子。
  • 给自己设计一份"体检报告",故意写两处矛盾,请同伴找出来。
  • 用伪代码写出"排序、拼接、HMAC"三步,再用另一个语言实现一遍,比对结果是否一致。
  • 画一张五层校验图,为每一层写一条可能的降级处理方式。
风控包Risk Data客户端随请求上报的一组设备、环境与行为信息。
设备指纹Device Fingerprint由多个设备特征组合成的稳定标识。
时间戳Timestamp请求生成时刻,用于压缩重放窗口。
随机数Nonce一次性随机值,服务端记住用过的以防重复。
序列号Sequence单调递增的计数,用于防乱序和重放。
消息认证码HMAC带密钥的哈希,既能验完整性也能验来源。
一致性Consistency多个字段之间不互相矛盾,组合在现实中成立。
降级验证Step-up Auth风险升高时追加验证码或二次验证,而非直接拒绝。
你现在应该能回答 风控包分哪六类?哈希、加密、编码各是什么?时间戳、nonce、序列号分别防什么?为什么单点伪装无效?服务端为什么倾向降级而不是直接拒绝?这五个问题讲得清,你就理解了风控体系的设计逻辑。
第 19 章 · 实战链路

自动化工程:从能跑到跑得稳

手动跑通一次,和每天稳定跑几千次,中间隔着一整套工程。这一页讲状态机、重试、限速、断点、日志和密钥管理——它们不产生"新功能",但决定了系统能不能长期活下去。

状态机指数退避令牌桶 断点续跑结构化日志失败分类
先记住这一句 自动化系统的质量不取决于最快的那一次,而取决于最差的那一次能否被识别、被重试、被记录。稳定性是设计出来的,不是调参调出来的。
人话版 会开车不等于能跑长途。跑长途要油表、备胎、休息站、路线图;自动化要重试、限速、断点、日志。

19.1单次成功,和规模化之间隔着什么

第一次跑通脚本的时候,人很容易产生错觉:接口通了、数据拿到了,事情就完成了。但这只是"一次成功",离"稳定运行"还有很远。

算一笔账。假设单次成功率是 95%,看起来很高。连续跑 1000 次,全部成功的概率是 0.95 的 1000 次方,大约是十的负二十三次方——比连续中几次彩票还难。也就是说,在规模化场景里,"不出错"才是小概率事件。

从成功一次到稳定批量之间的四道缺口 中间这四件事,才是工程化的主体 成功一次 手动跑通一条链路 重试退避 限速并发 断点续跑 日志告警 稳定批量 每天数千次不出事 单次成功率 95%,跑 1000 次全部成功的概率接近零:失败必须被当成常态
图 19-1 "能跑"和"跑得稳"之间不是调参,而是四块必须补上的工程能力

19.2状态机:让程序知道自己走到哪一步

最朴素的脚本是一条直线:初始化、登录、取数据、结束。中间任何一步失败,整个进程就退出,下次只能从头再来。规模化之后,这种做法会浪费大量已经完成的步骤。

更稳的做法是把流程写成状态机:程序随时知道自己处在哪个状态,以及什么条件能进入下一个状态。

自动化任务的六个状态与转移条件 初始化 读配置、建连接 登录 拿 Token 业务 执行主任务 刷新 换新 Token 刷新成功后回到业务 失败 分类、计数 退出 保存断点 认证失败 重试有上限 不可恢复 退出前先保存断点;刷新失败也走同一条退出路径
图 19-2 六个状态各管一件事;转移条件写清楚,程序才能在崩溃后接着跑

状态机有三个直接好处:

  • 可恢复:进程重启后读一下任务表,就知道该从"登录"还是"业务"继续;
  • 可测试:每个状态可以单独测,不用每次跑完整条链;
  • 可观测:日志按状态归类,一眼看出卡在哪一步。

可以把它想成电梯:待机、上行、下行、开门、故障,每个状态都有明确的进入条件和退出条件。电梯不会"大概在动",程序也不该"大概在跑"。

19.3重试与退避:只重试值得重试的错误

重试不是"失败就再来一次"。错误的分类比重试次数重要得多。网络抖动值得重试,参数写错重试一万次也没用。

先分清两类:

  • 可重试:连接超时、读超时、服务端 5xx、限流 429;
  • 不可重试:400 参数错误、403 拒绝、业务码明确表示失败、以及 401(应先刷新 Token,而不是原样重试)。

可重试的错误也不能立刻重试,否则会在服务端最脆弱的时候继续加压。标准做法是指数退避:第一次等 1 秒,第二次 2 秒,第三次 4 秒,第四次 8 秒,设一个上限比如 30 秒,最多重试 4 次。

指数退避的等待时间逐次翻倍 上限 30 秒,超过就放弃 1 秒 2 秒 4 秒 8 秒 16 秒 每次等待翻倍,并叠加正负 20% 的随机抖动,避免所有任务同一秒重试
图 19-3 退避让服务端有时间恢复;抖动让重试不要挤在同一秒

为什么要加抖动?假设 1000 个任务在同一秒全部失败,如果都按固定 4 秒重试,第 4 秒会再来一次 1000 个请求的尖峰,服务端可能直接崩掉。加随机抖动后,重试被摊平到一个区间里。

还有一个容易被忽略的前提:幂等。重试之前要问自己,这个操作重复执行会不会产生副作用。查询接口天然幂等,重复调用没问题;下单、支付这类写操作,必须有幂等键,否则重试等于重复提交。

19.4限速与并发:快不等于猛

新手最容易犯的错,是把并发开到最大。并发不是免费的:服务端有连接数和限流,网络有带宽,风控有频率阈值,任何一个先到顶,结果都是更慢、更容易失败。

控制节奏最常用的工具是令牌桶:桶里最多放 20 个令牌,每秒补充 5 个,每个请求执行前先取一个,取不到就等。这样既允许短时突发,又把长期速率压在 5 次每秒。

并发数可以用一个粗略公式估算:

  • 并发数约等于目标每秒请求数乘以单次平均耗时;
  • 例如目标每秒 20 次、单次 200 毫秒,理论并发是 4;
  • 实际取 3,留出余量给网络抖动、重试和服务端抖动。

更进一步的做法是自适应:监控最近一分钟的错误率,超过 5% 就把并发减半;低于 1% 且持续 5 分钟,再缓慢加回来。这样系统能自己找到当前环境下的合理速度,而不是靠人拍脑袋。

19.5断点续跑:进程死了也能接着干

批量任务最怕"跑到一半崩了,全部重来"。解决办法是把进度写进一张任务表,而不是只存在内存里。

任务表至少要有这些字段:

  • task_id:任务唯一编号;
  • dedup_key:业务唯一标识的哈希,用来去重,防止重复入队;
  • status:pending、running、done、failed、skipped;
  • attempts:已尝试次数,配合最大重试次数;
  • next_run_at:下次允许执行的时间,退避就写在这里;
  • last_error:最后一次错误摘要,方便复盘。

启动时先做一次"自愈":把所有状态是 running、但超过 10 分钟没有更新的任务重置为 pending。这对应"进程崩溃时来不及改状态"的情况。

这就像下载器的断点续传:文件下到 60% 断了,下次从 60% 接着下,而不是从 0 开始。

19.6日志与可观测性:出事了能查

"跑得稳"不只是不出错,还包括出错时能快速定位。这依赖结构化日志,而不是满屏的打印。

结构化日志的意思是:一行一条 JSON,字段固定,机器能检索、人能看懂。每条至少包含:

  • 时间、日志级别;
  • request_id:一次任务的所有请求共用同一个编号,方便串起来看;
  • task_id、当前状态或阶段;
  • 耗时(毫秒)、状态码、错误分类。

除了日志,还要看几个指标:成功率、95 分位耗时、重试率、限流命中次数、风控命中次数。这几个数字一旦异常,往往比单条报错更早发现问题。

日志里不能出现明文密码、完整 Token、完整手机号。Token 只留前六位,手机号中间四位打码,这是底线。

19.7配置与密钥管理:别把钥匙贴在门上

把密钥写进代码,是最常见也最危险的省事做法。代码一旦进仓库、被复制、被截图,密钥就等于公开了。

  • 不写死:密钥放环境变量或专门的密钥管理服务,代码里只读不存;
  • 分环境:开发、测试、生产各用各的配置,不要一套密钥走天下;
  • 定期轮换:旧密钥保留一段过渡期,确认无调用后再停用;
  • 最小权限:每个任务只拿它真正需要的权限,不做"万能账号"。

配置文件也要能区分环境,并且把敏感项单独抽出来。提交代码前跑一次检查,确认没有密钥、没有真实用户数据。

19.8失败分类:不同病,不同药

把所有失败都叫"报错",是排查效率低下的根源。下面这张表是工程上最实用的分类方式。

类别典型表现处理策略是否重试
网络连接超时、连接被重置指数退避后重试是
认证401、Token 过期刷新一次,失败重登一次
限流429、请求过于频繁降速、延长等待是
业务参数错误、余额不足记录并跳过该任务否
风控403、要求验证码停止并转人工否
代码缺陷解析异常、空指针修复代码后重跑否

这张表的关键不在"重试几次",而在"哪一类不该重试"。把不可重试的错误反复重试,轻则浪费配额,重则触发更严格的限制。

19.9数据存储:留证据,也留退路

一个成熟的自动化系统,数据通常分三层存:

  • 原始报文:请求和响应的原文,压缩后保留 7 到 30 天,用于复现和举证;
  • 解析结果:清洗后的结构化数据,带 request_id,供后续分析;
  • 失败样本:按错误类型分目录保存,保留更久,用于复盘和回归测试。

三层各有用途:原始报文回答"当时到底发了什么",解析结果回答"业务上得到了什么",失败样本回答"为什么没成"。只存其中一层,出问题时就只能猜。

存储时同样要脱敏。个人信息能哈希就哈希,能删就删,保留期限写进配置,到期自动清理。

19.10两个误区与合规边界

误区一:并发越高越快。实际上服务端限流、带宽争抢、风控频率阈值都会让高并发变得更慢。真正决定吞吐的是"服务端愿意接受的速度",不是"你能发多快"。

误区二:重试能解决所有失败。401、403、400 重试只会浪费配额,甚至让账号被限制。重试只对"临时性故障"有效,对"确定性拒绝"无效。

最后是边界。自动化系统跑起来之后,影响的是真实服务,必须守住四条:

  • 只对自有或书面授权的目标运行;
  • 遵守约定的频率,不做压力测试式的高并发;
  • 不采集超出范围的数据,个人信息最小必要;
  • 随时可停,有总开关,异常时能立刻降速或停止。
红线 自动化能力越强,越要先确认授权与影响面。未经允许对他人系统跑批量任务,不是"技术实验",而是可能影响服务、触犯法律的行为。
自测:单次成功率 95%,为什么跑 1000 次几乎必然失败?
因为总成功率是 0.95 的 1000 次方,约等于十的负二十三次方,几乎为零。规模化系统必须假设失败是常态,把重试、断点、告警设计进流程,而不是指望"不出错"。
自测:哪些错误不应该重试?
400 参数错误、403 拒绝、业务码明确失败,以及 401(应该先刷新 Token 而不是原样重试)。这些是确定性结果,重试不会改变答案,只会浪费配额。只有临时性故障才值得重试。
自测:目标每秒 20 次请求,单次耗时 200 毫秒,并发大概取多少?
理论并发等于 20 乘以 0.2,等于 4。实际建议取 3,留出余量应对网络抖动、重试和服务端波动。并发不是越大越好,够用且留余量才稳。
自测:断点续跑最关键的两个设计是什么?
第一是把状态持久化到任务表,而不是只存在内存里;第二是去重键,防止任务重复入队或重复执行。再加上启动时把超时的 running 任务重置,就能在崩溃后接着跑。
动手练
  • 给自己的脚本画一张状态机图,标出每个状态的进入条件和失败去向。
  • 写一个带指数退避和抖动的重试函数,用本地模拟服务制造 500 错误来验证。
  • 设计一张任务表,写出字段、状态流转和启动时的自愈逻辑。
  • 把现有日志改成一行一条 JSON,至少包含 request_id、阶段、耗时和错误分类。
状态机State Machine把流程拆成有限状态,并规定状态之间的转移条件。
指数退避Exponential Backoff每次失败后等待时间翻倍,给服务端恢复时间。
抖动Jitter在等待时间上加随机偏移,避免大量任务同时重试。
令牌桶Token Bucket按固定速率发令牌,请求取到令牌才能执行,用来限速。
幂等Idempotent同一操作执行多次,结果与执行一次相同。
断点续跑Resume把进度持久化,进程中断后从上次位置继续。
结构化日志Structured Log字段固定的机器可读日志,便于检索和统计。
请求编号Request ID贯穿一次任务所有请求的标识,用于串联排查。
你现在应该能回答 为什么单次成功不等于可批量?状态机解决什么问题?哪些错误不该重试?并发怎么估算?断点续跑靠什么字段?日志里必须有哪些信息?这六个问题答得上,你的脚本才算从"能跑"走到"跑得稳"。
第 20 章 · 实战

排错手册:症状、原因与定位顺序

同一个"登录失败",可能是手机没网,可能是代理证书没装,也可能只是签名里少了一个字段。这一页给你一套固定顺序:先分层定位,再二分缩小,最后最小复现。照着走,不靠猜。

分层定位二分法最小复现 症状对照表证据保存假故障
先记住这一句 排错不是"猜哪里坏了",而是按固定顺序把可疑范围一次次砍半。先确认哪一层不通,再在那一层里二分,最后把问题压成一段能稳定重放的请求。
人话版 别一看到报错就改代码。先问一句"这句话走到哪一步断了",把七层从上往下过一遍,八成的问题在前三步就找到了。

20.1三条铁律:顺序比经验重要

老手排错看起来很快,不是因为他记得更多报错,而是因为他从不跳步。三条铁律,顺序不能换:

铁律一

先分层

先确定"哪一层不通",再谈这一层里具体是什么错。网络层的问题,改业务代码永远改不好。

铁律二

再二分

把可疑范围一刀砍一半:注释一半代码,或换掉一半参数。几刀之后范围就小到能逐个看。

铁律三

后复现

把环境、参数、时间全部固定,做成一条能反复重放的请求。不能复现,就等于没定位。

为什么顺序不能换?因为分层决定"往哪看",二分决定"看多细",复现决定"结论算不算数"。跳过第一步,你会在一堆无关的代码里翻半天;跳过第三步,你改完一次以为好了,明天又坏。

注意 排错时最浪费时间的动作是"同时改三个地方"。改完好了,你也不知道是哪一个改动起的作用;改完还坏,你也不知道是哪个改动起了反作用。

20.2分层定位:从上往下走七级台阶

一次请求从手机到服务器,要经过七道关。任何一道关了,你看到的都是"失败",但失败的原因完全不同。按下面这张图的顺序走,每一层只问一个问题:这一层通了吗?

七层定位阶梯:从物理网络一路查到客户端自身 1 物理与网络:有没有网 飞行模式、Wi-Fi 断没断、能不能打开任意网页 2 DNS:域名能不能变成 IP 解析失败时,浏览器报的是"找不到服务器",不是超时 3 连接:端口通不通 TCP 建不起来就一直转圈;被重置就立刻断 4 TLS 握手:信不信得过对方 证书错误、证书固定拦截、系统时间不对,都卡在这里 5 HTTP:状态码说了什么 401 缺凭证、403 没权限、429 太快、404 路径错 6 业务:参数与签名对不对 200 也可能是假的成功:返回体里 code 不为 0 7 客户端自身:缓存、时间、编码、代码 上面全通还是错,问题往往在自己这台机器上 每下一级之前,先确认上一级是通的,不要跳级
图 20-1 七级台阶:任何一次失败,都能被归到其中某一级

每一层的验证动作都很便宜,一句话就能做完:

层一句话验证失败时的典型表现
物理与网络打开任意一个网页所有 App 都不通
DNS解析域名,看有没有 IP提示"找不到服务器"
连接直接连目标端口一直转圈,或立刻断开
TLS看握手是否完成证书错误、握手失败
HTTP看状态码和响应头401、403、429、404
业务看响应体里的业务码200 但 code 非 0
客户端换一台设备或清缓存重试只有你这台机器错
技巧 先做最便宜的一步:换一个网络、换一台设备、换一个账号。三个"换"做完,往往直接把范围砍掉一半,比你盯着代码看十分钟有用。

20.3症状对照表:把现象翻译成原因

下面这张表是本页最该收藏的部分。左边是你看到的现象,中间是最可能的原因,右边是验证动作和下一步。查到对应行,就按那一行的顺序做,不要跳到别的行。

症状最可能原因怎么验证下一步
完全抓不到包代理没生效,流量走了直连看设备 Wi-Fi 里代理地址填了没先用浏览器测一遍
只有部分请求抓不到部分流量走了 UDP 或独立通道看有没有到 443 端口的 UDP 包关掉 QUIC 再抓
抓到全是乱码TLS 没解开,只看到隧道看是不是只有 CONNECT 记录装并信任代理证书
客户端报证书错误证书没被信任,或有证书固定用官方客户端在同样网络下试处理证书信任或固定
返回 401没带凭证,或凭证已过期看请求里有没有凭证头重新登录换新凭证
返回 403有身份但没这个权限,或被风控换一个账号发同样的请求核对权限与风控规则
返回 412前置条件不满足,常见于风控与签名看响应体里的业务错误码对齐签名与设备信息
返回 429请求太密,被限流看响应头里的重试等待时间降速,加退避重试
签名错误参数顺序、编码、时间戳不一致用同一组参数重算一次签名逐字段对齐后再比
凭证突然失效过期、被顶号、服务端主动吊销解开凭证看有效期字段重新获取并加刷新
响应文本是乱码压缩没解,或字符编码不对看响应头里的压缩与编码声明先解压,再统一成同一种编码
一直转圈然后超时网络慢、服务端慢、连接没建起来分别测网络、端口、接口三步回到分层定位
域名解析失败域名写错、解析被污染、本地被改写换一个公共解析服务对比结果换解析服务再测
连接被重置中间设备拦截,或服务端主动断开看是握手阶段断还是传数据时断换网络对比
返回 200 但数据为空参数不匹配、分页越界、被权限过滤换一组参数看返回是否变化找服务端的筛选条件
时好时坏多台服务器不一致、限流、网络抖动统计成功与失败的比例固定出口再测
App 能通、脚本不通缺请求头、缺设备信息、连接特征不同逐个头补齐后对比补设备与客户端信息
昨天能跑今天不行版本升级改了接口或参数对比版本号与请求字段重新抓一轮包
发出去了但服务端没记录打到了测试环境或错误域名看请求头里的主机名与实际 IP校正目标环境
写入成功但读不到最终一致性、缓存没失效等几秒再读一次加轮询或加等待
中文变成问号编码不是同一种,中间转码丢了字看请求与响应的编码声明全链路统一编码
和时间有关的报错设备时间不同步,时间戳超出窗口对比本机时间与服务器时间开启自动同步
补充 表里同一个现象可能对应多个原因,比如"连接被重置"既可能是拦截,也可能是服务端主动断。这时不要选一个就信,而是用二分法继续缩小,见 20.5。

20.4六类高频症状,逐个拆开

20.4.1 抓不到包、抓到乱码、证书报错

这三件事经常一起出现,根子是同一个:流量没有经过你的代理,或者经过了但你没被信任。

  • 抓不到包:先确认代理地址真的填进了设备,再确认这个 App 没有绕过系统代理。用浏览器访问一个明文站点,能抓到就说明代理生效了。
  • 只抓到隧道:看到的目标端口是 443,但内容不可读,说明 TLS 没解开。此时你看到的是"信封",不是"信"。
  • 证书报错:客户端不认你签发的证书。要么是根证书没装进系统信任区,要么是客户端自己做了证书固定,只认它内置的那一张。

验证顺序固定:先证明代理生效,再证明证书被信任,最后才怀疑证书固定。反过来的话,你会把"证书没装"误判成"有固定",白折腾半天。

20.4.2 401 / 403 / 412 / 429 分别是什么意思

这四个码经常被混着叫"没权限",其实差别很大,处理方式完全不同:

状态码它在说什么你该做什么
401我不知道你是谁补上凭证,或重新登录
403我知道你是谁,但你不能做这件事换账号对比,确认是权限还是风控
412前置条件不满足看响应体,通常是签名、时间、版本不匹配
429你来得太频繁降速,按响应头给的等待时间退避

一个容易踩的坑:401 和 403 的边界,不同公司实现不一样。有的系统凭证过期也返回 403,有的系统越权也返回 401。所以不要背结论,要看响应体里那句话。

20.4.3 签名错误:九成是"不一致"

签名出错几乎不是"算法错了",而是参与签名的原料和你以为的不一样。按这个清单逐条对:

  • 参数顺序:是按字母排、按原始顺序,还是按某种固定字段序;
  • 空值处理:空字符串参不参与签名,null 算不算一个字段;
  • 编码:中文有没有做百分号编码,空格变成加号还是百分号二十;
  • 大小写:十六进制输出是小写还是大写;
  • 时间戳:秒还是毫秒,取的是本机时间还是服务器时间;
  • 拼接符:用和号还是竖线,末尾有没有多一个分隔符。

验证方法只有一个:拿同一组参数,用你的实现和已知正确的实现各算一次,逐字符比。差在哪一位,就说明哪一段原料不同。

20.4.4 凭证失效:先看有效期,再看吊销

凭证突然不能用了,按顺序查三件事:

  • 过期:凭证里通常带签发时间和有效期,解开就能看到;
  • 被顶号:在另一台设备登录,服务端可能把旧的作废;
  • 被吊销:改密码、退出登录、风控判定异常,都会让服务端主动作废。

如果凭证本身还在有效期内却不能用,基本可以判定是服务端状态变了,而不是你算错了。

20.4.5 乱码、超时、连接重置

这三个都属于"看起来像网络问题",但要分开处理:

  • 乱码:先看响应头有没有声明压缩,再看字符编码。压缩没解会得到二进制垃圾,编码错会得到问号或方块。
  • 超时:超时是"没有回音",不是"回了错"。先分清是连接没建起来,还是建起来了但服务端不返回。
  • 连接重置:重置是"有人主动掐断"。掐断可能来自中间设备,也可能来自服务端自己,看断在第几毫秒能大致区分。

20.4.6 返回空数据、时好时坏

"返回 200 但没有数据"最容易被误判成接口挂了。实际上服务端可能只是按你的身份过滤掉了所有结果。

  • 换一个肯定有数据的参数,看是不是还空;
  • 换一个高权限账号,看是不是就有了;
  • 看分页参数是不是超出了总页数。

"时好时坏"则说明你面对的不是一台服务器。多台机器版本不一致、缓存不一致、限流按机器算,都会造成这种抖动。先固定出口,再统计比例,比反复重试有用。

20.5二分法:一刀砍掉一半

范围缩小到某一层之后,下一步是二分。核心思想很朴素:一次只砍一半,然后判断问题在剩下的一半里还是在被砍掉的一半里。

二分法:每一刀砍掉一半可疑范围 可疑范围:12 个变量、300 行代码 第一刀 换掉这一半 保留这一半 第二刀 换掉 保留 第三刀 问题就在这 每次只砍一半,三刀之后范围从 300 行缩到 37 行
图 20-2 二分的价值不在于聪明,而在于每次都只改一个东西

二分有四把刀,按代价从低到高用:

刀法怎么切判断依据
换环境换网络、换设备、换账号换了就好,问题在环境
换参数只保留一半参数发出去少了就好,问题在被删的那半
换头逐组删减请求头删了就好,问题在被删的头
注释代码注释一半逻辑再跑注释了就好,问题在被注释的代码

四把刀里,换环境最便宜,因为它不需要改任何东西。很多人一上来就注释代码,其实换个网络就已经能定位了。

注意 二分的前提是"另一半保持不变"。如果你一边删参数一边改了请求头,那么结果无论好坏都无法解释。一刀只切一个维度。

20.6最小复现:把变量钉死

定位到最后,你需要一个最小复现:一段去掉所有无关内容、能稳定重现问题的请求。它的价值在于,别人拿着它也能看到同样的错误。

最小复现:固定其他变量,只留一个在变 设备 固定同一台 网络 固定同一出口 账号 固定同一个 时间 固定同一分钟 参数 只改这一个 一次只让一个变量变化 稳定复现之后,才谈定位 能复现 = 能定位 = 能验证修复是否有效
图 20-3 最小复现不是"写得少",而是"除了那一个变量,其他全都钉死"

做一个最小复现,只做四件事:

  1. 去掉无关字段:一个个删,删到不能再删还错,剩下的就是必需的;
  2. 固定环境:同一台设备、同一个网络、同一个账号、同一分钟;
  3. 记录环境:把设备型号、系统版本、App 版本、网络类型写下来;
  4. 写成一条命令:能一行执行、能重复执行、结果稳定。

为什么要写下来?因为不记录环境的复现,等于没复现。三天后你换了个网络,同样的请求通了,你会以为问题自己好了,其实只是变量变了。

20.7证据保存:五件套,缺一件都说不清

排错过程中会产生大量临时判断,但只有证据能留下来。五件套,每次都要存全:

证据为什么必须存怎么存
原始报文事后任何复述都会失真导出请求与响应的完整原文
时间对齐服务端日志的唯一线索精确到秒,注明时区
设备与版本很多差异只在这一层型号、系统、App 版本号
日志客户端内部到底走到哪一步保留完整时间顺序,不要只截一行
网络环境区分"环境问题"和"代码问题"运营商、Wi-Fi 名称、是否走代理
红线 原始报文里往往带着凭证和个人信息。保存可以,但不要随手发到公开群组或截图里带出完整凭证。分享前把凭证、手机号、身份证类字段打码,这是最基本的职业习惯。

20.8四类"假故障":不是代码的问题

有一半的"故障"其实根本没坏,只是某个中间环节没生效。这四类最常见:

假故障一

代理没生效

地址填了但没保存,或者只填了 HTTP 没填 HTTPS,或者客户端压根不走系统代理。表现是"抓不到包",实际是"根本没经过你"。

假故障二

时间不同步

手机时间比服务器慢了几分钟,签名里的时间戳就落在窗口外,服务端直接判为过期。表现是"签名错误",实际是时钟问题。

假故障三

缓存

你改了参数,客户端还在用上一次的响应。表现是"改了没用",实际是请求根本没发出去。

假故障四

编码

中文在某一跳被转成了别的编码,签名算的是转码前的内容,服务端算的是转码后的。表现是"偶尔签名错",实际是编码不一致。

这四类的共同点是:它们都发生在你的代码之外。所以排错时先排除它们,能省掉大量无用功。

补充 判断"是不是假故障"有个通用办法:换一个完全无关的简单请求,看它是否也异常。如果连打开网页都不正常,那就不是你的业务代码问题。

20.9两个最贵的误区

误区一:看到报错就改代码

报错是结果,不是原因。一个 403 可能是权限不足、可能是凭证过期、可能是设备指纹不匹配、也可能是触发了风控。四种原因,对应四种完全不同的改法。

正确做法是:先把这个报错归到七层里的某一层,再在那一层里二分。改代码是最后一步,不是第一步。

很多人以为"改到不报错"就是修好了,其实可能只是把错误吞掉了。真正的修好,是能解释为什么之前会错。

误区二:把风控拦截当成 bug

风控系统返回的错误,长得和普通错误一模一样:可能是 403,可能是 412,可能是 200 但业务码不为 0,也可能干脆返回假数据。

它和 bug 的区别在于:bug 是稳定复现的,风控是按条件触发的。同一个请求,换个账号就通了,换个设备就通了,隔十分钟就通了,那基本不是 bug。

现象更像 bug更像风控
复现率每次必错时好时坏
换账号仍然错可能就通了
换设备仍然错可能就通了
错误信息具体到字段笼统、含糊、带业务码
注意 本站只讨论原理与防护思路。真实的系统里,风控是对方的正当防御手段,不要在未授权目标上做绕过尝试。这里讲它,是为了让你在自有系统里把检测做得更准。
自测:抓包只看到 CONNECT 记录、内容是乱码,应该先查哪一层?
先查第 4 层 TLS。CONNECT 说明代理收到了隧道请求但没能解密,通常是根证书没被信任,或者客户端做了证书固定。不是"抓包工具坏了"。
自测:401 和 403 的区别是什么?
401 是"我不知道你是谁",缺凭证或凭证无效;403 是"我知道你是谁,但你不能做这件事",是权限或策略问题。但不同系统实现有差异,最终要看响应体里的说明。
自测:为什么二分法要求"一次只改一个变量"?
因为结果必须能归因。同时改两处,好了不知道是哪处起作用,坏了也不知道是哪处起反作用。一次一个变量,才能把结论钉死。
自测:怎么快速判断一个错误是 bug 还是风控?
看稳定性。每次必错、换账号换设备都错、错误信息具体到字段,更像 bug;时好时坏、换账号就通、提示笼统带业务码,更像风控。先分类,再决定改什么。
动手练
  • 挑一个你最近遇到的报错,把它归到七层里的某一层,写下判断依据。
  • 用二分法砍三刀:先换网络,再删一半参数,再删一半请求头,记录每一步结果。
  • 把一次失败请求写成最小复现,只保留必需字段,并记录设备、时间、网络三项环境。
  • 给自己的排错过程做一张证据清单,下次排错时逐项打勾。
分层定位Layered Isolation按网络层次从上往下逐层确认是否通畅。
二分法Bisection每次砍掉一半可疑范围,快速收敛到最小集合。
最小复现Minimal Repro去掉无关内容、能稳定重现问题的最小请求。
证书固定Certificate Pinning客户端只信任内置的那一张证书,拒绝其他。
限流Rate Limiting服务端按频率拒绝请求,常返回 429。
退避Backoff失败后按递增间隔重试,避免加剧拥塞。
最终一致性Eventual Consistency写入后要过一小段时间才能读到最新值。
假故障False Fault代码没坏,是代理、时间、缓存、编码等外部因素造成的。
你现在应该能回答 一次失败该按什么顺序查?401 和 403 差在哪?二分法为什么一次只改一个变量?最小复现要固定哪些东西?哪四类属于假故障?这五个问题能答上来,你排错就不会再靠猜。
第 21 章 · 实战

实战案例集:三个真实项目,三种分析方法

前面二十章讲的是零件,这一章讲零件怎么装成一台机器。三个案例都来自真实项目,全部做了脱敏:不写具体平台字段、不给可执行代码、不提供绕过步骤。你看到的是判断顺序、失败记录和验证方法——这三样才是能迁移到下一个项目的东西。

抓包点定位风控与限流图关系分析 对拍验证失败复盘
先记住这一句 真实项目里,80% 的时间花在"排除错误假设"上,而不是写代码。三个案例最有价值的部分都不是成功那一步,而是"我们一开始以为……后来发现不是"。
人话版 这一页不教你"怎么破",只教你"怎么想"。每个案例都按同一套顺序讲:目标是什么、先试了什么、为什么失败、怎么找到真正的位置、最后怎么证明自己没看错。

21.1案例一:电商 App 的抓包点为什么一直抓不到

背景

目标是一个电商类 App(脱敏后称为 A 应用)。需求很简单:看清楚它的商品搜索请求带了哪些参数。团队按标准流程做了三件事,结果一个请求都没抓到。

尝试做法结果
① 系统代理手机 Wi-Fi 设代理,装 CA 证书其他 App 能抓,A 应用 0 命中
② Hook QUIC拦截 HTTP/3 的发送函数库加载了,但业务流量 0 触发
③ Hook 系统 TLS拦截标准 SSL_write / SSL_read0 触发

三次失败指向同一个结论:A 应用没有走"大家以为它该走"的那条路。

定位过程

从假设到定位抓包点的排查路径 假设:走系统代理 装证书就能抓 失败:0 命中 网络库绕过系统代理 下一步:先确认它用什么网络栈 而不是继续换 hook 点 静态看依赖:自带网络库 + 自带 TLS 实现 系统代理、系统 libssl 都不在这条链路上 把 hook 点上移到业务层 在"请求提交入口"拿加密前明文 结果:拿到完整 URL、请求头、请求体;加密发生在更下层,业务层是明文 教训:抓不到包时,先画清楚"数据实际经过哪些库",再决定 hook 哪里 同一套方法可以迁移到任何"自带网络栈"的 App,不需要记住具体库名
图 21-1 抓不到包的正确反应:从"换工具"改为"画链路"

关键判断

  1. 先确认网络栈,再选抓包位置。看安装包里的原生库、看依赖、看它是否自带 TLS 实现。
  2. 代理失效不等于加密。有些网络库直接读网卡、不走系统代理,代理根本不在链路上。
  3. 越靠上越省事。在业务层拿到的已经是解密后的明文,不用关心下层用的是 TLS 1.2 还是 1.3。
  4. spawn 比 attach 稳。进程启动即注入,避免 PID 漂移导致挂不上。
失败记录比成功记录值钱 这个项目最有价值的产出不是"最终抓到了",而是一张三行表:试了什么、为什么失败、下一步排除了什么。没有这张表,团队会在同一个假设上反复换工具,浪费几天。

21.2案例二:企业信息平台的"挑战—响应"与限流

背景

某企业信息查询平台(脱敏后称为 B 平台)在访问详情页时会先弹一个验证环节。团队最初把它当成"一个图片识别问题",做了两周发现:识别率上去了,通过率没上去。真正的问题不在识别,而在整条会话的状态管理。

挑战响应与限流的完整状态 ① 请求详情 带会话 Cookie ② 返回挑战 不是数据,是一段挑战 ③ 完成挑战,拿回凭证 凭证有有效期,绑定会话 ④ 带凭证重试原请求 成功才拿到数据 ⑤ 继续翻页 / 看下一家 此时限流开始生效 ⑥ 触发限流:429 / 冷却 / 熔断 继续硬打只会延长冷却 ⑦ 退避 + 换窗口 + 降并发 把速率压到预算以内 识别率只是入场券;真正的通过率由"会话 + 预算 + 退避"共同决定 任何一环没做对,识别率再高也会被限流吃掉
图 21-2 挑战不是孤立的一步,它嵌在"请求—挑战—凭证—重试—限流"的循环里

三个被忽略的变量

变量一开始的做法后来的做法
会话每次请求新建会话复用同一会话,凭证绑定会话
预算不限速,跑到报错为止按分钟设配额,留 30% 余量
退避失败立即重试指数退避 + 冷却,连续失败熔断

可迁移的经验

  • 先测"不改任何东西"的基线。同一会话连续请求 10 次,看第几次开始被限流,这就是预算的起点。
  • 把"识别"和"提交"分开统计。识别对了但提交失败,问题在会话;识别就错,问题在模型。
  • 记录每一次失败的完整上下文。只记"失败"没用,要记响应码、响应体、当时的时间、当时的会话状态。
  • 队列要清洗。坏数据会让每次请求都失败,看起来像"风控变严",其实是自己的队列脏了。
一个反直觉的结论 这个项目里提升最大的一步不是算法优化,而是把队列里名称不匹配的脏数据提前过滤掉。失败率下降后,限流压力跟着下降,整体速率反而上去了。

21.3案例三:用图关系识别"批量执照"

背景

第三个案例不是攻击,而是风险识别:从公开的企业登记信息里,找出"同一个人名下挂着大量同模板公司"的现象。这类结构常被用来批量注册、拆分业务或规避监管。目标是给合规团队一份可复核的名单,而不是自动化处置。

人公司二部图与阈值筛选 法人 A 某某百货店 某某商贸行 某某工作室 ……共 14 家 第一层:度数筛选 名下活跃公司数 ≥ 3 才保留 ≥ 10 才继续展开 第二层:模板相似度 同名不同字、同县、同日注册 同模板后缀 ≥ 5 家加分 第三层:硬性排除 国资、金融、大型连锁、知名品牌 排除误报,避免冤枉真企业 人—公司 二部图 一人多照即高连接
图 21-3 先把关系画成图,再用阈值和排除规则把"可疑"缩到可人工复核的范围

为什么用图,而不是用关键词

最初的做法是搜"商贸、百货、工作室"这类关键词,结果噪声极大:真正的连锁企业、正常的多元化经营都会被命中。改成图分析后,判断依据从"名字像不像"变成了结构长什么样:

  • 一个法人节点连接大量公司节点;
  • 这些公司高度同质(同后缀、同区县、同注册时间);
  • 没有连锁企业常见的品牌复用、统一社会信用代码规律、分支机构结构。

误报控制

误报来源表现处理
真连锁同一品牌大量分公司品牌复用检测,命中即排除
集团架构母公司 + 子公司看股权层级,不看数量
代办注册同一地址大量公司地址聚类,单独成组
数据陈旧已注销仍被统计只统计"开业"状态
边界 这个案例的产出是给合规人员复核的线索,不是自动判定。任何名单都必须经过人工核实,不能直接用于处置。把"结构可疑"当成"违法事实",是这类分析最常见的错误。

21.4三个案例的共同点

共同点一

先画链路,再动手

案例一画网络栈,案例二画会话状态机,案例三画关系图。图画清楚了,问题往往自己浮出来。

共同点二

记录失败假设

三个项目都有一张"试过什么、为什么不行"的表。它比成功路径更能节省后来人的时间。

共同点三

用数据验收

不靠"我感觉对了",靠通过率、失败分类、人工复核一致率。数字对不上,就是没懂。

你现在应该能回答 抓不到包时第一步该做什么?挑战—响应为什么不能只看识别率?为什么风险识别要用图而不是关键词?如果这三个问题你能讲清楚,说明你已经具备把方法迁移到新项目的能力。
自测:系统代理装好证书,为什么还有 App 抓不到包?
因为它可能根本不走系统代理,也不使用系统 TLS 库。常见原因是 App 自带网络库、自带 TLS 实现,或者在应用层就完成了加密。正确做法是先确认网络栈,再选 hook 点。
自测:验证码识别率 95%,为什么整体通过率只有 60%?
因为通过率不只由识别决定,还受会话是否复用、凭证是否过期、限流预算、退避策略影响。要把"识别成功率"和"端到端成功率"分开统计,才能定位瓶颈。
自测:为什么"一人名下公司多"不能直接判定为异常?
正常的企业家、连锁品牌、集团架构都可能一人挂多家公司。必须结合同质化程度、注册时间、地址、股权结构、是否真连锁等信号,并且最终由人工复核。
自测:三个案例里,最值得复用到下一个项目的是什么?
不是某个工具或脚本,而是排查顺序和验证习惯:先画链路、一次只改一个变量、用真实数据对拍、把失败假设记下来。
动手练
  • 选一个你常用的 App,画出它可能的网络栈:系统代理、自带库、TLS 实现分别在哪一层。
  • 给自己正在做的项目建一张"失败假设表",三列:试了什么、结果、排除了什么。
  • 找一个你熟悉的领域,试着把"关键词筛选"改写成"结构筛选"。
网络栈Network Stack一个 App 从应用层到网卡所经过的全部网络库与协议实现。
挑战—响应Challenge-Response服务端先给一道题,客户端答对后才放行。
熔断Circuit Breaker连续失败后暂停请求一段时间,避免持续撞墙。
二部图Bipartite Graph节点分成两类、边只跨类的图,如"人—公司"。
度数Degree一个节点连了多少条边,这里指一个人名下有多少家公司。
误报False Positive把正常对象判成异常,代价是冤枉好人。
端到端End-to-End从输入到最终业务结果的全链路,而不是中间某一步。
脱敏De-identification去掉可识别个人或企业的具体信息,只保留方法。
第 22 章 · Web / API

Web 安全基础:OWASP Top 10 逐条

你可能听过"SQL 注入""越权""XSS",但这些词到底指什么、发生在哪一步、坏了会怎样,很少有人一次讲清。这一页用一张攻击面图加十条清单,把 Web 安全最常见的十类问题从头到尾过一遍。

攻击面OWASP Top 10注入 访问控制SSRF防护思路
先记住这一句 Web 安全的问题几乎都出在同一个地方:程序相信了不该相信的输入。要么相信了用户的输入,要么相信了用户的身份,要么相信了外部的组件。
人话版 这一页就讲一件事:别人能从哪里把手伸进你的系统。十个条目,就是十个最常见的伸手位置。

22.1先看一张图:一次请求的三个落点

一个网页或 App 的请求,从你的手机出发,经过三站:浏览器、服务器、数据库。每一站都有可以被攻击的位置,而且三站的防护方式完全不同。

Web 请求的三个落点:浏览器、服务器、数据库 前端校验可被绕过 请求可被伪造越权 查询可被注入拖库 浏览器 / App 用户输入、页面渲染 一切都在用户手里 请求 服务器 业务逻辑、鉴权 真正做决定的地方 查询 数据库 存数据、认权限 最后一道门 三个落点的共同点:每一站都要重新判断"这个人说的话能不能信" 只在前端做校验,等于只在门口贴了张"请勿入内"的纸条
图 22-1 浏览器、服务器、数据库三站,每一站都有独立的攻击面

把这三站想成一道快递流程:

  • 浏览器是寄件人。寄件人自己填单子,所以单子上的内容他随时能改;
  • 服务器是收件站。它要核对你是谁、你有没有权限,这里是真正把关的地方;
  • 数据库是仓库。如果收件站把"帮我拿所有包裹"当成正常指令转过去,仓库就全给了。

所以有一条铁律:前端校验只是体验,服务端校验才是安全。前端能做的,只是让正常用户少犯错;它拦不住任何一个有心人。

22.2OWASP Top 10 到底是什么

OWASP(Open Worldwide Application Security Project,开放 Web 应用安全项目)是一个非营利组织。它每隔几年发布一份清单,列出当前最普遍、最危险的十类 Web 安全问题。

它不是标准,也不是法规,而是一份行业共识。你可以把它理解成"体检报告的十大高发项":不是每个人都会得,但这十项覆盖了绝大多数真实案例。

编号名称一句话
A01失效的访问控制该拦的没拦,能看到别人的数据
A02加密机制失效该加密的没加密,或加密方式太弱
A03注入用户输入被当成了代码执行
A04不安全设计设计阶段就没考虑滥用场景
A05安全配置错误默认口令、调试开关、目录暴露
A06易受攻击和过时的组件用的库本身有已知漏洞
A07身份认证和授权失效登录、会话、凭证管得不好
A08软件和数据完整性失效更新和依赖没验签,被替换了也不知道
A09安全日志和监控失效被打了却没有记录,事后查不出来
A10服务端请求伪造让服务器替你去访问内网
补充 这份清单每隔几年会调整顺序和条目,所以看资料时要注意年份。本页讲的是 2021 版,也是目前引用最多的一版。顺序代表普遍程度,不代表危害大小,任何一条做不好都可能是致命的。

22.3十条逐条过:原理、类比、例子、防护

下面每一条都按同一个格式写:先用一句话说原理,再给一个生活类比,再给一个具体例子,最后给一条最有效的防护。不要跳读,十条之间有大量交叉。

22.3.1 A01 失效的访问控制

原理:系统知道你是谁,但没有认真检查"你能不能看这一条数据"。用户只要改一个编号,就能看到别人的东西。

类比:酒店前台认得你是住客,于是把整层楼所有房间的钥匙都给了你,因为"你确实是客人"。

例子:订单详情页的地址是 /api/order?id=1001,你把 1001 改成 1002,返回了别人的订单。这类问题有个专门名字,叫 IDOR(Insecure Direct Object Reference,不安全的直接对象引用)。

防护:每次查询都带上"当前登录用户"作为条件,而不是只按对象编号查。权限判断写在服务端,且写在每一个取数据的地方。

22.3.2 A02 加密机制失效

原理:敏感数据在传输或存储时没有加密,或者用了早已被攻破的算法。

类比:把存折密码写在存折封面上,然后把存折锁进抽屉,锁得再好也没用。

例子:密码用不加盐的单次哈希存储;或者内部系统还在用明文 HTTP 传登录请求。

防护:传输一律走加密通道;密码用专门的口令哈希算法(见第 23 章);密钥不要写进代码仓库。

22.3.3 A03 注入

原理:程序把"用户输入的数据"和"要执行的指令"拼在了一起,导致数据被当成了指令。

类比:你让助理"把这段话抄到表格里",结果这段话里写着"顺便把保险柜打开",助理照做了。

例子:查询语句被拼成 WHERE name = 'alice' OR '1'='1',条件永远成立,返回全部用户。除了 SQL,还有命令注入、模板注入、日志注入,本质完全一样。

防护:用参数化查询(把数据和指令分开传),不要用字符串拼接。这一条几乎是所有注入的通用解法。

22.3.4 A04 不安全设计

原理:代码写得没问题,但设计本身允许被滥用。比如找回密码只问"生日是多少"。

类比:门锁是好的,但你把钥匙放在门口脚垫下面,锁再结实也没意义。

例子:优惠券可以无限次叠加;下单和支付之间没有状态校验;短信验证码可以无限次尝试。

防护:在设计阶段就写"滥用场景清单":这个功能如果被恶意使用,最坏会怎样。这一条没法靠打补丁解决,只能靠改设计。

22.3.5 A05 安全配置错误

原理:系统本身没漏洞,但配置留了后门:默认口令没改、调试页面没关、备份文件放在公开目录。

类比:新房子装修得很好,但装修队留的临时钥匙一直没收回。

例子:管理后台用默认账号密码;错误页面把完整报错和文件路径返回给用户;对象存储权限设成了"所有人可读"。

防护:把配置当成代码管理,上线前跑一遍基线检查,默认值一律改掉,错误信息对外只给笼统提示。

22.3.6 A06 易受攻击和过时的组件

原理:你自己写的代码很干净,但你引用的第三方库有已知漏洞,而你没有升级。

类比:自家门窗都换了防盗的,但楼下的公共大门还是坏的。

例子:某个日志库、某个解析库爆出远程执行漏洞,你的项目正好用了旧版本。

防护:记录每个依赖的版本,定期比对公开漏洞库,能升级就升级;不能升级的,评估影响面并加临时缓解。

22.3.7 A07 身份认证和授权失效

原理:登录、会话、凭证的管理有缺陷。比如允许弱口令、会话永不过期、退出登录后凭证仍然有效。

类比:门禁卡丢了不注销,捡到的人可以一直刷进来。

例子:登录接口没有失败次数限制,可以无限次试密码;凭证签发后无法吊销;改密码后旧凭证还能用。

防护:限制尝试次数、支持吊销、短有效期加刷新机制、敏感操作二次验证。细节见第 23 章。

22.3.8 A08 软件和数据完整性失效

原理:代码、更新包、依赖、序列化数据没有校验来源和完整性,被替换了也发现不了。

类比:快递员说"这是你买的东西",你连箱子都没看就签收了。

例子:自动更新从不受信任的地址下载;反序列化用户可控的数据,直接构造出对象。

防护:更新包验签、依赖锁定版本并校验哈希、不要反序列化不可信来源的数据。

22.3.9 A09 安全日志和监控失效

原理:被攻击了却没有记录,或者记录了但没人看,导致无法发现、无法追溯、无法证明。

类比:装了摄像头,但硬盘早就坏了,出事才发现什么都没录上。

例子:登录失败没有任何日志;关键操作不记录操作人和时间;日志里反而打印了明文密码。

防护:记录"谁、什么时候、从哪里、做了什么、结果如何",设置异常告警,日志本身要防篡改且不含敏感明文。

22.3.10 A10 服务端请求伪造(SSRF)

原理:你的系统有一个"帮你抓取远程内容"的功能,攻击者让它去抓内网地址,服务器就成了跳板。

类比:你让前台帮忙去楼下取个快递,结果对方填的地址是"财务室保险柜"。

例子:图片转存功能接收一个网址,传入内网地址后,服务器把内网页面的内容返回给了攻击者。

防护:对目标地址做白名单,禁止访问内网网段和本机地址,禁止跟随跳转,出站请求单独走受限通道。

22.4重点一:注入为什么这么常见

注入能排到前三,是因为它写起来太自然了。想把用户输入放进查询里,最直觉的写法就是拼字符串。而拼字符串,正好就是注入的成因。

正常输入与注入输入拼出的两条查询对比 正常输入:数据就只是数据 alice SELECT * FROM users WHERE name='alice' 只返回 alice 这一行 恶意输入:数据变成了指令 alice' OR '1'='1 ... WHERE name='alice' OR '1'='1' 条件永远成立,返回全部用户
图 22-2 同样是拼字符串,多了一小段引号,语义就从"等于"变成了"永远成立"

为什么多一小段引号就能改变语义?因为数据库收到的不是"一个名字",而是一整条指令文本。它没法分辨哪一段是用户写的、哪一段是程序员写的。

防护的办法只有一个方向:让数据和指令分两条路走。

# 错误写法:把用户输入拼进语句
sql = "SELECT * FROM users WHERE name='" + name + "'"

# 正确写法:语句先定好,数据后填进去
sql = "SELECT * FROM users WHERE name = ?"
cursor.execute(sql, [name])

第二种写法里,数据库先解析好语句结构,再把 name 当成一个值填进去。哪怕这个值是 alice' OR '1'='1,它也只会被当成一个奇怪的名字去匹配,不会变成条件。

技巧 判断一段代码有没有注入风险,只问一句:这段字符串里有没有把外部输入拼进去。有拼接就有风险,不管是 SQL、命令行、模板还是日志。

22.5重点二:访问控制与 IDOR

访问控制失效能排到第一,是因为它不需要任何技术手段。不用构造特殊字符,不用懂数据库,只要把地址里的数字改一下。

IDOR:只改一个编号,就拿到了别人的数据 GET /api/order?id=1001 服务器 只按 id 查 你的订单 服务器只检查了"有没有登录",没检查"这个订单是不是你的" GET /api/order?id=1002 服务器 照样按 id 查 别人的订单 正确做法:查询时同时带上"当前登录用户"作为条件,而不是只按对象编号
图 22-3 IDOR 的本质不是"猜到了编号",而是"服务端没有校验归属"

为什么这种问题这么普遍?因为开发时最常见的思路是"先把数据取出来,再判断要不要给"。而正确的思路应该是"只取这个人有权看的数据"。

两者的差别在于:

  • 先取后判:数据已经离开数据库了,只要判断那一步漏了、写错了、被异常跳过了,就泄露了;
  • 只取该取的:条件直接写进查询里,取不到就是取不到,没有"漏判"的机会。

还有一类更隐蔽的越权:水平越权是同级用户之间互相看,垂直越权是普通用户做了管理员才能做的事。前者靠改编号,后者靠改角色字段或直接调用管理接口。

注意 很多人以为"编号是随机的、猜不到"就安全了。这叫"靠隐蔽求安全",不叫访问控制。编号只要泄露一次(分享链接、日志、截图),防线就没了。必须校验归属,不能只靠猜不到。

22.6一张总表:入口、后果、防护

把十条压缩成一张表,方便你快速对照。注意"入口"这一列:几乎所有问题都从用户可控的输入进来,只是进去之后走的路不同。

攻击类型入口后果防护
访问控制失效对象编号、接口路径看到或改到别人的数据每次校验归属
加密失效传输链路、存储字段数据被窃听或拖走全程加密、专用口令哈希
注入查询、命令、模板参数数据泄露、甚至执行代码参数化,不拼字符串
不安全设计业务流程本身被批量滥用、薅走资源设计阶段写滥用场景
配置错误后台、调试页、存储桶直接拿到管理权限配置基线检查
过时组件第三方库版本已知漏洞被直接利用跟踪版本、及时升级
认证失效登录、会话、凭证账号被撞开或长期冒用限次、可吊销、短有效期
完整性失效更新包、依赖、序列化被植入代码而不自知验签、锁版本
日志监控失效日志与告警体系被打了查不出来记录关键操作并告警
SSRF抓取、转存、回调地址服务器替攻击者访问内网地址白名单、禁内网

22.7两个最危险的误区

误区一:有 WAF 就安全了

WAF(Web Application Firewall,Web 应用防火墙)是一层过滤器,按规则拦掉看起来像攻击的请求。它有用,但它是补丁,不是地基。

  • 它按特征匹配,特征之外的东西它不认识;
  • 它可能误伤正常请求,于是有人会把规则放宽;
  • 它挡不住逻辑漏洞,比如"改一个编号就能看别人订单",因为这个请求长得完全正常。

所以正确的顺序是:先把代码写对,再加 WAF 兜底。反过来做,你会以为自己在防守,其实只是把门虚掩着。

误区二:前端校验够了

前端校验的作用是"让正常用户少犯错",比如邮箱格式不对时立刻提示。它的价值是体验,不是安全。

原因很简单:前端代码跑在用户的设备上,用户对自己的设备有完全控制权。他可以改代码、可以拦截请求、可以不用你的页面直接发请求。

很多人以为"按钮置灰了就点不了",其实请求照样能发出去。所有校验必须在服务端再做一遍,而且服务端那一遍才是有效的那一遍。

补充 判断一个校验有没有用,只看一件事:它是不是在服务端执行的。在前端执行的校验,只能算输入提示。
自测:为什么参数化查询能防住注入?
因为它把"语句结构"和"数据值"分成两步处理。数据库先解析好语句,再把用户输入当成纯值填进去。哪怕值里带引号和逻辑符号,也只会被当成字符串内容。
自测:把订单编号改成别人的,属于哪一类问题?
失效的访问控制,具体叫 IDOR(不安全的直接对象引用)。服务器认出了你是谁,但没校验这条数据是不是你的。防护是查询时带上当前用户作为条件。
自测:有 WAF 是不是就不用改代码了?
不是。WAF 按特征匹配,挡不住逻辑漏洞,也可能被绕过或误伤。正确顺序是先修代码,再用 WAF 兜底。WAF 是补丁,不是地基。
自测:为什么前端校验不能算安全措施?
前端代码运行在用户自己的设备上,用户可以改代码、拦请求、绕开页面直接发请求。前端校验只改善体验。所有校验必须在服务端重做一遍。
动手练
  • 在公开教学靶场(如 OWASP 提供的练习环境)里,亲手试一次 SQL 注入和一次 IDOR,观察返回差异。
  • 把你手上项目的一个查询接口找出来,检查它是拼接字符串还是参数化传参。
  • 列出你项目里所有"按编号取数据"的接口,逐个确认有没有校验归属。
  • 用一句话向非技术的同事解释"为什么前端校验不算安全"。
OWASPOpen Worldwide Application Security Project发布 Web 安全共识清单的非营利组织。
攻击面Attack Surface所有能被外部输入触达的位置总和。
注入Injection用户输入被当成指令执行,如 SQL 注入、命令注入。
参数化查询Parameterized Query语句与数据分开传递,防注入的根本办法。
IDORInsecure Direct Object Reference改对象编号就能访问别人的数据。
水平越权Horizontal Privilege Escalation同级用户之间互相访问对方数据。
垂直越权Vertical Privilege Escalation低权限用户执行高权限操作。
SSRFServer-Side Request Forgery让服务器代替攻击者访问内网地址。
你现在应该能回答 一次请求在哪三站可能出问题?注入的本质是什么?参数化查询为什么有效?IDOR 为什么不需要技术手段?WAF 和前端校验各能做什么、不能做什么?这五个问题答得出来,你就已经能看懂大部分 Web 安全报告了。
第 23 章 · Web / API

认证与授权:Session、JWT、OAuth

"登录"这两个字背后其实有两件事:先证明你是谁,再决定你能干什么。很多人把这两件事混成一件,于是写出了"改个字段就变管理员"的系统。这一页把 Session、JWT、OAuth 三条主线一次讲清。

认证 / 授权Session / CookieJWT OAuth2 / PKCEOIDC密码存储
先记住这一句 认证回答"你是谁",授权回答"你能干什么"。先把身份确认清楚,再逐条检查权限,两件事分开做,才不会出现"登录了就等于什么都能做"。
人话版 认证是进大楼时查工牌,授权是进楼之后哪几扇门能刷开。查了工牌不等于每扇门都开,这两步必须分开。

23.1认证与授权:两道门,不能合并

这两个词经常被混着用,但它们的失败后果完全不同:

  • 认证(Authentication):证明"你是你说的那个人"。密码、指纹、短信码,都在做这件事。
  • 授权(Authorization):决定"这个身份能做什么"。查订单、改资料、删数据,各自需要不同权限。
认证与授权:先确认身份,再检查权限 第一道门 第二道门 用户 带着凭证 认证 你是谁 核对密码或生物特征 授权 你能干什么 查权限表,逐条判断 数据 放行 认证不过:401,我不知道你是谁 授权不过:403,我知道你是谁,但你不能做这件事
图 23-1 认证是"验身份",授权是"查权限",顺序不能颠倒,也不能合并

用一个生活场景说明:你进公司大楼,保安看工牌,这是认证;你走到财务室门口,门禁判断你有没有这个权限,这是授权。有工牌不等于能进财务室。

系统里最常见的错误,就是把这两步合成一步:"只要登录了,就什么都能调"。这种设计一旦被利用,普通用户改一个角色字段就能变成管理员。

23.2Session 与 Cookie:把状态放在服务器

HTTP 本身是无状态的:每个请求都是独立的,服务器不会自动记得"你刚才来过"。但登录之后它必须记得你,于是有了 Session。

Session(会话)的做法很朴素:服务器自己记一本账,账上写"这个随机编号对应哪个用户"。手机只拿一个编号。

Session 与 Cookie:服务器记账,浏览器只拿编号 浏览器 只存一个编号 服务器 存整本账 ① 提交账号密码,只发这一次 ② 核对通过,在服务端创建 Session ③ 返回 Cookie:session_id=abc123 ④ 之后每个请求都自动带上这个 Cookie ⑤ 服务端查账,认出你是 uid 10086
图 23-2 Session 的核心是"账在服务器",浏览器手上只有一个看不懂的编号

这套机制的好处是可以随时作废。用户点"退出登录",服务器把那条记录删掉,编号立刻失效。管理员想强制某人下线,删账就行。

代价是服务器要存东西。用户多了,这本账会很大,通常要放到一个共享的存储里,让多台服务器都能查。

要点做法为什么
编号要随机足够长、不可预测否则能被猜出来冒用
Cookie 要设标记只走加密通道、禁止脚本读取降低被窃取和被注入读取的风险
要有有效期闲置超时加绝对超时编号泄露后不会永久有效
要能吊销退出、改密、异常时删除这是 Session 最大的优势

23.3JWT:把账本交给用户自己拿

JWT(JSON Web Token,JSON 网络令牌)走了另一条路:服务器不记账,把"你是谁"写在一张纸上,签个名,交给用户自己保管。以后每次请求把这张纸带上,服务器验一下签名就知道真假。

这张纸由三段组成,用两个点隔开:

# 长相:三段,两个点
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMDA4NiJ9.7Xk2...

# 第一段 Header:说明用什么算法签名
{"alg":"HS256","typ":"JWT"}

# 第二段 Payload:放声明,谁、什么时候过期
{"sub":"10086","name":"demo","iat":1729900000,"exp":1729903600}

# 第三段 Signature:用密钥对前两段做签名
HMAC-SHA256(base64url(header) + "." + base64url(payload), secret)
段放什么能不能改
Header算法名、类型能改,但改了签名就对不上
Payload用户标识、过期时间、角色能改,但改了签名就对不上
Signature对前两段的签名结果改不了,需要密钥

关键点在这里:前两段只是编码,不是加密。所谓 Base64URL 编码,就是把二进制换成可打印字符,任何人都能还原。你随便找个在线工具粘贴一段 JWT,就能看到里面的全部内容。

红线 JWT 的 Payload 是明文可读的。不要把手机号、身份证、住址、密码这类内容放进去。需要放敏感信息时,要么加密,要么只放一个编号,真正的数据留在服务端。

那 JWT 到底解决了什么?它解决的是信任:服务器不需要记账,只要验签就能确认这张纸是自己发的、内容没被改过。这在多台服务器、跨服务调用的场景下很省事。

代价是不好作废。签名一旦发出,在过期之前它一直有效。想提前作废,就得在服务端另外维护一张"黑名单",绕了一圈又回到了记账。

23.4JWT 的五个经典坑

JWT 本身没问题,出问题的是校验实现。下面五个坑,每一个都在真实系统里出现过。

坑一:alg=none

JWT 的 Header 里有一个 alg 字段,声明用什么算法签名。规范里允许 none,表示"不签名"。

如果服务端信任了客户端给的 alg,攻击者把 alg 改成 none、把签名段删掉,服务端就会认为"这是合法的不签名令牌",直接放行。

防护:服务端自己定死允许的算法,绝不按令牌里写的来。看到 none 一律拒绝。

坑二:算法混淆

系统本来用非对称算法(私钥签名、公钥验签)。攻击者把 alg 改成对称算法,然后拿公开的公钥当密钥去算签名。如果实现没锁算法,验签就会通过。

类比:门卫本来只认"公司印章",你告诉他"今天改认身份证复印件",然后自己复印了一张。

防护:同样一句话,算法由服务端定,不由令牌定。

坑三:kid 注入

kid(Key ID,密钥编号)是 Header 里用来告诉服务端"用哪把钥匙验签"的字段。有的实现会拿它去查文件或查数据库。

如果 kid 直接拼进路径或查询语句,就可能被用来指向别的文件、别的记录,甚至执行注入。这和第 22 章的注入是同一类问题。

防护:kid 只允许匹配一个预定义的白名单,不要参与任何路径拼接或语句拼接。

坑四:弱密钥

对称签名用的是密钥。如果密钥是 secret、123456、公司名这类可猜的字符串,攻击者拿到一个合法令牌后,可以离线反复试密钥,直到试出能对上的那一个。

离线意味着没有次数限制、没有告警、不需要再碰你的服务器。所以密钥强度比什么都重要。

防护:密钥用足够长的随机值,定期轮换,不要写进代码仓库。

坑五:只验签名,不验声明

很多人以为"签名对了就万事大吉"。但签名只证明"这张纸是我发的",不证明"这张纸现在还能用"。

必须逐条检查的声明:

  • exp:过期时间,过了就不能用;
  • nbf:生效时间,没到就不能用;
  • aud:这张令牌是发给哪个服务的,不能拿去给别的服务用;
  • iss:是谁签发的,只认自己信任的签发方。
注意 这五个坑的共同点是:服务端把决定权交给了令牌。算法、密钥编号、有效期,都应该是服务端说了算,而不是令牌里写什么就信什么。

23.5OAuth2:让别人代你授权,而不是把密码给它

先纠正一个最常见的误解:OAuth2 是授权框架,不是认证协议。它解决的是"怎么让 A 应用在有限范围内访问我在 B 平台的数据,而不用把 B 的密码告诉 A"。

类比:你住酒店,想让朋友帮你进房间取个东西。你不会把房卡给他(等于给密码),而是去前台开一张只限今天、只限这个房间的临时卡。

流程用在什么场景现在还行吗
授权码有后端服务的网站和 App推荐,配合 PKCE 更好
隐式纯前端页面,历史上用过已不推荐,令牌易暴露
密码式用户直接把密码交给应用已不推荐,违背初衷
客户端凭证服务对服务,没有用户参与仍然常用

四种里最该弄懂的是授权码加 PKCE。它是目前移动端和单页应用的标准做法。

授权码加 PKCE:五步换到访问令牌 1 生成随机串 verifier,算出它的哈希 challenge 2 打开授权页,只带 challenge,不带 verifier 3 用户同意,回调一个一次性 code 4 用 code 加原始 verifier 换令牌,服务器核对哈希 5 拿到 access_token,之后每次请求都带上它
图 23-3 PKCE 的作用是让"偷到 code 也没用",因为换令牌还需要原始随机串

为什么要有 PKCE(Proof Key for Code Exchange,授权码交换证明密钥)?因为移动端的"回调地址"不一定可靠,别的应用可能抢到那个 code。

PKCE 的思路很巧:

  • 第一步先在本地生成一个随机串,只把它的哈希发出去;
  • 第三步拿回来的 code 是公开的,路上可能被截;
  • 第四步换令牌时,必须同时交出原始随机串,服务器算一遍哈希,对得上才给令牌。

结果就是:截到 code 的人没有原始随机串,换不到令牌。这是典型的"用一次性证明换长期凭证"。

补充 授权码流程里还有一个关键参数叫 state,作用是防跨站请求伪造。它是一段随机值,发出去时存一份,回来时必须一模一样,否则拒绝。

23.6OIDC:在 OAuth 上加一层"你是谁"

OAuth2 只解决授权,它不告诉你用户是谁。你拿到一个访问令牌,只能证明"有权访问某些数据",不能证明"操作的人叫张三"。

OIDC(OpenID Connect,开放身份连接)就是在 OAuth2 之上加了一层身份层。它的核心产物叫 ID Token,格式就是 JWT。

ID Token 里通常包含这些声明:

  • sub:这个用户的唯一标识;
  • iss:谁签发的;
  • aud:签给哪个应用的;
  • exp:什么时候过期;
  • nonce:一次性随机值,防重放。

一句话区分:access_token 用来访问资源,id_token 用来说明身份。两者不能混用。

23.7SAML:企业里最常见的那一种

SAML(Security Assertion Markup Language,安全断言标记语言)是一套基于 XML 的单点登录标准。你在公司点"用企业账号登录",背后很可能就是它。

它的结构和 OIDC 很像:一个身份提供方负责认证,一个服务提供方负责提供服务,中间传一份签名过的断言,告诉服务方"这个人是谁、有什么属性"。

差别在形态:SAML 用 XML,走浏览器重定向,配置复杂,企业里用得多;OIDC 用 JSON,走接口,移动端友好。功能上高度重叠,选哪个通常取决于对方支持哪个。

23.8MFA 与 TOTP:第二把钥匙

MFA(Multi-Factor Authentication,多因素认证)的意思是:至少用两类不同的东西证明身份。三类分别是"你知道的"(密码)、"你有的"(手机、硬件钥匙)、"你是什么"(指纹、人脸)。

只用密码,是单因素。密码加短信码,是双因素。密码加密码,还是单因素。

TOTP(Time-based One-Time Password,基于时间的一次性口令)是最常见的第二因素,就是那个每 30 秒变一次的 6 位数字。

# 原理:双方共享一个密钥,用当前时间片算同一个值
时间片 = 当前 Unix 时间 / 30
口令   = HMAC-SHA1(共享密钥, 时间片) 取其中 6 位

# 服务器允许前后各一个时间片,容忍时钟误差
所以在 30 秒边界附近,同一个码可能还有效

为什么它能防住密码泄露?因为攻击者只有密码,没有那个共享密钥,算不出当前的 6 位数字。而且这个数字 30 秒就废,截到了也用不上。

它的弱点也很明确:共享密钥一旦泄露,第二因素就没了。所以密钥要存在安全的地方,不要截图、不要明文备份。

23.9密码怎么存:为什么不能明文,也不能单次哈希

先说不该做的两件事:

  • 存明文:数据库一旦泄露,所有人的密码直接暴露,而且用户往往在多处复用同一个密码;
  • 存单次哈希:看起来安全,其实挡不住现代硬件。普通哈希算得极快,一秒能试几十亿次。

正确的做法是用专门为口令设计的慢哈希。它们的共同特点是:故意算得慢,而且可以调慢。

算法靠什么变慢特点
bcrypt可调的计算轮数成熟稳定,有长度上限
scrypt大量内存加计算抗硬件加速,参数较多
Argon2内存、时间、并行度都可调较新,推荐用 Argon2id

除了慢,还必须加盐。盐是每个用户一份的随机值,和密码一起参与计算。

不加盐的后果是:相同密码算出相同哈希,攻击者可以预先算好一张巨大的对照表,一次查表破解一大批账号。加盐之后,每个人的计算结果都不同,预先算好的表就废了。

技巧 判断一个系统的密码存储好不好,只看两点:用的算法是不是慢哈希,有没有每人一份的盐。两点都满足,基本合格;只要存了明文或用了普通哈希,就是严重问题。

23.10两个最普遍的误区

误区一:JWT 是加密的

不是。JWT 默认只做签名和编码,不做加密。签名保证内容没被改,编码只是换成可打印字符,两者都不隐藏内容。

你随便把一段 JWT 粘到解码工具里,Payload 会原样显示出来。所以"把用户角色放进 JWT 里就安全了"这个想法是错的:别人能看见,只是改不了。

要隐藏内容,得额外做加密(JWE 这一类)。多数场景不需要,正确做法是不放敏感信息。

误区二:OAuth 是认证协议

不是。OAuth2 是授权委托框架:它让 A 应用拿到一张有限权限的票,去访问 B 平台的数据。它本身不回答"这个人是谁"。

早期很多人拿 access_token 去换用户信息,然后当成"登录完成",这就是所谓的"用 OAuth 做认证"。问题在于:拿到令牌不等于证明了这个人的身份,令牌可能是别的应用转手来的。

正确做法是用 OIDC:让身份提供方额外签发一个 id_token,里面明确写了 sub、aud、nonce,并且由你信任的签发方签名。

注意 一句话记牢:OAuth 管"能访问什么",OIDC 管"你是谁"。要登录,用 OIDC;要代访问,用 OAuth。混着用就会出现"令牌被盗等于账号被盗"的问题。
自测:认证和授权,哪个先发生?
认证先发生。先确认你是谁,才能判断你能干什么。认证失败返回 401,授权失败返回 403。两者不能合并成一步。
自测:JWT 的 Payload 能不能放手机号?
不能。Payload 只是 Base64URL 编码,任何人解码就能看到,等于明文。需要放敏感数据时应该加密,或者只放一个编号,真实数据留在服务端。签名不等于加密。
自测:PKCE 解决了什么问题?
解决"授权码在回调路上被截获"的问题。换令牌时必须同时提交原始随机串,服务器核对哈希。截到 code 但没有原始串,就换不到令牌。本质是用一次性证明保护长期凭证。
自测:为什么密码不能只用普通哈希存?
普通哈希算得太快,现代硬件一秒能试几十亿次,短口令很快被穷举。而且不加盐时相同密码哈希相同,可以预先算表批量破解。要用 bcrypt、scrypt、Argon2 这类慢哈希,并给每人加盐。
动手练
  • 找一段公开的示例 JWT,把三段分别解码,看看 Header 和 Payload 里到底有什么。
  • 画一遍 Session 流程:哪一步服务器存了什么,哪一步浏览器存了什么。
  • 用一句话向同事解释 OAuth 和 OIDC 的分工,不许用"授权""认证"这两个词。
  • 检查你项目里的令牌:有效期多长,能不能吊销,Payload 里有没有不该放的信息。
认证Authentication证明"你是你说的那个人"。
授权Authorization决定"这个身份能做什么"。
会话Session服务器保存的登录状态记录,浏览器只拿编号。
JWTJSON Web Token三段式令牌,签名保证不被篡改,但不加密。
声明ClaimJWT 里的字段,如 sub、exp、aud。
PKCEProof Key for Code Exchange用一次性随机串保护授权码交换。
OIDCOpenID Connect在 OAuth2 上加身份层,产出 ID Token。
慢哈希Password Hashing故意算得慢的口令哈希,如 bcrypt、Argon2。
你现在应该能回答 认证和授权分别回答什么问题?Session 和 JWT 各自把状态放在哪?JWT 三段里哪一段能改、为什么?OAuth 为什么不是认证协议?密码为什么要用慢哈希加盐?这五个问题答得上来,你已经能看懂绝大多数登录方案的取舍。
第 24 章 · 接口与云端

API、GraphQL 与 WebSocket:接口长什么样,又会在哪里漏

App 上每一个按钮,背后都在调一个接口。接口设计得好不好,直接决定别人能不能看到你的订单、你的手机号。这一页把 REST、GraphQL、WebSocket 三种最常见的接口讲清楚,重点讲它们各自会在哪里漏。

REST鉴权水平越权 GraphQLWebSocket限流
先记住这一句 接口出问题,绝大多数不是"加密被破了",而是该检查的没检查:只验了你有没有登录,没验这条数据是不是你的;只按请求数限流,没算一次请求里塞了多少活。
人话版 这一页讲三件事:接口怎么约规矩、怎么证明"你是你"、以及为什么"登录了"不等于"什么都能看"。

24.1接口就是餐厅的点菜窗口

你去餐厅吃饭,不会直接闯进厨房翻冰箱,而是看菜单、报菜名、等上菜。接口(API,Application Programming Interface,应用程序编程接口)就是这个窗口:它规定你能点什么、怎么点、多久能上。

最常见的接口风格叫 REST(Representational State Transfer,表述性状态转移)。它只有两条规矩:

  • 地址表示"哪件东西":/api/orders/1001 就是 1001 号订单;
  • 方法表示"对它做什么":读、建、改、删。
REST 用同一个地址加不同方法完成增删改查 地址指"哪件东西",方法指"对它做什么" GET 查 读这张订单 POST 增 新建一张订单 PATCH 改 只改地址一栏 DELETE 删 作废这张订单 服务器用状态码回答结果:2xx 成功,4xx 你这边有问题,5xx 服务器自己出错 200 / 201 拿到了 / 建好了 401 / 403 没登录 / 没权限 404 / 429 / 500 不存在 / 太快 / 崩了 401 和 403 是两件事:前者是"你是谁我不知道",后者是"知道你是谁,但不许"
图 24-1 四个方法加一个地址,就是 REST 的全部骨架
方法干什么例子重复发一次
GET读/api/orders/1001结果不变
POST新建/api/orders多出一张订单
PATCH改一部分/api/orders/1001结果不变
DELETE删除/api/orders/1001第二次报 404

"重复发一次结果不变"这件事有个专门的名字,叫幂等(Idempotent)。它的实际意义是:网络抖动导致你没收到回包,敢不敢直接重试。GET、PATCH、DELETE 可以放心重试,POST 不行——否则一次下单会变成两次。

补充 状态码只是"结果分类",不是"安全标记"。很多系统不管成功失败都回 200,把真实结果塞在 {"code":403} 里。看接口时要两处都看,别只盯状态码。

24.2四种证明"你是你"的方式

接口不能谁都能调。服务器要确认两件事:你是哪个调用方,以及你代表哪个用户。常见的四种做法,安全强度和成本完全不同。

方式证明什么泄露后果典型场景
API Key你是哪个调用方谁拿到谁用,长期有效开放平台
HMAC 签名内容没被改、请求是新的密钥泄露等于全部失守支付、内部服务
OAuth Token某用户授权了哪些范围有效期内可冒充该用户第三方登录
mTLS双方都持有合法证书证书私钥泄露服务之间互调

用生活场景对照:API Key 像小区的固定门禁卡,丢了别人能一直用;HMAC 签名像每次进门都要当场签一张带时间的条子,条子过期就作废;OAuth Token 像酒店房卡,只开你这间房,退房自动失效;mTLS 像双方都要出示身份证才肯谈事。

HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码)值得多说一句。它不是加密,内容还是明文,只是额外附一段用密钥算出来的"指纹"。服务器收到后自己也算一遍,对不上就丢。所以它能防篡改,但防不了偷看——偷看要靠 TLS。

注意 HMAC 签名必须把时间戳和一次性随机数(Nonce)一起签进去。否则攻击者把昨天的请求原样重发一遍,签名照样能对上,这叫重放(Replay)。

24.3换个编号就能看别人的数据:BOLA 与 IDOR

这是接口里最常见、也最容易被忽视的一类问题。它不需要破解密码,不需要伪造 Token,只需要把地址里的数字改一下。

水平越权:Token 有效但数据属于别人 两个人登录后各拿一张 Token,区别只在订单号 账号 A Token A 有效 换个编号再请求 GET /orders/1001 只验 Token 通过,放行 返回别人的单 姓名 电话 地址 订单 1001 属于 B 编号是连号,很好猜 没检查归属 认证只回答"你是谁",授权才回答"这条数据你能不能看" 少写一行归属校验,等于把整张表开放
图 24-2 Token 是真的,订单也是真的,只是这张订单不是你的

这类问题的学名叫 BOLA(Broken Object Level Authorization,对象级授权缺失),旧一点的说法叫 IDOR(Insecure Direct Object Reference,不安全的直接对象引用)。名字很绕,本质一句话:

一句话 服务器拿请求里的 ID 去查数据库,却没有用 Token 里的用户 ID 做过滤条件。

正确的写法是让数据库帮忙兜底:select * from orders where id = 1001 and uid = 当前登录用户。查不到就返回 404,而不是 403——连"这条数据存在"都不要告诉对方。

为什么这么容易漏?因为开发时测试账号永远只测自己的数据。自己看自己的订单,永远是通的。只有换成别人的 ID 才会暴露,而这恰恰没人测。

24.4批量赋值:多传一个字段,权限就变了

假设注册接口只该收昵称,正常请求是这样:

POST /api/register
{"nickname": "xiaoming", "phone": "13800000000"}

如果后端偷懒,直接把收到的 JSON 整个塞进用户对象,那么攻击者只要多写一行:

{"nickname": "xiaoming", "phone": "13800000000",
 "role": "admin", "balance": 999999}

账号建出来的那一刻就是管理员。这叫批量赋值(Mass Assignment),比喻起来就是:你去办会员卡,表格上只让你填姓名,但你把"等级"那一栏也填了,柜台看都没看就盖章。

  • 根因:把"用户能提交的字段"和"数据库里的字段"当成同一份清单。
  • 正确做法:只允许白名单里的字段进来,其余一律丢弃;角色、余额这类字段只能由服务端逻辑写。
  • 同类问题:改 uid、改 status、改 created_at,都属于这一类。

24.5限流:为什么只按 IP 限不够

限流(Rate Limiting)是给接口装一个"水龙头",单位时间内最多放多少请求过去。它保护三件事:服务器不被压垮、密码不被暴力猜、优惠券不被脚本抢光。

常见的四种算法,用一句话说清:

  • 固定窗口:每分钟清零一次,简单,但窗口边界会漏;
  • 滑动窗口:只看最近 60 秒,边界更平滑,成本略高;
  • 令牌桶:桶里按固定速度放令牌,来一个请求取一个,取完就排队——能容忍短时突发;
  • 漏桶:请求先进桶再从底部匀速漏出,输出永远平稳。

真正决定成败的不是算法,而是按什么维度计数。这是防守方最容易做窄的地方:

只按这个限怎么被绕开该补的维度
IP换出口 IP,成本很低账号 + 设备
账号换账号,注册成本更低设备 + 行为
单接口换一个功能相同的接口全局配额
请求次数一次请求里干很多活按"工作量"计费

最后一行是 GraphQL 特有的坑:一次 HTTP 请求可以包含几十个查询,按请求数限流等于没限。所以成熟的做法是按查询复杂度扣配额,而不是按请求条数。

补充 限流是"减速",不是"身份判断"。它拦不住一个慢慢来的脚本——每分钟 3 次、持续 10 小时,任何阈值都不会触发。真正能发现这种节奏的,是下一章的检测体系。

24.6GraphQL:一个入口,一张自己写的菜单

REST 的菜单是店家定好的:这个接口返回这几个字段。GraphQL 反过来,菜单由客户端现场写——你说要哪些字段,服务器就给你哪些。

好处很明显:一次请求拿全所需数据,不用来回调五个接口,手机流量也省。代价同样明显:服务器把"取什么"的控制权交出去了一半。

一次 GraphQL 查询可以同时展开多个分支 一个请求,三个分支,服务器全部照单执行 query 一次请求 POST /graphql user(id: 1002) 读这个人的资料 orders(first: 100) 一次拉一百条 __schema 列出所有类型 email phone 地址 字段由客户端定 数据库压力 深一层再深一层 接口地图全暴露 内省默认是开的 三个分支共用一次鉴权和一次限流配额,这就是它的结构性弱点
图 24-3 GraphQL 把"取哪些字段"的决定权交给了调用方

下面四个词,是 GraphQL 特有的风险点,逐个说清:

  • 内省(Introspection):GraphQL 自带一份"自我说明书",问一句就能列出所有类型和字段。开发阶段很方便,生产环境开着等于把接口地图贴在门口。多数团队会在线上关闭它,但关闭只是提高门槛,不是安全边界。
  • 别名轰炸(Alias Overloading):GraphQL 允许给同一个字段起多个别名,一次请求就能重复调用几十次。按"请求数"限流的系统完全看不见这几十次。
  • 深度查询(Deep Query):字段可以嵌套,A 里有 B,B 里又有 A。写十层嵌套,服务器就可能做十次关联查询,一次请求打垮数据库。
  • 过度取数(Over-fetching):既然能一次要全部字段,那就一次要全部字段。本该只返回昵称的接口,可能顺手把手机号、身份证、内部备注都带出来。

对应的防护也是四条:限制查询深度、给查询算复杂度并扣配额、字段级白名单、线上关闭内省。这四条要一起上,缺一条都能被绕。

24.7WebSocket:连上之后,规矩变了

HTTP 是"你问一句,它答一句"。WebSocket 是打一通电话:握手一次,之后双方随时都能说话,服务器可以主动推送。

握手仍然借用 HTTP。客户端发一个带 Upgrade: websocket 的请求,服务器同意就回 101 Switching Protocols,这条连接从此不再走 HTTP 的问答模式。

之后传输的是帧(Frame),每帧头部只有几个字节,包含:

  • FIN:这一帧是不是消息的最后一帧;
  • Opcode:是文本、二进制、ping、pong 还是关闭;
  • Mask:客户端发出的帧必须打掩码,这是协议强制的,防止中间代理缓存污染;
  • Payload length:载荷长度,短消息只有 7 位,长消息扩展成 16 位或 64 位。
对比项HTTPWebSocket
方向客户端发起双向随时
连接短连接或复用一条长连接
头部开销每次几百字节每帧几字节
鉴权时机每个请求都验只在握手时验一次

最后一行是关键。握手时验过身份,之后这条连接上发什么,很多实现就不再检查了。于是出现订阅越权:连上之后发一条 {"action":"subscribe","channel":"order.1002"},服务器如果不核对"1002 是不是你的订单",就会把别人的订单状态实时推给你。

心跳(Ping/Pong)是另一件常被忽略的事。长时间没数据的连接会被中间的代理或负载均衡器悄悄掐断,所以双方要定期发心跳保活。对防守方来说,心跳还有个副作用:它让"连接活着"和"用户在操作"变成两件事,风控不能只看连接是否存在。

注意 WebSocket 没有同源策略保护。浏览器允许任意页面连任意地址,服务器必须在握手阶段校验 Origin,否则别的网站可以在用户不知情时借用他的登录态建立连接。

24.8gRPC 与 Protobuf:一句话版本

gRPC 是把接口调用做成"像调本地函数一样"的框架,数据用 Protobuf(Protocol Buffers)序列化成二进制,跑在 HTTP/2 上。相比 JSON,它体积小、解析快、有强类型约束,所以微服务之间大量使用。

代价是:抓包看到的是一堆二进制,没有 .proto 定义文件就基本读不懂。这也是为什么分析内部服务时,拿到 proto 文件往往比拿到流量更值钱。

24.9网关站在链路的哪个位置

一个请求从手机到数据库,通常要过好几道门。API 网关(API Gateway)是其中唯一统一收口的那一道:

手机 → CDN / WAF → 网关 → 业务服务 → 数据库

  • CDN / WAF:挡流量洪水和明显的攻击特征;
  • 网关:验 Token、限流、路由到具体服务、记录访问日志、脱敏;
  • 业务服务:真正的权限判断和数据查询;
  • 数据库:最后一道,也是唯一能保证"只返回自己的数据"的地方。

网关好用,但也制造了一个经典盲区:只在网关做鉴权。服务之间的内部调用往往直连,不走网关,于是"内网就是可信的"这种假设一旦成立,任何能进入内网的人都能横着走。这也是第 25 章要讲的容器与云环境风险的前置条件。

24.10两个流传很广的误区

误区一:有 Token 就不会越权。Token 只证明"你是登录用户",不证明"这条数据归你"。图 24-2 里那张 Token 完全合法,问题出在没人检查归属。认证和授权是两道独立的门,前一道开了不代表后一道也开了。

误区二:GraphQL 比 REST 安全,因为端点只有一个。恰恰相反。REST 的每个接口可以单独配权限、单独限流;GraphQL 把所有能力集中到一个端点,攻击面没有变小,反而更难按接口做细粒度控制。端点少只是"看起来清爽",不是"更安全"。

自测:401 和 403 的区别是什么?
401 是"我不知道你是谁",需要先登录;403 是"我知道你是谁,但你没这个权限"。返回哪个本身就是信息:查别人的订单时更稳妥的做法是返回 404,连"这条数据存在"都不透露。
自测:为什么说"有 Token 就不会越权"是错的?
Token 解决的是认证(你是谁),越权属于授权(你能看哪条)。如果服务端只验 Token 有效、不验数据归属,合法 Token 一样能读到别人的数据。BOLA 就发生在这种情况下。
自测:一次 GraphQL 请求里写 50 个别名,为什么能绕开限流?
因为多数限流按 HTTP 请求数计数,而这 50 次查询共用同一个请求,只扣一次配额。防护要改成按查询复杂度计费,并限制单次请求的字段数量和嵌套深度。
自测:WebSocket 为什么容易出订阅越权?
因为鉴权通常只在握手时做一次,之后这条长连接上发的订阅指令如果不再核对资源归属,就能订阅到别人的频道。每条订阅消息都要重新校验一次权限。
自测:只在网关做鉴权,风险在哪里?
服务之间的内部调用往往直连、不过网关,一旦有人进入内网,就绕过了所有校验。每个服务都要自己再校验一次,网关只是第一道,不是唯一一道。
动手练
  • 打开浏览器开发者工具的网络面板,随便点几个页面,记录调用了哪些路径、用了哪些方法、返回了什么状态码。
  • 找一个自己有权访问的详情页,观察地址里有没有连续数字的 ID;只在自己的账号范围内改动,确认是否被拒绝。
  • 用一句话向别人解释"认证"和"授权"的区别,不许用这两个词本身。
接口API程序之间约定好的调用入口,规定能要什么、怎么要。
REST表述性状态转移用地址表示资源、用方法表示动作的接口风格。
幂等Idempotent同样的请求发多次,结果和发一次一样。
BOLA对象级授权缺失只验身份不验归属,换个 ID 就能读别人的数据。
批量赋值Mass Assignment多提交一个不该由用户控制的字段,被直接写进数据库。
内省IntrospectionGraphQL 自带的接口说明书,能列出所有类型和字段。
订阅越权Subscription Abuse连上长连接后订阅不属于自己的频道。
API 网关API Gateway所有外部请求的统一入口,负责鉴权、限流、路由和日志。
你现在应该能回答 资源和方法分别表示什么?四种鉴权方式各证明什么?为什么 Token 有效还会越权?GraphQL 的四个风险点是什么?WebSocket 的鉴权为什么容易漏?网关为什么不能是唯一防线?这六个问题能答上来,接口这一层就算过关了。
第 25 章 · 云原生

云原生与容器安全:谁该为哪一层负责

"上云"不等于"安全交给云厂商"。云上的事故,绝大多数发生在用户自己负责的那一半:权限配太宽、存储桶设成公共读、容器里挂着宿主机的控制接口。这一页把责任边界、身份体系、容器隔离和集群结构讲清楚。

责任共担IAM元数据服务 容器隔离Kubernetes最小权限
先记住这一句 云安全的失分点几乎都指向同一件事:权限给多了、边界画错了。容器不是虚拟机,Secret 不是保险箱,云厂商只负责到虚拟化层。
人话版 这一页讲:租云主机就像租公寓,房东管楼体,你管屋里。屋里丢了东西,不能怪房东。

25.1责任共担:一把刀从中间切开

云厂商卖的不是"安全",是"算力和存储"。它负责到哪一层,写在合同里,叫责任共担模型(Shared Responsibility Model)。

用租房打比方:房东负责楼体结构、水电入户、大门门禁;你负责自己屋里的门锁、财物、访客。房东不会替你保管钱包,你也不能去改楼体的承重墙。

责任共担模型:从机房到数据,责任逐步移交给使用方 越往上,责任越靠近你自己 云厂商负责 使用方负责 机房、电力、网线 不归你管 宿主机与虚拟化层 不归你管 托管服务本身的补丁 你自己装的系统自己补 不归你管 代码、账号、数据、权限 分界线会随着服务模式上下移动,但最上面一层永远是你的 数据泄露、权限配错、密钥外泄,这三件事云厂商一律不背
图 25-1 责任共担:越靠近业务,越没人替你兜底
模式厂商管到你管什么
IaaS 云主机机房与虚拟化系统、中间件、应用、数据
PaaS 托管平台到运行时应用代码、配置、数据
SaaS 成品软件几乎全部账号、权限、数据内容

看这张表要抓住一点:服务越"托管",你手上的活越少,但剩下的活越关键。SaaS 里你几乎不用运维,可一旦把权限设成"所有人可编辑",数据照样会没。

25.2IAM:云上的身份系统

IAM(Identity and Access Management,身份与访问管理)就是云平台的"人事部加门禁系统"。四个概念,必须分清:

  • 用户(User):一个长期存在的身份,通常对应一个人或一个系统。它有固定的密钥,也最容易变成历史包袱。
  • 角色(Role):一个"可以被扮演"的身份,本身没有密码。谁满足条件,谁就能临时变成它。
  • 策略(Policy):一段 JSON,写清"允许谁,对什么资源,做哪些动作"。它是权限的真正来源。
  • 临时凭证(Temporary Credentials):扮演角色后拿到的一组密钥,带有效期,通常几十分钟到几小时。

类比:用户是员工工牌,长期有效,丢了很麻烦;角色是"今日值班经理"这个岗位,谁戴上袖标谁就是,下班自动失效。临时凭证就是那枚几小时后自动消失的袖标。

策略里最危险的两个词是 Action: "*" 和 Resource: "*"。翻成大白话就是"任何动作,对任何资源"。很多事故的起点,就是某人为图省事写了这一行。

// 图省事的写法:等于把整栋楼的钥匙串交出去
{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}
注意 长期密钥(Access Key)一旦写进代码、配置文件、Git 历史,就等于永久泄露。正确做法是用角色加临时凭证,让密钥天生带过期时间。

25.3元数据服务:云主机的"身份证窗口"

云主机里跑的程序,怎么知道自己是谁、有哪些权限?靠一个特殊的内网地址:169.254.169.254。

这是一个链路本地地址,只在当前网络段内可达,外面访问不到。程序向它发一个请求,就能拿到实例 ID、区域、网络配置,以及绑定在这个实例上的临时凭证。设计初衷是好的:不用在机器上存密码,凭证自动轮换。

问题在于,它不区分请求是谁发的。只要请求来自这台机器,它就照给。

于是 SSRF(Server-Side Request Forgery,服务端请求伪造)就有了用武之地。假设网站有个"图片抓取"功能,用户填一个网址,服务器替他去下载。攻击者填的不是图片地址,而是那个内网地址。服务器乖乖去取,把临时凭证原样返回给攻击者。

这条链之所以危险,是因为它绕过了所有网络边界:攻击者从头到尾没进过内网,只是让一台有权限的机器替他跑了一趟腿。

  • 缓解一:元数据服务开启强制令牌模式,请求必须先拿到一次性令牌,简单的一行 GET 拿不到东西;
  • 缓解二:限制实例的出口,禁止访问链路本地地址段;
  • 缓解三:给"替用户抓取 URL"这类功能加协议与地址白名单,只允许公网地址。

25.4对象存储:门没锁,还贴了清单

对象存储(Object Storage)就是云上的网盘,用"桶(Bucket)"装"对象(Object)"。它的权限有两层:桶级策略决定"谁能对这个桶做什么",对象级 ACL 决定"单个文件谁能读"。

最常见的失误是公共读。设成公共读的桶,任何人只要知道地址就能下载,不需要登录。更糟的是如果还允许"列举",访问者能看到桶里所有文件名。

类比:你把仓库门敞着,还在门口贴了一张完整清单,写着"客户资料 2024 全年备份.zip"。别人不需要撬锁,按清单拿就行。

配置实际效果风险
公共读 + 可列举任何人可下载、可看目录最危险
公共读 + 不可列举知道地址就能下地址一旦外流即失守
私有 + 预签名限时链接,过期失效链接被转发

正确姿势是默认私有,需要分享时生成预签名 URL(Presigned URL):一条带签名和过期时间的临时链接,几分钟后自动失效。再配一条"阻止公共访问"的总开关,从组织层面堵死误操作。

25.5容器隔离:三道墙,一个内核

容器不是"小虚拟机"。它只是被限制住的一组进程,跑在宿主机同一个内核上。隔离靠三样东西:

容器的三道隔离墙都建立在同一个内核之上 容器里的进程 它以为自己是唯一的 namespace:看不见别人的文件、进程、网络 cgroup:限制 CPU、内存、磁盘用量 capabilities:砍掉不需要的特权 宿主机内核 其实只有一个
图 25-2 三道墙都建在同一个内核上,所以内核出问题,墙就一起塌
机制管什么类比
namespace看不见别人的东西单面镜房间
cgroup用不超配额每月水电上限
capabilities拿不到额外特权只给一把钥匙

注意这三样都是访问控制,不是硬件隔离。虚拟机有独立内核,容器共用宿主内核。所以一旦内核有提权漏洞,或者容器被授予了过多能力(比如 --privileged),隔离就可能被打穿。

真正需要强隔离的场景(多租户、跑不受信任的代码),应该用更硬的边界:独立虚拟机、用户态内核,或者专门的沙箱运行时。

25.6Docker socket:把方向盘交出去

/var/run/docker.sock 是 Docker 的控制接口。谁能让它执行命令,谁就能让宿主机起新容器、挂载任意目录、读取任意文件。

把 socket 挂进容器,常见的理由是"让容器里的程序管理其他容器"。但这等于把车钥匙留在车里,还开着窗:容器里只要能连上这个 socket,就能起一个挂载宿主机根目录的新容器,然后读取宿主机上的一切。

  • 不要挂:绝大多数业务容器根本不需要它;
  • 要用就代理:在 socket 前放一个只放行特定接口的代理;
  • 更彻底:用无 root 的容器运行时,从根上减少特权。

25.7Kubernetes:一个集群里的五个角色

Kubernetes(常简称 K8s)是容器的调度系统。你告诉它"我要三个副本一直活着",它负责找机器、拉镜像、起容器、挂了重启。

Kubernetes 集群里从提交到读取密钥的路径 你或 CI 提交一份配置 API Server 唯一入口 etcd 存全部状态 ServiceAccount Pod 的身份 Pod 跑你的容器 Secret 密码与证书 Pod 里默认挂着一个 Token,它就是这张 ServiceAccount 的通行证
图 25-3 集群里所有操作都经过 API Server,而每个 Pod 天生带着一个身份
  • Pod:最小调度单位,里面可以有一个或多个容器,共享网络与存储。
  • ServiceAccount:Pod 的身份。默认会自动挂一个 Token 进容器,让容器能调用 API Server。
  • RBAC:基于角色的访问控制。用"角色 + 绑定"决定谁能对哪些资源做哪些动作。
  • Secret:存放密码、证书、Token 的对象。注意它默认只是 base64 编码,不是加密。
  • API Server:所有操作的唯一入口,也是所有权限检查发生的地方。

风险链条很清晰:容器被攻破 → 攻击者读到挂载的 Token → 用 Token 调 API Server → 如果这个 ServiceAccount 的 RBAC 权限过宽(比如能读所有 Secret),就能横向拿到整个集群的凭证。

所以集群加固的重点不是"防住每一个容器",而是假设容器一定会被攻破,把 Token 的权限压到最小:关闭不需要的自动挂载、每个应用一个专用账号、禁止跨命名空间读取。

25.8供应链:你信任的是别人的构建结果

现代应用的代码里,自己写的往往不到一成,其余都是基础镜像和第三方依赖。这条链上有三个入口:

  • 基础镜像:用了来源不明的镜像,或者长期用 latest 标签,今天拉到的和明天拉到的可能不是一个东西;
  • 依赖包:包名相似的仿冒包、被接管的维护者账号、构建脚本里藏的钩子;
  • CI/CD:构建流水线里存着部署密钥,一旦流水线配置可被修改,等于把发布权交出去。

类比:餐厅的食品安全,不只取决于厨师,还取决于食材从哪来、谁运的、仓库锁没锁。你端上桌的菜,责任是你的。

可行的做法:镜像固定到具体版本甚至摘要值、只从可信仓库拉取、构建环境与生产密钥隔离、每次构建留下可追溯的记录。

25.9最小权限:一句原则,三条落地

最小权限原则(Principle of Least Privilege)说起来只有一句:只给完成这件事所需的权限,并且只给到这件事结束为止。

落地成三条可执行的动作:

  • 按需申请:默认没有权限,需要时申请,写清用途和时长;
  • 到期回收:临时凭证自动过期,项目结束后角色立即解绑;
  • 用角色代替用户:人和系统都通过角色获取临时权限,减少长期密钥的数量。

代价是麻烦。每次都要申请,效率会降。这就是为什么很多团队明知有风险,还是留着一把"万能钥匙"。安全上的每一个便利,最后都会变成一次事故的放大器。

25.10两个必须纠正的误区

误区一:容器等于虚拟机。虚拟机靠硬件虚拟化,每台有独立内核;容器只是被限制的进程,共用宿主内核。所以"容器里跑的东西坏了也影响不到外面"这个假设是错的:内核一破,隔离全破。

误区二:Secret 放进环境变量就安全。环境变量会被子进程继承,会出现在崩溃转储、调试接口和某些监控采集里。更稳妥的做法是以文件形式挂载到内存文件系统,或者由应用在启动时向密钥管理服务现取,用完不落盘。

自测:为什么"上云了,安全就是云厂商的事"是错的?
责任共担模型里,云厂商只负责到虚拟化层,系统以上(配置、权限、代码、数据)都是使用方的责任。数据泄露和权限配错,永远算在使用方头上。
自测:元数据服务为什么会被 SSRF 利用?
它只判断"请求是否来自这台机器",不判断"是谁让这台机器去请求的"。攻击者只要让服务器替他去访问那个内网地址,就能拿到实例的临时凭证。防护关键是强制令牌、限制出口、给抓取功能加白名单。
自测:namespace 和 cgroup 分别解决什么问题?
namespace 解决"看不见"(文件、进程、网络互相隔离),cgroup 解决"用不超"(限制 CPU、内存、磁盘)。两者都不提供硬件级隔离,因为它们共用同一个内核。
自测:把 Docker socket 挂进容器,最坏会怎样?
容器里的程序可以通过它指挥宿主机启动新容器,并挂载宿主机的根目录,从而读写宿主机上的任意文件。这等于把宿主机控制权交出去,不是"只是多一个挂载点"。
自测:为什么 Kubernetes 的 Secret 不能当成保险箱?
Secret 默认只做 base64 编码,任何能读这个对象的人都能还原出明文。真正的保护来自 RBAC 收紧、静态加密和外部密钥管理,而不是"它叫 Secret"。
动手练
  • 在自己的电脑上装一个容器工具,运行一个最简镜像,然后查看容器内的进程列表,观察它和宿主机的差异。
  • 找一个云平台的权限策略文档,读一遍它的 JSON 结构,找出"允许动作"和"资源范围"分别写在哪。
  • 用一句话向别人解释"为什么容器不是虚拟机",不许用"轻量"这个词。
责任共担Shared Responsibility云厂商与使用方各自负责一段,分界线随服务模式移动。
IAM身份与访问管理云上的账号、角色、策略与临时凭证体系。
策略Policy用 JSON 写明"谁对什么资源能做哪些动作"。
临时凭证Temporary Credentials扮演角色后获得、自带过期时间的密钥。
元数据服务Metadata Service云主机内网可访问的身份与凭证查询入口。
命名空间Namespace让进程看不到其他进程的文件、网络与进程表。
控制组cgroup限制一组进程能使用的 CPU、内存等资源上限。
ServiceAccountKubernetes 中 Pod 的身份,决定它能调用哪些接口。
你现在应该能回答 责任分界线画在哪?用户、角色、策略、临时凭证分别是什么?元数据服务为什么怕 SSRF?容器靠哪三样做隔离、为什么仍不安全?Pod 被攻破后攻击者下一步拿什么?Secret 为什么不是保险箱?这六个问题答得上来,云这一层就有了基本判断力。
第 26 章 · 检测与对抗

检测与对抗:风控是怎么看出来的

很多人以为风控就是"封号",以为"换个 IP 就没事了"。真实的检测是一套从单点规则到多维打分的体系,它看的不是你某一个动作,而是你的动作像不像一个人。这一页讲清它怎么判断,以及为什么单点伪装没用。

规则 / 统计 / 模型特征工程行为序列 图关系误报漏报处置梯度
先记住这一句 检测的本质不是抓"坏人长什么样",而是找不一致:设备和账号对不上、节奏和人不一样、关系和正常用户不同。改一个维度,只会让不一致更明显。
人话版 这一页讲:风控先看规则,再看统计,最后看整体像不像人。它抓的是"对不上",不是"长得坏"。

26.1三层检测:从死规矩到整体判断

风控系统不是一套算法,而是三层叠加。越往下越准但越死板,越往上越灵活但越难解释。

检测的三个层次:规则、统计、模型 同一个动作,三层各自给一个判断,最后合成一个分数 第一层 规则 等于某值就拦 最准,也最死板 第二层 统计 频次、间隔、分布 抓异常,不好解释 第三层 模型 多维一起打分 抓组合,难复盘 规则能说清"为什么拦你",模型只能说"你的分很高" 三层是叠加关系,不是替代关系:模型再强,也要规则兜住明显的红线 越往上,越依赖数据质量和特征设计
图 26-1 三层检测:规则保底、统计找异常、模型看组合
  • 规则层:如果同一设备在 10 分钟内登录了 30 个账号,直接拦。写得出来、说得清楚、执行很快。
  • 统计层:这个账号平时的下单时间是晚上八点,今天凌晨三点连下十单,偏离了自己的历史分布。
  • 模型层:把设备、IP、行为、关系等几十个维度放在一起算一个分,判断"整体像不像正常用户"。

规则层的价值被低估了。它最大的优点不是准,而是可解释、可申诉、可回滚。真出了误判,你能指着那条规则说"这里写错了"。模型做不到这一点。

26.2日志来源:五双眼睛,各看一角

检测的前提是"看得见"。数据来自五个地方,各有盲区。

来源能看到什么看不到什么可信度
客户端埋点点击、滑动、页面停留可被伪造低
网关日志IP、请求头、频率业务含义中
业务日志谁做了什么、结果如何设备细节高
数据库最终状态、变更历史过程最高
设备指纹机型、系统、传感器特征真人身份中高

为什么强调"可信度"?因为越靠近客户端的日志,越容易被动过。请求头里的机型可以随便写,埋点事件可以被脚本批量伪造。所以真正做判定时,业务日志和数据库才是最后的事实来源。

设备指纹是中间地带。它不直接说明"你是谁",但能说明"这台设备还是不是上次那台"。换了一台设备、换了一套传感器特征,即使账号密码都对,也值得多看一眼。

26.3特征工程:把行为翻译成数字

模型只认数字,不认故事。所谓特征工程,就是把"这个人有点怪"翻译成一组可计算的量。

  • 频次:单位时间内做了多少次。一分钟点 200 次,和一分钟点 3 次,含义完全不同。
  • 时间间隔:两次动作之间隔多久。人会有停顿、犹豫、看错、返回,机器通常均匀得像节拍器。
  • 序列:动作的先后顺序。正常人"浏览、加购、下单、支付";脚本可能跳过浏览直接支付。
  • 熵:行为的杂乱程度。真人今天看手机、明天看电脑、偶尔半夜下单;脚本的行为高度重复,熵很低。
  • 一致性:多个维度是否互相对得上。机型是高端机、系统版本很旧、时区却和 IP 归属地对不上,这就是不一致。

用生活例子说"熵":一个人的购物清单如果是"牛奶、面包、鸡蛋、电池、拖鞋",看着很正常;如果连续三十次都是"牛奶、面包、鸡蛋",一模一样,那多半不是人在买。

26.4规则引擎与评分卡

规则引擎负责"命中即记录",评分卡负责"把命中换算成分数"。两者配合,才能既抓得准又分得出轻重。

举个简化例子,每条规则给不同权重:

命中项权重为什么这么给
同设备多账号30强关联,但存在家庭共用
操作间隔过于均匀20机器特征明显,可被模仿
IP 与常用地不符10出差、VPN 都会触发
新设备首次支付15换手机是正常事

分数累加后落在不同区间,走不同处置。这里的关键是权重不是拍脑袋,而是根据历史案件反推:哪条规则在真实案件里出现得多,权重就高。

补充 评分卡最大的敌人是"规则堆叠"。三年积累下来几百条规则,谁也不敢删,最后一算,几乎所有用户都能凑够分数。规则需要定期清理和回归测试,和代码一样。

26.5行为序列:节奏比内容更难伪装

内容可以编,节奏很难编。人在操作时会有大量无意识的小停顿,而脚本的节奏是程序写死的。

正常用户与脚本在操作节奏上的差异 正常用户 8 秒 14 秒 5 秒 脚本 间隔几乎一模一样,连续几千次 人可以伪装内容,很难伪装时间 但慢速脚本也能学人,所以还要看跨天、跨设备的一致性
图 26-2 同一段时间轴:上面的点忽疏忽密,下面的点像尺子量过

注意最后一行图注。只看"间隔是否均匀",慢速脚本很容易骗过去:把每次操作间隔随机化到 3 到 30 秒,看起来就很像人。所以节奏只是特征之一,必须和别的维度一起看。

26.6图关系:从单点看整体

单个账号可能伪装得很好,但账号之间会共享东西:同一台设备、同一个出口 IP、同一张支付卡、同一个收货地址。把这些关系画成一张图,异常就会浮出来。

通过共享设备把两个陌生账号连成同一个团伙 账号 A 注册 3 天 账号 B 注册 5 天 同一台设备 设备指纹相同 同一个出口 IP 机房或代理池 同一张支付卡 绑定同一个持卡人 共享的中间节点,把两个看起来无关的账号连成了同一个团伙
图 26-3 单点可以伪装,共享的关系很难一起伪装

图分析的价值在于发现"应该没关系的东西之间有关系"。两个账号注册时间不同、昵称不同、下单品类不同,看起来毫无关联,但它们在同一台设备上登录过,共用一张支付卡——这就不是巧合了。

26.7误报与漏报:一道没有完美解的题

任何检测都会犯错,错误只有两种:

  • 误报(False Positive):正常人被拦了。用户会投诉,客服成本上升,严重时直接流失;
  • 漏报(False Negative):坏人放过去了。平台承担资金损失和合规风险。

两者此消彼长。你把阈值调严,误报上升、漏报下降;调松则相反。这条曲线叫 ROC 曲线,不用记公式,只要记住它的形状:想两头都占,只能靠更好的特征,不能靠调阈值。

所以真正的问题不是"怎么把阈值调到最优",而是"哪一边的代价更高"。支付场景漏报一次可能损失几千元,误报一次只是多收一条短信验证,那就该偏严;内容推荐场景误报会让用户直接关掉 App,那就该偏松。

注意 误报率看起来是个小数字,但基数一大就很吓人。日活一千万、误报率千分之一,每天就有一万人被误伤。所以处置要分级,不要一上来就封。

26.8对抗的本质:单点伪装没有意义

很多人以为"换个 IP 就查不到了"。这只改了一个维度,而检测看的是所有维度是否自洽。

举个具体的例子。你把 IP 换成了另一个城市,但:

  • 设备指纹还是原来那台;
  • 系统时区没跟着变;
  • 操作节奏还是每秒一次;
  • 常用收货地址还在原地;
  • 支付卡还是同一张。

这五个维度里,你只改了一个,剩下四个都在说"我还是我"。结果不是"藏起来了",而是新增了一条不一致:IP 变了,其他都没变。这本身就是可疑信号。

反过来说,防守方的思路也清楚了:不要依赖任何单一维度。IP 可以换、设备可以刷、账号可以注册、行为可以模仿,但要让所有维度同时自洽,成本会指数上升。这就是为什么"多维一致"比"单点严格"更有效。

26.9处置梯度:从观察到封禁

风控的目标不是"抓住",而是"用最小代价控制风险"。所以处置是分级的,一级一级往上走。

等级动作用户感受
一级只记录,不打扰无感
二级加一次验证(短信、滑块)稍麻烦
三级限速、限制额度变慢
四级拒绝本次操作失败一次
五级冻结账号、封禁设备无法使用

这套梯度的意义在于给误报留退路。一个正常用户被加了短信验证,最多骂一句;被直接封号,就会去投诉、去社交平台发帖。而真正的攻击者,在二级就会暴露——他过不了验证,或者过验证的成本比收益高。

还有一层:处置要可申诉、可回滚。任何自动化决策都可能出错,出错后必须有人能查、能改、能恢复。

26.10隐私与合规:能不用就不用

检测能力越强,越容易越界。三条底线:

  • 最小必要:只采集完成判断所必需的数据。能用一个布尔值判断,就不要存整段原始记录。
  • 告知同意:用户要知道采了什么、用来干什么。藏在十几页协议里的"可能收集设备信息",不算有效告知。
  • 能哈希不存明文:设备标识、手机号这类信息,做不可逆处理后再用于关联分析,效果接近,风险低得多。

还要加一条常被忽略的:留存期限。数据不是存得越久越好。超过必要期限的数据,只会变成负债——一旦泄露,赔的是全部历史。

26.11两个最常见的误区

误区一:风控就是封号。封号是最后一招,成本最高、不可逆、最容易误伤。成熟的风控大部分时间在做"观察"和"加验",只有极少数情况才动用封禁。

误区二:改 IP 就能过。IP 只是几十个维度里的一个。设备、时区、节奏、关系、支付方式只要有一个对不上,改 IP 反而制造了新的矛盾点。

自测:为什么说"规则层"不能被模型完全替代?
因为规则可解释、可申诉、可回滚。模型只能给出一个分数,出问题时很难说清原因。实际系统里两层是叠加的:规则兜住明显红线,模型处理复杂组合。
自测:五种日志来源里,哪一种最可信?为什么?
数据库日志最可信,因为它记录的是最终落地的状态,最靠近事实。客户端埋点最不可信,因为请求在用户设备上生成,可以被随意伪造。越靠近客户端,越容易被篡改。
自测:把 IP 换成别的城市,为什么反而更可疑?
因为其他维度(设备指纹、时区、节奏、收货地址、支付卡)都没变。IP 变了、其他没变,本身就是一条新的不一致。检测看的是多维是否自洽,不是单点是否干净。
自测:误报率和漏报率能不能同时降到很低?
只靠调阈值不行,两者此消彼长。要同时改善,只能引入更强的特征或更多维度的信息。这也是为什么风控团队大量精力花在特征工程,而不是调参数。
自测:为什么处置要分级,而不是一次到底?
因为误报不可避免。分级给误报留了退路,用户最多觉得麻烦,不会直接流失;同时对真正的攻击者,二级验证往往就足以让成本超过收益。分级也是可申诉、可回滚的前提。
动手练
  • 记录自己一天里打开某个 App 的时间点,看看间隔有多不规律,体会"人的节奏"是什么样。
  • 列出你能想到的、判断"这是不是同一个人"的五个维度,并标注哪些容易被改变。
  • 找一个你被要求过短信验证的场景,想一想:如果直接封号,你会是什么反应。
规则引擎Rule Engine把"命中什么条件就做什么"写成可配置的规则集合。
评分卡Scorecard给每条命中项加权,累加成分数后分级处置。
特征Feature把行为翻译成可计算的数字,如频次、间隔、熵。
熵Entropy衡量行为的杂乱程度,越低越像机器。
设备指纹Device Fingerprint由机型、系统、传感器等特征合成的设备标识。
误报False Positive把正常人判成异常,代价是用户体验和客服成本。
漏报False Negative把异常放过去,代价是资金损失与合规风险。
最小必要Data Minimization只采集完成判断所必需的数据,不多存一份。
你现在应该能回答 三层检测各解决什么?五种日志来源哪个最可信?节奏为什么比内容难伪装?图关系能发现什么单点看不出的东西?误报和漏报为什么不能同时压到最低?为什么改 IP 反而更可疑?这六个问题能讲清楚,你就真正理解风控在做什么了。
第 28 章 · 附录

术语词典:一页查完所有名词

读协议分析的文章,最卡人的往往不是逻辑,而是名词:一会儿 sign,一会儿 token,一会儿 pinning。这一页把全站出现过的词按十个分组排好,每条一句话,并标明它在第几章讲过。看不懂某段正文时,回到这里查一个词,再回去接着读。

330 条术语10 个分组中英对照 标注章节支持搜索
先记住这一句 术语不是一份要背下来的清单,而是随时能查的工具:先记住它属于哪一组,再记住它解决什么问题,具体定义用到时再回来查。
人话版 这页就是一本随身小字典。看到不懂的词,在上面搜索框里打两个字,或者按分组往下翻,一句话就能知道它大概管什么、该去第几章细看。

28.1怎么用这一页

查词只需要三步,顺序固定:先判断它属于哪个大类,再读那一句话解释,最后按后面的章号回去看完整例子。

查术语的三步:定位分组、读一句话、回到章节 第一步 定位分组 十个大类 先想它属于哪一层 第二步 读一句话 每个词只有一条 看它解决什么问题 第三步 回章节 看标注的章号 那里有完整例子 查词只解决看不懂,真正会用还要回到对应章节走一遍完整流程 同一个词在不同公司叫法可能不同,看英文名往往比看中文名更准
图 28-1 三步查词法:先定位,再读一句,最后回章节补细节

很多人以为把术语背完就等于会分析协议,其实术语只是地图上的地名。知道"哈希"两个字怎么写,和能看出一段校验逻辑用的是哈希还是签名,是两件事。

补充 每条后面的章号指向本站的正文页,不是外部资料。第 1 章是零基础预备,第 2 到 11 章讲底层与协议,第 12 到 16 章讲移动端,第 17 到 21 章是实战,第 22 到 27 章讲 Web、云与合规。

28.2十个分组是什么关系

十个分组不是十门互不相干的课,而是同一句话在链路上经过的十个切面:先有地址和连接,再有加密和签名,然后才谈得上内容和平台。

术语十个分组的关系图 十组词描述的是同一件事:一句话怎么从手机走到服务器,又怎么被拆开 网络与传输 第 2、6 章 地址与连接 密码学 第 4、5 章 加密与签名 TLS 与证书 第 8、15 章 把通道锁上 HTTP 与 Web 第 7、22 章 说什么内容 编码与序列化 第 3、9 章 字节怎么排 移动端与逆向 第 12 到 14 章 端上怎么算 抓包与调试 第 10、20 章 路上怎么录 风控与指纹 第 16、18 章 平台怎么认人 云与 API 安全 第 23 到 25 章 服务端怎么守 工程与合规 第 19、27 章 怎么做得久 十组不是十门课 是同一件事的十个切面 先按分组定位,再回到对应章节看细节;实在找不到就用页面顶部的搜索框 从上往下读,正好是从底层到业务的一条线,顺序本身也是知识 第 17、21、26 章横跨多组,属于实战与检测,遇到时再单独回看
图 28-2 十个分组的关系:底层负责送到,密码学负责锁上,业务层负责说什么

28.3六组最容易混的词

下面这几对词,在初学者的问题里出现频率最高。先把它们分清楚,能省掉后面一大半的困惑。

容易混的两组一句话区别看哪章
加密 / 编码加密要钥匙才解得开,编码谁拿到都能还原第 4、9 章
哈希 / 加密哈希不可逆,加密可以解密回原文第 4 章
签名 / 加密签名防篡改和冒充,加密防偷看第 5 章
认证 / 授权认证是你是谁,授权是你能干什么第 23 章
去标识化 / 匿名化前者理论上还能还原,后者不能第 27 章
限流 / 频控限流管每秒速度,频控管一段时间总量第 18 章
静态 / 动态分析前者不运行只看文件,后者跑起来看行为第 11 章
注意 同一个中文词在不同公司可能指不同东西,例如"签名"有的团队指哈希校验,有的指非对称签名。看英文名和上下文,比只看中文名可靠。

28.4术语正文(十组 330 条)

按分组往下翻,或者直接用上面的搜索框。每条后面标注的章号,就是它被完整讲过的地方。

一、网络与传输NETWORK

协议Protocol
双方事先约定好的说话规则,就像寄快递必须填同一张单子,格式不对对方就看不懂。第 2 章
分层Layering
把一次通信拆成好几层,每层只管一件事,像公司里前台、财务、仓库各管一段流程。第 2 章
OSI 七层模型OSI Model
教科书里的七层划分,从网线一直排到应用,用来给问题先定位一个大概位置。第 2 章
TCP/IP 四层模型TCP/IP Model
互联网实际在用的四层划分:网络接口、网际、传输、应用,比七层更好记。第 2 章
应用层Application Layer
最上面一层,直接面对业务,HTTP、DNS、SMTP 都住在这里。第 2 章
传输层Transport Layer
负责两端之间的投递,TCP 保证送到,UDP 只管发出去,不管到没到。第 2 章
网络层Network Layer
负责跨网络找路,IP 地址和路由选择都在这一层,像导航决定走哪条高速。第 2 章
链路层Link Layer
管同一根网线或同一个 Wi-Fi 内的相邻设备,怎么把一帧数据递过去。第 2 章
封装Encapsulation
数据每往下一层就套一层信封,到最底下变成能在网线上跑的 0 和 1。第 2 章
报文Message
网络上传输的一份完整数据,通常由头部说明和正文内容两部分组成。第 7 章
数据包Packet
网络层的一个小包裹,带着源地址和目标地址,各自独立找路,到站再拼回去。第 6 章
数据帧Frame
链路层的一节车厢,有车头标识也有车尾校验,出了差错能被发现。第 6 章
IP 地址IP Address
设备在网络里的门牌号,IPv4 写成四段数字,例如 203.0.113.10。第 2 章
IPv4IPv4
32 位地址,写成四段 0 到 255 的数字,全世界一共约 43 亿个。第 2 章
IPv6IPv6
128 位地址,写成八组十六进制,数量多到几乎不用担心用完。第 2 章
子网掩码Subnet Mask
配合 IP 一起用,判断两台机器是不是同一栋楼里的邻居,决定要不要绕出去。第 2 章
私有地址Private Address
只在局域网内部有效的地址,例如 192.168 开头,出门要靠 NAT 转换。第 2 章
NAT 网络地址转换NAT
把家里一堆内网地址映射成一个公网地址,像公司前台统一收发快递。第 2 章
端口Port
一台机器上的房间号,范围 0 到 65535,443 通常留给加密网页。第 2 章
域名Domain Name
给人看的好记名字,例如 shop.example.com,机器本身只认 IP。第 2 章
DNS 域名系统DNS
互联网的通讯录,把域名翻译成 IP,查询默认走 53 端口。第 2 章
递归查询Recursive Query
你只问一次,DNS 服务器替你跑完全程,最后直接给答案,中间过程你看不到。第 2 章
TTL 生存时间TTL
一条 DNS 记录能被缓存多少秒,过期就得重新去问一遍,改域名后要等它失效。第 2 章
TCP 传输控制协议TCP
可靠、有序、有确认的传输,像挂号信,丢了会重寄,慢了会等,代价是慢一点。第 6 章
UDP 用户数据报协议UDP
不保证送达也不保证顺序,像明信片,快但可能丢,视频通话爱用。第 6 章
三次握手Three-way Handshake
建立 TCP 连接的三句话:我能连吗、可以、好的,少一句都连不上。第 6 章
四次挥手Four-way Teardown
关闭 TCP 连接要四个动作,双方各说一次再见,再各自确认。第 6 章
拥塞控制Congestion Control
发现网络堵了就主动减速,避免所有人一起把路彻底堵死,思路和地铁限流一样。第 6 章
路由Routing
数据包在多个网络之间选路的过程,像导航软件替你规划一条路线。第 6 章
CDN 内容分发网络CDN
把内容复制到离你最近的机房,少走几千公里,打开就快,也扛得住高峰。第 22 章
反向代理Reverse Proxy
站在服务器前面的门卫,统一接收外部请求,再分给后面真正干活的机器。第 22 章

二、密码学CRYPTO

明文Plaintext
没有加密、谁都能看懂的内容,像写在白纸上、连信封都没装的字。第 4 章
密文Ciphertext
加密之后得到的内容,看上去就是一串随机字符,看不出原意,也改不动。第 4 章
加密Encryption
用密钥把明文变成密文,目的是让路上的人即使拿到,也看不懂里面写了什么。第 4 章
解密Decryption
拿到密文的一方用对应密钥,把它还原成能读懂的明文,这一步通常很快。第 4 章
密钥Key
加密解密用的那把钥匙,长度常见 128 位或 256 位,越长越难猜。第 4 章
对称加密Symmetric Encryption
加密和解密用同一把钥匙,速度快,适合加密大量数据,难点是怎么安全地分发钥匙。第 4 章
非对称加密Asymmetric Encryption
一把公钥一把私钥配对使用,公钥可以随便发,私钥必须自己留着。第 4 章
公钥Public Key
可以公开给任何人的那把钥匙,用来加密内容,或者验证别人的签名。第 4 章
私钥Private Key
只有自己知道的那把钥匙,一旦泄露等于把家门钥匙复制给了陌生人。第 4 章
密钥对Key Pair
一把公钥加一把私钥,成对生成,数学上互相绑定,不能拆开单用。第 4 章
AES 高级加密标准AES
目前最常用的对称加密算法,密钥常见 128 位或 256 位,速度很快。第 4 章
RSARSA
经典的非对称算法,密钥常见 2048 或 4096 位,安全但比 AES 慢很多。第 4 章
椭圆曲线ECC
用椭圆曲线数学做非对称加密,密钥更短却能提供相当的安全性,手机上更省算力。第 4 章
分组密码Block Cipher
把数据切成固定大小的块,一块一块加密,AES 就属于这一类。第 4 章
流密码Stream Cipher
一边生成密钥流一边逐字节异或,适合实时语音这类连续数据,不用等攒满一整块。第 4 章
工作模式Mode of Operation
规定分组密码怎么串起来用,常见的有 CBC、CTR、GCM。第 4 章
初始化向量IV
每次加密都换一个随机起点,防止相同的明文加出相同的密文,省掉它就会泄露规律。第 4 章
填充Padding
数据长度不够一整块时补几个字节,补法不对会泄露原文长度,历史上有专门的填充攻击。第 4 章
哈希Hash
把任意长度的内容压成固定长度的指纹,只能单向算,不能倒推,改一个字结果全变。第 4 章
摘要Digest
哈希算出来的那串固定长度结果,常见 32 字节,用来比对内容。第 4 章
MD5MD5
早期哈希算法,输出 128 位,早已不安全,现在只适合做文件校验。第 4 章
SHA-1SHA-1
输出 160 位的哈希算法,已被证明能人为构造碰撞,不建议再用。第 4 章
SHA-256SHA-256
输出 256 位的哈希算法,目前广泛用于签名、证书和完整性校验。第 4 章
碰撞Collision
两份不同内容算出同一个哈希值,好的算法让这件事几乎做不到,做到了就能伪造。第 4 章
加盐Salt
哈希之前先拼一段随机字符串,让相同的密码算出完全不同的结果。第 4 章
慢哈希Slow Hash
故意设计得很慢的哈希,例如 bcrypt,用来拖慢暴力猜密码。第 4 章
HMAC 消息认证码HMAC
用密钥和哈希一起算出的校验值,既能防篡改,也能验身份,比裸哈希多一把钥匙。第 5 章
消息认证码MAC
用双方共享的密钥算出的校验值,证明内容在路上没被改过,但不证明是谁发的。第 5 章
数字签名Digital Signature
用私钥对摘要签名,任何人拿公钥都能验证它确实出自你手,且内容没被动过。第 5 章
验签Signature Verification
用公钥检查签名和内容是否匹配,匹配才认为这份内容可信,不匹配就该直接拒绝。第 5 章
随机数Random Number
密码学里必须用安全随机源,普通随机数能被预测,等于把钥匙提前告诉了别人。第 4 章
随机数种子Seed
伪随机算法的起点,种子一旦被猜到,后面整串数字都能算出来,令牌也就废了。第 4 章
密钥派生KDF
从一个密码派生出更长的密钥材料,常见做法是 PBKDF2、HKDF。第 4 章
密钥交换Key Exchange
双方在公开通道上协商出同一把密钥,例如 ECDHE,全程不直接传密钥。第 8 章
前向保密Forward Secrecy
长期私钥以后泄露,也解不开以前录下的会话,因为每次密钥都不同。第 8 章

三、TLS 与证书TLS

TLS 传输层安全TLS
给 TCP 套一层加密外壳,让普通 HTTP 变成 HTTPS,防止路上被看。第 8 章
SSLSSL
TLS 的前身,现在口头说 SSL 其实多半指 TLS,老版本早已被淘汰。第 8 章
HTTPSHTTPS
HTTP 加上 TLS,默认走 443 端口,浏览器地址栏会显示一把小锁。第 8 章
握手Handshake
双方先协商版本和算法、交换密钥材料,谈妥了才开始传业务数据。第 8 章
ClientHelloClientHello
握手第一句,客户端报出支持的版本、算法,并附上一个随机数,供后面派生密钥。第 8 章
ServerHelloServerHello
服务器从客户端给的清单里挑一套,回一个自己的随机数,握手继续。第 8 章
会话密钥Session Key
握手协商出来的临时钥匙,只用于这一次连接,断了就作废,抓到旧包也解不开。第 8 章
预主密钥Pre-master Secret
握手过程中算出的中间材料,再派生出真正的会话密钥,本身不会被直接拿来加密。第 8 章
证书Certificate
一张电子身份证,把域名和公钥绑在一起,由受信任的机构签发,过期就得换。第 8 章
X.509X.509
证书的标准格式,规定字段怎么排、签名怎么放、有效期写在哪,各家实现都要照它来。第 8 章
CA 证书颁发机构CA
大家都信得过的发证机关,例如 Let's Encrypt,它签字别人才认。第 8 章
根证书Root Certificate
预装在系统里的信任起点,整条信任链都从这里往上追,它被换掉等于信任崩塌。第 8 章
中间证书Intermediate Certificate
根证书不直接签网站,而是先签中间证书,再往下一层层签,这样根私钥能离线保存。第 8 章
证书链Certificate Chain
从网站证书一路往上追到根证书的那条链,缺一环就验不过,服务器要发全整条链。第 8 章
证书固定Certificate Pinning
App 只认自己内置的那张证书,换成别的证书就直接拒绝连接。第 15 章
公钥固定Public Key Pinning
不固定整张证书,只固定公钥,换证书时更灵活,也更难维护,轮换时容易翻车。第 15 章
SNI 服务器名称指示SNI
握手时明文告诉服务器要访问哪个域名,一个 IP 才能托管多个网站。第 8 章
ALPN 应用层协议协商ALPN
握手时顺便商量好这次用 HTTP/1.1 还是 HTTP/2,省一次往返。第 8 章
会话恢复Session Resumption
再次连接时复用上次的密钥材料,省掉一次完整握手,手机更省电。第 8 章
会话票据Session Ticket
服务器加密后交给客户端保存的恢复凭据,下次带回来就能续上,省一次完整握手。第 8 章
TLS 1.2TLS 1.2
较老的版本,完整握手要两次往返,目前仍在大量服务器上使用,兼容性最好。第 8 章
TLS 1.3TLS 1.3
较新的版本,握手压到一次往返,砍掉了所有已知不安全的算法,配置也更简单。第 8 章
中间人攻击MITM
攻击者站在你和服务器中间,两边都骗,转发的同时把内容看一遍。第 15 章
证书校验Certificate Validation
客户端检查域名、有效期、签名链是否都合格,任何一项不过就报错。第 8 章
自签证书Self-signed Certificate
自己给自己签的证书,没有第三方背书,系统默认不信任,抓包时才临时装一张。第 10 章
证书透明度CT
要求公开证书都登记到公开日志,方便事后发现有人冒签,域名主也能收到告警。第 8 章
OCSP 在线证书状态协议OCSP
实时去问 CA 这张证书有没有被吊销,比下载整份清单更快,但会多一次请求。第 8 章
CRL 证书吊销列表CRL
CA 定期发布的一份已作废证书清单,客户端下载后自己比对,问题是清单会越来越大。第 8 章
密钥日志Key Log
浏览器把本次会话密钥写进文件,抓包工具靠它解密 HTTPS。第 10 章
双向认证mTLS
不只服务器出示证书,客户端也要出示,双方互相验证对方身份,常用于内部系统。第 8 章
降级攻击Downgrade Attack
想办法让双方退回用弱算法或老版本,从而打开突破口,所以老版本要彻底关掉。第 8 章
密码套件Cipher Suite
一组算法组合,规定密钥交换、对称加密和校验各用哪一种,名字通常是一长串。第 8 章

四、HTTP 与 WebHTTP

HTTP 超文本传输协议HTTP
浏览器和服务器说话的格式,一问一答,默认走 80 端口,加密后改走 443。第 7 章
请求Request
客户端发出的一问,包含方法、路径、请求头,通常还带一个请求体。第 7 章
响应Response
服务器回的一答,包含状态码、响应头,以及真正要给你的正文,三者缺一不可。第 7 章
请求行Request Line
请求的第一行,写明用什么方法、访问哪个路径、用哪个协议版本。第 7 章
状态行Status Line
响应的第一行,写明协议版本、三位状态码,以及一句简短的原因。第 7 章
请求头Header
附在请求上的说明,例如内容格式、客户端标识、携带的凭证,业务参数一般不放这里。第 7 章
请求体Body
真正的业务内容,常见是 JSON,也可能是一段表单数据或上传的文件。第 7 章
GETGET
取数据的方法,参数拼在网址里,按规范它不应该改变服务器上的状态。第 7 章
POSTPOST
提交数据的方法,内容放在请求体里,登录、下单、上传都常用它。第 7 章
PUTPUT
整体替换一个资源,同一个请求发两遍,结果和只发一遍是一样的。第 7 章
DELETEDELETE
删除指定资源的方法,语义很清楚,所以权限必须比查询接口管得更严。第 7 章
HEADHEAD
只要响应头、不要正文,常用来快速探测某个地址上的资源还在不在。第 7 章
状态码Status Code
三位数字表示结果,200 成功,404 找不到,500 是服务器自己出错。第 7 章
301 永久重定向301
资源永久搬家了,浏览器以后会直接去新地址,不再问旧的,搜索引擎也会跟着换。第 7 章
302 临时重定向302
这次先去别处取,下次还可能回到原地址,语义上只是临时跳一下。第 7 章
401 未认证401
你还没登录,或者手里的凭证已经失效,服务器要你先证明自己是谁。第 7 章
403 禁止访问403
身份已经认出来了,但你并没有被授予查看这个资源的权限,别再试了。第 7 章
429 请求过多429
你发得太快被限流了,服务器让你等一会儿再试,通常还会给个等待秒数。第 7 章
CookieHTTP Cookie
服务器让浏览器存下的一小段文字,下次请求会自动带回去,所以它能当登录态用。第 7 章
Set-CookieSet-Cookie
服务器下发的指令,告诉浏览器存什么、存多久、给哪个域名,还能标记只走加密连接。第 7 章
会话Session
登录状态存在服务器那边,浏览器手里只拿着一个短短的会话编号。第 7 章
User-AgentHTTP User-Agent
客户端自报家门的字符串,写明系统版本、浏览器或 App 版本。第 16 章
RefererReferer
告诉服务器你是从哪个页面跳过来的,常用于访问统计和防盗链,也可能泄露内部地址。第 7 章
Content-TypeContent-Type
说明正文是什么格式,例如 application/json 或 multipart/form-data。第 9 章
Content-LengthContent-Length
正文一共有多少字节,服务器靠这个数字判断读到哪儿算读完,短了就会一直等。第 7 章
分块传输Chunked
不知道总长度时一块一块地发,最后发一个空块表示到此结束,直播和流式接口常用。第 7 章
Keep-AliveKeep-Alive
在一条连接上连续发多个请求,省掉反复握手建立连接的时间,手机端省电也明显。第 7 章
HTTP/2HTTP/2
二进制分帧、多路复用,一个连接可以同时跑很多个请求,头部还能压缩,比 1.1 快不少。第 7 章
HTTP/3HTTP/3
跑在 UDP 上的新版本,用 QUIC 自己实现可靠传输,弱网下更稳。第 7 章
WebSocketWebSocket
一次握手后变成双向长连接,服务器可以主动推消息给你,聊天和行情都靠它。第 24 章
CORS 跨域资源共享CORS
浏览器规定网页只能读同源响应,跨域必须服务器明确点头,否则前端拿不到数据。第 22 章
同源策略Same-origin Policy
协议、域名、端口三者完全相同才算同源,否则互相隔离,这是浏览器最基础的一道墙。第 22 章
XSS 跨站脚本XSS
把恶意脚本塞进网页,让别人的浏览器替他执行,偷走登录态,防御靠转义和 CSP。第 22 章
CSRF 跨站请求伪造CSRF
借你已经登录的身份,偷偷替你发一个你并不知道的请求,防御靠令牌和 SameSite。第 22 章
SQL 注入SQL Injection
把数据库命令混进输入框,骗后台照着他的话去查数据,防御靠参数化查询。第 22 章

五、编码与序列化ENCODING

字节Byte
计算机的基本单位,1 字节等于 8 位,能表示 0 到 255 共 256 种值。第 3 章
位Bit
最小的数据单位,只有 0 和 1 两种状态,像一盏只能开关的灯。第 3 章
十六进制Hex
用 0 到 9 和 A 到 F 表示字节,两个字符正好对应一个字节。第 3 章
大端序Big-endian
高位字节放在前面,像把年份写在月份前面,人读起来比较顺,网络协议常用这种排法。第 3 章
小端序Little-endian
低位字节放在前面,x86 和 ARM 常见这种排法,看内存时要留意。第 3 章
字符集Character Set
一张对照表,规定哪个数字代表哪个字符,没有表就全是乱码,表和编码方式要配套。第 3 章
ASCIIASCII
最早的单字节字符集,只覆盖英文、数字和常见符号,中文放不下。第 3 章
UnicodeUnicode
给全世界所有字符统一编号的字符集,中文、日文都有自己的码位。第 3 章
UTF-8UTF-8
变长编码,英文占 1 字节,中文通常占 3 字节,是目前最通用的写法。第 3 章
GBKGBK
中文双字节编码,老系统里很常见,和 UTF-8 互转时最容易出乱码。第 3 章
乱码Mojibake
用错的字符集去解读字节,结果变成一堆看不懂的方块和问号,换对编码就能恢复。第 3 章
Base64Base64
把二进制换成 64 个可打印字符,方便放进文本,体积会增大约三分之一。第 9 章
URL 编码URL Encoding
把空格、中文等特殊字符转成百分号加十六进制,网址才合法,参数里最常见。第 9 章
百分号编码Percent-encoding
URL 编码的正式叫法,例如空格会变成 %20 这三个字符。第 9 章
JSONJSON
用花括号和方括号写数据的格式,人容易读,机器解析起来也快,接口里最通用的选择。第 9 章
键值对Key-Value
一个名字配一个值,JSON 里最常见的结构,像表格里的一行记录。第 9 章
数组Array
用方括号包起来的一串有序的值,位置从 0 开始数,不是从 1。第 9 章
嵌套Nesting
对象里再放对象、数组里再放数组,像抽屉里面还套着更小的抽屉。第 9 章
XMLXML
用尖括号标签描述数据的格式,老系统和安卓界面布局里很常见,比 JSON 啰嗦但更严格。第 9 章
表单编码Form Encoding
网页表单提交时的键值拼接格式,用与号把多组参数连起来,空格会变成加号或 %20。第 9 章
序列化Serialization
把内存里的对象变成能传输、能存盘的字节流,比如变成 JSON。第 9 章
反序列化Deserialization
把收到的字节流还原成程序里的对象,处理不当会出安全问题,所以别信来路不明的数据。第 9 章
Protobuf 协议缓冲Protobuf
谷歌的二进制序列化格式,体积小、解析快,但人眼基本读不懂,要靠定义文件。第 9 章
TLV 格式TLV
用类型、长度、值三段描述一个字段,扩展方便,很多私有协议在用。第 9 章
定长字段Fixed-length Field
不管内容多长都占固定字节,解析简单,但会浪费不少空间,适合长度稳定的字段。第 9 章
变长字段Variable-length Field
长度不固定,前面通常要带一个长度值,读的时候先看长度,再决定往后读多少字节。第 9 章
魔数Magic Number
文件开头几个固定字节,用来标识格式,例如 PNG 开头是 89 50 4E 47。第 3 章
字节序Endianness
多字节数据在内存里谁先谁后的约定,两端不一致就会读出怪值,跨平台对接必须问清。第 3 章
校验和Checksum
把数据按规则累加得到的小数字,用来快速发现传输中有没有出错。第 3 章
压缩Compression
用算法把数据变短,例如 gzip,发送前压、收到后解,省流量。第 9 章
编码Encoding
把数据换成另一种表示形式,它不等于加密,谁拿到都能照样还原。第 9 章
转义Escape
用特殊符号表示本来有特殊含义的字符,例如 JSON 里用反斜杠。第 9 章

六、移动端与逆向MOBILE

APKAndroid Package Kit
安卓安装包,本质是一个压缩包,里面装着代码、资源和清单文件。第 12 章
DEXDalvik Executable
安卓的字节码文件,Java 或 Kotlin 代码编译之后,最终就变成它。第 12 章
SmaliSmali
DEX 反汇编出来的汇编语言,能看也能改,但比 Java 难读很多。第 12 章
JADXJADX
把 DEX 反编译成接近 Java 源码的工具,多数人的逆向第一步都用它。第 12 章
APKToolAPKTool
拆包和回包工具,能改清单、改资源、改 smali,再重新打包签名。第 12 章
AndroidManifestAndroidManifest.xml
安卓应用的说明书,写明申请了哪些权限、有哪些组件、入口是谁。第 12 章
组件Component
安卓的四种基本积木:活动、服务、广播接收器、内容提供者,每种对外暴露的方式不同。第 12 章
活动Activity
你看到的一个界面,例如登录页、首页,一个应用有很多个,跳转靠意图来调度。第 12 章
服务Service
没有界面、在后台长期运行的组件,例如播放器和下载器,容易被滥用成常驻后台。第 12 章
广播接收器Broadcast Receiver
监听系统或应用广播的组件,例如电量变化、开机完成,导出后可能被外部触发。第 12 章
内容提供者Content Provider
应用之间共享数据的通道,配置不当就可能被别人读走,常见于聊天记录和相册。第 12 章
导出组件Exported Component
允许别的应用直接调用的组件,是安卓上最常见的入口之一,没做权限校验就危险。第 12 章
Native 库Native Library
用 C 或 C++ 编译出来的动态库,签名、加密算法常藏在这里。第 12 章
JNIJNI
Java 调用本地 C 代码的桥梁,逆向时经常在这座桥上设断点。第 12 章
SO 文件Shared Object
Linux 下的动态库,安卓里的 native 代码就是这种文件。第 12 章
汇编Assembly
CPU 能直接执行的指令的文本形式,例如 mov、call、jmp。第 12 章
反汇编Disassembly
把机器码还原成一条条汇编指令,看得到每一步在干什么,但读起来比源码费劲。第 12 章
反编译Decompile
把字节码或汇编尽量还原成高级语言,读起来更接近源码,但变量名和注释通常已经丢了。第 12 章
静态分析Static Analysis
不运行程序,只读代码、清单和文件结构,安全但可能看漏,尤其遇到运行时解密。第 11 章
动态分析Dynamic Analysis
把程序真正跑起来,观察它实际发了什么请求、改了什么文件,能补上静态看不到的部分。第 11 章
Hook 钩子Hook
在函数执行前后插一段自己的代码,偷看参数或直接改返回值,是动态分析的主力手段。第 12 章
FridaFrida
常用的动态插桩工具,用 JavaScript 写脚本,可以附着到运行中的 App。第 12 章
XposedXposed
安卓上的 hook 框架,需要先装框架,再写模块去改目标应用。第 12 章
脱壳Unpacking
把加壳应用在内存里解密后的真实代码 dump 出来,才能继续分析。第 14 章
加壳Packer
把真实代码加密压缩,运行时再解密执行,用来增加逆向难度,代价是启动变慢。第 14 章
混淆Obfuscation
把类名方法名改成 a、b、c,功能不变,但读起来像天书,配合控制流变换效果更好。第 14 章
控制流平坦化Control Flow Flattening
把正常流程打散成一个大 switch,读起来像走迷宫,需要先还原出真实执行顺序。第 14 章
反调试Anti-debugging
检测自己是不是被调试器附着,一旦发现就退出或走假逻辑,常见手法还有查端口和线程。第 14 章
完整性校验Integrity Check
启动时算一遍自己的哈希,发现被改过就拒绝继续运行,改包之后往往卡在这一步。第 14 章
IPAiOS App Store Package
iOS 安装包,本质也是压缩包,里面是可执行文件和一堆资源。第 13 章
Mach-OMach-O
iOS 和 macOS 的可执行文件格式,地位相当于 Windows 上的 PE。第 13 章
Objective-CObjective-C
iOS 早期的主力语言,方法调用在运行时才确定,很适合 hook。第 13 章
SwiftSwift
苹果现在主推的语言,符号会被 mangle 成一长串,读之前要先还原。第 13 章
越狱Jailbreak
解除 iOS 的系统限制拿到最高权限,才能方便地装插件和 hook。第 13 章
重签名Re-sign
用你自己的证书把 IPA 重新签一遍,才能装到测试设备上,签名一变校验也可能失败。第 13 章

七、抓包与调试CAPTURE

抓包Packet Capture
把网络上来回的数据原样录下来,事后再慢慢拆开逐条分析,是理解协议最快的入口。第 10 章
代理Proxy
手机把请求先发给它,由它转发给服务器,顺便把内容记下来,抓包工具本质就是代理。第 10 章
透明代理Transparent Proxy
不修改客户端设置,直接在网络层把流量劫持过来,再转发出去,用户往往察觉不到。第 10 章
系统证书System Certificate
装在系统分区的根证书,安卓 7 以后默认不信任用户自己装的证书。第 10 章
用户证书User Certificate
用户自己安装的证书,很多 App 根本不认,抓包会直接失败。第 10 章
证书信任设置Certificate Trust
iOS 装完描述文件还要手动打开完全信任,否则等于没装,这一步最容易被忘。第 10 章
断点Breakpoint
让程序在指定位置暂停,方便看当时每个变量到底是什么值,是调试最基础的动作。第 10 章
条件断点Conditional Breakpoint
只有满足条件才停下来,避免被高频调用的函数刷屏,例如只在对应用户名时暂停。第 10 章
调用栈Call Stack
当前函数是被谁一层层调过来的,像顺着上级一路往上追,能还原完整调用路径。第 10 章
单步Step
一次只执行一行或一条指令,慢慢看清整个流程到底是怎么走的,通常配合断点使用。第 10 章
寄存器Register
CPU 内部最快的小格子,存放当前参与运算的值和地址,容量很小。第 10 章
内存断点Memory Breakpoint
某块内存被读或被写时停下,适合找出到底是谁改了数据,比逐个函数下断点省事。第 10 章
日志Log
程序自己打出来的运行记录,排错时第一件事通常就是先看它,比凭空猜要快得多。第 20 章
堆栈跟踪Stack Trace
报错时打印的一串函数位置,顺着它能定位到出错的那一行,是崩溃分析的第一手材料。第 20 章
崩溃Crash
程序遇到无法处理的错误直接退出,通常会留下日志和堆栈,先看最后一行往往就有线索。第 20 章
空指针Null Pointer
程序去用一个根本不存在的对象,是最常见的崩溃原因之一,多出现在没判空的分支。第 20 章
超时Timeout
等太久就主动放弃,抓包里表现为请求发出去却一直没响应,先分清是网络慢还是服务端慢。第 20 章
重试Retry
失败之后再试一次,但要注意别让重试变成重复下单或重复扣款,接口必须支持幂等。第 19 章
重放Replay
把抓到的请求原样再发一次,看服务器会不会照样认账、照样执行。第 18 章
对拍Diff
拿正常请求和异常请求逐字段比对,找出真正起作用的那个差异,比闷头读代码快很多。第 11 章
基线Baseline
先记录一份完全正常的状态,后面出现的所有异常都拿它来对比,没有基线就无法判断异常。第 11 章
变量Variable
程序里存放数据的名字,调试时最常盯的就是它的当前值,尤其是和签名相关的中间量。第 10 章
参数Parameter
调用函数时传进去的值,hook 时重点看它,往往藏着关键输入。第 10 章
返回值Return Value
函数执行完交回来的结果,校验函数的返回值通常就是结论,改它比改整个流程省事。第 10 章
桩Stub
用一份假实现替换真函数,骗程序按你希望的方向继续走,常用于隔离依赖。第 11 章
模拟器Emulator
在电脑上模拟一台手机,调试方便,但特征明显,很容易被识别,风控场景基本绕不开。第 12 章
真机Real Device
真实手机,行为最接近线上环境,风控也更难被糊弄过去,代价是调试和批量都更麻烦。第 12 章
adbadb
安卓调试桥,电脑和手机之间的命令行通道,装应用、看日志都靠它。第 12 章
日志级别Log Level
从调试、信息、警告到错误,级别越高,排错时越应该优先去看,先看错误再看警告。第 20 章
环境隔离Sandbox
把实验限制在独立环境里,出问题也不会影响你的主系统和真实数据。第 19 章
时间戳Timestamp
标记事件发生的具体时刻,拼时间线时它是第一顺位的线索,还能用来判断请求是否过期。第 18 章
时钟偏移Clock Skew
两台设备的时间不一致,会导致签名或令牌校验莫名其妙地失败,先校时再排查逻辑。第 20 章

八、风控与指纹RISK

设备指纹Device Fingerprint
把设备的各项特征拼起来,形成一个相对稳定、能重复认出的编号。第 16 章
指纹熵Entropy
特征越多越杂,区分度越高,越不容易和别人撞成同一个值,但太细也会误伤正常用户。第 16 章
IMEIInternational Mobile Equipment Identity
老式手机的硬件编号,现在权限收紧,很多场景已经拿不到了,属于正在退场的标识。第 16 章
Android IDSSAID
安卓系统给应用的一个随机编号,重装或恢复出厂设置后会变,稳定性只能算一般。第 16 章
IDFAIdentifier for Advertisers
苹果的广告标识符,用户可以关闭跟踪,关闭后值会变成一串零,跨应用追踪因此受限。第 16 章
OAIDOpen Anonymous Device Identifier
国内移动安全联盟推出的匿名设备标识,用户可以在设置里主动重置它。第 16 章
硬件特征Hardware Trait
CPU 核数、内存大小、屏幕分辨率这类不容易随手改的属性,组合起来区分度不错。第 16 章
传感器指纹Sensor Fingerprint
每台设备的陀螺仪和加速度计都有细微偏差,可以当作特征,模拟器往往没有真实噪声。第 16 章
Canvas 指纹Canvas Fingerprint
让浏览器画同一张图,不同设备的渲染结果有细微差别,取像素哈希就能区分开。第 16 章
WebGL 指纹WebGL Fingerprint
通过显卡的渲染能力识别设备,思路和 Canvas 指纹类似。第 16 章
字体列表Font List
设备上装了哪些字体,能反映系统和地区,也常被拿来识别,但系统更新后容易变。第 16 章
时区Timezone
设备设置的时区,如果和 IP 归属地对不上,就会显得很可疑。第 16 章
语言Locale
系统和应用的语言设置,同样是风控判断环境是否真实的一项,要和时区、IP 对得上。第 16 章
模拟器检测Emulator Detection
通过硬件、传感器等特征,判断应用是不是跑在模拟器里,是风控的第一道筛子。第 16 章
Root 检测Root Detection
检查系统有没有被改过、有没有 su 文件,判断设备是否被接管。第 16 章
越狱检测Jailbreak Detection
iOS 上对应 Root 检测,查 Cydia 之类越狱痕迹是否存在。第 16 章
风控Risk Control
平台用一套规则加模型,判断这次操作到底像不像真人干的,结论往往只是一个分数。第 18 章
风控包Risk Package
请求里附带的一大坨设备与环境信息,交给服务端打分用,通常还会整体加密或签名。第 18 章
指纹上报Fingerprint Report
应用启动时收集设备特征并上传的行为,通常还会加密,启动阶段最容易观察到。第 18 章
行为轨迹Behavior Trace
点击、滑动、停留时长的序列,用来区分真人操作和脚本,机器生成的轨迹往往太规整。第 18 章
人机验证CAPTCHA
让你拼图或点选,用一件机器不擅长的事来证明操作者是真人,失败了还能重试。第 18 章
滑块验证Slider CAPTCHA
拖动滑块对齐缺口,同时悄悄采集你拖动时的速度和轨迹,光把缺口对齐并不算通过。第 18 章
限流Rate Limiting
单位时间内只允许固定次数的请求,超出的部分直接拒绝掉,保护的是服务端容量。第 18 章
频控Frequency Control
针对某个具体动作的频率限制,例如一天只能发几条消息,保护的是业务规则本身。第 18 章
黑名单Blacklist
明确禁止的名单,一旦命中就直接拦下,不再走后面的流程,特点是快但可能误伤。第 18 章
白名单Whitelist
明确放行的名单,通常用于内部测试或可信设备,绕过部分检查,也意味着更高风险。第 18 章
风险评分Risk Score
把多个信号加权成一个分数,超过阈值就拦截或加验证,比一刀切更平滑。第 18 章
设备关联Device Correlation
多个账号共用同一台设备,会被判定成很可能是一伙人,是团伙识别的常用信号。第 18 章
IP 画像IP Profile
这个 IP 是机房、代理还是家庭宽带,风险等级差别非常大,机房 IP 常被直接拦。第 18 章
代理检测Proxy Detection
识别请求有没有经过代理或 VPN,机房 IP 很容易被标出来。第 18 章
环境一致性Environment Consistency
时区、语言、IP 归属地、机型要能互相对得上,不能自相矛盾。第 18 章
设备农场Device Farm
大量真机集中在一起跑脚本,靠规模去模拟正常用户的行为,是风控重点盯的对象。第 26 章

九、云与 API 安全CLOUD / API

API 应用程序接口API
程序之间说话的约定,规定要传什么、会回什么、错了怎么报,和点菜菜单差不多。第 24 章
RESTRepresentational State Transfer
一种 API 风格,用网址表示资源,用 GET、POST 这些方法表示动作。第 24 章
端点Endpoint
API 的一个具体地址,例如 /api/v1/user/profile 这样的一条路径。第 24 章
GraphQLGraph Query Language
让客户端自己写要哪些字段的查询语言,一次就能拿够想要的数据。第 24 章
自省Introspection
GraphQL 自带的功能,可以直接问出整个数据结构长什么样。第 24 章
轮询Polling
客户端隔一会儿问一次有没有新数据,简单,但会浪费很多请求,数据更新慢时尤其明显。第 24 章
长轮询Long Polling
服务器先把请求挂住,有数据才回,减少大量空跑的问询,但连接被占用的时间变长。第 24 章
JWT 令牌JWT
三段式令牌:头、载荷、签名,载荷只是编码,不是加密,别往里塞敏感信息。第 23 章
JWT 载荷Payload
JWT 中间那一段,存用户编号、过期时间这类声明信息,谁拿到都能解开看。第 23 章
JWT 签名JWT Signature
第三段,服务器用它确认令牌在传输中没被改过,算法选错等于根本没签。第 23 章
alg 字段alg
JWT 头里声明用哪种签名算法,把它改成 none 是经典攻击手法。第 23 章
kid 字段kid
JWT 头里指明该用哪把钥匙,处理不当可能被拿来做路径穿越。第 23 章
Bearer 令牌Bearer Token
放在 Authorization 头里的一种写法,谁拿到谁就能用。第 23 章
OAuth 2.0Open Authorization 2.0
授权框架,让第三方在不拿你密码的前提下访问你授权的数据,核心是令牌而不是密码。第 23 章
授权码模式Authorization Code
OAuth 最常用的流程,先拿一个短期码,再换正式令牌,中间还要求校验客户端身份。第 23 章
回调地址redirect_uri
授权完成后跳回哪里,校验不严就可能被人用来偷授权码,所以必须逐字比对。第 23 章
权限范围Scope
申请令牌时声明需要哪些权限,要得越少,出问题时损失越小,审查时先看这一项。第 23 章
单点登录SSO
一次登录,多个系统都认,靠一个统一的身份中心来发凭证,退出也要能一起退。第 23 章
越权Broken Access Control
已经登录,但能看到不属于自己的数据,是最常见的接口问题,比注入更容易被忽略。第 24 章
不安全的直接对象引用IDOR
把网址里的编号改成别人的,就能读到别人的资料,本质是只信了客户端输入。第 24 章
对象级授权缺失BOLA
接口没检查这个对象是不是你的,常和 IDOR 一起出现,属于接口层最常见的漏洞。第 24 章
容器Container
把应用和依赖打包在一起运行,像一台很轻的独立小机器,启动快但隔离不如虚拟机。第 25 章
镜像Image
容器的模板文件,分层存储,启动时按它变出一个容器实例,改一层只多存一层。第 25 章
DockerDocker Engine
最常用的容器工具,用一份 Dockerfile 描述怎么构建镜像。第 25 章
KubernetesK8s
容器编排系统,负责调度、扩缩容、服务发现和故障自愈,管的是成百上千个容器。第 25 章
PodKubernetes Pod
Kubernetes 里最小的调度单位,一个或多个容器住在同一个 Pod 里。第 25 章
基于角色的访问控制RBAC
先给角色配权限,再把人绑到角色上,避免一个个单独授权,也方便审计。第 25 章
密钥对象Secret
Kubernetes 里存密码和令牌的对象,默认只做 Base64 编码。第 25 章
身份与访问管理IAM
云上的权限系统,规定谁能在哪个资源上做什么操作,策略写错就是最大的洞。第 25 章
最小权限Least Privilege
只给完成工作所必需的权限,多一点都不给,减少被利用面,也减少误操作。第 25 章
元数据服务Metadata Service
云主机内部的一个特殊地址,能查到实例身份和临时凭证,SSRF 最爱打这里。第 25 章
SSRF 服务端请求伪造SSRF
让服务器替你去访问内网地址,常被用来打元数据服务,防御靠出站白名单。第 25 章
WAF 网站应用防火墙WAF
挡在网站前面的规则引擎,拦截常见注入、扫描和恶意请求,但绕过手法一直有。第 26 章
零信任Zero Trust
不默认信任内网,每一次访问都要重新验证身份和权限是否足够,内网也不再是保险箱。第 25 章

十、工程与合规ENGINEERING

需求Requirement
先写清到底要解决什么问题,再决定用什么技术,顺序不能反,否则越做越偏。第 19 章
可行性验证PoC
先做一个小验证,证明思路能走通,再投入时间做工程化,失败了也亏得少。第 19 章
最小可行版本MVP
先做能跑通的最小版本,先拿到结果,别一上来就把功能堆满,容易半途而废。第 19 章
幂等Idempotency
同一个请求执行多次,结果和只执行一次一样,重试才安全,下单接口尤其要保证。第 19 章
并发Concurrency
同时处理多件事情,要特别小心共享数据,以及它们之间的执行顺序。第 19 章
队列Queue
把任务排成一条队,一个一个处理,用来削峰填谷、平滑压力,代价是结果会延迟。第 19 章
重试退避Backoff
失败之后等一会儿再试,而且一次比一次等得更久,避免打垮对方。第 19 章
熔断Circuit Breaker
下游一直失败就先断开,避免把调用方自己也一起拖垮,过一会儿再试探性放一点流量。第 19 章
灰度Canary
先放一小部分流量到新版本,观察没问题,再逐步放大到全量,出问题影响面也小。第 19 章
回滚Rollback
出问题能快速退回上一个版本,动手之前就该想好怎么退,否则只能硬扛着修。第 19 章
备份Backup
改动任何文件之前先留一份原件,出问题时既能对照,也能还原,是最便宜的安全网。第 19 章
版本控制Version Control
用 Git 这类工具记录每一次改动,方便对比、回退和协作,改坏了也有据可查。第 19 章
单元测试Unit Test
针对一个小函数验证它该有的行为,以后改代码时能替你兜底,成本低收益高。第 19 章
回归测试Regression Test
改完之后把老用例再跑一遍,确认没有把别的地方弄坏,改动越大越要跑。第 19 章
日志脱敏Log Redaction
打日志之前把手机号、令牌这类敏感字段遮掉或替换掉,日志泄露往往比数据库泄露更常见。第 27 章
最小必要Data Minimization
只收集业务真正需要的数据,多收一份就多一份泄露风险,也少一份合规负担。第 27 章
告知同意Informed Consent
收集个人信息前要讲清用途,并取得对方明确的同意,默认勾选不能算作同意。第 27 章
个人信息Personal Data
能单独或者结合其他信息,识别到某个具体自然人的数据,手机号和设备号都算。第 27 章
去标识化De-identification
删掉或替换能直接指向个人的字段,但理论上还可能还原,所以仍按个人信息管。第 27 章
匿名化Anonymization
处理之后无法再还原到具体个人,和去标识化不是一回事,标准更高也更难做到。第 27 章
数据留存Retention
数据存多久要有明确期限,到期就删,不能无限期地一直堆着,堆着就是风险。第 27 章
书面授权Written Authorization
动手测试前必须拿到书面许可,口头答应不能当作授权,范围和时间都要写清楚。第 27 章
测试范围Scope of Work
写清能测哪些域名、哪些接口、哪个时间段,超出范围就停,这是保护双方。第 27 章
渗透测试报告Pentest Report
记录问题、复现步骤、影响面和修复建议的正式文档,只报漏洞不给修法等于没做完。第 21 章
漏洞分级Severity
按利用难度和影响面打分,常见分成严重、高、中、低四档,方便排修复顺序。第 21 章
负责任披露Responsible Disclosure
发现问题先私下告诉厂商,给对方合理的修复时间再公开,直接公开会害到用户。第 27 章
靶场Lab
专门搭起来的练习环境,怎么打都不影响真实业务和真实用户,练习和验证都靠它。第 21 章
蜜罐Honeypot
故意暴露的假目标,用来观察攻击者到底怎么下手、用什么工具,也能提前预警。第 26 章
威胁建模Threat Modeling
动手之前先想清楚谁会怎么打你,再决定重点防哪里,比事后补洞便宜得多。第 26 章
攻击面Attack Surface
所有能被外部接触到的入口总和,入口越多,需要守的地方就越多。第 26 章
纵深防御Defense in Depth
不指望一道墙挡住所有人,而是多层各自设防,层层减速,让攻击成本越来越高。第 26 章
可观测性Observability
靠日志、指标和链路追踪,看清系统内部到底发生了什么,出事时才知道从哪查。第 26 章

28.5自测:五个最容易答错的

下面五道题都来自真实提问。能全部答对,说明你已经不会被名词骗过去了。

自测:加密和编码,最本质的区别是什么?
加密需要密钥,没有钥匙就解不开;编码只是换一种表示形式,任何人拿到都能按规则还原。看到一串 Base64 不要以为它被加密了,它只是换了个写法。
自测:JWT 的载荷里为什么不能放密码?
因为 JWT 的载荷只做了 Base64 编码,没有加密,任何人拿到令牌都能直接解开看内容。载荷里只该放用户编号、过期时间这类不怕被看到的声明。
自测:手机装了代理证书,为什么还是抓不到明文?
可能是三种原因:证书没被系统信任(安卓 7 以后用户证书默认不被信任)、App 做了证书固定、或者流量根本没走代理。先确认证书是否生效,再排查证书固定。
自测:设备指纹为什么要检查时区和 IP 是否对得上?
因为时区、语言、IP 归属地本该互相吻合。一个显示北京时间、IP 却在另一个大洲的设备,很可能经过了代理或改过设置。风控看的不是单个值,而是这些值之间的一致程度。
自测:认证和授权,差在哪一个字上?
认证解决"你是谁",授权解决"你能干什么"。登录成功只完成认证,能不能看这条订单还要看授权。401 通常是认证问题,403 通常是授权问题。

28.6背词的三个建议

术语量很大,但不需要一次记完。按下面的顺序来,效率最高。

建议一

先记分组

记住十个大类各管什么,比记住任何单个定义都值。知道"这词大概属于证书那组",就已经能猜个七成。

建议二

再记英文

中文译名各家不统一,英文名相对固定。看到不认识的缩写,先看它是不是某个英文词的首字母。

建议三

最后记章号

每个词后面都有章号。真正要用的词,回那一章看一遍完整例子,比在这里读十遍解释管用。

注意 不要在同一个下午硬背一百条。更有效的做法是:读正文时遇到卡住的词,回来查一次,查完立刻回去接着读。在上下文里查一次,胜过孤立地背十次。
动手练
  • 用搜索框依次查"签名""pinning""风控",看每次命中的条数,体会哪些词是全站高频。
  • 挑三个你完全没听过的词,按后面的章号回到正文,各读一遍那一节。
  • 把"加密 / 编码 / 哈希 / 签名"四个词,用自己的话讲给一个不懂技术的人听。
  • 找一段真实的请求头,指出里面哪些字段属于认证、哪些属于指纹、哪些属于内容格式。
你现在应该能回答 加密和编码差在哪?哈希为什么不能倒推?证书固定防的是谁?设备指纹为什么看一致性?认证和授权分别对应哪个状态码?这五个问题能用自己的话讲清楚,这一页就真正用起来了。
第 29 章 · 附录 · 工具

工具速查:按任务选工具,别按名气选

新手最常见的动作是:看到十个推荐工具,一口气全装上,然后一个都跑不通。这一页反过来——先说清楚你要拿到什么信息,再说该用哪一类工具。工具是放大镜,不是眼睛;放大镜再好,也得先知道往哪儿看。

抓包静态分析动态调试 密码学网络排查学习曲线
先记住这一句 选工具的唯一标准是"我要拿到哪一类信息":要流量就抓包,要代码就静态,要运行时真实值就动态。类别选错,再贵的工具也白搭;类别选对,一个免费工具就能解决问题。
人话版 这一页就是一张"看什么用什么"的对照表。你不需要装全,只需要先装对一个,跑通,再加下一个。

29.1先问任务,再问工具

工具本身没有高低,只有"合不合这件事"。同样是想知道"登录时发了什么",用 IDA 去看汇编就是绕远路,用代理抓一次包十分钟就完了。反过来,想知道"签名串是怎么拼出来的",光抓包永远看不出来,必须静态加动态。

所以动手之前,先在心里填一句话:"我要拿到的是(流量 / 代码 / 运行时的值 / 计算结果 / 网络状态)"。填完这句,工具自己就浮出来了。

我要做什么:五类任务对应五类工具 我要做什么? 抓包 看手机在发什么 Wireshark mitmproxy 静态 拆安装包看代码 jadx apktool 动态 看运行时真实值 Frida objection 密码学 算哈希看证书 openssl xxd 网络 查端口查解析 nmap curl 先选一类,跑通一条完整链路,再扩展到第二类 同时装五个工具的人,通常五个都跑不通
图 29-1 先定位任务类型,再挑具体工具;顺序反了就是瞎装

29.2七类工具,一张表看清

把所有工具按"解决什么问题"分成七类。你会发现,日常八成的问题都落在前三类里。

类别解决什么问题代表工具典型场景
抓包看设备与服务器之间发了什么Wireshark、tcpdump、mitmproxy、Charles还原一次登录请求
逆向看安装包里的代码长什么样jadx、apktool、IDA、Ghidra找签名函数的实现
动态调试看运行时真实的入参和返回值Frida、objection拿到加密前的明文
密码学算哈希、看证书、编解码openssl、xxd、keytool验证签名对不对
网络排查查端口、查解析、查连通性nmap、curl、dig、ss接口为什么连不上
取证把证据固定下来、能复现tcpdump、哈希工具、日志记录一次异常行为
序列化把二进制/文本还原成结构xxd、protobuf 工具、JSON 查看器看懂一段自定义协议

29.3十四个常用工具对照

下面这张表只回答四件事:干什么、在哪些系统上跑、有没有更省事的替代、学起来难不难。学习曲线分四档:低、中、高、很高。

工具一句话用途平台 / 替代品学习曲线
Wireshark图形化看完整对话,协议解析最全全平台 / 替代 tshark中
tcpdump命令行抓包,服务器上唯一选择Linux、macOS / 替代 tshark中
mitmproxy代理抓 HTTPS,能用脚本改请求全平台 / 替代 Charles中高
Charles图形化代理,配手机最省事Windows、macOS / 替代 mitmproxy低
Frida运行时改函数入参和返回值全平台客户端 / 替代 Xposed高
jadx把 DEX 反编译成接近 Java 的代码全平台 / 替代 GDA低
apktool解资源、反编译 smali、能重打包全平台 / 无完整替代中
IDA反汇编与反编译,Native 分析标准全平台 / 替代 Ghidra很高
Ghidra免费开源的反汇编反编译全平台 / 替代 IDA Free高
objectionFrida 的封装,一条命令做常见操作全平台 / 替代 自己写脚本低
openssl证书、哈希、编解码、对称加解密全平台 / 替代 语言内置库中
nmap端口与服务识别全平台 / 替代 masscan中
curl手工发一条 HTTP 请求全平台 / 替代 httpie低
xxd十六进制查看文件内容全平台 / 替代 hexdump低

29.4重点工具:什么时候用,什么时候别用

三类工具不是三选一,而是三条互相验证的线。抓包看到的是结果,静态看到的是代码,动态看到的是过程。只有三条线对得上,结论才算站得住。

三条路互相验证,才算真懂 抓包 看到的是结果 静态分析 看到的是代码 动态调试 看到的是过程 对拍 三条线对得上才算懂 只做一边,结论一定是错的
图 29-2 抓包、静态、动态是三条独立的证据线,交叉验证才有结论

抓包四件套

Wireshark 是图形化的"全协议显微镜"。它的强项是解析深:从 TCP 重传到 TLS 握手、从 HTTP/2 帧到 DNS 查询,都能展开到字段级。适合做"一次完整对话到底发生了什么"的分析。缺点是抓手机流量不方便,超大文件会卡。想省内存就用命令行版 tshark。

tcpdump 是服务器上的唯一选择,因为服务器通常没有图形界面。它只负责抓,不负责好看:抓成 pcap 文件,拖回本地用 Wireshark 打开。真正需要记的只有三个参数——-i 选网卡、-w 写文件、-r 读文件,再加一个过滤器就够了。

mitmproxy 是代理型抓包。它站在手机和服务器中间,能解密 HTTPS(前提是手机信任它的证书),还能用 Python 脚本改写请求和响应。适合做自动化:批量重放、批量改参数、把流量导出成结构化数据。它也是免费的。

Charles 是图形化代理,配手机代理最省事,点几下就能看到请求列表,适合入门和手工改包。它是收费软件。功能上 mitmproxy 更全,但 Charles 的上手成本更低,二选一即可,不要两个都装。

静态四件套

jadx 是把 Android 的 DEX 字节码还原成 Java 代码的工具,看代码最快。即使经过混淆,类名变成 a、b、c,代码结构、字符串常量、调用关系都还在。日常九成的"这个接口地址写在哪"用它就能回答。

apktool 干的是另一件事:解出资源文件、把代码反编译成 smali 汇编、并且能重新打包。要改 AndroidManifest、改资源、加一行日志再装回去,就得用它。它和 jadx 不是替代关系,是分工关系。

IDA 是 Native 分析的事实标准,反汇编加反编译,能读懂 .so 里的算法。它是商业软件,价格不低。真正需要它的场景是:关键逻辑用 C/C++ 写在 so 里,Java 层只看到一个 native 方法。

Ghidra 是免费开源的反汇编反编译工具,反编译质量对多数场景够用。缺点是启动慢、吃内存、界面需要适应。预算有限就先学 Ghidra,思路和 IDA 是通的。

动态两件套

Frida 是运行时插桩工具。代码可以混淆、可以加密,但运行的时候必须解密成明文才能用,Frida 就在这一刻拦下来,打印入参、改返回值。它是"看到真实值"的最后一招。代价是:需要 root 或重打包,客户端版本要和手机架构、App 版本对得上,否则连不上。

objection 是 Frida 的封装,把常见操作做成一条命令:关掉证书固定、列出存储内容、跑几个内置检查。不想写脚本的人先用它,能解决就不必自己写 hook。

密码学与网络四件套

openssl 是命令行的瑞士军刀:看证书、生成自签证书、算哈希、做 base64、做对称加解密。验证"我算出来的签名和服务器要的是不是一致",基本都靠它。

nmap 用来在授权范围内梳理端口和服务。它回答的是"这台机器开了哪些门",不是"怎么进去"。用之前先确认目标在授权清单里。

curl 是手工发 HTTP 请求最直接的方式。-v 会把请求头、响应头、TLS 握手过程全打出来,排错时比图形工具更快定位问题。

xxd 用来看二进制。判断一个文件到底是什么类型、一段数据开头是不是已知的魔数,一条命令就够。

29.5环境搭建顺序:三步,不要跳

工具之间存在依赖关系:不会抓包,就不知道要 hook 哪个函数;不会静态,就不知道函数在哪个类里。所以顺序是固定的。

先抓包,再静态,最后动态 第一步 抓包 先让手机流量过代理 达标:看到明文 JSON 工具:mitmproxy 替代:Charles 第二步 静态 先拆开一个 APK 达标:找到请求类 工具:jadx 替代:apktool 第三步 动态 再 hook 一个函数 达标:打印真实参数 工具:Frida 替代:objection 上一步没达标,就不要装下一步的工具 证书问题没解决就上 Frida,只会多一个变量
图 29-3 每一步都有"达标标准";达不到就停下来解决,不要往下堆工具

三步的达标标准写清楚,你才知道自己是不是真的跑通了:

  • 抓包达标:能看到自己 App 一次登录的完整请求,包括 URL、请求头、请求体,内容是明文 JSON 而不是乱码。
  • 静态达标:能在 jadx 里搜到那个 URL 字符串,并找到调用它的类和方法。
  • 动态达标:能 hook 一个方法,在控制台打印出它的入参,且和抓到的内容对得上。
注意 抓包这一步卡住,九成是证书问题:手机没装代理证书、证书装到了用户证书区而 App 只信任系统证书、或者 App 做了证书固定。这是后面第 15 章的内容,但你现在就要知道:这一步不通,后面两步全是白费。

29.6三周计划:一次只加一个

把上图的顺序摊到三周,每周只增加一类工具。这样每一步出问题,你都知道是新加的那个引起的。

周次装什么达标标准
第 1 周mitmproxy 或 Charles,加 Wireshark能抓到并看懂一次登录的明文请求
第 2 周jadx,再加 apktool能在代码里找到那个接口地址
第 3 周Frida,再加 objection能 hook 一个方法并打印入参

IDA 和 Ghidra 放在第四周以后。它们解决的是"逻辑在 so 里"这一类问题,前三周基本用不到。很多人的挫败感来自顺序颠倒:第一天就装 IDA,打开一看满屏汇编,直接放弃。

29.7两个误区

误区一:工具越贵越强。工具的价格反映的是目标客户和授权模式,不是你的需求。IDA 很贵,但 Android 上多数问题用免费的 jadx 就能回答;Charles 收费,而 mitmproxy 免费且能脚本化。先用免费的跑通,真的卡住了再考虑付费,这是更划算的顺序。

误区二:装了 IDA 就会逆向。工具是放大镜,不是眼睛。看不懂汇编,IDA 只会给你一屏看不懂的指令;不会写脚本,装了 Frida 也 hook 不到东西。真正的门槛是知识:汇编、系统调用、App 的运行机制。工具只是把这些知识放大。

技巧 判断自己是不是"只会装工具",有个简单方法:合上工具,用嘴讲一遍"这个 App 登录时大概做了哪几步、签名可能在哪一层"。讲得出来,工具只是加速;讲不出来,装十个也没用。
自测:我想知道"登录请求里到底带了哪些参数",该用哪一类工具?
抓包。参数在请求里,直接看流量最快。只有在流量被加密、或者参数是运行时才拼出来的情况下,才需要转静态或动态。
自测:jadx 和 apktool 有什么区别,能不能只装一个?
jadx 把 DEX 还原成 Java,适合读代码;apktool 解资源、反编译 smali、并且能重新打包。只想看懂逻辑,jadx 够用;要改 Manifest、改资源、加日志再装回去,就必须用 apktool。两者是分工,不是替代。
自测:为什么建议先跑通抓包,再学 Frida?
因为抓包告诉你"目标是什么":哪个接口、哪些参数、什么顺序。没有这个前提,你不知道该 hook 哪个函数,只能在几百个方法里瞎试。抓包是地图,Frida 是手术刀。
自测:装了 IDA 却看不懂,问题出在哪?
不在工具,在基础。IDA 展示的是汇编和反编译结果,看不懂说明缺的是汇编、调用约定、数据结构这些知识。正确顺序是先补基础,再回到工具;而不是换一个更贵的工具。工具不会替你理解代码。
动手练
  • 只装一个抓包工具,抓一次自己 App 的登录请求,把 URL、请求头、请求体各抄一行下来。
  • 用 jadx 打开同一个 App,搜索上一步抄到的 URL 片段,看能不能定位到类。
  • 给自己写一张"工具清单":已经跑通的打勾,没跑通的写清卡在哪一步、下一步该做什么。
  • 把已经装了但三个月没用过的工具卸掉,只留一个类别里最顺手的那一个。
抓包Packet Capture把网络上来回的数据原样记录下来再分析。
静态分析Static Analysis不运行程序,直接看安装包里的代码和资源。
动态调试Dynamic Analysis在程序运行时观察和修改它的行为。
代理Proxy站在客户端和服务器中间,转发并可以查看流量。
插桩Instrumentation在运行时往函数里插入代码,读取或改写数据。
反编译Decompile把机器码或字节码还原成接近源代码的形式。
证书固定Certificate Pinning客户端只信任指定证书,代理证书会被拒绝。
学习曲线Learning Curve从入门到能用所需的时间与难度。
你现在应该能回答 要流量用什么、要代码用什么、要运行时真实值用什么?jadx 和 apktool 分工在哪?为什么必须先抓包再动态?三步各自的达标标准是什么?如果这四个问题你能答上来,你选工具就不会再靠别人推荐。
第 30 章 · 附录 · 命令

命令速查:可以直接抄的一组

这一页不解释原理,只给能直接抄的命令。每条都配一句"它在干什么"和一个真实格式的输出示例。输出里的设备名、IP、时间都是示例,你自己跑出来的肯定不一样——看格式,不要对字符串。

adbfridaopenssl tcpdumpmitmproxycurl
先记住这一句 命令不用背,只要记住三件事:这条命令在哪台机器上跑、它要什么参数、它的输出长什么样。剩下的交给 --help 和搜索。
人话版 这一页是"抄作业"用的。先照着抄,跑通了再回头理解每一段在做什么,比先背参数快得多。

30.1先学会读一条命令

所有命令行工具都遵循同一个结构:程序名 + 子命令 + 参数 + 值。看不懂一条命令,通常不是不会用,而是没拆开看。

命令拆开看:程序名、子命令、手机上的命令、参数 adb shell pm list packages -3 adb 程序名:在你的电脑上运行 shell 子命令:切到手机内部执行 pm list packages 手机上的命令:列出所有应用包 -3 参数:只要第三方应用 参数一般以 - 或 -- 开头;不带参数时用默认值
图 30-1 一条命令拆成四段,每段各回答一个问题

读命令时抓住三个约定:

  • 短参数和长参数:-v 和 --verbose 通常是一回事,短的省事,长的好记。
  • 占位符:文档里写成 包名、文件路径 的地方,是要你替换成真实值的,不要原样敲进去。
  • 引号:带空格或特殊符号的值要加引号,比如过滤器写成 "host 1.2.3.4 and port 443"。

不确定参数时,先敲 命令 --help。这比搜索快,而且不会看到过时的博客。

30.2命令在哪台机器上跑

这是新手最容易混淆的一点。你在电脑上敲 adb,但真正干活的是手机里的进程。命令在电脑上敲,进程在手机上跑,中间靠 adb 或 frida 的通道连起来。

命令在电脑上敲,被启动的进程在手机上跑 你的电脑 adb 命令在这里敲 frida 命令在这里敲 代理工具在这里跑 抓到的文件存这里 手机 adbd 守护进程 frida-server 目标 App 进程 被 hook 的函数 adb 命令 frida 脚本注入 USB 或 Wi-Fi 连接 报错时先分清:是电脑上的命令找不到,还是手机上的进程没起来 这两类错误长得完全不一样,处理方式也不同
图 30-2 同一条命令里,前半段在电脑执行,后半段在手机执行

报错分三类,处理方式完全不同:

  • command not found:电脑上没装这个工具,或者没加进环境变量。
  • no devices / device not found:电脑认不到手机,问题在连接和授权。
  • 手机上的命令报错:命令进到手机了,但权限不够、进程不存在、路径写错。

分不清这三类,就会一直在错误的方向上折腾。看到报错先问一句:这句话是电脑说的,还是手机说的?

30.3adb:和手机说话

adb 是 Android 调试桥,电脑和手机之间的一条命令行通道。它是后面所有手机操作的基础。

看设备连上没有

# 列出电脑认到的设备,-l 带详细信息
adb devices -l
List of devices attached
emulator-5554  device product:sdk_gphone64_x86_64 transport_id:1

关键是第二列:device 表示正常;unauthorized 表示手机上还没点"允许调试";offline 表示连接不稳定,重插一次通常能好。

安装应用

adb install -r app-release.apk
Performing Streamed Install
Success

-r 表示覆盖安装、保留数据。签名不一致时会失败,输出类似 Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE],这时要先卸载旧版本。

进入手机的命令环境

adb shell
beyond1q:/ $ id
uid=2000(shell) gid=2000(shell) context=u:r:shell:s0

进去之后,你敲的就是手机上的命令了,提示符会变成设备名加斜杠。默认身份是 shell,权限有限;要读应用私有目录,需要 root 或者用 run-as。

看日志

# 只看 ActivityManager 这个标签的 Info 及以上级别
adb logcat -s ActivityManager:I
--------- beginning of main
10-27 14:02:11.338  1204  1230 I ActivityManager: Start proc 8821:com.example.app

格式是"时间 进程号 线程号 级别 标签: 内容"。排错时先按标签过滤,不然刷屏速度跟不过来。想看崩溃就加 -b crash。

传文件

adb push note.txt /sdcard/Download/
note.txt: 1 file pushed, 0 skipped.
adb pull /sdcard/Download/note.txt ./
/sdcard/Download/note.txt: 1 file pulled, 0 skipped.

push 是电脑到手机,pull 是手机到电脑。最后一个参数是电脑上的目标目录,./ 表示当前目录。

端口转发

adb forward tcp:27042 tcp:27042
adb forward --list
emulator-5554 tcp:27042 tcp:27042

把电脑的端口映射到手机的端口。Frida 默认走 27042,所以这条命令在连 frida-server 时经常用到。

启动一个界面

adb shell am start -n com.example.app/.MainActivity
Starting: Intent { cmp=com.example.app/.MainActivity }

-n 后面是"包名/活动名"。活动名以点开头表示省略了包名。不知道活动名时,可以先 adb shell cmd package resolve-activity --brief 包名 查一下。

30.4frida:在运行时插一脚

frida 由两部分组成:电脑上的客户端,手机上的 frida-server。两边版本要匹配,否则连不上。

看手机上有哪些进程

frida-ps -U
 PID  Name
----  ------------------------------
8821  com.example.app
1204  system_server

-U 表示通过 USB 连接。先在这里确认目标进程名,后面才能 attach 上去。

启动应用并注入脚本

frida -U -f com.example.app -l hook.js
    / _  |   Frida 16.1.4
   | (_| |
    > _  |   [USB::com.example.app]->

-f 表示由 frida 帮你启动应用(这样能抓住启动阶段的调用),-l 指定脚本文件。看到那个 -> 提示符,说明已经挂上,脚本里的 console.log 会直接打在这里。

只挂到已经在跑的进程

frida -U com.example.app
[USB::com.example.app]->

不加 -f 就是 attach 到现有进程。注意:如果 App 有反调试,attach 可能会让进程退出,这时要用 -f 在启动时就注入。

30.5证书、哈希与编解码

openssl 一条命令能干很多事。下面五条覆盖日常九成的需求。

看一张证书里写了什么

openssl x509 -in server.crt -noout -text
        Version: 3 (0x2)
        Issuer: C = US, O = Let's Encrypt, CN = R3
        Subject: CN = example.com
        Not After : Nov  4 12:00:00 2026 GMT

重点看三行:Subject 是证书给谁的,Issuer 是谁签的,Not After 是什么时候过期。抓包看到证书错误,先查这三项。

生成一张自签证书

openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
  -subj "/CN=test.local" -keyout key.pem -out cert.pem
Generating a RSA private key
writing new private key to 'key.pem'

自签证书只适合本地测试。手机上要信任它,得把它装进系统证书区,用户证书区很多 App 不认。

算哈希

openssl dgst -sha256 abc.txt
SHA2-256(abc.txt)= ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad

上面这个值就是三个字节 "abc" 的 SHA-256,可以用来验证自己算得对不对。改算法名就能换 MD5、SHA1、SHA512。

base64 编解码

echo -n "abc" | openssl base64
YWJj
echo "YWJj" | openssl base64 -d
abc

base64 是编码不是加密,任何人拿到都能还原。看到一长串大小写字母加等号,先试试它。

对称加解密

openssl enc -aes-256-cbc -pbkdf2 -in plain.txt -out plain.enc
enter aes-256-cbc encryption password:
openssl enc -d -aes-256-cbc -pbkdf2 -in plain.enc -out plain.txt

-d 是解密,参数必须和加密时完全一致。-pbkdf2 是现代版本的推荐写法,老教程里没有它,照着敲可能对不上。

看 keystore 里有什么

keytool -list -v -keystore release.jks
Keystore type: JKS
Your keystore contains 1 entry
Alias name: release
Entry type: PrivateKeyEntry

Android 的签名文件就是一个 keystore。要确认一个 APK 是不是同一个 key 签的,比对证书指纹(SHA-256)最快。

30.6抓包:tcpdump 与显示过滤器

tcpdump 负责"抓下来",Wireshark 负责"看明白"。两个配合用。

抓包

sudo tcpdump -i eth0 -w cap.pcap
tcpdump: listening on eth0, link-type EN10MB (Ethernet)
# 按 Ctrl+C 停止
12 packets captured
12 packets received by filter

-i 选网卡(any 表示所有网卡),-w 写文件。抓完把 cap.pcap 拖进 Wireshark 就行。

边抓边过滤

sudo tcpdump -i any -nn "host 203.0.113.10 and port 443"
14:02:11.338 IP 192.168.1.20.51234 > 203.0.113.10.443: Flags [S], length 0

-nn 表示不把 IP 反解成域名、不把端口反解成服务名,输出更干净。过滤器里的 host、port、and、or、not 可以自由组合。

读回已保存的文件

tcpdump -r cap.pcap -nn -c 5
reading from file cap.pcap, link-type EN10MB (Ethernet)

-r 读文件,-c 只输出前几条。想在服务器上快速确认"包里有没有我要的东西",这条比开 Wireshark 快。

Wireshark 显示过滤器

注意区分:tcpdump 的过滤器是"抓的时候就筛",Wireshark 的显示过滤器是"抓完再筛"。后者更灵活,语法也不同。

过滤器作用什么时候用
ip.addr == 203.0.113.10只看这个 IP 相关的包锁定一台服务器
tcp.port == 443只看 443 端口只看 HTTPS
http.request只看 HTTP 请求数一数发了几个请求
tls.handshake只看 TLS 握手查证书和协商参数
# 组合:某个 IP 上的 HTTPS 请求
ip.addr == 203.0.113.10 && tcp.port == 443
# 只看 ClientHello(握手第一步)
tls.handshake.type == 1

&& 是"并且",|| 是"或者",!= 是"不等于"。写错的时候输入框会变红,比抓瞎猜快。

30.7mitmproxy:代理抓包与重放

代理抓包和网卡抓包的区别:代理能看到解密后的 HTTPS 内容,网卡抓包只能看到密文。

启动

mitmproxy -p 8080
[14:02:11] HTTP(S) proxy listening at *:8080.

然后把手机的 Wi-Fi 代理设成电脑的 IP 加 8080 端口,再在手机上装 mitmproxy 的 CA 证书。手机和电脑要在同一个网络里。

保存流量

mitmdump -w flows.mitm
[14:02:11] HTTP(S) proxy listening at *:8080.

mitmdump 是没有交互界面的版本,适合挂着慢慢存。-w 指定保存文件,全程流量都会写进去。

读回与重放

# 离线浏览已保存的流量
mitmproxy -r flows.mitm
# 读回并重发给服务器
mitmdump -nr flows.mitm

重放前先想清楚:这条请求会不会产生真实业务后果。下单、支付、发消息这类接口,重放一次就是一次真实操作,只能在授权环境里做。不同版本的参数可能略有差异,拿不准就 mitmdump --help 查一下。

30.8拆包与重打包

jadx 负责看,apktool 负责改。两个工具的输出目录不要混在一起。

反编译看代码

jadx -d out app.apk
INFO  - output directory: out
INFO  - processing ...
INFO  - done

-d 指定输出目录。跑完用编辑器打开 out 目录,或者直接用 jadx 的图形界面版,搜索更方便。

解包(资源加 smali)

apktool d app.apk -o app_src
I: Using Apktool 2.9.3 on app.apk
I: Decoding AndroidManifest.xml with resources...

解出来的是可读的 AndroidManifest、资源文件和 smali 汇编。改 Manifest 里的权限、改字符串资源,都在这一步做。

重打包

apktool b app_src -o app_patched.apk
I: Smaling smali folder into classes.dex...
I: Building resources...

打包出来的 APK 没有签名,装不上。要用 apksigner 或 keytool 重新签名,而且签名和原版不一致时,覆盖安装会失败,得先卸载。

30.9网络排查四条

接口连不上时,按"从应用到网络"的顺序查,比乱试快得多。

看请求到底发了什么

curl -v https://api.example.com/v1/ping
> GET /v1/ping HTTP/1.1
> Host: api.example.com
< HTTP/1.1 200 OK
< content-type: application/json

-v 会把请求头、响应头、TLS 握手都打出来。> 开头是发出去的,< 开头是收回来的。加 -H "Header: value" 可以带自定义头。

查域名解析

dig example.com A +short
93.184.216.34
dig example.com A
;; ANSWER SECTION:
example.com.        3600    IN    A    93.184.216.34

+short 只给结果,适合脚本里用;不带它就能看到 TTL 和记录类型。解析不出来时,先换一个公共 DNS 再试,判断是本机 DNS 的问题还是域名本身的问题。

查路由

traceroute example.com
traceroute to example.com (93.184.216.34), 64 hops max
 1  192.168.1.1  2.145 ms  1.982 ms  1.876 ms
 3  * * *
 4  203.0.113.45  24.552 ms  23.998 ms  24.771 ms

每一行是一跳路由器。* * * 表示这一跳不回包,通常是对方禁了 ICMP,不一定是故障。看的是"在哪一跳之后延迟突然变大"。

看本机监听端口

ss -lntp
State  Recv-Q  Send-Q  Local Address:Port  Process
LISTEN 0       128     0.0.0.0:22          users:(("sshd",pid=812,fd=3))
LISTEN 0       511     127.0.0.1:8080      users:(("mitmdump",pid=3312,fd=9))

-l 只看监听,-n 不解析名字,-t 只看 TCP,-p 显示进程。注意 127.0.0.1 和 0.0.0.0 的区别:前者只有本机能连,后者对外网开放。

30.10命令没反应时的排查顺序

命令敲下去没输出、或者报了个看不懂的错,不要猜。按下面四步走,每一步都能排除掉一大片可能。

命令没反应:按顺序查四件事 ① 命令在不在 which adb adb --version 先确认装没装 ② 设备连上没 adb devices 看有没有 device 再看授权弹窗 ③ 权限够不够 要不要 root 有没有弹窗拒绝 看报错关键词 ④ 目标在不在 进程名对不对 路径写没写错 大小写敏感 别猜。每往下一步,都要先确认上一步的结果是好的 跳过第一步直接查第四步,是新手最常犯的错
图 30-3 四步排查:工具、连接、权限、目标,顺序不能反

再补三个高频细节:

  • 版本不匹配:frida 客户端和 frida-server 大版本必须一致,否则连上了也立刻断。
  • 路径大小写:Android 的文件系统区分大小写,/sdcard/Download 和 /sdcard/download 是两个地方。
  • 端口被占:代理起不来先看 ss -lntp,8080 被别的程序占了就换一个。

30.11安全提醒

上面这些命令,每一条都有明确的适用范围。同一行命令,在自己手机上跑是调试,在别人手机上跑就是入侵。

  • 只在自己的设备上执行:自己的手机、自己的电脑、自己搭的服务器。
  • 只对授权目标执行:扫描、爆破、重放这类动作,必须有书面授权,且在范围内。
  • 不碰生产数据:测试用假账号、假订单,不要拿真实用户做实验。
  • 抓到的包限期删除:里面通常含完整的请求体和 Token。
  • 报告只给甲方:不公开发布,不在群里传阅。
红线 不要对任何未授权的系统执行扫描、爆破、重放、注入类命令。技术上一条命令几秒钟,法律上的后果可能是几年。判断标准不在命令本身,而在"你有没有权限对那个目标动手"。
自测:adb devices 显示 unauthorized,该怎么处理?
说明手机还没授权这台电脑调试。解锁手机,看屏幕上有没有"允许 USB 调试"的弹窗,勾选"一律允许"再确认。如果没弹窗,先关掉再打开 USB 调试开关。
自测:抓包抓到一堆乱码,是工具坏了吗?
不是。乱码通常意味着流量是加密的,而你的工具没有被客户端信任,拿不到会话密钥。下一步是让手机信任代理证书;如果仍然不行,就是 App 做了证书固定。
自测:tcpdump 的过滤器和 Wireshark 的显示过滤器,区别在哪?
tcpdump 的过滤器在抓的时候就筛,不匹配的包直接不记录;Wireshark 的显示过滤器在抓完之后筛,原始数据还在,随时能改条件。所以抓包时尽量少过滤,避免把有用的包漏掉。
自测:apktool 打包出来的 APK 为什么装不上?
因为重打包后没有签名,或者签名和原版不一致。Android 要求 APK 必须签名才能安装,覆盖安装还要求签名一致。所以要先 apksigner 签名;签名变了就得先卸载旧版本。
动手练
  • 在自己手机上跑一遍 adb devices、adb shell、adb logcat -s ActivityManager:I,把三条输出各抄一行。
  • 用 openssl dgst -sha256 算一个文件的哈希,再换 SHA1 算一次,比较长度。
  • 用 curl -v 请求一个公开接口,找出请求行、请求头、响应状态码各在哪一行。
  • 用 ss -lntp 看本机开了哪些端口,判断哪个只对本机开放。
adbAndroid Debug Bridge电脑与安卓设备之间的命令行调试通道。
frida-serverFrida Server跑在手机上的服务端,与电脑客户端配对使用。
注入Injection把脚本或代码放进正在运行的进程里执行。
显示过滤器Display FilterWireshark 里抓完之后再筛包的条件表达式。
重放Replay把抓到的请求原样再发一次,用来验证行为。
keystore密钥库存放签名私钥和证书的文件,Android 签名靠它。
TTLTime To LiveDNS 记录的缓存时间,单位秒。
监听端口Listening Port本机正在等待连接的网络端口。
你现在应该能回答 一条命令由哪几段组成?报错时怎么区分是电脑说的还是手机说的?adb 和 frida 各自解决什么问题?抓包乱码怎么办?tcpdump 和 Wireshark 的过滤有什么不同?这五个问题答得上来,这一页的命令你就不是死记,而是会用。
第 31 章 · 附录 · 面试

面试题库:120 问,从基础到实战

面试卡住,多数时候不是没学过,而是没按面试官想听的方式组织答案。这一页把十个方向的高频问题按难度排好,每题先给结论、再给理由。建议遮住答案自问一遍,说不顺的做标记,回到对应章节补。

10 个方向120 道题结论先行 难度递进口述版答案可回查章节
先记住这一句 面试考的不是背诵量,而是你能不能把一个技术问题讲到别人听懂。每题都按"结论、理由、边界"三段说,比堆十个术语有用得多。
人话版 这页是一本可以遮住答案的口述练习册。先自己答一遍,再看答案差在哪,比通读十遍有效。

31.1怎么用这一页:四句话的答题结构

技术面试的答案不需要长,但必须有结构。面试官在三十秒内要判断三件事:你知不知道结论、你知不知道原因、你知不知道什么时候不成立。对应到回答,就是四步。

面试答题四步:结论、理由、边界、例子 一道题的标准答法:四句话,三十秒到一分钟 0 秒 ① 先给结论 一句话说清是什么,不要先铺背景 10 秒 ② 再说为什么 给两三条理由,最好带一个具体数字 30 秒 ③ 补边界和代价 什么情况下不成立,成本花在哪里 1 分 ④ 举一个例子 抓过的包、见过的报错、跑过的命令 只答第一步,像背书;只答第四步,像聊天
图 31-1 答题节奏:先用结论抓住注意力,再用理由和例子证明你想清楚了

下面这张表是十个分组的分布。每组 12 题,前几题偏基础,后几题偏排查和取舍。第一次练可以按顺序走;考前突击就只挑每组的后四题。

分组题数难度走向面试里常怎么问
一 网络基础12握手到排查这个请求为什么慢
二 密码学12概念到取舍为什么不用 RSA 加密数据
三 TLS 与证书12流程到攻击面装了证书为什么还解不开
四 HTTP 与 Web12语义到漏洞这个接口有什么风险
五 编码与序列化12格式到解析安全这段乱码是什么
六 Android 逆向12结构到对抗你怎么定位加密函数
七 iOS 逆向12结构到对抗越狱之后你先做什么
八 抓包与调试12准备到排错抓不到包怎么办
九 风控与指纹12概念到一致性怎么判断是同一台设备
十 工程与合规12流程到边界怎么保证稳定和合规
十组考点的依赖关系:地基、协议、客户端、落地 前四组决定你能不能听懂问题,后六组决定你能不能给出方案 一 底层地基 网络基础 · 密码学 · TLS 证书 听不懂这三组,后面全程卡住 二 应用协议 HTTP 与 Web · 编码序列化 所有抓到的内容都落在这里 三 客户端对抗 Android · iOS · 抓包调试 决定你能不能真的动手 四 业务与落地 风控指纹 · 工程与合规 决定你的答案有没有分寸 同一道题往往横跨两三组:比如"抓不到包"同时考 TLS、Android 和工程排查
图 31-2 十组考点不是并列的,前四组是后六组的前提
补充 很多人的误区是把面试当考试,追求"答对"。真实面试更看你如何面对不会的题:说清已知条件、给出排查路径、标明不确定的地方,比硬编一个答案得分更高。

31.2一、网络基础(12 题)

这一组是所有问题的地基。前六题必须张口就来,后六题考的是"出问题怎么分层定位"。

三次握手为什么不能只握两次?
两次只能让服务端确认"客户端能发、自己收得到",无法确认客户端也收得到。第三次 ACK 让服务端确认客户端接收正常,同时能挡掉网络里滞留的旧 SYN,避免为一个早已关闭的连接分配资源。核心是双向确认加防历史连接。
四次挥手后,主动关闭的一方为什么要停在 TIME_WAIT?
一是最后的 ACK 可能丢,对方会重发 FIN,得留着能回;二是让这条连接的四元组在网络里彻底消失,避免残余报文串到复用的新连接上。Linux 上固定等 60 秒,也就是 2MSL。不是没关干净,是刻意等。
TCP 和 UDP 最本质的区别是什么?各举一个真实场景。
TCP 是字节流,保证有序可靠,代价是握手、重传和队头阻塞;UDP 是独立报文,不保证送达,开销小、时延低。网页和文件传输用 TCP;DNS 查询、实时音视频、游戏状态同步用 UDP,可靠性由应用层自己补。选型看能不能忍丢包。
DNS 为什么默认用 UDP?什么时候会切到 TCP?
查询是一问一答的小包,UDP 省掉握手,一次往返就能拿到结果。响应超过长度限制被截断(TC 标志置位)、做区域传送,或走 DoT、DoH 时才用 TCP。UDP 是默认,TCP 是兜底。
MTU 和 MSS 是什么关系?为什么大包反而更慢?
MTU 是链路层一帧能装的最大字节,以太网通常 1500;超过就由 IP 分片,任一分片丢失,整个包都要重传。TCP 用 MSS 协商(通常 1460)把数据切成不超过 MTU 的段,从源头避免 IP 分片。
家里设备都能上网,为什么外网主动连不进来?
NAT 只维护"内网先发出去"的映射表,外部没有对应表项,就不知道该转给谁。要主动进入得做端口映射、UPnP,或用 STUN、TURN 做穿透与中继。NAT 是一扇单向门,不是完整的防火墙。
长连接为什么还要应用层心跳?TCP keepalive 不够吗?
NAT 和负载均衡会静默回收空闲连接,两端都以为还活着;TCP keepalive 默认两小时才探一次,太慢,还可能被中间设备忽略。应用层心跳(常见 30 秒)能及时发现断链并重连。心跳主要是给中间设备看的。
HTTP/1.1 的队头阻塞是什么?HTTP/2 怎么缓解?
一条连接同一时刻只能有一个请求在等响应,前面慢后面全堵。HTTP/2 把消息拆成帧,用 stream 多路复用,一个连接并发跑多个请求。但 TCP 层丢包仍会阻塞所有流,这也是 QUIC 出现的动机。
QUIC 为什么选择跑在 UDP 上?
它要在用户态自己实现可靠传输、拥塞控制和加密,不受操作系统 TCP 实现和中间设备的影响;TLS 1.3 内建,握手更快,连接 ID 还能让换网络时不断连。不是 UDP 更快,是能自己改。
输入一个域名到拿到 IP,中间经历了什么?
先查本地缓存和 hosts,再问本地 DNS;本地 DNS 按需向根、顶级、权威服务器逐级查询,递归与迭代配合完成,结果按 TTL 缓存。任一环节命中缓存就提前结束。排查解析问题要一层层看缓存。
TCP 粘包是什么?为什么 UDP 没有这个问题?
TCP 是连续字节流,没有消息边界,发送方两次写可能被合并,也可能被拆开。UDP 保留报文边界,一次发送对应一次接收。解决靠应用层定界:长度前缀、分隔符或定长。粘包的本质是没有边界,不是粘住了。
用户说"接口很慢",你怎么判断慢在 DNS、TCP、TLS 还是服务端?
分层测:先 dig 看解析耗时,再用 curl 的 time_namelookup、time_connect、time_appconnect、time_starttransfer 分段看,哪一段突然变大就查哪一层。不要凭感觉换网络或重启服务。

31.3二、密码学(12 题)

这一组最容易被问倒的地方不是算法细节,而是"为什么不那样做"。答案里多讲代价,少背参数。

哈希和加密,最根本的区别是什么?
加密可逆,需要密钥,目的是保护内容;哈希不可逆、输出定长,目的是做指纹和校验。存密码用哈希,传数据用加密。看到能解回来就是加密,解不回来才是哈希。
存密码为什么不能用 SHA-256,而要用 bcrypt 这类慢哈希?
SHA-256 太快,一张显卡每秒能算几十亿次,弱密码瞬间被穷举。bcrypt、scrypt、argon2 故意慢、吃内存,还自带盐,把每次尝试的成本抬高。密码哈希的目标是让爆破变贵。
对称加密和非对称加密,各自的代价是什么?
对称加密(AES)快、适合大数据,但双方要先有同一把钥匙,分发是难题;非对称(RSA、ECC)能解决密钥交换和签名,但运算慢几个数量级、密钥更长。实际系统是两者配合,不是二选一。
既然有非对称加密,为什么传输数据还是用对称?
非对称运算太慢,加密大文件会把 CPU 吃满。实际做法是用非对称协商出一把临时会话密钥,之后全程用 AES 搬数据。非对称负责安全地换钥匙,对称负责搬东西。
AES-GCM 比 AES-CBC 好在哪?
GCM 是 AEAD,一次运算同时给出机密性和完整性,还能并行;CBC 只负责加密,必须额外加 HMAC,否则可能被填充预言攻击,还要小心 IV 的选取。能选 AEAD 就不要自己拼加密加校验。
盐(salt)是干什么的?需要保密吗?
盐是每个用户独立的随机值,和密码一起哈希,让相同密码得到不同结果,防彩虹表和批量撞库。盐不需要保密,但要唯一、够长。盐防的是一次算好到处用。
HMAC 和直接 hash(key + msg) 有什么区别?
HMAC 用两次哈希和密钥派生,能抵抗长度扩展攻击;直接拼接对 SHA-256 这类结构可以被攻击者在已知摘要后继续追加数据。要认证就用 HMAC,不要自己拼字符串。
数字签名和加密,钥匙的方向为什么相反?
加密用对方公钥,只有对方私钥能解,目的是保密;签名用自己私钥,任何人拿公钥都能验,目的是证明确实是我发的。签名不是"用私钥加密",虽然 RSA 在数学上很像。
RSA 签名和 ECDSA 签名有什么实际差别?
RSA 密钥和签名都长(2048 位起),验证快、签名慢;ECDSA 密钥短、签名短,但签名带随机数 k,同样内容每次签名都不同。k 一旦复用会直接泄露私钥,这是历史上有名的翻车点。
为什么说 ECB 模式不能用?举一个直观例子。
ECB 把每个数据块独立加密,相同明文块得到相同密文块。加密一张纯色背景的图片,轮廓依然清晰可见,等于泄露了结构。要加 IV 用 CBC,或直接用 GCM。
前向保密(PFS)解决什么问题?
让每次会话都用临时密钥协商,服务器长期私钥以后泄露,也解不开之前录下的流量。静态 RSA 密钥交换没有这个性质,私钥一泄露,历史流量全解。现代 TLS 默认用临时 DH,就是为了它。
为什么密钥和 nonce 不能用普通随机函数生成?
rand() 是伪随机,种子常取时间,可被预测,必须用密码学安全随机源(/dev/urandom、SecureRandom)。GCM 下 nonce 复用会暴露两组明文的异或关系,严重时能恢复认证密钥。随机数质量直接决定加密强度。

31.4三、TLS 与证书(12 题)

这一组的高频场景几乎都来自同一句抱怨:"我证书装了,为什么还是不行。"先把"信任根"和"证书固定"分开,问题就清楚一半。

TLS 到底解决了哪几个问题?
三件事:机密性(内容加密)、完整性(篡改能被发现)、身份认证(证书证明对面真是它)。它不解决服务器被攻破,也不解决业务逻辑漏洞。TLS 保护的是传输,不是应用。
TLS 1.3 相比 1.2 的主要改进有哪些?
砍掉 RSA 密钥交换和一批弱算法,只留 AEAD 与临时 DH;握手从 2-RTT 降到 1-RTT,会话恢复可以 0-RTT;更多握手消息被加密,证书也不再明文传输。更快、更简单、更难配错。
证书链是怎么验证的?根证书凭什么可信?
服务器发站点证书和中间 CA 证书,客户端用本地预装的根证书逐级验签,同时检查有效期、域名、用途和吊销状态。根证书由系统和浏览器预置,靠 CA 的审核流程建立信任。信任的起点是你装了什么。
证书里的 CN 和 SAN 有什么区别?
CN 是旧字段,现代浏览器只认 SAN(subjectAltName)里列出的域名。一个证书可以写多个域名,通配符只覆盖一级,写星号点 example 点 com 并不覆盖 a.b.example.com。
证书为什么要吊销?CRL 和 OCSP 各有什么问题?
私钥泄露或误签发时要尽快失效。CRL 是全量列表,体积大、更新慢;OCSP 实时查询但增加延迟、还会泄露用户访问的站点,所以有了 OCSP stapling,把响应附在握手时一起发。吊销检查长期是能用但不完美。
会话复用(session ticket)是什么?有什么风险?
把上次协商的密钥材料存成 ticket,下次直接恢复,省掉完整握手。风险是 ticket 加密密钥泄露后可解密流量,且 0-RTT 阶段的数据可能被重放。复用要配合有效期和密钥轮换。
SNI 是什么?为什么说它会泄露隐私?
客户端在 TLS 握手时明文告诉服务器要访问哪个域名,方便同一个 IP 复用证书。中间人因此能看到你访问的域名,即使内容已加密。ECH 就是想把这个字段也加密。
双向认证(mTLS)用在哪?它能替代业务鉴权吗?
客户端也出示证书,常用于服务间调用、企业内网、硬件设备接入。它证明这个客户端是谁,但不说明它能做什么。认证之后还要授权,两者不能合并。
抓包工具为什么必须让你装证书?装了之后为什么就能解密?
工具要冒充服务器,必须出示一张客户端信任的证书。你装了它的根证书,它就现场签发证书完成握手,于是持有会话密钥。这不是破解了 TLS,是你主动信任了它。
TLS 1.3 的 0-RTT 为什么有重放风险?
0-RTT 数据在第一个飞行就发出,服务器还没完成新鲜性确认,攻击者可以把这段数据原样重放。所以只应对幂等、无副作用的请求开启。快和防重放要取舍。
证书固定(pinning)通常在哪几个层次实现?
代码层比对证书或公钥哈希(最常见)、网络库配置(如 OkHttp 的 CertificatePinner)、系统配置层(network_security_config、信任锚)。越靠底层越难绕过,维护成本也越高。
用户把手机时间改到 2020 年,HTTPS 会怎样?
证书校验会失败,提示证书尚未生效或已经过期,因为有效期是拿本地时间比的。正确做法是校时并拒绝明显异常的时间,而不是简单跳过时间检查。时间本身也是安全边界的一部分。

31.5四、HTTP 与 Web(12 题)

这一组的问题在面试里经常以"这个接口有什么风险"的形式出现。答题时先说攻击者能做什么,再说怎么防。

GET 和 POST 的本质区别是什么?
本质是语义:GET 取资源、应当无副作用、可缓存可重放;POST 提交数据、可能改变状态。所谓 GET 有长度限制、POST 更安全,都是实现约定,不是协议规定。别用 GET 传密码。
301、302、307、308 有什么区别?
301 和 302 历史上允许把 POST 改成 GET,307 和 308 明确保持原方法和请求体,308 还要求永久。301 和 308 默认可被缓存,302 和 307 通常不缓存。要保留 POST 就用 307 或 308。
Cookie 的 HttpOnly、Secure、SameSite 各防什么?
HttpOnly 让 JS 读不到,缓解 XSS 偷 cookie;Secure 只允许在 HTTPS 上传输;SameSite 限制跨站请求携带,主要防 CSRF。三个一起用才算基本合格。
有了 CORS,后端还需要做鉴权吗?
需要。CORS 是浏览器的读保护,只约束页面里的 JS;攻击者用 curl 或自己的服务器直接请求,完全不受它限制。CORS 是浏览器行为,不是服务端防线。
CSRF 的原理是什么?怎么防?
浏览器会自动带上目标站点的 cookie,攻击者诱导已登录用户打开一个页面,就替他发出了请求。防御靠 SameSite、CSRF token、校验 Origin 和 Referer,敏感操作再确认一次。核心是请求不是你本意发的。
反射型、存储型、DOM 型 XSS 有什么区别?
反射型是 payload 在 URL 里被当场回显;存储型是存进数据库、对每个访问者触发,危害最大;DOM 型是前端 JS 把不可信数据写进页面,服务端日志里看不到。防御核心是输出编码加 CSP。
SQL 注入的本质是什么?预编译为什么能防?
本质是把用户输入当代码拼进了 SQL 语句。预编译先固定语句结构,再把参数当纯数据传入,参数不参与语法解析。靠拼接字符串加手工转义,迟早出事。
SSRF 怎么防?为什么黑名单不够?
让服务器替你发请求,就能打内网和云元数据接口。防御要白名单、解析后校验真实 IP(防 DNS 重绑定)、禁止跟随跳转、限制出网。黑名单挡不住 302、十进制 IP、IPv6 等变形。默认拒绝,按需放行。
JWT 最常见的几个漏洞是什么?
alg 设为 none 不验签、弱密钥可爆破、kid 注入、算法混淆(把 RS256 当 HS256 用,公钥当密钥)、只解码不校验、忽略过期时间。服务端必须固定算法并真正验签。
JWT 签发后无法主动失效,工程上怎么解决?
缩短有效期配合 refresh token;维护黑名单或 token 版本号(jti、token_version),改密码或封号时递增版本;关键操作再校验一次。不是发了就管不了,是要额外设计。
OAuth 授权码流程为什么要多一步"用 code 换 token"?
code 走前端通道,token 走后端通道,避免 token 出现在浏览器历史、Referer 和日志里;code 一次性、寿命短,配合 PKCE 还能防截获。多一步是为了少暴露。
什么是 IDOR?怎么系统性排查?
越权访问对象:把 URL 或参数里的 ID 改成别人的,看能不能读到。排查方法是列出所有带 ID 的接口,用两个账号交叉重放,确认服务端每次都校验这个对象属于当前用户。前端藏起来不算防护。

31.6五、编码与序列化(12 题)

这一组看起来最"基础",实际最容易露怯。能一眼分清编码、加密、哈希的人,通常也能一眼看懂抓包里的乱码。

编码、加密、哈希,三者怎么区分?
编码是格式转换,可逆、不需要密钥(Base64、URL 编码);加密可逆、需要密钥;哈希不可逆、输出定长。三者目的完全不同,不能互相替代。看到能随便解开,那就是编码。
Base64 是加密吗?为什么体积会变大?
不是,任何人拿到都能解。它把 3 个字节转成 4 个可打印字符,所以体积约增加三分之一,代价换来的是能安全放进文本协议里传输。Base64 只负责变成安全字符。
URL 编码为什么必要?%20 和加号有什么区别?
URL 里的 &、等号、问号、井号、斜杠都有语法含义,出现在值里必须转义成百分号形式。%20 是空格的百分号编码;加号是表单编码里空格的旧约定,在路径里它就是加号本身。路径用 %20,表单才用加号。
UTF-8 和 GBK 有什么区别?乱码是怎么产生的?
UTF-8 是变长编码、兼容 ASCII、中文占 3 字节;GBK 是双字节、只覆盖中文。用 A 编码写入、用 B 解码就会乱码,而且常常不可逆。乱码多半是编码声明和实际字节不一致。
大端和小端是什么?为什么网络字节序用大端?
大端把高位字节放前面,小端相反。网络协议统一规定用大端,避免不同 CPU 解释不一致;x86 本机是小端,所以代码里要 htons、htonl 转换。看到 00 00 00 0A 这种,多半是大端长度。
JSON 和 Protobuf 该怎么选?
JSON 可读、通用、调试方便,但体积大、解析慢;Protobuf 是二进制,体积小、快,但必须有 .proto 才能读懂,抓包后不能直接看。对外接口用 JSON,内部高频通信用 Protobuf。
Protobuf 消息里没有字段名,怎么保证前后兼容?
靠字段编号,不靠名字。编号一旦发布不能改、不能复用;新增字段用新编号,旧版本遇到不认识的编号就跳过。改名字安全,改编号会直接读错数据。
MessagePack 和 JSON 有什么不同?
都是结构化数据,MessagePack 是二进制表示,体积更小、解析更快,且不需要 schema,比 Protobuf 通用;但没有类型约束,出错时更难发现。抓包里看到 82 A4 6E 61 6D 65 这类,可以怀疑它。
什么是 TLV?为什么私有协议爱用它?
Type-Length-Value,先声明字段类型、再声明长度、最后放值。解析器遇到不认识的字段也能按长度跳过,扩展性好、天然向前兼容。比固定位置固定长度灵活得多。
JSON 标准不允许注释和尾逗号,这会带来什么安全问题?
严格解析器会直接报错,但有些实现(JSON5、部分 JS 引擎)更宽松。同一份数据在不同端解释不同,就可能出现风控看到 A、业务看到 B 的绕过。解析器不一致本身就是漏洞。
反序列化漏洞的原理是什么?
把字节流还原成对象时,如果攻击者能控制类型和字段,就可能触发构造好的方法调用链,最终执行任意代码。防御是不反序列化不可信数据、强制类型白名单。能解析不等于能安全解析。
一个字段看起来像 Base64,你怎么判断它是编码、加密还是压缩?
先解码看内容:是可读文本或 JSON 就是编码;解出来是高熵随机字节,才可能是加密或压缩。再结合长度是否固定、换一次请求是否变化来推断。先排除最简单的解释。

31.7六、Android 逆向(12 题)

这一组前半段考结构,后半段考方法论。面试官更想听"你怎么找到它",而不是"你用了哪个工具"。

一个 APK 拆开,里面主要有哪些东西?
classes.dex 是代码,resources.arsc 是资源索引,AndroidManifest.xml 是二进制清单,res 放布局和图片,lib 放各架构的 .so,META-INF 是签名。先看 lib 和 dex 的大小,能快速判断重心在哪。
DEX 和 smali 是什么关系?
DEX 是给 Android 运行时执行的字节码;smali 是 DEX 的可读汇编表示,一条指令对应一条 smali。要改逻辑通常改 smali 再回编译,jadx 则是把 DEX 还原成接近 Java 的代码。读用 jadx,改用 smali。
jadx 和 apktool 该用哪个?
jadx 擅长把代码还原成可读 Java,适合读逻辑;apktool 擅长解资源、保留 smali 并能回编译,适合改 manifest、资源和打补丁。实际流程通常是先用 jadx 读,确定位置再用 apktool 改。一个用来看,一个用来改。
为什么很多 App 把关键逻辑放在 .so 里?
Java 层容易被反编译成接近源码的样子,native 层是编译后的机器码,逆向成本高得多;而且签名、加密、风控核心本来就常用 C 库。放 .so 不是绝对安全,是提高门槛。
加固通常做了哪些事?
加密或抽取 DEX、运行时解密加载、反调试、完整性校验、检测 hook 框架,更强的会用自定义虚拟机指令。目标是让静态分析拿不到完整逻辑。加固提高成本,但不能让程序变得不可分析。
常见的反调试手段有哪些?
检测调试器附加(ptrace、进程状态)、检测 Frida 和 Xposed 特征、检测 root、扫描内存里的异常模块、用时间差判断是否被单步、多进程互相守护。对抗思路是先找检测点,再决定绕还是改。
Frida 的 spawn 和 attach 有什么区别?
spawn 是让 App 以挂起状态启动、注入后再放行,能拦到早期初始化逻辑,成功率高;attach 是附加到已经跑起来的进程,可能错过早期代码,也更容易被检测。优先用 spawn。
拿到一个 App,怎么定位某个按钮背后的加密函数?
先从界面找点击事件,顺调用链找到网络请求;或直接 hook 网络库的拦截器看真实入参,再往回追到加密函数;最后用真实请求对拍,确认参数一致。从结果反推,比从入口正推快。
Xposed、LSPosed 和 Frida 有什么区别?
Xposed 系在 Java 层 hook,改的是运行时方法,适合改 App 逻辑、写模块;Frida 同时支持 Java 和 native,脚本化强、迭代快,适合分析。两者通常都需要 root 或重打包。改功能用 Xposed,做分析用 Frida。
为什么有时候 hook 了系统 API 却没有效果?
可能目标根本没走 libc(直接发起系统调用)、hook 时机太晚、函数被内联或用了自己的实现,也可能有多处校验而只 hook 了一处。先确认调用路径,再谈 hook。
Android 上的证书校验通常分布在哪几层?
网络库层(OkHttp 的 TrustManager 和 CertificatePinner)、native 层(BoringSSL 或 OpenSSL 自己校验)、业务层(自己比对证书或公钥哈希)。只改一层,往往还会被另一层拦住。
怎么快速判断一个 App 有没有加壳?
看 Manifest 里的 Application 是否被替换成陌生类、dex 是否异常小、lib 下有没有可疑的 .so、是否有大量加载和解密相关的 native 调用。壳的典型特征是代码很少,但运行时什么都有。

31.8七、iOS 逆向(12 题)

iOS 的面试题通常不考工具命令,而考"你为什么必须越狱""砸壳到底砸的是什么"。把这两件事讲清,后面就顺了。

从 App Store 下载的包,和网上流传的 IPA 有什么区别?
App Store 的可执行文件是加密的,直接静态分析只能看到加密段;流传的 IPA 通常是砸壳或重签名后的,代码可读。拿到加密包,第一步是确认它是否已经解密。
iOS 逆向为什么通常需要越狱设备?
系统权限封闭,不越狱就拿不到 root、不能关签名校验、不能往别的进程注入。越狱后可以装 Frida、改文件、调试系统进程。越狱是打开动态分析这扇门。
Mach-O 是什么?
iOS 和 macOS 的可执行文件格式,由 header、load commands 和若干段组成,__TEXT 放代码、__DATA 放数据。逆向工具读的就是这些段。看到 Mach-O 就知道这是苹果平台的原生二进制。
砸壳到底在做什么?
App Store 包里的代码段是加密的,运行时才由系统解密到内存。砸壳就是在运行时把解密后的内容 dump 出来,并修复成可静态分析的二进制。砸的是磁盘上加密、内存里明文这个差。
Objective-C 的 method swizzling 是什么?
在运行时把某个方法的实现换掉,也就是交换实现指针,常用于 hook 和打补丁。因为 OC 有运行时和方法表,替换成本很低。防守方也会检测方法实现是否被换过。
Swift 逆向和 Objective-C 逆向有什么不同?
OC 有丰富的类名和方法名,符号可读;Swift 名字被 mangle 成一长串(常见以 $s 开头),需要 demangle,且大量逻辑被内联优化,静态阅读更难。Swift 更依赖动态分析和符号还原。
iOS 上常见的证书固定是怎么实现的?
在 NSURLSession 的认证回调里比对服务器证书或公钥哈希、用现成的 pinning 库、或在 native 层用 OpenSSL、BoringSSL 自行校验。看到回调里出现证书哈希比较,基本就是 pinning。
不越狱的设备能做动态分析吗?
可以,但要重签名 IPA 并注入 Frida 之类的库,再安装到自己的设备上。代价是签名变了,可能触发完整性校验,而且只能在自己的设备和授权目标上做。能力有,边界也要清楚。
iOS 的设备指纹通常来自哪些信息?
机型与系统版本、屏幕与语言时区、IDFV、Keychain 里的持久标识、广告标识(需用户授权)、部分硬件与传感器特征。苹果限制越多,指纹越依赖组合而不是单个 ID。
为什么说 iOS 的 Keychain 比 NSUserDefaults 安全?
Keychain 由系统加密存储、按应用隔离、可设置访问控制(例如仅本机、需解锁);NSUserDefaults 是明文 plist,越狱设备或备份里能直接读。敏感数据放 Keychain 是基本要求。
静态分析时字符串被加密了,怎么找关键逻辑?
先找解密函数的调用点(往往在初始化阶段),用 Frida hook 它打印明文;或用调试器跟到解密循环,把结果回填到静态分析里。字符串加密只挡住第一眼,挡不住运行时。
一个 iOS App 的请求参数每次都不一样,你会怎么拆?
先抓包确认哪些字段在变(时间戳、随机数、签名),再定位生成逻辑:从网络回调往上追,或 hook 常见加密库。最后用同样输入复算,能对上才算拆明白。先分类字段,再逐个攻破。

31.9八、抓包与调试(12 题)

这一组几乎是面试里的"实操过滤器":真抓过包的人,会先问代理有没有收到请求;只背过概念的人,会直接说"pinning"。

开始抓包前,要先确认哪三件事?
设备与目标是否可控、流量是否被加密或做了固定、代理是否真的生效(证书、网络、App 是否遵循系统代理)。这三件没确认,抓到的"没有请求"多半是假象。
HTTPS 抓包失败,最常见的原因有哪些?
证书没装或没被信任、App 做了证书固定、App 不走系统代理(自己实现网络栈或用 QUIC)、双向认证、用了非 HTTP 协议。按这个顺序排查,能覆盖大多数情况。
代理模式和 VPN 模式抓包有什么区别?
代理模式只接管遵循系统代理设置的流量,配置简单但会漏;VPN 模式在系统层接管 TCP 和 UDP,能抓到不走代理的 App,但要自己处理证书和路由。抓不到就先换 VPN 模式试试。
Android 7 以后,为什么装了用户证书还是抓不到?
从 Android 7 起,targetSdk 24 及以上的 App 默认只信任系统证书,用户安装的 CA 不生效,除非 App 在 network_security_config 里显式声明信任用户证书。这是默认收紧,不是你证书装错了。
在自己设备上,怎么让 App 信任代理证书?
可以 root 后把证书装进系统证书目录,或改包在配置里加入信任用户证书,或用 hook 替换校验逻辑。三种都会改变运行环境,要评估完整性校验的影响。只在自己设备或授权目标上做。
App 能上网,但抓包工具里一片空白,怎么排查?
先看代理有没有收到任何请求,再确认 App 是否直连 IP 或走 QUIC,然后看 DNS 是否被绕过、有没有 VPN 冲突、证书是否导致连接直接失败。空白通常意味着根本没走你的代理。
抓到的请求原样重放却失败了,可能是什么原因?
参数里有时间戳、一次性随机数或签名,过期或不匹配就拒;也可能是 token 一次性、设备绑定、服务端记录了请求序号。重放失败不代表抓错了,往往说明有防重放设计。
时间戳和随机数各自防什么?为什么要一起用?
时间戳限制请求的有效窗口,随机数保证同一请求只能用一次;只有时间戳,窗口内仍可重放,只有随机数,服务端要永久存记录。两者一起用,成本最低。服务端还要缓存已用过的随机数。
已经装了证书,为什么还是解密不了?
因为证书固定只信任特定证书或公钥,你的代理证书不在名单里,握手直接失败。装证书解决的是信任哪个根,解决不了只信这一张。这两个问题要分开看。
怎么判断一段流量是 HTTP/2 还是 HTTP/1.1?
看 ALPN 协商结果和帧格式:HTTP/2 是二进制帧(HEADERS、DATA),头部小写并用 HPACK 压缩;HTTP/1.1 是纯文本的请求行和头。看到 PRI 星号 HTTP/2.0 就是 h2c 的预检。
WebSocket 抓包和普通 HTTP 有什么不同?
先做一次 HTTP Upgrade 握手,之后变成双向帧,不再是一问一答;连接会长时间保持,帧可能带掩码和分片。工具需要支持 WebSocket 帧解析。看不到"响应"不代表没数据,可能在帧里。
一开抓包 App 就崩溃或断网,可能是什么原因?
客户端检测到代理或 VPN、证书校验失败直接退出、工具改写了请求导致签名不匹配,或工具不支持 HTTP/2、QUIC。也可能是 App 本身对网络异常处理很差。先分清是检测还是不兼容。

31.10九、风控与指纹(12 题)

这一组考的是判断力。答题时讲原理和检测思路,不讲具体绕过步骤,这是行业里默认的分寸。

设备指纹是什么?风控拿它做什么?
把设备与环境的多个特征组合成一个较稳定的标识,用来判断是不是同一台设备,进而发现多账号、批量操作和异常环境。它不要求绝对唯一,要求稳定可比。指纹是认人,不是查户口。
为什么 IMEI、MAC 这些传统标识越来越不可靠?
系统权限收紧,Android 10 起普通应用拿不到 IMEI,MAC 也默认随机化;用户还可以重置广告标识。于是厂商转向 OAID、Android ID 加软件层特征组合。单一硬件 ID 的时代基本结束了。
Android ID 和 OAID 有什么区别?
Android ID 是设备首次启动生成的 64 位十六进制串,同设备同签名应用一致,恢复出厂会变;OAID 是国内移动安全联盟的广告标识,可由用户重置,需要通过 SDK 获取。一个偏系统,一个偏广告。
为什么说指纹的"一致性"比"唯一性"更重要?
风控要判断的是这次和上次是不是同一台设备。如果每次请求指纹都在变,就说明环境被改过或数据是拼的;唯一性太强反而容易误伤真实用户,还带来合规风险。稳定压倒独特。
常见的一致性矛盾有哪些?举两个例子。
UA 声称是 Android 14 的新机型,但图形渲染器是很老的型号;时区是 UTC 而语言是 zh-CN;屏幕分辨率和像素密度对不上;声称真机但传感器列表为空。矛盾比单个异常值更有说服力。
环境检测一般在检测什么?
检测运行环境是否可信:是否 root 或越狱、是否模拟器、是否多开或改机、是否装了 hook 框架、是否处于调试状态、是否有代理或 VPN。它不判断你是谁,判断你的环境像不像真的。
为什么改机工具容易被识别?
多数工具只改表层属性(系统属性、build 配置),但底层硬件、传感器数据、图形信息和系统调用行为对不上,一致性检查就会暴露。改得越全,成本越高。改一个字段容易,改一整套环境很难。
风控为什么不能只看设备,还要看行为?
设备可以换、可以改,但人的操作节奏难伪装:请求间隔、点击轨迹、页面停留、操作顺序、失败后的重试习惯。行为特征组合起来更能区分人和脚本。设备是静态的,行为是动态的。
什么是设备关联?它解决什么问题?
把设备、账号、IP、支付工具、行为特征连成图,找出共用关系,比如同一设备登录多个账号、同一支付方式绑定多台设备。单点看不出来,连起来就清楚了。团伙的特征是共用,不是相似。
为什么"每个请求都带全量设备信息"反而可疑?
真实客户端通常按场景传必要字段,值也会随环境变化;每次都传得又全又完全一致,更像是脚本按模板拼出来的。过度完整、过度稳定本身就是特征。正常的数据是有噪声的。
指纹做得太细会有什么代价?
会误伤真实用户:换电池、升级系统、装新应用都可能让特征变化,导致正常用户被拦。通常要分层:强特征用于拦截,弱特征用于加分,并保留人工复核。风控是在误伤和漏放之间找平衡。
采集设备信息时,合规上要注意什么?
最小必要、告知同意、能哈希不存明文、可撤回、有保存期限;设备标识属于个人信息,采集要有明确目的,不能超范围使用或转卖。技术上拿得到,不等于法律上能用。

31.11十、工程与合规(12 题)

最后一组不考知识点,考的是做事方式。面试官问这些,是想知道你会不会把一件小事做成事故。

接到一个自动化任务,第一步该做什么?
先明确目标和边界:要拿到什么数据、允许对哪些资产操作、失败怎么回滚。再看目标有没有公开 API 或书面授权,最后才动手写代码。方向错了,写得越快错得越远。
为什么脚本要尽量做到幂等?
网络会超时、会重试,同一操作可能被执行两次。幂等意味着重复执行结果一致,不会重复下单、重复扣款、重复写入。用唯一键或状态机保证只生效一次。
重试策略该怎么设计?
只对可重试的错误重试(超时、服务端 5xx),用指数退避加随机抖动,设最大次数和总超时,并区分幂等与非幂等操作。不加抖动,所有客户端会同时打回来。
并发一高就被限流或封禁,怎么处理?
按目标承载能力限速、用队列削峰、错峰执行,并监控错误率动态降速。并发不是越高越好,超过阈值只会让成功率下降。稳定性优先于速度。
日志该记什么,不该记什么?
记请求 ID、时间、状态、耗时、错误码和关键分支;不记明文密码、完整 token、身份证等敏感信息,必要时脱敏或只存哈希。日志本身也是泄露源。
密钥硬编码在代码里有什么问题?
会随代码仓库和安装包一起泄露,而且换密钥要重新发版,无法轮换。应放在密钥管理服务或环境变量里,按最小权限发放并留审计。硬编码的密钥等于公开的密钥。
灰度发布和影子流量各解决什么问题?
灰度先放小比例流量验证新逻辑,出问题影响面小;影子流量把真实请求复制一份给新逻辑但不返回结果,用来对比行为差异。一个控影响面,一个控正确性。
单测、联调、线上验证,各验证什么?
单测验证函数逻辑和边界,联调验证接口契约和字段,线上验证真实环境(网络、证书、风控、限流)。三层都过才算可用。本地跑通不等于线上能跑。
线上出问题,你的排查顺序是什么?
先定范围(全部还是个别、什么时候开始),再看最近变更,然后看日志和指标定位环节,最后本地复现。不要一上来就改代码。先缩小范围,再动手。
什么是"最小必要"?举一个采集上的例子。
只采集达成目的所必需的最少信息。比如只需要判断是不是同一台设备,就不该上传通讯录、短信和精确位置;能用聚合值就不用原始值。多收的每一条都是负债。
做安全研究的授权边界在哪?
只做自有设备、书面授权目标和公开靶场;不碰未授权的第三方系统,不窃取或泄露数据,不做批量注册、薅取优惠这类伤害业务的动作。技术能力不等于许可。
面试官问"你怎么保证方案可靠",该怎么答?
按目标、方案、取舍、验证、回滚五段答:先说清要达到什么,再说为什么选这个方案,主动讲代价和边界,给出验证方法和失败后的退路。能讲清取舍,比说"没问题"可信得多。
动手练
  • 每天遮住答案抽 10 题,用手机录音说一遍,再回听哪里卡住。
  • 把每组的最后四题抄到纸上,只写关键词,第二天凭关键词复述完整答案。
  • 找一道答不上来的题,回到对应章节读十分钟,再用自己的话写三行结论。
  • 找一个不懂技术的朋友,用五分钟给他讲清"为什么装了证书还是抓不到包"。
三次握手Three-way HandshakeTCP 建立连接时双方各确认一次收发能力的三步过程。
前向保密PFS长期私钥泄露后,历史会话仍无法被解密的性质。
证书链Certificate Chain站点证书、中间 CA、根证书逐级验签的信任路径。
粘包Sticky PacketTCP 字节流没有消息边界,多条消息被合并或拆开的现象。
证书固定Pinning客户端只信任指定证书或公钥,拒绝其他任何证书。
设备指纹Device Fingerprint多个设备与环境特征组合成的稳定标识。
幂等Idempotent同一操作重复执行多次,结果与执行一次相同。
重放攻击Replay Attack把截获的合法请求原样再发一次,骗取服务端执行。
你现在应该能回答 为什么三次握手不能两次?哈希和加密差在哪?装了证书为什么还抓不到包?JWT 为什么撤不掉?指纹为什么要看一致性?如果这五个能用自己的话讲清,这一页就过关了。
第 32 章 · 附录

学习路线:四阶段、十二周与岗位地图

很多人不是学不会,而是不知道下一步该学什么:今天看 TLS,明天看逆向,后天又去装工具,三周之后什么都没留下。这一页给一张能照着走的表:四个阶段、十二周、每天一小时,最后落到五类岗位各自要什么。你可以把它当成课表,也可以当成验收清单。

四阶段十二周计划每天一小时 三条路线岗位地图验收标准
先记住这一句 学习协议分析不是"把 36 页读完",而是每两周交出一个能给别人看的东西。有产出物,你才知道自己是真的会了,还是只是看懂了。
人话版 这页就是一张课表:先看懂,再会抓,然后会拆,最后能自己从头复现一遍。照着周次做,别跳步,也别同时开三条线。

32.1先看清整条路:四个阶段

全站 36 页看起来很多,但能力其实只有四层。四层像盖楼:下面一层没打牢,上面一层看再多也记不住。

第一层是看懂:知道数据从手机到服务器要经过哪些层,每一层加了什么。第二层是会抓:能把一次真实请求完整录下来,并逐行解释。第三层是会拆:加密或混淆之后,还能把逻辑还原出来。第四层是会复现:换一台机器、换一个账号,你写的脚本仍然能跑通。

学习的四个阶段:看懂、会抓、会拆、会复现 ① 看懂 分层 编码 加密 签名 第 1–5 章 ② 会抓 代理 证书 读懂请求 第 6–10 章 ③ 会拆 静态 动态 定位算法 第 11–15 章 ④ 会复现 独立跑通 写成脚本 第 16–21 章 四个阶段是四种能力,不是四个章节:能看懂不等于会抓,会抓不等于会拆 每一阶段的过关标准只有一个:不看资料,能独立做一遍 允许回退:拆不动的时候,往往是抓包还没抓明白
图 32-1 四阶段是一条单行道,但可以回头补课
阶段核心能力过关标准
看懂解释分层、编码、加密、签名能用自己的话讲给外行听
会抓完整录下一次请求并逐行解释换一台手机能照做
会拆在加密或混淆后还原逻辑能说出函数的输入和输出
会复现把过程写成可重复的脚本别人照文档能跑通
补充 阶段之间不是严格顺序。你可能在"会拆"卡住,回头发现是"会抓"没打牢。这时候正确的动作是回到上一阶段补两小时,而不是硬啃更难的资料。

32.2十二周计划:每周一个产出物

下面这张表按每周 5 到 7 小时设计,十二周大约 80 小时。它的重点不是"学完",而是每周都留下一个可以检查的痕迹。产出物可以是一张图、一份表格、一段脚本,不必漂亮,但必须是真的。

周次主题产出物验收标准
第 1 周分层与封装手画一张封装图能说出每层加了什么头
第 2 周编码与字节序手算三个进制转换全对,能解释大小端
第 3 周对称加密与哈希一张算法对照表能说出哪些不能当加密
第 4 周签名与证书一份证书字段解读能指出签发者、有效期、公钥
第 5 周TCP 与 UDP一次握手抓包记录能标出 SYN、SYN-ACK、ACK
第 6 周DNS 与 HTTP一份请求头逐行注释每个头都能解释作用
第 7 周TLS 握手一张握手时序图能说出两个随机数的用途
第 8 周抓包工具链一份可复用的抓包清单换设备也能照着做
第 9 周序列化格式同一份数据的三种写法能互相对照字段
第 10 周协议逆向方法一个接口的字段表写清字段名、类型、是否必填
第 11 周静态与动态分析一个函数的伪代码笔记能说出输入输出和分支
第 12 周完整复现一个可运行脚本别人照文档能跑通

这张表有三个设计原则,理解了可以自己改:

  • 先宽后深:前四周把地基铺满,不追求任何一项精通;
  • 每周都能验收:产出物必须能被别人看到,不能是"我心里懂了";
  • 最后一周做整合:把前面十一周的东西串成一次完整复现。
注意 进度落后时,砍产出物的规模,不要砍动手的环节。把"完整脚本"降级成"能跑通一个接口",也比只看两小时视频强。

32.3每天一小时怎么安排

每天一小时,最容易犯的错是全部用来输入:看视频、读文章、刷别人的分析。输入很舒服,但不会留下能力。比较稳的分配是四段:

每天一小时的四段分配:复习、学新、动手、记录 复习 10 分钟 学新内容 25 分钟 动手做 20 分钟 记录 5 分 先复习再学新:昨天的内容忘得最快,10 分钟能救回大半 动手那 20 分钟不能省:看十遍不如自己跑一遍 最后 5 分钟写三行:学了什么、哪里不懂、明天先做什么 周末留一次两小时整块时间,专门做完整复现
图 32-2 一小时里,输入只占 25 分钟,剩下都是复习和动手
时段做什么为什么这样排
前 10 分钟合上资料,先自问昨天的三个问题先暴露遗忘,再补才有效
中间 25 分钟只读一小节,读完立刻合上限制输入量,避免看完就忘
后 20 分钟抓一个包、跑一条命令、画一张图把知识变成肌肉记忆
最后 5 分钟写三行笔记,标出不懂的点给明天留一个明确的起点

如果某天只有二十分钟,就只做两件事:复习十分钟,动手十分钟。宁可少学一点,也不要整天只看不动手。

32.4三条路线:安全入门、移动端逆向、风控反欺诈

前六周的地基对三条路线都一样:分层、编码、密码学、HTTP、TLS、抓包。第六周之后开始分叉。分叉不是换赛道,而是把练习对象换成你真正想做的方向。

路线一

安全入门

重点在 Web 与内网:请求怎么被篡改、权限怎么被绕过、日志怎么留痕。产出是一份授权范围内的测试报告。

路线二

移动端逆向

重点在 App:静态看结构,动态看行为,最后用真实流量对拍。产出是一个自己 App 的接口字段表。

路线三

风控与反欺诈

重点在数据与规则:设备指纹、行为序列、误伤与漏放。产出是一份规则说明加误伤分析。

路线第 7–12 周重点最终产出
安全入门Web 安全、认证授权、API、云原生一份带证据的测试报告
移动端逆向Android、iOS、加固、证书固定一个接口的完整字段表
风控反欺诈设备指纹、登录风控、检测对抗一份规则与误伤分析
注意 三条路线不要同时学。它们的术语体系不一样,混着学会让你在每个方向都停在入门。先选一条走完十二周,另外两条当作选修。

32.5岗位地图:五类岗位各自要什么

学会技能之后,落到工作上通常是五类岗位。它们共享同一套地基,但每天做的事差别很大。看清"日常在做什么",比看岗位名字有用。

五类安全岗位共享同一套地基 共同地基 网络 密码学 HTTP 渗透测试 Web 与内网 报告与证据 授权边界 安全研发 代码审计 加密实现 开发流程 逆向工程 汇编与调试 脱壳 反混淆 耐心 风控策略 数据与规则 误伤与漏放 对抗意识 合规审计 法规条文 数据分级 沟通 岗位之间可以迁移:渗透转安全研发、逆向转风控都很常见 迁移时补的是业务知识,不是重新学一遍协议
图 32-3 五类岗位共享同一套地基,分叉的是每天做的事
岗位日常在做什么入门门槛
渗透测试按授权范围找问题、复现、写报告网络与 Web 基础、写作能力
安全研发写加密、鉴权、审计与防护工具会写代码、懂协议细节
逆向工程拆 App、定位算法、输出分析结论汇编、调试、长期专注
风控策略定规则、看误伤、做攻防迭代数据敏感度、业务理解
合规审计对照法规查数据处理流程条文理解、跨部门沟通

很多人以为"证书越多越好"。其实招聘方更在意两件事:你独立做过什么,以及你能不能把过程讲清楚。证书能证明你学过,产出物才能证明你会做。

32.6学习资源类型:书、文档、靶场

资源不在多,在于每种只留一个主力。下面五类各有用处,也各有坑。

类型怎么用常见坑
教材选一本当主线,按目录推进同时开三本,每本都停在第三章
官方文档带着具体问题去查字段和参数一上来通读,读两页就放弃
靶场按难度刷,每做完一题写记录只刷简单题,不总结
他人分析学思路和排查顺序把结论当标准答案背下来
源码带着一个问题读一个函数从头读到尾,三天后放弃
技巧 每存一个资料,就在文件名或笔记里写一句"我为什么存它"。写不出来的,直接删。收藏夹不是知识库,是待办清单。

32.7怎么判断自己真的学会了

"看懂了"是一种很不可靠的感觉。真正可靠的判断有三条,一条比一条严格。

检验是否学会的三条标准:复述、复现、排错 ① 复述 用自己的话讲给外行 不许用术语挡回去 ② 复现 换台机器还能跑通 不靠上次的临时环境 ③ 排错 出错能定位到哪一层 不是靠重启和重装 三条都过才算学会;只过第一条,说明你只是看懂了 排错最能检验理解深度,因为它没有标准答案可背
图 32-4 复述、复现、排错,三条都过才算真的会

复述:找一个人,用三分钟讲清"TLS 为什么需要两个随机数"。如果他听懂了,说明你懂了;如果你只能说"就是防止重放",说明你只是记住了结论。

复现:把电脑重装,或者换一台设备,照着你自己写的笔记重做一遍。中间卡住的每一步,都是你没真正掌握的地方。

排错:故意制造三个错误,比如证书没装、端口写错、时间不同步,看自己能不能在不查资料的情况下定位。能定位到"哪一层出了问题",才算理解结构。

自查清单
  • 能不能不用"加密""签名"这两个词,解释一次登录发生了什么?
  • 能不能在十分钟内,从零配好一次抓包?
  • 拿到一段看不懂的乱码,能不能列出三种可能原因?
  • 脚本报错时,第一反应是看日志还是重装环境?

32.8常见放弃原因与对策

放弃通常不是因为难,而是因为某一个小坎没人告诉你怎么办。下面六种最常见。

原因典型表现对策
贪多同时开五本书、三套视频只留一条主线,其余全部移出视线
跳步没学分层直接看 TLS回到前一阶段补两小时,再往上走
只收藏收藏两百篇,一篇没打开收藏时写一句"为什么存"
卡环境装工具装了三天先用现成环境或在线靶场,跑通再折腾
没反馈不知道自己做对没有每周交一个产出物给人看
怕看不懂一遇公式就合上书先记住结论和用途,公式留到第二遍

还有一个隐性原因:把"学安全"当成"看热闹"。看别人抓包、脱壳、写脚本很爽,但那是别人的过程。你必须自己经历一次从失败到成功的完整循环,能力才会长在身上。

32.9两个最大的误区

误区一:收藏等于学会。收藏的动作只证明"我现在不想看"。真正学会的标志是能在没有资料的情况下复述和操作。判断方法很简单:随机抽一条收藏,合上手机,讲三分钟。讲不出来,就说明它只是躺在收藏夹里。

误区二:只看不做。视频和文章会把复杂过程剪掉,只留下顺畅的部分。你自己动手时遇到的报错、超时、证书错误,才是真正要学的内容。看十遍不如做一遍,做一遍不如讲一遍。

很多人以为"等我基础打好再动手"。其实基础就是在动手里打出来的:先做一个很小的任务,遇到缺什么补什么,比按目录啃完一本书快得多。

自测:四个阶段分别是什么?各自的过关标准是什么?
看懂、会抓、会拆、会复现。过关标准依次是:能用自己的话讲给外行、换台设备能照做、能说出函数的输入输出、别人照文档能跑通。标准都是可验证的动作,不是"我感觉懂了"。
自测:每天一小时为什么要留 10 分钟复习、5 分钟写笔记?
复习放在最前面,是因为遗忘在学完的当天最快,先自问能暴露漏洞。写笔记放在最后,是为了给明天留一个明确起点,避免第二天重新想要学什么。输入只占 25 分钟,其余都用于巩固和动手。
自测:三条路线能不能同时学?
不建议。三条路线的术语体系和练习对象不同,混着学会让每个方向都停在入门。正确做法是前六周共用一套地基,第六周后只选一条走完十二周,其余当选修。先深后广,比同时铺开更省时间。
自测:怎么判断自己是真的学会了,而不是看懂了?
三条依次检验:能不能复述给外行、能不能换台机器复现、能不能在出错时定位到具体层。排错是最硬的检验,因为它没有标准答案可以背,只能靠对结构的理解。
四阶段Four Stages看懂、会抓、会拆、会复现,四种可验证的能力。
产出物Deliverable每周留下的可检查成果,如图、表、脚本、报告。
验收标准Acceptance判断产出物合格的具体条件,必须能被别人检查。
复现Reproduce在另一台设备上照文档重做一遍并得到同样结果。
靶场Lab专门用来练习的合法环境,允许你随便试错。
岗位地图Role Map五类安全岗位各自的能力要求与日常任务。
你现在应该能回答 四阶段分别是什么?十二周里每周要交什么?每天一小时怎么切?三条路线怎么选?五类岗位各自看重什么?如果这些问题你能不看笔记答出来,这张路线图就已经变成你自己的了。
第 33 章 · 附录

速记卡:一页刷完核心概念

前面 30 多页讲的是"为什么",这一页只留"是什么"。90 张卡按九个主题排好,每张卡一个问题和一句话答案。用法不是从头读到尾,而是遮住答案先自问,答不出的做记号,按间隔重复刷。手机上一次刷十张,比坐着看两小时更有效。

90 张卡9 个主题先自问再看 间隔重复手机友好可反复刷
先记住这一句 速记卡的价值不在"读",而在先回忆、再核对。直接翻开答案看十遍,不如合上答案想一遍,哪怕想错了。
人话版 这就是一副扑克牌:先看问题自己答,答完再点开对答案。答错的放回去,答对的往后推,刷三轮基本就记住了。

33.1怎么用这 90 张卡

记忆有一个反直觉的规律:费力回忆一次,比轻松阅读五次更牢。所以刷卡的正确动作是"先遮住、再回答、最后核对",而不是顺着往下读。

间隔重复:复习间隔逐渐拉长 第 1 天 学 第 2 天 复习 第 4 天 复习 第 7 天 复习 第 15 天 复习 每次复习都把遗忘曲线拉回原点,但间隔可以越拉越长 答不出的卡放回当天,答得出的卡往后推一天
图 33-1 间隔重复:在快要忘的时候复习,效率最高
刷一张卡的四个动作:遮住、自问、核对、安排 遮住答案 只看问题 先自己说 出声或默念 点开核对 找差在哪 安排下次 推后或当天 答对不是目的,找到"自己漏了哪一句"才是目的 一张卡超过二十秒还没答出,就直接看答案,不要耗
图 33-2 刷卡四步:遮住、自问、核对、安排下次

每天刷多少张合适?新卡十张,旧卡二十张,大约十分钟。三轮之后,把答错的卡单独抽出来,只刷那一小叠。

轮次时间做法
第一轮当天新卡十张,全部过一遍
第二轮第二天只刷昨天答错的,加新卡十张
第三轮第四天混合刷,答对的推后
第四轮第七天随机抽二十张,检查长期记忆
补充 卡片上的答案都刻意压到一句话。如果你发现某张卡怎么都记不住,多半不是记性问题,而是你还没理解它背后的原因。回到对应章节补十分钟,比反复背十遍有效。

33.2网络:地址、连接与传输

这一组解决"数据怎么从 A 到 B"。答不出时,先想寄快递:地址、门牌、房间号。

OSI 七层从上到下是哪七层?
应用、表示、会话、传输、网络、数据链路、物理。
TCP 和 UDP 最大的区别?
TCP 有连接、可靠、按序;UDP 无连接、不保证到达。
三次握手做了什么?
同步双方序号,确认两个方向都能通。
端口的作用是什么?
在同一台机器上区分不同的程序。
DNS 是干什么的?
把好记的域名翻译成机器用的 IP 地址。
三个私有 IP 段是哪些?
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。
NAT 解决什么问题?
让多台内网设备共用一个公网 IP 上网。
MTU 是什么?
链路一次能传的最大数据单元,以太网常见 1500 字节。
为什么断开 TCP 要四次挥手?
两个方向要各自关闭,各发一次、各确认一次。
CDN 的作用是什么?
把内容缓存到离用户更近的节点,减少延迟。

33.3密码学:加密、哈希与密钥

这一组解决"别人看不懂、改不了"。记住一条主线:对称快、非对称慢、哈希不可逆。

对称加密的特点?
加解密用同一把钥匙,速度快,但钥匙分发是难题。
非对称加密的特点?
公钥加密、私钥解密,速度慢,主要用来解决密钥分发。
哈希有哪三个关键性质?
单向不可逆、输出定长、难以找到碰撞。
为什么密码不能明文存储?
数据库一旦泄露,所有人的密码直接暴露。
加盐解决什么问题?
让相同密码得到不同哈希,挡住彩虹表。
AEAD 是什么?
加密的同时校验完整性,例如 GCM 模式。
为什么 ECB 模式不安全?
相同明文块会产生相同密文块,泄露结构。
为什么随机数不能用时间戳?
时间可预测,等于把密钥的一部分告诉别人。
数字签名用哪把钥匙?
发送方用私钥签名,任何人用公钥验证。
密钥是不是越长越好?
不是,算法和实现错了,长度再长也没有意义。

33.4TLS:握手、证书与加密通道

这一组解决"HTTPS 到底怎么安全"。核心只有一句:用非对称握手,换成对称密钥,再加密传输。

TLS 解决哪三件事?
机密性、完整性、身份认证。
证书里最关键的三样东西?
公钥、域名、签发者的签名。
握手为什么要两个随机数?
双方各出一个,拼成会话密钥,避免被预测或重放。
会话密钥是对称还是非对称?
对称;非对称只用在握手阶段交换材料。
SNI 是什么?
握手时明文告诉服务器要访问哪个域名。
证书固定(pinning)是什么?
客户端只信任指定的证书或公钥,不信任系统根证书。
为什么中间人抓包会失败?
客户端不信任代理证书,或者做了证书固定。
TLS 1.3 的主要改进?
一轮往返完成握手,删掉了不安全的旧套件。
前向保密是什么意思?
长期私钥泄露,也解不开以前录下的流量。
为什么抓到的 HTTPS 是乱码?
你没有这次会话的对称密钥。

33.5HTTP:请求、响应与状态

这一组解决"网页和接口在说什么"。记住结构:起始行、头、体。

GET 和 POST 的核心区别?
语义不同:GET 取数据,POST 提交数据;两者都能带参数。
200、301、403、404、500 分别是什么?
成功、永久跳转、禁止访问、不存在、服务器错误。
Header 和 Body 分别放什么?
头是说明,体是内容。
Cookie 和 Token 的区别?
Cookie 由浏览器自动携带,Token 通常手动放进请求头。
什么是幂等?
同样的请求执行多次,结果和一次相同。
HTTP/2 最大的变化?
一个连接上多路复用,多个请求并行。
Content-Type 干什么用?
告诉对方请求体的格式,例如 JSON 还是表单。
为什么一次点击会有很多请求?
页面会加载接口、图片、脚本、埋点等多个资源。
301 和 302 的区别?
301 永久,302 临时,缓存行为不同。
REST 的核心想法?
用 URL 表示资源,用方法表示动作。

33.6编码:字节、进制与格式

这一组解决"看起来像乱码的东西到底是什么"。先分清编码、加密、压缩是三件事。

ASCII 和 UTF-8 是什么关系?
UTF-8 兼容 ASCII,英文字符仍然只占一个字节。
Base64 是加密吗?
不是,只是编码,任何人拿到都能还原。
URL 编码解决什么?
把空格、斜杠等特殊字符变成 %XX 形式。
为什么十六进制常用?
一个字节正好两位,肉眼可读,便于对照。
大端和小端是什么?
多字节数字在内存里的两种存放顺序。
JSON 和表单的区别?
JSON 有数字、布尔等类型,表单基本都是字符串。
Protobuf 的优点和代价?
体积小、解析快,但必须有 schema 才能读懂。
中文为什么会乱码?
编码时用的字符集和解码时用的不一致。
Unix 时间戳 1729900000 是什么?
从 1970 年起算的秒数,注意有时是毫秒。
压缩和加密能一起用吗?
可以,通常先压缩再加密,顺序反了会压不动。

33.7Android:包、代码与运行时

这一组解决"安卓 App 里有什么、怎么拆"。三条线:DEX、so、运行时。

APK 里主要有什么?
DEX 字节码、资源文件、原生 so、清单文件。
DEX 是什么?
安卓的字节码格式,可以用 jadx 反编译成 Java。
smali 是什么?
DEX 的可读汇编形式,适合做小范围修改。
so 文件是什么?
C 或 C++ 编译出的原生库,关键算法常藏在这里。
Frida 是干什么的?
在运行时拦截函数,查看真实入参和返回值。
root 的作用是什么?
给调试工具更高权限,能读写系统目录和进程。
加固通常做了什么?
加密 DEX、改加载流程、加反调试和完整性校验。
Android 7 之后用户证书有什么变化?
App 默认不再信任用户安装的证书。
adb 是什么?
电脑和安卓设备之间的调试通道。
为什么要脱壳?
壳里的 DEX 在运行时才解密,静态看不到真实代码。

33.8iOS:包、可执行文件与调试

这一组解决"iOS 和安卓哪里不一样"。核心差异在签名、包格式和证书信任。

IPA 是什么?
iOS 应用包,本质是一个 zip 压缩包。
Mach-O 是什么?
iOS 和 macOS 的可执行文件格式。
越狱的作用是什么?
突破签名限制,允许安装调试和注入工具。
砸壳是什么意思?
从内存里导出解密后的可执行文件。
Objective-C 的方法调用怎么表示?
通过 objc_msgSend 加 selector 动态派发。
Swift 方法名为什么难读?
编译时做了名称修饰,符号被编码过。
Keychain 是什么?
系统提供的加密存储,用来放密码和密钥。
iOS 抓包为什么更难?
证书信任更严格,证书固定也更常见。
Frida 在 iOS 上怎么用?
越狱设备直接注入,或重打包后注入。
描述文件是什么?
安装测试包和证书时用的配置文件。

33.9风控:指纹、行为与处置

这一组解决"平台怎么判断你是不是机器人"。记住:风控不是一条规则,而是多层叠加。

设备指纹是什么?
由一组设备特征组合出的相对稳定的标识。
指纹为什么既要有稳定性又要能变?
太稳定容易被长期追踪,太易变会造成大量误伤。
行为风控看哪些东西?
点击节奏、滑动轨迹、操作顺序、停留时长。
什么是误伤?
把正常用户判成了风险用户。
什么是漏放?
把真正的风险用户放了过去。
规则和模型是什么关系?
规则可解释、管边界;模型覆盖长尾,通常叠加使用。
为什么换账号还会被关联?
设备指纹、网络环境等特征没有变。
环境检测通常查什么?
root、模拟器、多开、hook 框架、调试状态。
风控为什么也看时间?
深夜批量操作、固定间隔请求都很异常。
对抗会有结束的一天吗?
不会,特征会过期,攻防会持续迭代。

33.10合规:数据、授权与边界

这一组解决"什么事能做、做到什么程度"。技术再熟,越界就是另一回事。

个人信息处理的三个基本原则?
告知同意、最小必要、目的限定。
什么是最小必要?
只收集完成当前功能真正需要的信息。
为什么能哈希就不存明文?
泄露时降低数据被直接利用的价值。
做抓包测试的前提是什么?
自有设备,或者拿到书面授权。
为什么不能随便测别人的站点?
可能构成非法侵入或破坏计算机信息系统。
日志可以无限期保存吗?
不行,要按目的和法规设定保存期限。
数据出境要注意什么?
需要走相应的评估和合规路径。
什么是去标识化?
去掉能直接对应到个人的字段,但仍可能复原。
匿名化和去标识化的区别?
匿名化不可复原,去标识化仍存在复原可能。
发现漏洞后的正确做法?
先私下报告厂商,给修复时间,不公开利用细节。

33.11刷完之后怎么巩固

90 张卡刷完,只完成了"认得"。要变成"会用",还得做三件事:把卡上的词放回真实请求里、把相近的概念做成对照表、把答错的卡讲给别人听。

  • 放回真实请求:抓一个包,在请求里找出至少五个卡上的名词;
  • 做对照表:加密与签名、编码与加密、Cookie 与 Token,各写三行区别;
  • 讲给别人听:挑三张最容易混的卡,用生活比喻讲一遍。
注意 卡片是压缩过的结论,不能替代理解。比如"两个随机数防重放"这句话,背下来只要五秒,但为什么是两个、为什么不能只用一个,还是得回到 TLS 那一章看一遍。
自测:为什么刷卡要"先自问再看答案"?
因为费力回忆会强化记忆,而直接阅读只会带来"我见过"的错觉。先答错再核对,比顺着读五遍更牢。
自测:Base64、哈希、加密,三者最核心的区别是什么?
Base64 是编码,可逆且不需要钥匙;哈希是单向压缩,不可逆;加密是可逆的,但需要钥匙。看到一段乱码,先判断它属于哪一类,再决定怎么处理。
自测:TLS 里为什么会话密钥用对称,而不用非对称?
非对称运算慢,适合少量数据交换密钥;对称运算快,适合大量业务数据。握手用非对称协商出对称密钥,之后全程用对称加密。这是性能与安全的折中。
自测:设备指纹为什么不能只看一个字段?
单个字段容易伪造,也容易因为系统升级而变化。多个特征组合后,既能提高区分度,也能通过一致性检查发现异常。风控看的是组合与一致性,不是某一个值。
动手练
  • 今天只刷网络组十张,全部遮住答案自问一遍,记录答错的编号。
  • 把答错的卡抄成三行笔记:问题、我的答案、正确的一句话。
  • 明天先刷错题,再学下一组十张,不要一次刷完九组。
第 34 章 · 附录

常见误区:40 个似是而非的说法

学协议分析,最费时间的不是不懂,而是记了一堆听起来对、其实是错的结论。它们通常来自半句话、一张截图、一次偶然成功。这一页把 40 个高频误区集中拆开,每条都写清三件事:为什么会被误解、真相是什么、正确理解应该怎样。建议当工具用,遇到争论时回来查。

40 条6 个分组为什么会被误解 正确理解可自查反例思维
先记住这一句 判断一句话靠不靠谱,最快的办法是找它的反例和边界:它在什么条件下成立,在什么条件下失效。只记结论不记条件,就会变成误区。
人话版 这页就是"打假清单"。每条先给一个常见说法,再告诉你它哪里不严谨、应该怎么改口。看的时候先自己想一遍,再看答案,印象更深。

34.1密码学:七个最容易混的说法

密码学的误区有个共同点:把"解决一个问题"当成了"解决所有问题"。加密、签名、哈希各管一件事,混着用就会出漏洞。

加密与签名解决的是不同问题 ① 加密:让别人看不懂 发送方 明文 账号密码 密文 x9F2aC7e1B4 接收方 用密钥解开 ② 签名:证明是谁发的、有没有被改 发送方 + 私钥 对内容做签名 签名值 附在内容后面 任何人 用公钥验证 加密保护内容,签名保护来源和完整性,两者不能互相替代 签名不隐藏内容:签名过的文件仍然是明文,谁都能读 实际系统常常先签名再加密,两个都要
图 34-1 加密防偷看,签名防篡改和冒充,这是两条不同的线
误区:加密就等于安全。
真相:加密只防偷看,不防篡改、不防重放、不防冒充。为什么会被误解:看到一堆乱码,就以为别人拿它没办法。正确理解:安全是一组机制的组合:加密保机密,签名保完整,时间戳和随机数防重放,证书证明身份。少任何一环都可能被利用。
误区:哈希可以还原成原文。
真相:哈希是单向的,只能穷举候选再比对,不能从结果反推输入。为什么会被误解:网上有"哈希解密"网站,输入一个值真能查出原文。正确理解:那是查预先算好的表,只对常见弱口令有效。加盐、加长口令之后,这类表就失效了。
误区:Base64 是一种加密。
真相:Base64 只是编码,任何人拿到都能立刻还原。为什么会被误解:编码后变成一串大小写字母,看起来像密文。正确理解:它解决的是"二进制怎么放进文本协议",不解决保密。看到 Base64 先解码,不要当加密处理。
误区:密钥越长越安全。
真相:算法选错、模式用错、随机数可预测,密钥再长也没有意义。为什么会被误解:数字大听起来更可靠,容易被当成唯一指标。正确理解:先保证算法和实现正确,再谈长度。长度只需要满足当前的安全目标,过长只会增加计算成本。
误区:签名会把内容藏起来。
真相:签名不加密,内容仍然是明文,任何人都能读。为什么会被误解:签名和加密经常一起出现,名字又都很"安全"。正确理解:签名解决"谁发的、有没有被改",加密解决"谁能看"。需要保密时,签名之后还要再加密。
误区:自己设计一个异或加密就够用了。
真相:自制算法几乎一定能被破,尤其是短密钥和固定密钥。为什么会被误解:自己看不懂自己的密文,就以为别人也看不懂。正确理解:密码学算法的安全性要经过长期公开分析。工程上直接用成熟算法和成熟库,不要自己发明。
误区:随机数随便取一个就行。
真相:可预测的随机数等于把密钥的一部分送给攻击者。为什么会被误解:普通编程语言里的随机函数看起来已经很"随机"。正确理解:安全场景必须用密码学安全的随机源。时间戳、自增数、普通伪随机都不合格。

34.2协议:七个被简化过头的结论

协议类的误区通常来自"把某一层的结论套到另一层"。TCP 可靠,不代表应用层不会丢数据;HTTPS 安全,不代表内容可信。

误区:HTTPS 就是"加了密的 HTTP"。
真相:HTTP 的报文格式没有变,只是在外面套了一层 TLS 通道。为什么会被误解:名字只多了一个 S,看起来像同一套东西的升级版。正确理解:先完成 TLS 握手,再在加密通道里传 HTTP。抓包时看到的仍然是 HTTP 结构,只是被包在 TLS 记录里。
误区:TCP 一定比 UDP 好。
真相:实时音视频更怕延迟,宁愿丢几帧也不愿意等重传。为什么会被误解:"可靠"听起来总是优点。正确理解:选协议看场景:要完整、要顺序,用 TCP;要低延迟、能容忍丢包,用 UDP。很多实时协议在 UDP 之上自己做部分可靠性。
误区:端口开着就说明有漏洞。
真相:开放端口只说明有服务在监听,是否可利用要看版本和配置。为什么会被误解:扫描器常把 open 标成显眼的颜色。正确理解:端口是入口,不是漏洞。要结合服务指纹、版本、认证方式和实际响应判断。
误区:DNS 只用来把域名换成 IP。
真相:DNS 还承载邮件路由、域名验证、服务发现等信息。为什么会被误解:日常只接触网页访问这一种用法。正确理解:MX 记录决定邮件去哪,TXT 记录常用于验证和策略,SRV 记录用于服务定位。分析域名时这些记录都要看。
误区:HTTP/2 一定比 HTTP/1.1 快。
真相:多路复用减少了连接层的等待,但服务端处理和数据量才是主要瓶颈。为什么会被误解:版本号更大,直觉上应该更快。正确理解:HTTP/2 的收益取决于请求数量和网络条件。单个大文件下载,差别很小;大量小请求,收益明显。
误区:长连接一定比短连接省资源。
真相:长连接会占用内存、文件描述符,还需要心跳维持。为什么会被误解:少握手听起来总是划算。正确理解:高频请求适合长连接,低频请求用短连接更省。连接数上去之后,服务端的维持成本会很明显。
误区:IPv6 只是地址变长了。
真相:IPv6 还改了头部结构、邻居发现、地址自动配置等一整套机制。为什么会被误解:最直观的差别就是地址长度。正确理解:它更像一套重新设计的协议。排查 IPv6 问题时,不能照搬 IPv4 的经验。

34.3抓包:七个"抓不到就怪工具"的想法

抓包失败几乎都能按层定位。先确认有没有包,再确认能不能解密,最后才怀疑工具。顺序反了,就会在错误的地方浪费几小时。

抓包失败的分层排查:从应用层到链路层 层 你看到的现象 更可能的原因 应用层 明文请求 正常,可以直接读 TLS 层 一堆乱码 没装证书或没被信任 TCP 层 连接被重置 代理端口或转发配置错 网络层 一个包都没有 路由、VPN、抓错网卡 链路层 只有别的设备的包 抓错接口或没开混杂模式 从下往上排查:先确认有没有包,再确认能不能解密,最后才怀疑工具
图 34-2 抓包失败的五层原因,按顺序排查就不会乱
误区:抓不到包就是抓包工具坏了。
真相:多数情况是证书、代理、路由或网卡选错。为什么会被误解:工具只给一个"连接失败",不告诉你哪一层出错。正确理解:按层排查:有没有包、能不能解密、端口对不对、设备有没有走代理。工具本身出问题的概率最低。
误区:装上证书就能抓所有 App。
真相:很多 App 做了证书固定,只信任自己的证书或公钥。为什么会被误解:浏览器能正常抓,就以为 App 也一样。正确理解:证书固定是另一道门。需要单独分析客户端的校验逻辑,不能只靠装证书解决。
误区:抓到乱码说明加密很强。
真相:乱码只说明你没有这次会话的密钥,跟算法强度没有关系。为什么会被误解:乱码看起来很神秘,容易被当成"不可破解"。正确理解:要么让客户端信任你的证书,要么在客户端里拿到明文。乱码本身不提供任何安全性结论。
误区:手机连上代理,所有流量就都走代理。
真相:有些流量会走 VPN、QUIC 或直接连 IP,绕过代理设置。为什么会被误解:Wi-Fi 里填了代理,看起来是全局设置。正确理解:代理只对遵守系统代理配置的流量生效。要确认协议、端口和路由,必要时在应用层单独处理。
误区:抓包工具能改请求,所以什么都能改。
真相:能改,但改完可能签名对不上,被服务端直接拒绝。为什么会被误解:断点改包在工具里确实一键就能做。正确理解:先搞清签名覆盖了哪些字段。签名覆盖的字段一改,请求就废了;没覆盖的字段才有操作空间。
误区:抓一次包就能看懂协议。
真相:一次只能看到一组值,分不清哪些是常量、哪些是变量。为什么会被误解:单次请求的结构看起来很整齐。正确理解:至少抓三次,每次只改一个条件,对比哪些字段变了。变量、序号、时间戳都是这样找出来的。
误区:流量里没看到,就说明这件事没发生。
真相:可能发生在别的设备、别的网卡、别的协议上。为什么会被误解:只看了一个抓包窗口,就以为看到了全部。正确理解:先确认抓取范围:哪台设备、哪个接口、哪些协议。范围没覆盖到,看不到是正常的。

34.4逆向:七个高估或低估的判断

逆向的误区集中在两个方向:要么把反编译结果当源代码,要么把加固当成不可分析。真相在中间:它是成本问题,不是有无问题。

误区:反编译出来的就是源代码。
真相:那是近似还原,变量名、注释、部分结构都已经丢失。为什么会被误解:jadx 输出的 Java 看起来非常完整。正确理解:把它当参考,不当真相。关键逻辑要回到字节码或汇编确认,尤其是加密和校验部分。
误区:加了壳就没法分析。
真相:加固提高成本,但程序运行时必须解密才能执行。为什么会被误解:静态打开全是乱码,看起来无从下手。正确理解:转动态分析:找解密点、dump 内存、在关键函数上下钩子。壳保护的是静态,不是运行时。
误区:字符串搜不到,就说明程序里没有。
真相:字符串可能被加密、拆分拼接,或者运行时才生成。为什么会被误解:搜索框里输入关键词没有结果,很容易下结论。正确理解:从调用点反推:先找可疑函数,再看它引用了哪些数据。也可以动态跑起来,在内存里搜。
误区:汇编看不懂就学不了逆向。
真相:起步只需要看懂调用约定、几种跳转和常见指令。为什么会被误解:汇编教材一上来就是几百页,容易吓退。正确理解:带着问题学:先看懂一个函数的入参、返回值和分支,再逐步扩展。不需要一次学完。
误区:钩子挂上了,就等于拿到了算法。
真相:钩子只能看到入参和返回值,算法本身还在函数内部。为什么会被误解:能看到明文很兴奋,容易以为已经完成。正确理解:钩子是定位手段,不是终点。要复现还得静态读逻辑,或者构造多组输入反推规则。
误区:一个版本能跑,就永远能跑。
真相:App 更新会改字段、改偏移、改加固方式。为什么会被误解:脚本第一次跑通时特别顺。正确理解:记录版本号,用特征而不是固定偏移来定位,并准备一套快速回归的检查清单。
误区:逆向就是找漏洞。
真相:逆向是理解实现,找漏洞是另一项工作,两者方法有重叠但目标不同。为什么会被误解:两个词经常出现在同一篇文章里。正确理解:逆向回答"它是怎么做的",漏洞研究回答"它在哪一步做错了"。先理解,再判断。

34.5风控:六个把复杂问题说简单的说法

风控的本质是在误伤和漏放之间找平衡。任何"一招搞定"的说法,都忽略了另一边的代价。

风控的四层处置:规则、组合、模型、人工 单条规则 命中即拦 组合条件 多条件叠加 模型评分 看整体概率 分层处置 限流 验证 封禁 单条规则容易误伤,模型难以解释,实际系统通常叠加使用 处置也不是只有封号:验证码、限流、降权都是中间档
图 34-3 风控是多层叠加与分层处置,不是一条规则定生死
误区:风控就是封号。
真相:封禁只是最重的一档,前面还有限流、验证码、降权等处置。为什么会被误解:普通用户只在自己被封时感受到风控。正确理解:风控的目标是降低损失,不是封得越多越好。分级处置能在控制风险的同时减少误伤。
误区:设备指纹就是一个设备 ID。
真相:它是一组特征的组合,还要评估稳定性和一致性。为什么会被误解:接口里常常只有一个 device_id 字段。正确理解:那个 ID 可能由多个特征生成,也可能是本地随机生成后缓存的。判断是不是同一台设备,要看特征组合而不是单个值。
误区:改一个机型字段就能过环境检测。
真相:检测看一致性:机型、分辨率、系统版本、传感器数据要互相对得上。为什么会被误解:改完一个字段,界面确实变了。正确理解:单点修改会制造矛盾。风控找的往往不是"值对不对",而是"这些值放在一起合不合理"。
误区:规则越多越安全。
真相:规则多会带来误伤和维护成本,规则之间还会互相冲突。为什么会被误解:每加一条都感觉堵住了一个口子。正确理解:看命中率和误伤率,定期清理低效规则。规则的价值在于准确,不在于数量。
误区:有了模型就不需要规则。
真相:模型覆盖长尾,规则负责明确边界,实际系统通常并存。为什么会被误解:模型听起来更先进,像是规则的升级版。正确理解:规则可解释、可审计、响应快;模型泛化好但难解释。两者互补,不是替代关系。
误区:对抗一次成功就结束了。
真相:特征会过期,对方也会更新,攻防是持续迭代。为什么会被误解:第一次绕过成功时,感觉已经通关。正确理解:要建立监控和回归机制,把"这次成功"变成"下次还能快速定位"。

34.6工程与合规:六个容易被忽略的边界

技术能跑通,不等于事情能交付。工程讲可重复,合规讲可证明,这两件事都不在代码里。

误区:脚本能跑就行,不用写文档。
真相:不能复现的脚本,过两周连你自己都跑不起来。为什么会被误解:写的时候环境还在,记忆还热。正确理解:至少写清版本、依赖、参数、失败处理。文档不是给别人看的,是给未来的自己看的。
误区:日志越多越好。
真相:日志里常有个人信息,存得越多,泄露时的风险越大。为什么会被误解:排查问题时,日志多确实方便。正确理解:按目的采集,能脱敏就脱敏,设定保存期限。方便和安全要一起考虑。
误区:测试环境的数据可以随便用。
真相:测试数据也可能包含真实个人信息,同样受约束。为什么会被误解:觉得不是生产环境,就不算真实数据。正确理解:优先用造出来的数据;必须用真实数据时,做脱敏并限制访问范围。
误区:授权一次就永久有效。
真相:授权有范围、期限和用途,超出范围就不算授权。为什么会被误解:拿到一封授权邮件,就以为拿到了通行证。正确理解:每次测试前确认边界:哪些域名、哪些时间、允许做什么。范围变了要重新确认。
误区:开源软件没有法律风险。
真相:开源有许可证,商用、修改、再分发都有条款。为什么会被误解:能免费下载,就以为能随便用。正确理解:看 LICENSE:有的要求保留版权声明,有的要求衍生作品同样开源。用之前先确认。
误区:把数据删掉就算合规了。
真相:还要能证明收集、使用、删除的过程合法且可追溯。为什么会被误解:删除是最直观的动作。正确理解:合规看的是全流程:有没有告知、有没有同意、有没有最小必要、有没有记录。删除只是其中一步。

34.7怎么自己发现误区

误区不会自己消失,但可以用三个动作主动发现。第一个动作是找反例:听到一个绝对化的说法,先想有没有例外。第二个动作是问边界:它在什么条件下成立,条件变了还成立吗。第三个动作是做实验:能动手验证的,就不要只靠记忆。

还有一个便宜的办法:把结论改写成"在什么条件下,什么成立"。写不出条件的结论,多半是背下来的口号,不是理解。

综合自测:为什么"加密等于安全"是错的?
因为安全至少包含机密性、完整性、身份认证和防重放四个方面,加密只覆盖第一个。没有签名,内容可能被改;没有随机数,请求可能被重放;没有证书,对面可能是冒充的。
综合自测:抓包抓到乱码,应该先检查什么?
先确认抓包范围对不对,再确认客户端有没有信任你的证书,最后才考虑证书固定。顺序是从下往上:有没有包、能不能解密、是不是被固定。一上来就重装工具,通常解决不了问题。
综合自测:为什么风控不能只靠一条规则?
单条规则容易误伤正常用户,也容易被针对性地绕开。实际系统用多层叠加:规则管明确边界,模型覆盖长尾,处置分档。目标是整体损失最小,而不是某一条规则命中最多。
你现在应该能回答 加密和签名各防什么?抓包失败应该按什么顺序排查?反编译结果为什么不能当源代码?风控为什么不能只靠一条规则?授权为什么有边界?如果这五个问题你能各用三句话讲清,这一页的 40 条就已经变成你的判断力了。
动手练
  • 从这一页挑三条你最意外的误区,用一句话改写成"在什么条件下成立"。
  • 找一篇你收藏的分析文章,标出里面所有绝对化的说法,逐个问"有没有反例"。
  • 把"加密等于安全"讲给一个不懂技术的朋友,看他会不会追问"那改了怎么办"。
误区Misconception听起来合理、但缺少条件限定的结论。
反例Counterexample能让一个普遍结论失效的具体情况。
边界条件Boundary结论成立所依赖的前提,条件变了结论就可能变。
误伤False Positive把正常对象判成异常,风控里的主要成本之一。
漏放False Negative把异常对象判成正常,风控里的主要风险之一。
可复现Reproducible换环境、换时间仍能得到同样结果。
第 35 章 · 附录

关于本站、更新日志与离线使用

这一页说清四件事:这个站是给谁写的、内容边界在哪、怎么在手机和断网环境下用、以及它到底收集不收集你的数据。如果你只想知道怎么用,直接看第三节和第四节;如果你关心技术实现,第六节有完整说明。

站点定位更新日志三种离线方式 手机操作数据与隐私技术说明
先记住这一句 这是一个纯静态、无账号、无追踪的单文件教程:所有内容都在一个 HTML 里,不需要联网也能读,不会向任何服务器发送你的阅读数据。
人话版 这就是一本可以装进口袋的电子书。第一次打开需要联网,之后可以缓存、可以下载、也可以添加到主屏幕,断网照样翻。

35.1站点定位与内容边界

这个站面向技术基础很弱、从零开始的读者。它不假设你学过网络、写过代码、接触过密码学,所以每个概念都先用生活比喻讲一遍,再给技术定义。全站按"看懂、会抓、会拆、会复现"四个阶段组织,从网络分层一直讲到移动端逆向、设备指纹、风控与合规。

它不是一本速查手册,也不是一本工具说明书。工具会更新,命令会变化,但"数据怎么分层、签名为什么不能替代加密、抓包失败该按什么顺序排查"这些问题十年后仍然成立。本站优先讲这些不容易过期的部分。

覆盖不覆盖
网络分层、编码、密码学、TLS、HTTP具体产品的内部接口与私有字段
抓包、协议逆向、移动端静态与动态分析绕过他人系统风控的操作步骤
设备指纹、登录风控、检测与对抗原理批量注册、刷量等滥用方法
Web 与 API 安全、云原生基础、合规红线未授权目标的实战攻击
红线 全站技术内容只适用于自有设备、书面授权的目标、公开教学靶场。涉及个人信息时,强调告知同意、最小必要、能哈希不存明文。任何越界使用都与本站无关。

35.2版本与更新日志

站点从最初的一版讲义扩到现在的规模,主要经历了两次大改。下面这张表记录的是结构性变化,不记录错别字修正。

版本规模主要变化
v1.011 页初版:分层、编码、密码学、TLS、抓包、逆向入门
v1.521 页补充移动端、登录案例、排错手册与术语表
v2.036 页扩到 36 页;新增风控、合规、附录;手机端适配;支持离线

v2.0 的三项重点:

  • 页数翻倍:补齐 Web 与 API、云原生、检测对抗、法律红线,让"从底层到实战"这条线完整;
  • 手机端重排:段落更短、表格不超过四列、图可以点开放大、底部有固定工具栏;
  • 离线可用:整站打包成一个 HTML,既可以缓存,也可以下载,还可以添加到主屏幕。

35.3离线使用:三种方式

三种方式解决的是不同场景:只是想快点打开,用在线缓存;想拷到别的设备,用下载单文件;想天天看,添加到主屏幕。

离线使用的三种方式:在线缓存、下载单文件、添加到主屏幕 在线缓存 下载单文件 添加到主屏幕 第一次正常访问 浏览器自动存副本 断网也能打开 保存网页为文件 拷到任意设备 双击即开,完全离线 浏览器菜单选添加 桌面生成图标 像 App 一样全屏 三种方式可以同时用,互不冲突
图 35-1 在线缓存省事,下载单文件最稳,主屏幕入口最方便
方式适合场景注意
在线缓存同一个浏览器反复看清缓存后会失效
下载单文件拷到电脑、平板、U 盘文件较大,首次加载稍慢
添加到主屏幕手机上天天看需要浏览器支持

在线缓存依赖浏览器提供的离线能力:第一次访问时把页面存一份,之后优先从本地读。它不需要你注册、不需要登录,也不会上传任何内容。如果换了浏览器或清理了缓存,重新访问一次即可恢复。

35.4手机端使用说明

手机是主要阅读场景,所以界面做了几处专门设计。第一次用可能找不到,下面逐条说明。

操作怎么做效果
底部工具栏屏幕底部四个按钮目录、搜索、上一页、下一页
左右滑动在正文区域横向滑切换上一页或下一页
看图放大点图右下角的"放大"全屏查看,可滚动,按关闭退出
调字号顶部 A- 与 A+四档字号,刷新后仍保留

几个细节值得注意:

  • 左右滑动不会在图上生效,避免你看图时误翻页;
  • 表格过宽时可以在表格内部横向滚动,不会把整页撑开;
  • 字号设置只保存在本机浏览器,换设备不会同步;
  • 顶部进度条显示的是当前页的阅读进度,不是全站进度。
技巧 在手机上读图时,先点"放大"再双指缩放,比直接看缩略图清楚得多。图里的字号是按桌面宽度设计的,缩小后一定会糊。

35.5数据与隐私

这个站不收集任何数据。具体说:

  • 没有账号:不需要注册、登录,也没有任何表单提交;
  • 没有统计:不接入访问统计、不记录阅读行为、不生成用户画像;
  • 没有第三方请求:不加载外部字体、脚本、图片、广告;
  • 没有 Cookie 追踪:只在本机保存字号等偏好,不上传。

因为整站是一个 HTML 文件,所有样式、脚本、图示都内联在里面。断网打开时,浏览器不会向任何服务器发起请求,也就不存在"偷偷上报"的可能。你可以用浏览器的网络面板自行验证这一点。

补充 "不收集数据"也包括不收集搜索词。全站搜索是在你的浏览器里对已加载的文本做匹配,搜索内容不会离开你的设备。

35.6技术说明:单文件、纯 CSS 路由、无外部依赖

整站构建产物是一个 HTML 文件。样式写在文件内的样式表里,脚本也内联,36 个页面按顺序排在同一份文档中。没有构建服务器、没有数据库、没有接口。

单文件结构:样式、内容、脚本全部内联 一个 HTML 文件 样式:全部 CSS 内联 内容:36 个 section 依次排列 脚本:搜索、字号、离线保存 没有外部图片,图示全部是内联 SVG 无外链 无图片 无数据库 页面切换用纯 CSS 的 :target 选择器,不依赖前端框架 这也是页面内部不能出现额外 id 的原因:会抢占路由目标
图 35-2 单文件结构:样式、内容、脚本都在同一个文档里

页面切换用的是浏览器原生的 :target 伪类:地址栏的片段(例如 #page-tls)决定显示哪一页,CSS 负责把其余页面隐藏。这种做法的好处是:

  • 没有路由库:不需要加载任何框架,首屏更快;
  • 可以前进后退:浏览器的历史记录直接可用;
  • 可以分享定位:把链接发给别人,打开就是你正在看的那一页;
  • 降级友好:即使脚本被禁用,阅读和翻页仍然正常。

代价是:页面内部的任何元素都不能再写 id,否则会抢走 :target,导致整页消失。这也是内容规范里最严格的一条禁令。

35.7如何反馈与纠错

发现错误时,最有用的反馈不是"这里不对",而是指出具体位置、给出依据、说明应该改成什么。推荐的格式是三行:

# 反馈模板
位置:第 8 章 8.3 节,第二张图下方
问题:把会话密钥说成了非对称加密
依据:TLS 1.3 握手后用对称密钥传输应用数据
建议:改为"握手用非对称,传输用对称"

如果是文字错误,指出段落开头几个字就够定位。如果是图示错误,说明是哪张图、哪个方框。如果涉及事实争议,附上可查证的来源类型(规范文档、官方说明、可复现的实验),不要只给结论。

本站不提供实时客服,也不承诺响应时间。所有修正会累积到下一次版本更新,并在更新日志里体现结构性变化。

35.8免责声明与法律提示

本站内容仅用于安全学习、防御建设与授权测试。所有示例都限定在自有设备、书面授权目标或公开教学靶场。以下行为与本站无关,也可能违法:

  • 未经授权扫描、入侵、干扰他人系统;
  • 绕过他人产品的安全机制以获取数据或牟利;
  • 批量注册、刷量、薅羊毛等滥用行为;
  • 收集、买卖、泄露他人个人信息。

技术本身是中性的,但使用技术的行为有边界。不同地区的法律对"未经授权访问"的界定不完全相同,动手前请确认:目标是不是你的、有没有书面授权、授权范围包不包括你要做的动作。

红线 不确定是否越界时,先停下,去确认授权。一次越界可能同时带来法律责任和职业风险,代价远高于一次测试的收益。
自测:本站为什么能做到断网阅读?
因为整站是一个 HTML 文件,样式、脚本、图示全部内联,没有外部依赖。第一次访问后浏览器可以缓存,或者直接下载文件本地打开。断网时不会发起任何请求。
自测:页面内部为什么不能写 id?
因为路由用的是纯 CSS 的 :target 选择器,靠地址栏片段匹配页面 section 的 id。页面内部再出现 id,会抢走 :target,导致整页显示异常甚至消失。这是硬性规范。
自测:本站收集哪些用户数据?
不收集任何数据:没有账号、没有统计、没有第三方请求、没有追踪 Cookie。字号等偏好只保存在本机浏览器,搜索也在本地完成,不会上传。
自测:发现内容错误时,怎样反馈最有效?
给出三样东西:具体位置、问题是什么、依据或建议改法。只说"这里不对"无法定位,也无法判断该不该改。涉及事实争议时,附上可查证的来源类型。
动手练
  • 在手机上把本站添加到主屏幕,断网后打开,确认还能翻页。
  • 打开浏览器的网络面板,刷新一次,观察有没有第三方请求。
  • 点开任意一张图,用"放大"看清楚最小的那行注释。
单文件Single File样式、脚本、内容全部内联在一个 HTML 里。
离线缓存Offline Cache浏览器把页面存一份,断网时优先读本地。
:target 路由Target Routing用地址栏片段匹配元素,纯 CSS 实现页面切换。
主屏幕应用Home Screen App把网页添加到桌面,以接近全屏的方式打开。
第三方请求Third-party Request页面加载时向其他域名发起的资源请求。
授权范围Scope of Authorization书面授权里明确的目标、时间和允许的动作。
你现在应该能回答 这个站是给谁写的、不写什么?三种离线方式分别适合什么场景?手机上怎么翻页、怎么放大图?它收集哪些数据?为什么页面里不能写 id?如果这些你都能答上来,就可以放心把它推荐给别人了。
图解