好友
阅读权限10
听众
最后登录1970-1-1
|
一、概述本文记录了对某 Flutter 房产类 App(Android arm64-v8a)的 API 响应内容进行逆向分析的完整过程。目标接口为附近设施查询接口,响应体经过了编码/压缩处理,初看像是加密数据。通过 IDA Pro 静态分析结合 Node.js 动态验证,最终完整还原了响应的处理算法。 二、响应体初步观察抓包得到目标接口的响应体为一段 Base64 字符串(已脱敏截取片段):eJyNlntsFEUcx+d6RBLwgfEJPgCjEUHJ7OzOPnJ/iIIaCUSjgRCV4O7OLjbWVrAG
iRqhWChQ6APoUVr6oA8gcKUtxd4d7RUTkMhD/1ETiX+oudm7S/RfpRWjs3e7173r
...(共 1780 字符)对其进行 Base64 解码后,得到 1334 字节的二进制数据。查看前两个字节:00000000: 78 9c 8d 96 7b 6c 14 45 1c c7 e7 7a 44 12 f0 81关键发现:78 9c 是 RFC 1950 zlib 压缩格式的标准魔数(CMF=0x78, FLG=0x9C,表示 deflate 算法,默认压缩级别)。这说明响应体并非加密,而是压缩后再 Base64 编码的数据。 三、IDA 静态分析3.1 搜索压缩相关字符串在 IDA 中搜索 zlib 和压缩相关字符串,找到以下关键证据:0x196f6f "Failed to create ZLibInflateFilter"
0x196f92 "Failed to create ZLibDeflateFilter"
0x1a3f94 "Filter_CreateZLibInflate"
0x1a3fad "Filter_CreateZLibDeflate"
0x19ab65 "bad compression info"
0x19af99 "unexpected zlib return"
0x1a86ac "unexpected zlib return code"这些字符串来自 Dart SDK 的 dart:io 库中的 ZLibInflateFilter / ZLibDeflateFilter 实现,确认了 Flutter 运行时内置了完整的 zlib 解压能力。3.2 搜索 Base64 相关字符串0x1d61b9 "Base64 decode error: "
0x1d68d0 "Base64 can't decode: "
0x24d100 "BAD_BASE64_DECODE"
0x24f235 "BASE64_DECODE_ERROR"追踪 0x1d61b9 的交叉引用,定位到错误处理函数 sub_A2A730,其调用者为 sub_4EADC0(PersistentCache::LoadSkSLs)。该函数中同时出现了 Base32 解码、Base64 解码和 zlib inflate 的调用链,与响应处理逻辑高度吻合。3.3 搜索加密相关字符串在 .rodata 段搜索 AES、ChaCha20 等加密算法字符串:0x18336e "Vector Permutation AES for ARMv8, Mike Hamburg (Stanford University)"
0x1833e0 "ChaCha20 for ARMv8, CRYPTOGAMS by <appro@openssl.org>"
0x182b40 "SHA1 block transform for ARMv8, CRYPTOGAMS by <appro@openssl.org>"
0x195a7f "aes-128-ctr"
0x195a8b "aes-256-ctr"这些字符串均来自 BoringSSL 库的版权声明,属于 Flutter 引擎的标准组件,并非 App 业务层的加密逻辑。针对该接口响应体,未发现任何 AES 密钥或加密调用。 四、算法还原与验证4.1 确定完整算法综合以上分析,响应体的处理算法为:响应体 = Base64( zlib_deflate( BSON ) )三层结构从外到内:层次格式说明
第一层Base64将二进制数据转为可传输的文本
第二层zlib deflateRFC 1950,魔数 78 9C,默认压缩级别
第三层BSONBinary JSON,MongoDB 使用的二进制序列化格式4.2 zlib 解压细节标准 zlib 格式(RFC 1950)结构如下:[2字节 header][deflate 压缩数据][4字节 Adler-32 校验和]解压时需跳过前 2 字节的 zlib header,对剩余数据执行 raw inflate。测试数据中 Adler-32 校验和不匹配(数据可能被截断),但 raw inflate 可以正常解压出完整内容。4.3 BSON 结构分析解压后得到 2709 字节的 BSON 文档,前 4 字节 95 0a 00 00 为小端 int32,值为 2709,与实际缓冲区大小完全吻合,符合 BSON 规范。文档结构如下(已脱敏处理地理坐标精度):{
"$array": {
"0": {
"keyword": "教育学校",
"data": {
"0": {
"id": "d09cbdcf07d959de49fc8dc9",
"title": "某幼儿园",
"distance": 588,
"location": { "lat": 31.xx, "lng": 119.xx },
"tag": "教育培训;幼儿园"
}
}
},
"1": { "keyword": "交通设施", "data": { ... } },
"2": { "keyword": "综合医院", "data": { ... } },
"3": { "keyword": "综合商场", "data": { ... } },
"4": { "keyword": "娱乐休闲", "data": { ... } }
}
} 五、解码实现基于以上分析,实现了零外部依赖的 Node.js 解码脚本,内置轻量 BSON 解析器:/**
* 响应解码: Base64 → zlib raw inflate → BSON 解析
* 零 npm 依赖,Node.js 内置模块即可运行
*/
const zlib = require("zlib");
function parseBSON(buf) {
let pos = 0;
function cstring() {
const start = pos;
while (buf[pos] !== 0) pos++;
const s = buf.toString("utf8", start, pos);
pos++;
return s;
}
function str() {
const len = buf.readInt32LE(pos);
pos += 4;
const s = buf.toString("utf8", pos, pos + len - 1);
pos += len;
return s;
}
function parseDocument() {
const size = buf.readInt32LE(pos);
const end = pos + size;
pos += 4;
const doc = {};
while (pos < end && pos < buf.length && buf[pos] !== 0) {
const type = buf[pos++];
if (type === 0) break;
const name = cstring();
switch (type) {
case 0x01: doc[name] = buf.readDoubleLE(pos); pos += 8; break; // double
case 0x02: doc[name] = str(); break; // string
case 0x03: doc[name] = parseDocument(); break; // embedded doc
case 0x04: doc[name] = parseDocument(); break; // array
case 0x10: doc[name] = buf.readInt32LE(pos); pos += 4; break; // int32
default: pos = end; // 跳过未知类型
}
}
if (pos < buf.length && buf[pos] === 0) pos++;
return doc;
}
return parseDocument();
}
function decodeResponse(base64Body) {
// Step 1: Base64 解码
const compressed = Buffer.from(base64Body, "base64");
// Step 2: 校验 zlib 魔数
if (compressed[0] !== 0x78) {
throw new Error(
`Expected zlib header 0x78, got 0x${compressed[0].toString(16)}`
);
}
// Step 3: 跳过 2 字节 zlib header,执行 raw inflate
const decompressed = zlib.inflateRawSync(compressed.subarray(2));
// Step 4: BSON 解析
return parseBSON(decompressed);
}
module.exports = { decodeResponse };验证结果$ node map-facility-decode.js
{
"$array": {
"0": {
"keyword": "教育学校",
"data": {
"0": {
"id": "d09cbdcf07d959de49fc8dc9",
"title": "某幼儿园",
"distance": 588,
"location": { "lat": 31.780119, "lng": 119.898167 },
"tag": "教育培训;幼儿园"
},
...
}
}
}
}解码成功,输出结构完整,与 App 界面展示的附近设施数据完全吻合。 六、总结该接口响应体并未加密,而是使用了 Base64 + zlib + BSON 的三层编码/压缩方案。问题结论
是否加密?否,无任何加密操作
编码方式Base64(最外层)
压缩算法zlib deflate,RFC 1950,魔数 78 9C
数据格式BSON(Binary JSON)
客户端实现Dart dart:convert + dart:io 内置库
是否需要密钥不需要这种设计的目的是减少网络传输体积(zlib 压缩),同时使用 BSON 提升序列化/反序列化效率,并非出于安全考虑。
本报告仅用于技术研究与学习目的,所有地理坐标及 ID 信息均已脱敏处理。
|
|
发帖前要善用【论坛搜索】功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。 |
|
|
|
|
|