把网络安全与协议分析
从头到尾讲成一条线
这不是一堆名词的堆砌。全站按数据真实的流动顺序组织:从一根网线里的电平,到 TLS 握手里的每一个字节,再到 App 里的签名、风控包与设备指纹。每一层都回答三个问题:它解决什么问题、长什么样、怎么被看懂。
0.1先建立一张地图:数据到底怎么走
很多人学安全,卡在"知识点都会,但串不起来"。原因是没建立分层直觉。先看这张图:你点一下"登录",数据从手指到服务器,再回来,中间穿了五层衣服。
0.2三条学习路线,按你的底子选
先把地基打牢
不懂网络也不懂编程。按 01 → 02 → 03 → 04 → 05 → 06 → 07 顺序读,每章做完自测再走。
预计 6 到 8 天直接进协议与逆向
会写代码、懂 HTTP。从 04 → 07 → 08 → 09 → 10 → 11 切入,遇到密码学名词回 03 补。
预计 3 到 4 天补移动端与风控
熟悉 Web 安全。重点读 11 → 13 → 14 → 15 → 16 → 17,再做 20 到 24 的对照。
预计 2 到 3 天0.3全站地图
七个分组,36 页。每一页都是独立完整的,可以从任意一页开始;页尾的"上一页 / 下一页"会把线接回去。
零基础预备课
第一次接触这些概念,先读这一页:用寄快递和门牌号讲清客户端、服务器、域名、Token。
先读这一页 →底层地基
网络分层与封装、二进制与编码、密码学、数字签名与证书。看懂后面所有章节的前提。
从 01 开始 →协议与抓包
TCP/UDP、DNS/HTTP、TLS、序列化格式、抓包工具链、逆向方法论。把"看得见"变成"看得懂"。
从 05 开始 →App 逆向与指纹
Android/iOS 逆向、加固混淆、SSL Pinning、设备指纹与风控体系。移动端分析的主战场。
从 11 开始 →完整案例与工程化
纯协议登录全链路、签名与风控包构造原理、自动化稳定性、排错手册。把知识变成可复现的流程。
从 16 开始 →服务端与云
OWASP、认证授权、API/GraphQL/WebSocket、云原生与容器。补齐"另一侧"的视角。
从 20 开始 →检测与合规
风控如何识别异常、日志与告警怎么建、法律边界在哪里。做技术的人必须懂的另一半。
从 24 开始 →真实案例集
三个脱敏案例:电商 App 协议分析、企业信息平台风控、批量执照识别。看方法怎么落到真实项目上。
看案例 →词典与题库
330 条术语、工具与命令速查、面试题库、学习路线、速记卡、常见误区。
随时查阅 →关于本站
版本、更新日志、内容边界、离线使用与反馈方式。
查看 →0.4怎么用这套教程才有效
先猜,再看
每张图出现前,先在心里猜"下一步会发生什么"。猜错的地方就是你真正的知识缺口,比从头读到尾有效得多。
用自己的话复述
读完一节,合上页面,用三句话讲给一个不懂技术的人听。讲不下去的地方,说明你只是记住了词,没记住因果。
对拍,不靠感觉
凡是"我算出来了"的结论,都要拿真实流量或真实设备对一遍。对不上就是没懂,不要用"应该是环境问题"糊过去。
间隔重复
当天读、隔天看速记卡、一周后做面试题库。术语表按主题分组,适合一次刷一组。
0.5快捷键与阅读功能
| 功能 | 怎么用 | 说明 |
|---|---|---|
| 全文搜索 | Ctrl / ⌘ + K,手机点底部「搜索」 | 跨 36 页搜索正文,回车直接跳到命中段落 |
| 翻页 | ← → | 按全站顺序上一页 / 下一页 |
| 本页目录 | 右下角圆形按钮 / 手机底部「目录」 | 列出当前页所有小节,点击平滑滚动 |
| 字号 | 右上角 A- A+ | 四档字号,自动记住选择 |
| 离线保存 | 右上角「离线保存」 | 下载完整单文件,断网可读 |
| 看图 | 手机上左右滑动 / 点「放大」 | 图解保持字号不缩小,可全屏查看 |
| 翻页 | 手机左右滑动屏幕 | 在正文区域横向滑动切换上一页 / 下一页 |
| 阅读进度 | 自动 | 侧栏打绿点的页面表示已读过 |
:target 实现,不依赖 JavaScript。即使预览器禁用了脚本,左侧导航、折叠问答、字号之外的所有阅读功能都正常;只有全文搜索、进度条和离线保存按钮会失效。
0.6内容边界:先把红线说清楚
这不是一句免责套话。真正做这行的人都明白:技术能力的边界,和授权文件的边界是重合的。第 25 章会把法律条款、授权范围怎么写、哪些行为绝对不要碰,一条条讲清楚。
零基础预备课:先搞懂这 8 个问题
如果你没学过网络、没写过代码,直接看后面的 TLS、逆向、风控会像看天书。这一页不涉及任何技术细节,只用寄快递、门牌号、身份证这些日常经验,把后面 35 页要用到的底层概念先铺一遍。看完这页,你就能看懂全站 80% 的图。
0.1问题一:点一下按钮,发生了什么
你在 App 里输入账号密码,点"登录"。这一瞬间发生了四件事,顺序固定:
把这四步想成寄快递:
- 请求:你写一张单子(账号密码),装进信封寄出去;
- 响应:对方收到后回一张单子(登录成功),还附了一张"取件卡";
- 带凭证:以后每次取件都出示这张卡,对方就知道是你;
- 取数据:对方核对卡片,把包裹给你。
0.2问题二:手机怎么找到那台电脑
互联网上有几十亿台设备,你的手机怎么知道该把请求发给哪一台?靠三层地址,缺一不可。
| 概念 | 生活类比 | 作用 |
|---|---|---|
| 域名 | 大厦名字 | 给人看的,好记 |
| 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问题四:为什么这句话要加密
因为这句话一路上要经过很多台你不认识的机器:你家的路由器、运营商的基站、机房的交换机。任何一环都能偷看。看下面这张对比图:
但光加密还不够。攻击者虽然看不懂内容,却可以把整句话原样再寄一遍(重放),或者把信封上的名字换掉(冒充)。所以还需要:
- 加密:防偷看;
- 签名:防篡改、防冒充;
- 时间戳 + 随机数:防重放;
- 证书:证明"对面那台机器真的是它"。
0.5问题五:登录到底在干什么
登录不是"把密码发给服务器然后它记住你"。真实过程更像办一张临时门禁卡:
这也解释了一个常见疑问:为什么改密码之后,别的设备会被踢下线?因为服务器把旧 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 | 两小时 |
0.9接下来的学习顺序
自测:域名、IP、端口,分别解决什么问题?
自测:加密和签名,哪个负责防偷看?
自测:为什么抓到 Token 不等于拿到密码?
自测:抓包抓到一堆乱码,最可能是什么原因?
网络分层:数据是怎么一层层穿上衣服的
你在抓包软件里点开一个包,会看到 Ethernet、IP、TCP、HTTP 一层套一层,像俄罗斯套娃。这一页用寄快递要套好几层包装讲清为什么要分层、每层加了什么,以及为什么改一个 HTTP 头根本不影响数据怎么走路。
2.1先看一次真实的寄快递
你给朋友寄一部手机。你不会把手机裸着塞进邮筒,而是分四步:
- 先用泡沫裹一层,防摔;
- 装进纸箱,保证装得下、不散架;
- 箱子上贴一张面单,写清收件人地址;
- 交给快递员,由他决定这一站送到哪个分拣中心。
这四步各管一件事。泡沫管"别摔坏",纸箱管"装得下",面单管"送到哪",快递员管"这一站交给谁"。
关键在这里:分拣中心只看面单,不会拆开箱子研究你寄的是什么。它也不需要知道。
网络用的是同一套思路。你要发的一句话,也会被裹上好几层"包装",每一层只写自己关心的信息。这套做法叫分层(Layering)。
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)。
每一层的数据单位有专门的名字,看文档时经常遇到:
| 层 | 数据单位 | 典型头长度 |
|---|---|---|
| 应用层 | 报文(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告诉服务器你要访问哪个站点,一台服务器可以靠它区分很多网站。
2.6MTU 1500:超重必须拆包
网线不是想传多大就传多大。以太网一帧最多扛 1500 字节的 IP 数据,这个上限叫 MTU(Maximum Transmission Unit,最大传输单元)。
为什么是 1500?这是早期以太网定下的工程折中:太小,头部开销占比高;太大,一个包出错要重传的代价高,而且别的设备可能收不下。这个数字一直沿用至今。
超过 1500 怎么办?IP 层会把大包切成几片,每片都带自己的 IP 头,头里记着"我是第几片、从原始数据的第几个字节开始、后面还有没有"。这个过程叫分片(Fragmentation)。
用快递打比方:一件超重货物必须拆成三个箱子,每个箱子贴同一张运单的编号,收件方按编号拼回去。少一个箱子,整件货就不完整。
分片的代价很实在:
- 丢任何一片,整个原始包都得重传。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 代理 / WAF | HTTP 头 | 看 Host、User-Agent、Cookie |
| CDN | HTTP 头 | 按 Host 决定回源到哪台服务器 |
所以"改 User-Agent 能骗过某些检测"和"改 User-Agent 能改变路由"是两件事。前者是应用层的事,后者根本不可能,除非中间有应用层设备插手。
顺带说清一个常见混淆:CDN 按 Host 头把请求分到不同源站,这叫应用层负载均衡,不是 IP 路由。它的决策依据是 HTTP 头,不是路由表。
自测:数据从应用到网线,依次加了哪些头?
自测:MTU 1500 是什么,超了怎么办?
自测:为什么路由器不看你 HTTP 头?
自测:OSI 七层和 TCP/IP 四层是什么关系?
字节、编码与字节序:所有协议的底层语言
抓包看到一串 48 54 54 50,逆向看到 0x12345678,网页里看到 %E4%B8%AD。这些不是天书,是同一种东西的不同写法。这一页把"0 和 1 怎么变成人能读的字"讲透,让你以后看到字节先能判断它是什么,而不是直接喊加密。
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 表示"这是十六进制"。
所以 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 默认用这种。
网络传输统一用大端,这叫网络字节序(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 个字符。
以 "Man" 为例,完整过程是:
M = 01001101 a = 01100001 n = 01101110
连起来:010011 010110 000101 101110
查表: T W F u
结果:TWFu
3 字节变 4 字符,体积涨约 33%。如果原文不是 3 的倍数,用 = 补齐,所以你会看到结尾有 = 或 ==。
重点:Base64 不是加密。它没有密钥,任何人拿到都能还原。它只是换了一种写法,作用是让二进制能安全穿过文本通道。
3.7UTF-8:一个汉字通常 3 字节
计算机只认字节,要把"字"变成字节,需要字符编码(Character Encoding)。
UTF-8 是目前最通用的编码。它的规则是变长:
| 字符类型 | 字节数 | 例子 |
|---|---|---|
| 英文、数字、符号 | 1 | A 等于 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)。改扩展名骗不了它。
| 格式 | 开头字节 | 说明 |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A | 图片 |
| ZIP | 50 4B 03 04 | 压缩包,APK 也是 ZIP |
| 25 50 44 46 | 文档,即 %PDF | |
| ELF | 7F 45 4C 46 | Linux 可执行文件 |
| DEX | 64 65 78 0A | Android 字节码 |
用法:拿到一个可疑文件,先看前 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误区:看到十六进制就以为加密
这是初学者最常见的误判。判断顺序应该是:
- 先看是不是文本编码。能按 UTF-8 正常读出中文英文,就是编码问题。
- 再看是不是 Base64。全是 A 到 Z、a 到 z、0 到 9、加号、斜杠,长度是 4 的倍数,结尾可能有等号,大概率是 Base64。
- 再看是不是压缩。开头
1F 8B是 gzip,50 4B是 zip。 - 最后才考虑加密。加密后的字节应该接近随机:每个值出现频率差不多,没有明显的重复结构。
一个实用判据:把字节画成直方图。文本和 Base64 的分布很不均匀,压缩和加密的分布接近均匀。这就是"熵"的直观含义,下一章会讲。
另一条:如果一段数据你能用 Base64 解开,得到可读内容,那它从一开始就不是加密。
自测:0x41 是什么?三种写法怎么换算?
自测:0x12345678 在小端机器内存里怎么排?
78 56 34 12。大端则是 12 34 56 78。网络传输统一用大端,本机内存多半是小端,所以代码里要做转换。自测:Base64 是加密吗?为什么?
自测:抓包里看到 e4 b8 ad,该怎么判断?
密码学基础:四把锁和一台榨汁机
加密、哈希、签名、证书,这四个词经常被混着用。这一页用四把锁和一台榨汁机,把它们各自解决什么问题、不能解决什么问题分清楚。看完你会明白:为什么 TLS 不直接用非对称加密传数据,为什么同一个 IV 用两次会出大事。
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 | 计数器生成密钥流再异或 | 可并行,可随机访问 | 计数器绝不能重复 |
| GCM | CTR 加认证标签 | 同时加密和防篡改 | nonce 重复会泄露密钥流 |
重点说 ECB。它把每一块单独加密,完全不管上下文。结果就是:明文里相同的块,密文里也相同。
最著名的演示是"ECB 企鹅":一张图片用 ECB 加密后,企鹅的轮廓依然清晰可见,因为大面积的同色区域产生了相同的密文块。
所以 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):用非对称加密只传一把临时对称密钥,之后所有数据都用对称加密。
步骤拆开看:
- 客户端连上服务器,服务器出示证书,证书里含公钥。
- 客户端验证证书,确认公钥真的是这家网站的。
- 客户端生成一个随机的对称密钥,叫会话密钥。
- 客户端用服务器公钥加密这个会话密钥,发过去。
- 服务器用私钥解开,拿到会话密钥。
- 之后双方用会话密钥做 AES-GCM 加密通信。
为什么这么设计?因为非对称加密慢,只用来传 32 字节的密钥很划算;对称加密快,用来传几 MB 的网页刚好。
这也是为什么"破解 TLS"实际上不是破解 AES,而是想办法拿到会话密钥:要么拿到服务器私钥,要么在客户端装证书,要么直接读进程内存。
4.7哈希:一台不可逆的榨汁机
哈希(Hash)把任意长度的输入,压成固定长度的输出。SHA-256 的输出永远是 256 位,也就是 32 字节。
最好的类比是榨汁机:苹果进去,果汁出来。你没法从果汁还原出苹果。
哈希有三个关键性质:
- 单向:给定输出,反推输入在计算上不可行。
- 抗碰撞:很难找到两个不同输入产生相同输出。
- 雪崩:输入改 1 个比特,输出大约一半的比特会变。
echo -n "hello" | sha256sum
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
echo -n "hellp" | sha256sum
输出完全不同,哪怕只改了一个字母
常见算法现状:
| 算法 | 输出长度 | 现状 |
|---|---|---|
| MD5 | 128 位 | 已破解,能人为构造碰撞,不能用于安全 |
| SHA-1 | 160 位 | 已破解,浏览器和证书早已弃用 |
| SHA-256 | 256 位 | 目前安全,广泛使用 |
| 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 不是加密。它没有密钥,只是编码,任何人可还原。上一章已经讲过原理。
第二个:加密不等于签名。加密只保证别人看不懂,不保证是谁发的,也不保证没被改。攻击者可以整段替换你的密文,接收方照样能解开,只是内容变了。
第三个:哈希不等于加密。哈希不可逆,加密可逆。用哈希存密码是对的,用哈希"加密"需要还原的数据是错的。
4.13一张表收尾
| 你要解决 | 用什么 | 一句话 |
|---|---|---|
| 机密性 | AES-GCM | 一把钥匙,快,还顺手防篡改 |
| 完整性 | HMAC、SHA-256 | 改一个字节就露馅 |
| 身份认证 | 证书、签名 | 证明对面真的是它 |
| 不可否认 | 数字签名 | 私钥签的,赖不掉 |
记住这张表,遇到"这个场景该用什么"的问题,先想它属于哪一行。
自测:对称加密和非对称加密,各自的问题是什么?
自测:为什么 ECB 模式不能用?
自测:同一个密钥下重复使用同一个 IV 或 nonce 会怎样?
自测:哈希和 HMAC 差在哪?
自测:为什么存密码要用 Argon2 而不是一次 SHA-256?
数字签名与证书:谁在证明这句话是你说的
加密能挡住偷看,却挡不住"改一个字"和"对面是假货"。这一页用蜡封、印模、身份证把签名、证书、证书链、吊销一路讲通,最后说清为什么"地址栏有锁"不等于"这个网站可信"。
5.1先分清:加密、哈希、签名是三件事
初学者最容易把这三个词混着用。它们解决的是完全不同的问题,用错地方就会得出完全错误的结论。
打个比方:加密是把信锁进保险箱,只有拿钥匙的人能打开;哈希是给信算一个指纹,指纹不能还原出信;签名是在指纹上盖一个只有你能盖的章。
| 手段 | 用什么 | 解决什么 | 能否还原原文 |
|---|---|---|---|
| 对称加密(AES) | 一把钥匙,收发双方都有 | 防偷看 | 能,有钥匙就行 |
| 哈希(SHA-256) | 不需要钥匙 | 做指纹、做比对 | 永远不能 |
| 数字签名 | 私钥签,公钥验 | 防篡改、防冒充 | 不能,也不负责保密 |
一句话记住分工:加密管"看不懂",哈希管"变了没有",签名管"谁说的"。这三件事经常一起出现,但不能互相替代。
5.2蜡封:把签名的动作拆开看
中世纪的信封会用火漆封口,再盖上家族印章。收信人看到蜡封完整、印章对得上,就能得出两个结论:信没被拆过,而且确实出自那个家族。
数字签名就是把这件事搬到电脑上。私钥是你的印章,只有你有;公钥是印在名片上的印模,谁都能拿到。用私钥盖一次,全世界都能用公钥验,但没人能伪造。
注意图里两行:上面那行是签名方做的事,下面那行是任何拿到数据的人都能做的事。签名本身是一串字节,通常和原文一起发出去,或者单独放在请求头里。
长度要有概念:RSA-2048 的签名是 256 字节;Ed25519 只有 64 字节;ECDSA P-256 大约 70 到 72 字节。签名会明显增加报文体积,抓包时看到一段很长的 base64,往往就是它。
5.3为什么必须先哈希再签名
直接拿整份文件去做非对称运算,有两个硬伤。
- 太慢:非对称运算比哈希慢好几个数量级,签一个 1GB 的安装包要等到天荒地老。
- 装不下:RSA 一次能处理的数据长度受密钥长度限制,2048 位密钥只能处理几百字节以内的内容。
所以标准做法是:先对原文算一个哈希(比如 SHA-256,固定输出 32 字节),再对这 32 字节的摘要签名。验证时,对方也对原文算一次哈希,再和"用公钥解出来的摘要"比对。
| 做法 | 运算量 | 能签多大文件 |
|---|---|---|
| 直接签原文 | 与文件大小成正比 | 受密钥长度限制 |
| 先哈希再签 | 固定一次运算 | 任意大小 |
哈希还有一个好处:把任意长度的内容压成固定长度,签名算法就不用关心原文有多长,实现起来简单,验证也快。
这也解释了为什么哈希算法必须选强的。MD5 和 SHA-1 已经被证明能人为制造碰撞(两份不同内容算出同一个值),现在做签名普遍要求 SHA-256 及以上。
5.4四种常见签名算法
你不需要懂数学,只要记住它们的取舍。四种算法的共同点只有一句:私钥签,公钥验。
| 算法 | 签名长度 | 特点 | 常见场景 |
|---|---|---|---|
| RSA PKCS#1 v1.5 | 256 字节 | 确定性,同样输入永远同样签名 | 老证书、部分安装包 |
| RSA-PSS | 256 字节 | 随机化填充,每次签名都不同 | TLS 1.3、新系统 |
| ECDSA | 约 70 字节 | 短、快,极度依赖随机数质量 | 证书、JWT、Android |
| Ed25519 | 64 字节 | 快、确定、实现简单不易出错 | SSH 密钥、新协议 |
"确定性"是指同样的原文和私钥,每次算出来的签名一模一样;"随机化"是指每次都不同,但都能验证通过。两者都安全,只是用途和兼容性不同。
选哪个?一般不用你选。协议和平台会规定好,比如 TLS 1.3 里 RSA 证书要求用 PSS,很多新系统默认 Ed25519。
5.5证书:把公钥和身份钉在一起
光有公钥还不够。攻击者完全可以把自己的公钥发给你,说"我是银行"。你怎么知道这把公钥真的属于银行?
答案是找一个大家都信的第三方,给你的公钥签个字。这张"签过字的纸"就是证书(Certificate),格式标准叫 X.509。
| 字段 | 内容示例 | 作用 |
|---|---|---|
| 版本 | v3 | 目前基本都是 v3 |
| 序列号 | 一串十六进制 | CA 分配的唯一编号,吊销时用它指认 |
| 颁发者 | 某某 CA | 谁签的这张证书 |
| 主体 | example.com 及公司名 | 证书写给谁 |
| 公钥 | RSA 2048 位或 ECDSA | 真正要绑定的东西 |
| 有效期 | 起止时间 | 过期即失效,现在普遍不超过一年 |
| SAN | DNS: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)。
浏览器从服务器证书开始,逐级用上一级的公钥验证签名,一直验到本机信任库里的根证书。根证书是自签名的,靠操作系统和浏览器预装来"无条件信任"。这也解释了为什么根证书数量很少:它是整个体系的锚点。
完整验证清单,缺一不可:
- 每一级签名都对得上,用上一级的公钥验下一级;
- 当前时间在有效期内,没有过期也没到生效时间;
- 访问的域名出现在 SAN 里,通配符范围匹配;
- 用途正确,服务器证书不能拿来签代码;
- 没有被吊销,这一步经常是"尽力而为";
- 根证书在本机信任库里,并且没有被用户手动移除。
5.7吊销、私有 CA 与"合法中间人"
证书有有效期,但私钥可能提前泄露,需要"提前作废"。业界有三种做法。
| 方式 | 谁去查 | 代价 |
|---|---|---|
| CRL 证书吊销列表 | 客户端下载整张名单 | 名单越来越大,更新有延迟 |
| OCSP 在线状态查询 | 客户端实时问 CA | 多一次请求,且暴露你在访问谁 |
| OCSP Stapling | 服务器代查,随握手下发 | 需要服务器支持并定期更新 |
CRL 是 CA 定期发布一张"作废名单",客户端把整张表下载下来自己比对。证书多了以后,这张表会非常大。
OCSP 是拿着证书序列号实时问 CA"这张还有效吗"。快,但多一次网络往返,而且等于告诉 CA"我正在访问这个网站",隐私上有代价。
OCSP Stapling 是服务器替你去问,把 CA 的答复(带时间戳)在 TLS 握手时一起发给你。省一次请求,也不泄露你的访问记录。代价是服务器要自己维护这份答复。
5.7.1 自签名、企业私有 CA 和抓包代理
这三种场景经常被混为一谈,其实动机完全不同:
- 自签名:自己给自己签,测试环境常见,浏览器会红,但开发时够用。
- 企业私有 CA:公司自建根证书,装进员工电脑和手机,用于内部系统,方便统一管理。
- 抓包代理:Charles、Fiddler、mitmproxy 这类工具会生成自己的 CA。你把它装进手机并设为信任,它就能给任何网站现场签一张证书。
原理不复杂:手机以为自己在和服务器说话,其实是在和代理说话;代理再用真实身份去连服务器。中间人之所以能看懂内容,是因为你把信任交给了它。
平台差异很关键:Android 7 以后,App 默认不信任用户自己安装的证书;iOS 需要在"关于本机"里找到"证书信任设置",手动打开完全信任。这就是为什么"证书装了,包还是抓不到"。
反过来,App 也会用证书固定(pinning)来防这一手:只认自己写死的那张证书或那把公钥,换了就不连。攻防就在这一层反复拉锯。
5.8代码签名:安装包也要盖章
你下载的 APK、IPA、EXE 本质上都是"代码",同样需要证明"没被改过、是谁发的"。否则任何人往里塞一段恶意代码,用户根本看不出来。
| 平台 | 方案 | 签的是什么 |
|---|---|---|
| Android v1 | JAR 签名 | 逐个文件算摘要,不解压整包 |
| Android v2 | 整包签名 | 对整个 APK 二进制签名,改一字节就失效 |
| Android v3 | 支持换密钥 | 带密钥轮换证明,换完旧版本仍能升级 |
| iOS | 苹果签发证书加描述文件 | 绑定 Bundle ID、权限和设备 |
| Windows | Authenticode | 对 PE 文件签名,并附时间戳 |
Android 的 v1 方案只签每个文件的摘要,安装时逐个校验,历史上出过漏洞;v2 改成对整个安装包做一次签名,校验快、覆盖全;v3 增加了换密钥的能力,方便密钥泄露后平滑过渡。
iOS 更严格:证书由苹果签发,还要配描述文件,绑定 Bundle ID、权限和设备列表。所以"重签名"在 iOS 上是个专门的技术活,不是改个文件就行。
Windows 的 Authenticode 会给签名加一个时间戳。这样即使证书过期,之前签过的程序仍然有效,不会因为证书到期就全部失效。
5.9四个常见误区
- 以为签名能保密:签名不隐藏内容,明文照样能读。要保密得另外加密。
- 以为有证书就可信:钓鱼站也能申请到合法证书。证书只证明域名归属,不证明内容善意。
- 以为签名能防重放:签名不绑定时间,原样再发一次照样验通过。要配合时间戳、随机数和服务端去重。
- 以为哈希就是签名:哈希谁都能算,没有身份含义,谁都能伪造一个"看起来对"的哈希。
把这四点想清楚,你在抓包里看到签名、证书、指纹时,就不会再把它们当成同一件事。
自测:为什么签名之前一定要先算哈希?
自测:既然根 CA 可以直接签服务器证书,为什么还要中间 CA?
自测:抓包代理为什么能看懂 HTTPS 内容?
自测:地址栏有小锁,是不是就说明网站安全?
TCP 与 UDP:可靠到底是怎么做到的
很多人背过"TCP 可靠、UDP 不可靠",但说不出可靠是靠什么换来的。这一页用打电话、寄明信片、编号回执、路口限流,把握手、挥手、重传、粘包、NAT 一次讲透,最后告诉你为什么抓包里会出现 RST 和零窗口。
6.1打电话和寄明信片
传输层要解决的是"两台机器上的两个程序怎么对话"。最常用的两套方案,思路完全相反。
TCP 像打电话:先拨号,等对方接起来,确认双方都能听见,再开始说;说的时候对方会"嗯"一声表示收到;没听清就让他重复。挂电话也要互相确认。
UDP 像寄明信片:写好地址投进邮筒,不确认对方收没收到,也不保证顺序。便宜、快,但可能丢、可能后发的先到。
| 对比项 | TCP | UDP |
|---|---|---|
| 要不要先建立连接 | 要,三次握手 | 不要,直接发 |
| 丢包怎么办 | 自动重传 | 不管,交给应用 |
| 顺序 | 保证按发送顺序交付 | 不保证 |
| 典型用途 | 网页、文件、登录 | DNS、视频、游戏、QUIC |
注意最后一行:QUIC 跑在 UDP 上,但它自己在应用层把可靠性做回来了。所以"UDP 不可靠"说的是协议本身不负责,不是说用了它就一定丢数据。
6.2三次握手:为什么不能是两次
建立连接要交换三句话。核心目标只有一个:双方都要确认"我发的能到、你发的能到"。
为什么两次不行?服务器发完 SYN-ACK 就单方面认为连接建立了,但它并不知道这个包有没有送到客户端。如果客户端根本没收到,服务器就白建了一条连接,还占着内存。
更麻烦的是旧包。网络里可能残留很久以前发出的重复 SYN,如果只握两次,服务器会被这些过期请求骗出一堆无效连接。三次握手能让这种老包自然失效。
还有一个细节:起始序号(ISN)是随机选的,不是从 0 开始。这样攻击者更难猜到下一个序号,也更难伪造报文。
抓包里看到 SYN、SYN-ACK、ACK 三行,就是一次成功的握手;只看到 SYN 后面跟着 RST,通常是端口没开,或者被防火墙直接拒绝。
6.3四次挥手与 TIME_WAIT
关闭连接要四句话,因为 TCP 是全双工的:两个方向要各自关一次。你说"我不发了",不代表对方也发完了。
主动关闭的一方最后要进入 TIME_WAIT,等 2 倍报文最大生存时间。这个等待不是浪费,它有两个明确作用:
- 万一最后那个 ACK 丢了,对方重发 FIN 时,还能再回一次,不至于让对方一直卡着;
- 让这条连接的旧报文在网络里彻底消失,避免污染下一条恰好用了相同端口的新连接。
服务器上出现大量 TIME_WAIT 是正常现象,不是"连接泄漏"。真正需要担心的是本地端口被占满,导致新连接建不起来。
6.4编号、回执和窗口
TCP 把数据看成一条很长的字节流,每个字节都有编号。发送方说"我从 1001 号开始给你 500 字节",接收方回"1001 到 1500 收到,下一条我要 1501"。
这个"下一条我要第几号"就是确认号(ACK)。它是累积的:确认了 1501,就代表 1501 之前的全部收到,不用一条条确认。
窗口(Window)是接收方的缓冲区还剩多少空间。窗口变成 0,发送方就必须停下来等,这就是抓包里的"零窗口"。
| 字段 | 含义 | 生活类比 |
|---|---|---|
| 序列号 | 这一段的第一个字节编号 | 这是第几页 |
| 确认号 | 我下一条想要第几号 | 回执写"1 到 5 页已收到" |
| 窗口 | 我还能接多少字节 | 桌子只能再放三摞 |
这三样合起来就是可靠传输的骨架:编号让接收方能排序和去重,确认让发送方知道哪些到了,窗口让发送方别把接收方撑爆。丢包、乱序、重复,都靠这套机制兜住。
6.5重传与拥塞控制
丢包了怎么办?等。等多久?根据往返时间动态估算,这个等待时间叫 RTO。超时还没收到确认就重传,而且每次超时等待时间翻倍,避免把已经堵死的链路砸得更烂。
还有一种更快的办法:如果连续收到三个重复的确认,说明中间缺了一段,不用等超时,立刻重传缺的那段。这叫快重传,能让恢复从"秒级"降到"毫秒级"。
拥塞控制像路口限流。发送方维护一个"拥塞窗口",开始时很小,每收到确认就涨一点(慢启动阶段实际是每轮翻倍),摸到上限后改成缓慢增长(拥塞避免);一旦检测到丢包,就把窗口砍半,退回去重新试探。
现代系统常用 CUBIC 或 BBR 这类算法。你不需要记公式,只要记住一句话:TCP 会主动试探带宽上限,一丢包就退让。
6.6粘包与拆包:TCP 是字节流,不是消息流
这是初学者最容易被坑的地方。你调了两次发送,对方不一定读两次;你发了一条完整消息,对方可能只读到一半。
你写了两次,可能被合成一次读到(粘包);也可能一次写入被拆成两半(拆包)。这跟"TCP 可靠"不矛盾:字节一个没少,只是边界没了。
解决办法都在应用层,常见三种:
- 定长:每条消息固定长度,不够就补齐,简单但浪费;
- 分隔符:用换行或特殊字符分隔,HTTP 头就是用空行结束的;
- 长度前缀:先发 4 字节表示后面有多长,再发内容,最通用,很多私有协议都这么干。
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 | 端口没开、被防火墙拒绝,或应用主动重置 | 服务是否在听、中间设备是否拦截 |
| 大量重传 | 链路丢包或拥塞 | 丢包率、是否集中在某一跳 |
| 零窗口 | 接收方应用没在读,缓冲区满了 | 应用是否卡住、处理是否太慢 |
把这句话记牢,你在排查"用户说没收到"这类问题时,就不会只盯着网络层了。
自测:为什么握手是三次而不是两次?
自测:TIME_WAIT 等的是什么?
自测:为什么会粘包?
自测:TCP 已经保证可靠了,为什么应用还要重试?
DNS 与 HTTP 家族:从域名到页面的每一跳
你在地址栏敲下域名到看见页面,中间至少经过一次域名查询、一次 TCP 握手、一次 HTTP 请求、若干次缓存判断。这一页把这条链路上的每一跳拆开,告诉你 301 和 302 差在哪、304 为什么没有正文、HTTP/2 到底解决了什么又没解决什么。
7.1第一跳:把域名换成 IP
你在地址栏输入 example.com 回车。第一件事不是发 HTTP,而是问"这个域名对应哪个 IP"。因为网络只认 IP,不认名字。
查询顺序是逐级往上的:浏览器缓存、系统缓存(含 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 审计会失效,所以很多公司会主动禁用它。
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 网关等上游超时。
7.5Cookie 和 Session 到底谁存什么
HTTP 本身是无状态的:服务器不记得上一次是谁来过。要让"登录状态"延续,需要额外的东西。
Cookie 是存在浏览器里的一小段文本,每次请求自动带上。Session 是存在服务器上的一份数据,浏览器只保存一个编号(Session ID)。
| 对比 | Cookie | Session |
|---|---|---|
| 存在哪 | 浏览器 | 服务器 |
| 存什么 | 编号或少量数据 | 用户状态、权限 |
| 谁控制过期 | 浏览器按 Max-Age | 服务器说了算 |
Cookie 有几个关键属性要认识:
- HttpOnly:脚本读不到,降低被注入脚本窃取的风险;
- Secure:只在 HTTPS 下发送,防止明文链路泄露;
- SameSite:限制跨站携带,防跨站请求伪造,常见值 Lax 和 Strict;
- Domain 和 Path:决定哪些请求会带上它。
7.6HTTP/2:一条连接跑多个请求
HTTP/1.1 最大的问题是:一条连接同一时刻只能处理一个请求。浏览器只能开六条左右的连接来并行,开太多又浪费资源。
HTTP/2 把报文改成二进制分帧:把每个请求和响应切成小帧,打上"流编号",然后在一条连接上交错发送。接收方按流编号重组。这就是多路复用。
它还做了两件事:HPACK 压缩头部(把重复的头字段放进表里,只发索引),以及服务器推送(服务端主动推资源,实际用得很少,主流浏览器已经移除)。
但 HTTP/2 仍然跑在 TCP 上。TCP 丢一个包,后面所有流的数据都要等它重传,这叫 TCP 层队头阻塞。HTTP/2 只解决了 HTTP 层的排队问题。
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 之后,源站看到的 IP 往往是代理的。所以会有 X-Forwarded-For 这类头来传递真实客户端 IP。这个头由客户端和代理逐跳追加,可以被伪造,做风控时不能单独信它。
最后是三个必须纠正的误区:
- HTTP/2 一定比 1.1 快:不一定,见上一节,丢包严重时反而更糟;
- 200 就代表业务成功:很多系统把错误放在响应体里,HTTP 照样回 200,判断业务要看体里的 code 字段;
- HTTPS 就安全:HTTPS 只保证传输这一段,服务端漏洞、越权、注入它一概不管。
自测:输入域名后,DNS 的查询顺序是什么?
自测:301 和 302 有什么区别?
自测:Cookie 和 Session 分别存在哪?
自测:HTTP/2 用了多路复用,为什么还会有队头阻塞?
TLS 深入:握手、密钥与加密记录
浏览器地址栏那把小锁,背后是一套跑了三十年的协议。这一页把它拆成四件事:怎么谈、怎么算钥匙、怎么装信封、怎么证明身份。看懂之后,抓包里那些看不懂的握手包就有了名字。
8.1它到底在防谁
先想一个场景:你在咖啡店连公共 WiFi,打开网页输入账号密码。这条链路上有多少人能复制你的数据?
至少五六个:你的手机、店里的路由器、运营商、中间经过的机房。任何一环都能录一份。
TLS(Transport Layer Security,传输层安全)做的事,就是在这条路上临时修一条管道。管道外面的人只能看到"有数据在流",看不到里面是什么。
它只解决三个问题,不多不少:
- 机密(Confidentiality):别人看不懂。靠加密。
- 完整(Integrity):别人改不了。改一个字节,接收方就会发现。
- 身份(Authentication):对面真的是它。靠证书。
拿寄合同打比方:
- 不加密,等于寄明信片,路上谁都能读;
- 加密,等于装进保险箱;
- 完整性,等于保险箱贴封条,被撬过就有痕迹;
- 身份,等于对方出示营业执照,确认不是隔壁老王冒充。
| 攻击者想干的事 | 具体手段 | TLS 的对策 |
|---|---|---|
| 偷看 | 在 WiFi 上嗅探数据包 | 对称加密,看到的是密文 |
| 篡改 | 往网页里插广告、改转账金额 | 认证标签,改一个字节就失败 |
| 冒充 | 假基站、假网站、DNS 污染 | 证书链验证 |
| 重放 | 把旧请求原样再发一遍 | 序列号与一次性随机数 |
8.2TLS 1.2 握手:两个来回
握手(Handshake)就是开场谈判。双方要在这几步里完成四件事:交换随机数、确认算法、验证身份、算出钥匙。
RTT(Round Trip Time,往返时延)指"一句话发出去再收到回复"要多久。手机 4G 下通常 30 到 80 毫秒,跨洋可能 200 毫秒以上。
TLS 1.2 的完整握手需要 2 个 RTT。也就是说,在你看到网页之前,光握手就多花了两个来回。
逐条看这十条消息在说什么:
- ① ClientHello:客户端说"我支持这些算法,这是我的随机数 A,我要访问 example.com"。
- ② ServerHello:服务器从清单里挑一个双方都支持的算法,给出随机数 B。
- ③ Certificate:服务器把证书链发过来,证明"我是 example.com"。
- ④ ServerKeyExchange:用 ECDHE 时,服务器给出一个临时公钥,并用证书私钥签名。
- ⑤ ServerHelloDone:服务器表示"我这边说完了,轮到你"。
- ⑥ ClientKeyExchange:客户端给出自己的临时公钥。双方各自算出同一个共享秘密。
- ⑦⑧:客户端发 ChangeCipherSpec 和 Finished,表示"接下来我加密,这是校验值"。
- ⑨⑩:服务器同样回两条,握手结束,开始传业务数据。
Finished 里装的是"前面所有握手消息的摘要"。任何一条被改过,摘要就对不上,握手立刻失败。这是防篡改的最后一道闸。
8.3三个随机数与密钥派生
很多人以为 TLS 只用一把钥匙。实际上,握手阶段产生了三份关键材料:
| 材料 | 谁生成 | 作用 |
|---|---|---|
| 客户端随机数 Client Random,32 字节 | 客户端 | 给本次会话加盐,让每次的钥匙都不同 |
| 服务器随机数 Server Random,32 字节 | 服务器 | 同上,并防止旧握手被重放 |
| 预主密钥 Pre-Master Secret,48 字节 | 双方协商 | 真正的秘密来源,只有双方知道 |
为什么需要两个随机数,而不是一个?
因为随机数的作用是"加盐"。假如只有预主密钥,同一对客户端和服务器每次算出来的会话密钥都一样。攻击者录下今天的密文,明天偷到钥匙,就能解开昨天的全部记录。
加入两个随机数之后,每一次连接算出来的钥匙都不同。昨天的密文,今天解不开。
那为什么还要预主密钥?因为随机数是公开传的,光有它没有秘密。预主密钥才是只有双方知道的那部分。
协商预主密钥有两种方式,差别很大:
- RSA 密钥交换(老):客户端生成预主密钥,用服务器证书里的公钥加密后发过去。缺点是谁录下了流量,将来只要拿到服务器私钥,就能解开全部历史记录。这叫没有前向保密。
- ECDHE(椭圆曲线临时迪菲-赫尔曼):双方各生成一对临时密钥,交换公钥,各自算出同一个共享秘密。临时私钥用完就扔。即使服务器私钥泄露,也解不开过去的流量。这就是前向保密(Forward Secrecy)。
现在几乎所有正规站点都用 ECDHE。看到"没有前向保密"这个词,基本等于"这套配置已经过时"。
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.2 | TLS 1.3 |
|---|---|---|
| 完整握手 | 2 个 RTT | 1 个 RTT |
| 证书 | 明文可见 | 加密 |
| 密钥交换 | RSA 或 ECDHE | 只有 ECDHE / PSK |
| 加密模式 | CBC、GCM 等 | 只有 AEAD |
8.5记录层:AEAD、nonce 与重放
握手只是开场。真正传数据的是记录层(Record Layer)。它把上层的数据切成一块块,每块加上头和认证标签。
TLS 1.3 的记录结构非常简单:
AEAD(Authenticated Encryption with Associated Data,带关联数据的认证加密)是一类算法,一次调用同时完成"加密"和"防篡改"。常见的有 AES-GCM 和 ChaCha20-Poly1305。
它需要三样输入:明文、密钥、nonce。nonce 是"一次性数字",同一把钥匙下绝不能重复。
TLS 1.3 的 nonce 怎么来?把静态 IV 和序列号做异或。
- 静态 IV 是握手时派生出来的 12 字节,双方都知道;
- 序列号从 0 开始,每发一条记录加一,不放在网络上,双方各自维护;
- 异或之后得到本次使用的 nonce。
为什么这样能防重放?
因为同一条连接里序列号只增不减。攻击者把第 5 条记录再发一次,接收方按第 8 条去解,nonce 对不上,认证标签校验失败,直接丢弃。跨连接呢?每条连接的钥匙都是新算的,旧密文在新连接里根本解不开。
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,服务器放降级哨兵 |
| BEAST | TLS 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 密钥交换更安全?
自测:TLS 1.3 的 nonce 为什么不会重复?
自测:抓到 TLS 流量,为什么还能看到域名?
自测:会话票据(Session Ticket)解决了什么问题,代价是什么?
序列化格式:JSON 之外的二进制世界
同样一句"张三二十岁",可以写成一张表、一封信,也可以写成一段只有约定好的人才能读的密码。这三种写法就是序列化的三个流派。看懂它们的取舍,你才能在抓包里认出那堆字节到底是什么。
9.1序列化:把一张表变成一串字节
先做一个思想实验。你要把"张三,20 岁,VIP"这条信息发给另一台电脑,有几种写法?
- 写成表格:一行三列,谁都看得懂,但占地方。
- 写成一封信:有称呼、有格式、有落款,啰嗦但清楚。
- 写成一段密码:最短,但只有事先约好暗号的人能读。
这三种写法,分别对应今天要讲的三个流派:文本格式、标记语言格式、二进制格式。
为什么不能直接把内存里的东西发过去?两个原因:
- 不同机器存同一个数字的字节顺序可能相反,这叫字节序(Endianness)。
- 内存里有"指针"这种东西,它记录的是本机地址,换台机器就完全失效。
所以必须先翻译成一套双方都认的中间形式,这个过程就叫序列化(Serialization),反过来叫反序列化(Deserialization)。
评价一种格式,看四个指标:可读性、体积、解析速度、跨语言支持。它们没法同时最优。
| 格式 | 可读性 | 体积 | 典型场景 |
|---|---|---|---|
| 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 把它暴露在你面前。
"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)。
| 线类型 | 用于 | 后面跟什么 |
|---|---|---|
| 0 | int32 / int64 / bool / enum | varint 值 |
| 1 | fixed64 / double | 固定 8 字节 |
| 2 | string / bytes / 嵌套消息 | 长度 + 内容 |
| 5 | fixed32 / 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 字节。
向后兼容规则,这几条必须背下来:
- 字段编号一旦发布就不能改,也不能复用。删掉的编号要写进 reserved,防止后人误用。
- 可以新增字段,老程序会忽略不认识的编号,不会报错。
- 不能把 int32 改成 string,线类型变了就解不出来。
- 1 到 15 号字段的 tag 只占 1 字节,留给最常用的字段。
0A 03 这样的字节,必须拿到 .proto 才能还原出含义。
9.5MessagePack 与 CBOR
这两个可以理解为"二进制版 JSON":不需要 .proto,自己带类型信息,所以拿到字节就能解,但体积比 Protobuf 大一点。
| 对比项 | JSON | MessagePack | CBOR |
|---|---|---|---|
| 可读性 | 人直接能读 | 需要工具 | 需要工具 |
| 体积 | 大 | 小 | 小 |
| 是否要 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:固定几个字节,用来一眼判断"这是不是我的包"。抓错端口时能立刻发现。
- 版本:以后改协议时,老客户端还能被识别出来。
- 标志位:一位表示是否压缩,一位表示是否加密,省得再加字段。
- 类型:这条消息是登录、心跳还是拉数据。
- 长度:告诉对方 payload 有多少字节。
- 序列号:对应答、防重放、排查丢包。
- 校验和:CRC32 之类,快速发现传输错误。
9.8怎么认出一个未知格式
拿到一段来路不明的字节,按这四步走,通常十分钟内能定性。
看可打印字符比例
把字节按 ASCII 看一遍。文本格式通常九成以上是可打印字符;二进制格式里会夹杂大量 00 到 1F 的控制字符。
算熵
熵衡量"有多乱"。压缩或加密的数据接近每字节 8 比特;结构化二进制通常在 3 到 6 之间;纯文本更低。
找 magic bytes
看开头 16 字节。很多格式有固定文件头,比如 PNG 开头是 89 50 4E 47。对不上已知表,就说明是私有格式。
试长度字段
找连续的 4 字节,看它的值是否约等于"剩余长度"。抓多个不同大小的包对比,如果这个位置的数值随包长同步变化,基本就是长度字段。
还有两个小技巧:看有没有规律重复的字节(可能是分隔符),看末尾是不是固定长度的一段(可能是校验值)。多个包一起看,比盯着一个包有效得多。
9.9两个必须记住的误区
误区一:JSON 字段顺序不重要,所以签名时也不用管顺序。
JSON 对象在语义上确实无序,但签名是对字节做的。序列化时字段顺序一变,字节就变,签名就对不上。所以签名前必须约定规范化规则:键按字典序排、去掉多余空格、数字格式统一、Unicode 转义统一。这类约定通常叫规范化(Canonicalization)。
误区二:Protobuf 字段名改了没关系,反正传的是编号。
前半句对,后半句对,但结论危险。字段名确实不上线,改名不影响兼容。可是字段编号绝对不能改、不能复用。改了编号,老客户端会把新字段当成另一个字段解析,数据直接错位,而且不会报错,只会静默出错。
自测:为什么用户 ID 常常用字符串传,而不是数字?
自测:Protobuf 的 tag 字节是怎么算出来的?
自测:XXE 能读到服务器文件,根因是什么?
自测:自定义协议为什么一定要有长度字段?
自测:熵很高说明什么?
抓包与工具链:先看得见,才谈得上分析
分析协议的第一步不是读代码,是先把流量完整地看见。这一页讲清楚抓包发生在哪三个位置、每件工具适合干什么、为什么装了证书还是抓不到,以及一条从"打不开网页"到"证书固定"的排查路线。
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 协议设置里填上这个文件路径,就能看到明文。
10.4代理为什么能解密
代理(Proxy)的本质是中间人:客户端以为在跟服务器说话,其实在跟代理说话;服务器以为在跟客户端说话,其实也在跟代理说话。
把过程拆成六步:
- 客户端配置了代理地址,比如 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 | 图形界面 | 上手快,查看和重发方便,收费 |
| Fiddler | Windows 老牌 | 规则脚本强,Classic 版免费 |
选型不用纠结:手工看几个请求,Charles 最快;要写脚本批量处理、做自动化,mitmproxy 更合适;只在 Windows 上临时用,Fiddler 够。
10.6Android 的两套证书,以及 Android 7 的分水岭
Android 里有两个证书库,地位完全不同:
- 系统证书:预装在系统分区里,出厂就有,普通应用改不了。
- 用户证书:用户自己安装的,存在用户数据区。
Android 7(API 24)之前,应用默认两者都信任。所以那个年代,装上代理的 CA 就能抓包。
Android 7 之后,规则变了:应用默认只信任系统证书,用户证书被忽略。除非应用在自己的网络安全配置里显式声明信任用户证书,或者这个应用是可调试的。
10.7抓不到包,按这个顺序查
不要一上来就怀疑工具。按下面的顺序,一步一停,能省掉大量时间。
再补两句容易忽略的:
- 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,而链路抓包不能?
自测:Android 7 之后,为什么用户证书默认不生效?
自测:代理里一条记录都没有,最先怀疑什么?
自测:显示过滤器和抓包过滤器有什么区别?
自测:为什么说 keylog 文件等于万能钥匙?
-w 和 -s 0 各是什么意思?代理为什么能解密?Android 7 改了什么?抓不到包时第一步查什么?如果这五个问题你能顺下来,遇到"抓不到"就不会乱试了。
协议逆向方法论:从黑盒到可复现
你抓到一条登录请求,里面有二十多个字段,sign 是一串看不懂的十六进制,ts 每次都变。凭感觉试了三天,偶尔能通一次,但说不清为什么。问题不在技术,在于没有方法。这一页给一套可以照着做的实验流程:怎么拆、怎么比、怎么验证、怎么记。
11.1三种盒子:你手上有什么,决定你从哪下手
同样一个 App,三个人手里的东西可能完全不同:一个人只有一段抓包,一个人有安装包,一个人有源码。这三种起点,做法差得很远。用透明度打个比方,就是黑盒、灰盒、白盒。
- 黑盒(Black Box):只有一个输入口和一个输出口,里面完全看不见。你只能发请求、看响应,靠对比猜规律。
- 灰盒(Grey Box):外壳是半透明的。你有安装包,能反编译看到类名、字符串、调用关系,但看不到完整源码,也看不懂所有细节。
- 白盒(White Box):全透明。你有源码,或者有带完整符号表的二进制,逻辑直接摆在眼前。
| 盒子 | 你手上有什么 | 典型起点 | 常见卡点 |
|---|---|---|---|
| 黑盒 | 一段抓包,没有安装包 | 字段清单 + 差异对比 | 字段多、没有参照,容易瞎猜 |
| 灰盒 | APK 或 IPA,能反编译 | 先看字符串和类名,再回到流量 | 代码被混淆,关键逻辑在 native 层 |
| 白盒 | 源码,或带符号的二进制 | 直接读签名函数 | 版本不一致,或线上走了另一条分支 |
真实项目里,九成情况是灰盒:有安装包,但代码被混淆过,关键算法还塞进了 .so。所以这一页的方法默认按灰盒写,黑盒和白盒都能套用。
11.2八步工作流:顺序错了,全是返工
下面这八步是一条固定流水线。它的核心不是"步骤多",而是前四步只观察、不动手。绝大多数人失败,是因为第一步还没定清楚,就直接跳到第五步去脱壳了。
每一步的产出物写清楚,你就知道自己在哪:
定目标:缩到一条请求
把"搞懂这个 App 的登录"改成"复现 /api/login 请求体里的 sign"。产出:一句可验收的话。
抓样本:同一操作抓三次
一次不够。三次才能区分"固定值"和"每次变"。产出:三份完整请求,含头和体。
拆请求:分块归档
路径、方法、头、体分开列。产出:一张字段清单,见 11.3。
找变量:标出会变的部分
哪些每次变、哪些跟着别的字段变。产出:变量分类表,见 11.5。
后四步是另一套节奏:定位算法靠静态看结构、动态看入参;复现是自己写代码算一遍;对拍是拿真机结果逐字节比对;固化是把结论写成脚本和文档,让别人也能跑。
11.3输入输出清单法:把请求变成一张可实验的表
把一次请求想成一排开关。你要做的是:先数清有多少个开关,然后一次只拉一个,看灯怎么变。一次拉三个,灯变了你也不知道是谁干的。
先把所有字段抄成表。抄的时候不要漏:路径、方法、每个头、每个体字段,一个都不能少。
| 字段 | 示例值 | 长度 | 三次抓包的变化 |
|---|---|---|---|
| uid | 10086 | 5 位数字 | 固定 |
| ts | 1729900000 | 10 位数字 | 每次加约 8 |
| nonce | a7f3c9e1 | 8 位十六进制 | 每次不同,无规律 |
| sign | 9f2c...(32 位) | 32 位十六进制 | 每次不同,随 ts 变 |
列完之后做四件事:
- 给每个字段标上"固定 / 时间 / 随机 / 依赖别人"四类之一;
- 挑一个字段,只改它,其他一个字节都不动,发出去;
- 记录响应的状态码、错误码、提示语;
- 把这次实验写进记录表(模板在 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-f | MD5,或 16 字节数据的十六进制 | 16 字节乘 2 个字符 |
| 40 位,只有 0-9a-f | SHA-1 | 20 字节乘 2 |
| 64 位,只有 0-9a-f | SHA-256 | 32 字节乘 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、异或。
- 输出完全不变:这个字段根本没进计算,或者被服务端忽略了。
11.7对拍方法学:把服务器当成你的判卷老师
"对拍"这个词来自算法竞赛:写完程序,用一个肯定正确的慢程序跑同一组输入,比输出是否一致。在协议分析里,那个"肯定正确的慢程序"就是真实客户端加真实服务器。
对拍的核心概念叫 Oracle(预言机,这里指判定器):一个你能反复问"这个答案对不对"的东西。服务器就是最好的 Oracle,因为它会明确告诉你签名通过还是失败。
对拍有三种做法,解决的问题不一样:
同一输入,多次运行
输入完全不变,跑五次。输出每次一样,说明没有随机数;输出每次不同,说明有随机数或时间参与。
多设备、多账号运行
换一台手机、换一个账号,同样输入。结果变了,说明字段绑定了设备或账号。
逐字节比对
把两个结果按位置对齐,标出第几位开始不同。差异位置往往对应拼接顺序或字段长度。
这三种方法要配合"假设-验证"循环用。假设必须写成可以被证伪的句子,不能写成"应该是某种加密"这种模糊判断。
一个合格的假设长这样:
不好的假设
sign 应该是 MD5 之类的东西
好的假设
sign = MD5(uid + ts + key) 的小写十六进制
可证伪的预测:改 uid,sign 整段变化;改 nonce,sign 不变
写完预测,再去做实验。如果结果和预测一致,假设暂时成立;连续三次不一致,说明假设错了,回到起点重新提,不要在错的假设上继续加条件。
11.8实验记录、常见死胡同与最大的误区
方法论里最不起眼、但最能救命的是记录。人的短期记忆只能装三四个变量,第五个开始就全靠猜。一张表就能解决:
| 时间 | 改动 | 预期 | 实际与结论 |
|---|---|---|---|
| 10:20 | 基线,不改 | 登录成功 | 成功,记录 sign 原值 |
| 10:26 | ts 加 1 | sign 整段变 | 确实变了,ts 参与计算 |
| 10:33 | nonce 改一位 | sign 不变 | 不变,nonce 不参与 |
| 10:41 | uid 改成别人 | 签名失败 | 报错 1003,uid 参与 |
四列就够。写的时候注意两点:一是"预期"必须写,不写预期的实验等于没做;二是"实际"要抄原文错误码,不要写"失败了"。
下面这些死胡同,几乎每个人都会掉进去一次:
- 一上来就脱壳。还不知道要找什么,就先花两天脱壳,脱完发现关键逻辑根本不在被脱的那一层。
- 只看静态。反编译出来的代码可能被混淆、可能走不到、可能线上根本不执行。静态给结构,动态给事实。
- 把巧合当规律。改了 A 之后成功了一次,就认定 A 是关键。一次成功可能只是缓存、可能只是重试。
- 同时改多个变量。这是最浪费时间的做法,实验做完什么也证明不了。
- 不做记录。三天后回来,连自己改过什么都想不起来。
- 用"环境问题"解释对不上。对不上就是没懂,先把差异定位到字节,再谈环境。
最后说这一页最重要的误区:很多人以为"能复现一次"就等于"搞懂了"。
能复现一次,可能只是照抄了一次成功请求,或者运气好碰上了。真正的标准有三个:
- 给你一个没见过的输入,你能算出正确输出;
- 你能解释每个字段为什么这么算,包括顺序、编码、大小写;
- 别人拿着你的记录,不用问你也能重跑一遍。
三条都满足,才叫可复现。差一条,都还停在"试出来了"的阶段。
自测:黑盒、灰盒、白盒,做法的差别到底在哪?
自测:一个 32 位、只有 0-9a-f 的字段,能不能直接判定是 MD5?
自测:两次请求只有 ts 不同,sign 也跟着变了,能得出什么结论?
自测:为什么强调"一次只改一个变量"?
自测:能复现一次,算不算搞懂了?
Android 逆向基础:APK、DEX 与 Frida
你下载了一个 APK,双击解压,看到一堆不认识的文件夹;用工具反编译,出来几千个文件,不知道从哪看起。这一页把 Android 拆成四层:安装包、字节码、原生库、运行时,每一层告诉你它是什么、用什么工具看、能看到什么。
12.1APK 的本质:一个改了后缀的 zip
APK(Android Package,安卓安装包)不是什么神秘格式。把 app.apk 改名成 app.zip,用任何解压工具都能打开。它就是一个压缩包,里面按固定目录放了几类文件。
这一点很重要:你不需要特殊工具就能看到它的骨架。解压出来先看一眼目录,比直接上反编译工具更快建立整体印象。
每个位置对应一个问题:
- AndroidManifest.xml:这个 App 叫什么、能干什么、从哪个界面启动、申请了哪些权限。
- classes.dex:Java 和 Kotlin 代码编译后的字节码,业务逻辑主要在这里。
- lib/:C 和 C++ 编译出来的原生库,加密、签名、反调试常藏在这里。
- res/ 与 assets/:界面、图片、字符串、配置文件。
- META-INF/:签名信息,用来证明这个包没被人改过。
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(那就是桌面图标的入口),再从它的代码里看它启动了哪些界面、调用了哪些网络类。
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回编译、重新签名。
回编译之后必须重新签名,否则装不上。这一步用的是你自己的调试证书,只对你自己测试的设备有意义。
12.5native 库与 JNI:Java 怎么落到 .so
Java 代码跑在虚拟机里,碰不到硬件,也做不了高性能计算。需要时,它会调用 C 或 C++ 写的函数,这些函数编译成 .so(Shared Object,共享库)放在 lib/ 下。
连接两边的桥叫 JNI(Java Native Interface,Java 原生接口)。可以把它想成翻译:Java 说"我要调用 sign",JNI 负责把这句话翻译成 C 函数能听懂的参数格式。
名字绑定有两种方式,这决定了你在 .so 里怎么找函数:
- 静态注册:函数名按
Java_包名_类名_方法名拼出来。在 IDA 的导出表里直接搜方法名就能找到。 - 动态注册:在
JNI_OnLoad里用RegisterNatives把名字和函数地址绑起来。导出表里看不到业务函数名,只能从JNI_OnLoad顺藤摸瓜。
看到动态注册不用慌。它只是"把电话本换成了另一张纸",函数本体还在,只是名字不写在导出表上。顺着 JNI_OnLoad 里的那张表,一个个对过去就行。
lib/ 下常按 CPU 架构分目录,比如 arm64-v8a、armeabi-v7a、x86_64。分析时优先看 arm64-v8a,因为现在的手机基本都是这个架构。
12.6Frida 原理:在运行时看真实入参
静态分析有个天生的短板:代码可以加密、可以混淆、可以动态加载。但程序要运行,就必须把真实的参数交给真实的函数。Frida 就是在这个时刻插一脚。
它的原理分三步:
- 往目标进程里注入一个 agent(代理模块),让目标进程多出一段外来代码;
- 这段代码里带一个 JavaScript 运行时,你用 JS 写逻辑;
- 用
Interceptor把目标函数的开头改成跳转,先跳到你的回调,再跳回原函数。
下面两个脚本都只做观察,不改任何逻辑。第一个挂在 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);
}
});
这两段代码的价值不在语法,而在思路:与其猜算法,不如让程序自己告诉你它收到了什么、算出了什么。拿到真实入参之后,再回到静态代码里对照,比纯看汇编快得多。
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 挂钩,只是读起来更费劲。
自测:APK 里最该先看哪个文件,为什么?
自测:为什么读代码用 jadx,改代码却要用 apktool?
自测:Java 里的 native 方法,真正的代码在哪里?
lib/ 目录下对应架构的 .so 文件里。Java 层只有一个声明,运行时通过 JNI 按名字或注册表找到函数地址再跳过去执行。所以看到 native 就要转去分析 .so。自测:混淆和加密的区别是什么?
自测:为什么说 root 不是协议分析的必要条件?
iOS 逆向基础:Mach-O、签名与运行时
iOS 的安装包不像 APK 那样一眼能看穿:它没有 DEX,代码是编译好的机器码;它还有一整套签名与授权机制,让"装不上"和"跑不起来"成为常态。这一页讲清四件事:IPA 里有什么、Mach-O 怎么读、签名在防谁、运行时怎么观察。
13.1IPA 的结构:也是一个 zip
IPA(iOS App Store Package,iOS 应用包)和 APK 一样,本质是 zip。把它改名成 .zip 就能解压。但它的目录结构只有一个固定入口:Payload/。
用寄快递打比方:Payload 是外箱,MyApp.app 是里面那个贴着标签的包裹,包裹里才是真正的东西。
四样关键东西,各管一件事:
- 可执行文件:和 .app 同名的那个文件(没有后缀),就是 Mach-O 格式的程序本体。
- Info.plist:应用的身份证,写着包名(Bundle ID)、版本、支持的设备、启动配置。
- embedded.mobileprovision:描述文件,写着"哪个证书签的、允许哪些设备装、能用哪些能力"。
- Frameworks/:内嵌的动态库,第三方 SDK 通常以这种形式存在,每个都是独立的 Mach-O。
13.2Mach-O:iOS 程序的真实形状
Mach-O(Mach Object,Mach 目标文件格式)是苹果系统的可执行文件格式。它的结构像一本印刷书:先有封面和目录,再有正文,正文按章节分块。
三大部分,从前往后:
- Header(头部):开头一小段,说明"我是什么架构、有多少条目录项"。头 4 个字节是魔数,用来确认这是 Mach-O。
- Load Commands(加载命令):一串目录项,每一项说"某一段在文件的哪个位置、多大、加载到内存哪里"。
- Segments 与 Sections(段与节):真正的数据。段是大块(代码段、数据段),节是段里的细分(代码段里的指令、常量字符串)。
实际分析时,最常打交道的不是头部,而是两个地方:
__TEXT段里的__text节:所有机器指令都在这里。反汇编工具默认打开的就是它。__cstring节:常量字符串。接口路径、错误提示、密钥片段、加密算法名,常常在这里露出来。
还有一类文件叫胖二进制(Fat Binary)或通用二进制:一个文件里同时包含 arm64 和 x86_64 等多份代码,方便在不同设备上运行。分析前要先用工具"瘦身",只留你关心的架构,否则地址会对不上。
13.3签名机制:不是加密,是封条
很多人把 iOS 签名理解成"给应用加密"。不是。签名更像火漆印:它不隐藏内容,只证明"这封信是谁写的、路上有没有被拆过"。
一套完整的签名,涉及四样东西:
| 环节 | 作用 | 失效会怎样 |
|---|---|---|
| 证书 | 证明签名者身份 | 系统不认识签名者,直接拒绝 |
| 描述文件 | 限定设备、权限、有效期 | 设备不在名单或已过期,装不上 |
| 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:,一眼就能看出用途。
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 对证书的信任分两层:
- 把代理工具的根证书装进设备(作为描述文件安装);
- 到"证书信任设置"里手动打开完全信任。
只做第一步是不够的,这是 iOS 和很多 Android 设备最大的操作差异。很多人卡在"证书装了但还是抓不到",原因就是第二步没开。
即使两步都做了,仍可能看不到明文。因为应用可以做证书固定(Certificate Pinning):不信任系统证书库,只认自己内置的那张证书或公钥。这时握手会失败,流量依然加密。
从防守方角度,证书固定是有效手段;从分析角度,它是一个明确的信号:看到握手失败而不是乱码,就该怀疑固定,而不是怀疑抓包工具坏了。
13.10两个必须纠正的误区
误区一:不越狱就没法分析 iOS 应用。
不对。越狱解决的是"往别人的进程里注入代码"这一件事。而下面这些工作都不需要越狱:
- 解压 IPA、读 Info.plist、看资源文件;
- 对已解密的 Mach-O 做静态反汇编、搜字符串、看依赖库;
- 用开发者证书给自己编译的测试应用做动态调试;
- 在信任了证书的前提下抓自己应用的流量。
越狱只是让"动态注入"这一步更省事。把越狱当成前置条件,会让很多人还没开始就放弃。
误区二:iOS 一定比 Android 安全。
这句话混淆了"恶意软件少"和"不可被分析"。iOS 的封闭生态确实降低了普通用户中招的概率:应用必须经过审核、不能随意侧载、沙盒更严格。但这不等于系统不可分析,也不等于没有漏洞。
更准确的说法是:两者的威胁模型不同。
| 维度 | iOS | Android |
|---|---|---|
| 应用来源 | 以官方商店为主,侧载受限 | 渠道多,侧载容易 |
| 权限模型 | 粒度较粗,但审核严格 | 粒度细,用户可控性高 |
| 分析难度 | 签名与加密门槛高 | 包结构开放,工具链成熟 |
对做安全的人来说,结论是:没有"更安全的平台",只有"不同的攻击面"。iOS 把门槛放在了签名和加密上,Android 把门槛放在了混淆和加固上,两边都要学。
自测:IPA 解压后,真正可分析的目录是哪一个?
Payload/。里面只有一个 .app 目录,可执行文件、Info.plist、描述文件、Frameworks 都在它里面。其他位置基本是打包工具留下的附属文件。自测:签名是加密吗?它到底防什么?
自测:为什么 ObjC 应用比 Swift 应用更容易做动态分析?
自测:iOS 抓包时证书装了还是抓不到,最可能是什么原因?
自测:不越狱能不能分析 iOS 应用?
加固、混淆与反调试:对抗的成本
很多人第一次拆一个商业 App,打开反编译工具会愣住:类名全是 a.b.c,字符串全是乱码,调试器一附加程序就退出。这不是工具坏了,而是对方花钱买了对抗。这一页讲清楚加固到底做了什么、为什么有效、边界在哪里。
14.1先给结论:加固是把成本抬高
把一个 App 装进保险柜,和把它装进纸箱,区别不在"能不能打开",而在"打开要多久、要什么工具、要懂多少东西"。加固(Hardening)做的正是这件事。
它抬高三类成本,缺一不可:
- 时间成本:原来十分钟能读完的逻辑,现在要三天。
- 工具成本:原来一个免费反编译器就够,现在要会写脚本、会看汇编、会搭动态环境。
- 知识成本:原来会 Java 就行,现在要懂链接器、内存布局、进程状态。
对防守方来说,这是一笔很划算的买卖:一次投入,长期生效。对分析方来说,这是一笔持续支出:每出一个新版本,之前的经验要重来一遍。
所以评估一个 App 的加固,不要问"能不能破",要问"破它值不值"。这句话是整章的主线。
14.2混淆:让代码能跑但读不懂
混淆(Obfuscation)的生活比喻是:把一篇文章里所有人名换成"甲、乙、丙",再把段落顺序打乱。文章照样能读通,但你已经不知道谁是谁。
它在编译阶段完成,代码的行为完全不变,变的只是"名字"和"结构"。常见四种手法:
改名(R8 / ProGuard)
把 checkLicense 改成 a,把 LicenseManager 改成 b。名字里不再有任何线索。
字符串加密
代码里出现的网址、密钥提示、错误文案,全部变成密文,运行时由一个小函数现场解出来。
控制流平坦化
把顺序执行的语句塞进一个巨大的 switch,靠一个状态变量决定下一步跳哪。读起来像迷宫。
无效分支
插入大量永远不成立的判断和死代码,让真正的逻辑淹没在噪声里。
| 手法 | 改了什么 | 分析方的代价 |
|---|---|---|
| 改名 | 类名、方法名、字段名 | 要顺着调用关系重新起名 |
| 字符串加密 | 常量变成密文 | 要先找到解密函数并还原 |
| 控制流平坦化 | 执行顺序被打散 | 要重建状态机才能读懂 |
| 无效分支 | 插入大量死代码 | 要逐条判断哪些分支有用 |
关键判断:混淆完全不改变程序语义。所以它只对"静态阅读"有效,对"动态观察"几乎无效。程序运行时该调用的函数照样调用,该传的参数照样传。
14.3加壳:外面套一层,运行时才解开
加壳(Packing)的比喻是快递里的泡沫箱:你收到的是一个箱子,拆开泡沫才见货。App 也一样,磁盘上那份文件里装的是"壳",不是真正的代码。
常见三层,保护强度递增:
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 | 核心算法 | 很高,要还原解释器 |
14.4完整性校验:改了就要被发现
完整性校验(Integrity Check)的比喻是门上的封条:内容不重要,重要的是"有没有被撕开过"。
它有四种常见强度,从弱到强:
| 方式 | 强度 | 常见时机 |
|---|---|---|
| CRC 校验 | 弱,改一个字节就变 | 资源文件加载前 |
| 文件哈希 | 中,比对 SHA-256 | 启动时、关键功能前 |
| 签名校验 | 强,比对签名证书摘要 | 启动时、登录时 |
| 分段校验 | 很强,多点分散 | 分散在多个业务流程里 |
签名校验值得单独说。Android 安装包必须签名,重打包必然换证书。客户端只要读出自己的签名摘要,和内置的期望值比一下,就知道有没有被改过。
分段校验是"分散投资":不在一处检查全部,而是把校验点撒在启动、登录、下单、支付前。改一处只能过一关,下一关还会拦。
14.5反调试:让你连不上
反调试(Anti-Debugging)的目标不是"让你看不懂",而是"让你没法停下来看"。调试的核心动作是"暂停程序、检查状态",反调试就围绕这一点做文章。
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 的思路是:不管你用什么框架,它都要在进程里留下痕迹。
痕迹主要有四类:
| 检测面 | 观察对象 | 说明 |
|---|---|---|
| 端口 | 本机监听端口 | 注入框架默认会开固定端口 |
| 内存映射 | 进程加载的库列表 | 多出来的陌生库很可疑 |
| 线程名 | 运行中的线程 | 框架的线程名有固定特征 |
| 进程与文件 | 运行中的进程、临时文件 | 服务端进程名是公开信息 |
这四类检测的共同点是:它们观察的是"环境",不是"代码"。所以加固厂商会把它们打包成一个"环境体检"模块,在启动、登录、支付前各跑一次。
但反过来,环境检测也有代价:扫描内存映射、遍历线程都要时间,做得太频繁会拖慢启动,做得太粗会误伤正常用户(比如某些安全软件、无障碍服务、输入法也会注入进程)。
14.7多进程与双进程保护
单进程的保护有个天然弱点:所有判断都在一个进程里,只要控制了这个进程,判断就全废了。于是有了多进程方案。
比喻:两个保安互相盯着,一个被放倒,另一个立刻报警。
常见做法有三种:
- 守护进程:主进程和一个轻量守护进程互相心跳,一方消失另一方重启它。
- 校验分工:把签名校验、环境检测放到独立进程,主进程只接收结论。
- 数据分散:密钥分片存放在不同进程,单独一个进程拿不到完整密钥。
但这条路在近年的 Android 上越来越难走。系统对后台进程限制很严,守护进程被杀后不一定能拉起来,反而导致崩溃率上升、耗电变差。所以现在的产品更倾向于"多进程分工",而不是"互相拉活"。
14.8怎么评估一个 App 的加固强度
不要凭"打开反编译器看到什么"下结论。用四个维度打分,才比较靠谱。
| 维度 | 观察方法 | 弱的表现 |
|---|---|---|
| 静态可读性 | 反编译后有多少有意义的类名和字符串 | 能看到业务词汇、接口路径 |
| 动态可观察性 | 能否附加、能否看到关键函数入参 | 一附加就退出,或毫无阻碍 |
| 校验点分布 | 几个校验、在哪些时机触发 | 只有启动时一次 |
| 崩溃行为 | 被改动后是静默失败还是立即崩溃 | 改了也没反应,说明没校验 |
第三项最容易被忽略。一个只在启动时校验一次的 App,和分别在启动、登录、下单、支付前校验四次的 App,防护强度差一个数量级。
第四项是"诚实度测试":故意改一个无关紧要的字节,看它有没有反应。反应越干脆(立刻退出、功能不可用),说明校验越硬;毫无反应,说明基本没做完整性保护。
14.9攻防的经济学:谁更划算
把加固看成一场成本竞赛,很多现象就解释得通了。
| 环节 | 防守方投入 | 攻击方每次成本 | 结果 |
|---|---|---|---|
| 改名混淆 | 几乎为零,编译开关 | 低,读慢一点 | 防守方划算 |
| 字符串加密 | 低,改构建脚本 | 中,要找解密函数 | 防守方划算 |
| DEX 壳 | 中,买或自研 | 中高,要抓解密点 | 看版本迭代速度 |
| VMP | 高,性能与人力 | 很高,要还原解释器 | 只保护核心算法 |
关键洞察:防守方一次投入长期有效,攻击方每个版本都要重来。所以加固真正的价值不在"挡住一次",而在"让每次都要重新付出"。
反过来说,如果一个 App 一年只发一个版本,加固的边际收益就低;如果两周发一版,加固的收益会成倍放大。这也解释了为什么大厂更愿意在加固上花钱。
14.10两个最常见的误区
误区一:混淆等于加密。很多人看到乱码字符串,就以为内容被加密了、拿不到了。其实混淆后的代码必须能正常运行,而运行就必须解密。解密函数就在同一个文件里,只是名字难看。加密保护的是"传输和存储",混淆保护的是"阅读体验",两件事。
误区二:加壳等于安全。壳只保护磁盘上的文件,程序跑起来之后内存里一定是明文。而且壳本身是一个复杂的加载器,历史上不少壳自己就有漏洞,反而扩大了攻击面。
还有一个变体误区:以为加固能防住"有授权的人"。加固防的是批量和自动化,防不住一个愿意投入几周时间、并且有合法授权做安全评估的团队。这是设计目标决定的,不是实现缺陷。
14.11合规提醒
加固分析在合法场景里非常常见:安全公司做渗透评估、厂商做兼容性排查、研究机构分析恶意样本、开发者检查自家 App 有没有被二次打包。
这些场景有一个共同点:要么是自己的 App,要么有书面授权。
- 只分析自己开发的、或客户书面授权的 App。
- 分析他人 App 的付费、授权、会员逻辑并加以利用,可能构成侵权甚至犯罪。
- 不要把分析结果、密钥、绕过方法公开传播。
- 涉及用户数据时,遵守最小必要原则,能不留存就不留存。
自测:混淆和加密,最本质的区别是什么?
自测:为什么说"解密点"是所有壳的共同软肋?
自测:一个 App 只在启动时校验一次签名,这个设计的问题在哪?
自测:为什么加固厂商更愿意"降级"而不是"直接崩溃"?
SSL Pinning 与中间人:证书信任的边界
很多人第一次抓包会遇到同一种挫败:代理装好了,证书也装好了,浏览器能抓,偏偏这个 App 一点流量都没有,或者干脆报错退出。原因几乎总是同一个:这个 App 不信任"系统里所有受信任的证书",它只信任自己认定的那一张。
15.1中间人是怎么成立的
先说清楚一件事:中间人(Man-in-the-Middle,MITM)不是"破解"了 TLS,而是把一条 TLS 拆成了两条。
生活比喻:你以为在跟房东打电话,其实中间有个中介。中介一边跟你说"我是房东",一边跟真房东说"我是租客"。两边都以为在跟对方说话。
技术上,抓包代理干的就是中介的活:
所以问题变成:客户端为什么会接受"代理的证书"?因为代理的证书也是某个受信任的机构签的——只要你在手机上装了代理的根证书,系统就认为它是合法的。
这正是抓包工具的标准流程:生成一张自己的根证书,让你装进系统信任库。从系统角度看,它和正规证书颁发机构没有区别。
15.2证书校验的三个层次
客户端验证一张证书,通常分三步,像机场安检的三道关:
第一层,系统信任库。手机里预装了一百多个受信任机构的根证书,操作系统和浏览器都认这份名单。你在设置里装的证书,也会进这份名单。
第二层,主机名校验。证书上写着"这个证书属于哪些域名"。如果证书是给 api.example.com 的,你却拿它去访问 pay.example.com,就要报错。
第三层,证书固定。客户端不看"谁签的",只看"是不是那一张"。这一层是应用自己实现的,不是 TLS 协议强制的。
15.3证书固定的三种类型
"固定"(Pinning)的意思是:把某个值提前写进客户端,运行时比对。固定的对象不同,严格程度和可维护性也不同。
15.3.1 整证书固定
把整张证书的字节做一次哈希,客户端存这个哈希。服务器换证书(哪怕只是续期),哈希就变了,客户端立刻连不上。
比喻:门卫手里拿着你的整张身份证复印件,你换了张新身份证(号码没变),他也不认。
所以这种方式几乎只用在极少数场景,比如固定硬件的物联网设备。
15.3.2 公钥固定
只比对证书里的公钥。证书可以换,只要公钥不变就能通过。
但公钥是一段很长的二进制,要在客户端里解析证书、取出公钥再比对,实现成本高,也容易因为解析差异出问题。
15.3.3 SPKI 哈希固定
SPKI(Subject Public Key Info)是证书里"公钥加算法标识"那一段。把它单独取出来做 SHA-256,得到一串 32 字节的值,编码成 Base64 就是一串短文本。
客户端只需要算一次哈希,和存好的字符串比一下。简单、稳定、体积小,所以 OkHttp、AFNetworking 这类库默认都走这条路。
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/哈希值"。
它的好处是粒度细:可以只对支付相关域名严格固定,对日志上报域名放宽。坏处是绕过了系统配置,排查问题时不容易一眼看出来。
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 或书面授权的目标做抓包分析。
- 不要在他人设备上安装根证书。
- 抓到的流量里可能包含个人信息,按最小必要原则处理,用完即删。
- 企业内部审计必须告知员工,并限定范围与保存期限。
自测:中间人为什么能"看见"加密流量?
自测:为什么证书固定能挡住中间人,即使客户端已经装了代理的根证书?
自测:SPKI 哈希固定为什么比整证书固定更适合移动端?
自测:App 里所有请求都超时,浏览器访问同一域名却正常,最可能是什么原因?
设备指纹与风控:八类参数与一致性
你有没有想过:为什么换了个手机号注册,平台还是知道"又是你"?为什么刚注册的新号,第一次下单就被要求人脸验证?答案通常不是某个神秘的"唯一编号",而是一堆普通参数拼出来的画像,再加上它们之间的一致性。
16.1什么是指纹:稳定和唯一的拉扯
设备指纹(Device Fingerprint)的定义很朴素:从一台设备上采集一组参数,用来在多次访问之间认出"这是同一台"。
它有两个目标,而这两个目标天然冲突:
- 稳定性:同一个用户下次来,这些值最好别变。
- 唯一性:不同用户之间,这些值最好别撞。
比喻:用"身高"当指纹,很稳定(成年后基本不变),但一点都不唯一(一米七的人太多了);用"今天穿的衣服"当指纹,很唯一,但明天就失效了。
一个好的指纹要同时满足四个条件:稳定、唯一、难伪造、可采集。现实中没有任何一项能全占,所以工程上用的是"多项加权":单个值只贡献一点分数,组合起来才有判断力。
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 指纹都属于某 App 的官方库,只有一小撮请求的指纹属于某个脚本库——这一小撮就值得重点看。
但它不能单独用来认人。同一个版本的 App,几百万用户共享同一个指纹。把它当"唯一 ID"用,是典型误用。
16.7HTTP 头的顺序和大小写
还有一个更细的层面:HTTP 请求头的顺序、大小写、有没有多余空格。
正规的网络库通常按固定顺序输出头,大小写也统一。而用脚本手工拼出来的请求,顺序往往和官方库不一致,甚至会出现不该有的头。
比喻:伪造一封信,内容可以抄得一字不差,但信封的折法、邮票贴的位置、笔迹的力度,往往会露馅。
服务端把"头顺序 + 头名大小写 + 头集合"作为一个整体特征,和账号、设备、IP 一起看,就能发现"这个请求不是这个 App 发的"。
16.8一致性校验:矛盾才是信号
前面讲的都是"采集"。真正决定风控效果的,是参数之间对不对得上。
单点异常很常见(用户换了网络、装了新输入法),所以不会直接判定。但多个维度同时矛盾,就很难解释了。
常见的矛盾类型有四组:
- 性能与行为:参数显示是低端机,行为却是高频并发。
- 时间与地点:时区、语言、IP 归属地三者互相打架。
- 新与重:设备是全新的,但操作路径像老手,直接找到最深的功能。
- 账号与设备:一个新设备,登录了一堆注册时间接近、行为相似的账号。
这些矛盾单看都不致命,组合起来就有说服力。风控的本质是概率判断,不是事实认定,所以它给出的通常是"分数"和"动作",而不是"结论"。
16.9关系图谱:从单点走向网络
单台设备的信息再多,也有上限。真正的识别能力来自关系。
把账号、设备、IP、支付方式、收货地址都当成"点",把它们之间的使用关系当成"线",就得到一张图。风控在这张图上找"团伙":
- 多个账号共用同一台设备;
- 多个设备共用同一个支付账号;
- 一批账号在短时间内从同一批 IP 登录;
- 多个账号填写的收货地址高度重合。
比喻:单看一个人可能毫无问题,但一看他的人际关系图,全是同一伙人,那就值得注意了。
图谱的价值在于抗伪造:你可以改设备参数,但改不了"你的支付账号和另外二十个账号绑在一起"这个事实。
16.10隐私合规:能哈希就不存明文
指纹技术天然涉及个人信息,所以合规不是可选项。
- 告知同意:采集哪些参数、用来做什么、保存多久,要说清楚。
- 最小必要:能不用 IMEI 就不用,能用匿名标识就不用硬件号。
- 能哈希不存明文:设备号、手机号这类值,存哈希比存原文安全得多。
- 可撤回、可删除:用户要求删除时,要能真正删掉。
哈希也有讲究。手机号只有十一位,直接做一次 SHA-256 是可以被穷举的——攻击者把所有可能的号码都算一遍,就能反查。所以正规做法是加盐(在原文里混入一段随机值)或用带密钥的哈希。
16.11两个最常见的误区
误区一:改一个参数就能换身份。很多人以为改掉设备号就"变成新设备"了。但风控看的是组合:设备号变了,硬件参数没变、行为习惯没变、IP 没变、支付方式没变,几十个维度里只有一个变了,反而更可疑——正常用户换手机,是整机参数一起换。
误区二:指纹等于唯一 ID。指纹是一组概率性的特征,不是数据库主键。它会有碰撞(两台设备算出一样的组合),也会有漂移(系统升级后特征变了)。所以工程上用的是"置信度",不是"是或否"。
还有一个相关误区:以为指纹只在客户端采集。实际上服务端能看到的东西同样多,甚至更多——TLS 指纹、请求头顺序、时序特征,这些客户端根本改不了。
自测:为什么没有"完美的单一设备标识"?
自测:Android ID 为什么不能用来跨应用追踪同一台手机?
自测:TLS 指纹能识别"人"吗?
自测:为什么"只改一个参数"反而更容易被识别?
纯协议登录:一个完整案例的全链路
不启动 App,只用代码把登录过程重做一遍,到底要发几个请求、带哪些字段、在哪一步判断成败?这一页用一个脱敏的电商类 App 做样本,把六个请求串成一条可以逐步验证的链路,并告诉你什么才算真正跑通。
17.1什么叫"纯协议"
"纯协议"这三个字,指的是不启动 App、不点界面,直接用代码构造 HTTP 请求,把 App 在背后做的事情在命令行里重做一遍。它不需要手机,也不需要模拟器,只需要一台能联网的电脑。
你可以把它想成不去柜台排队,而是照着柜台的表单格式,把材料直接寄过去。表单的栏目、顺序、盖章位置都必须对,错一项就被退回。
要跑通它,至少需要三样东西:
- 看得懂请求:知道每一步发到哪个路径、带哪些字段,这来自抓包;
- 算得出签名:请求里通常有一个随时间变化的签名字段,这来自逆向或算法还原;
- 保得住会话:登录成功后拿到的 Token 要能存下来、带上去、及时换。
17.2案例背景:一条被拆成六步的登录链
样本是一个电商类 App 的账号密码登录。为了避免指向任何真实系统,下文域名统一写成 api.example.com,路径和字段名都做了替换,数值只作示例。
这个 App 的登录不是"一个请求搞定",而是六个请求前后依赖,像一条流水线:
- 设备注册:先告诉服务端"我是一台什么设备",换回一个设备编号;
- 预登录:领取本次登录专用的盐值和挑战码;
- 登录:提交账号、密码派生值、挑战答案和签名;
- 换取 Token:用登录结果换访问凭证;
- 拉用户信息:验证这张凭证是不是真的能用;
- 业务接口:取购物车、订单等真实数据,确认整条链真的通了。
依赖关系很清楚:第 1 步的 deviceId 是后面每个请求的必需参数;第 2 步的盐值和挑战码决定第 3 步的签名内容;第 4 步的 Token 决定第 5、6 步能不能过。
这条链有个很坑的特点:任何一步失败,后面都会以"参数缺失"或"未授权"结束,而不是告诉你真正的原因。所以排错必须从第一步开始,不能从报错的那一步猜。
17.3全链路时序:六个请求的先后关系
为什么第一步必须是设备注册?因为服务端需要给你发一个稳定的编号,用来关联后续的登录、风控和行为记录。没有这个编号,登录请求会被当成"来路不明",很多系统会直接返回一个含糊的错误。
为什么要有预登录?如果每次登录都用同一个固定签名,别人抓一次就能无限重放。预登录下发一次性的盐值和挑战码,让每次的签名都不一样。这就像考试前先领一张带编号的答题卡,抄上次的答案没有用。
17.4每一跳的请求结构
下面这张表只列决定成败的字段。真实请求还会有一些通用头,例如内容类型、客户端标识,它们不参与核心判断,但缺失时同样可能被拒。
| 步骤 | 方法与路径 | 关键字段 | 作用 |
|---|---|---|---|
| ① 注册 | POST /v1/device/register | model、osVer、installId、ts、sign | 建立设备档案,换 deviceId |
| ② 预登录 | POST /v1/auth/prelogin | account、deviceId、ts、sign | 领取 salt 与 challenge |
| ③ 登录 | POST /v1/auth/login | account、pwdHash、answer、nonce、sign | 提交凭证,服务端校验 |
| ④ 换 Token | POST /v1/auth/token | ticket、deviceId、sign | 换 accessToken 与 refreshToken |
| ⑤ 用户信息 | GET /v1/user/profile | Authorization、deviceId | 确认 Token 可正常使用 |
| ⑥ 业务数据 | GET /v1/cart/list | Authorization、deviceId、page | 取真实数据,判断链路真通 |
17.5签名参数从哪来
签名的本质,是给这一堆参数按一个只有双方知道的规则算一个"指纹"。服务端收到后按同样规则再算一遍,两边一致才认。所以签名的输入必须双方都能独立算出来。
这个案例里,签名用到四类输入:
- 时间戳 ts:10 位秒级数字。服务端只接受与服务器时间相差 300 秒以内的请求,过期直接拒,用来压缩重放的窗口;
- 随机数 nonce:每次请求重新生成。服务端短期缓存用过的值,重复即拒,用来防止同一时间窗内的重复提交;
- 设备标识 did:注册时拿到。它把签名和具体设备绑在一起,换设备后旧签名就失效;
- 请求体哈希 bodyHash:把请求体按固定规则序列化后做 SHA-256。任何一个参数被改,哈希就变,签名随之失效。
用伪代码写出来大概是这样,注意这里只是示意,真实项目里参数名和规则都不一样:
# 伪代码:算一次签名
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,重复登录会把旧的顶掉。
17.7端到端判据:不是 HTTP 200
初学者最容易在这里误判:看到状态码 200、看到返回里有一个 code: 0,就认为登录成功。其实网关只要把请求转给后端就会回 200,业务失败也常常包在 200 里,用响应体里的业务码表示。
为什么"有 Token"也还不算?因为 Token 可能是空串、可能是过期占位符、也可能权限不足。真正的判据是:调一个必须登录才能访问的业务接口,返回的数据字段非空且结构符合预期。
把判据写成断言,程序才能自己判断成败,而不是靠人看日志:
- 状态码等于 200,且响应体能正常解析;
- Token 字段存在,长度大于 20,字符集符合预期;
- 用户信息里 uid 非空,昵称或手机号后四位存在;
- 购物车列表能解析成数组,且元素数量与账号状态相符。
17.8失败分支与错误码
排错的第一步不是改代码,而是把失败分类。同样是"登录失败",原因可能完全不同,处理方式也相反。
| 现象 | 可能原因 | 该怎么做 | 别做什么 |
|---|---|---|---|
| HTTP 401 | Token 过期或被顶掉 | 刷新一次,失败再重登 | 无限重试 |
| 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,为什么还不能算成功?
自测:遇到 403 和 429,处理方式有什么不同?
签名与风控包:构造原理与一致性
服务端看不见你的手机,它凭什么判断一个请求"像人发的"?答案藏在客户端随请求附带的那一包字段里。这一页讲清风控包的分层结构、签名链路,以及为什么单点伪装没有用。
18.1风控包是什么:一份客户端自报的体检报告
你打开 App,服务端其实什么也看不见。它不知道你的手机是什么型号、屏幕多大、在哪个时区、刚才点得快不快。这些信息只能由客户端自己报上去。
客户端上报的这组信息,通常塞在请求头的一个 JSON 字段里,或者放在请求体的固定位置。业内叫法不统一,有叫设备信息、有叫风险参数、有叫指纹包,这一页统称"风控包"。
它不是 HTTP 标准的一部分。每个公司自己定义字段名、编码方式和加密方式,所以你在不同 App 上看到的形态完全不一样。这也意味着没有通用模板,只能一个目标一个目标地看。
为什么要有这个东西?因为"自报"天然不可信。服务端能做的,是让自报的细节足够多、彼此足够自洽,从而提高伪造的代价。它不追求"证明你是好人",只追求"让造假变得不划算"。
18.2风控包一般包含哪几类字段
虽然字段名千差万别,但按用途归类,基本逃不出六类。理解分类比记住字段名有用,因为字段会变,分类不会。
| 类别 | 典型内容 | 服务端用它判断 |
|---|---|---|
| 设备 | 机型、屏幕分辨率、CPU 核数、传感器数量 | 是不是真实设备,是否模拟器 |
| 环境 | 时区、语言、系统版本、亮度、字体列表 | 环境是否自洽,是否被改动 |
| 行为 | 点击间隔、滑动轨迹、页面停留、输入节奏 | 操作像人还是像脚本 |
| 应用 | 版本号、渠道、安装来源、首次安装时间 | 是否官方包,是否刚装上 |
| 网络 | 运营商、Wi-Fi 标识哈希、IP、代理特征 | 地理位置,是否经过代理 |
| 账号 | 注册时长、历史登录设备数、近期操作 | 这个账号本身是否异常 |
18.3为什么字段这么多:细节密度就是成本
一个字段很好编,十个字段就难了,几十个彼此关联的字段几乎编不圆。这就是风控包设计的核心逻辑:用细节密度换伪造成本。
举个具体例子。一台真实手机上报的字段是相互约束的:
- 机型决定了屏幕分辨率的合理范围;
- 分辨率决定了像素密度和屏幕尺寸;
- 系统版本决定了可用的接口和字体集合;
- 时区决定了语言习惯和当前时间;
- 传感器数量和型号决定了这台设备能不能"感知"移动。
如果你只改机型,分辨率却还是五年前小屏机的值,传感器数量是 0,系统版本又是最新的,这三条放在一起就互相打架。服务端不需要知道哪一条是假的,只要发现"这个组合在真实设备里不存在",就可以打低分。
18.4签名与加密:六个层次别混在一起
很多人把编码、哈希、加密、签名混成一件事。它们在链路里各占一层,作用完全不同。先看一条完整的处理链:
三件事必须分清:
- 编码(如 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服务端可能怎么校验
服务端的校验通常不是一道门,而是五层筛子。任何一层不过,结果不一定是拒绝,更常见的是"降级"。
把五层拆开看:
- 验签:重算签名,不一致说明参数被改过或密钥不对;
- 时间窗:时间戳偏差过大,说明请求是旧的或时钟被改过;
- 频率:同一设备或同一地址在短时间内请求过多;
- 画像:字段组合与历史记录、机型约束是否匹配;
- 序列:请求顺序是否像正常业务,例如没浏览商品就直接下单。
这五层不是每次都全跑。低风险请求可能只过前两层,高风险请求才会走完整流程。所以你在外部看到的现象经常是"有时成功有时失败",而不是稳定的通过或拒绝。
18.9检测与防护视角:为什么单点伪装没用
安全上有个木桶效应:水位由最短的那块板决定。风控包也是,整体可信度由最不自洽的那个字段决定。你把机型改得天衣无缝,只要时区和地址对不上,前面的努力全部作废。
从防守方角度看,一套好的校验体系要满足四点:
- 多维度:不依赖单一字段,任何单点被攻破都不致命;
- 可解释:每个低分要能说清是哪条规则触发的,方便运营复核;
- 可运营:阈值能调、规则能灰度、误伤能回滚;
- 可对抗:定期看新型伪造手法,更新约束关系。
反过来,从分析者角度看,这也解释了为什么"找一个能过检的字段"是伪命题:系统看的不是某个字段的值,而是整份报告的内部逻辑。这一页只讲原理,不针对任何具体平台提供操作步骤。
18.10合规提醒
风控包里大量字段与个人信息相关:设备标识、位置、网络、行为轨迹。处理这些数据必须守住几条线:
- 授权范围:只对自有系统或书面授权的目标做分析;
- 最小必要:只采集完成目标所需的最少字段,不扩大范围;
- 告知与同意:涉及真实用户数据时,必须有明确的告知与同意基础;
- 脱敏存储:能哈希就不存明文,日志里的手机号、地址要打码;
- 可删除:任务结束后按约定清理原始数据。
自测:风控包的字段是不是越多越安全?
自测:哈希和加密,最关键的区别是什么?
自测:为什么"录制回放"很快就不能用了?
自测:举一个字段互相矛盾的例子,并说明服务端会怎么处理。
自动化工程:从能跑到跑得稳
手动跑通一次,和每天稳定跑几千次,中间隔着一整套工程。这一页讲状态机、重试、限速、断点、日志和密钥管理——它们不产生"新功能",但决定了系统能不能长期活下去。
19.1单次成功,和规模化之间隔着什么
第一次跑通脚本的时候,人很容易产生错觉:接口通了、数据拿到了,事情就完成了。但这只是"一次成功",离"稳定运行"还有很远。
算一笔账。假设单次成功率是 95%,看起来很高。连续跑 1000 次,全部成功的概率是 0.95 的 1000 次方,大约是十的负二十三次方——比连续中几次彩票还难。也就是说,在规模化场景里,"不出错"才是小概率事件。
19.2状态机:让程序知道自己走到哪一步
最朴素的脚本是一条直线:初始化、登录、取数据、结束。中间任何一步失败,整个进程就退出,下次只能从头再来。规模化之后,这种做法会浪费大量已经完成的步骤。
更稳的做法是把流程写成状态机:程序随时知道自己处在哪个状态,以及什么条件能进入下一个状态。
状态机有三个直接好处:
- 可恢复:进程重启后读一下任务表,就知道该从"登录"还是"业务"继续;
- 可测试:每个状态可以单独测,不用每次跑完整条链;
- 可观测:日志按状态归类,一眼看出卡在哪一步。
可以把它想成电梯:待机、上行、下行、开门、故障,每个状态都有明确的进入条件和退出条件。电梯不会"大概在动",程序也不该"大概在跑"。
19.3重试与退避:只重试值得重试的错误
重试不是"失败就再来一次"。错误的分类比重试次数重要得多。网络抖动值得重试,参数写错重试一万次也没用。
先分清两类:
- 可重试:连接超时、读超时、服务端 5xx、限流 429;
- 不可重试:400 参数错误、403 拒绝、业务码明确表示失败、以及 401(应先刷新 Token,而不是原样重试)。
可重试的错误也不能立刻重试,否则会在服务端最脆弱的时候继续加压。标准做法是指数退避:第一次等 1 秒,第二次 2 秒,第三次 4 秒,第四次 8 秒,设一个上限比如 30 秒,最多重试 4 次。
为什么要加抖动?假设 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 次几乎必然失败?
自测:哪些错误不应该重试?
自测:目标每秒 20 次请求,单次耗时 200 毫秒,并发大概取多少?
自测:断点续跑最关键的两个设计是什么?
排错手册:症状、原因与定位顺序
同一个"登录失败",可能是手机没网,可能是代理证书没装,也可能只是签名里少了一个字段。这一页给你一套固定顺序:先分层定位,再二分缩小,最后最小复现。照着走,不靠猜。
20.1三条铁律:顺序比经验重要
老手排错看起来很快,不是因为他记得更多报错,而是因为他从不跳步。三条铁律,顺序不能换:
先分层
先确定"哪一层不通",再谈这一层里具体是什么错。网络层的问题,改业务代码永远改不好。
再二分
把可疑范围一刀砍一半:注释一半代码,或换掉一半参数。几刀之后范围就小到能逐个看。
后复现
把环境、参数、时间全部固定,做成一条能反复重放的请求。不能复现,就等于没定位。
为什么顺序不能换?因为分层决定"往哪看",二分决定"看多细",复现决定"结论算不算数"。跳过第一步,你会在一堆无关的代码里翻半天;跳过第三步,你改完一次以为好了,明天又坏。
20.2分层定位:从上往下走七级台阶
一次请求从手机到服务器,要经过七道关。任何一道关了,你看到的都是"失败",但失败的原因完全不同。按下面这张图的顺序走,每一层只问一个问题:这一层通了吗?
每一层的验证动作都很便宜,一句话就能做完:
| 层 | 一句话验证 | 失败时的典型表现 |
|---|---|---|
| 物理与网络 | 打开任意一个网页 | 所有 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.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二分法:一刀砍掉一半
范围缩小到某一层之后,下一步是二分。核心思想很朴素:一次只砍一半,然后判断问题在剩下的一半里还是在被砍掉的一半里。
二分有四把刀,按代价从低到高用:
| 刀法 | 怎么切 | 判断依据 |
|---|---|---|
| 换环境 | 换网络、换设备、换账号 | 换了就好,问题在环境 |
| 换参数 | 只保留一半参数发出去 | 少了就好,问题在被删的那半 |
| 换头 | 逐组删减请求头 | 删了就好,问题在被删的头 |
| 注释代码 | 注释一半逻辑再跑 | 注释了就好,问题在被注释的代码 |
四把刀里,换环境最便宜,因为它不需要改任何东西。很多人一上来就注释代码,其实换个网络就已经能定位了。
20.6最小复现:把变量钉死
定位到最后,你需要一个最小复现:一段去掉所有无关内容、能稳定重现问题的请求。它的价值在于,别人拿着它也能看到同样的错误。
做一个最小复现,只做四件事:
- 去掉无关字段:一个个删,删到不能再删还错,剩下的就是必需的;
- 固定环境:同一台设备、同一个网络、同一个账号、同一分钟;
- 记录环境:把设备型号、系统版本、App 版本、网络类型写下来;
- 写成一条命令:能一行执行、能重复执行、结果稳定。
为什么要写下来?因为不记录环境的复现,等于没复现。三天后你换了个网络,同样的请求通了,你会以为问题自己好了,其实只是变量变了。
20.7证据保存:五件套,缺一件都说不清
排错过程中会产生大量临时判断,但只有证据能留下来。五件套,每次都要存全:
| 证据 | 为什么必须存 | 怎么存 |
|---|---|---|
| 原始报文 | 事后任何复述都会失真 | 导出请求与响应的完整原文 |
| 时间 | 对齐服务端日志的唯一线索 | 精确到秒,注明时区 |
| 设备与版本 | 很多差异只在这一层 | 型号、系统、App 版本号 |
| 日志 | 客户端内部到底走到哪一步 | 保留完整时间顺序,不要只截一行 |
| 网络环境 | 区分"环境问题"和"代码问题" | 运营商、Wi-Fi 名称、是否走代理 |
20.8四类"假故障":不是代码的问题
有一半的"故障"其实根本没坏,只是某个中间环节没生效。这四类最常见:
代理没生效
地址填了但没保存,或者只填了 HTTP 没填 HTTPS,或者客户端压根不走系统代理。表现是"抓不到包",实际是"根本没经过你"。
时间不同步
手机时间比服务器慢了几分钟,签名里的时间戳就落在窗口外,服务端直接判为过期。表现是"签名错误",实际是时钟问题。
缓存
你改了参数,客户端还在用上一次的响应。表现是"改了没用",实际是请求根本没发出去。
编码
中文在某一跳被转成了别的编码,签名算的是转码前的内容,服务端算的是转码后的。表现是"偶尔签名错",实际是编码不一致。
这四类的共同点是:它们都发生在你的代码之外。所以排错时先排除它们,能省掉大量无用功。
20.9两个最贵的误区
误区一:看到报错就改代码
报错是结果,不是原因。一个 403 可能是权限不足、可能是凭证过期、可能是设备指纹不匹配、也可能是触发了风控。四种原因,对应四种完全不同的改法。
正确做法是:先把这个报错归到七层里的某一层,再在那一层里二分。改代码是最后一步,不是第一步。
很多人以为"改到不报错"就是修好了,其实可能只是把错误吞掉了。真正的修好,是能解释为什么之前会错。
误区二:把风控拦截当成 bug
风控系统返回的错误,长得和普通错误一模一样:可能是 403,可能是 412,可能是 200 但业务码不为 0,也可能干脆返回假数据。
它和 bug 的区别在于:bug 是稳定复现的,风控是按条件触发的。同一个请求,换个账号就通了,换个设备就通了,隔十分钟就通了,那基本不是 bug。
| 现象 | 更像 bug | 更像风控 |
|---|---|---|
| 复现率 | 每次必错 | 时好时坏 |
| 换账号 | 仍然错 | 可能就通了 |
| 换设备 | 仍然错 | 可能就通了 |
| 错误信息 | 具体到字段 | 笼统、含糊、带业务码 |
自测:抓包只看到 CONNECT 记录、内容是乱码,应该先查哪一层?
自测:401 和 403 的区别是什么?
自测:为什么二分法要求"一次只改一个变量"?
自测:怎么快速判断一个错误是 bug 还是风控?
实战案例集:三个真实项目,三种分析方法
前面二十章讲的是零件,这一章讲零件怎么装成一台机器。三个案例都来自真实项目,全部做了脱敏:不写具体平台字段、不给可执行代码、不提供绕过步骤。你看到的是判断顺序、失败记录和验证方法——这三样才是能迁移到下一个项目的东西。
21.1案例一:电商 App 的抓包点为什么一直抓不到
背景
目标是一个电商类 App(脱敏后称为 A 应用)。需求很简单:看清楚它的商品搜索请求带了哪些参数。团队按标准流程做了三件事,结果一个请求都没抓到。
| 尝试 | 做法 | 结果 |
|---|---|---|
| ① 系统代理 | 手机 Wi-Fi 设代理,装 CA 证书 | 其他 App 能抓,A 应用 0 命中 |
| ② Hook QUIC | 拦截 HTTP/3 的发送函数 | 库加载了,但业务流量 0 触发 |
| ③ Hook 系统 TLS | 拦截标准 SSL_write / SSL_read | 0 触发 |
三次失败指向同一个结论:A 应用没有走"大家以为它该走"的那条路。
定位过程
关键判断
- 先确认网络栈,再选抓包位置。看安装包里的原生库、看依赖、看它是否自带 TLS 实现。
- 代理失效不等于加密。有些网络库直接读网卡、不走系统代理,代理根本不在链路上。
- 越靠上越省事。在业务层拿到的已经是解密后的明文,不用关心下层用的是 TLS 1.2 还是 1.3。
- spawn 比 attach 稳。进程启动即注入,避免 PID 漂移导致挂不上。
21.2案例二:企业信息平台的"挑战—响应"与限流
背景
某企业信息查询平台(脱敏后称为 B 平台)在访问详情页时会先弹一个验证环节。团队最初把它当成"一个图片识别问题",做了两周发现:识别率上去了,通过率没上去。真正的问题不在识别,而在整条会话的状态管理。
三个被忽略的变量
| 变量 | 一开始的做法 | 后来的做法 |
|---|---|---|
| 会话 | 每次请求新建会话 | 复用同一会话,凭证绑定会话 |
| 预算 | 不限速,跑到报错为止 | 按分钟设配额,留 30% 余量 |
| 退避 | 失败立即重试 | 指数退避 + 冷却,连续失败熔断 |
可迁移的经验
- 先测"不改任何东西"的基线。同一会话连续请求 10 次,看第几次开始被限流,这就是预算的起点。
- 把"识别"和"提交"分开统计。识别对了但提交失败,问题在会话;识别就错,问题在模型。
- 记录每一次失败的完整上下文。只记"失败"没用,要记响应码、响应体、当时的时间、当时的会话状态。
- 队列要清洗。坏数据会让每次请求都失败,看起来像"风控变严",其实是自己的队列脏了。
21.3案例三:用图关系识别"批量执照"
背景
第三个案例不是攻击,而是风险识别:从公开的企业登记信息里,找出"同一个人名下挂着大量同模板公司"的现象。这类结构常被用来批量注册、拆分业务或规避监管。目标是给合规团队一份可复核的名单,而不是自动化处置。
为什么用图,而不是用关键词
最初的做法是搜"商贸、百货、工作室"这类关键词,结果噪声极大:真正的连锁企业、正常的多元化经营都会被命中。改成图分析后,判断依据从"名字像不像"变成了结构长什么样:
- 一个法人节点连接大量公司节点;
- 这些公司高度同质(同后缀、同区县、同注册时间);
- 没有连锁企业常见的品牌复用、统一社会信用代码规律、分支机构结构。
误报控制
| 误报来源 | 表现 | 处理 |
|---|---|---|
| 真连锁 | 同一品牌大量分公司 | 品牌复用检测,命中即排除 |
| 集团架构 | 母公司 + 子公司 | 看股权层级,不看数量 |
| 代办注册 | 同一地址大量公司 | 地址聚类,单独成组 |
| 数据陈旧 | 已注销仍被统计 | 只统计"开业"状态 |
21.4三个案例的共同点
先画链路,再动手
案例一画网络栈,案例二画会话状态机,案例三画关系图。图画清楚了,问题往往自己浮出来。
记录失败假设
三个项目都有一张"试过什么、为什么不行"的表。它比成功路径更能节省后来人的时间。
用数据验收
不靠"我感觉对了",靠通过率、失败分类、人工复核一致率。数字对不上,就是没懂。
自测:系统代理装好证书,为什么还有 App 抓不到包?
自测:验证码识别率 95%,为什么整体通过率只有 60%?
自测:为什么"一人名下公司多"不能直接判定为异常?
自测:三个案例里,最值得复用到下一个项目的是什么?
Web 安全基础:OWASP Top 10 逐条
你可能听过"SQL 注入""越权""XSS",但这些词到底指什么、发生在哪一步、坏了会怎样,很少有人一次讲清。这一页用一张攻击面图加十条清单,把 Web 安全最常见的十类问题从头到尾过一遍。
22.1先看一张图:一次请求的三个落点
一个网页或 App 的请求,从你的手机出发,经过三站:浏览器、服务器、数据库。每一站都有可以被攻击的位置,而且三站的防护方式完全不同。
把这三站想成一道快递流程:
- 浏览器是寄件人。寄件人自己填单子,所以单子上的内容他随时能改;
- 服务器是收件站。它要核对你是谁、你有没有权限,这里是真正把关的地方;
- 数据库是仓库。如果收件站把"帮我拿所有包裹"当成正常指令转过去,仓库就全给了。
所以有一条铁律:前端校验只是体验,服务端校验才是安全。前端能做的,只是让正常用户少犯错;它拦不住任何一个有心人。
22.2OWASP Top 10 到底是什么
OWASP(Open Worldwide Application Security Project,开放 Web 应用安全项目)是一个非营利组织。它每隔几年发布一份清单,列出当前最普遍、最危险的十类 Web 安全问题。
它不是标准,也不是法规,而是一份行业共识。你可以把它理解成"体检报告的十大高发项":不是每个人都会得,但这十项覆盖了绝大多数真实案例。
| 编号 | 名称 | 一句话 |
|---|---|---|
| A01 | 失效的访问控制 | 该拦的没拦,能看到别人的数据 |
| A02 | 加密机制失效 | 该加密的没加密,或加密方式太弱 |
| A03 | 注入 | 用户输入被当成了代码执行 |
| A04 | 不安全设计 | 设计阶段就没考虑滥用场景 |
| A05 | 安全配置错误 | 默认口令、调试开关、目录暴露 |
| A06 | 易受攻击和过时的组件 | 用的库本身有已知漏洞 |
| A07 | 身份认证和授权失效 | 登录、会话、凭证管得不好 |
| A08 | 软件和数据完整性失效 | 更新和依赖没验签,被替换了也不知道 |
| A09 | 安全日志和监控失效 | 被打了却没有记录,事后查不出来 |
| A10 | 服务端请求伪造 | 让服务器替你去访问内网 |
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重点一:注入为什么这么常见
注入能排到前三,是因为它写起来太自然了。想把用户输入放进查询里,最直觉的写法就是拼字符串。而拼字符串,正好就是注入的成因。
为什么多一小段引号就能改变语义?因为数据库收到的不是"一个名字",而是一整条指令文本。它没法分辨哪一段是用户写的、哪一段是程序员写的。
防护的办法只有一个方向:让数据和指令分两条路走。
# 错误写法:把用户输入拼进语句
sql = "SELECT * FROM users WHERE name='" + name + "'"
# 正确写法:语句先定好,数据后填进去
sql = "SELECT * FROM users WHERE name = ?"
cursor.execute(sql, [name])
第二种写法里,数据库先解析好语句结构,再把 name 当成一个值填进去。哪怕这个值是 alice' OR '1'='1,它也只会被当成一个奇怪的名字去匹配,不会变成条件。
22.5重点二:访问控制与 IDOR
访问控制失效能排到第一,是因为它不需要任何技术手段。不用构造特殊字符,不用懂数据库,只要把地址里的数字改一下。
为什么这种问题这么普遍?因为开发时最常见的思路是"先把数据取出来,再判断要不要给"。而正确的思路应该是"只取这个人有权看的数据"。
两者的差别在于:
- 先取后判:数据已经离开数据库了,只要判断那一步漏了、写错了、被异常跳过了,就泄露了;
- 只取该取的:条件直接写进查询里,取不到就是取不到,没有"漏判"的机会。
还有一类更隐蔽的越权:水平越权是同级用户之间互相看,垂直越权是普通用户做了管理员才能做的事。前者靠改编号,后者靠改角色字段或直接调用管理接口。
22.6一张总表:入口、后果、防护
把十条压缩成一张表,方便你快速对照。注意"入口"这一列:几乎所有问题都从用户可控的输入进来,只是进去之后走的路不同。
| 攻击类型 | 入口 | 后果 | 防护 |
|---|---|---|---|
| 访问控制失效 | 对象编号、接口路径 | 看到或改到别人的数据 | 每次校验归属 |
| 加密失效 | 传输链路、存储字段 | 数据被窃听或拖走 | 全程加密、专用口令哈希 |
| 注入 | 查询、命令、模板参数 | 数据泄露、甚至执行代码 | 参数化,不拼字符串 |
| 不安全设计 | 业务流程本身 | 被批量滥用、薅走资源 | 设计阶段写滥用场景 |
| 配置错误 | 后台、调试页、存储桶 | 直接拿到管理权限 | 配置基线检查 |
| 过时组件 | 第三方库版本 | 已知漏洞被直接利用 | 跟踪版本、及时升级 |
| 认证失效 | 登录、会话、凭证 | 账号被撞开或长期冒用 | 限次、可吊销、短有效期 |
| 完整性失效 | 更新包、依赖、序列化 | 被植入代码而不自知 | 验签、锁版本 |
| 日志监控失效 | 日志与告警体系 | 被打了查不出来 | 记录关键操作并告警 |
| SSRF | 抓取、转存、回调地址 | 服务器替攻击者访问内网 | 地址白名单、禁内网 |
22.7两个最危险的误区
误区一:有 WAF 就安全了
WAF(Web Application Firewall,Web 应用防火墙)是一层过滤器,按规则拦掉看起来像攻击的请求。它有用,但它是补丁,不是地基。
- 它按特征匹配,特征之外的东西它不认识;
- 它可能误伤正常请求,于是有人会把规则放宽;
- 它挡不住逻辑漏洞,比如"改一个编号就能看别人订单",因为这个请求长得完全正常。
所以正确的顺序是:先把代码写对,再加 WAF 兜底。反过来做,你会以为自己在防守,其实只是把门虚掩着。
误区二:前端校验够了
前端校验的作用是"让正常用户少犯错",比如邮箱格式不对时立刻提示。它的价值是体验,不是安全。
原因很简单:前端代码跑在用户的设备上,用户对自己的设备有完全控制权。他可以改代码、可以拦截请求、可以不用你的页面直接发请求。
很多人以为"按钮置灰了就点不了",其实请求照样能发出去。所有校验必须在服务端再做一遍,而且服务端那一遍才是有效的那一遍。
自测:为什么参数化查询能防住注入?
自测:把订单编号改成别人的,属于哪一类问题?
自测:有 WAF 是不是就不用改代码了?
自测:为什么前端校验不能算安全措施?
认证与授权:Session、JWT、OAuth
"登录"这两个字背后其实有两件事:先证明你是谁,再决定你能干什么。很多人把这两件事混成一件,于是写出了"改个字段就变管理员"的系统。这一页把 Session、JWT、OAuth 三条主线一次讲清。
23.1认证与授权:两道门,不能合并
这两个词经常被混着用,但它们的失败后果完全不同:
- 认证(Authentication):证明"你是你说的那个人"。密码、指纹、短信码,都在做这件事。
- 授权(Authorization):决定"这个身份能做什么"。查订单、改资料、删数据,各自需要不同权限。
用一个生活场景说明:你进公司大楼,保安看工牌,这是认证;你走到财务室门口,门禁判断你有没有这个权限,这是授权。有工牌不等于能进财务室。
系统里最常见的错误,就是把这两步合成一步:"只要登录了,就什么都能调"。这种设计一旦被利用,普通用户改一个角色字段就能变成管理员。
23.2Session 与 Cookie:把状态放在服务器
HTTP 本身是无状态的:每个请求都是独立的,服务器不会自动记得"你刚才来过"。但登录之后它必须记得你,于是有了 Session。
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 到底解决了什么?它解决的是信任:服务器不需要记账,只要验签就能确认这张纸是自己发的、内容没被改过。这在多台服务器、跨服务调用的场景下很省事。
代价是不好作废。签名一旦发出,在过期之前它一直有效。想提前作废,就得在服务端另外维护一张"黑名单",绕了一圈又回到了记账。
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(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,并且由你信任的签发方签名。
自测:认证和授权,哪个先发生?
自测:JWT 的 Payload 能不能放手机号?
自测:PKCE 解决了什么问题?
自测:为什么密码不能只用普通哈希存?
API、GraphQL 与 WebSocket:接口长什么样,又会在哪里漏
App 上每一个按钮,背后都在调一个接口。接口设计得好不好,直接决定别人能不能看到你的订单、你的手机号。这一页把 REST、GraphQL、WebSocket 三种最常见的接口讲清楚,重点讲它们各自会在哪里漏。
24.1接口就是餐厅的点菜窗口
你去餐厅吃饭,不会直接闯进厨房翻冰箱,而是看菜单、报菜名、等上菜。接口(API,Application Programming Interface,应用程序编程接口)就是这个窗口:它规定你能点什么、怎么点、多久能上。
最常见的接口风格叫 REST(Representational State Transfer,表述性状态转移)。它只有两条规矩:
- 地址表示"哪件东西":
/api/orders/1001就是 1001 号订单; - 方法表示"对它做什么":读、建、改、删。
| 方法 | 干什么 | 例子 | 重复发一次 |
|---|---|---|---|
| GET | 读 | /api/orders/1001 | 结果不变 |
| POST | 新建 | /api/orders | 多出一张订单 |
| PATCH | 改一部分 | /api/orders/1001 | 结果不变 |
| DELETE | 删除 | /api/orders/1001 | 第二次报 404 |
"重复发一次结果不变"这件事有个专门的名字,叫幂等(Idempotent)。它的实际意义是:网络抖动导致你没收到回包,敢不敢直接重试。GET、PATCH、DELETE 可以放心重试,POST 不行——否则一次下单会变成两次。
{"code":403} 里。看接口时要两处都看,别只盯状态码。
24.2四种证明"你是你"的方式
接口不能谁都能调。服务器要确认两件事:你是哪个调用方,以及你代表哪个用户。常见的四种做法,安全强度和成本完全不同。
| 方式 | 证明什么 | 泄露后果 | 典型场景 |
|---|---|---|---|
| API Key | 你是哪个调用方 | 谁拿到谁用,长期有效 | 开放平台 |
| HMAC 签名 | 内容没被改、请求是新的 | 密钥泄露等于全部失守 | 支付、内部服务 |
| OAuth Token | 某用户授权了哪些范围 | 有效期内可冒充该用户 | 第三方登录 |
| mTLS | 双方都持有合法证书 | 证书私钥泄露 | 服务之间互调 |
用生活场景对照:API Key 像小区的固定门禁卡,丢了别人能一直用;HMAC 签名像每次进门都要当场签一张带时间的条子,条子过期就作废;OAuth Token 像酒店房卡,只开你这间房,退房自动失效;mTLS 像双方都要出示身份证才肯谈事。
HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码)值得多说一句。它不是加密,内容还是明文,只是额外附一段用密钥算出来的"指纹"。服务器收到后自己也算一遍,对不上就丢。所以它能防篡改,但防不了偷看——偷看要靠 TLS。
24.3换个编号就能看别人的数据:BOLA 与 IDOR
这是接口里最常见、也最容易被忽视的一类问题。它不需要破解密码,不需要伪造 Token,只需要把地址里的数字改一下。
这类问题的学名叫 BOLA(Broken Object Level Authorization,对象级授权缺失),旧一点的说法叫 IDOR(Insecure Direct Object Reference,不安全的直接对象引用)。名字很绕,本质一句话:
正确的写法是让数据库帮忙兜底: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 请求可以包含几十个查询,按请求数限流等于没限。所以成熟的做法是按查询复杂度扣配额,而不是按请求条数。
24.6GraphQL:一个入口,一张自己写的菜单
REST 的菜单是店家定好的:这个接口返回这几个字段。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 位。
| 对比项 | HTTP | WebSocket |
|---|---|---|
| 方向 | 客户端发起 | 双向随时 |
| 连接 | 短连接或复用 | 一条长连接 |
| 头部开销 | 每次几百字节 | 每帧几字节 |
| 鉴权时机 | 每个请求都验 | 只在握手时验一次 |
最后一行是关键。握手时验过身份,之后这条连接上发什么,很多实现就不再检查了。于是出现订阅越权:连上之后发一条 {"action":"subscribe","channel":"order.1002"},服务器如果不核对"1002 是不是你的订单",就会把别人的订单状态实时推给你。
心跳(Ping/Pong)是另一件常被忽略的事。长时间没数据的连接会被中间的代理或负载均衡器悄悄掐断,所以双方要定期发心跳保活。对防守方来说,心跳还有个副作用:它让"连接活着"和"用户在操作"变成两件事,风控不能只看连接是否存在。
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 的区别是什么?
自测:为什么说"有 Token 就不会越权"是错的?
自测:一次 GraphQL 请求里写 50 个别名,为什么能绕开限流?
自测:WebSocket 为什么容易出订阅越权?
自测:只在网关做鉴权,风险在哪里?
云原生与容器安全:谁该为哪一层负责
"上云"不等于"安全交给云厂商"。云上的事故,绝大多数发生在用户自己负责的那一半:权限配太宽、存储桶设成公共读、容器里挂着宿主机的控制接口。这一页把责任边界、身份体系、容器隔离和集群结构讲清楚。
25.1责任共担:一把刀从中间切开
云厂商卖的不是"安全",是"算力和存储"。它负责到哪一层,写在合同里,叫责任共担模型(Shared Responsibility Model)。
用租房打比方:房东负责楼体结构、水电入户、大门门禁;你负责自己屋里的门锁、财物、访客。房东不会替你保管钱包,你也不能去改楼体的承重墙。
| 模式 | 厂商管到 | 你管什么 |
|---|---|---|
| IaaS 云主机 | 机房与虚拟化 | 系统、中间件、应用、数据 |
| PaaS 托管平台 | 到运行时 | 应用代码、配置、数据 |
| SaaS 成品软件 | 几乎全部 | 账号、权限、数据内容 |
看这张表要抓住一点:服务越"托管",你手上的活越少,但剩下的活越关键。SaaS 里你几乎不用运维,可一旦把权限设成"所有人可编辑",数据照样会没。
25.2IAM:云上的身份系统
IAM(Identity and Access Management,身份与访问管理)就是云平台的"人事部加门禁系统"。四个概念,必须分清:
- 用户(User):一个长期存在的身份,通常对应一个人或一个系统。它有固定的密钥,也最容易变成历史包袱。
- 角色(Role):一个"可以被扮演"的身份,本身没有密码。谁满足条件,谁就能临时变成它。
- 策略(Policy):一段 JSON,写清"允许谁,对什么资源,做哪些动作"。它是权限的真正来源。
- 临时凭证(Temporary Credentials):扮演角色后拿到的一组密钥,带有效期,通常几十分钟到几小时。
类比:用户是员工工牌,长期有效,丢了很麻烦;角色是"今日值班经理"这个岗位,谁戴上袖标谁就是,下班自动失效。临时凭证就是那枚几小时后自动消失的袖标。
策略里最危险的两个词是 Action: "*" 和 Resource: "*"。翻成大白话就是"任何动作,对任何资源"。很多事故的起点,就是某人为图省事写了这一行。
// 图省事的写法:等于把整栋楼的钥匙串交出去
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
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 | 用不超配额 | 每月水电上限 |
| capabilities | 拿不到额外特权 | 只给一把钥匙 |
注意这三样都是访问控制,不是硬件隔离。虚拟机有独立内核,容器共用宿主内核。所以一旦内核有提权漏洞,或者容器被授予了过多能力(比如 --privileged),隔离就可能被打穿。
真正需要强隔离的场景(多租户、跑不受信任的代码),应该用更硬的边界:独立虚拟机、用户态内核,或者专门的沙箱运行时。
25.6Docker socket:把方向盘交出去
/var/run/docker.sock 是 Docker 的控制接口。谁能让它执行命令,谁就能让宿主机起新容器、挂载任意目录、读取任意文件。
把 socket 挂进容器,常见的理由是"让容器里的程序管理其他容器"。但这等于把车钥匙留在车里,还开着窗:容器里只要能连上这个 socket,就能起一个挂载宿主机根目录的新容器,然后读取宿主机上的一切。
- 不要挂:绝大多数业务容器根本不需要它;
- 要用就代理:在 socket 前放一个只放行特定接口的代理;
- 更彻底:用无 root 的容器运行时,从根上减少特权。
25.7Kubernetes:一个集群里的五个角色
Kubernetes(常简称 K8s)是容器的调度系统。你告诉它"我要三个副本一直活着",它负责找机器、拉镜像、起容器、挂了重启。
- 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 分别解决什么问题?
自测:把 Docker socket 挂进容器,最坏会怎样?
自测:为什么 Kubernetes 的 Secret 不能当成保险箱?
检测与对抗:风控是怎么看出来的
很多人以为风控就是"封号",以为"换个 IP 就没事了"。真实的检测是一套从单点规则到多维打分的体系,它看的不是你某一个动作,而是你的动作像不像一个人。这一页讲清它怎么判断,以及为什么单点伪装没用。
26.1三层检测:从死规矩到整体判断
风控系统不是一套算法,而是三层叠加。越往下越准但越死板,越往上越灵活但越难解释。
- 规则层:如果同一设备在 10 分钟内登录了 30 个账号,直接拦。写得出来、说得清楚、执行很快。
- 统计层:这个账号平时的下单时间是晚上八点,今天凌晨三点连下十单,偏离了自己的历史分布。
- 模型层:把设备、IP、行为、关系等几十个维度放在一起算一个分,判断"整体像不像正常用户"。
规则层的价值被低估了。它最大的优点不是准,而是可解释、可申诉、可回滚。真出了误判,你能指着那条规则说"这里写错了"。模型做不到这一点。
26.2日志来源:五双眼睛,各看一角
检测的前提是"看得见"。数据来自五个地方,各有盲区。
| 来源 | 能看到什么 | 看不到什么 | 可信度 |
|---|---|---|---|
| 客户端埋点 | 点击、滑动、页面停留 | 可被伪造 | 低 |
| 网关日志 | IP、请求头、频率 | 业务含义 | 中 |
| 业务日志 | 谁做了什么、结果如何 | 设备细节 | 高 |
| 数据库 | 最终状态、变更历史 | 过程 | 最高 |
| 设备指纹 | 机型、系统、传感器特征 | 真人身份 | 中高 |
为什么强调"可信度"?因为越靠近客户端的日志,越容易被动过。请求头里的机型可以随便写,埋点事件可以被脚本批量伪造。所以真正做判定时,业务日志和数据库才是最后的事实来源。
设备指纹是中间地带。它不直接说明"你是谁",但能说明"这台设备还是不是上次那台"。换了一台设备、换了一套传感器特征,即使账号密码都对,也值得多看一眼。
26.3特征工程:把行为翻译成数字
模型只认数字,不认故事。所谓特征工程,就是把"这个人有点怪"翻译成一组可计算的量。
- 频次:单位时间内做了多少次。一分钟点 200 次,和一分钟点 3 次,含义完全不同。
- 时间间隔:两次动作之间隔多久。人会有停顿、犹豫、看错、返回,机器通常均匀得像节拍器。
- 序列:动作的先后顺序。正常人"浏览、加购、下单、支付";脚本可能跳过浏览直接支付。
- 熵:行为的杂乱程度。真人今天看手机、明天看电脑、偶尔半夜下单;脚本的行为高度重复,熵很低。
- 一致性:多个维度是否互相对得上。机型是高端机、系统版本很旧、时区却和 IP 归属地对不上,这就是不一致。
用生活例子说"熵":一个人的购物清单如果是"牛奶、面包、鸡蛋、电池、拖鞋",看着很正常;如果连续三十次都是"牛奶、面包、鸡蛋",一模一样,那多半不是人在买。
26.4规则引擎与评分卡
规则引擎负责"命中即记录",评分卡负责"把命中换算成分数"。两者配合,才能既抓得准又分得出轻重。
举个简化例子,每条规则给不同权重:
| 命中项 | 权重 | 为什么这么给 |
|---|---|---|
| 同设备多账号 | 30 | 强关联,但存在家庭共用 |
| 操作间隔过于均匀 | 20 | 机器特征明显,可被模仿 |
| IP 与常用地不符 | 10 | 出差、VPN 都会触发 |
| 新设备首次支付 | 15 | 换手机是正常事 |
分数累加后落在不同区间,走不同处置。这里的关键是权重不是拍脑袋,而是根据历史案件反推:哪条规则在真实案件里出现得多,权重就高。
26.5行为序列:节奏比内容更难伪装
内容可以编,节奏很难编。人在操作时会有大量无意识的小停顿,而脚本的节奏是程序写死的。
注意最后一行图注。只看"间隔是否均匀",慢速脚本很容易骗过去:把每次操作间隔随机化到 3 到 30 秒,看起来就很像人。所以节奏只是特征之一,必须和别的维度一起看。
26.6图关系:从单点看整体
单个账号可能伪装得很好,但账号之间会共享东西:同一台设备、同一个出口 IP、同一张支付卡、同一个收货地址。把这些关系画成一张图,异常就会浮出来。
图分析的价值在于发现"应该没关系的东西之间有关系"。两个账号注册时间不同、昵称不同、下单品类不同,看起来毫无关联,但它们在同一台设备上登录过,共用一张支付卡——这就不是巧合了。
26.7误报与漏报:一道没有完美解的题
任何检测都会犯错,错误只有两种:
- 误报(False Positive):正常人被拦了。用户会投诉,客服成本上升,严重时直接流失;
- 漏报(False Negative):坏人放过去了。平台承担资金损失和合规风险。
两者此消彼长。你把阈值调严,误报上升、漏报下降;调松则相反。这条曲线叫 ROC 曲线,不用记公式,只要记住它的形状:想两头都占,只能靠更好的特征,不能靠调阈值。
所以真正的问题不是"怎么把阈值调到最优",而是"哪一边的代价更高"。支付场景漏报一次可能损失几千元,误报一次只是多收一条短信验证,那就该偏严;内容推荐场景误报会让用户直接关掉 App,那就该偏松。
26.8对抗的本质:单点伪装没有意义
很多人以为"换个 IP 就查不到了"。这只改了一个维度,而检测看的是所有维度是否自洽。
举个具体的例子。你把 IP 换成了另一个城市,但:
- 设备指纹还是原来那台;
- 系统时区没跟着变;
- 操作节奏还是每秒一次;
- 常用收货地址还在原地;
- 支付卡还是同一张。
这五个维度里,你只改了一个,剩下四个都在说"我还是我"。结果不是"藏起来了",而是新增了一条不一致:IP 变了,其他都没变。这本身就是可疑信号。
反过来说,防守方的思路也清楚了:不要依赖任何单一维度。IP 可以换、设备可以刷、账号可以注册、行为可以模仿,但要让所有维度同时自洽,成本会指数上升。这就是为什么"多维一致"比"单点严格"更有效。
26.9处置梯度:从观察到封禁
风控的目标不是"抓住",而是"用最小代价控制风险"。所以处置是分级的,一级一级往上走。
| 等级 | 动作 | 用户感受 |
|---|---|---|
| 一级 | 只记录,不打扰 | 无感 |
| 二级 | 加一次验证(短信、滑块) | 稍麻烦 |
| 三级 | 限速、限制额度 | 变慢 |
| 四级 | 拒绝本次操作 | 失败一次 |
| 五级 | 冻结账号、封禁设备 | 无法使用 |
这套梯度的意义在于给误报留退路。一个正常用户被加了短信验证,最多骂一句;被直接封号,就会去投诉、去社交平台发帖。而真正的攻击者,在二级就会暴露——他过不了验证,或者过验证的成本比收益高。
还有一层:处置要可申诉、可回滚。任何自动化决策都可能出错,出错后必须有人能查、能改、能恢复。
26.10隐私与合规:能不用就不用
检测能力越强,越容易越界。三条底线:
- 最小必要:只采集完成判断所必需的数据。能用一个布尔值判断,就不要存整段原始记录。
- 告知同意:用户要知道采了什么、用来干什么。藏在十几页协议里的"可能收集设备信息",不算有效告知。
- 能哈希不存明文:设备标识、手机号这类信息,做不可逆处理后再用于关联分析,效果接近,风险低得多。
还要加一条常被忽略的:留存期限。数据不是存得越久越好。超过必要期限的数据,只会变成负债——一旦泄露,赔的是全部历史。
26.11两个最常见的误区
误区一:风控就是封号。封号是最后一招,成本最高、不可逆、最容易误伤。成熟的风控大部分时间在做"观察"和"加验",只有极少数情况才动用封禁。
误区二:改 IP 就能过。IP 只是几十个维度里的一个。设备、时区、节奏、关系、支付方式只要有一个对不上,改 IP 反而制造了新的矛盾点。
自测:为什么说"规则层"不能被模型完全替代?
自测:五种日志来源里,哪一种最可信?为什么?
自测:把 IP 换成别的城市,为什么反而更可疑?
自测:误报率和漏报率能不能同时降到很低?
自测:为什么处置要分级,而不是一次到底?
合规与法律红线:你的能力边界,就是授权边界
前面二十几章教的是"怎么做":怎么抓包、怎么逆向、怎么绕过证书固定。这一章讲"做到哪里必须停"。同样的技术动作,在授权范围内是工作成果,在授权范围外是犯罪证据。两者之间没有灰色地带,只有一份文件。
27.1为什么技术人必须先懂法
你花二十几章学到的东西,本质上是三种能力:看见(抓包)、拆开(静态分析)、插手(动态调试)。这三种能力本身没有方向。
就像一把手术刀:在手术室里,它是救人;在街上随便一个人拿着它靠近你,它是伤害。刀没有变,场景变了,性质就变了。技术也一样。
法律判断一件事,看的是三个客观事实:目标归谁、你有没有得到许可、你做了什么。它不看你在心里怎么想,也不看你事后有没有道歉。
三个最常见的自我安慰,全都不成立:
- "我只是看看,没改数据":未经授权读取本身就是问题,不需要你改动什么。
- "我没卖钱,也没传播":绝大多数计算机犯罪不以牟利为前提。
- "我是内部员工,这系统我天天用":使用权和测试权是两件事。
举个具体例子。你抓自己公司 App 的包,看到接口 /api/user/info?uid=10086,好奇把 uid 改成 10087,服务器返回了另一个同事的手机号和身份证号。技术上你只改了一个数字,法律上这已经是未经授权获取公民个人信息。改一个数字,和写一个爬虫批量拉取,性质是同一类,只是数量不同。
27.2授权三要素:谁、什么、多久
借车的比喻最直观:谁的车(谁有权借给你)、借哪辆(车牌号)、借到几点(时间)。三样缺一样,你开出去就是偷。
第一样,谁授权。必须是有权处分这个系统的人。公司员工不代表公司,项目经理口头说"可以"不代表公司授权,朋友说"你随便弄"更不代表 App 的运营方授权。外包项目里,你只对甲方负责,不能顺手测甲方的客户。
第二样,授权什么。要写到能唯一识别的粒度:域名写全,IP 写段,App 写包名,账号写测试账号。还要写清楚排除项,例如"不含支付接口""不含生产数据库"。
第三样,授权多久。起止日期精确到日。有些合同还会限定每日时段,比如只允许凌晨两点到五点,因为要避开业务高峰。到期就是到期,不存在"多测一天没关系"。
口头同意和聊天记录可以当证据,但证明力很弱。正式项目一律要书面文件:授权书、渗透测试合同,或者公司内部的测试审批单。
27.3一份能保护你的授权书
授权文件不是走过场的公文。出事的时候,它是唯一能证明"我不是入侵者"的东西。至少要写清七件事,少一件就少一层保护。
| 要素 | 必须写清什么 | 缺了会怎样 |
|---|---|---|
| 委托方 | 公司全称、统一社会信用代码、授权代表 | 不知道谁有权授权,出事没人兜底 |
| 目标范围 | 域名、IP 段、App 包名、账号,以及明确排除项 | 多扫一个域名就是越界 |
| 时间窗 | 起止日期,必要时加每日允许时段 | 半夜跑扫描惊动运维,被当成攻击 |
| 测试类型 | 允许只读探测、漏洞验证;禁止破坏、社工、物理接触 | 做了禁止项,合同不保护你 |
| 数据处置 | 采集范围、存放位置、保存期限、销毁方式 | 拿着用户数据,就是另一个罪名 |
| 联系人 | 甲方技术负责人与全天应急电话 | 触发告警时没人给你作证 |
| 签字盖章 | 授权代表签字、公章、日期 | 只有邮件截图,证明力很弱 |
合同层面还有几个最容易被忽略的点:
- 范围与排除项写死:不要用"相关系统""关联业务"这种模糊表述。
- 变更必须书面:新增一个域名,要签补充协议,不能靠微信一句话。
- 速率与时段限制:写清并发数、每秒请求数,避免把业务打挂。
- 数据处理与销毁:测试数据什么时候删、谁来删、怎么证明删了。
- 保密与交付:报告只交给甲方指定的人,不公开发布。
- 责任划分:故意或重大过失通常不能靠合同免责。
- 应急联系人:出事十分钟内能找到人,比什么都重要。
- 分包限制:不能把项目随手转给第三方,授权是给"你"的。
27.4三部法律各管什么
三部法律分工不同,但会叠加适用。同一个行为可能同时违反两部,还可能触犯刑法。
| 法律 | 主要管什么 | 落到你身上的场景 |
|---|---|---|
| 网络安全法 | 网络运行安全、等级保护、关键信息基础设施、禁止非法侵入干扰破坏 | 未授权扫端口、爆破登录、植入程序 |
| 数据安全法 | 数据分类分级、重要数据、数据出境、安全评估 | 把业务数据带出公司、传到境外服务器 |
| 个人信息保护法 | 个人信息处理规则、告知同意、敏感个人信息、个人权利 | 抓到手机号身份证、把用户数据存自己电脑 |
网络安全法还建立了等级保护制度,也就是常说的等保。系统按重要程度分级,级别越高,运营者义务越重。企业项目里甲方常问"我们这套系统等保几级",问的就是这个。
三部法律主要对应行政处罚和民事赔偿。如果行为再严重一些,就进入刑法管辖,那是另一套后果。
27.5刑法里的行为边界
刑法是最后一道线。下面只讲罪名和行为边界,不逐条引用条文编号——条文会修订,行为边界不会。真要用的时候,请以现行法律文本和专业律师意见为准。
与计算机、数据相关的常见罪名有这几类:
- 非法侵入计算机信息系统:未经许可进入特定领域的系统。
- 非法获取计算机信息系统数据、非法控制计算机信息系统:进别人的系统拿数据,或者在别人系统里留后门、装程序。
- 破坏计算机信息系统:删除、修改、增加数据或干扰系统功能,导致不能正常运行。
- 提供侵入、非法控制计算机信息系统的程序、工具:编写、出售、传播专门用于入侵的工具。
- 侵犯公民个人信息:非法获取、出售、提供他人的个人信息。
- 帮助信息网络犯罪活动:明知别人在犯罪,还提供技术支持或工具。
换成动作,就是下面这些会直接踩线:
- 未经授权登录、扫描、探测他人的系统。
- 绕过身份验证,读取不属于你的数据。
- 在他人系统里植入程序、留下后门。
- 批量获取、出售或提供公民个人信息。
- 编写、出售、传播专门用于入侵的工具。
- 删除或篡改数据,导致系统无法正常运行。
四个必须记住的判断点:
- 不要求牟利:非法获取数据、非法控制、破坏系统,都不以赚钱为前提。
- "没造成损失"不等于没事:非法获取数据本身就可能构成犯罪,数量、次数影响量刑。
- 单位行为也追责:直接负责的主管人员和直接责任人一起担。
- 未遂也可能处罚:着手实施了但没成功,同样有法律风险。
27.6漏洞披露:发现之后怎么办
发现漏洞之后怎么处理,决定了你是安全研究者还是犯罪嫌疑人。区别只有两点:有没有先私下通知、有没有超出证明问题所需的范围。
四个阶段各自要做什么:
- 发现:只在你被授权的那部分系统里确认。证据留最小必要——一张截图、一条请求、一个时间点,够了。
- 私下报告:找厂商的安全响应渠道,比如安全邮箱、漏洞收集平台、官方客服转安全团队。不要发到公开群、社交平台、技术论坛。
- 修复验证:给合理修复期,业界常见 90 天,涉及关键系统可以更长。到期前复测确认是否真的修好。
- 公开:修复完成后再公开,隐去可直接利用的细节、真实用户数据、内部地址。
这几件事一件都不要做:
- 拿漏洞向厂商要钱,说"不付就公开"——这可能构成敲诈勒索。
- 先公开、再通知厂商,等于把用户暴露在风险里。
- 用"我发现了漏洞"换取"让我继续测更多系统"的许可。
- 为了证明影响,把整张用户表拉下来。
- 把漏洞细节当成社交货币,在小群里传播。
27.7个人信息与数据出境
个人信息保护的四个原则,听起来抽象,落到你手上就是四个具体动作。
| 原则 | 落到操作上是什么 |
|---|---|
| 告知同意 | 收集前告诉用户:收什么、干什么用、给谁、留多久。敏感信息要单独取得同意。 |
| 最小必要 | 只收完成功能所必需的最少信息。能哈希就不存明文,能本地处理就不上传。 |
| 目的限定 | 为 A 目的收的数据,不能拿去做 B 目的。换用途要重新告知并取得同意。 |
| 可删除 | 用户要求删除时要能删;保存期限到了要主动删;测试数据用完就清。 |
做协议分析的人,最容易在这四个地方出问题:
- 测试用真数据:注册一批假账号,造一批假订单,不要在真实用户身上做实验。
- 截图不打码:报告里的手机号、身份证号、地址、头像,一律打码或替换。
- 原始流量不删:抓包文件里往往包含完整的请求体,项目结束就该删。
- 随手传网盘:把含个人信息的抓包文件传到公共网盘,等于公开泄露。
27.8四个真实场景,分别怎么定性
下面四个场景,是初学者最常遇到的。判断方法都一样:先看目标归谁,再看有没有授权。
| 场景 | 定性 | 关键判断 | 要注意 |
|---|---|---|---|
| 抓自己的 App | 合法 | 设备、账号、代码都是你的 | App 内他人信息仍受保护 |
| 抓公司 App | 看授权 | 员工身份不等于授权 | 查内部审批单与工单 |
| 抓朋友手机 | 基本不行 | 设备所有权不等于数据处分权 | 口头同意不能替代服务方授权 |
| 抓公开网站 | 浏览可以,测试不行 | 公开可访问不等于授权测试 | 扫描、爆破、批量抓取都越界 |
抓自己开发的 App:这是最干净的场景。你自己的设备、自己的账号、自己的代码,随便抓。但有一个前提容易被忽略:如果 App 里存着别的用户的数据,你抓到的内容仍然属于个人信息,不能随便存、随便发。
抓公司的 App:不一定合法。员工身份只说明你能用,不说明你能测。要看有没有书面授权——安全测试工单、审批邮件、渗透测试合同都算。很多公司的员工手册把"未审批测试生产系统"列为严重违纪,这一条不需要等到出事才生效。
抓朋友的手机:基本不行。手机是他的,但 App 和服务是第三方的。他的口头同意,不能替代 App 运营方的授权。而且流量里既有他的个人信息,也有对方的服务数据。这是四个场景里风险最高的一个。
抓公开网站:公开能打开,只说明它对外提供服务。正常浏览公开页面没问题;一旦开始扫描目录、尝试弱口令、测试越权、批量抓取数据,就进入了未授权访问的范畴。很多公开的漏洞赏金项目有自己的规则页,在规则范围内做才算授权。
27.9两个致命误区
误区一:技术中立。技术本身确实中立,但"使用技术的行为"和"提供工具的行为"不中立。菜刀摆在商店里不犯法,明知对方要去伤人还把刀递过去,就不是中立了。写一个专门用于入侵的工具并传播,即使你一次没用过,责任也算在你头上。
误区二:只要不赚钱就没事。绝大多数计算机犯罪不以牟利为要件。非法获取数据、非法控制、破坏系统,都不要求你赚钱。"我就看看""我没卖钱""我删掉了"都不是免罪理由。真正的判断标准还是那三条:有没有授权、有没有越界、有没有后果。
想保护自己,其实只有四个动作:
- 存好授权文件:纸质件扫描、电子件归档,随时能拿出来。
- 留操作日志:什么时间、对哪个目标、做了什么,自己记一份。
- 报告只给甲方:不公开发布,不在群里传阅。
- 能自证:触发告警时,十分钟内能拿出授权书和操作记录。
自测:判断一次抓包是否合法,最先看什么?
自测:我是公司员工,抓公司 App 的包需要授权吗?
自测:发现漏洞后,直接发到技术群里讨论,有什么问题?
自测:"我没赚钱,也没造成损失",能不能免责?
术语词典:一页查完所有名词
读协议分析的文章,最卡人的往往不是逻辑,而是名词:一会儿 sign,一会儿 token,一会儿 pinning。这一页把全站出现过的词按十个分组排好,每条一句话,并标明它在第几章讲过。看不懂某段正文时,回到这里查一个词,再回去接着读。
28.1怎么用这一页
查词只需要三步,顺序固定:先判断它属于哪个大类,再读那一句话解释,最后按后面的章号回去看完整例子。
很多人以为把术语背完就等于会分析协议,其实术语只是地图上的地名。知道"哈希"两个字怎么写,和能看出一段校验逻辑用的是哈希还是签名,是两件事。
28.2十个分组是什么关系
十个分组不是十门互不相干的课,而是同一句话在链路上经过的十个切面:先有地址和连接,再有加密和签名,然后才谈得上内容和平台。
28.3六组最容易混的词
下面这几对词,在初学者的问题里出现频率最高。先把它们分清楚,能省掉后面一大半的困惑。
| 容易混的两组 | 一句话区别 | 看哪章 |
|---|---|---|
| 加密 / 编码 | 加密要钥匙才解得开,编码谁拿到都能还原 | 第 4、9 章 |
| 哈希 / 加密 | 哈希不可逆,加密可以解密回原文 | 第 4 章 |
| 签名 / 加密 | 签名防篡改和冒充,加密防偷看 | 第 5 章 |
| 认证 / 授权 | 认证是你是谁,授权是你能干什么 | 第 23 章 |
| 去标识化 / 匿名化 | 前者理论上还能还原,后者不能 | 第 27 章 |
| 限流 / 频控 | 限流管每秒速度,频控管一段时间总量 | 第 18 章 |
| 静态 / 动态分析 | 前者不运行只看文件,后者跑起来看行为 | 第 11 章 |
28.4术语正文(十组 330 条)
按分组往下翻,或者直接用上面的搜索框。每条后面标注的章号,就是它被完整讲过的地方。
一、网络与传输NETWORK
二、密码学CRYPTO
三、TLS 与证书TLS
四、HTTP 与 WebHTTP
五、编码与序列化ENCODING
六、移动端与逆向MOBILE
七、抓包与调试CAPTURE
八、风控与指纹RISK
九、云与 API 安全CLOUD / API
十、工程与合规ENGINEERING
28.5自测:五个最容易答错的
下面五道题都来自真实提问。能全部答对,说明你已经不会被名词骗过去了。
自测:加密和编码,最本质的区别是什么?
自测:JWT 的载荷里为什么不能放密码?
自测:手机装了代理证书,为什么还是抓不到明文?
自测:设备指纹为什么要检查时区和 IP 是否对得上?
自测:认证和授权,差在哪一个字上?
28.6背词的三个建议
术语量很大,但不需要一次记完。按下面的顺序来,效率最高。
先记分组
记住十个大类各管什么,比记住任何单个定义都值。知道"这词大概属于证书那组",就已经能猜个七成。
再记英文
中文译名各家不统一,英文名相对固定。看到不认识的缩写,先看它是不是某个英文词的首字母。
最后记章号
每个词后面都有章号。真正要用的词,回那一章看一遍完整例子,比在这里读十遍解释管用。
工具速查:按任务选工具,别按名气选
新手最常见的动作是:看到十个推荐工具,一口气全装上,然后一个都跑不通。这一页反过来——先说清楚你要拿到什么信息,再说该用哪一类工具。工具是放大镜,不是眼睛;放大镜再好,也得先知道往哪儿看。
29.1先问任务,再问工具
工具本身没有高低,只有"合不合这件事"。同样是想知道"登录时发了什么",用 IDA 去看汇编就是绕远路,用代理抓一次包十分钟就完了。反过来,想知道"签名串是怎么拼出来的",光抓包永远看不出来,必须静态加动态。
所以动手之前,先在心里填一句话:"我要拿到的是(流量 / 代码 / 运行时的值 / 计算结果 / 网络状态)"。填完这句,工具自己就浮出来了。
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 | 高 |
| objection | Frida 的封装,一条命令做常见操作 | 全平台 / 替代 自己写脚本 | 低 |
| openssl | 证书、哈希、编解码、对称加解密 | 全平台 / 替代 语言内置库 | 中 |
| nmap | 端口与服务识别 | 全平台 / 替代 masscan | 中 |
| curl | 手工发一条 HTTP 请求 | 全平台 / 替代 httpie | 低 |
| xxd | 十六进制查看文件内容 | 全平台 / 替代 hexdump | 低 |
29.4重点工具:什么时候用,什么时候别用
三类工具不是三选一,而是三条互相验证的线。抓包看到的是结果,静态看到的是代码,动态看到的是过程。只有三条线对得上,结论才算站得住。
抓包四件套
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 哪个函数;不会静态,就不知道函数在哪个类里。所以顺序是固定的。
三步的达标标准写清楚,你才知道自己是不是真的跑通了:
- 抓包达标:能看到自己 App 一次登录的完整请求,包括 URL、请求头、请求体,内容是明文 JSON 而不是乱码。
- 静态达标:能在 jadx 里搜到那个 URL 字符串,并找到调用它的类和方法。
- 动态达标:能 hook 一个方法,在控制台打印出它的入参,且和抓到的内容对得上。
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 的运行机制。工具只是把这些知识放大。
自测:我想知道"登录请求里到底带了哪些参数",该用哪一类工具?
自测:jadx 和 apktool 有什么区别,能不能只装一个?
自测:为什么建议先跑通抓包,再学 Frida?
自测:装了 IDA 却看不懂,问题出在哪?
命令速查:可以直接抄的一组
这一页不解释原理,只给能直接抄的命令。每条都配一句"它在干什么"和一个真实格式的输出示例。输出里的设备名、IP、时间都是示例,你自己跑出来的肯定不一样——看格式,不要对字符串。
--help 和搜索。
30.1先学会读一条命令
所有命令行工具都遵循同一个结构:程序名 + 子命令 + 参数 + 值。看不懂一条命令,通常不是不会用,而是没拆开看。
读命令时抓住三个约定:
- 短参数和长参数:
-v和--verbose通常是一回事,短的省事,长的好记。 - 占位符:文档里写成
包名、文件路径的地方,是要你替换成真实值的,不要原样敲进去。 - 引号:带空格或特殊符号的值要加引号,比如过滤器写成
"host 1.2.3.4 and port 443"。
不确定参数时,先敲 命令 --help。这比搜索快,而且不会看到过时的博客。
30.2命令在哪台机器上跑
这是新手最容易混淆的一点。你在电脑上敲 adb,但真正干活的是手机里的进程。命令在电脑上敲,进程在手机上跑,中间靠 adb 或 frida 的通道连起来。
报错分三类,处理方式完全不同:
- 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命令没反应时的排查顺序
命令敲下去没输出、或者报了个看不懂的错,不要猜。按下面四步走,每一步都能排除掉一大片可能。
再补三个高频细节:
- 版本不匹配:frida 客户端和 frida-server 大版本必须一致,否则连上了也立刻断。
- 路径大小写:Android 的文件系统区分大小写,
/sdcard/Download和/sdcard/download是两个地方。 - 端口被占:代理起不来先看
ss -lntp,8080 被别的程序占了就换一个。
30.11安全提醒
上面这些命令,每一条都有明确的适用范围。同一行命令,在自己手机上跑是调试,在别人手机上跑就是入侵。
- 只在自己的设备上执行:自己的手机、自己的电脑、自己搭的服务器。
- 只对授权目标执行:扫描、爆破、重放这类动作,必须有书面授权,且在范围内。
- 不碰生产数据:测试用假账号、假订单,不要拿真实用户做实验。
- 抓到的包限期删除:里面通常含完整的请求体和 Token。
- 报告只给甲方:不公开发布,不在群里传阅。
自测:adb devices 显示 unauthorized,该怎么处理?
自测:抓包抓到一堆乱码,是工具坏了吗?
自测:tcpdump 的过滤器和 Wireshark 的显示过滤器,区别在哪?
自测:apktool 打包出来的 APK 为什么装不上?
面试题库:120 问,从基础到实战
面试卡住,多数时候不是没学过,而是没按面试官想听的方式组织答案。这一页把十个方向的高频问题按难度排好,每题先给结论、再给理由。建议遮住答案自问一遍,说不顺的做标记,回到对应章节补。
31.1怎么用这一页:四句话的答题结构
技术面试的答案不需要长,但必须有结构。面试官在三十秒内要判断三件事:你知不知道结论、你知不知道原因、你知不知道什么时候不成立。对应到回答,就是四步。
下面这张表是十个分组的分布。每组 12 题,前几题偏基础,后几题偏排查和取舍。第一次练可以按顺序走;考前突击就只挑每组的后四题。
| 分组 | 题数 | 难度走向 | 面试里常怎么问 |
|---|---|---|---|
| 一 网络基础 | 12 | 握手到排查 | 这个请求为什么慢 |
| 二 密码学 | 12 | 概念到取舍 | 为什么不用 RSA 加密数据 |
| 三 TLS 与证书 | 12 | 流程到攻击面 | 装了证书为什么还解不开 |
| 四 HTTP 与 Web | 12 | 语义到漏洞 | 这个接口有什么风险 |
| 五 编码与序列化 | 12 | 格式到解析安全 | 这段乱码是什么 |
| 六 Android 逆向 | 12 | 结构到对抗 | 你怎么定位加密函数 |
| 七 iOS 逆向 | 12 | 结构到对抗 | 越狱之后你先做什么 |
| 八 抓包与调试 | 12 | 准备到排错 | 抓不到包怎么办 |
| 九 风控与指纹 | 12 | 概念到一致性 | 怎么判断是同一台设备 |
| 十 工程与合规 | 12 | 流程到边界 | 怎么保证稳定和合规 |
31.2一、网络基础(12 题)
这一组是所有问题的地基。前六题必须张口就来,后六题考的是"出问题怎么分层定位"。
三次握手为什么不能只握两次?
四次挥手后,主动关闭的一方为什么要停在 TIME_WAIT?
TCP 和 UDP 最本质的区别是什么?各举一个真实场景。
DNS 为什么默认用 UDP?什么时候会切到 TCP?
MTU 和 MSS 是什么关系?为什么大包反而更慢?
家里设备都能上网,为什么外网主动连不进来?
长连接为什么还要应用层心跳?TCP keepalive 不够吗?
HTTP/1.1 的队头阻塞是什么?HTTP/2 怎么缓解?
QUIC 为什么选择跑在 UDP 上?
输入一个域名到拿到 IP,中间经历了什么?
TCP 粘包是什么?为什么 UDP 没有这个问题?
用户说"接口很慢",你怎么判断慢在 DNS、TCP、TLS 还是服务端?
31.3二、密码学(12 题)
这一组最容易被问倒的地方不是算法细节,而是"为什么不那样做"。答案里多讲代价,少背参数。
哈希和加密,最根本的区别是什么?
存密码为什么不能用 SHA-256,而要用 bcrypt 这类慢哈希?
对称加密和非对称加密,各自的代价是什么?
既然有非对称加密,为什么传输数据还是用对称?
AES-GCM 比 AES-CBC 好在哪?
盐(salt)是干什么的?需要保密吗?
HMAC 和直接 hash(key + msg) 有什么区别?
数字签名和加密,钥匙的方向为什么相反?
RSA 签名和 ECDSA 签名有什么实际差别?
为什么说 ECB 模式不能用?举一个直观例子。
前向保密(PFS)解决什么问题?
为什么密钥和 nonce 不能用普通随机函数生成?
31.4三、TLS 与证书(12 题)
这一组的高频场景几乎都来自同一句抱怨:"我证书装了,为什么还是不行。"先把"信任根"和"证书固定"分开,问题就清楚一半。
TLS 到底解决了哪几个问题?
TLS 1.3 相比 1.2 的主要改进有哪些?
证书链是怎么验证的?根证书凭什么可信?
证书里的 CN 和 SAN 有什么区别?
证书为什么要吊销?CRL 和 OCSP 各有什么问题?
会话复用(session ticket)是什么?有什么风险?
SNI 是什么?为什么说它会泄露隐私?
双向认证(mTLS)用在哪?它能替代业务鉴权吗?
抓包工具为什么必须让你装证书?装了之后为什么就能解密?
TLS 1.3 的 0-RTT 为什么有重放风险?
证书固定(pinning)通常在哪几个层次实现?
用户把手机时间改到 2020 年,HTTPS 会怎样?
31.5四、HTTP 与 Web(12 题)
这一组的问题在面试里经常以"这个接口有什么风险"的形式出现。答题时先说攻击者能做什么,再说怎么防。
GET 和 POST 的本质区别是什么?
301、302、307、308 有什么区别?
Cookie 的 HttpOnly、Secure、SameSite 各防什么?
有了 CORS,后端还需要做鉴权吗?
CSRF 的原理是什么?怎么防?
反射型、存储型、DOM 型 XSS 有什么区别?
SQL 注入的本质是什么?预编译为什么能防?
SSRF 怎么防?为什么黑名单不够?
JWT 最常见的几个漏洞是什么?
JWT 签发后无法主动失效,工程上怎么解决?
OAuth 授权码流程为什么要多一步"用 code 换 token"?
什么是 IDOR?怎么系统性排查?
31.6五、编码与序列化(12 题)
这一组看起来最"基础",实际最容易露怯。能一眼分清编码、加密、哈希的人,通常也能一眼看懂抓包里的乱码。
编码、加密、哈希,三者怎么区分?
Base64 是加密吗?为什么体积会变大?
URL 编码为什么必要?%20 和加号有什么区别?
UTF-8 和 GBK 有什么区别?乱码是怎么产生的?
大端和小端是什么?为什么网络字节序用大端?
JSON 和 Protobuf 该怎么选?
Protobuf 消息里没有字段名,怎么保证前后兼容?
MessagePack 和 JSON 有什么不同?
什么是 TLV?为什么私有协议爱用它?
JSON 标准不允许注释和尾逗号,这会带来什么安全问题?
反序列化漏洞的原理是什么?
一个字段看起来像 Base64,你怎么判断它是编码、加密还是压缩?
31.7六、Android 逆向(12 题)
这一组前半段考结构,后半段考方法论。面试官更想听"你怎么找到它",而不是"你用了哪个工具"。
一个 APK 拆开,里面主要有哪些东西?
DEX 和 smali 是什么关系?
jadx 和 apktool 该用哪个?
为什么很多 App 把关键逻辑放在 .so 里?
加固通常做了哪些事?
常见的反调试手段有哪些?
Frida 的 spawn 和 attach 有什么区别?
拿到一个 App,怎么定位某个按钮背后的加密函数?
Xposed、LSPosed 和 Frida 有什么区别?
为什么有时候 hook 了系统 API 却没有效果?
Android 上的证书校验通常分布在哪几层?
怎么快速判断一个 App 有没有加壳?
31.8七、iOS 逆向(12 题)
iOS 的面试题通常不考工具命令,而考"你为什么必须越狱""砸壳到底砸的是什么"。把这两件事讲清,后面就顺了。
从 App Store 下载的包,和网上流传的 IPA 有什么区别?
iOS 逆向为什么通常需要越狱设备?
Mach-O 是什么?
砸壳到底在做什么?
Objective-C 的 method swizzling 是什么?
Swift 逆向和 Objective-C 逆向有什么不同?
iOS 上常见的证书固定是怎么实现的?
不越狱的设备能做动态分析吗?
iOS 的设备指纹通常来自哪些信息?
为什么说 iOS 的 Keychain 比 NSUserDefaults 安全?
静态分析时字符串被加密了,怎么找关键逻辑?
一个 iOS App 的请求参数每次都不一样,你会怎么拆?
31.9八、抓包与调试(12 题)
这一组几乎是面试里的"实操过滤器":真抓过包的人,会先问代理有没有收到请求;只背过概念的人,会直接说"pinning"。
开始抓包前,要先确认哪三件事?
HTTPS 抓包失败,最常见的原因有哪些?
代理模式和 VPN 模式抓包有什么区别?
Android 7 以后,为什么装了用户证书还是抓不到?
在自己设备上,怎么让 App 信任代理证书?
App 能上网,但抓包工具里一片空白,怎么排查?
抓到的请求原样重放却失败了,可能是什么原因?
时间戳和随机数各自防什么?为什么要一起用?
已经装了证书,为什么还是解密不了?
怎么判断一段流量是 HTTP/2 还是 HTTP/1.1?
WebSocket 抓包和普通 HTTP 有什么不同?
一开抓包 App 就崩溃或断网,可能是什么原因?
31.10九、风控与指纹(12 题)
这一组考的是判断力。答题时讲原理和检测思路,不讲具体绕过步骤,这是行业里默认的分寸。
设备指纹是什么?风控拿它做什么?
为什么 IMEI、MAC 这些传统标识越来越不可靠?
Android ID 和 OAID 有什么区别?
为什么说指纹的"一致性"比"唯一性"更重要?
常见的一致性矛盾有哪些?举两个例子。
环境检测一般在检测什么?
为什么改机工具容易被识别?
风控为什么不能只看设备,还要看行为?
什么是设备关联?它解决什么问题?
为什么"每个请求都带全量设备信息"反而可疑?
指纹做得太细会有什么代价?
采集设备信息时,合规上要注意什么?
31.11十、工程与合规(12 题)
最后一组不考知识点,考的是做事方式。面试官问这些,是想知道你会不会把一件小事做成事故。
接到一个自动化任务,第一步该做什么?
为什么脚本要尽量做到幂等?
重试策略该怎么设计?
并发一高就被限流或封禁,怎么处理?
日志该记什么,不该记什么?
密钥硬编码在代码里有什么问题?
灰度发布和影子流量各解决什么问题?
单测、联调、线上验证,各验证什么?
线上出问题,你的排查顺序是什么?
什么是"最小必要"?举一个采集上的例子。
做安全研究的授权边界在哪?
面试官问"你怎么保证方案可靠",该怎么答?
学习路线:四阶段、十二周与岗位地图
很多人不是学不会,而是不知道下一步该学什么:今天看 TLS,明天看逆向,后天又去装工具,三周之后什么都没留下。这一页给一张能照着走的表:四个阶段、十二周、每天一小时,最后落到五类岗位各自要什么。你可以把它当成课表,也可以当成验收清单。
32.1先看清整条路:四个阶段
全站 36 页看起来很多,但能力其实只有四层。四层像盖楼:下面一层没打牢,上面一层看再多也记不住。
第一层是看懂:知道数据从手机到服务器要经过哪些层,每一层加了什么。第二层是会抓:能把一次真实请求完整录下来,并逐行解释。第三层是会拆:加密或混淆之后,还能把逻辑还原出来。第四层是会复现:换一台机器、换一个账号,你写的脚本仍然能跑通。
| 阶段 | 核心能力 | 过关标准 |
|---|---|---|
| 看懂 | 解释分层、编码、加密、签名 | 能用自己的话讲给外行听 |
| 会抓 | 完整录下一次请求并逐行解释 | 换一台手机能照做 |
| 会拆 | 在加密或混淆后还原逻辑 | 能说出函数的输入和输出 |
| 会复现 | 把过程写成可重复的脚本 | 别人照文档能跑通 |
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 分钟 | 写三行笔记,标出不懂的点 | 给明天留一个明确的起点 |
如果某天只有二十分钟,就只做两件事:复习十分钟,动手十分钟。宁可少学一点,也不要整天只看不动手。
32.4三条路线:安全入门、移动端逆向、风控反欺诈
前六周的地基对三条路线都一样:分层、编码、密码学、HTTP、TLS、抓包。第六周之后开始分叉。分叉不是换赛道,而是把练习对象换成你真正想做的方向。
安全入门
重点在 Web 与内网:请求怎么被篡改、权限怎么被绕过、日志怎么留痕。产出是一份授权范围内的测试报告。
移动端逆向
重点在 App:静态看结构,动态看行为,最后用真实流量对拍。产出是一个自己 App 的接口字段表。
风控与反欺诈
重点在数据与规则:设备指纹、行为序列、误伤与漏放。产出是一份规则说明加误伤分析。
| 路线 | 第 7–12 周重点 | 最终产出 |
|---|---|---|
| 安全入门 | Web 安全、认证授权、API、云原生 | 一份带证据的测试报告 |
| 移动端逆向 | Android、iOS、加固、证书固定 | 一个接口的完整字段表 |
| 风控反欺诈 | 设备指纹、登录风控、检测对抗 | 一份规则与误伤分析 |
32.5岗位地图:五类岗位各自要什么
学会技能之后,落到工作上通常是五类岗位。它们共享同一套地基,但每天做的事差别很大。看清"日常在做什么",比看岗位名字有用。
| 岗位 | 日常在做什么 | 入门门槛 |
|---|---|---|
| 渗透测试 | 按授权范围找问题、复现、写报告 | 网络与 Web 基础、写作能力 |
| 安全研发 | 写加密、鉴权、审计与防护工具 | 会写代码、懂协议细节 |
| 逆向工程 | 拆 App、定位算法、输出分析结论 | 汇编、调试、长期专注 |
| 风控策略 | 定规则、看误伤、做攻防迭代 | 数据敏感度、业务理解 |
| 合规审计 | 对照法规查数据处理流程 | 条文理解、跨部门沟通 |
很多人以为"证书越多越好"。其实招聘方更在意两件事:你独立做过什么,以及你能不能把过程讲清楚。证书能证明你学过,产出物才能证明你会做。
32.6学习资源类型:书、文档、靶场
资源不在多,在于每种只留一个主力。下面五类各有用处,也各有坑。
| 类型 | 怎么用 | 常见坑 |
|---|---|---|
| 教材 | 选一本当主线,按目录推进 | 同时开三本,每本都停在第三章 |
| 官方文档 | 带着具体问题去查字段和参数 | 一上来通读,读两页就放弃 |
| 靶场 | 按难度刷,每做完一题写记录 | 只刷简单题,不总结 |
| 他人分析 | 学思路和排查顺序 | 把结论当标准答案背下来 |
| 源码 | 带着一个问题读一个函数 | 从头读到尾,三天后放弃 |
32.7怎么判断自己真的学会了
"看懂了"是一种很不可靠的感觉。真正可靠的判断有三条,一条比一条严格。
复述:找一个人,用三分钟讲清"TLS 为什么需要两个随机数"。如果他听懂了,说明你懂了;如果你只能说"就是防止重放",说明你只是记住了结论。
复现:把电脑重装,或者换一台设备,照着你自己写的笔记重做一遍。中间卡住的每一步,都是你没真正掌握的地方。
排错:故意制造三个错误,比如证书没装、端口写错、时间不同步,看自己能不能在不查资料的情况下定位。能定位到"哪一层出了问题",才算理解结构。
32.8常见放弃原因与对策
放弃通常不是因为难,而是因为某一个小坎没人告诉你怎么办。下面六种最常见。
| 原因 | 典型表现 | 对策 |
|---|---|---|
| 贪多 | 同时开五本书、三套视频 | 只留一条主线,其余全部移出视线 |
| 跳步 | 没学分层直接看 TLS | 回到前一阶段补两小时,再往上走 |
| 只收藏 | 收藏两百篇,一篇没打开 | 收藏时写一句"为什么存" |
| 卡环境 | 装工具装了三天 | 先用现成环境或在线靶场,跑通再折腾 |
| 没反馈 | 不知道自己做对没有 | 每周交一个产出物给人看 |
| 怕看不懂 | 一遇公式就合上书 | 先记住结论和用途,公式留到第二遍 |
还有一个隐性原因:把"学安全"当成"看热闹"。看别人抓包、脱壳、写脚本很爽,但那是别人的过程。你必须自己经历一次从失败到成功的完整循环,能力才会长在身上。
32.9两个最大的误区
误区一:收藏等于学会。收藏的动作只证明"我现在不想看"。真正学会的标志是能在没有资料的情况下复述和操作。判断方法很简单:随机抽一条收藏,合上手机,讲三分钟。讲不出来,就说明它只是躺在收藏夹里。
误区二:只看不做。视频和文章会把复杂过程剪掉,只留下顺畅的部分。你自己动手时遇到的报错、超时、证书错误,才是真正要学的内容。看十遍不如做一遍,做一遍不如讲一遍。
很多人以为"等我基础打好再动手"。其实基础就是在动手里打出来的:先做一个很小的任务,遇到缺什么补什么,比按目录啃完一本书快得多。
自测:四个阶段分别是什么?各自的过关标准是什么?
自测:每天一小时为什么要留 10 分钟复习、5 分钟写笔记?
自测:三条路线能不能同时学?
自测:怎么判断自己是真的学会了,而不是看懂了?
速记卡:一页刷完核心概念
前面 30 多页讲的是"为什么",这一页只留"是什么"。90 张卡按九个主题排好,每张卡一个问题和一句话答案。用法不是从头读到尾,而是遮住答案先自问,答不出的做记号,按间隔重复刷。手机上一次刷十张,比坐着看两小时更有效。
33.1怎么用这 90 张卡
记忆有一个反直觉的规律:费力回忆一次,比轻松阅读五次更牢。所以刷卡的正确动作是"先遮住、再回答、最后核对",而不是顺着往下读。
每天刷多少张合适?新卡十张,旧卡二十张,大约十分钟。三轮之后,把答错的卡单独抽出来,只刷那一小叠。
| 轮次 | 时间 | 做法 |
|---|---|---|
| 第一轮 | 当天 | 新卡十张,全部过一遍 |
| 第二轮 | 第二天 | 只刷昨天答错的,加新卡十张 |
| 第三轮 | 第四天 | 混合刷,答对的推后 |
| 第四轮 | 第七天 | 随机抽二十张,检查长期记忆 |
33.2网络:地址、连接与传输
这一组解决"数据怎么从 A 到 B"。答不出时,先想寄快递:地址、门牌、房间号。
OSI 七层从上到下是哪七层?
TCP 和 UDP 最大的区别?
三次握手做了什么?
端口的作用是什么?
DNS 是干什么的?
三个私有 IP 段是哪些?
NAT 解决什么问题?
MTU 是什么?
为什么断开 TCP 要四次挥手?
CDN 的作用是什么?
33.3密码学:加密、哈希与密钥
这一组解决"别人看不懂、改不了"。记住一条主线:对称快、非对称慢、哈希不可逆。
对称加密的特点?
非对称加密的特点?
哈希有哪三个关键性质?
为什么密码不能明文存储?
加盐解决什么问题?
AEAD 是什么?
为什么 ECB 模式不安全?
为什么随机数不能用时间戳?
数字签名用哪把钥匙?
密钥是不是越长越好?
33.4TLS:握手、证书与加密通道
这一组解决"HTTPS 到底怎么安全"。核心只有一句:用非对称握手,换成对称密钥,再加密传输。
TLS 解决哪三件事?
证书里最关键的三样东西?
握手为什么要两个随机数?
会话密钥是对称还是非对称?
SNI 是什么?
证书固定(pinning)是什么?
为什么中间人抓包会失败?
TLS 1.3 的主要改进?
前向保密是什么意思?
为什么抓到的 HTTPS 是乱码?
33.5HTTP:请求、响应与状态
这一组解决"网页和接口在说什么"。记住结构:起始行、头、体。
GET 和 POST 的核心区别?
200、301、403、404、500 分别是什么?
Header 和 Body 分别放什么?
Cookie 和 Token 的区别?
什么是幂等?
HTTP/2 最大的变化?
Content-Type 干什么用?
为什么一次点击会有很多请求?
301 和 302 的区别?
REST 的核心想法?
33.6编码:字节、进制与格式
这一组解决"看起来像乱码的东西到底是什么"。先分清编码、加密、压缩是三件事。
ASCII 和 UTF-8 是什么关系?
Base64 是加密吗?
URL 编码解决什么?
为什么十六进制常用?
大端和小端是什么?
JSON 和表单的区别?
Protobuf 的优点和代价?
中文为什么会乱码?
Unix 时间戳 1729900000 是什么?
压缩和加密能一起用吗?
33.7Android:包、代码与运行时
这一组解决"安卓 App 里有什么、怎么拆"。三条线:DEX、so、运行时。
APK 里主要有什么?
DEX 是什么?
smali 是什么?
so 文件是什么?
Frida 是干什么的?
root 的作用是什么?
加固通常做了什么?
Android 7 之后用户证书有什么变化?
adb 是什么?
为什么要脱壳?
33.8iOS:包、可执行文件与调试
这一组解决"iOS 和安卓哪里不一样"。核心差异在签名、包格式和证书信任。
IPA 是什么?
Mach-O 是什么?
越狱的作用是什么?
砸壳是什么意思?
Objective-C 的方法调用怎么表示?
Swift 方法名为什么难读?
Keychain 是什么?
iOS 抓包为什么更难?
Frida 在 iOS 上怎么用?
描述文件是什么?
33.9风控:指纹、行为与处置
这一组解决"平台怎么判断你是不是机器人"。记住:风控不是一条规则,而是多层叠加。
设备指纹是什么?
指纹为什么既要有稳定性又要能变?
行为风控看哪些东西?
什么是误伤?
什么是漏放?
规则和模型是什么关系?
为什么换账号还会被关联?
环境检测通常查什么?
风控为什么也看时间?
对抗会有结束的一天吗?
33.10合规:数据、授权与边界
这一组解决"什么事能做、做到什么程度"。技术再熟,越界就是另一回事。
个人信息处理的三个基本原则?
什么是最小必要?
为什么能哈希就不存明文?
做抓包测试的前提是什么?
为什么不能随便测别人的站点?
日志可以无限期保存吗?
数据出境要注意什么?
什么是去标识化?
匿名化和去标识化的区别?
发现漏洞后的正确做法?
33.11刷完之后怎么巩固
90 张卡刷完,只完成了"认得"。要变成"会用",还得做三件事:把卡上的词放回真实请求里、把相近的概念做成对照表、把答错的卡讲给别人听。
- 放回真实请求:抓一个包,在请求里找出至少五个卡上的名词;
- 做对照表:加密与签名、编码与加密、Cookie 与 Token,各写三行区别;
- 讲给别人听:挑三张最容易混的卡,用生活比喻讲一遍。
自测:为什么刷卡要"先自问再看答案"?
自测:Base64、哈希、加密,三者最核心的区别是什么?
自测:TLS 里为什么会话密钥用对称,而不用非对称?
自测:设备指纹为什么不能只看一个字段?
常见误区:40 个似是而非的说法
学协议分析,最费时间的不是不懂,而是记了一堆听起来对、其实是错的结论。它们通常来自半句话、一张截图、一次偶然成功。这一页把 40 个高频误区集中拆开,每条都写清三件事:为什么会被误解、真相是什么、正确理解应该怎样。建议当工具用,遇到争论时回来查。
34.1密码学:七个最容易混的说法
密码学的误区有个共同点:把"解决一个问题"当成了"解决所有问题"。加密、签名、哈希各管一件事,混着用就会出漏洞。
误区:加密就等于安全。
误区:哈希可以还原成原文。
误区:Base64 是一种加密。
误区:密钥越长越安全。
误区:签名会把内容藏起来。
误区:自己设计一个异或加密就够用了。
误区:随机数随便取一个就行。
34.2协议:七个被简化过头的结论
协议类的误区通常来自"把某一层的结论套到另一层"。TCP 可靠,不代表应用层不会丢数据;HTTPS 安全,不代表内容可信。
误区:HTTPS 就是"加了密的 HTTP"。
误区:TCP 一定比 UDP 好。
误区:端口开着就说明有漏洞。
误区:DNS 只用来把域名换成 IP。
误区:HTTP/2 一定比 HTTP/1.1 快。
误区:长连接一定比短连接省资源。
误区:IPv6 只是地址变长了。
34.3抓包:七个"抓不到就怪工具"的想法
抓包失败几乎都能按层定位。先确认有没有包,再确认能不能解密,最后才怀疑工具。顺序反了,就会在错误的地方浪费几小时。
误区:抓不到包就是抓包工具坏了。
误区:装上证书就能抓所有 App。
误区:抓到乱码说明加密很强。
误区:手机连上代理,所有流量就都走代理。
误区:抓包工具能改请求,所以什么都能改。
误区:抓一次包就能看懂协议。
误区:流量里没看到,就说明这件事没发生。
34.4逆向:七个高估或低估的判断
逆向的误区集中在两个方向:要么把反编译结果当源代码,要么把加固当成不可分析。真相在中间:它是成本问题,不是有无问题。
误区:反编译出来的就是源代码。
误区:加了壳就没法分析。
误区:字符串搜不到,就说明程序里没有。
误区:汇编看不懂就学不了逆向。
误区:钩子挂上了,就等于拿到了算法。
误区:一个版本能跑,就永远能跑。
误区:逆向就是找漏洞。
34.5风控:六个把复杂问题说简单的说法
风控的本质是在误伤和漏放之间找平衡。任何"一招搞定"的说法,都忽略了另一边的代价。
误区:风控就是封号。
误区:设备指纹就是一个设备 ID。
误区:改一个机型字段就能过环境检测。
误区:规则越多越安全。
误区:有了模型就不需要规则。
误区:对抗一次成功就结束了。
34.6工程与合规:六个容易被忽略的边界
技术能跑通,不等于事情能交付。工程讲可重复,合规讲可证明,这两件事都不在代码里。
误区:脚本能跑就行,不用写文档。
误区:日志越多越好。
误区:测试环境的数据可以随便用。
误区:授权一次就永久有效。
误区:开源软件没有法律风险。
误区:把数据删掉就算合规了。
34.7怎么自己发现误区
误区不会自己消失,但可以用三个动作主动发现。第一个动作是找反例:听到一个绝对化的说法,先想有没有例外。第二个动作是问边界:它在什么条件下成立,条件变了还成立吗。第三个动作是做实验:能动手验证的,就不要只靠记忆。
还有一个便宜的办法:把结论改写成"在什么条件下,什么成立"。写不出条件的结论,多半是背下来的口号,不是理解。
综合自测:为什么"加密等于安全"是错的?
综合自测:抓包抓到乱码,应该先检查什么?
综合自测:为什么风控不能只靠一条规则?
关于本站、更新日志与离线使用
这一页说清四件事:这个站是给谁写的、内容边界在哪、怎么在手机和断网环境下用、以及它到底收集不收集你的数据。如果你只想知道怎么用,直接看第三节和第四节;如果你关心技术实现,第六节有完整说明。
35.1站点定位与内容边界
这个站面向技术基础很弱、从零开始的读者。它不假设你学过网络、写过代码、接触过密码学,所以每个概念都先用生活比喻讲一遍,再给技术定义。全站按"看懂、会抓、会拆、会复现"四个阶段组织,从网络分层一直讲到移动端逆向、设备指纹、风控与合规。
它不是一本速查手册,也不是一本工具说明书。工具会更新,命令会变化,但"数据怎么分层、签名为什么不能替代加密、抓包失败该按什么顺序排查"这些问题十年后仍然成立。本站优先讲这些不容易过期的部分。
| 覆盖 | 不覆盖 |
|---|---|
| 网络分层、编码、密码学、TLS、HTTP | 具体产品的内部接口与私有字段 |
| 抓包、协议逆向、移动端静态与动态分析 | 绕过他人系统风控的操作步骤 |
| 设备指纹、登录风控、检测与对抗原理 | 批量注册、刷量等滥用方法 |
| Web 与 API 安全、云原生基础、合规红线 | 未授权目标的实战攻击 |
35.2版本与更新日志
站点从最初的一版讲义扩到现在的规模,主要经历了两次大改。下面这张表记录的是结构性变化,不记录错别字修正。
| 版本 | 规模 | 主要变化 |
|---|---|---|
| v1.0 | 11 页 | 初版:分层、编码、密码学、TLS、抓包、逆向入门 |
| v1.5 | 21 页 | 补充移动端、登录案例、排错手册与术语表 |
| v2.0 | 36 页 | 扩到 36 页;新增风控、合规、附录;手机端适配;支持离线 |
v2.0 的三项重点:
- 页数翻倍:补齐 Web 与 API、云原生、检测对抗、法律红线,让"从底层到实战"这条线完整;
- 手机端重排:段落更短、表格不超过四列、图可以点开放大、底部有固定工具栏;
- 离线可用:整站打包成一个 HTML,既可以缓存,也可以下载,还可以添加到主屏幕。
35.3离线使用:三种方式
三种方式解决的是不同场景:只是想快点打开,用在线缓存;想拷到别的设备,用下载单文件;想天天看,添加到主屏幕。
| 方式 | 适合场景 | 注意 |
|---|---|---|
| 在线缓存 | 同一个浏览器反复看 | 清缓存后会失效 |
| 下载单文件 | 拷到电脑、平板、U 盘 | 文件较大,首次加载稍慢 |
| 添加到主屏幕 | 手机上天天看 | 需要浏览器支持 |
在线缓存依赖浏览器提供的离线能力:第一次访问时把页面存一份,之后优先从本地读。它不需要你注册、不需要登录,也不会上传任何内容。如果换了浏览器或清理了缓存,重新访问一次即可恢复。
35.4手机端使用说明
手机是主要阅读场景,所以界面做了几处专门设计。第一次用可能找不到,下面逐条说明。
| 操作 | 怎么做 | 效果 |
|---|---|---|
| 底部工具栏 | 屏幕底部四个按钮 | 目录、搜索、上一页、下一页 |
| 左右滑动 | 在正文区域横向滑 | 切换上一页或下一页 |
| 看图放大 | 点图右下角的"放大" | 全屏查看,可滚动,按关闭退出 |
| 调字号 | 顶部 A- 与 A+ | 四档字号,刷新后仍保留 |
几个细节值得注意:
- 左右滑动不会在图上生效,避免你看图时误翻页;
- 表格过宽时可以在表格内部横向滚动,不会把整页撑开;
- 字号设置只保存在本机浏览器,换设备不会同步;
- 顶部进度条显示的是当前页的阅读进度,不是全站进度。
35.5数据与隐私
这个站不收集任何数据。具体说:
- 没有账号:不需要注册、登录,也没有任何表单提交;
- 没有统计:不接入访问统计、不记录阅读行为、不生成用户画像;
- 没有第三方请求:不加载外部字体、脚本、图片、广告;
- 没有 Cookie 追踪:只在本机保存字号等偏好,不上传。
因为整站是一个 HTML 文件,所有样式、脚本、图示都内联在里面。断网打开时,浏览器不会向任何服务器发起请求,也就不存在"偷偷上报"的可能。你可以用浏览器的网络面板自行验证这一点。
35.6技术说明:单文件、纯 CSS 路由、无外部依赖
整站构建产物是一个 HTML 文件。样式写在文件内的样式表里,脚本也内联,36 个页面按顺序排在同一份文档中。没有构建服务器、没有数据库、没有接口。
页面切换用的是浏览器原生的 :target 伪类:地址栏的片段(例如 #page-tls)决定显示哪一页,CSS 负责把其余页面隐藏。这种做法的好处是:
- 没有路由库:不需要加载任何框架,首屏更快;
- 可以前进后退:浏览器的历史记录直接可用;
- 可以分享定位:把链接发给别人,打开就是你正在看的那一页;
- 降级友好:即使脚本被禁用,阅读和翻页仍然正常。
代价是:页面内部的任何元素都不能再写 id,否则会抢走 :target,导致整页消失。这也是内容规范里最严格的一条禁令。
35.7如何反馈与纠错
发现错误时,最有用的反馈不是"这里不对",而是指出具体位置、给出依据、说明应该改成什么。推荐的格式是三行:
# 反馈模板
位置:第 8 章 8.3 节,第二张图下方
问题:把会话密钥说成了非对称加密
依据:TLS 1.3 握手后用对称密钥传输应用数据
建议:改为"握手用非对称,传输用对称"
如果是文字错误,指出段落开头几个字就够定位。如果是图示错误,说明是哪张图、哪个方框。如果涉及事实争议,附上可查证的来源类型(规范文档、官方说明、可复现的实验),不要只给结论。
本站不提供实时客服,也不承诺响应时间。所有修正会累积到下一次版本更新,并在更新日志里体现结构性变化。
35.8免责声明与法律提示
本站内容仅用于安全学习、防御建设与授权测试。所有示例都限定在自有设备、书面授权目标或公开教学靶场。以下行为与本站无关,也可能违法:
- 未经授权扫描、入侵、干扰他人系统;
- 绕过他人产品的安全机制以获取数据或牟利;
- 批量注册、刷量、薅羊毛等滥用行为;
- 收集、买卖、泄露他人个人信息。
技术本身是中性的,但使用技术的行为有边界。不同地区的法律对"未经授权访问"的界定不完全相同,动手前请确认:目标是不是你的、有没有书面授权、授权范围包不包括你要做的动作。