10.得物app逆向

1. app版本安装

选择的是4.75.5版本
Pasted image 20250508211556

断网绕过更新就行了

2. 抓包分析

小黄鸟抓包即可(需要root)
没有root的用 SocksDroid 转发socks协议代理
Pasted image 20250508212121

然后分析请求头与参数 这里没有请求体

根据抓包分析,我们发现/sns-rec/v1/recommend  地址为首页推荐接口
地址如下:https://app.dewu.com/sns-rec/v1/recommend/all/feed
	-地址:https://app.dewu.com/sns-rec/v1/recommend/all/feed
    -请求方式:get
    -请求头:
    	-X-Auth-Token:必须带
    -请求参数:
    	-newSign:这个接口不需要带,学习破解它,后续别的接口需要带

测试后发现 newSign 是必须带的,其他可以不用带
Pasted image 20250508213609

3. 破解newSign

3.1. 反编译搜索newSign

Pasted image 20250508213852
因为都在同一个类,这里直接看这个类即可。
而且可以发现有关键词 interceptor (拦截器)->本质上:所有请求,都走这个拦截器->所有请求都会带newSign

通过关键代码 host.addQueryParameter("newSign", RequestUtils.c(hashMap2, currentTimeMillis)); 发现调用了 RequestUtils.c
Pasted image 20250508214343
转到对应的声明看一下
Pasted image 20250508214516

3.2. 分析RequestUtils.c核心逻辑

public static synchronized String c(Map<String, String> map, long j2) throws UnsupportedEncodingException {
    synchronized (RequestUtils.class) {
        PatchProxyResult proxy = PatchProxy.proxy(new Object[]{map, new Long(j2)}, null, changeQuickRedirect, true, 6612, new Class[]{Map.class, Long.TYPE}, String.class);
        if (proxy.isSupported) {
            return (String) proxy.result;
        } else if (map == null) {
            return "";
        } else {
            // 1 参数拼接
            // 把uuid,platform,v,loginToken,timestamp放入map中
            map.put("uuid", DuHttpConfig.d.getUUID());
            map.put("platform", "android");
            map.put("v", DuHttpConfig.d.getAppVersion());
            map.put("loginToken", DuHttpConfig.d.getLoginToken());
            map.put("timestamp", String.valueOf(j2));
            ArrayList arrayList = new ArrayList(map.entrySet());
            // 2 把map转成ArrayList,并进行排序
            Collections.sort(arrayList, new Comparator<Map.Entry<String, String>>() { 
            // 3 构建字符串,循环把key和value取出来,拼接到sb中
            StringBuilder sb = new StringBuilder();
            for (int i2 = 0; i2 < arrayList.size(); i2++) {
                Map.Entry entry = (Map.Entry) arrayList.get(i2);
            //
                sb.append(((String) entry.getKey()) + ((String) entry.getValue()));
            }
            String sb2 = sb.toString();
            DuHttpConfig.LogConfig logConfig = DuHttpConfig.f15800h;
            String str = f16243a;
            logConfig.d(str, "StringToSign " + sb2);
            // 4 执行AESEncrypt.encode 加密
            // 5 把返回结果当参数传入a方法中
            return a(AESEncrypt.encode(DuHttpConfig.f15796c, sb2));
            //6 通过newSign的例子,判断出a方法就是md5加密
        }
    }
}

a方法的代码,md5加密
Pasted image 20250508215144

3.3. Hook-RequestUtils.c确认位置

我们现在要判断一下是不是这个文件与newSign的生成有关

import frida
import sys

rdev = frida.get_remote_device()
session = rdev.attach("得物(毒)")

scr = """
Java.perform(function () {
    var RequestUtils = Java.use("com.shizhuang.duapp.common.utils.RequestUtils");
    RequestUtils.c.implementation = function(map,j){
        console.log("----------------------------------------");
        console.log('1.参数字典为:',map); // 此处直接打印map,发现打印的是对象,我们需要转换一下
        console.log('1.参数字典为:',JSON.stringify(map));  // 查看一下类型 :<instance: java.util.Map, $className: java.util.HashMap>,把HashMap值取出来,做个转换,如下
        var Map = Java.use('java.util.HashMap');
        var obj = Java.cast(map, Map);
        console.log('1.参数字典为:',obj.toString());
        var res = this.c(map,j);
        console.log("4.newSign结果:", 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()

/*
'''

### 参数字典为: 
{abValue=1, deliveryProjectId=0, abRectagFengge=0, abType=social_brand_strategy_v454, limit=20, lastId=, abRecReason=0, abVideoCover=2}

### newSign结果: 
16aa2e250ac6c28c3166cce3d0561be8


### 拿到newSign和 抓包抓包的newSign比较发现是一样的,确定位置
'''

*/

执行后刷新一下页面即可
Pasted image 20250508215731
而且这个hook得到的与我们抓包抓到的是一样的
Pasted image 20250508215838

3.4. 分析Request.c执行流程

// 1 参数拼接
// 把uuid,platform,v,loginToken,timestamp放入map中
map.put("uuid", DuHttpConfig.d.getUUID());
map.put("platform", "android");
map.put("v", DuHttpConfig.d.getAppVersion());
map.put("loginToken", DuHttpConfig.d.getLoginToken());
map.put("timestamp", String.valueOf(j2));

//2 把map转成ArrayList,并进行排序
ArrayList arrayList = new ArrayList(map.entrySet());
Collections.sort(arrayList, new Comparator<Map.Entry<String, String>>()

// 3 构建字符串
StringBuilder sb = new StringBuilder();
sb.append()

// 4 执行AESEncrypt.encode 加密 
AESEncrypt.encode(DuHttpConfig.f15796c, sb2)

// 5 把返回结果当参数传入a中
a(AESEncrypt.encode(DuHttpConfig.f15796c, sb2));

// a函数就是md5加密

核心就是分析 AESEncrypt.encode()方法

3.5. AESEncrypt.encode()分析

查看 encode 声明
Pasted image 20250508220523
这里是第一个

 public static String encode(Object obj, String str) {
        return (String) NCall.IL(new Object[]{2, obj, str});
    }

然后看一下 NCall.IL
Pasted image 20250508220648

public class NCall {
    static {
        System.loadLibrary("GameVMP");
    }
    public static native Object IL(Object[] objArr);
}

这里实际上是进行了一个VMP加固
VMP加固(虚拟软件保护技术)大概思路就是自定义一套虚拟机指令和对应的解释器,并将标准的指令转换成自己的指令,然后由解释器将自己的指令给对应的解释器。由于兼容性和效率等问题,所以VMP一般只用于关键函数

3.6. 判断新老逻辑是否改变

这里就到头了,我们回头看一下 AESEncrypt.encode 类代码
Pasted image 20250508221447

/* loaded from: classes.dex */
public class AESEncrypt {
    static {
    // 实际是是在加载so文件---》又调用了jni方法实现的
    // 这种方式使用 VMP加固--》隐藏掉了java本来的代码
    // 本来应该是:System.loadLibrary("JNIEncrypt");
        NCall.IV(new Object[]{0});
    }
    public static String encode(Object obj, String str) {
        return (String) NCall.IL(new Object[]{2, obj, str});
    }
    public static native String encodeByte(byte[] bArr, String str);
    public static native String getByteValues();


//5 4.74.5版本AESEncrypt类如下
public class AESEncrypt {
    static {
        System.loadLibrary("JNIEncrypt");
            }
    public static String encode(Object obj, String str) {
        String byteValues = getByteValues();
        StringBuilder sb = new StringBuilder(byteValues.length());
        for (int i2 = 0; i2 < byteValues.length(); i2++) {
            if (byteValues.charAt(i2) == '0') {
                sb.append('1');
            } else {
                sb.append('0');
            }
        }
        return encodeByte(str.getBytes(), sb.toString());
    }
    public static native String encodeByte(byte[] bArr, String str);
    public static native String getByteValues();
}

通过老版本分析 :执行encode流程--》先调用:getByteValues--》再调用encodeByte,这个从新版本是看不出来的

   
然后我们要确定,新版本还是这个流程--》通过hook确认

import frida
import sys
rdev = frida.getremotedevice()
session = rdev.attach("得物(毒)")
scr = """
Java.perform(function () {
   var RequestUtils = Java.use("com.shizhuang.duapp.common.utils.RequestUtils");
   RequestUtils.c.implementation = function(map,j){
       console.log("-----------开始-----------------------------");
       var Map = Java.use('java.util.HashMap');
       var obj = Java.cast(map, Map);
       console.log('1.参数字典为:',obj.toString());
       var res = this.c(map,j);  // 内部调了--》encode--》getBytesValue--》
encodeByte
       console.log("6.newSign结果:", res);
       console.log("-----------结束-----------------------------");
       return res;
   }
   // hook---getByteValues
   var AESEncrypt = Java.use("com.duapp.aesjni.AESEncrypt");
   AESEncrypt.getByteValues.implementation = function(){
       var res = this.getByteValues();
       console.log('2.getByteValues返回值是:',res);
       return res;
   }
   // hook-encodeByte
   AESEncrypt.encodeByte.implementation = function(bArr,str){
       console.log('3.encodeByte-参数bArr是:',bArr);
       console.log('4.encodeByte-参数str是:',str);
       var res = this.encodeByte(bArr,str);
       console.log('5.encodeByte返回值是:',res);
       return res;
   }
   
   
});
"""
script = session.createscript(scr)
def onmessage(message, data):
   print(message, data)
script.on("message", on_message)
script.load()
sys.stdin.read()

Pasted image 20250508223459
可以发现老版本的逻辑还是可以用的

通过hook确认了,新版本的执行逻辑还是跟老版本执行逻辑是一样的,然后我们基于老版本分析--》老版本没有vmp加密

public class AESEncrypt {
    static {
        System.loadLibrary("JNIEncrypt");
    }
    public static String encode(Object obj, String str) {
        // 1 执行jni方法getByteValues--》返回了字符串
        // 一堆01010110固定的
        String byteValues = getByteValues();
        StringBuilder sb = new StringBuilder(byteValues.length());
        for (int i2 = 0; i2 < byteValues.length(); i2++) {
            if (byteValues.charAt(i2) == '0') {
                sb.append('1');
            } else {
                sb.append('0');
            }
        }
        // 2 上述代码是把一堆01010110固定的 取反
        // 2.1 str是RequestUtils.c中的:sb2
        // 2.2 sb.toString 是一堆10101010---》getByteValues()返回值取了反

        // 3 encodeByte 是jni方法---》整体的加密是用so加密
        // 3.1 so文件:libJNIEncrypt.so
        return encodeByte(str.getBytes(), sb.toString());
    }
    public static native String encodeByte(byte[] bArr, String str);
    public static native String getByteValues();
}

3.7. 分析hook得到的数据

Pasted image 20250508224946

-----------开始-----------------------------
1.参数字典为: {abValue=1, deliveryProjectId=0, abRectagFengge=0, abType=social_brand_strategy_v454, limit=20, lastId=, abRecReason=0, abLiveEntranceClose=0, abVideoCover=2}

2.getByteValues返回值是: 101001011101110101101101111100111000110100010101010111010001000101100101010010010101110111010011101001011101110101100101001100110000110100011101010111011011001101001101011101010100001101000011

3.encodeByte-参数bArr是: 97,98,76,105,118,101,69,110,116,114,97,110,99,101,67,108,111,115,101,48,97,98,82,101,99,82,101,97,115,111,110,48,97,98,82,101,99,116,97,103,70,101,110,103,103,101,48,97,98,84,121,112,101,115,111,99,105,97,108,95,98,114,97,110,100,95,115,116,114,97,116,101,103,121,95,118,52,53,52,97,98,86,97,108,117,101,49,97,98,86,105,100,101,111,67,111,118,101,114,50,100,101,108,105,118,101,114,121,80,114,111,106,101,99,116,73,100,48,108,97,115,116,73,100,108,105,109,105,116,50,48,108,111,103,105,110,84,111,107,101,110,112,108,97,116,102,111,114,109,97,110,100,114,111,105,100,116,105,109,101,115,116,97,109,112,49,55,52,54,55,49,52,56,50,54,54,50,50,117,117,105,100,101,51,51,52,50,101,57,55,48,97,56,49,48,56,54,55,118,52,46,55,53,46,53

4.encodeByte-参数str是: 010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100

5.encodeByte返回值是: 9tJX+w1shRuN3zryp4iAEDmdxT3kxfhu9LcLAWOYRGiQ3XG9XpJeHWaGVLm+Om3uC8r76v9XMpyS1hhggss2qtDgj4FE7aLO/UvBThkuSpkiIA0c2GsH9wChSqVm0eCQE1Z0YKSryWnJau1Xp99X4jaMfps52MgpnBKNklzvArEorlHc5Z99DPaAZaBd4yoXSQmvWNAqcj4PMeEIhMtJJr8UHHlskm8zWV/rFALUhHg4Cs4gqmp9krHBQEeckRUoRQZ+347RnoVMs6fUiwmdxg==

6.newSign结果: 263bdc5ecde6e077e98cabf2f110ffa8
-----------结束-----------------------------

java的bytes数组--》转成字符串看一下:

def java_array_to_string(arr):
    """
    将Java字节数组或int数组转为字符串
    :param arr: 例如 [65, 66, 67]
    :return: 字符串 "ABC"
    """
    return ''.join([chr(x) for x in arr])

java_arr = [97,98,76,105,118,101,69,110,116,114,97,110,99,101,67,108,111,115,101,48,97,98,82,101,99,82,101,97,115,111,110,48,97,98,82,101,99,116,97,103,70,101,110,103,103,101,48,97,98,84,121,112,101,115,111,99,105,97,108,95,98,114,97,110,100,95,115,116,114,97,116,101,103,121,95,118,52,53,52,97,98,86,97,108,117,101,49,97,98,86,105,100,101,111,67,111,118,101,114,50,100,101,108,105,118,101,114,121,80,114,111,106,101,99,116,73,100,48,108,97,115,116,73,100,108,105,109,105,116,50,48,108,111,103,105,110,84,111,107,101,110,112,108,97,116,102,111,114,109,97,110,100,114,111,105,100,116,105,109,101,115,116,97,109,112,49,55,52,54,55,49,52,56,50,54,54,50,50,117,117,105,100,101,51,51,52,50,101,57,55,48,97,56,49,48,56,54,55,118,52,46,55,53,46,53]
result = java_array_to_string(java_arr)
print(result) 

#abLiveEntranceClose0abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp1746714826622uuide3342e970a810867v4.75.5

newSign加密的本质

  • 通过参数010101和loginTokenplatformandroidtimestamp....
  • 执行:encodeByte 加密后得到的结果
  • jni方法--》硬核破解so文件才能知道如何加密的

3.8. 破解so文件(libJNIEncrypt.so)

1.使用IDA打开:libJNIEncrypt.so,点击exports
查看是动态注册还是静态注册
Pasted image 20250508225205

2.发现没有java_开发的方法,又有JNI_OnLoad说明是【动态注册】

3. java中:encodeByte 和c中方法的对应关系在 :JNI_OnLoad中对应的

4. 点击JNI_OnLoad--》按F5--》查看源代码
Pasted image 20250508225607

    -v4 = ("com/duapp/aesjni/AESEncrypt"); # 找到类
    -(v3, v4, off_15010, 8LL)  # 对应关系:off_15010

5 看对应关系:off_15010
Pasted image 20250508225825

  • java中的方法:encodeByte--》返回字符串
  • 根据签名:(byte[] bArr, String str)-->([BLjava/lang/String;)Ljava/lang/String;
  • 对应c的方法:encode

6 双击来到encode查看

  • 引入头文件
    Pasted image 20250508230206
    Pasted image 20250508230248
  • 函数的第一个参数,转成 JNIEnv_
    Pasted image 20250508230538
    Pasted image 20250508230548

转换后的代码

jstring __fastcall encode(JNIEnv_ *a1, __int64 a2, struct _jobject *a3, struct _jobject *a4)
{
  const char *v7; // x22
  __int64 Value; // x24
  unsigned int v9; // w25
  jbyte *v10; // x23
  jbyte *v11; // x0
  jbyte *v12; // x26
  __int64 v13; // x9
  jbyte *v14; // x10
  jbyte *v15; // x11
  __int64 v16; // x8
  jbyte v17; // t1
  const char *v18; // x24
  __int128 *v20; // x10
  _OWORD *v21; // x11
  __int64 v22; // x12
  __int128 v23; // q0
  __int128 v24; // q1

  v7 = a1->functions->GetStringUTFChars(a1, a4, 0LL);
  Value = getValue();
  v9 = a1->functions->GetArrayLength((JNIEnv *)a1, a3);
  v10 = a1->functions->GetByteArrayElements(a1, a3, 0LL);
  v11 = (jbyte *)malloc(v9 + 1);
  v12 = v11;
  if ( (int)v9 >= 1 )
  {
    if ( v9 <= 0x1F || v11 < &v10[v9] && v10 < &v11[v9] )
    {
      v13 = 0LL;
LABEL_6:
      v14 = &v11[v13];
      v15 = &v10[v13];
      v16 = v9 - v13;
      do
      {
        v17 = *v15++;
        --v16;
        *v14++ = v17;
      }
      while ( v16 );
      goto LABEL_8;
    }
    v13 = v9 & 0x7FFFFFE0;
    v20 = (__int128 *)(v10 + 16);
    v21 = v11 + 16;
    v22 = v9 & 0xFFFFFFE0;
    do
    {
      v23 = *(v20 - 1);
      v24 = *v20;
      v20 += 2;
      v22 -= 32LL;
      *(v21 - 1) = v23;
      *v21 = v24;
      v21 += 2;
    }
    while ( v22 );
    if ( v13 != v9 )
      goto LABEL_6;
  }
LABEL_8:
  v11[v9] = 0;
  v18 = (const char *)AES_128_ECB_PKCS5Padding_Encrypt(v11, Value);
  free(v12);
  a1->functions->ReleaseStringUTFChars((JNIEnv *)a1, a4, v7);
  a1->functions->ReleaseByteArrayElements((JNIEnv *)a1, a3, v10, 0LL);
  return a1->functions->NewStringUTF(a1, v18);
}

7 代码如下

jstring __fastcall encode(JNIEnv_ *a1, __int64 a2, __int64 a3, __int64 a4)
{
    # ...不用看
    # 2 v18是通过函数AES_128_ECB_PKCS5Padding_Encrypt执行,传入的v8得到的
    # 3 v8本质是传入的待加密的字符串:java传入的bytes数组
    v18 = (const char *)AES_128_ECB_PKCS5Padding_Encrypt(v11, v8);
    # 1 返回了字符串--》v18 就是返回的字符串--》c的字节数组---》v18是怎么来的
    return a1->functions->NewStringUTF(a1, v18);
}

8 AES_128_ECB_PKCS5Padding_Encrypt 是如何加密的--》双击进入
Pasted image 20250508230953

__int64 __fastcall AES_128_ECB_PKCS5Padding_Encrypt()
{
    return AES_128_ECB_PKCS5Padding_Encrypt();
}

9 AES_128_ECB_PKCS5Padding_Encrypt
Pasted image 20250508231118

__int64 __fastcall AES_128_ECB_PKCS5Padding_Encrypt(JNIEnv_ *a1, __int64 a2)
{
    # 1 循环执行 AES128_ECB_encrypt
    do
    {
        AES128_ECB_encrypt(v28, a2, v29);
        v29 += 16;
        --v27;
        v28 += 16;
    }
    while (v27);
    # 2 使用base64编码了
    v30 = b64_encode(v26, v24);
    # 3 返回了
    return v30;
}

9 猜测并验证

  • 加密方案是,把传入的字节数组--》使用aes加密--》把加密结果使用base64编码
  • 跟咱们hook到的结果是一样的,使用base64

10 拿到aes的key,使用python验证,看结果是否一样,就能判断

11 通过hook得到-AES_128_ECB_PKCS5Padding_Encrypt(v11, Value)
Pasted image 20250508231400
这里有两个参数

  • 一个是aes的key
  • 一个是待加密的明文

3.9. hook-AES_128_ECB_PKCS5Padding_Encrypt

import frida
import sys

rdev = frida.get_remote_device()
session = rdev.attach("得物(毒)")

scr = """
Java.perform(function () {
    // 1. 找到so文件libJNIEncrypt.so,第二个参数是要hook的函数名,返回值是函数的内存地址
    var addr_func = Module.findExportByName("libJNIEncrypt.so", "AES_128_ECB_PKCS5Padding_Encrypt");
    // 2. 传入要hook的函数内存地址
    Interceptor.attach(addr_func, {
        onEnter: function(args){
            console.log("--------------------------执行函数--------------------------");
            console.log("参数1-v11:", args[0].readUtf8String());
            console.log("参数2-v8:", args[1].readUtf8String());
        },
        onLeave: function(retValue){
            console.log("返回值newSign在md5之前的值:", retValue.readUtf8String());
        }
    });
});
"""

script = session.create_script(scr)

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

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

Pasted image 20250508232134
执行后发现 value的值一直没有变
那么大概率 V11就是明文 value就是aes-key

--------------------------执行函数--------------------------
参数1-v11: abLiveEntranceClose0abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp1746717601707uuide3342e970a810867v4.75.5
参数2-value: d245a0ba8d678a61
返回值newSign在md5之前的值: 9tJX+w1shRuN3zryp4iAEDmdxT3kxfhu9LcLAWOYRGiQ3XG9XpJeHWaGVLm+Om3uC8r76v9XMpyS1hhggss2qtDgj4FE7aLO/UvBThkuSpkiIA0c2GsH9wChSqVm0eCQE1Z0YKSryWnJau1Xp99X4jaMfps52MgpnBKNklzvArEorlHc5Z99DPaAZaBd4yoXSQmvWNAqcj4PMeEIhMtJJsVWNwoInkglJS9nGe8DdIflDGcTWJrt7Pb00Bk8ENlpRQZ+347RnoVMs6fUiwmdxg==

3.10. python实现aes加密

from Crypto.Cipher import AES  
from Crypto.Util.Padding import pad  
import base64  
  
def aes_encrypt(data_string):  
    key = "d245a0ba8d678a61"  
    aes = AES.new(  
        key=key.encode('utf-8'),  
        mode=AES.MODE_ECB,  
    )  
    raw = pad(data_string.encode('utf-8'), 16)  
    return aes.encrypt(raw)  
  
data_string = (  
    "abLiveEntranceClose0abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp1746717601707uuide3342e970a810867v4.75.5"  
  
)  
  
res = aes_encrypt(data_string)  
value = base64.b64encode(res)  # 推荐用b64encode,输出不会有换行  
print(value.decode())

'''  
hook得到的:9tJX+w1shRuN3zryp4iAEDmdxT3kxfhu9LcLAWOYRGiQ3XG9XpJeHWaGVLm+Om3uC8r76v9XMpyS1hhggss2qtDgj4FE7aLO/UvBThkuSpkiIA0c2GsH9wChSqVm0eCQE1Z0YKSryWnJau1Xp99X4jaMfps52MgpnBKNklzvArEorlHc5Z99DPaAZaBd4yoXSQmvWNAqcj4PMeEIhMtJJsVWNwoInkglJS9nGe8DdIflDGcTWJrt7Pb00Bk8ENlpRQZ+347RnoVMs6fUiwmdxg==  
加密脚本得到的:9tJX+w1shRuN3zryp4iAEDmdxT3kxfhu9LcLAWOYRGiQ3XG9XpJeHWaGVLm+Om3uC8r76v9XMpyS1hhggss2qtDgj4FE7aLO/UvBThkuSpkiIA0c2GsH9wChSqVm0eCQE1Z0YKSryWnJau1Xp99X4jaMfps52MgpnBKNklzvArEorlHc5Z99DPaAZaBd4yoXSQmvWNAqcj4PMeEIhMtJJsVWNwoInkglJS9nGe8DdIflDGcTWJrt7Pb00Bk8ENlpRQZ+347RnoVMs6fUiwmdxg==  
发现一模一样  
'''

把上述base64使用md5加密---得到结果是---》newSign---》跟抓包抓到的是一样的

import hashlib

md5 = hashlib.md5()
md5.update(
    b'9tJX+w1shRuN3zryp4iAEDmdxT3kxfhu9LcLAWOYRGiQ3XG9XpJeHWaGVLm+Om3uC8r76v9XMpyS1hhggss2qtDgj4FE7aLO/UvBThkuSpkiIA0c2GsH9wChSqVm0eCQE1Z0YKSryWnJau1Xp99X4jaMfps52MgpnBKNklzvArEorlHc5Z99DPaAZaBd4yoXSQmvWNAqcj4PMeEIhMtJJsVWNwoInkglJS9nGe8DdIflDGcTWJrt7Pb00Bk8ENlpRQZ+347RnoVMs6fUiwmdxg== '
)
print(md5.hexdigest())

#eaa1e29430ccfeb9eee40678f3043b13 输出

3.11. 最终--newSign的生成方案

1 把请求参数+固定的 中排序转成字符串---》使用aes+key加密---》使用base64编码---》使用md5签名得到

真正的加密参数,不是请求参数,是请求参数+uuid....

2 请求参数
{abValue=1, deliveryProjectId=0, abRectagFengge=0, abType=social_brand_strategy_v454, limit=20, lastId=, abRecReason=0, abLiveEntranceClose=0, abVideoCover=2}

3 请求参数再加入

  • uuid
  • platform
  • v
  • loginToken
  • timestamp

3.12. 破解uuid--随机生成imei

0 待加密字符串
abLiveEntranceClose0abRecReason0abRectagFengge0abTypesocialbrandstrategy_v454abValue1abVideoCover2deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp1746717601707uuide3342e970a810867v4.75.5

1 请求参数
abLiveEntranceClose0abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0lastIdlimit20

2 固定的key--》value时间戳会变--》uuid咱们不知道
loginToken
platform  android
timestamp  1746717601707
uuid   e3342e970a810867
v   4.75.5

3 破解uuid
Pasted image 20250508233633

map.put("uuid", DuHttpConfig.d.getUUID())

4 点进去,看不懂--》美团热修复类--》真正生成uuid的位置并不是这里

public String getUUID() {
    PatchProxyResult proxy = PatchProxy.proxy(new Object[0], this, 
changeQuickRedirect, false, 5131, new Class[0], String.class);
    return proxy.isSupported ? (String) proxy.result : "";
}

Pasted image 20250508234154
Pasted image 20250508234247

5 直接搜
hashMap.put("uuid",  # 很多-->其中有真正uuid生成到的位置
在破解X-Auth-Token时,也有uuid---》猜这个uuid应该是一样的
Pasted image 20250508234507

6 hashMap.put("uuid", HPDeviceInfo.b(BaseApplication.c()).a((Activity) null));
Pasted image 20250508234548

// 先从内存中拿,没有用a()生成
    public String a(Activity activity) {
        String str = this.f14858b;
        if (str != null) {
            return str;
        }
        if (activity != null) {
            final TelephonyManager telephonyManager = (TelephonyManager) activity.getApplication().getSystemService("phone");
            new RxPermissions(activity).c("android.permission.READ_PHONE_STATE").subscribe(new Consumer() { // from class: g.c.a.a.f.m
                @Override // io.reactivex.functions.Consumer
                public final void accept(Object obj) {
                    HPDeviceInfo.this.a(telephonyManager, (Boolean) obj);
                }
            });
        } else {
            this.f14858b = a();
        }
        return this.f14858b;
    }

7 a()-->imei-->随机生成即可
Pasted image 20250508234739

public String a() {
    return Settings.Secure.getString(
        this.f14857a.getContentResolver(), 
        "android_id"
    );
}

3.13. python生成newSign

import time
import requests
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from urllib.parse import quote_plus
import base64
import json
import random
import copy

def create_android_id():
    data_list = []
    for i in range(1, 9):
        part = "".join(random.sample("0123456789ABCDEF", 2))
        data_list.append(part)
    return "".join(data_list).lower()

def md5(data_bytes):
    hash_object = hashlib.md5()
    hash_object.update(data_bytes)
    return hash_object.hexdigest()

def aes_encrypt(data_string):
    key = "d245a0ba8d678a61"
    aes = AES.new(
        key=key.encode('utf-8'),
        mode=AES.MODE_ECB,
    )
    raw = pad(data_string.encode('utf-8'), 16)
    return aes.encrypt(raw)

uid = create_android_id()
ctime = str(int(time.time() * 1000))

reply_param_dict = {
    "lastId": "",
    "limit": "20",
}

new_dict = copy.deepcopy(reply_param_dict)
new_dict.update(
    {
        "loginToken": "",
        "platform": "android",
        "timestamp": str(int(time.time() * 1000)),
        "uuid": uid,
        "v": "4.75.5"
    }
)
ordered_string = "".join(["{}{}".format(key, new_dict[key]) for key in sorted(new_dict.keys())])

aes_string = aes_encrypt(ordered_string)
aes_string = base64.encodebytes(aes_string)
aes_string = aes_string.replace(b"\n", b"")
sign_string = md5(aes_string)
print(sign_string)

4. 破解X-Auth-Token

X-Auth-Token 就是jwt的token
Pasted image 20250508235444

Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NDY3MTAyMTEsImV4cCI6MTc3ODI0NjIxMSwiaXNzIjoiZTMzNDJlOTcwYTgxMDg2NyIsInN1YiI6ImUzMzQyZTk3MGE4MTA4NjciLCJ1dWlkIjoiZTMzNDJlOTcwYTgxMDg2NyIsInVzZXJJZCI6MjU2MzUwNTM4NiwidXNlck5hbWUiOiLlvpfniallci1WMkMySjdWNiIsImlzR3Vlc3QiOnRydWV9.p0e-QMPLqWXslAi04Z2GTT4MqUPWhGDCovW9dmNTurTq_eLMcYAa7WeJFyITZnahnVOm0rZGP_qJxfB32hE59rZvFs0jSIou1SrvbhMo8Yb9fdqXXMp9HHYrcLWQtNDJhnz-hTb2dXO1udIA6CAvMtitQWAAWmagl-SAriZt7r0JOumXAJ5UstuiOVe3xI9NpYKvUEImtBdTTGERbqQMDYx4sucWgmNA2GvbPKxWvcYU7C6fxQRQoA4Y8mML9RwzB-EJfjs46xhtr1ExyP-9vUtAeKHzhCoY6TpCnhw9gUareXEl19mzdDdDKIg7FLopT_oT1CbV27Nk6PuRDj7e9w

4.1. jwt是什么

# 1 请求头中:X-Auth-Token
    X-Auth-Token    Bearer 
# 2 搜之前--》分析---》如果做过后端开发--》就知道--》这个东西就是jwt到的token
    -用户登录后---》服务端返回的三段式,每段使用base64编码--》里面放了用户信息
    -我们app没登录--》app第一次启动--》向后端发送请求--》后端返回的--》未登录用户的token
# 3 jwt的构成
# 第一部分:头--header--》这个token的加密方式--》固定一般不变
eyJhbGciOiJSUzI1NiJ9
# 第二部分:荷载--payload--》用户信息,签发时间,过期时间。。--》未登录用户
eyJpYXQiOjE3MjkwODAzODEsImV4cCI6MTc2MDYxNjM4MSwiaXNzIjoiZWUxMzg4NWU2OGQ3NmVkNCIsI nN1YiI6ImVlMTM4ODVlNjhkNzZlZDQiLCJ1dWlkIjoiZWUxMzg4NWU2OGQ3NmVkNCIsInVzZXJJZCI6Mj E0MTY4MTg1MywidXNlck5hbWUiOiLlvpfniallci1KMEQ4UzZHNSIsImlzR3Vlc3QiOnRydWV9.

# 第三部分:签名--》签名是由第一部分和第二部分签名得到--》是用来做jwt校验的--》校验这个jwt有没
有被别人篡改过
C_2WaciMJlyGCLUVsjdQ7Tt_sy_1aUQ0w-tH0DIHRWDRyPnpjyxC7Sqsaeg0PQ_sOWDp2UDWfnQhFdpjL9pTca5nl80r95S8hX8APLH3eEL56rIACgy lwJzANaIILwgggqqoXfoukq4t857RbJx4spgchPRDXSh9VibcvhWVi1oB1nQYsmqVrJM1CIIJf5b4XRBR mhS38CU1B_ckraYBsCfaKS1u6vDv5SGoDdrH9h1e-HQEWMvUISWMUd1TNM64tEJAyGPeoWvCNpURXFtH_rQ-fMj1aAWrKhiY5dBbSOJR5VwWyAn-CSTMK8ya7MangbUXHmdyc9QrB4vVdpcqtg

# 4 分别解出第一部分和第二部分看看内容

import base64
# res=base64.b64decode('eyJhbGciOiJSUzI1NiJ9') # b'{"alg":"RS256"}'
res=base64.b64decode('eyJpYXQiOjE3MjkwODAzODEsImV4cCI6MTc2MDYxNjM4MSwiaXNzIjoiZWU xMzg4NWU2OGQ3NmVkNCIsInN1YiI6ImVlMTM4ODVlNjhkNzZlZDQiLCJ1dWlkIjoiZWUxMzg4NWU2OGQ3 NmVkNCIsInVzZXJJZCI6MjE0MTY4MTg1MywidXNlck5hbWUiOiLlvpfniallci1KMEQ4UzZHNSIsImlzR 3Vlc3QiOnRydWV9') # b'{"alg":"RS256"}'
# 
{"iat":1729080381,"exp":1760616381,"iss":"ee13885e68d76ed4","sub":"ee13885e68d76e d4","uuid":"ee13885e68d76ed4","userId":2141681853,"userName":"\xe5\xbe\x97\xe7\x8 9\xa9er-J0D8S6G5","isGuest":true}'print(res.decode('utf-8'))

# 5 获得token---》服务端返回的--》找那个请求包返回即可
    -1 清空app数据
    -2 打开抓包--》打开app
    -3 找到是哪个请求返回的

找到是 getVisitorUserId 获取到 X-Auth-Token
Pasted image 20250509000200

4.2. 获取token

import time  
import requests  
import hashlib  
from Crypto.Cipher import AES  
from Crypto.Util.Padding import pad  
import base64  
import random  
  
def create_android_id():  
    data_list = []  
    for i in range(1, 9):  
        part = "".join(random.sample("0123456789ABCDEF", 2))  
        data_list.append(part)  
    return "".join(data_list).lower()  
  
def md5(data_bytes):  
    hash_object = hashlib.md5()  
    hash_object.update(data_bytes)  
    return hash_object.hexdigest()  
  
def aes_encrypt(data_string):  
    key = "d245a0ba8d678a61"  
    aes = AES.new(  
        key=key.encode('utf-8'),  
        mode=AES.MODE_ECB,  
    )  
    raw = pad(data_string.encode('utf-8'), 16)  
    return aes.encrypt(raw)  
  
uid = create_android_id()  
ctime = str(int(time.time() * 1000))  
  
param_dict = {  
    "loginToken": "",  
    "platform": "android",  
    "timestamp": ctime,  
    "uuid": uid,  
    "v": "4.75.5"  
}  
  
ordered_string = "".join(["{}{}".format(key, param_dict[key]) for key in sorted(param_dict.keys())])  
aes_string = aes_encrypt(ordered_string)  
aes_string = base64.encodebytes(aes_string)  
aes_string = aes_string.replace(b"\n", b"")  
sign = md5(aes_string)  
param_dict['newSign'] = sign  
  
res = requests.post(  
    url="https://app.dewu.com/api/v1/app/user_core/users/getVisitorUserId",  
    headers={  
        "duuuid": uid,  
        "duimei": "",  
        "duplatform": "android",  
        "appId": "duapp",  
        "timestamp": ctime,  
        'duv': '4.75.5',  
        'duloginToken': '',  
        'dudeviceTrait': 'Redmi',  
        'shumeiid': '20250509000001237a863bce0546c6b4661034f075ff9c005f94ed751fcf7a',  
        'User-Agent': 'duapp/4.75.5(android;11)'  
    },  
    json=param_dict,  
)  
  
print(res.headers)  
x_auth_token = res.headers.get('X-Auth-Token')  
print(x_auth_token)

Pasted image 20250509000455

5. 代码整合

import time
import requests
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import random
import urllib3
import copy

urllib3.disable_warnings()

def create_android_id():
    data_list = []
    for i in range(1, 9):
        part = "".join(random.sample("0123456789ABCDEF", 2))
        data_list.append(part)
    return "".join(data_list).lower()

def md5(data_bytes):
    hash_object = hashlib.md5()
    hash_object.update(data_bytes)
    return hash_object.hexdigest()

def aes_encrypt(data_string):
    key = "d245a0ba8d678a61"
    aes = AES.new(
        key=key.encode('utf-8'),
        mode=AES.MODE_ECB,
    )
    raw = pad(data_string.encode('utf-8'), 16)
    return aes.encrypt(raw)

uid = create_android_id()
ctime = str(int(time.time() * 1000))

param_dict = {
    "loginToken": "",
    "platform": "android",
    "timestamp": ctime,
    "uuid": uid,
    "v": "4.75.5"
}

ordered_string = "".join(["{}{}".format(key, param_dict[key]) for key in sorted(param_dict.keys())])
aes_string = aes_encrypt(ordered_string)
aes_string = base64.encodebytes(aes_string)
aes_string = aes_string.replace(b"\n", b"")
sign = md5(aes_string)
param_dict['newSign'] = sign

res = requests.post(
    url="https://app.dewu.com/api/v1/app/user_core/users/getVisitorUserId",
    headers={
        "duuuid": uid,
        "duimei": "",
        "duplatform": "android",
        "appId": "duapp",
        "timestamp": ctime,
        'duv': '4.75.5',
        'duloginToken': '',
        'dudeviceTrait': 'Pixel+2+XL',
        'shumeiid': '202308011759568af1c8fc75c211e7f876664d9493202d0055aeeb3dd6e38c',
        'User-Agent': 'duapp/4.75.5(android;11)'
    },
    json=param_dict,
    verify=False
)
x_auth_token = res.headers['X-Auth-Token']

reply_param_dict = {
    "lastId": "1",
    "limit": "20",
}

new_dict = copy.deepcopy(reply_param_dict)
new_dict.update({
    "loginToken": "",
    "platform": "android",
    "timestamp": str(int(time.time() * 1000)),
    "uuid": uid,
    "v": "4.75.5"
})
ordered_string = "".join(["{}{}".format(key, new_dict[key]) for key in sorted(new_dict.keys())])
aes_string = aes_encrypt(ordered_string)
aes_string = base64.encodebytes(aes_string)
aes_string = aes_string.replace(b"\n", b"")
sign_string = md5(aes_string)
reply_param_dict['newSign'] = sign_string

res = requests.get(
    url="https://app.dewu.com/sns-rec/v1/recommend/all/feed/",
    params=reply_param_dict,
    headers={
        "X-Auth-Token": x_auth_token,
        'User-Agent': 'duapp/4.75.5(android;11)'
    },
    verify=False
)
print(res.text)

Pasted image 20250509000658