逆向某物 App 登录接口:热修复逻辑挖掘隐藏参数、接口完整调用
版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/
uuid 参数分析
通过反编译 ff.l0.c 方法可以知道 uuid 来自于 he.a.i.t() 方法
map.put("uuid", he.a.i.t());
具体参考:
查找一下 he.a.i 在哪些 dex 有引用
2|wayne:/data/data/com.shizhuang.duapp/cyrus # grep -rl "he.a.i" *.txt
11994176_class_list.txt
11994176_class_list_execute.txt
1321896_class_list_execute.txt
1571616_class_list.txt
1571616_class_list_execute.txt
8183732_class_list.txt
8183732_class_list_execute.txt
8391604_class_list_execute.txt
9085048_class_list.txt
9085048_class_list_execute.txt
通过 jadx 反编译 he.a 源码如下:
package he;
/* loaded from: 11994176_dex_file_execute.jar:he/a.class */
public class a {
public static i i = new C0253a();
/* renamed from: he.a$a, reason: collision with other inner class name */
/* loaded from: 11994176_dex_file_execute.jar:he/a$a.class */
public class C0253a extends i {
}
/* loaded from: 11994176_dex_file_execute.jar:he/a$i.class */
public static abstract class i {
public static ChangeQuickRedirect changeQuickRedirect;
...
public String t() {
PatchProxyResult proxy = PatchProxy.proxy(new Object[0], this, changeQuickRedirect, false, 8474, new Class[0], String.class);
return proxy.isSupported ? (String) proxy.result : "";
}
...
}
}
he.a.i 是一个静态变量,类型 he.a$i ,默认值是 he.a.C0253a 实例。
但 C0253a 中并没有 t() 方法,而且其父类 he.a$i 中的 t() 只是调用了 Robust 的 patch 逻辑,默认返回空字符串。
/* loaded from: 11994176_dex_file_execute.jar:he/a$i.class */
public static abstract class i {
public static ChangeQuickRedirect changeQuickRedirect;
public String t() {
PatchProxyResult proxy = PatchProxy.proxy(new Object[0], this, changeQuickRedirect, false, 8474, new Class[0], String.class);
return proxy.isSupported ? (String) proxy.result : "";
}
}
所以 he.a.C0253a 应该不是运行时的 he.a$i 实现类,通过 frida 打印一下 he.a 静态变量 i 的实际类型
function uuid() {
Java.perform(function () {
const a = Java.use("he.a"); // 外部类 a
// 获取静态字段 i 的实例
const instance = a.i.value;
// 打印 instance 的类信息
const instanceClass = instance.getClass();
const className = instanceClass.getName();
console.log("[*] he.a.i 实际类型: " + className);
// 打印父类名
const superClass = instanceClass.getSuperclass();
console.log("[*] 继承自: " + superClass.getName());
// 调用 t() 方法
console.log("[*] 调用 he.a.i.t()...");
const result = instance.t();
console.log("[+] t() 返回值: " + result);
});
}
setImmediate(function () {
uuid();
});
// frida -H 127.0.0.1:1234 -F -l uuid.js
找到 he.a.i 实际类型是 com.shizhuang.duapp.common.base.delegate.tasks.net.a$d
[*] he.a.i 实际类型: com.shizhuang.duapp.common.base.delegate.tasks.net.a$d
[*] 继承自: he.a$i
[*] 调用 he.a.i.t()...
[+] t() 返回值: ****************
com.shizhuang.duapp.common.base.delegate.tasks.net.a$d 的 t() 源码如下:
@Override // he.a.i
public String t() {
PatchProxyResult proxy = PatchProxy.proxy(new Object[0], this, changeQuickRedirect, false, 1546, new Class[0], String.class);
return proxy.isSupported ? (String) proxy.result : e0.d(this.a).c(null);
}
t() 中调用了 yc.e0.c(null) 方法,最后实际调用的是 yc.e0 的 a() 返回 Android ID 。
@Deprecated
@SuppressLint({"HardwareIds"})
public String a() {
return PrivacyApiAsm.getSecureString(this.a.getContentResolver(), "android_id");
}
@Deprecated
@SuppressLint({"CheckResult", "MissingPermission", "HardwareIds"})
public String c(Activity activity) {
String str = this.b;
if (str != null) {
return str;
}
String a = a();
this.b = a;
return a;
}
通过 Hook yc.e0 可以看到 c 方法返回值就是 uuid
================= HOOK START =================
🎯 Class: yc.e0
🔧 Method: b
📥 Arguments:
[0]: com.shizhuang.duapp.modules.app.DuApplication@bd275e0
📤 Return value: 5.43.0
================== HOOK END ==================
================= HOOK START =================
🎯 Class: yc.e0
🔧 Method: c
📥 Arguments:
[0]: null
📤 Return value: ****************
================== HOOK END ==================
Android ID
Android ID 是系统为每台设备分配的、用于标识用户或设备的唯一字符串标识符,App 常用它来做用户追踪、唯一性识别或数据绑定。
Android 8.0(API 26)及以上:Android ID 与 App-Signature 绑定
不同签名的 App → 得到不同的 Android ID;
相同签名 + 相同 userId(sharedUserId) → 得到相同的 ID;
目的是防止 App 之间通过共享 Android ID 实现用户跟踪。
通过 Kotlin 代码获取 Android ID
@SuppressLint("HardwareIds")
private fun getAndroidId(): String {
return Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "Unavailable"
}
输出如下,可以看到和 某物APP 中获取到的 Android ID 是不一样的。
登录接口调用流程梳理
完整梳理一下登录接口相关参数和调用流程。
1. 接口抓包分析
通过抓包分析得到登录接口请求参数和响应如下:
关于抓包APP数据参考:安卓抓包实战:使用 Charles 抓取 App 数据全流程详解
请求参数:
{
"cipherParam": "userName",
"countryCode": 86,
"loginToken": "",
"newSign": "51f80ac693**********965a578fbc",
"password": "61f209b789**********6ad80b3a00",
"platform": "android",
"timestamp": "1750573190328",
"type": "pwd",
"userName": "d728cf5cbc**********87465d0370_1",
"v": "5.43.0"
}
返回数据:
{
"code": 704,
"data": "j9IR20d1kYqCYefwcS5j-fhYlbL-AOFt2uPB2UFE4SYfsjWi3K-Wyv******************************************************NdD-3-E2DFOGbY7JqyblKMX67D66Q2csIgqXVueFO3dQz-qLKJaQ==",
"status": 704
}
除了 userName、password、newSign、timestamp 其他都是固定参数
2. userName
使用 Python 还原 userName 加密算法
def encrypt_username(username):
return aes_ecb_encrypt(username, "****************").hex() + "_1"
3. password
使用 Python 还原 password 加密算法
def encrypt_password(password):
return md5_hash(password + "du")
4. timestamp
使用 Python 获取 timestamp
def get_current_timestamp() -> str:
"""
获取当前时间的 13 位毫秒级时间戳,并返回为字符串格式。
返回:
字符串形式的时间戳,例如 "1717085035150"
"""
timestamp_ms = int(time.time() * 1000)
return str(timestamp_ms)
5. newSign
ff.l0.c 方法接受请求参数并计算返回 newSIgn:
public static String c(Map<String, Object> map, long j, String str) {
synchronized (l0.class) {
try {
if (map == null) {
return "";
}
map.put("uuid", he.a.i.t());
map.put("platform", "android");
map.put(NotifyType.VIBRATE, he.a.i.b());
if (str == null) {
str = "";
}
map.put("loginToken", str);
map.put("timestamp", String.valueOf(j));
String i = i(map);
he.a.m.d(TAG, "StringToSign-body use Gson " + i);
String doWork = DuHelper.doWork(he.a.h, i);
map.remove("uuid");
return h(doWork);
} finally {
}
}
}
调用关系大概如下:
f.l0.c(...) → f.l0.i(map) → 拼接参数,按字母顺序排序
↓
DuHelper.doWork(he.a.h, i) → AES加密 + Base64编码
↓
h(...) → MD5加密
使用 Frida Hook ff.l0 中所有方法,点击登录后,日志输出如下:
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: i
📥 Arguments:
[0] Map content:
cipherParam => userName
countryCode => 86
loginToken =>
password => 61f209b789**********6ad80b3a00
platform => android
timestamp => 1750573190328
type => pwd
userName => d728cf5cbc**********87465d0370_1
uuid => ****************
v => 5.43.0
📤 Return value: cipherParamuserNamecountryCode86loginTokenpassword61f209b7895b6***************************************************************serNamed728cf5cbc788d13c1c9dc87465d0370_1uuid****************v5.43.0
================== HOOK END ==================
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: h
📥 Arguments:
[0]: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/A+KLRtWKw+AfFWotfKtx+5J+ONciO*********************************************************************************************SiRBWN9gZ49Jm7xeVpvA9lyQUJy+QoXOOZ9rtzwikaJnoJb/LNoSdn31WmSGP8gdVL0CuAKDFH2Xj9Krb/0jNsPgNnA==
📤 Return value: 51f80ac693**********965a578fbc
================== HOOK END ==================
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: c
📥 Arguments:
[0] Map content:
cipherParam => userName
countryCode => 86
password => 61f209b789**********6ad80b3a00
type => pwd
userName => d728cf5cbc**********87465d0370_1
[1]: 1750573190328
[2]:
📤 Return value: 51f80ac693**********965a578fbc
================== HOOK END ==================
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: g
📥 Arguments:
[0]: nd.l@8a5df89
[1] Map content:
cipherParam => userName
countryCode => 86
loginToken =>
newSign => 51f80ac693**********965a578fbc
password => 61f209b789**********6ad80b3a00
platform => android
timestamp => 1750573190328
type => pwd
userName => d728cf5cbc**********87465d0370_1
v => 5.43.0
[2]: 1750573190328
[3]: false
📤 Return value: null
================== HOOK END ==================
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: a
📥 Arguments:
[0]: [object Object]
📤 Return value: 6C7D61656D61352E6C7D7D7D616C353C6B3B69316A3B3039303A306E6A3E3B2E6769616C35
================== HOOK END ==================
================= HOOK START =================
🎯 Class: ff.l0
🔧 Method: d
📥 Arguments:
[0]:
[1]: ****************
[2]:
📤 Return value: 6C7D61656D61352E6C7D7D7D616C353C6B3B69316A3B3039303A306E6A3E3B2E6769616C35
================== HOOK END ==================
使用 Python 还原参数拼接规则
# 字符串拼接(按 key 字母顺序)
def build_concat_string(data: dict) -> str:
# 临时添加 uuid
data["uuid"] = get_uuid()
# 按 key 字母顺序拼接 key + value
text = ''.join(f"{k}{data[k]}" for k in sorted(data.keys()))
# 移除临时字段 uuid,保持原 data 不变
data.pop("uuid", None)
return text
使用 Python 计算 newSign
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import hashlib
def aes_ecb_encrypt(plaintext: str, key: str) -> bytes:
key_bytes = key.encode('utf-8')
data_bytes = pad(plaintext.encode('utf-8'), AES.block_size) # PKCS7 padding
cipher = AES.new(key_bytes, AES.MODE_ECB)
encrypted = cipher.encrypt(data_bytes)
print(f"[AES] 原文: {plaintext}")
print(f"[AES] 密钥: {key}")
print(f"[AES] 加密结果(Hex): {encrypted.hex()}")
return encrypted
def base64_encode(data: bytes) -> str:
encoded = base64.b64encode(data).decode('utf-8')
print(f"[Base64] 编码结果: {encoded}")
return encoded
def md5_hash(data: str) -> str:
md5_result = hashlib.md5(data.encode('utf-8')).hexdigest()
print(f"[MD5] Hash 结果: {md5_result}")
return md5_result
def newSign(text: str, key: str) -> str:
print("\n======= newSign 开始 =======")
encrypted = aes_ecb_encrypt(text, key)
b64 = base64_encode(encrypted)
md5_result = md5_hash(b64)
print("======= newSign 结束 =======\n")
return md5_result
newSign算法的完整逆向过程参考:逆向某物 App 登录接口:还原 newSign 算法全流程
完整调用登录接口
使用 Python 封装一个 login 方法调用登录接口
import json
import time
import requests
from newSign import newSign, aes_ecb_encrypt, md5_hash
def encrypt_username(username):
return aes_ecb_encrypt(username, "****************").hex() + "_1"
def encrypt_password(password):
return md5_hash(password + "du")
def get_current_timestamp() -> str:
"""
获取当前时间的 13 位毫秒级时间戳,并返回为字符串格式。
返回:
字符串形式的时间戳,例如 "1717085035150"
"""
timestamp_ms = int(time.time() * 1000)
return str(timestamp_ms)
def get_uuid():
return "****************"
# 字符串拼接(按 key 字母顺序)
def build_concat_string(data: dict) -> str:
# 临时添加 uuid
data["uuid"] = get_uuid()
# 按 key 字母顺序拼接 key + value
text = ''.join(f"{k}{data[k]}" for k in sorted(data.keys()))
# 移除临时字段 uuid,保持原 data 不变
data.pop("uuid", None)
return text
def login(username, password):
username = encrypt_username(username)
password = encrypt_password(password)
timestamp = get_current_timestamp()
headers = {
*****: ********************************************
***********: ************
******: ****************
*************: ***
*******: ************ ******* ******* *** ** ** ********************** *** ****************** ******* **** ****** *********** ********************* ****** ****************************************
************: **********
*******: ********
***********: *********
*************: ***
*****: *********
**************: ***
***************: ********
***************: *********
***********: **********
**********: *****************************************************************
************: ***************************
**************: ******* **************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
********: ****
*****: ****
*********: ****
****: *********************************************************************************************
*****: *****************************************************************************
*****: ****
*****: *********
*****: ************************
*****: *******************************
**************: ****************** ***************
******: **************
}
cookies = {
**************: ***************
}
url = "https://************/api/v1/app/user_core/users/unionLogin"
data = {
*************: ***********
*************: ***
************: ***
**********: *********
**********: **********
***********: **********
******: ******
**********: *********
***: ********
}
# ***************************************************************************************************************************************************************************************************
# 拼接参数
text = build_concat_string(data)
print("[*] 拼接参数:", text)
# 生成签名
data["newSign"] = newSign(text, "****************")
# key 按字母顺序排列
data = dict(sorted(data.items()))
print("[+] 最终 data:", data)
data = json.dumps(data, separators=(',', ':'))
response = requests.post(url, headers=headers, cookies=cookies, data=data)
print(response.text)
print(response)
输入用户名和密码调用 login
# 示例调用
if __name__ == "__main__":
login("***********", "******")
调用成功
假如 newSign 算法不对会调用失败,返回结果如下: