Qurtimurti/PicPat

GitHub: Qurtimurti/PicPat

该报告逆向分析了 PicPat 应用中 Amber Security SDK 的加密协议,发现了多个安全设计缺陷。

Stars: 0 | Forks: 0

image # PicPat: 逆向分析 Amber Security SDK 及其线协议 一份关于 Android 客户端 PicPat 如何对其后端请求进行签名和加密,以及其中哪些设计决策值得商榷的研究式分析报告。 ## 1. 关于该应用 PicPat 是 Locket Widget 的克隆版——一款通过主屏幕小组件分享照片的应用。你拍一张照片,点击发送,它就会实时显示在朋友的主屏小组件上;他们的照片也同样会出现在你的小组件上。这类应用的整体卖点是:没有通知、没有信息流,只是一个从你的核心朋友圈“实时”推送照片的“小组件”。 在内部,PicPat 几乎没有试图掩盖其与原版应用的渊源:包名为 `locket.live`,应用类为 `LocketApplication`,主题为 `Theme.Locket`。然而,“Locket”这个品牌在商店页面或用户界面中完全没有出现——它在商店里是一个独立的产品,但重命名从未传播到代码中。 | 字段 | 值 | |---|---| | 商店名称 | PicPat — Photos Widget & Share | | Android 包名 | `locket.live` | | 版本(分析时) | `1.5.7` (vcode 59) | | Google Play | [play.google.com/.../locket.live](https://play.google.com/store/apps/details?id=locket.live) — 100万+ 安装,2.8星(约2.6K条评论),社交类别,分级12+ | | 开发者网站 | [build4world.com](http://build4world.com/) | | 开发者 | Build4world(法律实体:One Dot Mobile Limited,香港) | | Split-APK 架构 | `armeabi-v7a`, `arm64-v8a` | | 后端 API | `https://api.locket.live` | | 媒体存储 | Firebase: `loli-2c6ab.appspot.com` (私有), `loli-2c6ab-public` (公共) | | 原生加密库 | `libamber_security.so` (~36 KB) | | 加密 SDK Java 包 | `com.amber.lib.security` | Play Store 应用卡中一个有趣的细节是:在数据安全声明下,它声称“**数据未加密**”。分析表明*存在*传输加密(AES-256-CBC + HMAC-SHA256);Google 的声明具体指什么是一个未解之谜——可能指静态存储,也可能指其他。无论如何,该声明与实际情况不符。 ## 2. 摘要 该应用使用基于 `com.amber.lib.security` SDK 构建的自定义客户端-服务器加密包装器。该 SDK 随附来自同一家中国供应商的 17 个应用家族。逆向分析完全恢复了其协议:请求签名 (`SIGN_V2`) 和请求体的认证加密 (`REQUEST_V2` / `RESPONSE_V2`)。 主要发现: - 原生库中内嵌了 **17 个硬编码的 32 字节 ASCII 密钥**——每个家族应用一个。提取一个库即可危及所有 17 个产品。 - 同一个 32 字节材料同时用作 **AES-256-CBC 密钥、HMAC-SHA256 密钥和 MD5 签名的盐值**——这是明显的密钥分离违规。 - 签名基于 **MD5**,采用自定义的 `MD5(prefix || secret || suffix)` 构造,而非 HMAC,尽管同一个库中已实现了 HMAC-SHA256 并用于密文认证。 - 在调试版本中,签名密钥是字面量字符串 `"release"`。 重建的协议已得到验证:解密我自己的捕获流量可获得正确的明文,并通过 HMAC 校验;自计算的签名与通过 Frida 钩子从原库捕获的签名逐字节匹配。 ## 3. 分析范围 工作在模拟器上完成——**Nox**,x86 架构带 ARM 转译,Android 7.1 (API 25),Samsung SM-G965N 配置文件。模拟器很方便,因为 frida-server 在上面运行稳定。 ### 3.1. Amber Security SDK 与应用家族 类 `com.amber.lib.security.NET` 声明了一个包含 17 个值的 `AppId` 枚举——来自同一供应商的每个应用一个,它们共享相同的加密包装器: ``` 0 WALLPAPER 8 POLICE_SCANNER 1 CALLER 9 LOCATOR 2 EASE 10 CLUBROOM 3 WEATHER_GEO 11 HIDEU 4 DAILY_LUCKY 12 OBOS 5 DAILY_NEWS 13 FILE2 6 DAILY_SALE 14 LOCKET 7 TRACK_PACK 15 WISE_MATE 16 WISE_ART ``` 仅从名称就可以勾勒出供应商的产品组合:壁纸、来电显示、天气、运势预测类(lucky)、新闻、促销优惠、追踪器、警察扫描仪、导航、聊天室、保险库类应用(`HIDEU`)、文件管理器(`FILE2`)、AI 工具。PicPat 注册为 `AppId.LOCKET = 14`。 ## 4. 工具与方法 逆向工程分两个阶段进行。 **静态分析。** 通过 [jadx](https://github.com/skylot/jadx) 反编译 APK。使用 [Ghidra](https://ghidra-sre.org/) 对 `libamber_security.so`(armeabi-v7a, 32位 ARM,Thumb 模式)进行反编译和反汇编。研究的部分包括:`SecurityController` 类(标志位)、`NET`(JNI 包装器)、`AES`(JNI 包装器),以及通过 `JNI_OnLoad → RegisterNatives` 进入原生代码的入口点。 **动态验证。** 在 Nox 模拟器上使用 [Frida](https://frida.re/),钩子挂在 `com.amber.lib.security.NET.getSign(String[], int, int)`。这为请求签名提供了实时的 `(输入数组, 输出数组)` 对——足以验证重建的算法。 对我自己捕获流量的解密通过中间人代理(HTTP Toolkit)完成,然后使用本地 Python 脚本进行后处理。 ## 5. 客户端保护架构 ### 5.1. `SecurityController` —— 版本标志 保存协议版本标志: ``` public class SecurityController { public static final int SIGN_V1; // = 1 public static final int SIGN_V2; // = 2 public static final int RESPONSE_V1; // = 256 (0x100) public static final int RESPONSE_V2; // = 512 (0x200) public static final int REQUEST_V1; // = 65536 (0x10000) public static final int REQUEST_V2; // = 131072(0x20000) // ... } ``` 三个独立加密机制的版本被打包到一个整数中(每个占 6 位): ``` getSignVersion(flag) -> flag & 0x3F // bits 0-5 getResponseVersion(flag) -> (flag >> 8) & 0x3F // bits 8-13 getRequestVersion(flag) -> (flag >> 16)& 0x3F // bits 16-21 ``` 被分析应用的活动配置是 `SIGN_V2 | REQUEST_V2 | RESPONSE_V2 = 131586`,通过 HTTP 头 `Security-Controller: v=131586` 传递给服务器。 初始化时,`SecurityController` 会尝试 `NET.getSign({"AAA","BBB"}, 2)` 作为自检。失败时,它会设置 `sLoadedSecurity = false`,加密静默关闭,并留下供应商未翻译的这条特征性日志消息: ``` 没有集成security,无法使用加密模块!!! ("Security not integrated, encryption module cannot be used!!!") ``` ### 5.2. `NET` 类与动态 JNI 注册 ``` public class NET { static { System.loadLibrary("amber_security"); } public static native String encrypt(String, int, int, byte[]); public static native String decrypt(String, int, int); public static native String[] getSign(String[], int, int); } ``` 原生方法**不是**以常见的 `Java_<包名>_<类名>_<方法名>` 方式导出的。相反,`JNI_OnLoad` 为类 `com/amber/lib/security/NET`(3个方法)和 `com/amber/lib/security/AES`(2个方法)调用了 `RegisterNatives`: ``` void JNI_OnLoad(JavaVM* vm) { JNIEnv* env; (*vm)->GetEnv(vm, &env, JNI_VERSION_1_4); register_natives(env, "com/amber/lib/security/AES", aes_methods, 2); register_natives(env, "com/amber/lib/security/NET", net_methods, 3); } ``` 这是一种**中等程度的反逆向措施**:如果不读取 `JNINativeMethod` 表,分析人员无法直接将 Java 方法映射到 C 函数(在 Ghidra 中,这些显示为匿名的 `FUN_xxxxxx`)。 一旦表被解包: | Java 方法 | C 函数 | |---|---| | `NET.encrypt(String,int,int,byte[])` | `0x17f08 → 0x1627c`(跳板) | | `NET.decrypt(String,int,int)` | `0x17f20 → 0x1627c`(跳板) | | `NET.getSign(String[],int,int)` | `0x16360` | | `AES.encrypt(byte[])` | `0x17efc → ...`(跳板) | | `AES.decrypt(byte[])` | `0x17f02 → ...`(跳板) | `encrypt` 和 `decrypt` 是一个通用函数的跳板,带方向标志。标准模式。 image ### 5.3. 密钥数组 在库 `.rodata` 段的地址 `0x19124` 处,存放着一个包含 17 个 `char*` 指针的数组,每个指针指向一个 32 字节的 ASCII 空字符结尾的字符串。这些字符串是 SDK 中每个加密操作的密钥材料。这些字符串是 `[0x21..0x7E]` 范围内的可打印字节,每个正好 32 字节(256 位——AES-256 密钥长度),没有明显的内部结构;它们看起来像是 `pwgen 32 17` 的输出。 secrets ## 6. 协议:签名与加密 ### 6.1. 签名 (`SIGN_V2`) 算法从地址 `0x16360` 处的原生函数恢复,并通过 Frida 钩子验证。 ``` inputs: params — dict[str, str] original request parameters secret — bytes (32) the secret picked by AppId algorithm: 1. random := uniform_int(1_000_000, 999_999_999) # 7-9 digit nonce 2. pairs := list(params.items()) + [("_random", str(random))] 3. pairs.sort(key=lambda kv: kv[0]) # sort pairs by key 4. concat := "".join(s for kv in pairs for s in kv) # k1 v1 k2 v2 ... no separator 5. pos := random mod len(concat) 6. salted := concat[:pos] + secret + concat[pos:] # secret inserted into the string 7. signature := md5(salted).hexdigest() # 32 hex chars output: params["_random"] := str(random) params["_sign"] := signature ``` 一个参考向量(通过 Frida 实时捕获): | 字段 | 值 | |---|---| | AppId | 14 | | Version | 2 | | 输入数组(扁平化) | `[os_ver, 7.1.2, language, ru, pkg, locket.live, vcode, 59, network, WIFI, image_firebase_id, k8GAudccnAMzatz23HvA53Wykpfh, os_vcode, 25, referrer, utm_source%3Dgoogle-play%26utm_medium%3Dorganic, uid, Z9E2102B6DA8BE106ACDC77022FE9454D9C, vname, 1.5.7, name, PicPat, os_name, Android, model, SM-G965N, time, 1778921496896, lang, _, brand, samsung, _timestamp, 1778929670]` | | 返回的 `_random` | `650720498` | | 返回的 `_sign` | `8de3d8ed288dc10df72c6ce3c26486ee` | 使用上述算法手动计算签名(使用原始未混淆的值)可逐字节重现相同的 `_sign`。上面显示的值是合成的——请参阅顶部的免责声明。 image Java 胶水代码大致这样构建输入数组(简化自 `createCall`): ``` Params params = createBaseParams(); params.merge(NetManager.getInstance().getGlobalParams()); if (request.getAddExtraParams()) params.merge(mPrivacyExtraParams.getExtraParams(context)); params.merge(request.getParams()); params.set("_timestamp", String.valueOf((System.currentTimeMillis() - mTimeDiff) / 1000)); String[] sign = NET.getSign(params.toArray(), signVersion); for (int i = 0; sign != null && i < sign.length; i += 2) params.set(sign[i], sign[i + 1]); ``` `params.toArray()` 返回扁平数组 `[k1, v1, k2, v2, ...]`。原生函数接收该数组,自身追加 `("_random", randomStr)`,按键进行成对排序,连接,将密钥插入到 `random % len` 偏移量处,计算 MD5。它返回 `["_random", str, "_sign", str]`,胶水代码将其写回 `params`。之后 `params` 被序列化为查询字符串并经过加密(§6.2)。 一些实现说明: - `_timestamp` 是 Unix 秒,由 `mTimeDiff`(累积的客户端-服务器时钟漂移)修正。单独的 `time` 字段以毫秒为单位,是**业务逻辑性的**——它表示相关实体最后一次更改的时间,而不是当前时刻。 - 排序是成对进行的:位置 `i` 和 `i+1` 一起移动。在反编译中,这表现为一个对两个索引都使用步长为 2 的双重循环,并同时交换 `(k, v)` 对。 - 连接不使用分隔符。理论上这引入了微小的歧义(`{a:bc}` 和 `{ab:c}` 得到相同的 `abc`),但 `params` 中的键是唯一的,因此不可利用。但作为设计原则,值得标记。 ### 6.2. 加密 (`REQUEST_V2` / `RESPONSE_V2`) 数据块格式: ``` ┌──────────┬──────────┬──────────────────────────────┐ plaintext ──AES──▶│ IV │ MAC │ AES-256-CBC ciphertext │──▶ base64 ──▶ URL-encode ──▶ HTTP body │ 16 bytes │ 32 bytes │ (len % 16 == 0) │ └──────────┴──────────┴──────────────────────────────┘ random nonce HMAC-SHA256(secret, ciphertext) ``` 该方案是 **加密后 MAC**(Bellare–Namprempre,2000)。与签名不同,这是认证加密的正确组合。 加密: ``` iv := random_bytes(16) padded := plaintext || pkcs7_pad(plaintext) ciphertext := AES_256_CBC(key=secret, iv=iv).encrypt(padded) mac := HMAC_SHA256(key=secret, msg=ciphertext) blob := iv || mac || ciphertext transport := base64(blob) ``` 解密是镜像过程:base64 解码,拆分为 `(IV, MAC, ciphertext)`,重新计算 HMAC 并进行比较,AES-256-CBC 解密,去除 PKCS#7 填充。 实现说明: - AES 是内部实现的(不是整体链接 OpenSSL/mbedTLS)。两个迹象:库的大小(~36 KB)和特征性的 `auStack_128[256]` 扩展密钥缓冲区——这是教科书式的 AES 密钥调度大小。 - SHA-256 也是内部实现的,具有规范的初始哈希值 `0x6a09e667, 0xbb67ae85, ...`。 - HMAC 按教科书实现:`H((K ⊕ opad) || H((K ⊕ ipad) || msg))`,其中 `H = SHA-256`,使用标准的 `0x36` 和 `0x5c` 常量。 - 填充采用 PKCS#7:加密时填充到块大小,解密时验证最后一个字节在 `[1..16]` 范围内并进行裁剪。 image ## 7. 分析发现的问题(设计缺陷) ### 7.1. 原生库中的硬编码共享密钥 **问题。** 17 个密钥以明文形式存在于库的 `.rodata` 段。该库随每个客户端分发,任何安装该应用的人都可以提取。 **影响。** 危及一个库 = 危及供应商的所有 17 个产品(它们之间共享)。拥有这些密钥的攻击者可以解密来自该家族任何应用的自己的流量;如果完全了解服务器 API,他们可以伪造签名并加密任意请求。 **如何修复。** 1. **用每安装实例的密钥替换硬编码共享密钥。** 首次启动时,客户端生成一个密钥对,并通过引导调用(例如带设备证明的引导调用)将其公钥发布到服务器。之后,使用密钥交换(ECDH / X25519)。一个在所有客户端共享的密钥不能真正称为密钥——它随 APK 一起在 Play Store 上发布。 2. **使用硬件支持的密钥存储**(Android Keystore + 可用的 StrongBox)来存储客户端的私钥。这可以阻止在 root 设备上的简单提取。 3. 如果由于某种原因确实需要共享密钥(例如,反作弊),那么至少应该**动态混淆并加密存储它**,仅在使用时刻解密,并具备内存转储保护。但这只是权宜之计。 ### 7.2. 跨加密原语的密钥重用 **问题。** 同一个 32 字节材料同时用作: - AES-256-CBC 密钥, - HMAC-SHA256 密钥, - MD5 签名中的盐/密钥。 **影响。** 密钥分离违规。在观察到的实现中,没有直接可利用的后果显现(在单一密钥的加密后 MAC 组合中,HMAC 和 AES 在理想化假设下仍提供 IND-CCA2 安全性),但这是糟糕的做法:将来对任何一个原语的任何更改,或对其中任何一个原语发现的任何新攻击,都会自动在其他原语中产生漏洞。 **如何修复。** 通过 **HKDF (RFC 5869)** 从一个主密钥派生三个独立密钥: ``` prk = HKDF-Extract(salt = "amber-v3", ikm = master_secret) K_enc = HKDF-Expand(prk, info = "enc", L = 32) K_mac = HKDF-Expand(prk, info = "mac", L = 32) K_sign = HKDF-Expand(prk, info = "sign", L = 32) ``` ### 7.3. 使用 MD5 和自定义的“盐值居中”构造而非 HMAC **问题。** 请求签名使用 MD5,构造为 `MD5(concat[:pos] || secret || concat[pos:])`,而不是标准 HMAC。MD5 在密码学上已死亡:碰撞几乎可以瞬间找到(Wang, Yu, 2004; 选择前缀 Stevens et al., 2007)。 **影响。** 对于这种特定构造,截至本分析时,尚无已知的实际攻击——位置相关的插入有些非标准。但是: - 该构造没有公开的安全证明(与 HMAC 不同)。 - 在 2026 年的新开发中,**为任何**密码学目的使用 MD5 都是不合理的。 - HMAC-SHA256 已在同一个库中实现,并且应该被使用。 **如何修复。** 替换为 `HMAC-SHA256(K_sign, canonical_serialization(params))`。规范化序列化可以是 JCS (RFC 8785) 或简单地按键排序的 `k1=v1&k2=v2&...` 字符串,并进行适当转义。 ### 7.4. 调试回退到字面量 `"release"` 密钥 **问题。** 在地址 `0x16360` 的函数内部,存在类似这样的分支: ``` if (FUN_00012f04() == 0) { // is debug build? secret = "release"; } else { secret = SECRETS[appId]; } ``` 在调试模式下,密钥是字面量 8 字符字符串 `"release"`。意图很明显——便于测试;讽刺的是,该字符串被命名为 `release`。 **影响。** 如果 `FUN_00012f04` 依赖于客户端可检查的标志(例如通过 JNI 读取 `BuildConfig.DEBUG`),则可以通过 Frida/Xposed 在运行时修补,将 SDK 切换到“调试密钥”模式。一旦切换,每个签名都可以被轻易伪造。 **如何修复。** **根本不要**在发布版本中保留调试分支:使用编译时 `#ifdef DEBUG ... #endif`,由预处理器移除,而不是运行时检查。此外——使用 SafetyNet / Play Integrity 进行服务器端构建验证。 release ### 7.5. 动态 JNI 注册作为唯一的反逆向层 **问题。** 没有其他防御的 `RegisterNatives` 会带来虚假的安全感。任何熟悉 JNI 的逆向工程师大约一分钟内就能读取 `JNINativeMethod` 表。 **影响。** 与其说是漏洞,不如说是对优先级的评论:在这方面花了功夫,但没有花在真正重要的事情上(硬编码密钥、MD5)。 **如何修复(如果将其视为硬性要求)。** 严肃的原生代码混淆:控制流平坦化(OLLVM)、字符串加密、反调试、完整性自检、带服务器报告的 root 检测。但这些都不能替代正确的密码学架构——它们只能提高提取成本,而不能使其变得不可能。 ## 8. 验证:解密捕获的流量 解密一个捕获的 `/user/info` 请求可获得正确的明文,并通过 HMAC 检查: ``` _random=359276626&os_ver=7.1.2&firebase_id=k8GAudccnAMzatz23HvA53Wykpfh& language=ru&_sign=8de3d8ed288dc10df72c6ce3c26486ee&pkg=locket.live&vcode=59& network=WIFI&os_vcode=25&referrer=utm_source%3Dgoogle-play%26utm_medium%3Dorganic& uid=Z9E2102B6DA8BE106ACDC77022FE9454D9C&vname=1.5.7&name=PicPat&os_name=Android& model=SM-G965N&lang=_&brand=samsung&_timestamp=1778926157 ``` 两个方向的收敛——自计算的 `_sign` 与捕获的匹配,解密捕获的数据块产生可读的明文并通过 HMAC——解决了重建的正确性问题。 ## 9. 结论与建议 ### 9.1. 优势 - 加密后 MAC 组合是正确的选择:MAC 是在**密文**上计算的,而不是在明文上。 - HMAC、SHA-256、AES 都按教科书实现,使用规范的常量和操作顺序。 - 协议版本控制在架构中内嵌:一个整数打包了 `SIGN/REQUEST/RESPONSE` 版本,并作为 `Security-Controller: v=N` 传递给服务器。 ### 9.2. 弱点(按重要性降序排列) 1. 每次安装中的硬编码共享密钥(§7.1)。 2. 密钥重用——一个密钥用于三种角色(§7.2)。 3. MD5 + 自定义的“盐值居中”构造而非 HMAC(§7.3)。 4. 调试回退到字面量 `"release"` 密钥(§7.4)。 5. 依赖 JNI 注册作为严肃的反逆向措施(§7.5)。 ### 9.3. 摘要建议(针对类似 SDK 的供应商) 1. **不要将共享密钥存储在客户端中。** 任何静态内嵌的密钥都随客户端一起发布。在首次启动时使用非对称密钥交换。 2. **一个密钥,一个角色。** 通过 HKDF 派生子密钥。 3. **使用标准构造。** 用于签名的 HMAC-SHA256。使用 AES-GCM(或 ChaCha20-Poly1305)进行 AEAD,而不是手写的 AES-CBC + HMAC 组合。 4. **签名前进行规范化。** 如果协议需要对结构化数据签名——使用 JCS (RFC 8785) 或具有确定性序列化的 CBOR,而不是自创的“排序并连接”。 5. **发布版本中不要有调试密钥。** 使用 `#ifdef`,而不是运行时标志。 6. **使版本控制严格。** 既然 `Security-Controller: v=N` 已经存在,服务器应该**要求**新客户端使用最新版本,并逐步停止对旧版本的支持;否则,原则上可能存在降级攻击。 7. **不要依赖“通过 JNI 混淆”。** 任何反逆向措施都只是对正确密码学的补充,而不是替代。 ## 10. 附录 ### 附录 A. 构件 | 文件 | 用途 | |---|---| | `frida_getsign_hook.js` | Frida 脚本:钩住 `com.amber.lib.security.NET.getSign` 以收集实时的 (输入, 输出) 对并验证签名算法。 | | `protocol_roundtrip.py` | 完整的协议往返:针对恢复的公式计算 `sign_v2`,`encrypt_v2` / `decrypt_v2`(AES-256-CBC + HMAC-SHA256),以及向 `/user/info` 发送实时请求并解密响应。SDK 密钥未包含在文件中——从 `libamber_security.so` 中地址 `0x19124` 处的数组中提取你 AppId 对应的 32 字节密钥,并在运行前放入 `SECRETS[14]`。 | ### 附录 B. 被分析库的结构 ``` libamber_security.so (armeabi-v7a, Thumb, ELF, ~36 KB) ├── JNI_OnLoad @ 0x1688c │ └── RegisterNatives × 2 for com/amber/lib/security/{AES,NET} ├── NET.encrypt (trampoline 0x17f08 → 0x1627c) ├── NET.decrypt (trampoline 0x17f20 → 0x1627c) ├── NET.getSign @ 0x16360 ├── AES.encrypt (trampoline 0x17efc → ...) ├── AES.decrypt (trampoline 0x17f02 → ...) ├── MD5_Update @ 0x13458 (standard A/B/C/D constants) ├── MD5_Final @ 0x13be8 ├── SHA-256 @ 0x15a4c (standard H0..H7 constants) ├── HMAC-SHA256 @ 0x15cd0 ├── AES-CBC encrypt/decrypt @ 0x15730 (with PKCS#7) ├── AES key expansion @ 0x14c10 ├── Base64 enc/dec @ 0x13244 / 0x13024 └── .rodata: └── 0x19124: array of 17 pointers to 32-byte ASCII secrets ``` ### 附录 C. 恢复算法的伪代码 **SIGN_V2:** ``` def sign_v2(params: dict, secret: str) -> dict: rnd = uniform_int(1_000_000, 999_999_999) pairs = list(params.items()) + [("_random", str(rnd))] pairs.sort(key=lambda kv: kv[0]) concat = "".join(s for kv in pairs for s in kv) pos = rnd % len(concat) salted = concat[:pos] + secret + concat[pos:] return {**params, "_random": str(rnd), "_sign": md5(salted).hexdigest()} ``` **REQUEST_V2 / RESPONSE_V2:** ``` def encrypt_v2(plaintext: bytes, secret: bytes) -> bytes: iv = random_bytes(16) ciphertext = AES_256_CBC(secret, iv).encrypt(pkcs7_pad(plaintext)) mac = HMAC_SHA256(secret, ciphertext) return base64(iv + mac + ciphertext) def decrypt_v2(blob_b64: bytes, secret: bytes) -> bytes: blob = base64_decode(blob_b64) iv, mac, ciphertext = blob[:16], blob[16:48], blob[48:] assert HMAC_SHA256(secret, ciphertext) == mac return pkcs7_unpad(AES_256_CBC(secret, iv).decrypt(ciphertext)) ``` ## 11. 免责声明 以上内容截至 **2026年5月17日** 是准确的,并指的是应用的一个特定版本,`1.5.7` (vcode 59)。SDK 的逻辑和服务器端验证可由供应商随时更改;实验结果和原生库内的地址反映了分析时的状态。 我进行**研究**,分享观察结果,并且不作任何保证——不保证其他版本的可重现性,不保证阅读时任何相关内容的适用性,也不保证协议重建中没有错误。根据本文档得出的任何结论或决定均由读者自行负责。
标签:AES-256, Amber Security SDK, Android安全, GHAS, HMAC-SHA256, Java安全, Native库分析, PicPat, SDK分析, SEO关键词, 云资产清单, 代码安全, 内核驱动, 加密算法, 密码学, 应用协议, 手动系统调用, 漏洞枚举, 漏洞评估, 目录枚举, 移动安全, 签名验证, 网络协议, 网络安全, 认证加密, 设计缺陷, 逆向工具, 逆向工程, 锁屏分享, 隐私保护