代理里没有请求,首先是路由问题;连接已经到达但 HTTPS 失败,才需要继续追查 TLS 决策。这个 2020 年 iOS 测试应用用 Dart HttpClient 分别发送 HTTP 与 HTTPS 请求,适合把两道边界拆开验证。
分析针对随包携带的 ARM64 Flutter 引擎,不代表所有 Flutter 版本或插件都采用相同网络栈。请求中出现 Dart/2.7 (dart:io),但仅凭这一字段仍不足以确定完整引擎提交和构建参数。
先建立 HTTP 基线
最小证据链包含设备、网关和代理三个观察点。应用显示成功,只能说明它取得了某个响应;要证明请求走过指定路径,还需在入口连接与代理记录中找到对应操作。
| 观察点 | 需要看到什么 | 缺失时优先检查 |
|---|---|---|
| 网关入口 | 来自测试设备的连接 | Wi-Fi、VPN、接口、路由和 DNS |
| HTTP 代理 | 主机、路径与响应状态 | 重定向、监听地址、透明解析 |
| TLS 握手 | 连接到达并开始握手 | 目的地址恢复、SNI、服务端可达性 |
| 应用结果 | 响应与代理记录一致 | 证书、主机名及业务层检查 |
本例可通过分析机热点或 OpenVPN 把设备流量交给 Linux 网关。目的不是再填一次系统 HTTP 代理,而是让网关确实处于转发路径。Dart 的 findProxy 是独立代理选择接口,官方说明其未设置时使用直接连接;平台通道与其他客户端仍需分别检查。Dart HttpClient.findProxy
- 1
准备专用配置
只传递本次测试所需的配置文件,通过受控方式交给设备。不要将整个主目录作为 HTTP 文件目录,避免顺带暴露 shell 历史、密钥目录或其他文件。
- 2
交给客户端
打开配置文件,选择 OpenVPN。历史界面显示文件导入页,再通过 ADD 完成导入;实际界面名称随客户端版本变化。
- 3
连接隧道
确认配置已导入,并进入 CONNECTED 状态。连接计数变化证明隧道有活动,不直接证明某条业务请求已进入代理。
- 4
先发送 HTTP
触发 HTTP Request,在设备、网关和代理三处关联同一次请求,再进入 HTTPS 检查。
重定向与透明解析是两件事
网关示例将专用入口上的 TCP 80/443 送到本地 8080 监听器。tun0、wlan0、eth0 是环境相关的接口名;转发、NAT、防火墙后端和路由应按实际拓扑确认。只匹配测试设备或专用入口,避免把代理自身的出站请求再次送回代理。
代理设置里的关键状态可以归纳为下表。192.0.2.1 仅为说明地址,不是部署值。
| 设置 | 说明值或状态 |
|---|---|
| Bind to port | 8080 |
| Bind to address | 指定网关入口地址,例如 192.0.2.1 |
| Support invisible proxying | 开启 |
| Force use of TLS | 未勾选 |
| Redirect to host / port | 本例界面留空 |
普通显式代理期待带完整 URL 的 HTTP 请求,HTTPS 通常先用 CONNECT 指明目的地;透明重定向过来的客户端可能直接发送路径形式请求或 TLS 握手。Burp 的 invisible proxying 处理这类输入,HTTP 目的主机依赖 Host,握手阶段证书选择还会受到 SNI 影响。缺少相关信息时应明确配置目标,而不是假定 NAT 已替代理解决全部语义。PortSwigger:Invisible proxying
从 IPA 确认映像和坐标系
样本使用的框架路径是:
- Payload/
- Runner.app/
- Frameworks/
- Flutter.framework/
- Flutter
- Flutter.framework/
- Frameworks/
- Runner.app/
样本包含 ARMv7 与 ARM64 切片,这里分析后者。先确认实际加载的切片,以及代码是否处于可分析状态;若加密代码尚未恢复成可信指令,特征搜索的命中没有足够解释力。
文件偏移、映像虚拟地址、运行时地址是三个坐标系。对文件支持的某个 segment,换算关系是:
image_va = segment_vmaddr + file_offset - segment_fileoff
runtime_va = image_va + slide
segment_fileoff <= file_offset < segment_fileoff + segment_filesizevmaddr、fileoff 和 filesize 的角色可与 Apple 的 Mach-O 结构定义核对。零填充区没有一一对应的文件字节;通用规则不是“把任意文件偏移直接加到模块基址”。Apple loader.h
常量只负责产生候选
对 0x186 的标量搜索出现两个候选位置,十进制值均为 390:
| 位置 | 指令 | 所在函数标签 |
|---|---|---|
| 0x0007c320 | mov w3, #0x186 |
FUN_0007c178 |
| 0x00406a08 | mov w3, #0x186 |
FUN_004068c8 |
这些是分析数据库中的地址,不是设备运行时地址。第二个候选的上下文出现 ssl_x509.cc、ssl_server 与 ssl_client 字符串,并含证书处理分支。把常量、字符串引用和调用关系放在一起,才支持将其识别为证书链校验外层决策函数。
该函数的可见控制流最终返回 uVar5:失败路径置为 0,成功路径置为 1。但其他 BoringSSL 接口可能返回枚举;例如 ssl_verify_result_t 的首项 ssl_verify_ok 对应 0。所以“把校验返回值统一改成 1”并不是跨函数的正确规则。BoringSSL ssl.h
前缀匹配需要区间与唯一性
五个 Flutter 映像的对照结果为四个命中、一个未命中;部分命中地址相同,部分已经移动。完整版本与映像摘要未给出,这组结果只支持有限构建间的复用观察,不支持稳定跨版本匹配。
缩短前缀可能增加召回,也会增加误命中。静态候选应依次满足:位于正确映像的可执行区域、确为函数入口或已理解的调用点、出现在当前 HTTPS 路径上、返回约定已经确认。唯一命中只是其中一项。
下面的离线检查覆盖 segment 范围、空特征、零命中、多命中和 32 字节长度。地址换算使用示例数值;它不是完整 Mach-O 搜索器,也没有连接设备。
def fileoff_to_runtime(fileoff, segment_fileoff, segment_filesize,
segment_vmaddr, slide):
if not segment_fileoff <= fileoff < segment_fileoff + segment_filesize:
raise ValueError("offset outside file-backed segment")
return segment_vmaddr + fileoff - segment_fileoff + slide
def unique_signature_offset(blob, signature):
if not signature:
raise ValueError("empty signature")
hits = [i for i in range(len(blob) - len(signature) + 1)
if blob[i:i + len(signature)] == signature]
if len(hits) != 1:
raise ValueError(f"expected one match, got {len(hits)}")
return hits[0]
assert fileoff_to_runtime(0x2340, 0x2000, 0x1000,
0x100004000, 0x200000) == 0x100204340
for offset in (0x1fff, 0x3000):
try:
fileoff_to_runtime(offset, 0x2000, 0x1000, 0x100004000, 0)
except ValueError:
pass
else:
raise AssertionError("out-of-range offset accepted")
assert unique_signature_offset(b"ABCDWXYZ", b"WXYZ") == 4
for blob, sig in ((b"ABAB", b"AB"), (b"AB", b"X"), (b"AB", b"")):
try:
unique_signature_offset(blob, sig)
except ValueError:
pass
else:
raise AssertionError("invalid signature match accepted")
prefix = bytes.fromhex(
"ff 03 05 d1 fc 6f 0f a9 f8 5f 10 a9 f6 57 11 a9 "
"f4 4f 12 a9 fd 7b 13 a9 fd c3 04 91 08 0a 80 52")
assert len(prefix) == 32
assert 0x004068c8 + len(prefix) == 0x004068e8
print("PASS: segment bounds; unique/zero/multiple/empty matches; 32-byte prefix")$ python flutter_ios_models.py
PASS: segment bounds; unique/zero/multiple/empty matches; 32-byte prefix在函数返回处调整结果会保留函数主体的副作用,这与直接替换整个函数不同。即使外层返回成功,内部会话、错误状态或后续校验仍可能不一致。动态验证需要继续追到应用实际收到响应,而不是以 Hook 被调用作为完成标志。
用代理与设备两端闭合证据
代理记录来自 Burp Suite Community Edition 2020.2.1。两条请求均为 GET,路径为 /,状态码为 200;以下主机使用说明域名,长度保留观察值,表示代理列表中的 Length 列而非臆定的正文长度。
| 协议 | 主机 | 方法与路径 | 状态码 | Length | MIME |
|---|---|---|---|---|---|
| HTTP | example.com |
GET / | 200 | 1833 | HTML |
| HTTPS | secure.example.com |
GET / | 200 | 43137 | HTML |
HTTPS 请求的可见头部如下,Host 已替换,未展示的响应内容不作补齐:
GET / HTTP/1.1
user-agent: Dart/2.7 (dart:io)
Accept-Encoding: gzip, deflate
content-length: 0
host: secure.example.com
Connection: close
界面示意图:并列展示 HTTP 基线和 HTTPS 检查的状态,不表示两次操作发生在同一时刻。
设备界面分别显示 HTTP 与 HTTPS 成功,代理侧同时具备可读请求记录。这支持对本例路由与证书决策两道边界的解释,但不是系统信任库设置成功的证明,也不证明业务层二次签名、固定证书或响应校验已被一并处理。
复核新样本时,应重新记录引擎版本、架构、映像摘要、候选位置、返回语义和前后请求状态。可复用的是验证顺序,不是这个构建的绝对地址和函数前缀。