jd app ep和sign参数获取解析

版本:v11.6.4

image-20240529104332702

image-20240529104403382

GET中传递的参数:

  • functionId, "functionId": "backupKeywords"
  • ep,需逆向
  • sign,需逆向
  • st,看似时间戳
  • sv,看似固定值
  • eid,删除后不影响(忽略)

“ep”:

1
2
{"hdid":"JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw=","ts":1716949561010,"ridx":-1,
"cipher":{"area":"CV83Cv8yDzu5XzK=","d_model":"UQv4ZWmzYG==","wifiBssid":"dW5hbw93bq==","osVersion":"EG==","d_brand":"H29lZ2nv","screen":"CtK4EMenCNqm","uuid":"ENZuYwSyY2S3ZJqmYtSzDG==","aid":"ENZuYwSyY2S3ZJqmYtSzDG==","openudid":"ENZuYwSyY2S3ZJqmYtSzDG=="},"ciphertype":5,"version":"1.2.0","appname":"com.jingdong.app.mall"}

拦截器

image-20240529110244755

builder.addEncodedQueryParameter(str6, urlParams4.get(str6))

Map<String, String> urlParams4 = getUrlParams(str4)

这里应该就是将url的参数转换成字典,而ep就是从字典里面取值,str4里面就是完整的url链接

image-20240529110359366

str4 就是从colorStatParamStr 中取出 “encrypt”

colorStatParamStr = JDHttpTookit.getEngine().getStatInfoConfigImpl().getColorStatParamStr(true, true, z, null, null);

colorStatParamStr 是个接口,我们需要找到谁实现了这个接口

image-20240529111130085

image-20240529111506908

image-20240529112627517

image-20240529112832190

image-20240529114939744

REPORT_PARAM_ENCRYPT_PARAM 里面能看到这里做了很多拼接

重点主要是看 EncryptTool.encryptAndEncode(hashMap3)

image-20240529115000610

image-20240529115210862

可以hook一下 encryptAndEncode 这个方法,encrypt 方法也在上面,这里应该是反编译出了问题,这里做了个缓存,判断如果有缓存取缓存,没有缓存就去取 r2.b(r7, r6) , r2 是 com.jd.phc.e.d 方法的返回值,那我们取看一下 com.jd.phc.e 里面是什么

image-20240529120915536

这里面应该就是专门的一个加密库

image-20240529121511114

下面就是r2.b方法,里面返回的就是ep参数里面的值

image-20240529121636820

可以看到,出了时间戳 currentTimeMillis 和 cipher 以外,其他的都是固定值,所以这里我们要看的只有 cipher,cipher里面有很多参数

1
{"area":"CV83Cv8yDzu5XzK=","d_model":"UQv4ZWmzYG==","wifiBssid":"dW5hbw93bq==","osVersion":"EG==","d_brand":"H29lZ2nv","screen":"CtK4EMenCNqm","uuid":"ENZuYwSyY2S3ZJqmYtSzDG==","aid":"ENZuYwSyY2S3ZJqmYtSzDG==","openudid":"ENZuYwSyY2S3ZJqmYtSzDG=="}

搜先我们需要知道,传进来的map值是什么,以及 里面的d.b对value做了什么处理

image-20240529144636044

我们可以通过hook前面的 encryptAndEncode 方法,查看传入的map值是什么

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
Java.perform(function () {
function showMap(title,map){
var result = "{";
var keyset = map.keySet();
var it = keyset.iterator();
while(it.hasNext()){
var keystr = it.next().toString();
var valuestr = map.get(keystr).toString();
result += '"' + keystr + '"';
result += ":";
result += '"' + valuestr + '"';
result += ",";
}
result += "}";
console.log(title, result);
}

var EncryptTool = Java.use("com.jingdong.common.network.encrypt.EncryptTool");
EncryptTool.encryptAndEncode.implementation = function(map){
console.log("-----------------------");
showMap("map字典->", map);
var res = this.encrypt(map);
console.log('返回值-->',res);
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
return res;
}

});

可以看到传入的是一些设备信息,而cipher是对这些设备信息做了类似于base64的加密

image-20240529150432778

d.b方法,需要将其转换成python代码

image-20240529150749508

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
import ctypes


def r(data_string):
def int_overflow(val):
maxint = 2147483647
if not -maxint - 1 <= val <= maxint:
val = (val + (maxint + 1)) % (2 * (maxint + 1)) - maxint - 1
return val

def unsigned_right_shitf(n, i):
# 数字小于0,则转为32位无符号uint
if n < 0:
n = ctypes.c_uint32(n).value
# 正常位移位数是为正数,但是为了兼容js之类的,负数就右移变成左移好了
if i < 0:
return -int_overflow(n << abs(i))
# print(n)
return int_overflow(n >> i)

char_list = []
aae = ['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']
bArr = data_string.encode('utf-8')
for i in range(0, len(bArr), 3):
bArr2 = [None for i in range(4)]
b2 = 0
for i2 in range(0, 3):
i3 = i + i2
if i3 <= len(bArr) - 1:
bArr2[i2] = b2 | unsigned_right_shitf((bArr[i3] & 255), ((i2 * 2) + 2))
b2 = unsigned_right_shitf(((bArr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255, 2)
else:
bArr2[i2] = b2
b2 = 64
bArr2[3] = b2
for i4 in range(4):
if bArr2[i4] <= 63:
char_list.append(aae[bArr2[i4]])
else:
char_list.append("=")

return "".join(char_list)

运行结果也没有问题

image-20240529151119393

最后我们可以拼接ep参数,其中uuid aid open_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
import time
import uuid
import ctypes
import json


def r(data_string):
def int_overflow(val):
maxint = 2147483647
if not -maxint - 1 <= val <= maxint:
val = (val + (maxint + 1)) % (2 * (maxint + 1)) - maxint - 1
return val

def unsigned_right_shitf(n, i):
# 数字小于0,则转为32位无符号uint
if n < 0:
n = ctypes.c_uint32(n).value
# 正常位移位数是为正数,但是为了兼容js之类的,负数就右移变成左移好了
if i < 0:
return -int_overflow(n << abs(i))
# print(n)
return int_overflow(n >> i)

char_list = []
aae = ['K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/']
bArr = data_string.encode('utf-8')
for i in range(0, len(bArr), 3):
bArr2 = [None for i in range(4)]
b2 = 0
for i2 in range(0, 3):
i3 = i + i2
if i3 <= len(bArr) - 1:
bArr2[i2] = b2 | unsigned_right_shitf((bArr[i3] & 255), ((i2 * 2) + 2))
b2 = unsigned_right_shitf(((bArr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255, 2)
else:
bArr2[i2] = b2
b2 = 64
bArr2[3] = b2
for i4 in range(4):
if bArr2[i4] <= 63:
char_list.append(aae[bArr2[i4]])
else:
char_list.append("=")

return "".join(char_list)


def encode_cipher(cipher_dict):
for k, v in cipher_dict.items():
cipher_dict[k] = r(v)


def gen_cipher_ep(uid, aid, open_udid, ts):
cipher_dict = {
"area": "1_72_2799_0",
"d_model": "Pixel3a",
"wifiBssid": "unknown",
"osVersion": "9",
"d_brand": "Google",
"screen": "2088*1080",
"uuid": uid,
"aid": aid,
"openudid": open_udid,
}
encode_cipher(cipher_dict)
data_dict = {
"hdid": "JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw=",
"ts": ts,
'ridx': -1,
'cipher': cipher_dict,
'ciphertype': 5,
"version": "1.2.0",
'appname': "com.jingdong.app.mall"
}
ep = json.dumps(data_dict, separators=(',', ':'))

return ep


def run():
aid = str(uuid.uuid4()).replace("-", "")
# aid = '86dbb2cb7e80b235'
uid = aid
open_udid = aid

ts = int(time.time() * 1000)
ep = gen_cipher_ep(aid, uid, open_udid, ts)

print(ep)


if __name__ == '__main__':
run()

'''
{"hdid":"JM9F1ywUPwflvMIpYPok0tt5k9kW4ArJEU3lfLhxBqw=","ts":1716967005172,"ridx":-1,"cipher":{"area":"CV83Cv8yDzu5XzK=","d_model":"UQv4ZWmzYG==","wifiBssid":"dW5hbw93bq==","osVersion":"EG==","d_brand":"H29lZ2nv","screen":"CtK4EMenCNqm","uuid":"ENZuYwSyY2S3ZJqmYtSzDG==","aid":"ENZuYwSyY2S3ZJqmYtSzDG==","openudid":"ENZuYwSyY2S3ZJqmYtSzDG=="},"ciphertype":5,"version":"1.2.0","appname":"com.jingdong.app.mall"}
'''

sign

如果通过关键字搜索的话,会有很多结果 如果要一个个看的话,会很耗时间,这里可以使用看调用栈的方式去找,通过抓包可以发现基本上每个请求都会携带sign, 而我们前面hook encryptAndEncode 的時候,也順便打印了调用栈,那我们可以去找一下这几个调用栈看下有没有有用的地方

image-20240529153039997

image-20240529153346246

com.jingdong.jdsdk.network.toolbox.ParamBuilderForJDMall.setupParams 里面 能看到有一个 signature 方法

image-20240529155430875

1
String signature = JDHttpTookit.getEngine().getSignatureHandlerImpl().signature(JDHttpTookit.getEngine().getApplicationContext(), functionId, str, str2, property, versionName);

我们再看下这个 getSignatureHandlerImpl().signature 方法,这里也是一个接口,通过查找用例 可以看到这里有个 getSignFromJni 方法,点进去看一下

image-20240529155650871

image-20240529161652618

可以发现这里就是native层加密了,可以hook一下这个 getSignFromJni 方法看一下对不对,我们用ida打开 jdbitmapkit 这个so文件看一下

image-20240529161739999

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java.perform(function () {

var BitmapkitUtils = Java.use("com.jingdong.common.utils.BitmapkitUtils");


BitmapkitUtils.getSignFromJni.implementation = function(ctx,str,str2,str3,str4,str5){
console.log("=================");
console.log("str=",str);
console.log("str2=",str2);
console.log("str3=",str3);
console.log("str4=",str4);
console.log("str5=",str5);
var res = this.getSignFromJni(ctx,str,str2,str3,str4,str5);
console.log("返回值=",res);
console.log("=================");
return res;
}


});

运行看一下,和抓包的结果是一样的,里面的参数就是这几个

image-20240529163023125

打开ida后,搜索 getSignFromJni 可以看到这是一个静态方法

image-20240529164335325

我们用unidbg模拟设备去主动调用c中的函数,

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
package com.nb.demo;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class jd extends AbstractJni {
private static AndroidEmulator emulator;
private static VM vm;

jd() {
// 1.创建模拟器(32位或64位),由jd的so文件在armeabi-v7a中,所以选择32位
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.jingdong.app.mall").build();

// 2.设置安卓sdk
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

// 3.创建安卓虚拟机
vm = emulator.createDalvikVM(new File("unidbg-android/jd/jd_11.6.4_apkcombo.com.apk"));
vm.setJni(this);
vm.setVerbose(true);

// 4.加载so文件
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/jd/libjdbitmapkit.so"), false);
//dm.callJNI_OnLoad(emulator);
}

public String sign() {
// 5.找到java中调用so的类和方法
DvmClass cSignUtil = vm.resolveClass("com.jingdong.common.utils.BitmapkitUtils");
String methodSign = "getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";

// 6.调用方法
StringObject obj = cSignUtil.callStaticJniMethodObject(
emulator,
methodSign,
null,
new StringObject(vm, "backupKeywords"),
new StringObject(vm, "{\"buriedExpLabel\":\"\",\"keyword\":\"烤箱\",\"populationType\":\"-1\"}"),
new StringObject(vm, "86dbb2cb7e80b235"),
new StringObject(vm, "android"),
new StringObject(vm, "11.6.4")
);

// 7.获取返回值
return obj.getValue();
}

public static void main(String[] args) {
jd obj = new jd();
String result = obj.sign();
System.out.println(result);
}
}

不出意外的报错,接下来就是补环境了

image-20240529174846784

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
@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if (signature.equals("java/lang/StringBuffer-><init>()V")) {
return vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());
}
if (signature.equals("java/lang/Integer-><init>(I)V")) {
return vm.resolveClass("java/lang/Integer").newObject(vaList.getIntArg(0));
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}

@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;")) {
StringBuffer str = (StringBuffer) dvmObject.getValue();
StringObject data = vaList.getObjectArg(0);
return vm.resolveClass("java/lang/StringBuffer").newObject(str.append(data.getValue()));
}
if (signature.equals("java/lang/Integer->toString()Ljava/lang/String;")) {
Integer iUse = (Integer) dvmObject.getValue();
return new StringObject(vm, Integer.toString(iUse));
}
if (signature.equals("java/lang/StringBuffer->toString()Ljava/lang/String;")) {
StringBuffer str = (StringBuffer) dvmObject.getValue();
return new StringObject(vm, str.toString());
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

环境补齐后,看下运行结果,成功取到值,搜索关键字那里只需要留一个 keyword 就好

image-20240529175147568

因为要打包给py调用,所以这里需要加上证书以及加上参数传递,完整代码如下

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
package com.nb.demo;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class jd extends AbstractJni {
private final AndroidEmulator emulator;
private final VM vm;

jd() {
// 1.创建模拟器(32位或64位),由jd的so文件在armeabi-v7a中,所以选择32位
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.jingdong.app.mall").build();

// 2.设置安卓sdk
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));

// 3.创建安卓虚拟机
vm = emulator.createDalvikVM(new File("unidbg-android/jd/jd_11.6.4_apkcombo.com.apk"));
vm.setJni(this);
//vm.setVerbose(true);
vm.setVerbose(false);

// 4.加载so文件
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/jd/libjdbitmapkit.so"), false);
//dm.callJNI_OnLoad(emulator);
}

public String sign(String func, String body, String uid, String platform, String version) {
// 5.找到java中调用so的类和方法
DvmClass cSignUtil = vm.resolveClass("com.jingdong.common.utils.BitmapkitUtils");
String methodSign = "getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";

// 6.调用方法
StringObject obj = cSignUtil.callStaticJniMethodObject(
emulator,
methodSign,
null,
new StringObject(vm, func),
new StringObject(vm, body),
new StringObject(vm, uid),
new StringObject(vm, platform),
new StringObject(vm, version)
);

// 7.获取返回值
return obj.getValue();
}


public static void main(String[] args) {
String functionId = "backupKeywords";
String body = " {\"keyword\":\"烤箱\"}";
String uuid = "86dbb2cb7e80b235";
String platform = "android";
String version = "11.6.4";

if (args.length == 3) {
functionId = args[0]; // "backupKeywords"
body = args[1]; // {"keyword":"烤箱"}
uuid = args[2]; // "86dbb2cb7e80b235"
}
jd obj = new jd();
String signString = obj.sign(functionId, body, uuid, platform, version);
System.out.println(signString);
}

@Override
public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
if (signature.equals("java/lang/StringBuffer-><init>()V")) {
return vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());
}
if (signature.equals("java/lang/Integer-><init>(I)V")) {
return vm.resolveClass("java/lang/Integer").newObject(vaList.getIntArg(0));
}
return super.newObjectV(vm, dvmClass, signature, vaList);
}

@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
if (signature.equals("java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;")) {
StringBuffer str = (StringBuffer) dvmObject.getValue();
StringObject data = vaList.getObjectArg(0);
return vm.resolveClass("java/lang/StringBuffer").newObject(str.append(data.getValue()));
}
if (signature.equals("java/lang/Integer->toString()Ljava/lang/String;")) {
Integer iUse = (Integer) dvmObject.getValue();
return new StringObject(vm, Integer.toString(iUse));
}
if (signature.equals("java/lang/StringBuffer->toString()Ljava/lang/String;")) {
StringBuffer str = (StringBuffer) dvmObject.getValue();
return new StringObject(vm, str.toString());
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
}

打包成jar包后,我们在用python调用试一下,发现能获得结果,那就没有问题

1
2
3
4
5
6
7
8
9
10
11
import uuid
import subprocess

function_id = "backupKeywords"
body = '{"keyword":"烤箱"}'
uid = str(uuid.uuid4()).replace("-", "")

cmd = f"java -jar unidbg-0.9.7.jar {function_id} '{body}' {uid}"
signature = subprocess.check_output(cmd, shell=True, cwd="unidbg_0_9_7_jar")
data_string = signature.strip()
print(data_string)

image-20240529182229524

代码整合

最后整理一下代码,因为unidbg对windows的支持问题,会导致sign的值无法通过校验,需要在linux或者ubuntu环境中运行

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
def run():
aid = gen_uuid()
# aid = '86dbb2cb7e80b235'
uid = aid
open_udid = aid
ts = int(time.time() * 1000)
ep = gen_cipher_ep(aid, uid, open_udid, ts)

function_id = "backupKeywords"
body = '{"buriedExpLabel":"","keyword":"小米","populationType":"-1"}'

sign_dict = gen_sign(function_id, body, uid)
print(sign_dict)
res = requests.post(
url="https://api.m.jd.com/client.action",
params={
"functionId": function_id,
"lmt": "0",
"clientVersion": "11.6.4",
"build": "98704",
"client": "android",
"partner": "tencent",
"eid": "eidAb9ca8121a7s6gyT2F NMSCiLrTFEdeFrUYMk96eiCfCTOKUs/7HWrHt PIn bp7iSDBzRZbyADnHJQFhti/BqtZUYVLuR7IyPn0k9Wx00ZqrQ91s",
"sdkVersion": "28",
"lang": "zh_CN",
"harmonyOs": "0",
"networkType": "wifi",
"uemps": "0-0",
"ext": '{"prstate":"0","pvcStu":"1"}',
"ef": "1",
"ep": ep,
"st": sign_dict['st'],
"sign": sign_dict['sign'],
"sv": sign_dict['sv'],
},
data='lmt=0&body={}&'.format(quote_plus(body)),
headers={
'Host': 'api.m.jd.com',
'x-referer-package': 'com.jingdong.app.mall',
'charset': 'UTF-8',
'x-referer-page': 'com.jd.lib.search.view.Activity.SearchActivity',
'cache-control': 'no-cache',
'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
'user-agent': 'okhttp/3.12.13',
},
)

print(res.text)


if __name__ == '__main__':
run()