抓到包发现请求里有一个
sign字段,修改参数就报错——这就是接口签名保护。本篇教你从零找到并复现任意 App 的签名算法。
一、常见加密场景
| 场景 | 表现 | 目标 |
|---|---|---|
| 请求签名 | 参数中有 sign/signature 字段 | 还原签名算法,伪造请求 |
| 请求体加密 | Body 是一段乱码/Base64 | 解密请求体,理解数据结构 |
| Token 生成 | 登录返回特殊格式的 Token | 理解 Token 构造方式 |
| 时间戳防重放 | 请求带 ts/nonce | 找到时间窗口限制逻辑 |
二、从抓包到定位算法(完整思路)
如图所示,标准路径分三步:抓包观察 → jadx 定位 → Frida 验证。
第一步:抓包,找到可疑字段
用 Burp 抓包,重点关注:
长度为 32 的字段 → MD5
长度为 64 的字段 → SHA-256
看起来像 Base64 的字段 → AES/RSA 加密
eyJ开头的字段 → JWT Token
第二步:jadx 搜索,定位代码
搜索关键词优先级:
1. 字段名字符串:"sign" / "token" / "encrypt"
2. API 路径:"/api/login"
3. Android 加密 API:MessageDigest / SecretKeySpec / Cipher
4. 常见库名:HmacUtils / AESUtil / MD5
第三步:Frida 验证
Hook 到候选函数,打印实际运行时的参数和返回值,确认就是签名函数。
三、常见算法识别特征
| 算法 | 特征 | jadx 搜索关键词 |
|---|---|---|
| MD5 | 输出 32 位十六进制 | MessageDigest.getInstance("MD5") |
| SHA-256 | 输出 64 位十六进制 | MessageDigest.getInstance("SHA-256") |
| HMAC-SHA256 | 需要 key,输出 64 位 | SecretKeySpec + HmacSHA256 |
| AES-CBC | 加密,需要 key + IV | Cipher.getInstance("AES/CBC") |
| AES-ECB | 无 IV,相同输入相同输出 | Cipher.getInstance("AES/ECB") |
| RSA | 非对称加密,输出长 | KeyFactory.getInstance("RSA") |
四、实战:还原 HMAC-SHA256 签名
场景: 抓包发现请求头有 X-Sign: a3f2b9e1...(64位hex)
定位过程:
# jadx 搜索
Ctrl+Shift+F 搜索: "X-Sign"
→ 找到赋值位置: headers.put("X-Sign", SignUtil.sign(body))
→ 进入 SignUtil.sign():
// jadx 还原的代码
publicstaticStringsign(Stringbody) {
Stringtimestamp=String.valueOf(System.currentTimeMillis() /1000);
Stringraw=body+timestamp+"app_secret_key_2024";
Macmac=Mac.getInstance("HmacSHA256");
mac.init(newSecretKeySpec("app_secret_key_2024".getBytes(), "HmacSHA256"));
returnbytesToHex(mac.doFinal(raw.getBytes()));
}
Frida 验证:
Java.perform(function() {
varSignUtil=Java.use("com.example.SignUtil");
SignUtil.sign.implementation=function(body) {
console.log("[Sign] 输入: "+body);
varresult=this.sign(body);
console.log("[Sign] 输出: "+result);
returnresult;
};
});
Python 复现:
importhmac, hashlib, time
defsign(body: str) ->str:
timestamp = str(int(time.time()))
raw = body+timestamp+"app_secret_key_2024"
key = b"app_secret_key_2024"
h = hmac.new(key, raw.encode(), hashlib.sha256)
returnh.hexdigest()
# 构造请求
importrequests
body = '{"user":"test","action":"query"}'
headers = {
"X-Sign": sign(body),
"Content-Type": "application/json"
}
resp = requests.post("https://api.example.com/data",
data=body, headers=headers)
五、实战:破解 AES 加密请求体
场景: 请求体是一段 Base64,解码后是乱码
定位 AES Key:
Java.perform(function() {
// Hook Cipher 初始化,拦截 key 和 IV
varCipher=Java.use("javax.crypto.Cipher");
varSecretKeySpec=Java.use("javax.crypto.spec.SecretKeySpec");
SecretKeySpec.$init.overload("[B", "java.lang.String")
.implementation=function(keyBytes, algorithm) {
varkey=Java.array('byte', keyBytes);
varkeyHex=Array.from(key)
.map(b=> ('0'+ (b&0xFF).toString(16)).slice(-2))
.join('');
console.log("[AES Key] algorithm: "+algorithm);
console.log("[AES Key] key (hex): "+keyHex);
console.log("[AES Key] key (str): "+String.fromCharCode.apply(null, key));
returnthis.$init(keyBytes, algorithm);
};
});
Python 解密:
fromCrypto.CipherimportAES
fromCrypto.Util.Paddingimportunpad
importbase64
key = b"your_16_byte_key"# 从 Frida 拿到的 key
iv = b"your_16_byte_iv_"# 同样从 Frida 获取
defdecrypt(encrypted_b64: str) ->str:
data = base64.b64decode(encrypted_b64)
cipher = AES.new(key, AES.MODE_CBC, iv)
returnunpad(cipher.decrypt(data), 16).decode()
六、动态密钥处理
有些 App 的 key 是运行时动态生成的(时间戳/设备ID混合),无法从静态代码中直接获取。需要从内存中提取:
Java.perform(function() {
// Hook 密钥生成函数
varKeyGenerator=Java.use("javax.crypto.KeyGenerator");
KeyGenerator.generateKey.implementation=function() {
varkey=this.generateKey();
// 将 SecretKey 转为字节数组
varencoded=key.getEncoded();
varhex=Array.from(Java.array('byte', encoded))
.map(b=> ('0'+ (b&0xFF).toString(16)).slice(-2))
.join('');
console.log("[动态Key] "+hex);
returnkey;
};
});
下一篇:Frida 反检测与对抗
夜雨聆风