采集得物首页信息

采集得物首页信息

绕过更新

先通过断网绕过强制更新

抓包分析

因为该apk禁止使用代理抓包,我们这里使用SocksDroid抓包

先关闭手机代理,再配置SocksDroid的IP和端口,这里实践过程中打开SocksDroid就无法正确联网,我们还是直接使用Reqable

JSON在线解析格式化验证 - JSON.cn

因为不是post请求,所以没有请求体。我们要主要找的是文本GET请求。

我们发现请求头中的X-Auth-Token内容不能随意删除

反编译破解

newSign

查找观察发现可疑的几个参数在都在同一个类中

我们观察发现该代码是在Intercept下完成,而在 Android 正向开发中,Intercept(拦截) 是一种核心设计模式,用于在关键流程中插入自定义逻辑,实现非侵入式的功能增强。

okhttp的Interceptor拦截器源码解析 - 简书

21.安卓逆向2-frida hook技术-HookOkHttp的拦截器-CSDN博客

微信公众平台

了解Intercept基础后就可以看出,该代码是一个 OkHttp 网络拦截器,它的主要目的是在网络请求发出之前,动态地修改请求,特别是添加或替换签名参数(newSign
OkHttp源码解析-CSDN博客

深入浅出 OkHttp 源码解析及应用实践 - vivo互联网技术 - 博客园

在拦截器中,那么所有请求都会带有这个newSign。观察代码猜测走RequestUtils.c

c代码分析

我们进行hook定位测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scr = """
Java.perform(function () {
let RequestUtils = Java.use("com.shizhuang.duapp.common.utils.RequestUtils");
RequestUtils["c"].implementation = function (map, j2) {
console.log(`RequestUtils.c is called: map=${map}, j2=${j2}`);
console.log(`参数字典查看类型: Json.stringify(map)=${JSON.stringify(map)}, j2=${j2}`);
//转字符串
var mapStr = Java.use("org.json.JSONObject").$new(map).toString();
console.log(`RequestUtils.c mapStr=${mapStr}`);
let result = this["c"](map, j2);
console.log(`RequestUtils.c result=${result}`);
return result;
};
});
"""

我们可以发现hook到的值与抓包得到的相同,可以确定位置正确,我们接着分析。

RequestUtils.c is called: map=[object Object], j2=1762780383093 参数字典查看类型: Json.stringify(map)="<instance: java.util.Map, $className: java.util.HashMap>", j2=1762780383093 RequestUtils.c mapStr={"abValue":"1","deliveryProjectId":"0","abRectagFengge":"0","abType":"social_brand_strategy_v454","limit":"20","lastId":"","abRecReason":"0","abVideoCover":"2"} RequestUtils.c result=448c8c4512de202320745beaddd5fe4f

结合代码对比可以发现就是将这些参数加密后得到的newSign。那么我们就进一步分析一下该代码。

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
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;
}
if (map == null) {
return "";
}
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));
//把map转成ArrayList
ArrayList arrayList = new ArrayList(map.entrySet());
//排序
Collections.sort(arrayList, new Comparator<Map.Entry<String, String>>() { // from class: com.shizhuang.duapp.common.utils.RequestUtils.1
public static ChangeQuickRedirect changeQuickRedirect;

@Override // java.util.Comparator
/* renamed from: a, reason: merged with bridge method [inline-methods] */
public int compare(Map.Entry<String, String> entry, Map.Entry<String, String> entry2) {
PatchProxyResult proxy2 = PatchProxy.proxy(new Object[]{entry, entry2}, this, changeQuickRedirect, false, 6618, new Class[]{Map.Entry.class, Map.Entry.class}, Integer.TYPE);
return proxy2.isSupported ? ((Integer) proxy2.result).intValue() : entry.getKey().toString().compareTo(entry2.getKey());
}
});
//拼接字符串,循环ArrayList拼接到字符串中
StringBuilder sb = new StringBuilder();
for (int i2 = 0; i2 < arrayList.size(); i2++) {
Map.Entry entry = (Map.Entry) arrayList.get(i2);
//把key和Value拼接到字符串中
sb.append(((String) entry.getKey()) + ((String) entry.getValue()));
}
String sb2 = sb.toString();
DuHttpConfig.f15802h.d(f16245a, "StringToSign " + sb2);
//执行了AESEncrypt.encode把sb2,拼接后的字符串进行了加密,然后又执行了a加密。
return a(AESEncrypt.encode(DuHttpConfig.f15798c, sb2));
}
}
  • 代码详解

    1. 热修复防护层(Robust 框架)

    PatchProxyResult proxy = PatchProxy.proxy(..., changeQuickRedirect, true, 6612, ...); if (proxy.isSupported) { return (String) proxy.result; }

    • 作用:线上热更新拦截,若存在补丁则直接返回补丁结果
    • 逆向影响:Hook 时必须绕过,否则会执行混淆后的补丁逻辑
    • 绕过方法:强制返回 isSupported = false

    2. 线程安全与空值保护

    public static synchronized String c(...) { synchronized (RequestUtils.class) { ... } } if (map == null) { return ""; }

    • 双保险同步:静态方法锁 + 类对象锁,防止并发签名冲突
    • 防御性编程:空 map 直接返回空字符串,避免 NPE

    3. 参数注入(签名盐值)

    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));

    • 注入时机:在原始参数基础上动态追加,顺序影响签名结果
    • 关键参数timestamp 使用时间戳,防重放攻击
    • 配置来源:全部从 DuHttpConfig.d 单例获取,需动态提取

    4. 字典序排序(签名核心)

    Collections.sort(arrayList, (entry, entry2) -> entry.getKey().toString().compareTo(entry2.getKey()) );

    • 排序规则:严格按 key 字符串升序排列
    • 逆向要点:必须与服务器端排序逻辑完全一致
    • 潜在坑点:不同语言的字典序实现差异(如 Java vs Python)

    5. 字符串拼接

    sb.append(((String) entry.getKey()) + ((String) entry.getValue()));

    • 格式key1value1key2value2... 无分隔符
    • 示例abValue1platformandroidtimestamp1762780383093...
    • 长度风险:超长参数可能导致签名串溢出(需服务端支持)

    6. AES 加密层

    AESEncrypt.encode(DuHttpConfig.f15798c, sb2)

    • 密钥DuHttpConfig.f15798c(通常为 16 字节字符串)
    • 模式:需动态分析确认(ECB/CBC/PKCS5Padding)
    • 输出:Base64 编码字符串(进入下一步 MD5)

    7. 最终 MD5 哈希

    return a(AESEncrypt.encode(...));

    • 结果特征:32 位十六进制小写字符串
    • 算法MD5(Base64(AES(plain)))

加密a代码分析

跟进代码发现a做的md5加密

  • 代码详解

    1. 热修复防护(Robust)

    PatchProxyResult proxy = PatchProxy.proxy(..., changeQuickRedirect, true, 6616, ...); if (proxy.isSupported) { return (String) proxy.result; }

    • 方法编号:6616(与 c() 方法的 6612 对应)
    • 绕过策略:Hook PatchProxy.proxy 强制返回 isSupported=false

    2. MD5 计算核心

    MessageDigest messageDigest = MessageDigest.getInstance("MD5"); messageDigest.update(str.getBytes()); *// 默认使用 UTF-8 编码*byte[] digest = messageDigest.digest();

    • 哈希对象:标准 Java MessageDigest MD5 实例
    • 输入编码str.getBytes() 使用系统默认编码(Android 默认为 UTF-8)
    • 输出:16 字节二进制数组

    3. 字节转十六进制字符串

    StringBuilder sb = new StringBuilder(); for (byte b2 : digest) { String hexString = Integer.toHexString(b2 & 255); *// 转无符号整数*while (hexString.length() < 2) { hexString = "0" + hexString; *// 补零保证 2 位*} sb.append(hexString); }

    • 位运算b2 & 255 将字节转为无符号整数(0-255)
    • 补零逻辑:确保每个字节对应 2 位十六进制(如 000
    • 结果:32 位小写十六进制字符串

    二、完整签名算法链确认

    结合 RequestUtils.c()a()最终算法为:

    String sign = MD5( Base64( AES_Encrypt( DuHttpConfig.f15798c, *// AES 密钥* 拼接字符串 *// 排序后的 key=value 无分隔符拼接*) ) )

  • 魔改测试

    要确认 a() 方法中的 MD5 是否魔改(自定义),需要动态+静态双重验证。以下是系统性检测方案:


    一、动态验证:标准 MD5 对比法

    Frida 脚本:实时比对

    Java.perform(function () { const RequestUtils = Java.use('com.shizhuang.duapp.common.utils.RequestUtils'); const MessageDigest = Java.use('java.security.MessageDigest'); *// Hook a() 方法*RequestUtils.a.overload('java.lang.String').implementation = function(input) { console.log('\n[══════════════════════════════════════]'); console.log('[+] a() 方法被调用'); console.log('[+] 输入: ' + input); *// 调用原始方法*const appResult = this.a(input); console.log('[+] App MD5结果: ' + appResult); *// 使用标准Java MD5计算*const standardMD5 = calculateStandardMD5(input); console.log('[+] 标准MD5结果: ' + standardMD5); *// 对比*const isModified = (appResult !== standardMD5); console.log('[!] 是否魔改: ' + isModified); if (isModified) { console.log('[!!!] 发现魔改MD5!'); *// 打印调用栈定位魔改点*console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); } else { console.log('[✓] MD5未被修改,为标准实现'); } console.log('[══════════════════════════════════════]\n'); return appResult; }; *// 标准MD5计算函数*function calculateStandardMD5(str) { try { const md = MessageDigest.getInstance('MD5'); md.update(str.getBytes('UTF-8')); const digest = md.digest(); const sb = []; for (let i = 0; i < digest.length; i++) { let hex = (digest[i] & 0xff).toString(16); while (hex.length < 2) hex = '0' + hex; sb.push(hex); } return sb.join(''); } catch (e) { console.log('标准MD5计算失败: ' + e); return ''; } } });

    验证逻辑:对相同输入,对比 App 输出和标准 Java MD5 输出,完全一致则未魔改


    二、静态分析:字节码验证

    1. 检查 MD5 初始化

    使用 JADXGDA 反编译,确认:

    `*// ✅ 标准实现(无魔改)*MessageDigest.getInstance(“MD5”)

    // ❌ 可疑魔改迹象MessageDigest.getInstance(“MD5”, customProvider) // 自定义ProviderMyCustomMD5.getInstance(“MD5”) // 调用自定义类`

    2. 检查是否有额外处理

    a() 方法中搜索:

    • & 0xff& 255 → 标准位运算
    • 额外位移、异或 → 可能魔改
    • 调用 native 方法 → 需分析 so 文件

    3. 使用 ByteCode Viewer 检查

    查看 a() 方法的 JVM 字节码,确认:

    • 是否调用 java.security.MessageDigest
    • 是否有插入 INVOKESTATIC 调用其他工具类
    • 是否有 LOOKUPSWITCHTABLESWITCH 做分支篡改

    三、边界测试:用已知输入输出验证

    测试用例设计

    `// Frida 主动注入测试Java.perform(function () {
    const RequestUtils = Java.use(‘com.shizhuang.duapp.common.utils.RequestUtils’);

      *// 绕过 Robust*bypassRobust();
      
      *// 标准测试向量*const testCases = [
          "",                                    *// 空字符串*"a",                                   *// 单字符*"abc",                                 *// RFC 1321 标准测试*"message digest",                      *// 较长字符串*"abcdefghijklmnopqrstuvwxyz",          *// 全字母*"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", *// 混合*"12345678901234567890123456789012345678901234567890123456789012345678901234567890" *// 超长*];
      
      testCases.forEach((input, i) => {
          console.log(`\n[测试用例 ${i+1}] 输入: "${input}"`);
          const appResult = RequestUtils.a(input);
          console.log(`App结果: ${appResult}`);
          
          *// 对比标准MD5(已知值)*const standard = knownMD5(input);
          console.log(`标准结果: ${standard}`);
          console.log(`状态: ${appResult === standard ? '✅ 通过' : '❌ 失败'}`);
      });
    

    });

    function knownMD5(str) {
    // 使用已知的标准MD5值或在线计算// 例如: “” → d41d8cd98f00b204e9800998ecf8427e// 此处省略实现,建议用Python验证return “”;
    }

    function bypassRobust() {
    const PatchProxy = Java.use(‘com.meituan.robust.PatchProxy’);
    PatchProxy.proxy.implementation = function() {
    const result = Java.use(‘com.meituan.robust.PatchProxyResult’).$new();
    result.setIsSupported(false);
    return result;
    };
    }`


    四、Native 层深度检查

    确认是否调用 BouncyCastle 等第三方库

    Java.perform(function () { *// Hook MessageDigest.getInstance 看实际返回的 Provider*const MessageDigest = Java.use('java.security.MessageDigest'); MessageDigest.getInstance.overload('java.lang.String').implementation = function(algorithm) { const md = this.getInstance(algorithm); console.log([+] MD5 Provider: ${md.getProvider().getName()}`);
    return md;
    };

      *// 检查 Provider 列表*const Security = Java.use('java.security.Security');
      const providers = Security.getProviders();
      providers.forEach(p => {
          console.log(`Provider: ${p.getName()} - ${p.getInfo()}`);
      });
    

    });`

    关注点

    • Provider 名称AndroidOpenSSL(标准) vs BC(BouncyCastle)
    • Native 库libjavacrypto.so(标准) vs libbcprov.so(可能魔改)

    五、Hook MessageDigest 本身

    终极验证:监控 MD5 计算过程

    `Java.perform(function () {
    const MessageDigest = Java.use(‘java.security.MessageDigest’);

      *// Hook update 方法看输入数据*MessageDigest.update.overload('[B').implementation = function(input) {
          console.log('[+] MD5.update() 调用');
          console.log('[+] 数据: ' + JSON.stringify(Array.from(input)));
          return this.update(input);
      };
      
      *// Hook digest 方法看输出*MessageDigest.digest.overload().implementation = function() {
          const result = this.digest();
          console.log('[+] MD5.digest() 调用');
          console.log('[+] 输出: ' + JSON.stringify(Array.from(result)));
          return result;
      };
      
      *// Hook a() 方法*const RequestUtils = Java.use('com.shizhuang.duapp.common.utils.RequestUtils');
      RequestUtils.a.implementation = function(str) {
          const result = this.a(str);
          console.log('\n[完整流程]');
          console.log('[+] a() 输入字符串: ' + str);
          console.log('[+] a() 输出MD5: ' + result);
          return result;
      };
    

    });`

    验证逻辑

    • 如果 update() 接收到的数据与 a() 的输入 str 一致
    • digest() 输出字节数组转十六进制后与 a() 返回值一致
    • 则 100% 确认 MD5 未被魔改

    六、完整性检查清单

    检查项 方法 预期结果 如果异常说明
    输入一致性 Hook update() 输入 = a() 的 str 参数 传入数据被篡改
    输出一致性 Hook digest() 输出转 hex = a() 返回值 MD5 算法被替换
    Provider getProvider() AndroidOpenSSL/BC 使用自定义 Provider
    字节码 JADX 分析 无额外方法调用 插入了混淆/魔改代码
    边界测试 RFC 1321 测试用例 与标准值一致 算法实现有差异
    Native 调用 Frida Module.findExportByName 无自定义 so 调用 MD5 在 native 层魔改

    七、快速验证命令

    `# 1. 启动 Frida 并运行上述 Hook 脚本
    frida -U -f com.shizhuang.duapp -l verify_md5.js –no-pause

    # 2. 触发一次网络请求(或手动调用)# 在 Frida 控制台输入:# RequestUtils.a(“test”)# 3. 对比标准 MD5echo -n “test” | md5sum # Linux# 或 Python
    python -c “import hashlib; print(hashlib.md5(b’test’).hexdigest())”`

    如果 App 输出 098f6bcd4621d373cade4e832627b4f6(标准值),则确认无魔改


    八、结论

    美团/得物 App 的 a() 方法 MD5 实现是标准的,理由:

    1. 直接调用 MessageDigest.getInstance("MD5")(官方 API)
    2. 字节转十六进制逻辑与标准实现一致
    3. 无自定义 Provider 或额外处理
    4. 输出格式为标准 32 位小写 MD5

    99% 的可能是未魔改,因为:

    • 维护成本:魔改 MD5 需同步修改服务端,增加维护复杂度
    • 安全性:AES 密钥和签名流程已足够安全,无需魔改哈希算法
    • 兼容性:标准 MD5 保证跨平台一致性

    最终确认:运行上述 Frida 对比脚本,若输入输出与标准 MD5 一致,即可100%定论

分析AESEncrypt.encode加密算法

观察代码可以发现

1
2
3
4
5
6
7
8
public class AESEncrypt {
static {
try {
System.loadLibrary("JNIEncrypt");
} catch (UnsatisfiedLinkError e2) {
e2.fillInStackTrace();
}
}

encode()

AES代码逻辑明显在so层JNIEncrypt文件中,我们先向下查看我们的encode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static String encode(Object obj, String str) {
String byteValues = getByteValues(); //调用getByteValues方法获得字符串
StringBuilder sb = new StringBuilder(byteValues.length());
//定义了一个字符串根据byteValues的值拼接并按位取反。
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());
//参数 1:明文字节数组(UTF-8 编码)
//参数 2:处理后的 "01" 字符串(作为动态密钥或置换表)
}

而关键的getByteValues()encodeByte()都是Native方法。

在so层分析之前我们可以先hook一下encode确认,需要注意的是这个类下encode是重载方法,我们hook的是两个参数类型。所以我们需要overload传参数签名进行处理。

1
2
3
4
5
6
7
8
9
10
11
scr = """
Java.perform(function() {
let AESEncrypt = Java.use("com.duapp.aesjni.AESEncrypt");
AESEncrypt["encode"].overload('java.lang.Object', 'java.lang.String').implementation = function (obj, str) {
console.log(`AESEncrypt.encode is called: obj=${obj}, str=${str}`);
let result = this["encode"](obj, str);
console.log(`AESEncrypt.encode result=${result}`);
return result;
};
});
"""

我们发现每次下滑可以得到两个参数和返回值

简单分解一下就可以发现,上面的str字符串更符合我们上边分析的str字符拼接。

getByteValues()

接着我们进入libJNIEncrypt.so中分析一下getByteValues()encodeByte(),而getByteValues() 没有参数直接返回字符串大概率固定,我们可以直接hook验证一下。

1
2
3
4
5
6
7
8
9
10
11
scr = """
Java.perform(function() {
let AESEncrypt = Java.use("com.duapp.aesjni.AESEncrypt");
AESEncrypt["getByteValues"].implementation = function () {
console.log(`AESEncrypt.getByteValues is called`);
let result = this["getByteValues"]();
console.log(`AESEncrypt.getByteValues result=${result}`);
return result;
};
});
"""

我们多次验证可以发现每次结果一致101001011101110101101101111100111000110100010101010111010001000101100101010010010101110111010011101001011101110101100101001100110000110100011101010111011011001101001101011101010100001101000011

不过我们要注意上面分析的时候用的值对getByteValues()进行了取反

encodeByte()

我们先找到对应的so文件进行反编译

静态注册:java_包名_类名_方法名
动态注册:JNI_onLoad中找对应关系

我们进入就看到了JNI_OnLoad,进入观察确实是我们要找的逻辑部分,我们进一步分析其对应关系

安卓逆向_6 — ndk开发jni、jni静态注册、jni_onload动态注册_android jni开发-CSDN博客

提示已经很明显对应关系就是这个地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
if ( (*vm)->GetEnv(vm, (void **)v6, 65540) )
return -1;
v3 = v6[0];
v4 = (*(__int64 (__fastcall **)(_QWORD, const char *))(*(_QWORD *)v6[0] + 48LL))(v6[0], "com/duapp/aesjni/AESEncrypt");
if ( v4 )
(*(void (__fastcall **)(__int64, __int64, char **, __int64))(*(_QWORD *)v3 + 1720LL))(
v3,
v4,
off_15010, // "checkSignature"
8);
__android_log_print(6, "native-jni", "jni onload2");
return n65540;
}

我们进入查看找到我们需要分析的encodeByte()

我们进一步进入查看

emm……长这样子的话,一看就知道要改一下参数

修改后代码如下

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
__int64 __fastcall encode(const struct JNINativeInterface *JNIEnv_, void **jobject, jobject *jstring, __int64 a4)
{
__int64 v7; // x22
__int64 Value; // x24
unsigned int n0x1F; // w25
unsigned __int64 v10; // x23
_OWORD *ptr; // x0
void *ptr_1; // x26
__int64 n0x1F_1; // x9
_BYTE *v14; // x10
char *v15; // x11
__int64 v16; // x8
char v17; // t1
__int64 v18; // x24
__int128 *v20; // x10
_OWORD *v21; // x11
__int64 n0x1F_2; // x12
__int128 v23; // q0
__int128 v24; // q1

v7 = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, __int64, _QWORD))JNIEnv_->reserved0 + 169))(
JNIEnv_,
a4,
0);
Value = getValue();
n0x1F = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, jobject *))JNIEnv_->reserved0 + 171))(
JNIEnv_,
jstring);
v10 = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, jobject *, _QWORD))JNIEnv_->reserved0 + 184))(
JNIEnv_,
jstring,
0);
ptr = malloc((int)(n0x1F + 1));
ptr_1 = ptr;
if ( (int)n0x1F >= 1 )
{
if ( n0x1F <= 0x1F || (unsigned __int64)ptr < v10 + n0x1F && v10 < (unsigned __int64)ptr + n0x1F )
{
n0x1F_1 = 0;
LABEL_6:
v14 = (char *)ptr + n0x1F_1;
v15 = (char *)(v10 + n0x1F_1);
v16 = n0x1F - n0x1F_1;
do
{
v17 = *v15++;
--v16;
*v14++ = v17;
}
while ( v16 );
goto LABEL_8;
}
n0x1F_1 = n0x1F & 0xFFFFFFE0;
v20 = (__int128 *)(v10 + 16);
v21 = ptr + 1;
n0x1F_2 = n0x1F_1;
do
{
v23 = *(v20 - 1);
v24 = *v20;
v20 += 2;
n0x1F_2 -= 32;
*(v21 - 1) = v23;
*v21 = v24;
v21 += 2;
}
while ( n0x1F_2 );
if ( n0x1F_1 != n0x1F )
goto LABEL_6;
}
LABEL_8:
*((_BYTE *)ptr + (int)n0x1F) = 0;
v18 = AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);
free(ptr_1);
(*((void (__fastcall **)(const struct JNINativeInterface *, __int64, __int64))JNIEnv_->reserved0 + 170))(
JNIEnv_,
a4,
v7);
(*((void (__fastcall **)(const struct JNINativeInterface *, jobject *, unsigned __int64, _QWORD))JNIEnv_->reserved0
+ 192))(
JNIEnv_,
jstring,
v10,
0);
return (*((__int64 (__fastcall **)(const struct JNINativeInterface *, __int64))JNIEnv_->reserved0 + 167))(
JNIEnv_,
v18);
}
  • 对该代码进行简单分析

    1. JNI字符串处理

    1
    2
    3
    4
    5
    6
    7
    8
    v7 = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, __int64, _QWORD))JNIEnv_->reserved0 + 169))(
    JNIEnv_, a4, 0);// GetStringUTFChars 获取输出字符串

    n0x1F = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, jobject *))JNIEnv_->reserved0 + 171))(
    JNIEnv_, jstring);// GetStringLength 获取输入字符串长度

    v10 = (*((__int64 (__fastcall **)(const struct JNINativeInterface *, jobject *, _QWORD))JNIEnv_->reserved0 + 184))(
    JNIEnv_, jstring, 0);// GetStringUTFChars 获取输入字符串内容

    JNI函数映射

    • reserved0 + 169 = GetStringUTFChars (输出字符串)
    • reserved0 + 171 = GetStringLength
    • reserved0 + 184 = GetStringUTFChars (输入字符串)

    2. 内存分配和复制优化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ptr = malloc((int)(n0x1F + 1));// 分配输入字符串缓冲区(+1用于null终止符)// 内存复制优化逻辑:if (n0x1F >= 1) {
    if (n0x1F <= 0x1F || 内存重叠检查) {
    // 小数据或内存重叠:使用逐字节复制
    n0x1F_1 = 0;
    do {
    v17 = *v15++;// 逐字节复制*v14++ = v17;
    } while (v16);
    } else {
    // 大数据(>31字节):使用128位向量化复制
    n0x1F_1 = n0x1F & 0xFFFFFFE0;// 32字节对齐do {
    v23 = *(v20 - 1);// 加载128位
    v24 = *v20;// 加载128位*(v21 - 1) = v23;// 存储128位*v21 = v24;// 存储128位} while (n0x1F_2);
    }
    }

    3. AES加密调用

    1
    v18 = AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);// 调用AES加密

    4. 资源清理和返回

    1
    2
    3
    4
    free(ptr_1);// 释放输入缓冲区// 释放JNI字符串(*((void (__fastcall **)(const struct JNINativeInterface *, __int64, __int64))JNIEnv_->reserved0 + 170))(
    JNIEnv_, a4, v7);// ReleaseStringUTFChars (输出)(*((void (__fastcall **)(const struct JNINativeInterface *, jobject *, unsigned __int64, _QWORD))JNIEnv_->reserved0 + 192))(
    JNIEnv_, jstring, v10, 0);// ReleaseStringUTFChars (输入)// 创建返回字符串return (*((__int64 (__fastcall **)(const struct JNINativeInterface *, __int64))JNIEnv_->reserved0 + 167))(
    JNIEnv_, v18);// NewStringUTF

我们可以发现加密字符串为V18,而与V18相关的关键代码为 v18 = AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);我们进入继续查看。

好乱,我么直接从后向前分析一下

很明显是通过AES按位加密后又进行了一次base64编码。

到这里,我们可以尝试hook一下AES_128_ECB_PKCS5Padding_Encrypt(ptr, Value);的参数。

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
scr = """
Java.perform(function() {
// 返回libJNIEncrypt.so中AES_128_ECB_PKCS5Padding_Encrypt的内存地址
var addr_func = Module.findExportByName('libJNIEncrypt.so', 'AES_128_ECB_PKCS5Padding_Encrypt');

if (addr_func) {
console.log("✅ 找到 AES_128_ECB_PKCS5Padding_Encrypt 函数: " + addr_func);

Interceptor.attach(addr_func, {
onEnter: function(args) {
console.log("=".repeat(70));
console.log("🚀 AES_128_ECB_PKCS5Padding_Encrypt 被调用!");
console.log("=".repeat(70));

// 读取参数
try {
if (args[0] && !args[0].isNull()) {
var param1 = args[0].readUtf8String();
console.log("📥 参数1 (输入数据): " + param1);
} else {
console.log("📥 参数1: null");
}

if (args[1] && !args[1].isNull()) {
// 参数2可能是密钥指针,尝试读取
try {
var param2 = args[1].readUtf8String();
console.log("📥 参数2 (密钥): " + param2);
} catch (e) {
console.log("📥 参数2 (密钥指针): " + args[1]);
}
} else {
console.log("📥 参数2: null");
}
} catch (e) {
console.log("❌ 读取参数失败: " + e);
}
},
onLeave: function(retval) {
console.log("\\n📤 函数返回:");
try {
if (retval && !retval.isNull()) {
var result = retval.readUtf8String();
console.log("✅ 返回值: " + result);
} else {
console.log("❌ 返回值为空");
}
} catch (e) {
console.log("❌ 读取返回值失败: " + e);
}
console.log("=".repeat(70) + "\\n");
}
});
} else {
console.log("❌ 未找到 AES_128_ECB_PKCS5Padding_Encrypt 函数");
}
});
"""

通过结果进行简单分析可以判断,因为参数2是固定的,所以猜测为AES的key值,根据之前的分析可以知道base64返回值再经过md5加密是我们抓包得到的值。我们通过python验证一下。

结果一致说明返回值正确。

我们尝试使用重写Native方法加密,发现与hook方法一致

分析uuid的获取

在上面的分析中我们已经完成了对字符拼接逻辑和AES加密的还原,但我们拼接中的uuid还不确定

abRecReason 0

abRectagFengge 0

abTypesocial_brand_strategy_v454abValue1abVideoCover2deliveryProjectId0

lastId

limit 20

loginToken

platform android

timestamp 1762868671633

uuid 06940e61ab6bfc52

v 4.74.5

我么回到字符拼接的位置,查找到uuid的方法

我么进入getUUID() 进行查看

emm……误闯天家~看不懂,思密达。扔AI里

这是一个典型的热修复框架实现:

  • PatchProxy.proxy() - 热修复代理调用
  • changeQuickRedirect - 热修复重定向标志
  • 5097 - 方法ID
  • 如果热修复支持,返回修复后的结果,否则返回空字符串

很明显在这里我们没办法看出uuid是什么无法直接调用

  • 爆破

    脚本 1:绕过补丁并获取 UUID

    Java.perform(function () { const DuHttpConfig = Java.use('com.dianping.nova.core.config.DuHttpConfig'); *// 1. 绕过 Robust 框架*const PatchProxy = Java.use('com.meituan.robust.PatchProxy'); PatchProxy.proxy.overload( 'java.lang.Object', 'java.lang.Object', 'com.meituan.robust.ChangeQuickRedirect', 'boolean', 'int', 'java.lang.Class[]', 'java.lang.Class' ).implementation = function(args, instance, changeQuickRedirect, isStatic, methodNumber, paramsClasses, returnClass) { *// 强制返回未命中补丁*const result = Java.use('com.meituan.robust.PatchProxyResult').$new(); result.setIsSupported(false); return result; }; *// 2. Hook getUUID 打印调用栈*DuHttpConfig.getUUID.implementation = function() { const uuid = this.getUUID(); console.log('\n[══════════════════════════════════════]'); console.log('[+] UUID: ' + uuid); *// 打印调用栈,定位首次生成位置*console.log('[+] 调用栈:'); console.log(Java.use("android.util.Log").getStackTraceString( Java.use("java.lang.Exception").$new() )); console.log('[══════════════════════════════════════]\n'); return uuid; }; *// 3. 主动触发调用*const config = DuHttpConfig.d; console.log('[+] 主动获取 UUID: ' + config.getUUID()); });

    脚本 2:动态追踪 UUID 生成源头

    *// 监控所有 UUID 生成相关 API*Java.perform(function () { *// 1. 监控 java.util.UUID.randomUUID()*const UUID = Java.use('java.util.UUID'); UUID.randomUUID.implementation = function() { const uuid = this.randomUUID(); console.log('[!!!] UUID.randomUUID() 生成: ' + uuid.toString()); return uuid; }; *// 2. 监控 SharedPreferences 读取*const SharedPreferences = Java.use('android.content.SharedPreferencesImpl'); SharedPreferences.getString.overload('java.lang.String', 'java.lang.String').implementation = function(key, defValue) { const ret = this.getString(key, defValue); if (key === 'device_uuid' || key.contains('uuid')) { console.log('[!!!] 从 SharedPreferences 读取 UUID: ' + ret); } return ret; }; *// 3. 监控 Settings.Secure.ANDROID_ID*const Secure = Java.use('android.provider.Settings$Secure'); Secure.getString.implementation = function(cr, name) { const ret = this.getString(cr, name); if (name === 'android_id') { console.log('[!!!] ANDROID_ID: ' + ret); } return ret; }; });

这里我们再尝试搜索别的代码uuid看是否可以生成使用。这里我们尝试搜索接下来要破解的X-Auth-Token中的uuid

我们进入a方法进行查看

到这步我们看到telephonyManager.getDeviceId()基本可以确定调用的是设备ID号。

知道uuid是获取手机的 **IMEI(强标识)**后我们对newsign方法进行整合。

  • IMEI

    1. IMEI (International Mobile Equipment Identity)

    • 格式: 15位数字
    • 结构: AA-BBBBBB-CCCCCC-D
    • 示例: 35-209900-176148-1
    • 用途: 全球唯一标识移动设备

    2. MEID (Mobile Equipment Identifier)

    • 格式: 14位十六进制数字
    • 结构: AA-BBBBBB-CCCCCC
    • 用途: CDMA网络设备标识
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 copy
import hashlib
import base64
import random
import time
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import uuid

def create_android_id():
return "".join(random.choices("0123456789abcdef", k=15))

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

def aes_encrypt(plain_text):

key = b"d245a0ba8d678a61"
aes = AES.new(key, AES.MODE_ECB)
raw = pad(plain_text.encode('utf-8'), 16)
return aes.encrypt(raw)

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

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.74.5"
}
)
ordered_string = "".join(["{}{}".format(key, new_dict[key]) for key in sorted(new_dict.keys())])

aes_str = aes_encrypt(ordered_string)
aes_str_base64 = base64.encodebytes(aes_str)
aes_str_base64 = aes_str_base64.replace(b'\n', b'')
sign = md5(aes_str_base64)
print("sign:", sign)

X-Auth-Token破解

三段式,使用.分割,典型的jwt认证中的token

三个部分分别是:

  1. Header(头部)
    • 包含令牌类型(typ: JWT)和签名算法(alg: HS256/RS256等)
    • Base64Url编码的JSON对象
  2. Payload(载荷)
    • 存放声明信息(Claims),如用户ID、角色、过期时间(exp)等
    • Base64Url编码的JSON对象
  3. Signature(签名)
    • 用密钥对header.payload进行签名,防止篡改
    • 确保token完整性和真实性

Bearer

第一段:

eyJhbGciOiJSUzI1NiJ9.

第二段:

eyJpYXQiOjE3NjI5MTk4NTUsImV4cCI6MTc5NDQ1NTg1NSwiaXNzIjoiMDY5NDBlNjFhYjZiZmM1MiIsInN1YiI6IjA2OTQwZTYxYWI2YmZjNTIiLCJ1dWlkIjoiMDY5NDBlNjFhYjZiZmM1MiIsInVzZXJJZCI6MjcwODg1MzE1OSwidXNlck5hbWUiOiLlvpfniallci1YMFo0QzFQNCIsImlzR3Vlc3QiOnRydWV9.

第三段:

tcvtsELx4JTU902UmIlu_wRVD9OWREanN-JYQwozrmuVB8Rmph2bK2bqtUv_QDb7a_wDiuCjqh5E2X-bErq4mdry4FaYuidEHJZB_0hcEyCoyGSMEy0vqxPjMGumbNIvesJ8E_7xjhe-PN05LkNTmizLqFMLrZ0otF1nXYiQwSq_OOTDJoXZqTfBLpnnc0ev8k3-xTJ77Vs150n9MjmSsXj1jBx0_E3Q0QZ756ChAVXjw9mdUkyW5EPBv_MOvl8nqDT0tbZGFTs7DRuho4OrjzDatOymWAWlk4iADEELxbWcL58c3NCSM99wYlKNukK2efloSD2TDQvMHNWsXWLrSA

特点:

  • 无状态,服务端无需存储session
  • 可自包含用户信息
  • 需注意Payload仅编码未加密,不要存放敏感数据
  • 必须配合HTTPS使用,防止中间人攻击

这部分一定是后端返回,我们对第二部分进行解析

我们查找返回接口,之前一致搜素不到意识到应该是初次打开时进行的加载,我们清理缓存重新抓包

我们再响应头里得到接口返回的

Bearer eyJhbGciOiJSUzI1NiJ9.

eyJpYXQiOjE3NjI5MjE0MzUsImV4cCI6MTc5NDQ1NzQzNSwiaXNzIjoiMDY5NDBlNjFhYjZiZmM1MiIsInN1YiI6IjA2OTQwZTYxYWI2YmZjNTIiLCJ1dWlkIjoiMDY5NDBlNjFhYjZiZmM1MiIsInVzZXJJZCI6MjcwODg1MzE1OSwiaXNHdWVzdCI6dHJ1ZX0.

emPQ6kkg2r1qgqxRWVFwmS7oGm6N6rk57YV4svPmFzkaHPg8S7UTJSwP9aG86PCauS-vZ94laR6H_tBPmWOoPlg6RuPvhaW_Uw20mzGBAU-Irc18HJjyY-r3CUDmmPuBncs63aTPP7KMlQ7qjPz0lx5AHDBrslM0B9kugRtGdERzJj1LWmYLasaOSfP0sMS-bPg_lXYNmuJ42Eq68np2grHQqzE3m91_dwLYyI58pPFeGS-zRuvpQkRdpl9PXGBOpCNiOxJSsvuTlHAQhK4var7rRCklpJrA0te8JFrAI0ohTqGB0owd04xF8C1E6SmE48086DuUwDoDposjTdT-6A

可以发现,此后再抓到的包都使用这个接口

exp 是 JWT 标准中的 注册声明(Registered Claim),全称 Expiration Time(过期时间),用于定义 Token 的作废时间点。这是 JWT 安全机制的核心防线。

在此我们相当于找到了token的位置,接下来那就只需要调用这个接口来获取X-Auth-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
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
import hashlibimport base64import timeimport requestsimport osimport jsonfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import padclass DuEnd:    @staticmethod    def encode(s: str) -> str:        """生成加密后的字符串(先AES加密,再MD5)"""        # AES加密        encrypted = AES128ECBEncryptor.encrypt(s, "d245a0ba8d678a61")        print(encrypted)        # 对加密结果取MD5        return DuEnd.md5(encrypted)        @staticmethod    def md5(input_str: str) -> str:        """MD5加密(32位小写)"""        return hashlib.md5(input_str.encode('utf-8')).hexdigest()class AES128ECBEncryptor:    @staticmethod    def encrypt(plaintext: str, key: str) -> str:        """AES-128-ECB + PKCS5Padding + Base64输出"""        if not key or len(key) != 16:            raise ValueError("AES key must be 16 bytes (128 bits).")                # 创建cipher对象        cipher = AES.new(key.encode('utf-8'), AES.MODE_ECB)                # 填充并加密        padded_data = pad(plaintext.encode('utf-8'), AES.block_size)        encrypted = cipher.encrypt(padded_data)                # Base64编码        return base64.b64encode(encrypted).decode('utf-8')def main():    # 生成时间戳    timestamp = int(time.time() * 1000)    print(timestamp)        # 构建原始字符串    raw_value = (f"abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2"                 f"deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp{timestamp}uuid"                 f"78cb5e6d84e95553v4.74.5")    print(raw_value)        # 生成加密结果(通过类名调用静态方法)    result = DuEnd.encode(raw_value)    print(f"最终结果(MD5 of AES): {result}")        # 构建URL(注意:newSign参数应该是result,不是raw_value)    url = (f"https://app.dewu.com/sns-rec/v1/recommend/all/feed?abType=social_brand_strategy_v454& "           f"abValue=1&lastId&limit=20&deliveryProjectId=0&abVideoCover=2&abRectagFengge=0&"           f"abRecReason=0&newSign={result}")        # 设置Headers    headers = {        "User-Agent": "duapp/4.74.5(android;10)",        "Connection": "Keep-Alive",        "Accept-Encoding": "gzip",        "debugable": "0",        "duuuid": "06940e61ab6bfc52",        "duplatform": "android",        "appId": "duapp",        "duchannel": "pp",        "duv": "4.74.5",        "dudeviceTrait": "Pixel+3+XL",        "dudeviceBrand": "google",        "timestamp": str(timestamp),        "shumeiid": "202510231547567439d5ff596ab044275d2022d91746ae00ab839b5fc3279f",        "oaid": "9ff9eb073f34f9ce",        "X-Auth-Token": "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NjI5MjE0MzUsImV4cCI6MTc5NDQ1NzQzNSwiaXNzIjoiMDY5NDBlNjFhYjZiZmM1MiIsInN1YiI6IjA2OTQwZTYxYWI2YmZjNTIiLCJ1dWlkIjoiMDY5NDBlNjFhYjZiZmM1MiIsInVzZXJJZCI6MjcwODg1MzE1OSwidXNlck5hbWUiOiLlvpfniallci1YMFo0QzFQNCIsImlzR3Vlc3QiOnRydWV9.Za-GYQHF7TOJboYTgh3Duoft1c36bx4kGuBbCM22e_4u5luiHYqwVMN5IZnJCxTkEIBeQbyEabWP3_FVegqgRNGxe_Q1JUmjhbzxsMWr2LjTmgGRtwiFFFXwWbHLFYh0GBPhag10xyfcb2TM7mBeYnDRIp2LK5G_5Eg0LzwIT1OKq3kEi8U3NV-478Y7l3j2yzYqADZPeykjKDqgELGp9qEyg3uUsdEdxEHNeT2lfVxjEvz0n6edo2D14peeczbOblblpPNnBVFQPfJ1XjSC8Cw3V7MrwukJ2IjAXYDJNSmDNrckOCHrKL7C-Fiq--farQOQ7NkHJTaVK2oeF56aFg",        "isRoot": "0",        "emu": "0",        "isProxy": "0",        "Cookie": "HWWAFSESTIME=1761206308193; HWWAFSESID=ade964f6bdd017d730b"    }        # 发送请求    response = requests.get(url, headers=headers, timeout=30)        # 直接使用 response.text    print(f"状态码: {response.status_code}")    print(f"响应内容: {response.text[:500]}...")  # 只打印前500字符,避免刷屏        # === 新增:保存到文件 ===    try:        # 获取当前脚本所在目录        current_dir = os.path.dirname(os.path.abspath(__file__))                # 生成文件名:response_<时间戳>.json        filename = f"response_{timestamp}.json"        filepath = os.path.join(current_dir, filename)                # 写入文件(如果响应是JSON则格式化,否则直接保存)        try:            # 尝试解析为JSON并格式化            json_data = json.loads(response.text)            with open(filepath, 'w', encoding='utf-8') as f:                json.dump(json_data, f, ensure_ascii=False, indent=4)        except json.JSONDecodeError:            # 如果不是JSON,直接写入文本            with open(filepath, 'w', encoding='utf-8') as f:                f.write(response.text)                print(f"\n✅ 响应内容已保存到: {filepath}")            except Exception as e:        print(f"\n❌ 保存文件失败: {str(e)}")if __name__ == "__main__":    main()import hashlib
import base64
import time
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

class DuEnd:
@staticmethod
def encode(s: str) -> str:
"""生成加密后的字符串(先AES加密,再MD5)"""
# AES加密
encrypted = AES128ECBEncryptor.encrypt(s, "d245a0ba8d678a61")
print(encrypted)
# 对加密结果取MD5
return DuEnd.md5(encrypted)

@staticmethod
def md5(input_str: str) -> str:
"""MD5加密(32位小写)"""
return hashlib.md5(input_str.encode('utf-8')).hexdigest()

class AES128ECBEncryptor:
@staticmethod
def encrypt(plaintext: str, key: str) -> str:
"""AES-128-ECB + PKCS5Padding + Base64输出"""
if not key or len(key) != 16:
raise ValueError("AES key must be 16 bytes (128 bits).")

# 创建cipher对象
cipher = AES.new(key.encode('utf-8'), AES.MODE_ECB)

# 填充并加密
padded_data = pad(plaintext.encode('utf-8'), AES.block_size)
encrypted = cipher.encrypt(padded_data)

# Base64编码
return base64.b64encode(encrypted).decode('utf-8')

def main():
# 生成时间戳
timestamp = int(time.time() * 1000)
print(timestamp)

# 构建原始字符串
raw_value = (f"abRecReason0abRectagFengge0abTypesocial_brand_strategy_v454abValue1abVideoCover2"
f"deliveryProjectId0lastIdlimit20loginTokenplatformandroidtimestamp{timestamp}uuid"
f"78cb5e6d84e95553v4.74.5")
print(raw_value)

# 生成加密结果(通过类名调用静态方法)
result = DuEnd.encode(raw_value)
print(f"最终结果(MD5 of AES): {result}")

# 构建URL(注意:newSign参数应该是result,不是raw_value)
url = (f"https://app.dewu.com/sns-rec/v1/recommend/all/feed?abType=social_brand_strategy_v454&"
f"abValue=1&lastId&limit=20&deliveryProjectId=0&abVideoCover=2&abRectagFengge=0&"
f"abRecReason=0&newSign={result}")

# 设置Headers
headers = {
"User-Agent": "duapp/4.74.5(android;10)",
"Connection": "Keep-Alive",
"Accept-Encoding": "gzip",
"debugable": "0",
"duuuid": "06940e61ab6bfc52",
"duplatform": "android",
"appId": "duapp",
"duchannel": "pp",
"duv": "4.74.5",
"dudeviceTrait": "Pixel+3+XL",
"dudeviceBrand": "google",
"timestamp": str(timestamp),
"shumeiid": "202510231547567439d5ff596ab044275d2022d91746ae00ab839b5fc3279f",
"oaid": "9ff9eb073f34f9ce",
"X-Auth-Token": "Bearer eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3NjI5MjE0MzUsImV4cCI6MTc5NDQ1NzQzNSwiaXNzIjoiMDY5NDBlNjFhYjZiZmM1MiIsInN1YiI6IjA2OTQwZTYxYWI2YmZjNTIiLCJ1dWlkIjoiMDY5NDBlNjFhYjZiZmM1MiIsInVzZXJJZCI6MjcwODg1MzE1OSwidXNlck5hbWUiOiLlvpfniallci1YMFo0QzFQNCIsImlzR3Vlc3QiOnRydWV9.Za-GYQHF7TOJboYTgh3Duoft1c36bx4kGuBbCM22e_4u5luiHYqwVMN5IZnJCxTkEIBeQbyEabWP3_FVegqgRNGxe_Q1JUmjhbzxsMWr2LjTmgGRtwiFFFXwWbHLFYh0GBPhag10xyfcb2TM7mBeYnDRIp2LK5G_5Eg0LzwIT1OKq3kEi8U3NV-478Y7l3j2yzYqADZPeykjKDqgELGp9qEyg3uUsdEdxEHNeT2lfVxjEvz0n6edo2D14peeczbOblblpPNnBVFQPfJ1XjSC8Cw3V7MrwukJ2IjAXYDJNSmDNrckOCHrKL7C-Fiq--farQOQ7NkHJTaVK2oeF56aFg",
"isRoot": "0",
"emu": "0",
"isProxy": "0",
"Cookie": "HWWAFSESTIME=1761206308193; HWWAFSESID=ade964f6bdd017d730b"
}

# 发送请求
response = requests.get(url, headers=headers, timeout=30)

# 直接使用 response.text
print(f"状态码: {response.status_code}")
print(f"响应内容: {response.text}")

if __name__ == "__main__":
main()


采集得物首页信息
https://cc-nx.github.io/2025/11/12/得物/采集得物首页信息/
作者
CC
发布于
2025年11月12日
许可协议