初探 Flutter 的猫腻
本文仅用于逆向工程技术学习与安全研究交流,所有分析均基于个人学习记录。逆向的目的不是破坏,而是为了学习原理、理解机制、提升防护能力。
特别鸣谢:感谢 正己 大佬的文章
大佬文章在这里: https://www.52pojie.cn/thread-1951619-1-1.html
推荐认真阅读!!! 再次鸣谢大佬的文章
材料准备
- 某漫画APP v3.0.3
- frida == 16.6.6 使用 florida服务
- Blutter(我会直接提供学习素材,这里你下不下都无所谓了)
- 几款大伙都很熟悉的分析工具啦
抓取目标接口 - 任意一部漫画的具体章节列表
最开始通过使用wifi代理,但是我们可以发现是完全抓不到任何包的,这个时候我就很奇怪,难道和某宝一样做了什么特色的手段处理吗?
抓包如下图:
酷安社区抓取是没有任何问题的,但是抓取这个漫画软件的包就是只能抓到广告的sdk
这时候就切换使用 postern 进行 vpn 全局流量转发,欸,柳暗花明又一村。
这里大概讲一下二者区别:
-
Wi-Fi 代理抓包流程(HTTP Proxy)
┌─────────────────┐
│ App │
│ (HTTP Client) │
└────────┬────────┘
│
│ HTTP / HTTPS
│
┌────────▼────────┐
│ System Proxy │
│ (Wi-Fi Proxy) │
│ 例如: Charles │
└────────┬────────┘
│
│ 转发请求
│
┌────────▼────────┐
│ Internet │
│ 服务器/接口 │
└─────────────────┘
只有 走系统代理的请求 才会经过这里
如果 App 直接建 socket 那么就会完全抓不到包
-
VPN 抓包流程(TUN 虚拟网卡)
┌─────────────────┐
│ App │
│ TCP / UDP / DNS│
└────────┬────────┘
│
│ 所有网络请求
│
┌────────▼────────┐
│ OS 网络栈 │
└────────┬────────┘
│
│ 路由
│
┌────────▼────────┐
│ TUN 虚拟网卡 │
│ (VPN Tunnel) │
└────────┬────────┘
│
│ VPN Client 拦截
│
┌────────▼────────┐
│ 抓包 / 解析 │
│ 修改 / 转发 │
└────────┬────────┘
│
│
┌────────▼────────┐
│ Internet │
└─────────────────┘
VPN 直接将流量强制转发
-
对比如下:
┌──────────┐
│ App │
└─────┬────┘
│
┌─────────────┴─────────────┐
│ │
WiFi Proxy VPN Proxy
(应用层) (网络层)
│ │
│HTTP/HTTPS │所有IP流量
▼ ▼
┌──────────┐ ┌──────────┐
│ Proxy │ │ TUN │
│ Charles │ │ 虚拟网卡 │
└─────┬────┘ └─────┬────┘
│ │
▼ ▼
Internet Internet
我们惊喜的发现,是可以抓到包的,但是证书校验不通过。这里上网搜了一下,发现此 大佬 所言一致。
具体证书绕过推荐去看一下文章
那么我们简单对比一下两次请求的差距。
实际上每次变化的参数只有两个:X-Auth-Timestamp 和X-Auth-Signature
我们既然已经知道了此app使用了Flutter技术(当使用 Flutter 构建Android APP时,在assets文件夹下会有dexopt和flutter_assets两个文件夹。lib文件夹会有两个so文件:libapp.so和libflutter.so(flutter动态链接库,与实际业务代码无关)。)
那么我们先采用 blutter反编译尝试解决。
Blutter 项目结构
关键几个文件:
blutter_output/
│
├─ asm/ Dart结构恢复
│ ├─ class_xxx.asm
│ ├─ network.asm
│ ├─ http_client.asm
│ └─ ... 名字不固定
│
├─ pp.txt --> Object Pool(对象池)
├─ objs.txt --> objs.txt —— 完整对象结构
│
├─ blutter_frida.js 动态Hook脚本
一般流程:
1 pp.txt 找关键字符串
2 asm 找使用这些字符串的函数
3 dump.dart 看类结构
4 在 IDA/Ghidra 定位函数
5 Frida hook 验证
所需文件均在材料中
逆向分析开始(按理来说直接使用那个py文件替换,但是我使用的ida 9 貌似不太支持部分函数,所以我干脆直接分析代码了)
1 pp.txt 找关键字符串
[pp+0x8d68] "X-Auth-Signature"
先简单确认pp 是哪个寄存器(一般是x27)
集中观看几个指令:
1. add x1, x27, #0x8d68
2. ldr x1, [x27, #0x8d68]
在arm文件下查找:
findstr /m /c "0x8d68" *.dart --> 查出结果为空,说明 它指令很可能做了拆分
findstr /n /c "[pp+" *.dart --> 统一输出观看内容
第一条:abi.dart:62: // 0xa36944: ldr x4, [PP, #0x3e0] ; [pp+0x3e0] List(5) [0, 0x3, 0x3, 0x3, Null] --> 去ida查看 0xa36944
.text:0000000000A36944 LDR X4, [X27,#0x3E0]
那么 pp 有可能是 x27 ,对查看几条也是准确的
但是 直接搜索 0x8d68 是搜不到还是,将注意力放到 x27 上,因为Flutter AOT 代码中有一个重要结构ObjectPool就是 x27。继续观看libapp.so文件的代码,关注 x27 的位置。发现:
.text:0000000000A36B50 ADD X4, X27, #0x19,LSL#12
.text:0000000000A36B54 LDR X4, [X4,#0x7D8]
这就很有意思了, X4 = X27 + (0x19 << 12) + 0x7D8 = x27 + 0x197D8; 刚刚好对应 pp.txt里面的 [pp+0x197d8] List(7) [0, 0x4, 0x4, 0x3, "ase", 0x3, Null]
验证一下:
PS D:\work\study\hooks\output\output\asm> findstr /n /c /i "0xA36B54" *.dart
FINDSTR: 忽略 /c
abi.dart:244: // 0xa36b54: ldr x4, [x4, #0x7d8]
继续向下看去发现很多都是这种。
那么 [pp+0x8d68] "X-Auth-Signature" 就可以拆分为:
ADD Xn, X27, #0x8, LSL #12
LDR Xm, [Xn, #0xD68] 搜索 #0xD68 发现一堆。那么再找一个锚点锚定它
[pp+0x8d60] String: "X-Auth-Timestamp"
ADD Xn, X27, #0x8, LSL #12
LDR Xm, [Xn, #0xD60]
锚定点
X-Auth-Signature
Address Function Instruction
.text:00000000002AFC88 sub_2AFC28 LDR X16, [X27,#0xD68]
.text:00000000002DA4D0 sub_2DA30C LDR X17, [X17,#0xD68]
.text:00000000002FB7CC sub_2F9BEC LDR X0, [X0,#0xD68]
.text:0000000000306A78 sub_3068F8 LDR X3, [X3,#0xD68]
.text:0000000000311E4C sub_311D0C LDR X17, [X17,#0xD68]
.text:0000000000335C38 sub_335B80 LDR X1, [X1,#0xD68]
.text:000000000033946C sub_3390B4 LDR X16, [X16,#0xD68]
.text:0000000000339A5C sub_339908 LDR X1, [X1,#0xD68]
.text:00000000003441C0 sub_344170 LDR X1, [X1,#0xD68]
.text:000000000034E524 sub_34E520 LDR X0, [X0,#0xD68]
.text:0000000000353AE0 sub_353ADC LDR X0, [X0,#0xD68]
.text:000000000035B878 sub_35B874 LDR X0, [X0,#0xD68]
.text:0000000000387288 sub_387250 LDR X16, [X16,#0xD68]
.text:000000000038B68C sub_38AFF0 LDR X16, [X16,#0xD68]
.text:000000000038BF30 sub_38BE98 LDR X2, [X2,#0xD68]
.text:000000000038D99C sub_38D8E8 LDR X2, [X2,#0xD68]
.text:00000000003D1AAC sub_3D1A8C LDR X30, [X30,#0xD68]
.text:00000000003E9084 sub_3E8E1C LDR X3, [X3,#0xD68]
.text:000000000044A4CC sub_44A400 LDR X3, [X3,#0xD68]
.text:000000000046C658 sub_46C5F0 LDR X1, [X1,#0xD68]
.text:0000000000486C3C sub_486AF4 LDR X8, [X8,#0xD68]
.text:0000000000486CB0 sub_486AF4 LDR X8, [X8,#0xD68]
.text:00000000004CAD94 sub_4CAB94 LDR X16, [X16,#0xD68]
.text:00000000005053A4 sub_5052B8 LDR D0, [X17,#0xD68]
.text:000000000054D5D0 sub_54D560 LDR X1, [X1,#0xD68]
.text:000000000055BBBC sub_55BACC LDR X1, [X1,#0xD68]
.text:000000000056FB60 sub_56F3FC LDR X4, [X4,#0xD68]
.text:0000000000582418 sub_5823F0 LDR X16, [X16,#0xD68]
.text:000000000058A98C sub_58A8B8 LDR X0, [X0,#0xD68]
.text:000000000059452C sub_594458 LDR X0, [X0,#0xD68]
.text:00000000005A3C98 sub_5A3C70 LDR X16, [X16,#0xD68]
.text:00000000005C3548 sub_5C3250 LDR X3, [X3,#0xD68]
.text:00000000005E53A0 sub_5E531C LDR X3, [X3,#0xD68]
.text:00000000005E935C sub_5E8A08 LDR X30, [X30,#0xD68]
.text:00000000005EBC10 sub_5E8A08 LDR X30, [X30,#0xD68]
.text:00000000005F30E8 sub_5F2F40 LDR X16, [X16,#0xD68]
.text:00000000005F3E60 sub_5F3CC0 LDR X16, [X16,#0xD68]
.text:0000000000607B90 sub_607B78 LDR X0, [X0,#0xD68]
.text:0000000000613BEC sub_613BB8 LDR X17, [X17,#0xD68]
.text:0000000000622A08 sub_62295C LDR X30, [X30,#0xD68]
.text:000000000064FE64 sub_64FA18 LDR X1, [X1,#0xD68]
.text:000000000065B9D8 sub_65B8AC LDR X16, [X16,#0xD68]
.text:000000000065E9A0 sub_65E7D4 LDR X0, [X0,#0xD68]
.text:000000000066327C sub_6611B4 LDR X16, [X16,#0xD68]
.text:00000000007A7E90 sub_7A7DA0 LDR X16, [X16,#0xD68]
.text:00000000008E738C sub_8E6FE0 LDR X0, [X0,#0xD68]
.text:00000000008F7DDC sub_8F3ED0 LDR X0, [X0,#0xD68]
.text:00000000008F9558 sub_8F3ED0 LDR X0, [X0,#0xD68]
.text:00000000008FB174 sub_8F3ED0 LDR X0, [X0,#0xD68]
.text:0000000000903190 sub_8FF284 LDR X0, [X0,#0xD68]
.text:000000000090490C sub_8FF284 LDR X0, [X0,#0xD68]
.text:0000000000906528 sub_8FF284 LDR X0, [X0,#0xD68]
.text:00000000009147C4 sub_9146B4 LDR X16, [X16,#0xD68]
.text:00000000009435D4 sub_942EE4 LDR X1, [X1,#0xD68]
.text:0000000000991FDC sub_991F98 LDR X1, [X1,#0xD68]
.text:00000000009B8990 sub_9B8974 LDR X16, [X16,#0xD68]
.text:00000000009B8C80 sub_9B8974 LDR X16, [X16,#0xD68]
.text:00000000009B99B0 sub_9B976C LDR X16, [X16,#0xD68]
.text:00000000009B9CDC sub_9B976C LDR X16, [X16,#0xD68]
.text:00000000009BC7C8 sub_9BC770 LDR X2, [X2,#0xD68]
.text:0000000000A4F638 sub_A4F50C LDR X1, [X1,#0xD68]
.text:0000000000A7779C sub_A7776C LDR X0, [X0,#0xD68]
.text:0000000000A77810 sub_A777D4 LDR X0, [X0,#0xD68]
.text:0000000000A7793C sub_A7790C LDR X0, [X0,#0xD68]
.text:0000000000A7E59C sub_A7E348 LDR X1, [X1,#0xD68]
.text:0000000000A833E8 sub_A833A0 LDR X0, [X0,#0xD68]
.text:0000000000A8352C sub_A834E4 LDR X0, [X0,#0xD68]
.text:0000000000A83780 sub_A83738 LDR X0, [X0,#0xD68]
.text:0000000000A83CA0 sub_A83C4C LDR X0, [X0,#0xD68]
.text:0000000000A83E60 sub_A83C4C LDR X16, [X16,#0xD68]
.text:0000000000A83F4C sub_A83F04 LDR X0, [X0,#0xD68]
.text:0000000000A89FF0 sub_A89FC4 LDR X5, [X5,#0xD68]
.text:0000000000A8C808 sub_A8C7D8 LDR X30, [X30,#0xD68]
.text:0000000000A8F314 sub_A8EEEC LDR X30, [X30,#0xD68]
.text:0000000000AA0738 sub_AA06C4 LDR X3, [X3,#0xD68]
X-Auth-Timestamp
Address Function Instruction
.text:00000000002AFC6C sub_2AFC28 LDR X1, [X27,#0xD60]
.text:00000000002C5C90 sub_2C5B60 LDR X17, [X17,#0xD60]
.text:00000000002C64F4 sub_2C5E04 LDR X3, [X3,#0xD60]
.text:00000000002DA474 sub_2DA30C LDR X17, [X17,#0xD60]
.text:00000000002FB2F0 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB314 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB330 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB364 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB384 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB3C4 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB40C sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FB458 sub_2F9BEC LDR X4, [X4,#0xD60]
.text:00000000002FBD7C sub_2F9BEC LDR X4, [X4,#0xD60]
.text:0000000000306A30 sub_3068F8 LDR X16, [X16,#0xD60]
.text:0000000000311DE4 sub_311D0C LDR X17, [X17,#0xD60]
.text:00000000003440E4 sub_343FE8 LDR X1, [X1,#0xD60]
.text:000000000035035C sub_350358 LDR X0, [X0,#0xD60]
.text:0000000000368D5C sub_368D58 LDR X0, [X0,#0xD60]
.text:00000000003690BC sub_3690B8 LDR X0, [X0,#0xD60]
.text:000000000038002C sub_37FDEC LDR X3, [X3,#0xD60]
.text:000000000038BED4 sub_38BE98 LDR X16, [X16,#0xD60]
.text:00000000003D1924 sub_3D1904 LDR X30, [X30,#0xD60]
.text:00000000003FB488 sub_3FB340 LDR X1, [X1,#0xD60]
.text:00000000004090B4 sub_40901C LDR X3, [X3,#0xD60]
.text:0000000000424B14 sub_42487C LDR X3, [X3,#0xD60]
.text:00000000004421A4 sub_4420A8 LDR X3, [X3,#0xD60]
.text:000000000046BBCC sub_46B97C LDR X1, [X1,#0xD60]
.text:0000000000486B40 sub_486AF4 LDR X16, [X16,#0xD60]
.text:00000000004CACD8 sub_4CAB94 LDR X16, [X16,#0xD60]
.text:0000000000505194 sub_505180 LDR X1, [X1,#0xD60]
.text:000000000054D0BC sub_54CDCC LDR X1, [X1,#0xD60]
.text:000000000055BB90 sub_55BACC LDR X1, [X1,#0xD60]
.text:0000000000582AE0 sub_582A50 LDR X30, [X30,#0xD60]
.text:000000000058A814 sub_58A780 LDR X4, [X4,#0xD60]
.text:00000000005A3C4C sub_5A3C1C LDR X30, [X30,#0xD60]
.text:00000000005C3474 sub_5C3250 LDR X1, [X1,#0xD60]
.text:00000000005D7E50 sub_5D7A64 LDR X2, [X2,#0xD60]
.text:00000000005E9348 sub_5E8A08 LDR X30, [X30,#0xD60]
.text:00000000005EBBFC sub_5E8A08 LDR X30, [X30,#0xD60]
.text:00000000005F3090 sub_5F2F40 LDR X16, [X16,#0xD60]
.text:00000000005F3E08 sub_5F3CC0 LDR X16, [X16,#0xD60]
.text:00000000005FF1D8 sub_5FF060 LDR X3, [X3,#0xD60]
.text:0000000000603594 sub_603590 LDR X0, [X0,#0xD60]
.text:0000000000622990 sub_62295C LDR X30, [X30,#0xD60]
.text:0000000000626480 sub_62642C LDR X17, [X17,#0xD60]
.text:000000000064FCE0 sub_64FA18 LDR X17, [X17,#0xD60]
.text:0000000000652F9C sub_652E18 LDR X4, [X4,#0xD60]
.text:000000000065B9CC sub_65B8AC LDR X30, [X30,#0xD60]
.text:000000000065E8B0 sub_65E7D4 LDR X4, [X4,#0xD60]
.text:000000000065EF24 sub_65E7D4 LDR X4, [X4,#0xD60]
.text:0000000000663258 sub_6611B4 LDR X16, [X16,#0xD60]
.text:000000000066326C sub_6611B4 LDR X30, [X30,#0xD60]
.text:0000000000773494 sub_773334 LDR X1, [X1,#0xD60]
.text:00000000007A7DEC sub_7A7DA0 LDR X1, [X1,#0xD60]
.text:00000000007B0410 sub_7B00F0 LDR X2, [X2,#0xD60]
.text:00000000007D23B4 sub_7D2368 LDR X3, [X3,#0xD60]
.text:00000000008E7360 sub_8E6FE0 LDR X0, [X0,#0xD60]
.text:00000000008EE5A8 sub_8E8C28 LDR X17, [X17,#0xD60]
.text:00000000008EEF58 sub_8E8C28 LDR X17, [X17,#0xD60]
.text:00000000008F9850 sub_8F3ED0 LDR X17, [X17,#0xD60]
.text:00000000008FA200 sub_8F3ED0 LDR X17, [X17,#0xD60]
.text:00000000008FB068 sub_8F3ED0 LDR X17, [X17,#0xD60]
.text:0000000000904C04 sub_8FF284 LDR X17, [X17,#0xD60]
.text:00000000009055B4 sub_8FF284 LDR X17, [X17,#0xD60]
.text:0000000000915BCC sub_915B88 LDR X3, [X3,#0xD60]
.text:000000000092A710 sub_92A434 LDR X16, [X16,#0xD60]
.text:0000000000969CFC sub_969CD8 LDR X16, [X16,#0xD60]
.text:0000000000991F18 sub_991D64 LDR X16, [X16,#0xD60]
.text:0000000000992FA8 sub_992F14 LDR X2, [X2,#0xD60]
.text:0000000000993AAC sub_993A24 LDR X2, [X2,#0xD60]
.text:0000000000993C34 sub_993A24 LDR X2, [X2,#0xD60]
.text:0000000000994298 sub_99420C LDR X2, [X2,#0xD60]
.text:0000000000994508 sub_994424 LDR X2, [X2,#0xD60]
.text:0000000000994694 sub_994424 LDR X2, [X2,#0xD60]
.text:0000000000994868 sub_994424 LDR X2, [X2,#0xD60]
.text:0000000000994A10 sub_994424 LDR X1, [X1,#0xD60]
.text:0000000000994A40 sub_994424 LDR X16, [X16,#0xD60]
.text:00000000009CA748 sub_9CA688 LDR X1, [X1,#0xD60]
.text:00000000009DB078 sub_9DA904 LDR X3, [X3,#0xD60]
.text:00000000009F1880 sub_9F15F0 LDR X17, [X17,#0xD60]
.text:0000000000A3DE10 sub_A3DC24 LDR X3, [X3,#0xD60]
.text:0000000000A5054C sub_A50024 LDR X3, [X3,#0xD60]
.text:0000000000A7E3C8 sub_A7E348 LDR X2, [X2,#0xD60]
.text:0000000000A90C94 sub_A90C28 LDR X2, [X2,#0xD60]
.text:0000000000A91E40 sub_A91CA8 LDR X2, [X2,#0xD60]
.text:0000000000A958F8 sub_A957E0 LDR X1, [X1,#0xD60]
.text:0000000000A98378 sub_A98304 LDR X3, [X3,#0xD60]
.text:0000000000A9E3FC sub_A9E3A8 LDR X3, [X3,#0xD60]
按照我们上面分析的格式,双重定位到并查看格式,我们最后定位到sub_311D0C,这个函数同时抽取了这两个字符串。
--> 0x311de4 访问 pp+0x8d60
--> 0x311e4c 访问 pp+0x8d68
函数如下:
__int64 sub_311D0C()
{
__int64 v0; // x15
__int64 v1; // x21
__int64 v2; // x26
_QWORD *v3; // x27
unsigned __int64 v4; // x28
__int64 v5; // x29
__int64 v6; // x30
__int64 v7; // x29
__int64 v8; // x0
__int64 *v9; // x15
__int64 v10; // x1
__int64 v11; // x3
__int64 v12; // x0
__int64 v13; // x3
__int64 v14; // x16
_QWORD *v15; // x15
__int64 v16; // x0
_QWORD *v17; // x15
__int64 v18; // x0
__int64 v19; // x16
_QWORD *v20; // x15
__int64 v21; // x0
__int64 v22; // x16
__int64 *v23; // x15
__int64 v24; // x0
__int64 v25; // x16
_QWORD *v26; // x15
__int64 v27; // x1
_QWORD *v28; // x15
__int64 v29; // x0
__int64 v30; // x2
__int64 v31; // x0
_QWORD *v32; // x15
__int64 v33; // x1
__int64 v34; // x2
__int64 v35; // x0
__int64 v36; // x16
*(v0 - 16) = v5;
*(v0 - 8) = v6;
v7 = v0 - 16;
if ( (v0 - 64) <= *(v2 + 56) )
sub_A9B1EC();
v8 = sub_A11BA4();
v10 = v8 >> 1;
if ( (v8 & 1) != 0 )
v10 = *(v8 + 7);
v11 = v10 / 1000 / 1000;
v12 = 2 * v11;
if ( v11 != v12 >> 1 )
{
v12 = sub_A9B36C();
*(v12 + 7) = v13;
}
*(v7 - 8) = v12;
*v9 = v12;
*(v7 - 16) = sub_254ED8();
v14 = v3[518];
*v15 = *(v7 + 16);
v15[1] = v14;
*(v7 - 24) = (sub_8D45D8)();
v16 = sub_A23FDC();
*(v7 - 32) = v16;
*v17 = *(v7 - 24);
v17[1] = v16;
v18 = sub_311EA0();
v19 = v3[518];
*v20 = *(v7 - 16);
v20[1] = v19;
v21 = sub_8D45D8(v18);
v22 = *(v7 - 32);
*v23 = v21;
v23[1] = v22;
v24 = sub_8D28F4();
v25 = v3[4313];
*v26 = *(v24 + 7) + (v4 << 32);
v26[1] = v25;
*(v7 - 16) = sub_8D45D8(v24);
v27 = sub_A9B030();
*(v7 - 24) = v27;
*(v27 + 15) = v3[4524];
v29 = *(v7 - 8);
v30 = 59;
if ( (v29 & 1) != 0 )
v30 = *(v29 - 1) >> 12;
*v28 = v29;
v31 = (*(v1 + 8 * (v30 + 30391)))();
v33 = *(v7 - 24);
*(v33 + 19) = v31;
if ( (v31 & 1) != 0 && (*(v31 - 1) & (*(v33 - 1) >> 2) & HIDWORD(v4)) != 0 )
sub_A99640();
v34 = *(v7 - 24);
*(v34 + 23) = v3[4525];
v35 = *(v7 - 16);
*(v34 + 27) = v35;
if ( (v35 & 1) != 0 && (*(v35 - 1) & (*(v34 - 1) >> 2) & HIDWORD(v4)) != 0 )
v35 = sub_A99640();
v36 = v3[1127];
*v32 = v34;
v32[1] = v36;
return sub_260234(v35);
}
function sign(tapString):
now = DateTime.now()
timestamp = now / 1000 / 1000
tsString = intToString(timestamp)
str1 = convertString(tsString)
digest = hash(str1)
str2 = convertString(digest)
signature = convertString(str2)
map = {}
map["X-Auth-Timestamp"] = tsString
map["X-Auth-Signature"] = signature
return map
0x311d0c 同时引用了 X-Auth-Timestamp 和 X-Auth-Signature
0xa23de8 唯一调用 0x311d0c ida 按一下 x 就知道了
- 在
0xa23d70 处加载 tapString,并传入 0x311d0c
这里我们简单的验证一下猜想:
function hook_flutter() {
var module = Process.findModuleByName("libflutter.so");
if (!module) return;
var addr = module.base.add(0x5DC3CC);
console.log("[+] ssl_verify_result:", addr);
Interceptor.attach(addr, {
onLeave: function (retval) {
console.log("[+] SSL bypass");
retval.replace(1);
}
});
}
function wait_flutter() {
var m = Process.findModuleByName("libflutter.so");
if (m) {
console.log("[+] flutter loaded");
hook_flutter();
} else {
setTimeout(wait_flutter, 1000);
}
}
setImmediate(wait_flutter);
const SO_NAME = 'libapp.so';
const OFF = {
signFn: 0x311d0c,
strConv: 0x8d45d8,
intToString: 0x254ed8,
signCallSite: 0xa23de8,
};
let base = null;
function hx(v) {
return '0x' + ptr(v).toString(16);
}
function rangeOf(p) {
try {
return Process.findRangeByAddress(ptr(p));
} catch (e) {
return null;
}
}
function canRead(p) {
const r = rangeOf(p);
return !!r && r.protection.indexOf('r') !== -1;
}
function toAscii(u8) {
let s = '';
for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
return s;
}
const CidString = 93;
const CidTwoByteString = 94;
const CidExternalOneByteString = 95;
const CidExternalTwoByteString = 96;
function getCidFromTagged(taggedPtr) {
try {
const p = ptr(taggedPtr);
if (p.isNull()) return -1;
if (p.and(1).toInt32() === 0) return -1;
const obj = p.sub(1);
if (!canRead(obj)) return -1;
const tag = obj.readU32();
return (tag >>> 12) & 0xfffff;
} catch (e) {
return -1;
}
}
function readDartString(taggedPtr) {
try {
const p = ptr(taggedPtr);
if (p.isNull()) return null;
if (!canRead(p)) return null;
if (p.and(1).toInt32() === 0) return null;
const obj = p.sub(1);
if (!canRead(obj)) return null;
const cid = getCidFromTagged(p);
const lenTagged = obj.add(0x8).readU32();
const len = lenTagged >>> 1;
if (len <= 0 || len > 4096) return null;
if (cid === CidString) {
const bytes = obj.add(0x10).readByteArray(len);
if (!bytes) return null;
return toAscii(new Uint8Array(bytes));
}
if (cid === CidTwoByteString) {
return obj.add(0x10).readUtf16String(len);
}
if (cid === CidExternalOneByteString || cid === CidExternalTwoByteString) {
return null;
}
return null;
} catch (e) {
return null;
}
}
function readTaggedValue(taggedPtr) {
try {
const p = ptr(taggedPtr);
if (p.isNull()) return 'null';
if (p.and(1).toInt32() === 0) {
const smi = p.toInt32() >> 1;
return `smi(${smi})`;
}
const s = readDartString(p);
if (s !== null) return `"${s}"`;
const cid = getCidFromTagged(p);
return `${hx(p)}(cid=${cid})`;
} catch (e) {
return `<err:${e}>`;
}
}
function hookSignFlow() {
const signFn = base.add(OFF.signFn);
const strConv = base.add(OFF.strConv);
const intToString = base.add(OFF.intToString);
const signCallSite = base.add(OFF.signCallSite);
Interceptor.attach(signCallSite, {
onEnter(args) {
const tap = readTaggedValue(this.context.x0);
console.log(`[sign_callsite] x0 (tapString?) = ${tap}`);
}
});
console.log(`[+] hook sign_callsite @ ${signCallSite}`);
Interceptor.attach(signFn, {
onEnter(args) {
const tap = readTaggedValue(this.context.x0);
console.log(`[sign_fn] IN tapString = ${tap}`);
},
onLeave(retval) {
console.log(`[sign_fn] OUT map = ${hx(retval)}`);
}
});
console.log(`[+] hook sign_fn @ ${signFn}`);
Interceptor.attach(intToString, {
onEnter(args) {
this.isTs = false;
const raOff = ptr(this.returnAddress).sub(base).toInt32();
if (raOff === 0x311d60) {
this.isTs = true;
}
},
onLeave(retval) {
if (!this.isTs) return;
const s = readDartString(retval);
console.log(`[sign_out] x-auth-timestamp = ${s !== null ? s : hx(retval)}`);
}
});
console.log(`[+] hook intToString @ ${intToString}`);
Interceptor.attach(strConv, {
onEnter(args) {
this.isSig = false;
const raOff = ptr(this.returnAddress).sub(base).toInt32();
if (raOff === 0x311dc8) this.isSig = true;
},
onLeave(retval) {
if (!this.isSig) return;
const s = readDartString(retval);
console.log(`[sign_out] x-auth-signature = ${s !== null ? s : hx(retval)}`);
}
});
console.log(`[+] hook strConv @ ${strConv}`);
}
function waitAndHook() {
const timer = setInterval(() => {
const m = Process.findModuleByName(SO_NAME);
if (!m) return;
clearInterval(timer);
base = m.base;
console.log(`[+] ${SO_NAME} base = ${base}`);
hookSignFlow();
}, 100);
}
waitAndHook();
得到日志:
[sign_fn] IN tapString = "3af08590311032efe0660550a0563a53"
[sign_out] x-auth-timestamp = 1773668340
[sign_out] x-auth-signature = ecdfa909b94e2d7b015fad36cdb843b7b122905dcdc9a7c92b75e272e0e8e91f
[sign_fn] OUT map = 0x7500adf3b9
动态日志反证:为什么可以确认是 HMAC-SHA256
tapString = 3af08590311032efe0660550a0563a53
x-auth-timestamp = 1773668340
x-auth-signature = ecdfa909b94e2d7b015fad36cdb843b7b122905dcdc9a7c92b75e272e0e8e91f
最后做本地复算:
import hmac
import hashlib
tap = "3af08590311032efe0660550a0563a53"
ts = "1773668340"
print(hmac.new(tap.encode(), ts.encode(), hashlib.sha256).hexdigest())
输出:
ecdfa909b94e2d7b015fad36cdb843b7b122905dcdc9a7c92b75e272e0e8e91f
与动态日志完全一致。猜想正确。其实你直接看代码里面有魔术字符的
x-auth-signature = HMAC_SHA256(key=tapString, msg=str(epoch_seconds)).hexdigest()
另外,sub_311EA0 的行为特征也和 HMAC 结构一致(逆向等价):
- key 长度按
0x40(64 字节)分支处理
- 长 key 先做一次摘要后再参与后续流程
- 两段摘要流程(inner/outer)后输出 32 字节摘要,再转 hex 字符串
关键函数定位总表
| 函数地址 |
作用 |
0x311d0c |
生成签名头 map(X-Auth-Timestamp / X-Auth-Signature) |
0x311ea0 |
HMAC 核心计算(被 0x311d0c 调用) |
0xa23d70 |
读取 tapString(pp+0x8740) |
0xa23de8 |
调用 0x311d0c 的 callsite |
0x9fe574 |
计算 umstring(摘要后转 hex) |
0x9fe5e4 |
写入 tapString 到 map |
0x9fe4d8 |
写入 umString 到 map |
0x9fd094 |
生成 16 位 pseudoid |
0xaa28fc / 0xaa2964 |
pseudoid 初始化:有值复用,无值生成 |
0x46e0bc |
cartoons 参数组装(free_type/limit/offset/ordering/theme) |
剩下那两个参数定位也是一样的:
- 先通过头部构造函数
0xa22ff0 确认 umString 是动态写入字段(key 在 pp+0x86b0)。
- 追
umString 键名引用,命中 0x9fe4d8/0x9fe500(写 map)。
- 继续往上看数据来源,命中
0x9fe574(输入字符串 -> 摘要 -> hex)。
- 该函数内部调用链显示“摘要后十六进制字符串化”模式(
0x8d281c + 0x8d45d8)。
- 先在常量池找到:
- key 名
pseudoID -> pp+0x8630
- 字符集
0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ -> pp+0x89d8
- 追
pseudoID 写入点:
0xaa28fc/0xaa2964(初始化流程)
0x9fcf24/0x9fcf4c(写入 map)
- 追生成函数:
- 命中
0x9fd094
- 看到固定循环 16 次、每次从
pp+0x89d8 字符集中取一个字符拼接。