吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 332|回复: 0
上一主题 下一主题
收起左侧

[Android 原创] 安卓逆向 -- 某房产 App 响应内容逆向分析

[复制链接]
跳转到指定楼层
楼主
sumith 发表于 2026-5-22 15:57 回帖奖励
一、概述本文记录了对某 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 信息均已脱敏处理。


发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-23 07:41

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表