1548 words
8 minutes
3.72.0 车智赢登陆api
https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx

image-20250304224756838

搜索url

image-20250304225638325

查找用例

image-20250304225723668

    public static void loginByPassword(String str, String str2, String str3, String str4, String str5, ResponseCallback<UserBean> responseCallback) {
        HttpUtil.Builder builder = new HttpUtil.Builder();
        builder.tag(str).method(HttpUtil.Method.POST).signType(1).url(LOGIN_URL).param("username", str2).param("type", str4).param("signkey", str5).param("pwd", SecurityUtil.encodeMD5(str3));
        doRequest(builder, responseCallback, new TypeToken<BaseResult<UserBean>>() { // from class: com.che168.autotradercloud.user.model.UserModel.5
        }.getType());
    }

pwd#

输入的是123456,一眼能看出来是md5加密,略

def encode_md5_hash(input_string):
    """Encodes the input string to MD5."""
    return hashlib.md5(input_string.encode('utf-8')).hexdigest()

定位#

image-20250305222657279

    public static TreeMap<String, String> getRequestParams(TreeMap<String, String> treeMap) {
        if (!treeMap.containsKey(KEY_APP_ID)) {
            treeMap.put(KEY_APP_ID, Constants.APP_ID);
        }
        if (!treeMap.containsKey("channelid")) {
            treeMap.put("channelid", AppUtils.getChannelId(ContextProvider.getContext()));
        }
        if (!treeMap.containsKey(KEY_APP_VERSION)) {
            treeMap.put(KEY_APP_VERSION, SystemUtil.getAppVersionName(ContextProvider.getContext()));
        }
        if (!treeMap.containsKey("udid")) {
            treeMap.put("udid", AppUtils.getUDID(ContextProvider.getContext()));
        }
        String userKey = UserModel.getUserKey();
        if (!ATCEmptyUtil.isEmpty((CharSequence) userKey)) {
            treeMap.put("userkey", userKey);
        }
        checkNullParams(treeMap);
        treeMap.put("_sign", SignManager.INSTANCE.signByType(0, treeMap));
        return treeMap;
    }

貌似是这个?只是猜测,hook了之后发现点登陆没有输出。


hook map的输出

Java.perform(function () {
    var TreeMap = Java.use('java.util.TreeMap');
    var Map = Java.use("java.util.Map");

    TreeMap.put.implementation = function (key,value) {
        if(key=="_sign" || key == 'udid'){ // 根据需要 看抓包
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            console.log(key + "=" + value);
        }
        var res = this.put(key,value);
        return res;
    }
});
java.lang.Throwable
	at java.util.TreeMap.put(Native Method)
	at com.che168.autotradercloud.launch.model.LaunchModel.lambda$initRequestCommonParams$0(LaunchModel.java:301)
	at com.che168.autotradercloud.launch.model.LaunchModel$$ExternalSyntheticLambda0.checkParams(Unknown Source:0)
	at com.che168.ahnetwork.http.HttpUtil$Builder.checkParams(HttpUtil.java:554)
	at com.che168.ahnetwork.http.HttpUtil$Builder.doRequest(HttpUtil.java:490)
	at com.che168.ahnetwork.http.HttpUtil$Builder.doRequest(HttpUtil.java:428)
	at com.che168.autotradercloud.base.httpNew.BaseModel.doRequest(BaseModel.java:104)
	at com.che168.autotradercloud.user.model.UserModel.loginByPassword(UserModel.java:274)
	at com.che168.autotradercloud.user.model.UserModel.login(UserModel.java:1474)
	at com.che168.autotradercloud.user.LoginActivity.login(LoginActivity.java:167)
	at com.che168.autotradercloud.user.view.LoginView$1.onClick(LoginView.java:150)
	at android.view.View.performClick(View.java:7297)
	at android.view.View.performClickInternal(View.java:7274)
	at android.view.View.access$3600(View.java:819)
	at android.view.View$PerformClick.run(View.java:28023)
	at android.os.Handler.handleCallback(Handler.java:914)
	at android.os.Handler.dispatchMessage(Handler.java:100)
	at android.os.Looper.loop(Looper.java:225)
	at android.app.ActivityThread.main(ActivityThread.java:7564)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)

udid=VyLCedZhA1IAEku0Esi4e2upmkwc8HWxoFJTXxCS+dniXLcWsfLdZmhSWX6p rWgye4Dy6BEz3gYxEX0Qncu+PQ==
java.lang.Throwable
	at java.util.TreeMap.put(Native Method)
	at com.che168.autotradercloud.launch.model.LaunchModel.lambda$initRequestCommonParams$0(LaunchModel.java:311)
	at com.che168.autotradercloud.launch.model.LaunchModel$$ExternalSyntheticLambda0.checkParams(Unknown Source:0)
	at com.che168.ahnetwork.http.HttpUtil$Builder.checkParams(HttpUtil.java:554)
	at com.che168.ahnetwork.http.HttpUtil$Builder.doRequest(HttpUtil.java:490)
	at com.che168.ahnetwork.http.HttpUtil$Builder.doRequest(HttpUtil.java:428)
	at com.che168.autotradercloud.base.httpNew.BaseModel.doRequest(BaseModel.java:104)
	at com.che168.autotradercloud.user.model.UserModel.loginByPassword(UserModel.java:274)
	at com.che168.autotradercloud.user.model.UserModel.login(UserModel.java:1474)
	at com.che168.autotradercloud.user.LoginActivity.login(LoginActivity.java:167)
	at com.che168.autotradercloud.user.view.LoginView$1.onClick(LoginView.java:150)
	at android.view.View.performClick(View.java:7297)
	at android.view.View.performClickInternal(View.java:7274)
	at android.view.View.access$3600(View.java:819)
	at android.view.View$PerformClick.run(View.java:28023)
	at android.os.Handler.handleCallback(Handler.java:914)
	at android.os.Handler.dispatchMessage(Handler.java:100)
	at android.os.Looper.loop(Looper.java:225)
	at android.app.ActivityThread.main(ActivityThread.java:7564)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)

_sign=C97B0C91FAB708871272894449ED99A3

之前找到的是loginByPassword,还没有_sign和udid。观察调用栈,感觉com.che168.autotradercloud.launch.model.LaunchModel.lambda$initRequestCommonParams$0(LaunchModel.java:311)比较可疑

image-20250305224642582

    public static /* synthetic */ TreeMap lambda$initRequestCommonParams$0(int i, TreeMap treeMap) {
        if (!treeMap.containsKey(KEY_APP_ID)) {
            treeMap.put(KEY_APP_ID, Constants.APP_ID);
        }
        if (!treeMap.containsKey("channelid")) {
            treeMap.put("channelid", AppUtils.getChannelId(ContextProvider.getContext()));
        }
        if (!treeMap.containsKey(KEY_APP_VERSION)) {
            treeMap.put(KEY_APP_VERSION, SystemUtil.getAppVersionName(ContextProvider.getContext()));
        }
        if (!treeMap.containsKey("udid")) {
            treeMap.put("udid", AppUtils.getUDID(ContextProvider.getContext()));
        }
        String userKey = UserModel.getUserKey();
        if (!ATCEmptyUtil.isEmpty((CharSequence) userKey)) {
            treeMap.put("userkey", userKey);
        }
        checkNullParams(treeMap);
        String signByType = SignManager.INSTANCE.signByType(i, treeMap);
        if (signByType != null) {
            treeMap.put("_sign", signByType);
        }
        return treeMap;
    }

hook一下AppUtils.getUDIDSignManager.INSTANCE.signByType

Java.perform(function () {
    let AppUtils = Java.use("com.che168.autotradercloud.util.AppUtils");
    AppUtils["getUDID"].implementation = function (context) {
        console.log(`AppUtils.getUDID is called: context=${context}`);
        let result = this["getUDID"](context);
        console.log(`AppUtils.getUDID result=${result}`);
        console.log("---------------------------------------------------")
        return result;
    };
    let SignManager = Java.use("com.che168.atclibrary.base.SignManager");
    SignManager["signByType"].implementation = function (i, paramMap) {
        console.log(`SignManager.signByType is called: i=${i}, paramMap=${paramMap}`);
        let result = this["signByType"](i, paramMap);
        console.log(`SignManager.signByType result=${result}`);
        return result;
    };
});
AppUtils.getUDID is called: context=com.che168.autotradercloud.ATCApplication@d39912d
AppUtils.getUDID result=VyLCedZhA1IAEku0Esi4e2upmkwc8HWxoFJTXxCS+dniXLcWsfLdZlro30Ck NN8m+Ay6omzg93gvOkT8rjjlKA==
---------------------------------------------------
SignManager.signByType is called: i=1, paramMap={_appid=atc.android, appversion=3.72.0, channelid=csy, pwd=e10adc3949ba59abbe56e057f20f883e, signkey=, type=, udid=VyLCedZhA1IAEku0Esi4e2upmkwc8HWxoFJTXxCS+dniXLcWsfLdZlro30Ck NN8m+Ay6omzg93gvOkT8rjjlKA==, username=14725836987}
SignManager.signByType result=CBDD85171AB7F8F0BF9BE4105B1C7A3F

和抓包对比,可以确定是这里加密的

udid#

    public static String getUDID(Context context) {
        return SecurityUtil.encode3Des(context, getIMEI(context) + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + (System.currentTimeMillis() / 1000) + ".000000|" + SPUtils.getDeviceId());
    }

hook encode 3Des

 SecurityUtil.encode3Des is called: context=com.che168.autotradercloud.ATCApplication@d39912d, str=ecff81b9-76cb-3891-8213-d16604b5b14c|1741186664.000000|350572
SecurityUtil.encode3Des result=VyLCedZhA1IAEku0Esi4e2upmkwc8HWxoFJTXxCS+dniXLcWsfLdZnpD/d9f01SFd4aqwQ8e+lsp5Zi0ELe+Pw==

进入encode3Des, hook key

AHAPIHelper.getDesKey result=appapiche168comappapiche168comap
import requests
from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad
import base64
import time
ENCODING = "utf-8"
IV = b'appapich'
def encode(key, data):
    """Encodes the byte array to a base64 string."""
    b64_res = base64.b64encode(data).decode(ENCODING)
    return b64_res[:60] + ' ' + b64_res[60:]

def encode_3des(des_key, str_to_encrypt):
    if not des_key:
        return None
    
    # Ensure the key is 24 bytes long for 3DES
    while len(des_key) < 24:
        des_key += des_key
    des_key = des_key[:24].encode(ENCODING)

    try:
        cipher = DES3.new(des_key, DES3.MODE_CBC, IV)
        padded_data = pad(str_to_encrypt.encode(ENCODING), DES3.block_size)
        encrypted_data = cipher.encrypt(padded_data)
        return encode(des_key, encrypted_data)
    except Exception as e:
        print(f"Encryption error: {e}")
        return None

def get_udid(imei, device_id, key):
    timestamp = int(time.time())
    data = f"{imei}|{timestamp}.000000|{device_id}"
    data = 'ecff81b9-76cb-3891-8213-d16604b5b14c|1741186664.000000|350572'
    return encode_3des(key, data)

if __name__ == '__main__':
    # Example usage
    imei = "ecff81b9-76cb-3891-8213-d16604b5b14c"
    device_id = "350572"
    key = "appapiche168comappapiche168comap"
    udid = get_udid(imei, device_id, key)
    print(udid)

结果与hook的对比,发现是对的

_sign#

public final class SignManager {
    public static final SignManager INSTANCE = new SignManager();
    public static final String KEY_AUTOHOME = "@7U$aPOE@$";
    public static final String KEY_SHARE = "moc.861ehc.relaed.bup.wyfv";
    public static final String KEY_V1 = "com.che168.www";
    public static final String KEY_V2 = "W@oC!AH_6Ew1f6%8";

    private SignManager() {
    }

    public final String signByType(@SignType int i, TreeMap<String, String> paramMap) {
        Intrinsics.checkNotNullParameter(paramMap, "paramMap");
        StringBuilder sb = new StringBuilder();
        String str = KEY_V1;
        if (i != 0) {
            if (i == 1) { // 走这里
                str = KEY_V2;
            } else if (i == 2) {
                str = KEY_SHARE;
            } else if (i == 3) {
                str = KEY_AUTOHOME;
            }
        }
        sb.append(str);
        for (String str2 : paramMap.keySet()) {
            sb.append(str2);
            sb.append(paramMap.get(str2));
        }
        sb.append(str);
        String encodeMD5 = SecurityUtil.encodeMD5(sb.toString()); // hook这里z
        if (encodeMD5 != null) {
            Locale ROOT = Locale.ROOT;
            Intrinsics.checkNotNullExpressionValue(ROOT, "ROOT");
            String upperCase = encodeMD5.toUpperCase(ROOT);
            Intrinsics.checkNotNullExpressionValue(upperCase, "this as java.lang.String).toUpperCase(locale)");
            if (upperCase != null) {
                return upperCase;
            }
        }
        return "";
    }
}

目测是把treemap拿进来进行拼接后md5加密再转大写.

SecurityUtil.encodeMD5 is called: str=W@oC!AH_6Ew1f6%8_appidatc.androidappversion3.72.0channelidcsypwde10adc3949ba59abbe56e057f20f883esignkeytypeudidVyLCedZhA1IAEku0Esi4e2upmkwc8HWxoFJTXxCS+dniXLcWsfLdZmdJ+gG9 BOq/C1gP2rFPrKOT21yJQx82xQ==username14725836987W@oC!AH_6Ew1f6%8
SecurityUtil.encodeMD5 result=3adecdd319a988d90cfc77d3fbef8280

hook的时候将str替换成”123456”看结果有没有魔改md5

promot: convert the provided java method into a python function. a standalone function without creating an object of the SignManager class,

KEY_V2 = "W@oC!AH_6Ew1f6%8"

def sign_by_type(param_map):
    sb = []
    sb.append(KEY_V2)

    # Sort the parameters by key to maintain order
    for key in sorted(param_map.keys()):
        sb.append(key)
        sb.append(param_map[key])

    sb.append(KEY_V2)
    concatenated_string = ''.join(sb)
    print(concatenated_string)
    encode_md5 = encode_md5_hash(concatenated_string)

    if encode_md5 is not None:
        return encode_md5.upper()

    return ""

def encode_md5_hash(input_string):
    """Encodes the input string to MD5."""
    return hashlib.md5(input_string.encode('utf-8')).hexdigest()

# Provided map as a dictionary
param_map = {
    "_appid": "atc.android",
    "appversion": "3.72.0",
    "channelid": "csy",
    "pwd": "e10adc3949ba59abbe56e057f20f883e",
    "signkey": "",
    "type": "",
    "udid": "VyLCedZhA1IAEku0Esi4e2upmkwc8HWxoFJTXxCS+dniXLcWsfLdZlro30Ck NN8m+Ay6omzg93gvOkT8rjjlKA==",
    "username": "14725836987"
}

# Call the function with the provided map
signature = sign_by_type(param_map)
print(signature)

完整代码#

import requests
from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad
import base64
import time
import hashlib

ENCODING = "utf-8"
IV = b'appapich'
KEY_V2 = "W@oC!AH_6Ew1f6%8"

def encode(key, data):
    """Encodes the byte array to a base64 string."""
    b64_res = base64.b64encode(data).decode(ENCODING)
    return b64_res[:60] + ' ' + b64_res[60:]

def encode_3des(des_key, str_to_encrypt):
    # Ensure the key is 24 bytes long for 3DES
    while len(des_key) < 24:
        des_key += des_key
    des_key = des_key[:24].encode(ENCODING)

    try:
        cipher = DES3.new(des_key, DES3.MODE_CBC, IV)
        padded_data = pad(str_to_encrypt.encode(ENCODING), DES3.block_size)
        encrypted_data = cipher.encrypt(padded_data)
        return encode(des_key, encrypted_data)
    except Exception as e:
        print(f"Encryption error: {e}")
        return None

def get_udid(imei, device_id, key):
    timestamp = int(time.time())
    data = f"{imei}|{timestamp}.000000|{device_id}"
    # data = 'ecff81b9-76cb-3891-8213-d16604b5b14c|1741258718.000000|350572'
    return encode_3des(key, data)

def sign_by_type(param_map):
    sb = []
    sb.append(KEY_V2)
    # Sort the parameters by key to maintain order
    for key in sorted(param_map.keys()):
        sb.append(key)
        sb.append(param_map[key])

    sb.append(KEY_V2)
    concatenated_string = ''.join(sb)
    # print(concatenated_string)
    encode_md5 = encode_md5_hash(concatenated_string)
    if encode_md5 is not None:
        return encode_md5.upper()
    return ""

def encode_md5_hash(input_string):
    """Encodes the input string to MD5."""
    return hashlib.md5(input_string.encode('utf-8')).hexdigest()


if __name__ == '__main__':
    # Example usage
    imei = "ecff81b9-76cb-3891-8213-d16604b5b14c"
    device_id = "350572"
    key = "appapiche168comappapiche168comap"
    udid = get_udid(imei, device_id, key)
    # print(udid)
    pwd = "123456"
    username = '14725836900'
    # Provided map as a dictionary
    param_map = {
        "_appid": "atc.android",
        "appversion": "3.72.0",
        "channelid": "csy",
        "pwd": encode_md5_hash(pwd),
        "signkey": "",
        "type": "",
        "udid": udid, # 空格可能要用%20代替。但经过测试,不用 :P
        "username": username
    }
    # Call the function with the provided map
    signature = sign_by_type(param_map)
    # print(signature)
    url = 'http://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx'
    # 请求头
    headers = {
        'Cache-Control': 'public, max-age=0',
        'traceid': 'atc.android_9f48f6a9-abc3-472c-a6ab-46435895ee3c',
        'Content-Type': 'application/x-www-form-urlencoded',
        'Host': 'dealercloudapi.che168.com',
        'Connection': 'Keep-Alive',
        'Accept-Encoding': 'gzip',
        'User-Agent': 'okhttp/3.14.9'
    }
    param_map["_sign"] = signature
    response = requests.post(url, headers=headers, data=param_map)
    print(f"Response Content: {response.text}")

🐛 补充#

image-20250306193443576

udid - getIMEI#

image-20220811140636534

import uuid
imei = str(uuid.uuid4())

nanoTime#

nano_time = random.randint(5136066335773, 7136066335773)  # 开机时间

SPUtils.getDeviceId()#

如果逆向过程中,获取XML数据,谨慎可能是会先注册设备。

image-20220811155255516

发现只去XML文件中读取,那么一定会有地方先在XML中设置,此处才能读取到。

猜想:

  • 程序启动时,通过算法或发送请求,获取数据写入到XML文件中。

    概率较大,很多APP都是咋启动时
    
  • 项目启动时,通过算法或发送请求,获取数据写入到XML文件中。

    有可能,但是概率不大。
    如果是这种情况的话,此处的方法一般会定义为:先去XML文件中获取,如果没有则用算法去生成,然后再讲生成的数据写入XML,便于后续获取。
    

逆向方法:需要先清空数据

  1. 查找用例
  2. hook调用栈

3des#

pip install pycryptodome
import base64
from Crypto.Cipher import DES3

BS = 8
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)

# 3DES的MODE_CBC模式下只有前24位有意义
key = b'appapiche168comappapiche168comap'[0:24]
iv = b'appapich'

plaintext = pad("xxxxxxxxx").encode("utf-8")

# 使用MODE_CBC创建cipher
cipher = DES3.new(key, DES3.MODE_CBC, iv)
result = cipher.encrypt(plaintext)
res = base64.b64encode(result)
print(res)
3.72.0 车智赢登陆api
https://zycreverse.netlify.app/posts/app-reverse/che168/
Author
会写点代码的本子画手
Published at
2025-03-05