乐于分享
好东西不私藏

Ai人柱力-某壳移动端分析

Ai人柱力-某壳移动端分析

介绍

花销

过程

调用链

OkHttp 框架    │ 发起请求前触发拦截器    ▼OkHttpSignInterceptor.intercept()   ← Java native 方法声明    │ JVM 通过 JNI 分发    ▼libkmsec.so: sub_170F0(JNIEnv* env, jobject, jstring canonical, jstring accessKeyId, jstring timestamp)    │                                                    ↑    │  native 持有 JNIEnv*,可以随时"反向"调用 Java      │    │                                                    │    ├─► env->FindClass("com/ke/securitylib/SecManager") ─┘    ├─► env->GetStaticMethodID("getCertificateMD5Digest")    ├─► env->CallStaticObjectMethod(...)   ← 调回 Java 执行 getCertificateMD5Digest()    │       │    │       └─► libkmsec.so 里另一个 native 函数(getCertificateMD5Digest 的实现)    │               └─► 返回 APK 证书 MD5 字符串    │    ├─► sprintf("%s%s%s%s", accessKeyId, salt, certMD5, timestamp)    ├─► env->FindClass("com/.../MD5Utils")    ├─► env->CallStaticObjectMethod("getMd5", ...)   ← 再次调回 Java    │       └─► Java MD5Utils.getMd5() 执行,返回 HMAC key    │    ├─► 用 key 做 HMAC-SHA256(canonical)  ← 纯 C,不回 Java    │    └─► 构造 String[] 返回给 Java 层,拦截器填入请求头

人柱力

接下俩总结下作为人柱力的我,提供了哪些信息

java层入口

com.ke.infrastructure.app.signature.addon.OkHttpSignInterceptor  └── intercept()  →  native (libkmsec.so)com.ke.securitylib.SecManager  └── sign(String canonical, String accessKeyId, String timestamp) → String[]  native  └── getCertificateMD5Digest() → String  native

所有的so文件

这让 AI 能做静态字符串扫描,找到:

  • uioefhli98476843(关键 salt,后来证明是 key 推导的核心)
  • getCertificateMD5Digest%s%s%s%sMD5Utils(提示了 key 的构造方式)
  • libzxprotect.so 里的 HmacSHA256SecretKeySpecsignatureNonce

运行Ai给的各种frida-hook

[sign] hex=F3E37E060A4F89197B4E2D0A055AECDDCF6473D3F91696D9A13740A3E1954DB5[sign] canonical=accessKeyId=sjoe98HI099dhdD7&appinfo-s=...(完整字符串)[WLL-KGSA] LJAPPVA ...; signature=8+N+BgpPiRl7Ti0KBVrs3c9kc9P5FpbZoTdAo+GVTbU=

这三条数据让 AI 能够:

  • 确认 base64(bytes.fromhex(hex)) == signature(签名格式)
  • 确认 canonical 字段构成和排序规则
  • 有了可以验证 key 的"答案"

关键性突破

在ai穷举各种key都验证失败后,从ai输出日志getCertificateMD5Digest找到可以位置sprintf((char *)v85, "%s%s%s%s", s1, "uioefhli98476843", v80, v71);AI 立刻要求你提供该字符串的引用函数的完整反编译代码。

第五步:提供 IDA 完整反编译函数

贴出了 sub_170F0 的完整 IDA 伪代码(数百行,LLVM 控制流平坦化混淆的状态机)。

AI 从中解读出:

// 核心行sprintf((char *)v85, "%s%s%s%s",    s1,                  // GetStringUTFChars(a4) = accessKeyId"uioefhli98476843",  // 硬编码 salt    v80,                 // getCertificateMD5Digest() 返回值    v71);                // GetStringUTFChars(a5) = timestampv76 = CallStaticMethod(MD5Utils, "getMd5", v85);  // HMAC keyptr = GetStringUTFChars(v76);

通过 JNI 虚表偏移量解码(offset 48 = FindClass,offset 904 = GetStaticMethodID 等),AI 完整还原了 key 推导公式:

HMAC_key = MD5(accessKeyId + "uioefhli98476843" + getCertificateMD5Digest() + timestamp).upper()

具体流程

Canonical String 构造

将以下所有字段按 key 字母序升序 排列,以 key=value 形式用 & 连接:

字段
来源
accessKeyId
固定值 sjoe98HI099dhdD7
host
请求域名,如 apps.api.ke.com
method
HTTP 方法,GET / POST
nonce
32 位随机字符串(字符集见下)
path
请求路径,如 /api/proxy/...
query
完整 query string(原始,不额外编码)
signedHeaders
固定值 Device-id-s,AppInfo-s,User-Agent,Hardware-s,Channel-s,SystemInfo-s
timestamp
Unix 时间戳(秒,字符串)
device-id-s
Device-id-s 头的值
appinfo-s
AppInfo-s 头的值
user-agent
User-Agent 头的值
hardware-s
Hardware-s 头的值
channel-s
Channel-s 头的值
systeminfo-s
SystemInfo-s 头的值

Nonce 字符集:

ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678

(排除了易混淆字符:I、L、O、0、1、g、l、o、u、v、9)

示例 Canonical:

accessKeyId=sjoe98HI099dhdD7&appinfo-s=Beike;3.03.31;3033100&channel-s=Android_ke_wandoujia&device-id-s=...&hardware-s=OnePlus;ONEPLUS A6000&host=apps.api.ke.com&method=GET&nonce=1oGlsi5W...&path=/api/proxy/...&query=cityId=310000&page=1&signedHeaders=Device-id-s,...&systeminfo-s=android;11&timestamp=1775716377&user-agent=Beike3.03.31;...

HMAC Key 推导

step 1: key_input = accessKeyId + "uioefhli98476843" + getCertificateMD5Digest() + timestampstep 2: hmac_key = MD5(key_input).upper()   // 32 字符大写 Hex 字符串

关键常量:

常量
固定 Salt
uioefhli98476843
getCertificateMD5Digest()
APK 签名证书的 MD5,本版本为 28B22DAC5AAD6BFBDBFD2DBB4F40E878

getCertificateMD5Digest() 是 SecManager 的 native 方法,返回 APK 签名证书的 MD5 摘要(32 位大写 Hex)。换包或重签后此值会变。

IDA 关键代码(混淆后伪代码片段):

// sub_170F0 = SecManager.sign() native 实现sprintf(v85, "%s%s%s%s", s1,               // accessKeyId (a4 参数)"uioefhli98476843"// 硬编码 salt                          v80,               // getCertificateMD5Digest() 返回值                          v71);              // timestamp (a5 参数)// v74 = FindClass("com/ke/infrastructure/app/signature/util/MD5Utils")// v57 = GetStaticMethodID("getMd5", "(Ljava/lang/String;)Ljava/lang/String;")v76 = CallStaticMethod(v74, v57, v85);      // MD5Utils.getMd5(v85)ptr = GetStringUTFChars(v76);               // ptr = HMAC key (32字符大写)

签名计算

signature_bytes = HMAC-SHA256(key=hmac_key.encode('utf-8'),                              msg=canonical.encode('utf-8'))signature = Base64(signature_bytes)

sign() native 方法返回 String[]result[0] 为 HMAC 结果的大写 Hex,拦截器再将其转为 Base64 填入 WLL-KGSA

验证

使用 Frida 捕获的真实请求数据验证:

字段
timestamp
1775716377
nonce
1oGlsi5WaJv3ZlwhRammzb8pw8xmsuS0
计算出的 hmac_key
12F6CC24101CE761A5F0D64CB3F32A4E
计算出的 signature
8+N+BgpPiRl7Ti0KBVrs3c9kc9P5FpbZoTdAo+GVTbU=
App 实际输出 signature
8+N+BgpPiRl7Ti0KBVrs3c9kc9P5FpbZoTdAo+GVTbU=
结果完全一致

广子

有需要用于爬虫、逆向、抓包的测试机又不想花时间折腾Root、刷机、环境配置可以来看下下面联系方式,或闲鱼链接