Qurtimurti/PicPat
GitHub: Qurtimurti/PicPat
该报告逆向分析了 PicPat 应用中 Amber Security SDK 的加密协议,发现了多个安全设计缺陷。
Stars: 0 | Forks: 0
# 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` 是一个通用函数的跳板,带方向标志。标准模式。
### 5.3. 密钥数组
在库 `.rodata` 段的地址 `0x19124` 处,存放着一个包含 17 个 `char*` 指针的数组,每个指针指向一个 32 字节的 ASCII 空字符结尾的字符串。这些字符串是 SDK 中每个加密操作的密钥材料。这些字符串是 `[0x21..0x7E]` 范围内的可打印字节,每个正好 32 字节(256 位——AES-256 密钥长度),没有明显的内部结构;它们看起来像是 `pwgen 32 17` 的输出。
## 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`。上面显示的值是合成的——请参阅顶部的免责声明。
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]` 范围内并进行裁剪。
## 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 进行服务器端构建验证。
### 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关键词, 云资产清单, 代码安全, 内核驱动, 加密算法, 密码学, 应用协议, 手动系统调用, 漏洞枚举, 漏洞评估, 目录枚举, 移动安全, 签名验证, 网络协议, 网络安全, 认证加密, 设计缺陷, 逆向工具, 逆向工程, 锁屏分享, 隐私保护