乐于分享
好东西不私藏

安卓逆向系列 08:加密算法逆向——登录签名与 Token 还原

安卓逆向系列 08:加密算法逆向——登录签名与 Token 还原

抓到包发现请求里有一个 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 + IVCipher.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 复现:

importhmachashlibtime

defsign(bodystr->str:
timestamp = str(int(time.time()))
raw = body+timestamp+"app_secret_key_2024"
key = b"app_secret_key_2024"
h = hmac.new(keyraw.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=bodyheaders=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(keyBytesalgorithm) {
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(nullkey));
returnthis.$init(keyBytesalgorithm);
    };
});

Python 解密:

fromCrypto.CipherimportAES
fromCrypto.Util.Paddingimportunpad
importbase64

key = b"your_16_byte_key"# 从 Frida 拿到的 key
iv  = b"your_16_byte_iv_"# 同样从 Frida 获取

defdecrypt(encrypted_b64str->str:
data = base64.b64decode(encrypted_b64)
cipher = AES.new(keyAES.MODE_CBCiv)
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 反检测与对抗