版权归作者所有,如有转发,请注明文章出处: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 是不一样的。

word/media/image1.png

登录接口调用流程梳理

完整梳理一下登录接口相关参数和调用流程。

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("***********", "******")

调用成功

word/media/image2.png

假如 newSign 算法不对会调用失败,返回结果如下:

word/media/image3.png