白乐天

道阻且长,行则将至。

车智赢app登录参数逆向

App信息

包名:com.che168.autotradercloud

抓包分析

输入手机号和密码进行登录(1888888888,12345678),出现账号和密码错误

分析数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
url: https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx
method: POST
headers: {
":authority": "dealercloudapi.che168.com",
":method": "POST",
":path": "/tradercloud/sealed/login/login.ashx",
":scheme": "https",
"cache-control": "public, max-age=0",
"traceid": "atc.android_75ca194d-982a-4c62-9c0a-eb647fcfff77",
"content-type": "application/x-www-form-urlencoded",
"content-length": "275",
"accept-encoding": "gzip",
"user-agent": "okhttp/3.14.9"
}
body: {
"_appid": "atc.android",
"_sign": "821E3885B2FC914FBFAD687399B650E7",
"appversion": "3.71.0",
"channelid": "csy",
"pwd": "25d55ad283aa400af464c76d713c07ad",
"signkey": "",
"type": "",
"udid": "aYU5PJ/U/aR1iOFu+Rd2HB7mst8DN5YX+4ft9CvXkL/YCpKYH4gg9+MiTNly tCkk7g9hVsWqMGbruaIqoS6K5A==",
"username": "18888888888"
}

这里面需要逆向的参数有_signpwdudid

jadx反编译分析

pwd参数还原

搜索接口

双击跳转

它是一个字段,右键字段名查找用例,然后跳转至调用的地方,如下,它是url方法的参数

loginByPassword()

1
2
3
4
5
6
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,并进行了MD5哈希算法签名

可以把输入的密码进行MD5签名,与抓包的数据进行对比,如下,是一样的

_sign参数还原

搜索"_sign"

当不确定是哪一个的时候,可以点进去看看上下文有没有其他相关的参数,或者可疑性比较大的方法进行hook,打印参数和返回值。

发现这几个sign的值都是通过toSign()方法得到的

toSign()

这里面对map中的内容通过字符串进行拼接,然后进行MD5签名,然后转为大写

可以通过hook toSign()方法查看参数和返回值

1
2
3
4
5
6
7
8
9
10
11
function hook_toSign(){
Java.perform(function(){
let AHAPIHelper = Java.use("com.autohome.ahkit.AHAPIHelper");
AHAPIHelper["toSign"].implementation = function (context, map) {
console.log(`AHAPIHelper.toSign is called: context=${context}, map=${map}`);
let result = this["toSign"](context, map);
console.log(`AHAPIHelper.toSign result=${result}`);
return result;
};
})
}

hook 之后点击登录没反应,可能是找错函数了,加密并没有走toSign()函数

接着找,看这几个函数

如下,这里的参数跟请求体中的参数也对得上

_sign的值是signByType,它的值是signByType()方法返回的

signByType()

这个方法根据signType的值来设置str的值,然后拼接paraMap的键和值,最后在拼接一个str,然后把字符串进行MD5签名

hook signByType()

1
2
3
4
5
6
7
8
9
10
11
function hook_sign(){
Java.perform(function(){
let SignManager = Java.use("com.che168.atclibrary.base.SignManager");
SignManager["signByType"].implementation = function (signType, paramMap) {
console.log(`SignManager.signByType is called: signType=${signType}, paramMap=${paramMap}`);
let result = this["signByType"](signType, paramMap);
console.log(`SignManager.signByType result=${result}`);
return result;
};
})
}

hook结果

1
2
SignManager.signByType is called: signType=1, paramMap={_appid=atc.android, appversion=3.71.0, channelid=csy, pwd=25d55ad283aa400af464c76d713c07ad, signkey=, type=, udid=aYU5PJ/U/aR1iOFu+Rd2HB7mst8DN5YX+4ft9CvXkL/YCpKYH4gg968SLlht BKxj7FEpfjd4+7OOqUly74+vGQ==, username=18888888888}
SignManager.signByType result=21DD339870BDF9DAE15131F6D01B9BB2

signTpe的值是1,在源码中找到对应的str"W@oC!AH_6Ew1f6%8"

hook encodeMD5()

可以hook encodeMD5查看MD5签名的参数

1
2
3
4
5
6
7
8
9
10
11
function hook_encodeMD5(){
Java.perform(function(){
let SecurityUtil = Java.use("com.autohome.ahkit.utils.SecurityUtil");
SecurityUtil["encodeMD5"].implementation = function (str) {
console.log(`SecurityUtil.encodeMD5 is called: str=${str}`);
let result = this["encodeMD5"](str);
console.log(`SecurityUtil.encodeMD5 result=${result}`);
return result;
};
})
}

hook 结果

1
2
SecurityUtil.encodeMD5 is called: str=W@oC!AH_6Ew1f6%8_appidatc.androidappversion3.71.0channelidcsypwd25d55ad283aa400af464c76d713c07adsignkeytypeudidaYU5PJ/U/aR1iOFu+Rd2HB7mst8DN5YX+4ft9CvXkL/YCpKYH4gg9/rOGiHZ fplHNBBIgv34WQSSYDABqqyELA==username18888888888W@oC!AH_6Ew1f6%8
SecurityUtil.encodeMD5 result=d0bf664cc12b402592b7bbf926087bcc

udid参数还原

udid是通过getUDID()方法获取的

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

可以看出udid是通过3Des加密得到的

分析3Des加密的明文,它是由getIMEI(context) + HiAnalyticsConstant.REPORT_VAL_SEPARATOR + (System.currentTimeMillis() / 1000) + ".000000|" + SPUtils.getDeviceId()拼接而成的

getIMEI()

首先分析getIMEI(),我在jadx里反编译不了这个函数,换用Jeb进行反编译

分析代码,生成IMEI有不同的方式

1
2
3
4
5
6
7
1.((TelephonyManager)context0.getSystemService("phone")).getDeviceId();
返回DeviceId
2.AppUtils.getIMEIbyAndroidIDandUUID(context0);
返回uuid
3.((WifiManager)context0.getSystemService("wifi")).getConnectionInfo().getMacAddress();
UUID.nameUUIDFromBytes(s1.getBytes("utf8")).toString();
先获取wifi mac地址,然后根据这个mac地址生成一个uuid,然后返回

这里面最简单的方式就是直接生成uuid的方式了。

getIMEIbyAndroidIDandUUID()

直接随机生成一个uuid就可以了。

REPORT_VAL_SEPARATOR

REPORT_VAL_SEPARATOR = "|"

getDeviceId()

它是从内存中获取的,需要找到这个值是什么时候如何存进去的

saveDeviceId()

这个方法里面传入了一个字符串,通过查找用例来找传入的字符串是什么

regDevice()

向服务器发送请求返回一个RegDeviceResult对象,然后RegDeviceResult.deviceid字段作为saveDeviceId()的参数

deviceid是从服务器返回的,而且是程序第一次启动的时候设置的。

请求的url是/tradercloud/v100/push/regdevice.ashx

可以通过抓包获取deviceid的值。

返回的deviceid值是436586

Python生成加密参数

1
2
3
4
5
6
7
8
9
10
11
12
import uuid
import time
def g_imei():
u4 = uuid.uuid4()
return str(u4)

def g_data():
REPORT_VAL_SEPARATOR = "|"
deviceid = "436586"
return g_imei()+REPORT_VAL_SEPARATOR+str(int(time.time()))+".000000|"+deviceid

print(g_data())

encode3Des()

获取key

getDesKey()

getSignKey

getSignDesKey()

get3desKey()

get3desKey()是一个native函数,需要到so中分析,它在native-lib.so

IDA打开native-lib.so反编译,现在静态函数里找

直接就找到了,跳转过去

分析代码,查看DES3_KEY的值

kei的值是appapiche168comappapiche168comap

hook getDesKey()

也可以通过hook 获取

1
2
3
4
5
6
7
8
9
10
11
12
function hook_getDesKey(){
Java.perform(function(){
let AHAPIHelper = Java.use("com.autohome.ahkit.AHAPIHelper");
AHAPIHelper["getDesKey"].implementation = function (context) {
console.log(`AHAPIHelper.getDesKey is called: context=${context}`);
let result = this["getDesKey"](context);
console.log(`AHAPIHelper.getDesKey result=${result}`);
return result;
};
})
}
hook_getDesKey()

hook 结果

1
2
AHAPIHelper.getDesKey is called: context=com.che168.autotradercloud.ATCApplication@ddbd62d
AHAPIHelper.getDesKey result=appapiche168comappapiche168comap

iv向量

iv的值为appapich

hook encode3Des

1
2
3
4
5
6
7
8
9
10
11
12
function hook_encode3Des(){
Java.perform(function(){
let SecurityUtil = Java.use("com.autohome.ahkit.utils.SecurityUtil");
SecurityUtil["encode3Des"].implementation = function (context, str) {
console.log(`SecurityUtil.encode3Des is called: context=${context}, str=${str}`);
let result = this["encode3Des"](context, str);
console.log(`SecurityUtil.encode3Des result=${result}`);
return result;
};
})
}
hook_encode3Des()

hook 结果

1
2
SecurityUtil.encode3Des is called: context=com.che168.autotradercloud.ATCApplication@77f5244, str=01f27aed_7242_412e_80fe_d77895dd8c6c|1739375229.000000|436588
SecurityUtil.encode3Des result=dtRLGew6PDK+XUyKHax77H3+dDRC6VqmOHCbYsmzYEtRxFWu5cuowjF1r6Jy DRO/48UWBBgl5RF4+L8WGxjENg==

Python生成uuid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import uuid
import time
from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad,unpad
import base64

def g_imei():
u4 = uuid.uuid4()
return str(u4)

def g_data():
REPORT_VAL_SEPARATOR = "|"
deviceid = "436586"
return g_imei()+REPORT_VAL_SEPARATOR+str(int(time.time()))+".000000|"+deviceid

def des_encrypt_cbc(plaintext,key,iv):
cipher = DES3.new(key,DES3.MODE_CBC,iv)
padded_text = pad(plaintext,DES3.block_size)
encrypted_data = cipher.encrypt(padded_text)
return encrypted_data

data = g_data().encode("utf-8")
key = b"appapiche168comappapiche"
iv = b"appapich"

udid = base64.b64encode(des_encrypt_cbc(data,key,iv))
print(udid)

登陆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
import requests
import hashlib
import uuid
import time
from Crypto.Cipher import DES3
from Crypto.Util.Padding import pad,unpad
import base64

def md5_call(data):
# 创建md5对象
md5 = hashlib.md5()
# 更新要加密的数据,参数要是字节类型
md5.update(data.encode("utf-8"))
# 获取加密后的数据,以16进制表示
md5_hex = md5.hexdigest()
return md5_hex

def g_pwd(pwd):
return md5_call(pwd)

def g_imei():
u4 = uuid.uuid4()
return str(u4)

def g_data():
REPORT_VAL_SEPARATOR = "|"
deviceid = "436586"
return g_imei()+REPORT_VAL_SEPARATOR+str(int(time.time()))+".000000|"+deviceid

def des_encrypt_cbc(plaintext,key,iv):
cipher = DES3.new(key,DES3.MODE_CBC,iv)
padded_text = pad(plaintext,DES3.block_size)
encrypted_data = cipher.encrypt(padded_text)
return encrypted_data

def g_udid():
data = g_data().encode("utf-8")
key = b"appapiche168comappapiche"
iv = b"appapich"
return base64.b64encode(des_encrypt_cbc(data,key,iv)).decode("utf-8")

def g_sign(body):
# print(body)
str = "W@oC!AH_6Ew1f6%8"
sign_str =""
sign_str+=str
sign_str+="_appid"
sign_str+=body["_appid"]
sign_str+="appversion"
sign_str+=body["appversion"]
sign_str+="channelid"
sign_str+=body["channelid"]
sign_str+="pwd"
sign_str+=body["pwd"]
sign_str+="signkey"
sign_str+="type"
sign_str+="udid"
sign_str+=body["udid"]
sign_str+="username"
sign_str+=body["username"]
sign_str+=str
# print(sign_str)
sign = md5_call(sign_str)
return sign.upper()




url = "https://dealercloudapi.che168.com/tradercloud/sealed/login/login.ashx"

headers = {
"cache-control": "public, max-age=0",
"traceid": "atc.android_75ca194d-982a-4c62-9c0a-eb647fcfff77",
"content-type": "application/x-www-form-urlencoded",
"content-length": "275",
"accept-encoding": "gzip",
"user-agent": "okhttp/3.14.9"
}

body = {
"_appid": "atc.android",
# "_sign": "821E3885B2FC914FBFAD687399B650E7",
"appversion": "3.71.0",
"channelid": "csy",
# "pwd": "25d55ad283aa400af464c76d713c07ad",
"signkey": "",
"type": "",
# "udid": "aYU5PJ/U/aR1iOFu+Rd2HB7mst8DN5YX+4ft9CvXkL/YCpKYH4gg9+MiTNly tCkk7g9hVsWqMGbruaIqoS6K5A==",
# "username": "18888888888"
}

username = input("Please input username:")
pwd = input("Please input pwd:")
body["username"] = username
body["pwd"] = g_pwd(pwd)
body["udid"] = g_udid()
sign = g_sign(body)
body["_sign"] = sign

response = requests.post(url=url,headers=headers,data=body)
print(response.status_code)
print(response.text)

Unidbg

调用get3desKey()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// MainActivity 继承 AbstractJni,通过 JNI (Java Native Interface) 与本地代码交互。
public class MainActivity extends AbstractJni {

// 声明用于安卓模拟器、内存和虚拟机(VM)设置的对象。
private final AndroidEmulator emulator;
private final Memory memory;
private final VM vm;
private String process = ""; // 进程名称(可以后续设置)。

// 构造函数,用于初始化模拟器、内存和虚拟机(VM)。
public MainActivity() {
// 创建一个 64 位的安卓模拟器实例。
emulator = AndroidEmulatorBuilder
.for64Bit() // 如果需要可以使用 for32Bit()。
.setProcessName(process) // 设置模拟器的进程名称。
.addBackendFactory(new Unicorn2Factory(true)) // 添加后端工厂(Unicorn 引擎)。
.build(); // 构建模拟器。

// 获取模拟器的内存。
memory = emulator.getMemory();

// 设置库解析器,指定安卓版本为 23(安卓 6.0 Marshmallow)。
memory.setLibraryResolver(new AndroidResolver(23));

// 使用指定的 APK 文件创建虚拟机(Dalvik VM)。
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/chezhiying/getkey/chezhiying_3.71.0.apk"));

// 设置 JNI,以便与本地方法交互。
vm.setJni(this);

// 加载模拟器所需的本地库(native-lib)。
vm.loadLibrary("native-lib", true); // 第二个参数指定是否动态加载该库。
}

// 通过 JNI 和模拟器获取 3DES 密钥的方法。
public void getkey() {
// 解析 APK 中的 CheckSignUtil 类,用于调用其方法。
DvmClass CheckSignUtilClass = vm.resolveClass("com.autohome.ahkit.jni.CheckSignUtil");

// 指定 get3desKey 方法的签名。
String method = "get3desKey(Landroid/content/Context;)Ljava/lang/String;";

// 调用 CheckSignUtil 类中的静态方法 get3desKey,传递一个空的 Context 对象。
StringObject stringObject = CheckSignUtilClass.callStaticJniMethodObject(emulator, method, vm.resolveClass("android/content/Context").newObject(null));

// 从返回的 StringObject 中获取字符串结果。
String resultstring = stringObject.getValue();

// 输出结果(即 3DES 密钥)。
System.out.println(resultstring);
}

// main 方法,用于启动模拟器、运行进程并调用 getkey 方法。
public static void main(String[] args) {
// 创建 MainActivity 实例,初始化模拟器和虚拟机。
MainActivity main = new MainActivity();

// 调用 getkey 方法,获取 3DES 密钥。
main.getkey();
}
}

输出

1
appapiche168comappapiche168comap