1 今日目标

目标:唯品会商品关键字搜索

版本:v7.83.3.apk

知识点:so延迟加载和反射

2 抓包分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 搜索接口
-地址
https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1
-请求方式
POST
-请求头
authorization:OAuth api_sign=5caa3620e8f9320a0b081b29d0a3881a28b1cccd #【逆向】
-请求体(有很多,其他都是固定,我们分析如下几个)
{
"api_key": "23e7f28019e8407b98b84cd05b5aef2c", # 【固定】
"did": "0.0.f5bb01a00a0871e0c5d670abbacda7d9.c75f2a",#【其他请求返回 generate_token】
"mars_cid": "a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d", # 【UUID】
"session_id": "a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d_shop_android_1692256940241", #【UUID+时间戳】
"skey": "6692c461c3810ab150c9a980d0c275ec", # 【固定】
}

image-20230817152835415

接下来,可以在charles中修改请求包,再次发送,将不必要的参数移除,测试发现不能删除,删除后就会提示相关错误,所以,上述参数都需要携带。

2.1 调整顺序

请求之前的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 注册设备
APP第一次打开,设备注册
- 向后端发送请求,后端返回token,保存,其他请求携带。
- 向后端发送请求+生成token,发送后端,授权,后续其他请求。
注意:有些app卸载
https://mp.appvipshop.com/apns/device_reg

# getTokenByFP
https://vcsp-api.vip.com/token/getTokenByFP

# generate_token
https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token
- 请求头:authorization: OAuth api_sign=2cc09d0c582a601d57548dadea3a87878fd4e176
- 请求体:
api_key
edata(会用到getTokenByFP返回的vcspToken 和 请求传递的vcspKey)
skey
- 响应:
"did": "0.0.f5bb01a00a0871e0c5d670abbacda7d9.c75f2a",

# 搜索
https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1

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
# 1 请求头中不能删
authorization OAuth api_sign=bf46ad824c4e372c310a5fd5091e1319de413049
# 2 一旦删除请求体中任意一个东西,验证就不通过了
-猜测,有个秘钥是对整个请求头加了密做了一个签名
-api_key 固定
-skey 固定
-mars_cid uuid
-session_id uuid+时间
-对请求体整体加密后做成了authorization的值


# 3 did:找不到
-did的生成:
-有可能代码生成
-发送某个请求,返回的:https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token
# 4 authorization:加密得到的



# 破解请求顺序

# 1 https://mp.appvipshop.com/apns/device_reg
-注册设备
-一般app都会有这个接口,第一次运行,注册设备接口

#2 https://vcsp-api.vip.com/token/getTokenByFP?vcspKey=4d9e524ad536c03ff203787cf0dfcd29
-生成edata,必须先调用
-该请求返回了一个vcspToken,生成的edata
#3 generate_token 返回did,给搜索接口用
https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token
# 请求体:
api_key 23e7f28019e8407b98b84cd05b5aef2c # 固定
did
edata 很多很多东西---》可能是返回的,可能是代码生成的---》一个接口返回的数据+代码生成
eversion 0
skey 6692c461c3810ab150c9a980d0c275ec# 固定
timestamp 1692706244 # 时间戳
#4 搜索接口
https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1

2.1 分析过程详细

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# 1 charles--->手机端配置代理

# 2 搜索---》抓包
-抓到包---》这个包需要携带很多数据(请求头,请求体)---》有一部分是之前别的请求返回的数据
-找之前请求--》app装好后第一次打开,这几个请求才会有(清空缓存)


# 3 搜索商品的请求:
地址:https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1
请求方式:post
请求头:(一个)
authorization OAuth api_sign=e571d25abbd4a1bb8cbd60beddab6c081b14a862
请求体:
api_key 23e7f28019e8407b98b84cd05b5aef2c # 需要破
app_name shop_android # 固定
app_version 7.83.3 # 固定的
bigSaleTagIds
brandIds
brandStoreSns
categoryId
channelId 1
channel_flag 0_1
client android
client_type android
darkmode 0
deeplink_cps
device_model Google Pixel 2 XL # 手机型号
elder 0
extParams {"priceVer":"2","mclabel":"1","cmpStyle":"1","statusVer":"2","ic2label":"1","video":"2","uiVer":"2","preheatTipsVer":"4","floatwin":"1","superHot":"1","exclusivePrice":"1","router":"1","coupons":"1","needVideoExplain":"1","rank":"2","needVideoGive":"1","bigBrand":"1","couponVer":"v2","videoExplainUrl":"1","live":"1","sellpoint":"1","reco":"1","vreimg":"1","search_tag":"2","tpl":"1","stdSizeVids":"","labelVer":"2"}
fdc_area_id 103101101113
functions RTRecomm,flagshipInfo,feedback,otdAds,zoneCode,slotOp,survey,hasTabs,floaterParams
harmony_app 0
harmony_os 0
headTabType all
height 2712
isMultiTab 0
keyword 长袖套衫 # 搜索条件
lastPageProperty {"isBgToFront":"0","suggest_text":"长袖套衫","scene_entry_id":"-99","refer_page_id":"page_te_globle_classify_search_1698840606324","text":"长袖套衫","tag":"1","module_name":"com.achievo.vipshop.search","type":"all","typename":"全部","is_back_page":"0"}
maker GOOGLE
mars_cid a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d # 需要破
mobile_channel oziq7dxw:::
mobile_platform 3
net WIFI
operator
os Android
osv 11
otddid
other_cps
page_id page_te_commodity_search_1698840618728
phone_model pixel 2 xl
priceMax
priceMin
props
province_id 103101 # 省份数字
referer com.achievo.vipshop.search.activity.TabSearchProductListActivity
rom Dalvik/2.1.0 (Linux; U; Android 11; Pixel 2 XL Build/RP1A.201005.004.A1)
sd_tuijian 0
service_provider
session_id a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d_shop_android_1698834176657 # 需要破
skey 6692c461c3810ab150c9a980d0c275ec # 需要破
sort 0
source app
source_app android
standby_id oziq7dxw:::
sys_version 30
timestamp 1698840618 # 时间戳
union_mark blank&_&blank&_&oziq7dxw:::&_&blank&_&blank
vipService
warehouse VIP_SH
width 1440


-----------需要破的-------------
api_key:23e7f28019e8407b98b84cd05b5aef2c # 看上去像sha1,md5摘要
mars_cid:a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d # uuid
session_id:a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d _shop_android_1698834176657 # mars_cid+时间戳
skey:6692c461c3810ab150c9a980d0c275ec # 摘要算法




# 4 改包测试
-去掉请求头中的(authorization)--》API signature must not be empty 必须要传
-请求体中:随便去一个,都报错
Invalid API signature 962b50c5714a4eb35590a265462877510e34aac2



# 5 猜测:
-把请求体的所有内容---》通过某种加密方式得到摘要
-把摘要放到了请求头中
-只要请求体中去掉任意一个参数,都会校验失败

# 6 多次请求:
api_key:23e7f28019e8407b98b84cd05b5aef2c # 多次请求是一样的
mars_cid:a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d # 多次请求是一样的
session_id:a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d _shop_android_1698834176657 # 多次请求是一样的
skey:6692c461c3810ab150c9a980d0c275ec # 多次请求是一样的


# 7 最核心就是 authorization 的破解
# 8 第一次发送请求搜索的时候---》did
did:0.0.f5bb01a00a0871e0c5d670abbacda7d9.c75f2a

# 9 反编译app---》搜索发现
api_key mars_cid session_id skey 都搜到,代码生成的
唯独:did搜不到
did可能如何产生的?
1 使用代码产生---》如果使用代码生成,一定能搜到
2 某个请求返回的数据----》下次请求携带它

# 10 如果要破搜索---》必须先要破--》https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token


# 11 generate_token接口---返回了did
-请求地址:https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token
-请求方式:post
-请求头:authorization OAuth api_sign=ff7c3d6bcfd91c91e6afdcc387e78db082037b05
-请求体:
api_key 23e7f28019e8407b98b84cd05b5aef2c # 看到过
did # 空的
edata # 非常多
eversion 0 # 固定的
skey 6692c461c3810ab150c9a980d0c275ec # 看到过
timestamp 1698842071 # 时间戳
# 12 edata不知道怎么来的
-搜:某个接口返回的(vcsptoken)用代码 最终生成的---》edata


# 13 edata是那个接口返回的呢?
https://vcsp-api.vip.com/token/getTokenByFP?vcspKey=4d9e524ad536c03ff203787cf0dfcd29
# 14 注册设备接口
https://mp.appvipshop.com/apns/device_reg?app_name=achievo_ad&app_version=7.83.3&device_token=a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d&status=1&warehouse=null&manufacturer=Google&device=Pixel+2+XL&os_version=30&channel=oziq7dxw%3A%3A%3A&vipruid=&regPlat=0&regid=null&rom=Dalvik%2F2.1.0+%28Linux%3B+U%3B+Android+11%3B+Pixel+2+XL+Build%2FRP1A.201005.004.A1%29&skey=6692c461c3810ab150c9a980d0c275ec


# 15 最终:破解四个接口
1 https://mp.appvipshop.com/apns/device_reg
2 https://vcsp-api.vip.com/token/getTokenByFP
3 https://mapi.appvipshop.com/vips-mobile/rest/device/generate_token
4 https://mapi.appvipshop.com/vips-mobile/rest/shopping/search/product/list/v1

三 注册设备device_reg

image-20240517180820867

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 请求地址
https://mp.appvipshop.com/apns/device_reg
# 请求方式
get
# 请求参数
device_token:a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d
skey:6692c461c3810ab150c9a980d0c275ec
# 请求头:
authorization:OAuth api_sign=bd16242e8738370e6ea310dad1ff92d49c181040


## 注意:
请求头的authorization删除再发送包,也可以成功,但是后续会有用,我们要继续逆向
请求体的:skey也可以不携带

3.1 逆向 device_token

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
# 1 jadx打开app
# 2 根据关键字 device_token 搜索
-搜出11个位置,我们随便点一个进去看,因为像device_token这种字段,很多请求都会携带
-生成的方案应该是固定的
-所以我们从一个位置进去,去寻找即可
# 3 找到位置--图2
sb2.append("&device_token=");
sb2.append(c.Q().m());
# 4 我们查看 c.Q().m()---》图3---》返回一个类中的变量----找它的赋值位置
public String m() {
if (TextUtils.isEmpty(this.f73030x)) {
MyLog.debug(c.class, "mid isEmpty!!!!!!");
}
return this.f73030x;
}
# 5 找到this.f73030x赋值位置---图4
public c u0(String str) {
this.f73030x = str;
CommonsConfig.getInstance().setMid(str);
ApiConfig.getInstance().setMid(str);
LogConfig.self().setMid(str);
return Q();
}

# 6 查找谁调用了u0--》可以通过hook查看调用栈,也可以直接查找用例
-查找用例---》我们查看第一个

# 7 hk.c.Q().u0调用了u0---传入了Utils.i(BaseApplication.getContextObject())
public Object call() throws Exception {
hk.c.Q().u0(Utils.i(BaseApplication.getContextObject()));
b1.j().f();
return null;
}
# 8 我们继续查找Utils.i 的声明---》图6---》最终确定,其实就是uuid
public static String i(Context context) {
if (CommonsConfig.getInstance().isPreviewModel) {
return hk.c.Q().m();
}
if (p(f39524g)) {
String stringByKey = CommonPreferencesUtils.getStringByKey(context, CommonsConfig.VIP_MID_KEY);
f39524g = stringByKey;
if (p(stringByKey) || DeviceUuidFactory.ANDROIDID_000000000_MID.equals(f39524g)) {
String uuid = DeviceUuidFactory.getDeviceUuid(context).toString();
f39524g = uuid;
if (p(uuid)) {
f39524g = UUID.randomUUID().toString();
CommonPreferencesUtils.addConfigInfo(context, CommonsConfig.MID_TYPE_KEY, "3");
}
CommonPreferencesUtils.addConfigInfo(context, CommonsConfig.VIP_MID_KEY, f39524g);
}
}
return f39524g;
}

image-20240517180835649

image-20240517180844467

image-20240517180854532

image-20240517180906015

image-20240517180914670

image-20240517180922911

3.1.0 破解过程

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
# 1 反编译--搜索---》 device_token---》很多
# 2 搜索出11个,不确定那个,随便看几个---》最终发现,殊途同归---》都是uuid
# 3 看第一个
sb2.append(s.b.f83866a);
sb2.append("?ordersn=");
sb2.append(str);
sb2.append("&device_token=");
sb2.append(c.Q().m());
# 4 c.Q().m()--》查找声明
public String m() {
if (TextUtils.isEmpty(this.f73030x)) {
MyLog.debug(c.class, "mid isEmpty!!!!!!");
}
return this.f73030x;
}
# 5 找 this.f73030x 在哪赋值的
public c u0(String str) {
this.f73030x = str; # 在这里赋值
CommonsConfig.getInstance().setMid(str);
ApiConfig.getInstance().setMid(str);
LogConfig.self().setMid(str);
return Q();
}

# 6 谁调用了u0: hook打印调用栈 查找用例
# 7 查找用例:有很多,大致有两类---》随便看---》最终 都是一个位置
-一类是:uui
-一类是:Utils.i 生成的

# 8 随便看第一个:
hk.c.Q().u0(Utils.i(BaseApplication.getContextObject()));

# 9 Utils.i:先去xml中找,找不到--》生成 安卓uuid---》还没生成就随机生成uuid
public static String i(Context context) {
if (CommonsConfig.getInstance().isPreviewModel) {
return hk.c.Q().m();
}
if (p(f39524g)) {
String stringByKey = CommonPreferencesUtils.getStringByKey(context, CommonsConfig.VIP_MID_KEY);
f39524g = stringByKey;
if (p(stringByKey) || DeviceUuidFactory.ANDROIDID_000000000_MID.equals(f39524g)) {
String uuid = DeviceUuidFactory.getDeviceUuid(context).toString();
f39524g = uuid;
if (p(uuid)) {
f39524g = UUID.randomUUID().toString(); # 随机生成uuid
CommonPreferencesUtils.addConfigInfo(context, CommonsConfig.MID_TYPE_KEY, "3");
}
CommonPreferencesUtils.addConfigInfo(context, CommonsConfig.VIP_MID_KEY, f39524g);
}
}
return f39524g;
}
# 9 使用随机生成的uuid测试---》发现可以

# 10 代码模拟:uuid
import uuid
device_token = str(uuid.uuid4())

3.1.1 python 还原device_token

1
2
import uuid
device_token = str(uuid.uuid4())

3.2 逆向skey

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
# 1 搜索 skey---》图1 
-发现有很多
-有一个常量是SKEY--》编程通常的做法,定义一个常量,以后直接取该常量使用

# 2 所以我们查找常量SKEY的用例---》
-找到很多,我们关注
-随便点进一个去看
-再看看其它的,其实最后定位到一个位置


# 3 treeMap.put(ApiConfig.SKEY, f(context, new String[0]));---》查看f---》图4
public static String f(Context context, String... strArr) {
if (TextUtils.isEmpty(f2017b)) {
String info = KeyInfoFetcher.getInfo(context, ApiConfig.SKEY);
f2017b = info;
if (TextUtils.isEmpty(info) || f2017b.startsWith("KI ")) {
KeyInfoFetcher.loadKeyInfoSoWarp((strArr == null || strArr.length <= 0) ? "" : strArr[0]);
f2017b = KeyInfoFetcher.getInfo(context, ApiConfig.SKEY);
}
}
return f2017b;
}

# 4 查看String info = KeyInfoFetcher.getInfo(context, ApiConfig.SKEY);--》图6
public static String getInfo(Context context, String str) {
try {
if (clazz == null || object == null || method == null) {
int i10 = KeyInfo.f69594a;
clazz = KeyInfo.class;
object = KeyInfo.class.newInstance();
method = clazz.getMethod("getInfo", Context.class, String.class);
}
return (String) method.invoke(object, context, str);
} catch (Exception e10) {
VCSPMyLog.error(KeyInfoFetcher.class, e10);
return "";
}
}

# 5 上述核心代码---java的反射机制
clazz = KeyInfo.class; # 找到类
object = KeyInfo.class.newInstance(); # 得到类的对象
method = clazz.getMethod("getInfo", Context.class, String.class) # 找到对象中的方法,传入方法名,参数
return (String) method.invoke(object, context, str);# 找到方法后真正调用对象的方法,传入参数,str就是 'skey' 字符串

# 6 正常代码
KeyInfo info=new KeyInfo()
String res=info.getInfo(context,str)
return res

# 7 我们需要去KeyInfo类中找 getInfo方法--》最终掉用了jni的getNavInfo方法,传入'skey' 字符串
public class KeyInfo {
private static final String LibName = "keyinfo";
static {
try {
System.loadLibrary(LibName);
} catch (Throwable th2) {
th2.printStackTrace();
}
}
public static String getInfo(Context context, String str) {
try {
try {
return getNavInfo(context, str);
} catch (Throwable th2) {
return "KI gi: " + th2.getMessage();
}
} catch (Throwable unused) {
SoLoader.load(context, LibName);
return getNavInfo(context, str);
}
}
private static native String getNavInfo(Context context, String str);
}

# 8 正常应该去so文件中逆向
-但是我们可以hook几次看看,返回值是否是固定的
-如果是固定的就不用逆向了,直接拿着用
-换多个设备hook发现,只要传入的是'skey' 字符串
-返回值就是固定的


# 9 hook代码如下
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
# 1 搜索---》skey--》搜索到很多
-发现有个常量是skey---》好多接口请求体中都携带skey
-程序员会经常性把常用的变量,定义长常量,以后直接取着用

# 2 点第一个常量进去发现,很多常量(好多接口都会用)
public static final String API_KEY = "api_key";
public static final String APP_NAME = "app_name";
public static final String DID = "did";
public static final String EDATA = "edata";
public static final String SKEY = "skey";
# 3 程序员习惯,如果一个字符串经常用,会把它定义成常量
-通过之前抓包--发现skey 多次发送请求,其实是不变的
-生成一次后,以后都用这个常量的
# 4 查找用例:三种方式:每个都看---》看完后,又是殊途同归--》最终定位到一个位置
1 调用f生成---》找到的位置
2 GobalConfig.getSecureKey生成--》一样的
3 直接使用SecureKey
# 5 看第一个:treeMap.put(ApiConfig.SKEY, f(context, new String[0]));
public static String f(Context context, String... strArr) {
if (TextUtils.isEmpty(f2017b)) {
String info = KeyInfoFetcher.getInfo(context, ApiConfig.SKEY);
f2017b = info;
if (TextUtils.isEmpty(info) || f2017b.startsWith("KI ")) {
KeyInfoFetcher.loadKeyInfoSoWarp((strArr == null || strArr.length <= 0) ? "" : strArr[0]);
f2017b = KeyInfoFetcher.getInfo(context, ApiConfig.SKEY);
}
}
return f2017b;
}
# 6 通过:KeyInfoFetcher.getInfo(context, ApiConfig.SKEY) 得到
public static String getInfo(Context context, String str) {
try {
if (clazz == null || object == null || method == null) {
int i10 = KeyInfo.f69594a;
clazz = KeyInfo.class;
object = KeyInfo.class.newInstance();
method = clazz.getMethod("getInfo", Context.class, String.class);
}
return (String) method.invoke(object, context, str);
} catch (Exception e10) {
VCSPMyLog.error(KeyInfoFetcher.class, e10);
return "";
}
}
# 7 看如下代码---》java的反射机制
# python的反射---》通过字符串去对象中找方法(执行)或属性---》java也是一样的
clazz = KeyInfo.class; # 得到这个类 KeyInfo
object = KeyInfo.class.newInstance(); # 实例化得到对象 之前new KeyInfo()
method = clazz.getMethod("getInfo", Context.class, String.class); # 通过字符串找到方法
method.invoke(object, context, str) # 调用方法,得到返回结果---》传入对象和函数的参数

# 8 上述代码的本质:调用KeyInfo类的getInfo方法 得到结果--》直接找到KeyInfo的getInfo
public static String getInfo(Context context, String str) {
try {
try {
return getNavInfo(context, str);
} catch (Throwable th2) {
return "KI gi: " + th2.getMessage();
}
} catch (Throwable unused) {
SoLoader.load(context, LibName);
return getNavInfo(context, str);
}
}
# 9 getNavInfo(context, str)--》jni中使用so生成的
private static native String getNavInfo(Context context, String str);


# 10 正常操作---》去so文件中逆向--》找到加密算法---》破解加密算法
-常量---》赋值一次---》以后请求都是这个---其实我们可以不破--》直接使用固定的值

-使用hook,hook到返回值--》确定--》无论你怎么换设备--》值都是一样的

image-20240517180951407

image-20240517180959442

image-20240517181006631

image-20240517181015495

image-20240517181025291

3.2.1 hook–getNavInfo查看返回值

3.2.1 绕过frida调试

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

# 获取设备信息
rdev = frida.get_remote_device()

# 枚举所有的进程
processes = rdev.enumerate_processes()
for process in processes:
print(process)

# 获取在前台运行的APP
front_app = rdev.get_frontmost_application()
print(front_app)
# Application(identifier="com.achievo.vipshop", name="唯品会", pid=22907, parameters={})


### hook运行了那些so
import frida
import sys

rdev = frida.get_remote_device()
pid = rdev.spawn(["com.achievo.vipshop"])
session = rdev.attach(pid)

scr = """
Java.perform(function () {

var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

Interceptor.attach(dlopen, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();
console.log("[dlopen:]", path);
},
onLeave: function (retval) {

}
});

Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();
console.log("[dlopen_ext:]", path);
},
onLeave: function (retval) {

}
});


});
"""
script = session.create_script(scr)


def on_message(message, data):
print(message, data)


script.on("message", on_message)
script.load()
rdev.resume(pid)
sys.stdin.read()


# /data/app/~~PYR1rBhukSZcT4B2KQ_VDw==/com.achievo.vipshop-LBTKI1qDnOWh1s-aZDbLIA==/lib/arm/libmsaoaidsec.so

3.2.2 多次测试getNavInfo查看返回值

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
# 现象:hook就闪退(反调试) 删除 `libmsaoaidsec.so`
import frida
import sys

rdev = frida.get_remote_device()
pid = rdev.spawn(["com.achievo.vipshop"])
session = rdev.attach(pid)

scr = """
Java.perform(function () {
var KeyInfo = Java.use("com.vip.vcsp.KeyInfo");

KeyInfo.getNavInfo.implementation = function (ctx, str) {
console.log("-----------------");
console.log("参数==>", str);
var res = this.getNavInfo(ctx, str);
console.log("返回的值==>", res);
return res;
}
});
"""


script = session.create_script(scr)


def on_message(message, data):
print(message, data)


script.on("message", on_message)
script.load()
rdev.resume(pid)
sys.stdin.read()
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
设备1
-----------------
参数==> app_name
返回的值==> shop_android
-----------------
参数==> vcsp_key
返回的值==> 4d9e524ad536c03ff203787cf0dfcd29
-----------------
参数==> api_key
返回的值==> 23e7f28019e8407b98b84cd05b5aef2c

-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec

设备2
-----------------
参数==> app_name
返回的值==> shop_android
-----------------
参数==> vcsp_key
返回的值==> 4d9e524ad536c03ff203787cf0dfcd29
-----------------
参数==> api_key
返回的值==> 23e7f28019e8407b98b84cd05b5aef2c
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec

skey可以不传,传了也是个固定的值。

1
skey = 6692c461c3810ab150c9a980d0c275ec

3.3 逆向authorization

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#1  authorization 
authorization:OAuth api_sign=bd16242e8738370e6ea310dad1ff92d49c181040
# 2 我们可以搜索---》图 1
authorization # 很多
api_sign # 少一些

# 3 找到 addHeader的位置---》图2
-发现api_sign=str
-我们查找str的位置
str = b.b(context, treeMap2, apiProccessModel2.tokenSecret, apiProccessModel2.url)

# 4 str都是调用了 b.b---》图3
public static String b(Context context, TreeMap<String, String> treeMap, String str, String str2) {
if (treeMap != null && TextUtils.isEmpty(treeMap.get(ApiConfig.SKEY))) {
treeMap.put(ApiConfig.SKEY, f(context, new String[0]));
}
return a(context, treeMap, str);
}

# 5 返回值是a 函数---》查看a---》图4
private static String a(Context context, TreeMap<String, String> treeMap, String str) {
try {
if (VCSPCommonsConfig.getContext() == null) {
VCSPCommonsConfig.setContext(context);
}
String apiSign = VCSPSecurityBasicService.apiSign(context, treeMap, str);
if (TextUtils.isEmpty(apiSign)) {
String a10 = com.achievo.vipshop.commons.c.a();
return "p: " + a10 + ", vcsp return empty sign :" + apiSign;
}
return apiSign;
} catch (Exception e10) {
e10.printStackTrace();
String a11 = com.achievo.vipshop.commons.c.a();
return "p: " + a11 + ", Exception:" + e10.getMessage();
} catch (Throwable th2) {
th2.printStackTrace();
String a12 = com.achievo.vipshop.commons.c.a();
return "p: " + a12 + ", Throwable:" + th2.getMessage();
}
}


# 6 查看 VCSPSecurityBasicService.apiSign---》图5
public static String apiSign(Context context, TreeMap<String, String> treeMap, String str) throws Exception {
if (context == null) {
context = VCSPCommonsConfig.getContext();
}
return VCSPSecurityConfig.getMapParamsSign(context, treeMap, str, false);
}

# 7 继续看 VCSPSecurityConfig.getMapParamsSign---》图 6
public static String getMapParamsSign(Context context, TreeMap<String, String> treeMap, String str, boolean z10) {
String str2 = null;
if (treeMap != null) {
boolean z11 = false;
Set<Map.Entry<String, String>> entrySet = treeMap.entrySet();
if (entrySet != null) {
Iterator<Map.Entry<String, String>> it = entrySet.iterator();
while (true) {
if (it == null || !it.hasNext()) {
break;
}
Map.Entry<String, String> next = it.next();
if (next != null && next.getKey() != null && ApiConfig.USER_TOKEN.equals(next.getKey()) && !TextUtils.isEmpty(next.getValue())) {
z11 = true;
break;
}
}
}
if (z11) {
if (TextUtils.isEmpty(str)) {
str = VCSPCommonsConfig.getTokenSecret();
}
str2 = str;
}
return getSignHash(context, treeMap, str2, z10);
}
return null;
}

# 8 继续看 getSignHash---》图 7
public static String getSignHash(Context context, Map<String, String> map, String str, boolean z10) {
try {
return gs(context.getApplicationContext(), map, str, z10);
} catch (Throwable th2) {
VCSPMyLog.error(clazz, th2);
return "error! params invalid";
}
}
# 9 继续看gs(context.getApplicationContext(), map, str, z10)--》熟悉的反射--》图8
private static String gs(Context context, Map<String, String> map, String str, boolean z10) {
try {
if (clazz == null || object == null) {
synchronized (lock) {
initInstance();
}
}
if (gsMethod == null) {
gsMethod = clazz.getMethod("gs", Context.class, Map.class, String.class, Boolean.TYPE);
}
return (String) gsMethod.invoke(object, context, map, str, Boolean.valueOf(z10));
} catch (Exception e10) {
e10.printStackTrace();
return "Exception gs: " + e10.getMessage();
} catch (Throwable th2) {
th2.printStackTrace();
return "Throwable gs: " + th2.getMessage();
}
}

#10 我们需要从 KeyInfo 类中找 gs 方法---》gs调用jni的gsNav方法---》图9
public class KeyInfo {
private static final String LibName = "keyinfo";
static {
try {
System.loadLibrary(LibName);
} catch (Throwable th2) {
th2.printStackTrace();
}
}
private static native String gsNav(Context context, Map<String, String> map, String str, boolean z10);

public static String gs(Context context, Map<String, String> map, String str, boolean z10) {
try {
try {
return gsNav(context, map, str, z10);
} catch (Throwable th2) {
return "KI gs: " + th2.getMessage();
}
} catch (Throwable unused) {
SoLoader.load(context, LibName);
return gsNav(context, map, str, z10);
}
}
}



# 11 我们hook看一下gsNav,先看看它的参数和返回值

3.3.0 破解过程

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# 1 搜索:
# 搜:authorization 多一些
# 搜 api_sign 少一些
# 2 第一个点进去
builder.addHeader("Authorization", "OAuth api_sign=" + str);
# 3 str是什么
str = b.b()得到的
# 4 b.b()源码---返回了a
public static String b(Context context, TreeMap<String, String> treeMap, String str, String str2) {
if (treeMap != null && TextUtils.isEmpty(treeMap.get(ApiConfig.SKEY))) {
treeMap.put(ApiConfig.SKEY, f(context, new String[0]));
}
return a(context, treeMap, str);
}
# 5 查看a
private static String a(Context context, TreeMap<String, String> treeMap, String str) {
try {
if (VCSPCommonsConfig.getContext() == null) {
VCSPCommonsConfig.setContext(context);
}
String apiSign = VCSPSecurityBasicService.apiSign(context, treeMap, str);
if (TextUtils.isEmpty(apiSign)) {
String a10 = com.achievo.vipshop.commons.c.a();
return "p: " + a10 + ", vcsp return empty sign :" + apiSign;
}
return apiSign;
} catch (Exception e10) {
e10.printStackTrace();
String a11 = com.achievo.vipshop.commons.c.a();
return "p: " + a11 + ", Exception:" + e10.getMessage();
} catch (Throwable th2) {
th2.printStackTrace();
String a12 = com.achievo.vipshop.commons.c.a();
return "p: " + a12 + ", Throwable:" + th2.getMessage();
}
}
# 6 返回了apiSign--》通过String apiSign = VCSPSecurityBasicService.apiSign(context, treeMap, str)生成
public static String apiSign(Context context, TreeMap<String, String> treeMap, String str) throws Exception {
if (context == null) {
context = VCSPCommonsConfig.getContext();
}
return VCSPSecurityConfig.getMapParamsSign(context, treeMap, str, false);
}

# 7 返回了VCSPSecurityConfig.getMapParamsSign--》返回了getSignHash
public static String getMapParamsSign(Context context, TreeMap<String, String> treeMap, String str, boolean z10) {
String str2 = null;
if (treeMap != null) {
boolean z11 = false;
Set<Map.Entry<String, String>> entrySet = treeMap.entrySet();
if (entrySet != null) {
Iterator<Map.Entry<String, String>> it = entrySet.iterator();
while (true) {
if (it == null || !it.hasNext()) {
break;
}
Map.Entry<String, String> next = it.next();
if (next != null && next.getKey() != null && ApiConfig.USER_TOKEN.equals(next.getKey()) && !TextUtils.isEmpty(next.getValue())) {
z11 = true;
break;
}
}
}
if (z11) {
if (TextUtils.isEmpty(str)) {
str = VCSPCommonsConfig.getTokenSecret();
}
str2 = str;
}
return getSignHash(context, treeMap, str2, z10);
}
return null;
}
# 8 getSignHash--》返回了gs
public static String getSignHash(Context context, Map<String, String> map, String str, boolean z10) {
try {
return gs(context.getApplicationContext(), map, str, z10);
} catch (Throwable th2) {
VCSPMyLog.error(clazz, th2);
return "error! params invalid";
}
}
# 9 gs
private static String gs(Context context, Map<String, String> map, String str, boolean z10) {
try {
if (clazz == null || object == null) {
synchronized (lock) {
initInstance();
}
}
if (gsMethod == null) {
gsMethod = clazz.getMethod("gs", Context.class, Map.class, String.class, Boolean.TYPE);
}
return (String) gsMethod.invoke(object, context, map, str, Boolean.valueOf(z10));
} catch (Exception e10) {
e10.printStackTrace();
return "Exception gs: " + e10.getMessage();
} catch (Throwable th2) {
th2.printStackTrace();
return "Throwable gs: " + th2.getMessage();
}
}
# 10 gs总结成四句
clazz = KeyInfo.class
object = KeyInfo.class.newInstance();
gsMethod = clazz.getMethod("gs", Context.class, Map.class, String.class, Boolean.TYPE);
gsMethod.invoke(object, context, map, str, Boolean.valueOf(z10))
# 11 本质就是在执行KeyInfo的gs方法,传入一堆参数

# 12 找KeyInfo的gs方法
public static String gs(Context context, Map<String, String> map, String str, boolean z10) {
try {
try {
return gsNav(context, map, str, z10);
} catch (Throwable th2) {
return "KI gs: " + th2.getMessage();
}
} catch (Throwable unused) {
SoLoader.load(context, LibName);
return gsNav(context, map, str, z10);
}
}


# 13 返回了:gsNav
# 14 gsNav 是jni的方法---》authorization---》传入一些参数--》返回了加密串
private static native String gsNav(Context context, Map<String, String> map, String str, boolean z10);
# 15 咱们之前分析的--》gsNav--》传入待加密数据(请求体内容)--》返回加密后的签名

# 16 hook-gsNav查看参数和返回值
# 17 so文件阅读

image-20240517181050252

image-20240517181058730

image-20240517181108814

image-20240517181115949

image-20240517181125166

image-20240517181133023

image-20240517181205860

image-20240517181217538

image-20240517181227231

3.3.1 hook–gsNav-看入参和返回值

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
# 清除一下数据
import frida
import sys

rdev = frida.get_remote_device()

session = rdev.attach("唯品会")

scr = """
Java.perform(function () {
var KeyInfo = Java.use("com.vip.vcsp.KeyInfo");
var TreeMap = Java.use('java.util.TreeMap');


KeyInfo.gsNav.implementation = function (ctx, map,str,z10) {
console.log("-----------------gsNav-----------------");
console.log("参数==>", Java.cast(map,TreeMap).toString());
console.log("参数==>", str);
console.log("参数==>", z10);
var res = this.gsNav(ctx, map,str,z10);
console.log("返回的值==>", res);
return res;
}
});
"""
script = session.create_script(scr)


def on_message(message, data):
print(message, data)


script.on("message", on_message)
script.load()
sys.stdin.read()

'''
1 根据抓包抓到的 OAuth api_sign=bd16242e8738370e6ea310dad1ff92d49c181040 搜索
2 如下
###这些参数就是device_reg这个请求拼出来一些参数,基本都是固定的
参数==> {app_name=achievo_ad, app_version=7.83.3, channel=oziq7dxw:::, device=Pixel 2 XL, device_token=a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d, manufacturer=Google, os_version=30, regPlat=0, regid=null, rom=Dalvik/2.1.0 (Linux; U; Android 11; Pixel 2 XL Build/RP1A.201005.004.A1), skey=6692c461c3810ab150c9a980d0c275ec, status=1, vipruid=, warehouse=null}
参数==> null
参数==> false
返回的值==> bd16242e8738370e6ea310dad1ff92d49c181040

'''

3.3.2 libkeyinfo.so–》静态注册

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
# 1 我们去so文件中找 libkeyinfo.so
public class KeyInfo {
private static final String LibName = "keyinfo";
static {
System.loadLibrary(LibName);
}
# 2 打开32为的IDA,将so文件拖入

# 3 搜索java_xxx_gsNav

# 4 load 头文件,代码如下,返回了v9,所以我们看v9怎么来的
int __fastcall Java_com_vip_vcsp_KeyInfo_gsNav(JNIEnv_ *a1, int a2, int a3, int a4, int a5, int a6)
{
int v9; // r5

if ( j_Utils_ima(a1, a2, a3) )
v9 = j_Functions_gs(a1, a2, a4, a5, a6);
else
v9 = 0;
j_Utils_checkJniException(a1);
return v9;
}

# 5 j_Functions_gs(a1, a2, a4, a5, a6)得到v9,双击查看
int __fastcall j_Functions_gs(int a1, int a2, int a3, int a4, int a5)
{
return Functions_gs(a1, a2, a3, a4, a5);
}

# 6 继续双击查看---倒着看
jstring __fastcall Functions_gs(JNIEnv_ *a1, int a2, int a3, int a4, int a5){
#。。。很多代码。。。
v55 = j_getByteHash(a1, a2, v30, v16, v80, 256);
if ( v55
&& (v56 = (const char *)v55,
v57 = strcpy(v79, dest),
strcat(v57, v56),
memset(v80, 0, sizeof(v80)),
v58 = strlen(v79), #v58是一个长度,所以v79是个字符串
(v59 = (const char *)j_getByteHash(a1, a2, v79, v58, v80, 256)) != 0) )# 3 v59在此处生成
{
v53 = a1->functions->NewStringUTF(a1, v59); # 2 通过v59字符串生成的
}
else
{
v53 = 0;
}
free(v30);
return v53; # 1 返回了v53
}

}


# 7 继续查看j_getByteHash
int __fastcall j_getByteHash(int a1, int a2, int a3, int a4, int a5, int a6)
{
return getByteHash(a1, a2, a3, a4, a5);
}

# 8 继续查看 getByteHash(a1, a2, a3, a4, a5)---》猜测可能用了sha1加密
char *__fastcall getByteHash(JNIEnv_ *a1, int a2, int a3, int a4, char *a5)
{
j_SHA1Reset(v12);
j_SHA1Input(v12, a3, a4);
if ( j_SHA1Result(v12) )
return v7;
}

#9 我们hook--getByteHash--查看参数和返回值--》第二个位置的参数是v79

image-20240517181244206

image-20240517181253957

image-20240517181303506

image-20230404191805721

image-20240517181355066

3.3.2.1 Hook getByteHash的参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 去内存中中 libkeyinfo.so  getByteHash
var addr = Module.findExportByName("libkeyinfo.so", "getByteHash");
console.log(addr); //0xb696387d



Interceptor.attach(addr,{
onEnter:function (args){
this.x1 = args[2];
},
onLeave:function(retval) {
console.log("--------------------")
console.log(Memory.readCString(this.x1));
console.log(Memory.readCString(retval));
}

})

// frida -U -f com.achievo.vipshop -l hook04.js 重启app+hook(出问题)
// frida -UF -l hook04.js 手动启动+hook

image-20240517181420720

image-20240517181430705

1.手动Hook

image-20240517181440888

点击同意时,立即开始Hook。

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
// 删除数据,重启,点了同意,里面启动脚本,把握时机,如果太早,so文件没加载,如果太晚,代码执行完了,不好控制
// 去内存中中 libkeyinfo.so getByteHash
var addr = Module.findExportByName("libkeyinfo.so", "getByteHash");
console.log(addr);



Interceptor.attach(addr,{
onEnter:function (args){
this.x1 = args[2];
},
onLeave:function(retval) {
console.log("--------------------")
console.log(Memory.readCString(this.x1));
console.log(Memory.readCString(retval));
}

})

// frida -UF -l hook04.js 手动启动+hook

// 通过抓包抓到的返回值去搜索

/*
--------------------
aee4c425dbb2288b80c71347cc37d04bapp_name=achievo_ad&app_version=7.83.3&channel=oziq7dxw:::&device=Pixel 2 XL&device_token=a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d&manufacturer=Google&os_version=30&regPlat=0&regid=null&rom=Dalvik/2.1.0 (Linux; U; Android 11; Pixel 2 XL Build/RP1A.201005.004.A1)&skey=6692c461c3810ab150c9a980d0c275ec&status=1&vipruid=&warehouse=VIP_SH
f5fb31c0c3081d18c3f15c193e6f0c3868ae8d14
--------------------
aee4c425dbb2288b80c71347cc37d04b f5fb31c0c3081d18c3f15c193e6f0c3868ae8d14
f4f002d40e112a06ecdc04e54d5bf9c92c0477aa
--------------------

###我们发现aee4c425dbb2288b80c71347cc37d04b是一样的,可以猜测,这就是sha1算法的盐

*/

2.延迟Hook

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
function do_hook() {
setTimeout(function(){
var addr = Module.findExportByName("libkeyinfo.so", "getByteHash");
console.log(addr); //0xb696387d


Interceptor.attach(addr, {
onEnter: function (args) {
this.x1 = args[2];
},
onLeave: function (retval) {
console.log("--------------------")
console.log(Memory.readCString(this.x1));
console.log(Memory.readCString(retval));
}
})
},10);

}

function load_so_and_hook() {
var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

Interceptor.attach(dlopen, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();
// console.log("[dlopen:]", path);
this.path = path;
}, onLeave: function (retval) {
if (this.path.indexOf("libkeyinfo.so") !== -1) {
console.log("[dlopen:]", this.path);
do_hook();

}
}
});

Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();

this.path = path;
}, onLeave: function (retval) {
if (this.path.indexOf("libkeyinfo.so") !== -1) {
console.log("\nandroid_dlopen_ext加载:", this.path);
do_hook();

}
}
});
}

load_so_and_hook();

// frida -U -f com.achievo.vipshop -l hook05.js

3.3.3 测试OK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import hashlib

data_string ="aee4c425dbb2288b80c71347cc37d04bapp_name=achievo_ad&app_version=7.83.3&channel=oziq7dxw:::&device=Pixel 2 XL&device_token=a9d1a2b9-2a79-36fd-a8ca-cbe24c03979d&manufacturer=Google&os_version=30&regPlat=0&regid=null&rom=Dalvik/2.1.0 (Linux; U; Android 11; Pixel 2 XL Build/RP1A.201005.004.A1)&skey=6692c461c3810ab150c9a980d0c275ec&status=1&vipruid=&warehouse=VIP_SH"

# sha1加密
hash_object = hashlib.sha1()
hash_object.update(data_string.encode('utf-8'))
arg7 = hash_object.hexdigest()
print(arg7) # f5fb31c0c3081d18c3f15c193e6f0c3868ae8d14

x = "aee4c425dbb2288b80c71347cc37d04b"+arg7
# sha1加密
hash_object = hashlib.sha1()
hash_object.update(x.encode('utf-8'))
arg7 = hash_object.hexdigest()
print(arg7) # f4f002d40e112a06ecdc04e54d5bf9c92c0477aa 跟hook出来的一样

3.4 代码整合(不带authorization)

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
import requests
import uuid

import hashlib


def sha1(data_string):
# sha1加密
hash_object = hashlib.sha1()
hash_object.update(data_string.encode('utf-8'))
arg7 = hash_object.hexdigest()
return arg7


device_token = str(uuid.uuid4())
param_dict = {
'app_name': 'achievo_ad',
'app_version': '7.83.3',
'device_token': device_token,
'status': 1,
'warehouse': 'null',
'manufacturer': 'Google',
'device': 'Pixel 2 XL',
'os_version': '30',
'channel': 'oziq7dxw:::',
'vipruid': '',
'regPlat': '0',
'regid': '',
'rom': 'Dalvik/2.1.0 (Linux; U; Android 11; Pixel 2 XL Build/RP1A.201005.004.A1)',
'skey': '6692c461c3810ab150c9a980d0c275ec',

}

ordered_string = "&".join(["{}={}".format(key, param_dict[key]) for key in sorted(param_dict.keys())])

salt = "aee4c425dbb2288b80c71347cc37d04b"
tmp = sha1(f"{salt}{ordered_string}")
api_sign = sha1(f"{salt}{tmp}")

res = requests.get(
url="https://mp.appvipshop.com/apns/device_reg",
params=param_dict,
headers={
# "Authorization": "OAuth api_sign={}".format(api_sign)
},
verify=False
)
print(res.text)

3.5 代码整合(带authorization)

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
import requests
import uuid

import hashlib


def sha1(data_string):
# sha1加密
hash_object = hashlib.sha1()
hash_object.update(data_string.encode('utf-8'))
arg7 = hash_object.hexdigest()
return arg7


device_token = str(uuid.uuid4())
param_dict = {
'app_name': 'achievo_ad',
'app_version': '7.83.3',
'device_token': device_token,
'status': 1,
'warehouse': 'null',
'manufacturer': 'Google',
'device': 'Pixel 2 XL',
'os_version': '30',
'channel': 'oziq7dxw:::',
'vipruid': '',
'regPlat': '0',
'regid': '',
'rom': 'Dalvik/2.1.0 (Linux; U; Android 11; Pixel 2 XL Build/RP1A.201005.004.A1)',
'skey': '6692c461c3810ab150c9a980d0c275ec',

}

ordered_string = "&".join(["{}={}".format(key, param_dict[key]) for key in sorted(param_dict.keys())])

salt = "aee4c425dbb2288b80c71347cc37d04b"
tmp = sha1(f"{salt}{ordered_string}")
api_sign = sha1(f"{salt}{tmp}")

res = requests.get(
url="https://mp.appvipshop.com/apns/device_reg",
params=param_dict,
headers={
"Authorization": "OAuth api_sign={}".format(api_sign)
},
verify=False
)
print(res.text)

四 getTokenByFP

image-20240517181458761

4.1 vcspKey破解

在之前搞skey的Hook时,其实也获取到vcspKey,其实就是一个固定值。

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
设备1:
-----------------
参数==> app_name
返回的值==> shop_android
-----------------
参数==> vcsp_key
返回的值==> 4d9e524ad536c03ff203787cf0dfcd29
-----------------
参数==> api_key
返回的值==> 23e7f28019e8407b98b84cd05b5aef2c

-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec

设备2:
-----------------
参数==> app_name
返回的值==> shop_android
-----------------
参数==> vcsp_key
返回的值==> 4d9e524ad536c03ff203787cf0dfcd29
-----------------
参数==> api_key
返回的值==> 23e7f28019e8407b98b84cd05b5aef2c
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec
-----------------
参数==> skey
返回的值==> 6692c461c3810ab150c9a980d0c275ec

4.2 vcspauthorization

这个其实固定就好。

因为:多次请求发现是固定 + 在请求中参数中只有固定的vcspKey(固定),所以,得到的结果理应固定。

4.3 代码

1
2
3
4
5
6
7
8
9
10
import requests

res = requests.get(
url="https://vcsp-api.vip.com/token/getTokenByFP?vcspKey=4d9e524ad536c03ff203787cf0dfcd29",
headers={
"vcspauthorization": "vcspSign=05a68135d2bfd322e3a22f95bbc25a24c777f387"
}
)

print(res.text)

4.4 逆向【可选】

搜索:vcspauthorization

image-20240517181511289

image-20240517181518784

image-20240517181527027

image-20240517181534539

image-20240517181544066

后续执行过程与注册设备相同(参数不同而已)。

1.hook

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
function do_hook() {

var addr = Module.findExportByName("libkeyinfo.so", "getByteHash");
console.log(addr); //0xb696387d


Interceptor.attach(addr, {
onEnter: function (args) {
this.x1 = args[2];
},
onLeave: function (retval) {
console.log("--------------------")
console.log(Memory.readCString(this.x1));
console.log(Memory.readCString(retval));
}

})

}

function load_so_and_hook() {
var dlopen = Module.findExportByName(null, "dlopen");
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");

Interceptor.attach(dlopen, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();
// console.log("[dlopen:]", path);
this.path = path;
}, onLeave: function (retval) {
if (this.path.indexOf("libkeyinfo.so") !== -1) {
console.log("[dlopen:]", this.path);
do_hook();

}
}
});

Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var path_ptr = args[0];
var path = ptr(path_ptr).readCString();

this.path = path;
}, onLeave: function (retval) {
if (this.path.indexOf("libkeyinfo.so") !== -1) {
console.log("\nandroid_dlopen_ext加载:", this.path);
do_hook();

}
}
});
}

load_so_and_hook();

// frida -U -f com.achievo.vipshop -l delay_hook.js

// 根据抓包抓到的数据,去搜索

2.验证

1
2
3
4
5
6
7
8
--------------------
da19a1b93059ff3609fc1ed2e04b0141vcspKey=4d9e524ad536c03ff203787cf0dfcd29
5a7c831821536f5a9d5244b99af681226dc8a277
--------------------
da19a1b93059ff3609fc1ed2e04b01415a7c831821536f5a9d5244b99af681226dc8a277
05a68135d2bfd322e3a22f95bbc25a24c777f387
--------------------

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import hashlib

data_string ="da19a1b93059ff3609fc1ed2e04b0141vcspKey=4d9e524ad536c03ff203787cf0dfcd29"

# sha1加密
hash_object = hashlib.sha1()
hash_object.update(data_string.encode('utf-8'))
arg7 = hash_object.hexdigest()
print(arg7) # 5a7c831821536f5a9d5244b99af681226dc8a277

x = "da19a1b93059ff3609fc1ed2e04b0141"+arg7
# sha1加密
hash_object = hashlib.sha1()
hash_object.update(x.encode('utf-8'))
arg7 = hash_object.hexdigest()
print(arg7)#05a68135d2bfd322e3a22f95bbc25a24c777f387

__END__