SO逆向之唯品会api_sign

抓包

postern+chales

image-20220301200720504

https://mapi.appvipshop.com/vips-mobile/rest/shopping/product/module/list/v2

OAuth Authentication
api_sign851720db2410ed92d3628f1739c00e1374d26c02

分析

jadx打开唯品会7.45.6.apk,搜索api_sign

image-20220301203612696

进入apiSign

1
2
3
4
5
6
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);
}

进入getMapParamsSign

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
public static String getMapParamsSign(Context context, TreeMap<String, String> treeMap, String str, boolean z) {
String str2 = null;
if (treeMap == null) {
return null;
}
boolean z2 = 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())) {
z2 = true;
break;
}
}
}
if (z2) {
if (TextUtils.isEmpty(str)) {
str = VCSPCommonsConfig.getTokenSecret();
}
str2 = str;
}
return getSignHash(context, treeMap, str2, z);
}

一顿操作不过是为了生成str2作为参数传入getSignHash

1
2
3
4
5
6
7
8
public static String getSignHash(Context context, Map<String, String> map, String str, boolean z) {
try {
return gs(context.getApplicationContext(), map, str, z);
} catch (Throwable th) {
VCSPMyLog.error(clazz, th);
return "error! params invalid";
}
}

调用了gs函数

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
private static String gs(Context context, Map<String, String> map, String str, boolean z) {
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(z));
} catch (Exception e) {
e.printStackTrace();
return "Exception gs: " + e.getMessage();
} catch (Throwable th) {
th.printStackTrace();
return "Throwable gs: " + th.getMessage();
}
}
private static void initInstance() {
if (clazz == null || object == null) {
try {
Class<?> cls = Class.forName("com.vip.vcsp.KeyInfo");
clazz = cls;
object = cls.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}

gs中通过反射获取KeyInfo类,并反射调用了该类的gs方法,进入KeyInfo

1
2
3
4
5
6
7
8
9
private static final String LibName = "keyinfo";
public static String gs(Context context, Map<String, String> map, String str, boolean z) {
try {
return gsNav(context, map, str, z);
} catch (Throwable th) {
return "KI gs: " + th.getMessage();
}
}
private static native String gsNav(Context context, Map<String, String> map, String str, boolean z);

gsNav位于libkeyinfo.so中,想必就是api_sign生成的位置,IDA打开libkeyinfo.so

image-20220301205910870

F5反汇编,想必主要加密逻辑在 j_Functions_gs 函数里

1
2
3
4
5
6
7
8
9
10
11
int __fastcall Java_com_vip_vcsp_KeyInfo_gsNav(int a1, int a2, int a3, int a4, int a5, int a6)
{
int v9; // r5

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

进入j_Functions_gs后,上面对params的map做处理

image-20220301210225283

可疑函数j_getByteHash,看这逻辑要么返回0,要么返回((int (__fastcall *)(JNIEnv *))(*a1)->NewStringUTF)(a1)

image-20220301210301616

进入j_getByteHash,看着像是调用SHA1算法返回字符串,固定长度为160位的消息摘要,40个字符长度,和api_sign长度一致

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
char *__fastcall getByteHash(int a1, int a2, int a3, int a4, char *a5)
{
char *v7; // r4
int i; // r5
int v9; // r2
_QWORD v11[8]; // [sp+0h] [bp-D8h] BYREF
_DWORD v12[26]; // [sp+44h] [bp-94h] BYREF

if ( !a3 )
return 0;
v7 = a5;
j_SHA1Reset(v12);
j_SHA1Input(v12, a3, a4);
if ( j_SHA1Result(v12) )
{
for ( i = 0; i != 5; ++i )
{
v9 = v12[i];
v11[6] = 0LL;
v11[7] = 0LL;
v11[4] = 0LL;
v11[5] = 0LL;
v11[0] = 0LL;
v11[1] = 0LL;
v11[2] = 0LL;
v11[3] = 0LL;
sprintf((char *)v11, "%08x", v9);
strcat(a5, (const char *)v11);
}
}
return v7;
}

frida

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function main() {
Java.perform(function () {
// 导出jni函数
var native_func = Module.findExportByName(
"libkeyinfo.so", "getByteHash"
);

Interceptor.attach(native_func, {
onEnter: function (args) {
// getByteHash(int a1, int a2, int a3, int a4, char *a5)
// getByteHash(JNIEnv *env, jobject a2, int a3, int a4, char *a5)
console.log('args[2]: ', Memory.readCString(args[2]));
},
onLeave: function (return_val) {
console.log('return_val: ', Memory.readCString(return_val));
}
});
});
}

setImmediate(main)

一次api_sign的生成调用了两次非魔改后的sha1算法,两次前面都加了盐a84c5883206309ad076deea939e850dc&8cf90dfd9a89070a9e8653d92f57349b

1
2
3
4
args[2]:  a84c5883206309ad076deea939e850dc&8cf90dfd9a89070a9e8653d92f57349bapi_key=23e7f28019e8407b98b84cd05b5aef2c&app_name=shop_android&app_version=7.45.6&channel_flag=0_1&client=android&client_type=android&context={"615":"1","872":"1"}&darkmode=0&deeplink_cps=&extParams={"floatwin":"1","preheatTipsVer":"4","exclusivePrice":"1","stdSizeVids":"","showSellPoint":"3","mclabel":"1","cmpStyle":"1","ic2label":"1","reco":"1","vreimg":"1","couponVer":"v2","live":"1"}&fdc_area_id=103102105112&mars_cid=ace44cff-85cb-37d0-a38a-b7499fa3e70b&mobile_channel=kowd7uq2:::&mobile_platform=3&other_cps=&page_id=page_te_commodity_search_1646140605059&phone_model=Pixel XL&productIds=6919509726012421404,6919653222532612239,6919504313346508736,6919594284198742478,6919507303163532941,6919724272812959428,6919299612550957276,6919665164573995599,6919665282973824340,6919553310211147976&province_id=103102&referer=com.achievo.vipshop.search.activity.VerticalTabSearchProductListActivity&rom=Dalvik/2.1.0 (Linux; U; Android 10; Pixel XL Build/QP1A.191005.007.A3)&scene=search&sd_tuijian=0&session_id=ace44cff-85cb-37d0-a38a-b7499fa3e70b_shop_android_1646136043568&skey=2d30297ff20ec9b7442dc4f3c335abdc&source_app=android&standby_id=kowd7uq2:::&sys_version=29&timestamp=1646140654&user_token=F4B99E10F54F31E68D773637A4FC3D78E92EC915&warehouse=VIP_SH
return_val: 924af08b6533322960ae5a597777084e226cbc7e
args[2]: a84c5883206309ad076deea939e850dc&8cf90dfd9a89070a9e8653d92f57349b924af08b6533322960ae5a597777084e226cbc7e
return_val: a9673ab3c2f64b755a639c9c9e641b2fc6f089ab

image-20220301211901669

文章作者: J
文章链接: http://onejane.github.io/2022/02/28/SO逆向之唯品会api-sign/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏