前言
简单分析{某豆荚}旧版本下载协议,本帖只负责分析交流切勿他用。若侵犯了权益,麻烦管理帮忙删除,谢谢!
工具
Tools |
Vsersion |
样本 |
8.3.1 |
Frida |
16.1.11 |
objection |
1.11.0 |
jadx-gui |
1.2.0 |
调试工具 |
Redmi Note 7 |
抓包工具 |
Charles 4.5.6 |
分析流程
1. 作为一个大部分旧版本APP的聚集地,上次的突然撤回让人很难受, 于是分析了下这里面的参数 然后批量一次性全部下载下来保存。各位下载自己喜欢的app的所有旧版本保存好,避免下次撤回就尴尬了。
2. 老规矩先上抓包,Charles 准备 代{过}{滤}理设置好,搜索一个app名字 点进去 找到历史版本的地方开始抓包
3. data数据包 如下 简单一看差不多都是设备id这些
-
{
"id": -1578021679051554492,
"client": {
"caller": "secret.wdj.client",
"ex": {
"osVersion": 29,
"ch": "wandoujia_sem_default",
"productId": 2011,
"brand": "xiaomi",
"udid": "4058b1fee4ad4c0692b4dd42a7072fd15ec20b17",
"utdid": "ZgOaZ8nmo58DAHHI6Ld5O2tx",
"utoken": "vfsB+fVLPEG1lQKOy68OB7kkwH11MW6I",
"joinTime": 1712807123345,
"aid": "92fd798146787a1c5cf64b9e64cc824b"
},
"versionCode": 803020002, //版本
"VName": "8.3.2.2",
"puid": "0191712815361058610001",
"uuid": "d3MyyXYe7kGvxo8xCQYT8GuUXo9z7raGe7Y3XQe4clnTQHcDTVHKhlLvBuQi6VCCIhFWm9ubWcoqUIPTEXi82zk9B1Q=",
"umid": "WV600011166175d68202106c7fd8675ee",
"recognition": "0_0",
"androidId": "d3MyyW\/UMjRMRPMKSOsC06dap5BuEa+tiejS+t8p1M+jewVb",
"oaid": "d3MyyV7SnjREDnFEU6vOjqILv8mjuf6tk8DSVLi2a7lug7b4"
},
"data": {
"offset": 0,
"appId": 44751, //app 的数字id
"count": 20,
"packageName": "com.dianping.v1",
"page": 1
},
"sign": "d027b1497fb4e705fde21728e7fc8d8c", //貌似有个签名Sign 经过测试不加和错误的并不会返回所需要数据
"encrypt": "md5"
}
5. 可以看到,Data数据udid utdid aid uuid umid androidId oaid
基本都是固定信息了 最主要的是看到一个 "sign": "d027b1497fb4e705fde21728e7fc8d8c",
整个数据包的签名 直接反编译app搜索 sign
6. 跟进去这个方法createRequestHeader.put("sign", generateMD5Key(createRequestData));
@Override // o.h.d.n.a
public byte[] getRequestBytes() { //疑似需要找的签名方法
byte[] bArr;
o.h.g.a.d dVar;
byte[] bArr2 = this.mRequestByteCache;
if (bArr2 != null) {
return bArr2;
}
onRequestStart(this.mArgs);
try {
String str = (String) this.mArgs.get("opt_fields");
if (!TextUtils.isEmpty(str)) {
this.mArgs.remove("opt_fields");
}
Object createRequestData = createRequestData();
JSONObject createRequestHeader = createRequestHeader(createRequestData);
if (isEncryptByM9()) {
createRequestHeader.put("sign", "");
String jSONObject = createRequestHeader.toString();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
GZIPOutputStream gZIPOutputStream = new GZIPOutputStream(byteArrayOutputStream);
gZIPOutputStream.write(jSONObject.getBytes("utf-8"));
gZIPOutputStream.flush();
gZIPOutputStream.close();
byte[] byteArray = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
if (byteArray == null) {
bArr = null;
} else {
o.h.g.a.c cVar = o.h.g.a.c.f;
if (cVar.b.f8191a) {
dVar = cVar.b;
} else {
dVar = cVar.f8189a;
}
bArr = dVar.a(byteArray);
}
} else {
createRequestHeader.put("sign", generateMD5Key(createRequestData)); // 有点明显了 签名地方 继续跟进
createRequestHeader.put(Body.CONST_ENCRYPT, "md5");
if (!TextUtils.isEmpty(str)) {
createRequestHeader.put("optFields", str);
}
bArr = createRequestHeader.toString().getBytes();
}
this.mRequestByteCache = bArr;
return bArr;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
7.继续跟进 generateMD5Key(createRequestData)
方法
public String generateMD5Key(Object obj) {
if (!(obj instanceof JSONObject)) {
return "";
}
JSONObject jSONObject = (JSONObject) obj;
StringBuilder S = o.e.a.a.a.S(sCaller);
for (String str : getSortedKeys(jSONObject)) {
o.e.a.a.a.S0(S, str, "=", getAsString(jSONObject.opt(str))); //把传进来的参数处理 "A=B"
}
return SignNative.getSign(S.toString(), 0); //继续调用方法签名 继续跟进
}
8.跟进 SignNative.getSign(S.toString(), 0)
9.祭出frida 大法 直接hook
java.perform(function () {
var Myclass= Java.use('com.lib.common.SignNative')
Myclass.getSign.implementation = function (a,b) {
send(a)
send(b)
var ret = this.getSign(a,b)
send("ret: "+ret)
return ret;
};
});
10. frida hook住 并且同时抓包也准备好 回到app 点击一下触发 打印参数 sign 结果出来了 与抓包的一致说明参数对了
C:\Users\LengLing\AppData\Local\Programs\Python\Python37-32\python.exe D:\python\wandoujia.py
Hook Start Running
secret.wdj.clientappId=44751count=20offset=0packageName=com.dianping.v1page=1
0
ret: d027b1497fb4e705fde21728e7fc8d8c
========================================
抓包数据:
{
"id": 3768127232815550033,
"client": {
"caller": "secret.wdj.client",
"ex": {
"osVersion": 29,
"ch": "wandoujia_sem_default",
"productId": 2011,
"brand": "xiaomi",
"udid": "4058b1fee4ad4c0692b4dd42a7072fd15ec20b17",
"utdid": "ZgOaZ8nmo58DAHHI6Ld5O2tx",
"utoken": "vfsB+fVLPEG1lQKOy68OB7kkwH11MW6I",
"joinTime": 1712807123345,
"aid": "92fd798146787a1c5cf64b9e64cc824b"
},
"versionCode": 803020002,
"VName": "8.3.2.2",
"puid": "0191712815361058610001",
"uuid": "d3MyyXYe7kGvxo8xCQYT8GuUXo9z7raGe7Y3XQe4clnTQHcDTVHKhlLvBuQi6VCCIhFWm9ubWcoqUIPTEXi82zk9B1Q=",
"umid": "WV600011166175d68202106c7fd8675ee",
"recognition": "0_0",
"androidId": "d3MyyW\/UMjRMRPMKSOsC06dap5BuEa+tiejS+t8p1M+jewVb",
"oaid": "d3MyyV7SnjREDnFEU6vOjqILv8mjuf6tk8DSVLi2a7lug7b4"
},
"data": {
"offset": 0,
"appId": 44751,
"count": 20,
"packageName": "com.dianping.v1",
"page": 1
},
"sign": "d027b1497fb4e705fde21728e7fc8d8c", //发现和hook出来的签名一致
"encrypt": "md5"
}
11.拿到了数据md5结果和hook出来的md5并不一致 说明so层对这个做了处理 要么就是魔改要么就是加盐了。所以必须跟进so层。打开ida64 pro 把so文件扔进去 定位到 JNI_OnLoad
jint __fastcall JNI_OnLoad(JavaVM *vm, void *reserved) //可能这个签名so文件没做处理 比较简单把
{
jint result; // w0
_jclass *v3; // x0
JNIEnv *v4; // [xsp+8h] [xbp-38h] BYREF
__int128 v5; // [xsp+10h] [xbp-30h] BYREF
jstring (__fastcall *v6)(JNIEnv *, jobject, jstring, jint); // [xsp+20h] [xbp-20h]
__int64 v7; // [xsp+28h] [xbp-18h]
v7 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v5 = *(_OWORD *)off_3D90; //方法结构体
v6 = getSign;
if ( vm->functions->GetEnv(vm, (void **)&v4, 65540LL) ) //获取 GetEnv
return -1;
v3 = v4->functions->FindClass(v4, "com/lib/common/SignNative"); //拿到java 类
if ( v4->functions->RegisterNatives(v4, v3, (const JNINativeMethod *)&v5, 1LL) < 0 ) 。//注册方法
result = -1;
else
result = 65542;
return result;
}
12.跟到结构体然后定位到getSign方法
string __fastcall getSign(JNIEnv *env, jobject obj, jstring jstr, jint callerKey) //
{
char *v7; // x21
unsigned int v8; // w0
char *v9; // x22
size_t v10; // x1
unsigned int v11; // w0
unsigned __int64 v12; // x1
const unsigned __int8 *v13; // x2
__int64 v14; // x24
__int128 v16[2]; // [xsp+0h] [xbp-D0h] BYREF
char v17; // [xsp+20h] [xbp-B0h]
unsigned __int8 digest[8]; // [xsp+30h] [xbp-A0h] BYREF
__int64 v19; // [xsp+38h] [xbp-98h]
MD5_CTX context; // [xsp+40h] [xbp-90h] BYREF
__int64 v21; // [xsp+98h] [xbp-38h]
v21 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v7 = (char *)env->functions->GetStringUTFChars(env, jstr, 0LL); //获取传进来的字符串
*(_QWORD *)&context.buffer[56] = 0LL;
*(_OWORD *)&context.buffer[24] = 0u;
*(_OWORD *)&context.buffer[40] = 0u;
*(_OWORD *)context.count = 0u;
*(_OWORD *)&context.buffer[8] = 0u;
*(_OWORD *)context.state = 0u;
MD5Init(&context); // 跟进去发现初始化常量一样 确定是 md5初始化了
0x01,0x23,0x45,0x67,
0x89,0xAB,0xCD,0xEF,
0xFE,0xDC,0xBA,0x98,
0x76,0x54,0x32,0x10
=======================================
context->state[0] = 0x67452301; //常量
context->state[1] = 0xefcdab89;
context->state[2] = 0x98badcfe;
context->state[3] = 0x10325476;
=======================================
v8 = strlen(v7); //获取长度
MD5Update(&context, (unsigned __int8 *)v7, v8); ////开始MD5Update
if ( callerKey == 1 ) //传进来的第二个参数为0 所以不会走这里
{
v9 = "dsfrvvbty55"; //这个好像是要拼接的字符串
v10 = 12LL;
goto LABEL_5;
}
if ( !callerKey ) //必须走这里来了
{
v9 = "LVJd97AbRtikeYRRhi3ocdwSD"; //拼接的字符串 也就是盐
v10 = 26LL;
LABEL_5:
v11 = __strlen_chk(v9, v10); //长度
MD5Update(&context, (unsigned __int8 *)v9, v11); //开始MD5Update
}
*(_QWORD *)digest = 0LL;
v19 = 0LL;
MD5Final(digest, &context); //最后把拼接的参数和盐一起 md5 了
v14 = 0LL; //V14初始化为0
v17 = 0;
v16[0] = 0u;
v16[1] = 0u;
do
sprintf((unsigned __int8 *const)v16, v12, v13, v16, digest[v14++]); //结果格式化
while ( v14 != 16 ); //V14 = 16 跳出
env->functions->ReleaseStringUTFChars(env, jstr, (const unsigned __int8 *)v7); //释放字符串
return env->functions->NewStringUTF(env, v16); //返回32位的md5
}
13.最后用C语言 跑一遍 对比分析结果正确
int main()
{
MD5_CTX md5;
MD5Init(&md5); //同样的初始化md5
unsigned char encrypt1[100]="secret.wdj.clientappId=44751count=20offset=0packageName=com.dianping.v1page=1";
unsigned char encrypt2[100]="LVJd97AbRtikeYRRhi3ocdwSD";
unsigned char decrypt[16];
MD5Update(&md5,encrypt1,strlen((char *)encrypt1));
MD5Update(&md5,encrypt2,strlen((char *)encrypt2));
MD5Final(decrypt,&md5);
for(int i=0;i<16;i++)
printf("%02x",decrypt[i]);
printf("\n");
}
=============================================
C:\Users\LengLing\CLionProjects\untitled\cmake-build-debug\untitled.exe
d027b1497fb4e705fde21728e7fc8d8c //与HOOK出来的结果一致
Process finished with exit code 0
总结
虽然这个app不算很难,分析经验够的话可能到了SO层看到字符串就知道是拼接然后md5了 但是对于经验不足的小白来说还是得慢慢跟进为好,从陌生到熟悉到直接上手一个过程,最后希望大家理解之后, 写个脚本下载自己所需要的旧版本app吧