首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
23
18
[原创]Android APP 抓包学习笔记
发表于: 2026-3-15 21:12
5019
[原创]Android APP 抓包学习笔记
plight
2026-3-15 21:12
5019
> **免责声明**:本文内容仅供安全学习和研究目的,旨在帮助读者理解网络协议和抓包技术的原理。请勿将本文所述技术用于未经授权的网络活动。未经明确授权对他人系统或应用进行抓包、逆向等操作可能违反相关法律法规。读者应自行承担因使用本文内容而产生的一切法律责任。 要理解 Android App 抓包,首先需要理解网络协议栈——数据如何从应用层层封装到网络上传输,以及 TLS 如何在其中提供安全保障。本文从协议基础出发,深入 TLS 原理,再到抓包实战与攻防对抗,系统性地梳理 Android App 抓包的完整知识体系。 # 一、常见协议 ## 1.1 协议栈模型 网络通信的核心是分层协议栈。业界有两种主流模型: ``` OSI 七层模型 TCP/IP 四层模型 ┌─────────────────┐ │ 应用层 │ ├─────────────────┤ ┌─────────────────┐ │ 表示层 │ │ 应用层 │ ├─────────────────┤ │ (HTTP/TLS/DNS) │ │ 会话层 │ └────────┬────────┘ ├─────────────────┤ │ │ 传输层 │ ┌────────┴────────┐ │ (TCP/UDP) │ │ 传输层 │ ├─────────────────┤ │ (TCP/UDP) │ │ 网络层 │ ├─────────────────┤ │ (IP) │ │ 网络层 │ ├─────────────────┤ │ (IP) │ │ 数据链路层 │ ├─────────────────┤ ├─────────────────┤ │ 网络接口层 │ │ 物理层 │ │ (以太网/Wi-Fi) │ └─────────────────┘ └─────────────────┘ ``` 实际工程中普遍使用 TCP/IP 四层模型。理解分层对抓包很重要:**不同的抓包工具工作在不同的层级**,决定了它能捕获什么样的数据。 ### 为什么不自定义协议? 一些开发者可能会想:能否自定义加密协议来保护通信?答案是**不推荐**,原因有二: 1. **算法层面**:密码学算法的安全性需要经过大量数学论证和长期实践检验,自研算法往往存在未知漏洞 2. **开发层面**:即使使用标准算法,实现中的细节错误(如随机数生成不当、padding oracle 漏洞等)也会导致安全性崩塌 因此,业界共识是使用经过充分审计的标准协议(如 TLS)和成熟的开源库(如 OpenSSL、BoringSSL)。 ## 1.2 TCP 与 UDP 传输层的两个基础协议,它们的特性决定了上层协议的设计选择: | 特性 | TCP | UDP | |------|-----|-----| | 连接方式 | 三次握手建立连接 | 无连接 | | 可靠性 | 可靠(重传、确认机制) | 不可靠(不重传) | | 有序性 | 有序传输 | 无序 | | 效率 | 开销较大 | 高效、低延迟 | | 典型应用 | HTTP、HTTPS、FTP | DNS、视频流、游戏、WebRTC | ## 1.3 应用层协议 ### HTTP 与 HTTPS HTTP 是应用层的核心协议,HTTPS 则是在 HTTP 和 TCP 之间加入了 TLS 层。 **HTTP/1.1 vs HTTP/2**: | 特性 | HTTP/1.1 | HTTP/2 | |------|----------|--------| | 传输格式 | 文本协议 | 二进制帧 | | 多路复用 | 不支持(一个连接一个请求) | 支持(一个连接多个流) | | 头部压缩 | 无 | HPACK 压缩 | | 服务器推送 | 不支持 | 支持 | | 队头阻塞 | 存在 | 应用层解决(TCP 层仍存在) | HTTP/2 的二进制帧格式和多路复用机制使得抓包分析更为复杂,需要抓包工具支持 HTTP/2 解析。 ### WebSocket HTTP 协议是**无状态**的——每次请求都是独立的,服务器不会记住之前的请求。HTTP/1.1 虽然引入了 `Keep-Alive` 复用 TCP 连接,但本质上仍然是"请求-响应"模式:客户端不发请求,服务器就无法主动推送数据。 这在实时场景中问题很大:聊天、实时推送、协作编辑等场景需要服务端主动向客户端推送消息。传统的解决方案(如轮询、长轮询)要么浪费带宽,要么延迟高,每次都要重新建立 HTTP 请求的开销。 WebSocket 就是为解决这个问题而生的。它通过一次 HTTP 握手**升级**为持久的双向通道,之后双方可以随时互发消息,不再需要反复建立连接: ``` HTTP 轮询 WebSocket Client ──req──> Server Client <===========> Server Client <──res── Server 持久双向连接 Client ──req──> Server 任意一方随时发送 Client <──res── Server 无需反复握手 (每次都要重新请求) (一次握手,持续通信) ``` - `ws://`:明文传输 - `wss://`:TLS 加密 **HTTP 升级为 WebSocket 的握手过程**: ```http # 客户端请求升级 GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: <base64-encoded-key> # 服务端同意升级 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade ``` 握手完成后,连接不再使用 HTTP 帧,而是切换为 WebSocket 帧格式: | 操作码 | 类型 | 说明 | |--------|------|------| | 0x1 | 文本帧 | UTF-8 编码的文本数据 | | 0x2 | 二进制帧 | 二进制数据 | | 0x8 | 关闭帧 | 任意一方可主动关闭 | | 0x9 | Ping | 心跳检测 | | 0xA | Pong | 心跳响应 | ### Protobuf #### 为什么需要 Protobuf App 与服务端通信时,需要把结构化数据(如用户信息、订单详情)序列化后通过网络传输。常见的方案有 JSON 和 Protobuf: ``` JSON(文本格式): {"id":150,"name":"hi!","timestamp":123456} → 41 字节,人类可读,但体积大、解析慢 Protobuf(二进制格式): 08 96 01 12 03 68 69 21 18 C0 C4 07 → 12 字节,不可读,但体积小、解析快 ``` 同样的数据,Protobuf 只需 JSON **约 1/3** 的体积。在移动端场景下,更小的包体意味着更省流量和更低延迟,这就是为什么大量 App(尤其是 Google 系、社交、游戏类)选择 Protobuf 作为通信格式。 Protobuf 通常工作在 **HTTP/HTTPS 之上**(作为请求/响应的 body),也常用于 **gRPC**(基于 HTTP/2 的 RPC 框架)。抓包时看到的 HTTP body 是一堆不可读的二进制数据,就很可能是 Protobuf。 #### 基本使用 首先定义 `.proto` 文件描述数据结构: ```protobuf // demo.proto syntax = "proto3"; message Demo { int32 id = 1; // 字段编号,不是默认值 string name = 2; int64 timestamp = 3; } ``` ```bash # 编译为 Python 代码 protoc --python_out=. demo.proto ``` ```python # 序列化 from demo_pb2 import Demo demo = Demo(id=150, name="hi!", timestamp=123456) data = demo.SerializeToString() # → 二进制 bytes # 反序列化 demo2 = Demo() demo2.ParseFromString(data) print(demo2.id, demo2.name) # → 150 hi! ``` #### Wire Format(二进制编码格式) Protobuf 序列化后的数据由一系列 `tag + value` 组成,没有分隔符,紧密排列: ``` ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐ │ tag │ value │ tag │ value │ tag │ value │ │(varint) │ │(varint) │ │(varint) │ │ └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘ 字段1 字段2 字段3 ``` **tag 的编码**:`tag = (field_number << 3) | wire_type` | wire_type | 含义 | 示例类型 | |-----------|------|----------| | 0 | Varint(变长整数) | int32, int64, bool | | 1 | 64-bit 固定长度 | fixed64, double | | 2 | Length-delimited(带长度前缀) | string, bytes, 嵌套消息 | | 5 | 32-bit 固定长度 | fixed32, float | **编码示例**:`int32 a = 1;` 设置 `a = 150` 后序列化得到 `08 96 01` ``` 08 = (1 << 3) | 0 → field_number=1, wire_type=0(varint) 96 01 的 Varint 解码: 96 = 1001_0110 → 最高位1表示后续还有字节,有效位:001_0110 01 = 0000_0001 → 最高位0表示结束,有效位:000_0001 小端拼接(低位在前):000_0001 001_0110 = 1001_0110 = 150 ``` Length-delimited 类型的格式为:`tag + length(varint) + payload`,例如 `string name = 2;` 设置 `name = "hi!"` 后编码为: ``` 12 03 68 69 21 │ │ └──────── payload: "hi!" 的 ASCII(3 字节) │ └─────────── length: 3(varint) └────────────── tag: (2 << 3) | 2 = 0x12,field_number=2, wire_type=2 ``` > 注意:Protobuf 的二进制数据中**不包含字段名称**,只有字段编号和类型信息。所以没有 `.proto` 定义文件时,反序列化只能得到 `field_1`、`field_2` 这样的编号,无法知道字段的语义。 #### 逆向 Protobuf 结构 抓包拿到 Protobuf 二进制数据后,如果没有 `.proto` 定义文件,需要逆向还原结构。 **第一步:从二进制数据推测结构** 使用 <a href="elink@22eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6E0K9h3I4V1M7%4g2F1M7X3W2K6k6g2)9J5c8Y4m8J5L8%4c8G2j5Y4g2X3i4K6u0V1K9h3&6K6M7r3g2U0N6r3!0J5">protobuf-inspector</a> 可以直接解析原始二进制数据,推测字段类型: ```bash # 将抓包得到的二进制数据传入 echo -ne '\x08\x96\x01\x12\x03\x68\x69\x21\x18\xc0\xc4\x07' | protobuf_inspector # 输出类似: # 1: 150 (varint) # 2: "hi!" (string) # 3: 123456 (varint) ``` 这一步能得到字段编号和大致类型,但无法确定字段名称和精确类型(比如分不清 int32 和 int64)。 **第二步:从二进制文件中恢复完整定义** 在 App 的 native 库(`.so`)中,`protoc` 编译器为每个 Protobuf 消息生成了 C++ 类,这些类包含丰富的类型信息: 1. 在 `.data.rel.ro` 段中查找 RTTI 类型信息,可能找到 proto 消息的虚表 2. 虚表中包含关键函数,可用于恢复完整结构 Protobuf 消息虚表中的关键函数偏移: | 偏移 | 函数 | 用途 | |------|------|------| | 0x10 | `GetTypeName()` | 返回消息类型名(如 `user.UserLogin`),**最容易定位** | | 0x50 | `_InternalParse()` | 反序列化核心,包含所有字段的编号和类型信息 | | 0x60 | `_InternalSerialize()` | 序列化核心,同样包含完整的字段信息 | **第三步:借助 AI 完成还原** 手动分析反汇编代码恢复 proto 结构非常繁琐,但这类工作非常适合 AI。推荐的工作流程: 1. 通过虚表中的 `GetTypeName()`(偏移 0x10)定位目标消息类型 2. 找到对应的 `_InternalParse`(偏移 0x50)或 `_InternalSerialize`(偏移 0x60)函数,在 IDA 中复制反汇编/伪代码 3. 同时用 protobuf-inspector 解析抓包得到的二进制数据 4. 将反汇编代码、抓包原始数据、protobuf-inspector 解析结果、接口 URL 等上下文一起交给 AI 分析: ``` 这是接口 /api/user/login 的 Protobuf 请求体。 1. 抓包原始 hex:08 96 01 12 03 68 69 21 18 C0 C4 07 2. protobuf-inspector 解析结果: 1: 150 (varint) 2: "hi!" (string) 3: 123456 (varint) 3. 以下是该消息的 _InternalSerialize 函数反编译代码: [粘贴 IDA 伪代码] 请结合以上信息,还原出完整的 .proto 定义,包括字段名称和精确类型。 ``` 反汇编代码提供字段编号和精确类型,抓包数据和 protobuf-inspector 提供实际值用于验证和推测字段语义,两者结合可以得到最准确的还原结果。 此外,`protoc` 为每个字段生成了 get/set 函数(如 `set_name()`、`id()`),在 IDA 中通过交叉引用找齐所有相关函数,同样可以交给 AI 进行关联分析。 --- # 二、TLS 协议 TLS(Transport Layer Security)是互联网安全通信的基石。理解 TLS 是进行 HTTPS 抓包的前提。 ## 2.1 公钥密码体系 ### 对称加密 最直观的加密方式是**对称加密**:加密和解密使用同一把密钥。 ``` 发送方 接收方 "Hello" ──[密钥K加密]──> "x7#f!" ──[密钥K解密]──> "Hello" ``` 常见的对称加密算法: | 算法 | 密钥长度 | 特点 | |------|----------|------| | AES-128/256 | 128/256 bit | 当前最主流,安全高效 | | ChaCha20 | 256 bit | 移动端友好,无需硬件加速 | | DES/3DES | 56/168 bit | 已淘汰,密钥太短 | 对称加密速度快、效率高,TLS 建立连接后的**数据传输阶段**就是使用对称加密(如 AES-GCM)。 但对称加密有一个根本问题:**双方必须事先共享同一把密钥**。在互联网场景中,客户端和服务器之间没有安全的信道来交换密钥——如果密钥在网络上传输,就可能被窃听者截获。 ### 非对称加密(公钥密码体系) **公钥密码体系**解决了密钥交换问题:每个人拥有一对密钥(公钥 + 私钥),公钥公开,私钥保密。公钥和私钥是数学上配对的,用其中一把处理过的数据,只有另一把能还原。这一特性产生了两种核心用法: #### 用法一:加密通信(公钥加密,私钥解密) 目的:**保密性** —— 确保只有接收方能读取内容。 ``` 发送方(Alice) 接收方(Bob) ┌──────────┐ ┌──────────┐ │ "Hello" │ │ 密文 │ │ ↓ │ ┌──────────┐ │ ↓ │ │ 加密 ←─┼─────┤ Bob 公钥 │ │ 解密 ←─┼── Bob 私钥(仅 Bob 持有) │ ↓ │ └──────────┘ │ ↓ │ │ 密文 ──┼──── 不安全信道 ────────>│ "Hello" │ └──────────┘ └──────────┘ ``` 任何人都可以用 Bob 的公钥加密消息,但只有 Bob 用自己的私钥才能解密。即使密文被截获也无法还原。 #### 用法二:数字签名(私钥签名,公钥验签) 目的:**身份认证 + 完整性** —— 证明消息确实来自某人,且未被篡改。 ``` 签名方(Bob) 验证方(Alice) ┌──────────┐ ┌──────────┐ │ 原始数据 │ │ 原始数据 │ │ ↓ │ │ ↓ │ │ Hash ──┼─→ 摘要 │ Hash ──┼─→ 摘要 │ ↓ │ │ │ │ 加密 ←─┼── Bob 私钥 │ 比对 ←─┼── 摘要 == 解密结果? │ ↓ │ │ ↑ │ │ 签名 ───┼──── 连同数据发送 ──────>│ 解密 ←─┼── Bob 公钥 └──────────┘ └──────────┘ ``` 签名过程:先对数据做 Hash 得到固定长度的摘要,再用私钥加密摘要,得到签名。验签过程:用公钥解密签名得到摘要,再与数据重新 Hash 的结果比对,一致则说明:① 数据确实由私钥持有者签发(身份认证);② 数据没有被篡改(完整性)。 #### 加密 vs 签名:本质区别 | | 加密 | 签名 | |------|------|------| | 谁操作 | 发送方用**对方公钥**加密 | 发送方用**自己私钥**签名 | | 谁验证 | 接收方用**自己私钥**解密 | 接收方用**对方公钥**验签 | | 解决的问题 | **保密性**:防窃听 | **身份认证 + 完整性**:防伪造、防篡改 | | 数学操作 | 完全相同(公私钥互操作) | 完全相同(公私钥互操作) | > 本质上加密和签名的数学运算是一样的(都是公私钥的互操作),区别在于**用哪把钥匙、由谁操作、解决什么问题**。 在 TLS 中两种用法都会用到: - **签名**:服务器用私钥签名握手参数,客户端用证书中的公钥验签 → 证明服务器身份 - **密钥协商**:通过 ECDHE 协商共享密钥(而非直接用公钥加密传输密钥,以实现前向安全) 非对称加密解决了密钥交换问题,但速度比对称加密慢得多(通常慢 100~1000 倍)。因此 **TLS 的实际做法是两者结合**:用非对称加密(ECDHE)协商出一个共享密钥,然后用这个共享密钥进行对称加密(AES-GCM)通信。 但公钥密码体系本身面临**中间人攻击**:攻击者可以替换公钥,让双方以为在与对方通信,实际上都在与攻击者通信。 **解决中间人攻击的演进路径**: 1. **HMAC 签名公钥**:使用预共享密钥对公钥做 HMAC,确保公钥完整性。但 HMAC 仍依赖预共享密钥,攻击者获取后可伪造 2. **CA 证书体系**:引入受信任的第三方(CA)对公钥签名,形成证书。客户端预装 CA 根证书,通过证书链验证公钥的真实性 ### CA(Certificate Authority)是什么? CA 本质上是一个**受信任的第三方公证机构**。可以类比为现实中的公证处: > 你要和陌生人签合同,怎么确认对方身份?——去公证处核实。公证处之所以可信,是因为它的资质由政府(更上级的权威)授予。 在数字世界中: ``` 网站(example.com)想让用户信任自己的公钥 ↓ 向 CA 提交申请(CSR),CA 验证域名所有权 ↓ CA 用自己的私钥对网站公钥签名,生成证书 ↓ 用户的浏览器/操作系统预装了 CA 的根证书(公钥) ↓ 用户收到网站证书后,用 CA 公钥验证签名 → 确认公钥真实 ``` **为什么 CA 能防止中间人?** 攻击者可以伪造公钥,但无法伪造 CA 的签名——因为 CA 的私钥只有 CA 自己持有。用户用预装的 CA 公钥验签,伪造的证书一定会验证失败。 常见的 CA 机构: | CA | 说明 | |------|------| | Let's Encrypt | 免费、自动化,目前使用最广泛 | | DigiCert | 商业 CA,常见于大型企业和金融机构 | | GlobalSign | 历史悠久的商业 CA | | Sectigo (原 Comodo) | 市场份额较大的商业 CA | 实际的信任体系是**分层**的:根 CA 不直接签发终端证书,而是签发中间 CA 证书,中间 CA 再签发网站证书,形成**证书链**: ``` 根 CA(Root CA)──── 离线保管私钥,预装在系统中 └─ 中间 CA(Intermediate CA)──── 日常签发工作 └─ 终端证书(example.com)──── 网站使用 ``` 这样即使中间 CA 被攻破,也可以吊销中间 CA 证书而不影响整个信任体系。根 CA 的私钥离线存储在硬件安全模块(HSM)中,极少使用。 ## 2.2 证书 ### 证书结构 一个 X.509 证书的核心字段: ``` Subject(主题) 证书所有者信息 Issuer(颁发者) 签发此证书的 CA Validity(有效期) Not Before 和 Not After Public Key(公钥) 证书持有者的公钥 SAN(主题备用名) 支持的域名列表 Basic Constraints CA:TRUE 表示这是 CA 证书 Key Usage 证书可用于哪些操作(签名、加密等) CA Signature CA 使用自己的私钥对以上内容的签名 ``` 证书签发流程:生成私钥 → 创建 CSR(含公钥、域名、私钥签名) → CA 签发证书 ### 证书格式 | 格式 | 编码 | 特点 | |------|------|------| | PEM | Base64 | 以 `-----BEGIN CERTIFICATE-----` 开头,文本可读 | | DER | 二进制 | 体积更小,不可直接阅读 | ### 证书校验 客户端收到服务器证书后的校验流程: ``` 服务器证书(server.crt) ↓ 用中间 CA 公钥验证签名 中间 CA 证书(intermediate.crt) ↓ 用根 CA 公钥验证签名 根 CA 证书(root.crt)← 系统预装,信任锚点 ``` 1. 服务端发送证书链(服务器证书 + 中间证书) 2. 检查证书是否过期 3. 检查域名是否匹配(CN 或 SAN) 4. 用上级 CA 的公钥验证签名,逐级向上直到找到系统预装的根证书 ### 证书绑定(Certificate Pinning) 证书绑定是指应用**内置期望的公钥指纹**(而非整个证书,因为证书会过期但公钥可以不变),在 TLS 握手时验证服务器证书的公钥是否与预期一致。 更高级的做法: - 客户端将获取到的公钥通过 HMAC 发送给服务端进行校验 - 客户端主动探测获取服务端证书,再发给服务端比对 ### 根证书存放位置 | 平台 | 位置 | |------|------| | Windows | `certmgr.msc` | | macOS | 钥匙串访问.app | | Linux | `/etc/ssl/certs` | | Android 系统证书 | `/system/etc/security/cacerts`<br>`/apex/com.android.conscrypt/cacerts`(Android 14+) | | Android 用户证书 | `/data/misc/user/0/cacerts-added` | **Android 证书体系的关键变化**: | 版本 | 变化 | |------|------| | Android 7 (API 24) | 默认不再信任用户安装的 CA 证书 | | Android 9 | 引入 Conscrypt 安全提供者(基于 BoringSSL),至今仍是默认提供者 | | Android 10 | 引入 Project Mainline,将证书存储模块化为 APEX 包 | | Android 14 | 新增 `/apex/com.android.conscrypt/cacerts` 路径 | 证书文件命名格式为 `<hash>.0`,hash 是证书 subject 的哈希值。多个证书哈希相同时后缀依次为 `.1`、`.2`。 ## 2.3 椭圆曲线密码学(ECC) TLS 中的密钥交换算法 ECDHE(Elliptic Curve Diffie-Hellman Ephemeral)基于椭圆曲线数学。 ### 椭圆曲线方程 椭圆曲线的一般形式为 `y² = x³ + ax + b`,比特币和以太坊使用的 **secp256k1** 曲线为 `y² = x³ + 7`(a=0, b=7)。 ``` y | / | / | / | / | / | / | / y² = x³ + 7 (secp256k1) | / .-----*--------' * (-∛7, 0) ≈ (-1.91, 0) / | * (0, ±√7) ≈ (0, ±2.65) --*-----------+---------------------------- x \ | '-----*--------. | \ | \ | \ | \ | \ | \ | \ | \ ``` 曲线关于 x 轴上下对称,左端在负 x 轴处(-∛7, 0)平滑转折,上下两支连续地向第 1、4 象限发散延伸,整体呈现向右开口的形态。 ### 椭圆曲线上的运算 **点加法**:给定曲线上两点 P 和 Q,过 P、Q 做直线交曲线于第三点 R',R' 关于 x 轴的对称点即为 P + Q = R。 **点倍乘(标量乘法)**:`P = k·G = G + G + ... + G`(k 次)。 - **G(基点)**:椭圆曲线上一个预先约定的固定点,所有参与方使用同一个 G(由曲线标准定义,如 secp256k1 规范中给出) - **k(私钥)**:一个随机生成的大整数,必须保密 - **P(公钥)**:对 G 做 k 次点加法得到的结果点,可以公开 ``` 点倍乘就是连续做"点加法": 1·G = G 已知起点 2·G = G + G 第 1 次加法 3·G = 2·G + G 第 2 次加法 ... ... k·G = ? 第 k-1 次加法 → 最终得到公钥 P 每一次加法都会"跳"到曲线上一个看似随机的新位置: y │ │ ● 2G ● 5G │ ● 3G │ ● G ● 7G │ ● 4G ─────┼─────────────────────────── x │ ● 8G │ ● 6G │ ``` ### 单向陷门:为什么安全? 椭圆曲线密码学的安全性在于**单向陷门函数**的性质: ``` ┌─────────────────────────────────────────────────────────────┐ │ │ │ 正向:私钥 k ──→ 公钥 P = k·G ✅ 非常容易 │ │ │ │ 就是做 k 次点加法,使用"倍加-累加"算法 │ │ 即使 k 是 256 位大数,也只需约 256 次运算 │ │ │ │─────────────────────────────────────────────────────────────│ │ │ │ 逆向:公钥 P ──→ 私钥 k = ? ❌ 几乎不可能 │ │ │ │ 已知 P 和 G,求 k 使得 P = k·G │ │ 这是"椭圆曲线离散对数问题"(ECDLP) │ │ 目前没有已知的高效算法,256 位密钥需要约 2^128 次运算 │ │ 即使全球算力集中也需要数十亿年 │ │ │ └─────────────────────────────────────────────────────────────┘ ``` 直觉理解:每次点加法都会跳到曲线上一个"不可预测"的位置。做了 k 次之后,最终位置 P 和起点 G 之间没有可利用的规律。正向计算像沿着路走 k 步,逆向破解像只看终点猜走了几步——而"路"是在曲线上不断折返的。 **私钥 k 是随机数,公钥 P = k·G**,这就是 ECC 的安全性基础。 ### 基于 ECC 的加解密(ECIES 思路) ECC 本身是基于点运算的,不能像 RSA 那样直接对消息加密。实际使用中通过 **ECDH 密钥协商 + 对称加密** 实现: ``` 加密过程(Alice → Bob): Bob 的公钥: P_b = k_b · G (公开) Bob 的私钥: k_b (保密) ① Alice 生成临时私钥 r(随机数),计算临时公钥 R = r·G ② Alice 用 Bob 的公钥计算共享点: S = r · P_b = r·k_b·G ③ 从 S 派生对称密钥 Key = KDF(S) ④ 用 Key 对消息做对称加密: 密文 = AES(Key, 明文) ⑤ 发送 (R, 密文) 给 Bob ↓ 解密过程(Bob): ① 收到 (R, 密文) ② Bob 用自己的私钥计算共享点: S = k_b · R = k_b·r·G ← 和 Alice 算出的一样! ③ 从 S 派生对称密钥 Key = KDF(S) ④ 用 Key 解密: 明文 = AES_Dec(Key, 密文) ``` > 核心思路:ECC 负责安全地协商出一个共享密钥,实际的加解密交给对称加密完成。TLS 中的 ECDHE 正是这个思路的应用。 ### ECDHE 密钥交换 ``` 客户端 服务端 私钥 a (随机数) 私钥 b (随机数) 公钥 A = a·G ──── 交换公钥 ────> 公钥 B = b·G <──────────────── 共享密钥 = a·B = a·(b·G) 共享密钥 = b·A = b·(a·G) = ab·G = ab·G ✓ 相同! ``` 窃听者只能获得 A 和 G,无法推算出 a,因此无法计算出共享密钥。共享密钥再通过 KDF(密钥派生函数)生成实际的会话密钥。 ECDHE 中的 **E(Ephemeral)** 表示每次握手都生成新的临时密钥对,即使长期私钥泄露,历史会话也无法被解密——这就是**前向安全性**。 ## 2.4 TLS 握手与密钥协商流程 ### 密码套件(Cipher Suite) 密码套件是一组算法的"套餐",告诉双方:用什么方式交换密钥、用什么算法加密数据、用什么算法校验完整性。 以 TLS 1.2 的一个典型密码套件为例: ``` TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 │ │ │ │ │ │ │ │ │ │ │ └── SHA256:PRF(伪随机函数)/ 密钥派生 │ │ │ │ └─────── GCM:认证加密模式(同时加密+完整性校验) │ │ │ └──────────── AES_128:对称加密算法,128 位密钥 │ │ └───────────────────── RSA:用 RSA 签名验证服务器身份(证书签名) │ └─────────────────────────── ECDHE:密钥交换算法(椭圆曲线 Diffie-Hellman) └───────────────────────────────── TLS:协议 ``` 简单说就是四个问题:**怎么交换密钥?怎么验证身份?怎么加密数据?怎么校验完整性?** TLS 1.3 做了大幅简化——密钥交换固定为 ECDHE,身份验证从套件中分离,套件只描述对称加密和哈希: ``` TLS_AES_128_GCM_SHA256 │ │ │ │ │ │ │ └── SHA256:HKDF 密钥派生 │ │ └─────── GCM:认证加密模式 │ └──────────── AES_128:对称加密 └───────────────── TLS:协议 密钥交换(ECDHE)和签名算法通过扩展字段单独协商 ``` ### TLS 1.3 握手流程(通俗版) 用一个比喻来理解整个握手过程——**两个人要在咖啡店(公共网络)交换秘密,旁边都是偷听的人**: ``` Client Server │ │ │ ─── ClientHello ──────────────────────────────> │ │ "我支持这些加密方式,这是我的临时公钥 A=a·G" │ │ │ │ <── ServerHello ────────────────────────────── │ │ "我选了这个加密方式,这是我的临时公钥 B=b·G" │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 双方各自计算共享密钥: │ │ │ │ Client: a·B = a·(b·G) = ab·G │ │ │ │ Server: b·A = b·(a·G) = ab·G ✓ 相同! │ │ │ │ │ │ │ │ 从 ab·G 派生出 Handshake Secret │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ══════ 此后所有消息用 Handshake Secret 加密 ═══ │ │ │ │ <── Certificate ────────────────────────────── │ │ 服务端发送证书链(服务器证书 + 中间CA证书) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 客户端验证证书: │ │ │ │ ① 检查证书是否过期 │ │ │ │ ② 检查域名是否匹配(SAN / CN) │ │ │ │ ③ 用中间CA公钥验证服务器证书签名 │ │ │ │ ④ 用根CA公钥验证中间CA证书签名 │ │ │ │ ⑤ 根CA证书在系统预装的信任列表中 → 信任链建立 │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ <── CertificateVerify ──────────────────────── │ │ 服务端用证书私钥对之前所有握手消息的哈希做签名 │ │ 客户端用证书中的公钥验签 → 证明服务端确实持有私钥 │ │ │ │ <── Finished ───────────────────────────────── │ │ 服务端发送所有握手消息的 HMAC 校验值 │ │ 客户端验证 → 确保握手过程未被篡改 │ │ │ │ ─── Finished ─────────────────────────────────> │ │ 客户端发送自己的 HMAC 校验值 │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ 从 Handshake Secret 派生出 Master Secret │ │ │ │ 再从 Master Secret 派生出 Application Secret │ │ │ └──────────────────────────────────────────────┘ │ │ │ │ ═══════════ 用 Application Secret 加密应用数据 ════ │ ``` > **证书中的公钥不用来加密数据吗?** > > 在 **TLS 1.3** 中:**不用**。注意看上面的流程——在 ClientHello / ServerHello 阶段,双方就已经通过 ECDHE 交换了临时公钥并计算出共享密钥。等到 Certificate 到达时,加密密钥早已协商完成。证书中的公钥**仅用于身份验证**(验证 CertificateVerify 的签名),不参与加密。 > > 在 **TLS 1.2 RSA 模式** 中:客户端会用证书中的公钥**加密一个预主密钥(Pre-Master Secret)** 发送给服务端,服务端用私钥解密得到预主密钥,双方再从中派生会话密钥。 > > ``` > TLS 1.2 RSA 模式(已被 TLS 1.3 淘汰): > > Client Server > │ ─── ClientHello ────────────────────────> │ > │ <── ServerHello + Certificate ─────────── │ > │ │ > │ 用证书公钥加密 Pre-Master Secret │ > │ ─── ClientKeyExchange ──────────────────> │ > │ [Encrypted(Pre-Master Secret)] │ > │ │ > │ 服务端用私钥解密得到 Pre-Master Secret │ > │ 双方从 Pre-Master Secret 派生会话密钥 │ > ``` > > 这种模式的致命缺陷:**没有前向安全性**。如果服务端的证书私钥日后泄露,攻击者可以解密**所有历史流量**(用私钥解密每个会话的 Pre-Master Secret)。而 ECDHE 每次握手都用新的临时密钥,证书私钥泄露不影响历史会话。这就是 TLS 1.3 强制使用 ECDHE 的原因。 ### ECC 参数在握手中的交换 回顾 2.3 节 ECDHE 的公式 `P = k·G`,看看各参数在 TLS 握手中是如何传递的: | ECC 参数 | 是什么 | 在哪个字段传递 | |------|------|------| | **G(基点)** | 曲线上的固定点 | 不直接传递。双方通过 `supported_groups` 扩展协商使用哪条曲线(如 x25519、secp256r1),曲线标准中已定义了 G | | **a(客户端私钥)** | 客户端生成的随机数 | **不传递**,始终保密 | | **A = a·G(客户端公钥)** | 客户端的临时公钥 | ClientHello 的 `key_share` 扩展 | | **b(服务端私钥)** | 服务端生成的随机数 | **不传递**,始终保密 | | **B = b·G(服务端公钥)** | 服务端的临时公钥 | ServerHello 的 `key_share` 扩展 | | **ab·G(共享密钥)** | 双方各自计算得到 | **不传递**,双方独立计算得到相同结果 | > 整个过程中,网络上只传输了公钥 A 和 B,私钥 a、b 和共享密钥 ab·G 从未出现在网络中。 ### 从共享密钥到通信密钥:密钥派生 ECDHE 协商出的 `ab·G` 并不直接用于加密通信。TLS 1.3 通过 **HKDF(基于 HMAC 的密钥派生函数)** 逐步派生出多把不同用途的密钥: ``` ECDHE 共享密钥 (ab·G) │ ▼ ┌─────────────┐ │ HKDF-Extract │ ← 提取熵,生成统一的密钥材料 └──────┬──────┘ │ ▼ Handshake Secret ──────── 用于加密握手阶段的消息 │ (Certificate、Finished 等) │ ▼ ┌─────────────┐ │ HKDF-Expand │ ← 派生出不同用途的子密钥 └──────┬──────┘ │ ▼ Master Secret │ ├──→ client_application_traffic_secret ──→ 客户端→服务端的加密密钥 │ ├──→ server_application_traffic_secret ──→ 服务端→客户端的加密密钥 │ └──→ exporter_master_secret ──→ 供应用层使用(如 EAP-TLS) ``` **为什么不直接用 ECDHE 的共享密钥加密数据?** | | ECDHE 共享密钥 (ab·G) | 最终通信密钥 (Application Secret) | |------|------|------| | 数量 | 只有 1 个 | 多个(读/写各一个) | | 长度 | 椭圆曲线点坐标(256 bit 点) | 精确匹配加密算法需求(如 AES-128 = 128 bit) | | 安全性 | 直接暴露可能泄露曲线信息 | 经 HKDF 处理后与原始值无数学关联 | | 绑定握手上下文 | 不包含握手信息 | 混入了 ClientHello、ServerHello 等握手内容的哈希,防止重放攻击 | | 密钥更新 | 需要重新握手 | 支持 KeyUpdate 消息在不重新握手的情况下更新密钥 | 简单说:ECDHE 只是提供了**原始的共享秘密**,经过密钥派生后,才变成真正安全、好用的通信密钥。 ### ClientHello 关键字段 加粗的字段可能被用作指纹: - 协议版本、随机数 - 会话/重用信息(session_id、PSK 等) - **密码套件列表**(Cipher Suites) - 压缩算法列表(TLS 1.2 及以前) - 扩展(Extensions): - SNI(server_name):一个 IP 可能托管多个 HTTPS 站点,服务器根据 SNI 返回对应证书 - **supported_groups**:支持的椭圆曲线,用于 ECDHE 密钥交换 - **signature_algorithms**:支持的签名算法,用于证书校验 - **ALPN**:协商应用层协议(如 h2、http/1.1),避免额外往返 - **supported_versions**:支持的 TLS 版本(TLS 1.3 中引入) - key_share:密钥交换参数,实现 1-RTT 握手 - session_ticket:会话恢复,快速重连 **ServerHello**:服务端从中选择一个密码套件、返回自己的临时公钥。此时双方已拥有共享密钥。 TLS 1.3 仅支持 5 种标准密码套件,全部使用 AEAD(同时提供加密和完整性校验): | 套件 | 特点 | |------|------| | TLS_AES_256_GCM_SHA384 | 最安全 | | TLS_AES_128_GCM_SHA256 | 性能与安全的平衡 | | TLS_CHACHA20_POLY1305_SHA256 | 移动设备友好 | | TLS_AES_128_CCM_SHA256 | IoT 场景 | | TLS_AES_128_CCM_8_SHA256 | 低功耗设备 | > TLS 1.3 中密码套件数量极少且全部使用 AEAD,因此**密码套件本身不再是有效的指纹特征**。 # 三、抓包 ## 3.1 传输层抓包:tcpdump tcpdump 工作在网络层,直接捕获原始数据包。常见的抓包软件如 Reqable、Charles、Fiddler 都是**应用层抓包**,只能解析应用层协议(HTTP/HTTPS);而 tcpdump 能捕获 TCP/UDP 等传输层数据。 ```bash # 捕获所有网络接口的 TCP 包,保存为 pcap 文件 tcpdump tcp -i any -w capture.pcap ``` 生成的 `.pcap` 文件可用 Wireshark 打开分析。 需要注意的是:**TLS 握手包本质上就是 TCP 包**,并且握手阶段的前几条消息(ClientHello、ServerHello)是**明文传输**的。用 tcpdump / Wireshark 抓到的 TCP 包可以直接看到: ``` TCP 包中能看到的 TLS 信息: ✅ 明文可见(握手协商阶段): ClientHello → 密码套件列表、SNI(域名)、supported_groups、key_share(公钥) ServerHello → 选定的密码套件、key_share(公钥) 加密不可见(Handshake Secret 加密): Certificate、CertificateVerify、Finished 加密不可见(Application Secret 加密): 应用层数据(HTTP 请求/响应等) ``` 所以即使不做 MITM,通过 tcpdump 也能获取不少信息:目标域名(SNI)、使用的密码套件、TLS 版本、椭圆曲线类型等。这也是 JA3/JA4 指纹的数据来源——它们完全基于 ClientHello 明文字段。但要看到实际的 HTTP 请求内容,就必须解密 TLS,这就需要应用层抓包。 ## 3.2 应用层抓包 ### TLS 抓包原理 MITM(中间人)代理抓包的核心原理: ``` 正常连接 MITM 抓包 Client ──────────────── Server Client ── Proxy ── Server TLS TLS① TLS② (代理证书) (真实证书) ``` 1. 抓包工具自己充当 CA,生成根证书 2. 用户将该根证书安装到系统信任列表 3. 客户端连接时,抓包工具根据请求的域名**动态签发**自签名证书返回给客户端 4. 客户端验证通过后,与代理建立 TLS 连接;代理再与真实服务器建立另一条 TLS 连接 5. 代理在中间解密、记录、再加密转发 #### 证书安装 在 Android 7+ 上,需要将抓包工具的 CA 证书安装到**系统证书目录**才能被信任。 **方式一:修改 App 网络安全配置(重打包)** ```xml <!-- AndroidManifest.xml --> <application android:networkSecurityConfig="@xml/network_security_config"> </application> ``` ```xml <!-- res/xml/network_security_config.xml --> <?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true"> <trust-anchors> <certificates src="system" /> <certificates src="user" /> </trust-anchors> </base-config> </network-security-config> ``` **方式二:安装到系统证书目录(需 root)** ```bash # 转换证书格式为 PEM openssl x509 -inform DER -in reqable.der -out reqable.pem # 计算证书 hash 值并重命名 CERT_HASH=$(openssl x509 -inform PEM -subject_hash_old -in reqable.pem | head -1) cp reqable.pem ${CERT_HASH}.0 # 获取 root 权限并挂载系统分区 adb root adb shell avbctl disable-verification # 禁用 AVB 验证 adb reboot && adb root adb remount # 推送证书并设置权限 adb push ${CERT_HASH}.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/${CERT_HASH}.0 adb shell chown root:root /system/etc/security/cacerts/${CERT_HASH}.0 adb shell chcon u:object_r:system_file:s0 /system/etc/security/cacerts/${CERT_HASH}.0 adb reboot ``` **方式三:tmpfs 临时挂载(remount 失败时的备选)** ```bash adb root adb shell mkdir -p /data/local/tmp/cacerts adb shell cp /system/etc/security/cacerts/* /data/local/tmp/cacerts/ adb push ${CERT_HASH}.0 /data/local/tmp/cacerts/ # 用 tmpfs 覆盖系统证书目录 adb shell mount -t tmpfs tmpfs /system/etc/security/cacerts adb shell cp /data/local/tmp/cacerts/* /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/* adb shell chown root:root /system/etc/security/cacerts/* adb shell chcon u:object_r:system_file:s0 /system/etc/security/cacerts/* # 注意:重启后失效,需重新执行或配置开机自动执行 ``` **方式四:Magisk 模块 <a href="elink@55aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6z5g2V1W2e0e0%4y4W2j5%4g2J5K9i4c8&6i4K6u0r3b7h3I4%4j5i4W2K6g2s2u0#2M7%4c8g2M7$3g2J5b7$3g2J5N6s2x3`.">AlwaysTrustUserCerts</a>** 自动将用户证书挂载到系统证书目录,无需手动操作。 ### DTLS 抓包 前面介绍了 TLS 的抓包原理(MITM 代理),它基于 TCP。而 DTLS(Datagram Transport Layer Security)是 **UDP 版本的 TLS**——在 UDP 基础上增加了加密和乱序/重传机制,常见于 WebRTC、IoT 等场景。 与 TLS 的区别:DTLS 握手多了一个 `HelloVerifyRequest` 步骤,用于防止 DoS 反射攻击。 **两种握手方式**: | 方式 | 适用场景 | 特点 | |------|----------|------| | 证书交换 | WebRTC 等 | 通常使用自签名证书,通过信令通道(SDP)交换的**指纹**来验证真伪 | | 预共享密钥(PSK) | IoT 等资源受限场景 | 双方提前配置相同的密钥和 ID,典型套件如 `TLS_PSK_WITH_AES_128_GCM_SHA256` | > **证书方式如何验证真伪?** 浏览器 A 在 SDP 中告诉浏览器 B:"我的证书哈希值是 `SHA-256: XX:YY:ZZ…`"。DTLS 握手时 B 收到证书后计算哈希值进行比对。 DTLS 抓包可以使用 mitmproxy 的 WireGuard 模式(仅限证书场景): ```bash mitmweb --mode wireguard -vvv --set proxy_debug=true ``` ### 代理抓包 最常用的方式,通过 HTTP/SOCKS 代理捕获应用层流量。 常用工具: - **Reqable**:跨平台 GUI 工具,支持 HTTP/HTTPS 和 SOCKS5 代理 - **mitmproxy / mitmweb**:开源命令行/Web 界面工具,支持脚本扩展 ```bash # mitmproxy 安装 pip install mitmproxy # Web 界面(默认代理端口 8080,Web 端口 8081) mitmweb --web-host 0.0.0.0 # WireGuard 模式:全局透明代理,不易被检测 mitmweb --mode wireguard --web-host 0.0.0.0 ``` > **mitmweb 的优势**:相比 Reqable、Charles 等工具,mitmweb 对 **WebSocket** 和 **DTLS** 有更好的支持。Reqable/Charles 主要面向 HTTP/HTTPS 请求-响应模式,对 WebSocket 的长连接帧和 DTLS(基于 UDP)的支持有限或不支持。而 mitmweb 可以实时查看 WebSocket 的每一帧数据(文本帧、二进制帧),也支持 UDP 流量的代理和解析,适合抓取游戏、音视频通话等使用 DTLS/UDP 的场景。 ### VPN 抓包 代理抓包属于**应用层抓包**(HTTP 代理),App 可以选择不使用系统代理;VPN 抓包位于**IP 层**,App 无法绕过。 ``` App 流量 → 虚拟网卡(tun0) → VPN 软件 → SOCKS5 代理 → 抓包工具 → 目标服务器 ``` 推荐的 VPN 代理工具:<a href="elink@0f5K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Z5k6h3W2Z5k6i4u0Q4x3V1k6K6L8$3y4C8M7%4c8#2L8R3`.`.">sockstun</a>(搭配 Reqable 的 SOCKS5 代理使用) > 如果应用检测 VPN(通过判断 tun0 网卡是否存在),可以将 VPN 部署到路由器上来规避。 ### iptables 抓包 通过 iptables 将流量透明重定向到 redsocks,再由 redsocks 转发到上游 SOCKS5 代理。应用**完全无法感知**代理的存在。 ```bash # 清空 NAT OUTPUT 链 iptables -t nat -F OUTPUT # 回环地址不重定向(避免死循环) iptables -t nat -A OUTPUT -d 127.0.0.1 -j RETURN # 上游代理地址不重定向(避免死循环) iptables -t nat -A OUTPUT -p tcp -d 192.168.1.1 --dport 1080 -j RETURN # 将 HTTP/HTTPS 流量重定向到 redsocks 监听端口 iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-ports 16666 iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-ports 16666 ``` redsocks 配置(<a href="elink@811K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6X3K9o6m8Q4x3X3g2Y4K9i4c8Z5N6h3u0Q4x3X3g2A6L8#2)9J5c8X3q4K6M7$3g2@1M7#2)9J5c8X3q4F1k6s2u0G2K9h3c8Q4x3X3c8J5k6h3c8K6L8$3y4C8M7K6u0Q4x3X3g2@1k6%4Z5`.">Android 预编译版本</a>): ```ini // redsocks.conf base { log_debug = on; log_info = on; log = stderr; daemon = off; redirector = iptables; } redsocks { bind = "127.0.0.1:16666"; // 接收 iptables 重定向的流量 relay = "192.168.1.1:1080"; // 上游 SOCKS5 代理 type = socks5; autoproxy = 0; timeout = 13; } ``` ```bash # 在 Android 设备上运行 ./redsocks5 -c redsocks.conf ``` ### Hook 抓包 当常规代理方式都被绕过时,可以直接 hook 应用的网络 I/O 函数。 > 通用的抓包软件(如 eCapture)只能抓使用**动态库**实现 TLS 的应用。如果应用**静态编译** TLS 库(如 Flutter 静态链接 BoringSSL),则需要自己分析定位 hook 点,可以结合 AI 分析反汇编代码。 #### Frida Hook send/recv 通过 Frida hook `socket`、`read`、`write`、`sendto`、`recvfrom` 等系统调用: ```javascript const socketFds = new Set(); // hook socket() 创建,记录文件描述符 Interceptor.attach(Module.getExportByName(null, 'socket'), { onEnter(args) { this.domain = args[0].toInt32(); this.type = args[1].toInt32(); this.protocol = args[2].toInt32(); }, onLeave(retval) { const fd = retval.toInt32(); if (fd >= 0) { socketFds.add(fd); console.log(`[socket] fd=${fd}, domain=${this.domain}, ` + `type=${this.type}, protocol=${this.protocol}`); } } }); // 通用数据打印函数 function tryPrintData(name, fd, buf, count) { try { if (!socketFds.has(fd)) return; const data = Memory.readByteArray(buf, count); console.log(`[${name}] fd=${fd}, size=${count}`); console.log(hexdump(data, { offset: 0, length: count, header: true, ansi: true })); } catch (e) { console.error(`[${name}] failed to read buffer: ` + e); } } // hook write/read/sendto/recvfrom Interceptor.attach(Module.getExportByName(null, 'write'), { onEnter(args) { this.fd = args[0].toInt32(); this.buf = args[1]; this.count = args[2].toInt32(); }, onLeave() { tryPrintData('write', this.fd, this.buf, this.count); } }); Interceptor.attach(Module.getExportByName(null, 'read'), { onEnter(args) { this.fd = args[0].toInt32(); this.buf = args[1]; this.count = args[2].toInt32(); }, onLeave(retval) { const n = retval.toInt32(); if (n > 0) tryPrintData('read', this.fd, this.buf, n); } }); Interceptor.attach(Module.getExportByName(null, 'sendto'), { onEnter(args) { tryPrintData('sendto', args[0].toInt32(), args[1], args[2].toInt32()); } }); Interceptor.attach(Module.getExportByName(null, 'recvfrom'), { onEnter(args) { this.fd = args[0].toInt32(); this.buf = args[1]; }, onLeave(retval) { const len = retval.toInt32(); if (len > 0) tryPrintData('recvfrom', this.fd, this.buf, len); } }); ``` #### eCapture —— 基于 eBPF 的 TLS 明文捕获 <a href="elink@341K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4L8$3A6#2k6g2)9J5c8X3g2U0j5i4m8@1N6i4u0W2">eCapture</a> 通过 eBPF uprobe hook SSL/TLS 库的 `SSL_read` / `SSL_write`,在加密前/解密后捕获明文。 支持的 TLS 库:OpenSSL、LibreSSL、BoringSSL、GnuTLS、NSPR(NSS) 前置条件:root 权限、Linux 内核支持 eBPF ```bash ecapture tls # 捕获所有 OpenSSL 流量 ecapture tls --pid 1234 # 捕获特定进程 ecapture tls -u 0 # 指定用户 UID ecapture tls --hex # 16 进制显示 ecapture tls --pcapfile="save.pcap" -m pcap # 保存为 pcap ecapture tls -l /tmp/tls.log # 保存日志 ``` #### 自定义协议栈 部分 App 不使用系统的 TLS 实现,而是自带 TLS 库(如 BoringSSL、Fizz 等)。好消息是这些库通常都是**开源的**,可以直接获取源码,结合 AI 分析证书验证逻辑和 hook 点,比纯黑盒逆向高效得多。 **为什么 Flutter 难抓包?** 1. **不使用系统代理**:直接使用底层 socket,不读取 Android 系统代理设置 2. **内置证书验证逻辑**:不信任用户安装的 CA 证书 3. **内置 TLS 库**:静态链接 BoringSSL,hook 系统 SSL 库的方式失效 **逆向 Flutter/DIO 的 BoringSSL**: 1. 使用 IDA 打开 `libflutter.so`,搜索 `ssl`、`tls` 字符串确认 SSL 库(出于安全考虑,应用通常不会自研 SSL 库) 2. 分析源码寻找 hook 点。可以借助 AI 加速分析,将 IDA 反编译结果和相关上下文提供给 AI,提示词参考: > 我正在逆向一个使用 BoringSSL 的 Flutter 应用(libflutter.so),需要绕过 SSL 证书校验。请帮我在 BoringSSL 源码中寻找合适的 hook 点,要求: > 1. 接口清晰,修改返回值即可关闭验证 > 2. 附近有特征字符串方便在 IDA 中定位 > 3. 给出函数签名、返回值含义、所在源文件路径 对于不熟悉的 TLS 库也是同样的思路——将反编译代码和字符串特征交给 AI,让它帮你定位验证函数和绕过方案。 3. BoringSSL 证书校验的关键函数:`ssl_crypto_x509_session_verify_cert_chain()` → 校验成功返回 1,位于全局函数表 `ssl_crypto_x509_method`,附近有 `ssl_server`、`ssl_client` 字符串 4. 使用 edbg 找到调用点,对返回值进行 patch;也可以 hook `memcmp` 等比较函数 **逆向 Fizz(Facebook 的 TLS 1.3 库)**: Fizz 证书校验的核心分支: ```cpp if (state.verifier()) { try { if (auto verifiedCert = state.verifier()->verify(state.unverifiedCertChain())) { newCert = verifiedCert; } else { newCert = std::move(leaf); } } catch (...) { /* 验证失败抛异常 */ } } else { newCert = std::move(leaf); // 没有 verifier,直接使用(不验证) } ``` 将 `if (state.verifier())` Patch 为永假,使其走 `else` 分支,可跳过所有证书验证(包括证书链验证)。 --- # 四、抓包检测与绕过 ## 4.1 代理检测 App 可以通过多种方式检测系统代理: **直接获取代理环境变量**: ```java // Java 层 String proxyHost = System.getProperty("http.proxyHost"); String proxyPort = System.getProperty("http.proxyPort"); if (proxyHost != null && proxyPort != null) { Log.w("Security", "Proxy detected!"); } // Native 层获取环境变量的函数不同,但字符串一样 ``` **通过系统 API 检测**: ```java String proxyHost = Settings.Secure.getString( getContentResolver(), Settings.Secure.HTTP_PROXY); ``` **选择不走代理**:App 也可以直接指定不使用系统代理(如 Flutter 的 DIO 库)。 **绕过方式**:使用 VPN 抓包或 iptables 透明代理;也可以通过 Frida hook 检测函数(如 `System.getProperty`、`Settings.Secure.getString`),使其返回空值。 ## 4.2 VPN 检测 **方式一:ConnectivityManager API(官方推荐,准确率高)** ```java ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); Network[] networks = cm.getAllNetworks(); for (Network network : networks) { NetworkCapabilities caps = cm.getNetworkCapabilities(network); if (caps != null && caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) { return true; } } ``` **方式二:网卡名称检测(启发式,可能误报/漏检)** ```java Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); while (en.hasMoreElements()) { NetworkInterface ni = en.nextElement(); String name = ni.getName().toLowerCase(Locale.getDefault()); if (name.startsWith("tun") || // OpenVPN, WireGuard name.startsWith("ppp") || // PPTP, L2TP name.startsWith("wg") || // WireGuard name.startsWith("ipsec")) { // IPSec return true; } } ``` **其他检测手段**: - **路由表检测**:检查路由表中是否存在 VPN 相关路由规则 - **DNS 检测**:检测 DNS 服务器是否被 VPN 修改 - **流量特征检测**:分析网络流量特征判断是否经过 VPN **绕过方式**:将 VPN 部署到路由器设备上,App 无法在本机检测到;也可以通过 Frida hook 检测函数(如 `NetworkCapabilities.hasTransport`、`NetworkInterface.getName`),篡改返回值绕过检测。 ## 4.3 JA3 指纹 JA3 通过提取 ClientHello 中的特定字段生成唯一指纹,用于识别客户端应用或 TLS 库: **提取字段**: 1. TLS 版本 2. **密码套件列表** 3. **扩展类型列表** 4. **椭圆曲线列表**(Supported Groups) 5. **椭圆曲线点格式**(EC Point Formats) 将以上字段转换为十进制,按固定格式拼接,做 MD5 得到 32 位十六进制 JA3 指纹。Wireshark 可直接识别。 > 服务端识别出指纹异常后通常**不会立即拒绝**,而是标记账号。App 比 Web 对协议栈的控制更底层,未来 TLS 指纹对抗会更加激烈。 ### 服务端指纹 客户端也可以检测服务端的 Server Hello 指纹。TLS 协议栈通常不支持直接获取 Server Hello,常用**探测法**: - 实现一个简易 TLS 协议专门用于探测(不用于实际通信,保证安全性) - 将服务端指纹通过 HMAC 发送给服务端进行互相验证 - 使用 stackplz / edbg 通过内存断点定位相关代码 ### uTLS —— 伪造 TLS 指纹 <a href="elink@e74K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6J5k6h3k6J5j5h3y4@1K9h3!0F1i4K6u0V1L8X3g2@1N6$3!0J5K9$3W2F1k6#2)9J5c8Y4g2@1L8s2x3`.">uTLS</a> 是 Go 标准 TLS 库的 fork,提供对 ClientHello 的低级访问,内置多种浏览器指纹: ```go import "github.com/refraction-networking/utls" // 内置指纹 uconn := utls.UClient(conn, config, utls.HelloChrome_Auto) uconn := utls.UClient(conn, config, utls.HelloFirefox_Auto) uconn := utls.UClient(conn, config, utls.HelloIOS_Auto) uconn := utls.UClient(conn, config, utls.HelloAndroid_11_OkHttp) // 完全自定义指纹 conn := utls.UClient(conn, config, utls.HelloCustom) spec := utls.ClientHelloSpec{ TLSVersMin: utls.VersionTLS13, TLSVersMax: utls.VersionTLS13, CipherSuites: []uint16{ 0x1302, // TLS_AES_256_GCM_SHA384 }, CompressionMethods: []byte{0}, Extensions: []utls.TLSExtension{ &utls.SupportedCurvesExtension{Curves: []utls.CurveID{utls.X25519}}, &utls.SupportedPointsExtension{SupportedPoints: []byte{0}}, &utls.SignatureAlgorithmsExtension{ SupportedSignatureAlgorithms: []utls.SignatureScheme{ utls.ECDSAWithP256AndSHA256, }, }, &utls.SupportedVersionsExtension{Versions: []uint16{utls.VersionTLS13}}, &utls.KeyShareExtension{KeyShares: []utls.KeyShare{ {Group: utls.X25519}, }}, }, } conn.ApplyPreset(&spec) ``` ## 4.4 证书绑定(SSL Pinning) ### 单向认证 标准 HTTPS 即为单向认证:客户端验证服务器身份。证书绑定在此基础上更进一步,不仅验证证书链,还要验证公钥指纹是否与预期一致。 ### 双向认证(mTLS) 服务器也要验证客户端身份。客户端持有含私钥的证书: - Android Java 中通常为 `.p12` 格式(通常有密码保护) - Python/curl/openssl 通常使用 PEM 格式,需要 `openssl` 命令转换 **提取客户端证书的方法**: - 通过 Hook App 提取证书和密码(OkHttp 如有混淆,可通过字符串定位) - 通过内存特征搜索,从进程内存中提取证书 - 提取后配置给抓包工具,指定密码和域名 ## 4.5 其他对抗手段 ### 自定义 TLS 扩展 OpenSSL 支持通过 `SSL_CTX_add_custom_ext` 在 ClientHello 中携带自定义数据(如设备 ID、混淆密钥)。 ```c // 注册自定义扩展(类型 0x1234) SSL_CTX_add_custom_ext( ctx, 0x1234, SSL_EXT_CLIENT_HELLO, obfs_add_ext_ex, obfs_free_ext_ex, nullptr, obfs_parse_ext_ex, nullptr ); // 回调:添加 2 字节负载 [enabled | xor_mask] static int obfs_add_ext_ex(SSL* ssl, unsigned int ext_type, unsigned int context, const unsigned char** out, size_t* out_len, X509*, size_t chainidx, int* out_alert, void* add_arg) { if (ext_type != OBFUSCATION_EXTENSION_TYPE) return 0; unsigned char* buf = (unsigned char*)OPENSSL_malloc(2); if (!buf) return 0; buf[0] = 1; // enabled buf[1] = XOR_MASK; // 混淆密钥 *out = buf; *out_len = 2; return 1; } ``` **对抗意义**:MITM 工具不会转发自定义扩展,服务端据此检测中间人;模拟发包缺少自定义扩展会被标记为异常。 ### 自定义 BIO 流量混淆 BIO 是 OpenSSL 的 I/O 抽象层,可以像管道一样串联。利用自定义 BIO Filter 可以在 TLS 密文之上再做一层混淆(如 XOR): ``` SSL_write(ssl, data, len) ↓ [SSL 加密] → TLS 密文 ↓ BIO_write(filter) → XOR 混淆 ↓ BIO_write(socket) → send() → 网络 ``` ```c // XOR 混淆 BIO Filter static int obfs_write(BIO* b, const char* data, int len) { BIO* next = BIO_next(b); if (!next || len <= 0) return 0; uint8_t* tmp = (uint8_t*)OPENSSL_malloc((size_t)len); if (!tmp) return 0; memcpy(tmp, data, (size_t)len); obfs_xor_fixed(tmp, (size_t)len); int ret = BIO_write(next, (const char*)tmp, len); OPENSSL_free(tmp); return ret; } ``` **抓包方式**:只能 hook `SSL_write` / `SSL_read` 捕获明文(下载源码通过调试信息中的 `SSL_write` 字符串定位函数)。 ### 密钥导出 TLS 1.3 支持通过 `ExportKeyingMaterial` 导出派生密钥(非通信密钥),不同 label 产生不同密钥。 应用场景:即使攻破了证书验证、对齐了各种指纹,客户端仍可在请求中附加基于导出密钥的 HMAC,服务端校验 HMAC 来确认 TLS 通道是真正的端到端连接。 ```go // Go 语言 key, err := connState.ExportKeyingMaterial( "EXPORTER-Channel-Binding", // label []byte("tls-fingerprint-lab"), // context 32, // key length ) ``` ```c // OpenSSL / C 语言 unsigned char key[32]; SSL_export_keying_material( ssl, key, 32, "EXPORTER-Channel-Binding", strlen("EXPORTER-Channel-Binding"), (unsigned char*)"tls-fingerprint-lab", strlen("tls-fingerprint-lab"), 1 // use_context ); ``` **绕过思路**:密钥导出依赖 TLS 库内部的 `ExportKeyingMaterial` / `SSL_export_keying_material` 函数,MITM 代理无法伪造(因为代理和真实服务器的 TLS 会话不同,派生出的密钥必然不同)。只能通过分析和 hook:定位 App 中调用导出密钥的位置,hook 该函数使其返回预期值,或者 hook 上层 HMAC 校验逻辑使其始终通过。 ### 其他指纹技术 | 指纹类型 | 基于 | 说明 | |----------|------|------| | HTTP/2 指纹 | HTTP/2 SETTINGS 帧、窗口大小、优先级 | 不同客户端的 HTTP/2 参数差异明显 | | TCP 指纹 | TCP 握手参数(窗口大小、MSS、TTL 等) | p0f 等工具可识别操作系统 | | 行为指纹 | 请求时序和请求模式 | 爬虫 vs 真实用户的行为差异 | | TLS 扩展指纹 | 更细粒度的 TLS 特征 | 扩展顺序、GREASE 值等 | --- # 五、抓包思路 经过前面的学习,我们对 Android 抓包的原理和方法有了系统性的了解。当碰到一个 App 无法抓到包时,可以按以下流程逐步分析和突破: ## 5.1 第一步:判断流量去哪了 首先确认 App 的网络请求是否真正发出,以及走的是什么协议: ``` tcpdump 抓取原始流量 │ ├── 有流量 → 确认目标 IP 和端口 │ ├── 443 端口 → HTTPS(常规 TLS) │ ├── 非标准端口 → 可能是自定义协议、DTLS、QUIC 等 │ └── UDP 流量 → 可能是 DTLS、QUIC、自定义 UDP 协议 │ └── 无流量 → 检查是否有网络权限、是否离线缓存 ``` ## 5.2 第二步:尝试常规代理抓包 使用 Reqable / Charles / mitmproxy 等代理工具,设置系统代理: ``` 设置系统代理 │ ├── 能抓到包 → 直接分析(最简单的情况) │ └── 抓不到包 → 分析原因: │ ├── App 不走系统代理? │ → 尝试 VPN 抓包或 iptables 透明代理 │ ├── 证书错误 / 连接失败? │ → 证书未安装或未被信任 │ → 尝试安装 CA 到系统证书目录 │ └── 连接成功但无数据? → 可能是非 HTTP 协议(WebSocket、gRPC、自定义协议) ``` ## 5.3 第三步:解决证书信任问题 如果代理工具抓不到包: ``` 证书不被信任 │ ├── Android 7+ 默认不信任用户证书 │ ├── 方案 A:重打包,修改 networkSecurityConfig 信任用户证书 │ ├── 方案 B:root 后安装到系统证书目录 │ ├── 方案 C:tmpfs 临时挂载 │ └── 方案 D:Magisk 模块 AlwaysTrustUserCerts │ └── 安装后仍然失败? → 很可能是证书绑定(SSL Pinning) ``` ## 5.4 第四步:绕过证书绑定 ``` SSL Pinning │ ├── 单向认证 Pinning │ ├── Frida + ssl_pinning_bypass 脚本(通用方案) │ ├── Xposed + JustTrustMe / TrustMeAlready │ ├── 针对 OkHttp:hook CertificatePinner.check() │ └── 针对 WebView:hook X509TrustManager │ └── 双向认证(mTLS) ├── 从 App 中提取客户端证书(.p12)和密码 │ ├── Hook App 的 KeyStore.load() 获取密码 │ ├── 内存搜索证书特征字节 │ └── 反编译定位证书资源文件 └── 将证书配置到抓包工具 ``` ## 5.5 第五步:对抗代理/VPN 检测 如果 App 检测到代理或 VPN 后拒绝请求: ``` 检测对抗 │ ├── 代理检测 │ ├── Hook System.getProperty() 返回 null │ ├── 改用 VPN 抓包(IP 层,不依赖代理设置) │ └── 改用 iptables 透明代理(完全不可感知) │ └── VPN 检测 ├── Hook ConnectivityManager / NetworkInterface 相关 API └── 将 VPN 部署到路由器上(App 在本机无法检测) ``` ## 5.6 第六步:处理自定义协议栈 如果 App 使用了自定义 TLS 库(如 Flutter 静态链接 BoringSSL): ``` 自定义 TLS 库 │ ├── 确认使用的 TLS 库 │ └── IDA 打开 .so,搜索 ssl/tls/boringssl 等字符串 │ ├── 定位证书校验函数 │ ├── 从开源代码分析校验流程 │ ├── 寻找特征字符串定位函数(如 ssl_server、ssl_client) │ └── 结合 AI 分析反汇编代码 │ └── Patch / Hook 校验逻辑 ├── 修改返回值使校验始终通过 ├── Hook memcmp 等比较函数 └── 使用 edbg / eBPF 定位并 patch ``` ## 5.7 第七步:应对高级防护 当常规手段都被绕过后,可能面临更深层的检测: ``` 高级防护 │ ├── TLS 指纹检测(JA3/JA4) │ ├── 使用 Wireshark 对比正常 App 和代理的 JA3 指纹 │ ├── 使用 uTLS 模拟目标指纹 │ └── 注意:服务端可能只是标记而非立即封禁 │ ├── 自定义 TLS 扩展 │ ├── 抓包对比正常请求,识别自定义扩展字段 │ └── 在代理工具或模拟请求中还原扩展 │ ├── BIO 流量混淆 │ ├── 网络层抓包看到的是混淆后的数据 │ └── 只能 hook SSL_write/SSL_read 获取明文 │ ├── 密钥导出校验 │ ├── 需要在真实 TLS 连接上导出密钥 │ └── MITM 代理无法生成正确的导出密钥 │ └── 服务端指纹校验 ├── 客户端验证 Server Hello 指纹 └── 需要分析校验逻辑并 hook 绕过 ``` ## 5.8 总结:抓包难度阶梯 ``` 难度递增 ──────────────────────────────────────────────> 无防护 证书信任 SSL Pinning 自定义协议栈 │ │ │ │ 直接代理 安装系统证书 Frida/Xposed IDA逆向+Patch 抓包即可 即可抓包 hook绕过 定位校验函数 │ │ 双向认证 TLS指纹 提取客户端证书 uTLS模拟 │ 自定义扩展/BIO 密钥导出校验 需要深度逆向 ``` 每一层防护都有对应的突破手段,关键在于**准确判断当前卡在哪一层**,然后对症下药。实际场景中往往是多种防护叠加使用,需要逐层剥离。 --- # 附录:常用命令速查 ## 证书操作 ```bash # 生成私钥 openssl genrsa -out server.key 2048 # 生成 CSR openssl req -new -key server.key -out server.csr # 生成自签名 CA 证书 openssl req -x509 -new -key ca.key -out ca.crt -days 3650 \ -subj "/C=CN/ST=Beijing/L=Beijing/O=My Company/CN=My Root CA" # 使用 CA 签发证书 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \ -out server.crt -days 365 -CAcreateserial # 查看证书 openssl x509 -in server.crt -text -noout # 验证证书链 openssl verify -CAfile ca.crt server.crt # 计算证书 hash(Android 证书命名用) openssl x509 -subject_hash_old -noout -in server.crt # 创建 SAN 扩展 echo "subjectAltName=DNS:example.local" > san.ext # 带 SAN 签发证书 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 365 -sha256 -extfile san.ext ``` SAN 配置文件(完整版): ```ini [req] default_bits = 2048 prompt = no default_md = sha256 distinguished_name = dn req_extensions = v3_req [dn] C = CN ST = Beijing L = Beijing O = MyOrganization CN = example.local [v3_req] subjectAltName = @alt_names keyUsage = digitalSignature, keyEncipherment extendedKeyUsage = serverAuth [alt_names] DNS.1 = example.local DNS.2 = www.example.local # DNS.3 = *.example.local # 通配符 # IP.1 = 192.168.1.100 # IP 地址 ``` ```bash # 使用配置文件生成 CSR openssl req -new -key server.key -out server.csr -config san.cnf # 带 SAN 扩展签发 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 365 -sha256 -extfile san.cnf -extensions v3_req ``` ## ADB 常用命令 ```bash adb devices # 查看连接设备 adb shell pm list packages | grep <keyword> # 查找应用包名 adb shell am start -n <package>/<activity> # 启动应用 adb logcat | grep -i vpn # 查看 VPN 相关日志 ``` ## 格式转换速查 ```bash # PEM → DER openssl x509 -in cert.pem -outform DER -out cert.der # DER → PEM openssl x509 -in cert.der -inform DER -outform PEM -out cert.pem # PEM → PKCS12(.p12/.pfx,含私钥,Android Java 常用) openssl pkcs12 -export -in cert.pem -inkey key.pem -out cert.p12 # PKCS12 → PEM(提取证书和私钥) openssl pkcs12 -in cert.p12 -out cert.pem -nodes # 查看 PKCS12 内容 openssl pkcs12 -in cert.p12 -info -nodes ```
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
#基础理论
收藏
・
23
点赞
・
18
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
wx_funcrever
期待更多优质内容的分享,论坛有你更精彩!
2026-9-5 16:57
tomggx
这个讨论对我很有帮助,谢谢!
2026-6-19 09:54
mb_appehgcn
你的帖子非常有用,感谢分享!
2026-5-29 00:36
ZSYL
非常支持你的观点!
2026-5-22 22:36
mb_ysyvtboy
你的分享对大家帮助很大,非常感谢!
2026-5-15 10:04
mb_ceuedqlu
为你点赞!
2026-4-30 10:11
dsapr
谢谢你的细致分析,受益匪浅!
2026-4-14 09:20
呼吸24K纯氧
+1
感谢你分享这么好的资源!
2026-4-13 15:00
mb_xrhmetpo
感谢你的积极参与,期待更多精彩内容!
2026-4-1 15:58
zhuzhu_biu
这个讨论对我很有帮助,谢谢!
2026-3-24 10:55
beimingyouyu
感谢你的积极参与,期待更多精彩内容!
2026-3-18 16:16
青眼白龙
谢谢你的细致分析,受益匪浅!
2026-3-16 21:23
P1umH0
这个讨论对我很有帮助,谢谢!
2026-3-16 19:28
sinker_
你的帖子非常有用,感谢分享!
2026-3-16 17:41
mb_upczeeld
为你点赞!
2026-3-16 15:43
签个到
谢谢你的细致分析,受益匪浅!
2026-3-16 08:16
逆天而行
感谢你的积极参与,期待更多精彩内容!
2026-3-16 03:46
Yves Luo
+1
谢谢你的细致分析,受益匪浅!
2026-3-15 23:18
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
7
)
Imxz
雪 币:
112
活跃值:
(9420)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
752
粉丝
9
关注
私信
Imxz
2
楼
tql
2026-3-16 10:41
0
mb_asiwnxyv
雪 币:
6
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
100
粉丝
0
关注
私信
mb_asiwnxyv
3
楼
感谢分享
2026-3-16 10:56
0
yuecanglan
雪 币:
233
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
3
粉丝
0
关注
私信
yuecanglan
4
楼
感谢分享!~
2026-3-19 21:34
0
呼吸24K纯氧
雪 币:
28
活跃值:
(1701)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
89
粉丝
2
关注
私信
呼吸24K纯氧
5
楼
太详细了,哈哈
2026-4-13 15:00
0
小Tzzz
雪 币:
37
活跃值:
(367)
能力值:
( LV2,RANK:10 )
在线值:
发帖
1
回帖
7
粉丝
0
关注
私信
小Tzzz
6
楼
酷
2026-4-20 15:36
0
mb_czvsfrqr
雪 币:
3
活跃值:
(230)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
24
粉丝
0
关注
私信
mb_czvsfrqr
7
楼
大佬我我这边有很多棋牌只有我希望可以认识你合作共赢共同发展 看到回复越秀区真心求合作一天上百个w
2026-8-21 23:46
0
一只鸭子
雪 币:
331
活跃值:
(920)
能力值:
( LV3,RANK:20 )
在线值:
发帖
15
回帖
57
粉丝
76
关注
私信
一只鸭子
8
楼
大佬对于抖音、淘宝、小红书这种定制ssl怎么看呢?感觉现在很少有大厂还在系统了,这块好难啊
2026-8-23 14:54
1
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
plight
2
发帖
0
回帖
20
RANK
关注
私信
他的文章
[原创]ptrace注入代码在不同平台的区别(ARM64、x86-64、MIPS64)
10711
[原创]Android APP 抓包学习笔记
5019
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部