样本地址: aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzY2NDk1OTEvaGlzdG9yeV92NzgyMDA=
抓包分析
首先肯定是需要先抓一个包,他这个参数也挺多的,主要也就是逆向 anti-token 这个参数

定位的话就直接 hook newStringUTF 吧,这个可以直接得到定位


通过 hook 可以得到它的参数,一个 context 对象,一个时间戳,可以去 unidbg 进行模拟了
至于属于哪个 so,签名是什么的,可以直接去 hook RegisterNative,就不说了
unidbg 模拟
老规矩,先搭个架子
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
|
package com.pinduoduo;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.BaseVM;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VaList;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.IHookZz;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.arm.context.RegisterContext;
import java.io.File;
public class Pdd extends AbstractJni implements IOResolver {
// ========== 每次新项目只改这里 ==========
// 模拟器位数:so在armeabi-v7a目录下填false,arm64-v8a目录下填true
private static final boolean IS_64BIT = false;
// app包名(随意,一般填真实包名)
private static final String PROCESS_NAME = "com.xunmeng.pinduoduo";
// apk路径,不需要apk时填null(填null时LOAD_BY_NAME必须为false)
private static final String APK_PATH = "apks/pdd/pdd.apk";
// so加载方式:
// false = 传文件路径(用SO_PATH),不依赖apk,最常用
// true = 传库名(用SO_NAME),unidbg自动去apk内查找,必须提供APK_PATH
private static final boolean LOAD_BY_NAME = false;
private static final String SO_PATH = "apks/pdd/libpdd_secure.so"; // LOAD_BY_NAME=false时生效
private static final String SO_NAME = "pdd_secure"; // LOAD_BY_NAME=true时生效,不带lib前缀和.so后缀
// 是否打印JNI调用细节,调试时改true
private static final boolean VERBOSE = true;
// ========================================
public static AndroidEmulator emulator;
public static Memory memory;
public static VM vm;
public static Module module;
// 1. 构造方法 —— 初始化模拟器
public Pdd() {
// 1. 创建模拟器(32/64位由IS_64BIT控制)
emulator = IS_64BIT
? AndroidEmulatorBuilder.for64Bit().setProcessName(PROCESS_NAME).build()
: AndroidEmulatorBuilder.for32Bit().setProcessName(PROCESS_NAME).build();
// 2. 获取内存对象
memory = emulator.getMemory();
// 3. 设置安卓SDK版本(只支持19、23)
memory.setLibraryResolver(new AndroidResolver(23));
// 4. 创建虚拟机
vm = APK_PATH != null
? emulator.createDalvikVM(new File(APK_PATH))
: emulator.createDalvikVM();
vm.setJni(this);
vm.setVerbose(VERBOSE);
// 5. 加载so文件(两种方式由LOAD_BY_NAME控制)
DalvikModule dm = LOAD_BY_NAME
? vm.loadLibrary(SO_NAME, false)
: vm.loadLibrary(new File(SO_PATH), false);
// 6. 动态注册才需要执行JNI_OnLoad
dm.callJNI_OnLoad(emulator);
// 7. 获取module对象(后续拿基址、偏移等)
module = dm.getModule();
}
// 5. main方法 —— 右键直接运行
public static void main(String[] args) {
Pdd pdd = new Pdd();
}
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("pathname:" + pathname);
return null;
}
}
|
一开始就报错,这个是关于日志信息的
1
2
3
|
java.lang.UnsupportedOperationException: com/xunmeng/core/log/Logger->i(Ljava/lang/String;Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:708)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:703)
|
日志信息,打不打印都无所谓,直接返回
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "com/xunmeng/core/log/Logger->i(Ljava/lang/String;Ljava/lang/String;)V":{
String arg1 = vaList.getObjectArg(0).getValue().toString();
String arg2 = vaList.getObjectArg(1).getValue().toString();
System.out.println(arg1);
System.out.println(arg2);
return;
}
}
super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
}
|
运行,报错
1
2
3
4
|
java.lang.UnsupportedOperationException: android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
这块通常是跟网络环境相关的,返回 TelephonyManager 这个对象的
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;":{
String arg = varArg.getObjectArg(0).getValue().toString();
System.out.println(arg);
if (arg.equals("phone")){
return vm.resolveClass("android/telephony/TelephonyManager").newObject(null);
}
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
|
运行,报错
1
2
3
4
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getSimState()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:965)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:938)
at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethod(DvmMethod.java:129)
|
这个通常是返回 5,常见有以下取值:
0 = SIM_STATE_UNKNOWN 未知状态。这可能是由于SIM卡正在初始化或发生错误。
1 = SIM_STATE_ABSENT SIM卡不存在。
2 = SIM_STATE_PIN_REQUIRED SIM卡已锁定,需要输入PIN码才能解锁。
3 = SIM_STATE_PUK_REQUIRED SIM卡已被PUK码锁定。
4 = SIM_STATE_NETWORK_LOCKED SIM卡已被网络锁定。
5 = SIM_STATE_READY SIM卡已准备就绪,可以进行通信。
直接返回 5
1
2
3
4
5
6
7
8
9
|
@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "android/telephony/TelephonyManager->getSimState()I":{
return 5;
}
}
return super.callIntMethod(vm, dvmObject, signature, varArg);
}
|
运行,报错
1
2
3
4
5
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getSimOperatorName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:119)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
这个需要返回一个运营商的名称,编一个吧,中国联通
1
2
3
|
case "android/telephony/TelephonyManager->getSimOperatorName()Ljava/lang/String;":{
return new StringObject(vm,"中国联通");
}
|
接着,又报错
1
2
3
4
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getSimCountryIso()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:119)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
|
返回国家的名称,直接"cn"
1
2
3
|
case "android/telephony/TelephonyManager->getSimCountryIso()Ljava/lang/String;":{
return new StringObject(vm,"cn");
}
|
运行,报错
1
2
3
4
5
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkType()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:965)
at com.pinduoduo.Pdd.callIntMethod(Pdd.java:129)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:938)
at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethod(DvmMethod.java:129)
|
这个是返回网络类型,这里直接返回 13,13 是 4G 网络的意思
1
2
3
|
case "android/telephony/TelephonyManager->getNetworkType()I":{
return 13;
}
|
运行,报错
1
2
3
4
5
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkOperator()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:119)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
因为刚才补的是中国联通,这里是,这里返回 46001 即可,这是它的网络标识
1
2
3
|
case "android/telephony/TelephonyManager->getNetworkOperator()Ljava/lang/String;":{
return new StringObject(vm,"46001");
}
|
继续报错
1
2
3
4
5
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkOperatorName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:122)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
直接返回中国联通,之前补过的,继续运行,报错
1
2
3
4
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getDataState()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:965)
at com.pinduoduo.Pdd.callIntMethod(Pdd.java:141)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:938)
|
getDataState 常见的有以下返回值:
- DATA_DISCONNECTED(0):数据未连接
- DATA_CONNECTING(1):数据连接中
- DATA_CONNECTED(2):数据已连接
- DATA_SUSPENDED(3):数据已挂起
直接返回 2
1
2
3
|
case "android/telephony/TelephonyManager->getDataState()I":{
return 2;
}
|
继续报错,继续补
1
2
3
4
5
|
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getDataActivity()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:965)
at com.pinduoduo.Pdd.callIntMethod(Pdd.java:144)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:938)
at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethod(DvmMethod.java:129)
|
这个方法返回当前移动网络的活动状态(是否有数据在收发),它有以下常量:
- DATA_ACTIVITY_NONE(0):没有数据活动
- DATA_ACTIVITY_IN(1):只有下行数据(接收)
- DATA_ACTIVITY_OUT(2):只有上行数据(发送)
- DATA_ACTIVITY_INOUT(3):上下行都有数据
- DATA_ACTIVITY_DORMANT(4):连接存在但休眠
那,直接返回 3 即可
1
2
3
|
case "android/telephony/TelephonyManager->getDataActivity()I":{
return 3;
}
|
继续报错
1
2
3
4
|
java.lang.UnsupportedOperationException: com/xunmeng/pinduoduo/secure/EU->gad()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:438)
at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethodV(DvmMethod.java:59)
|
直接使用的 frida 进行 hook,得到数据 result=d04ead9aadda38cc
1
2
3
4
5
6
7
8
9
|
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "com/xunmeng/pinduoduo/secure/EU->gad()Ljava/lang/String;":{
return new StringObject(vm,"d04ead9aadda38cc");
}
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
|
接着报错
1
2
3
4
5
|
java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethod(AbstractJni.java:181)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethod(AbstractJni.java:176)
at com.github.unidbg.linux.android.dvm.DvmMethod.CallStaticBooleanMethod(DvmMethod.java:179)
at com.github.unidbg.linux.android.dvm.DalvikVM$115.handle(DalvikVM.java:1864)
|
判断系统是否处于被调试状态,直接返回 false
1
2
3
4
5
6
7
8
9
|
@Override
public boolean callStaticBooleanMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "android/os/Debug->isDebuggerConnected()Z":{
return false;
}
}
return super.callStaticBooleanMethod(vm, dvmClass, signature, varArg);
}
|
继续报错,继续补
1
2
3
4
|
java.lang.UnsupportedOperationException: java/lang/Throwable-><init>()V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:753)
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:733)
at com.github.unidbg.linux.android.dvm.DvmMethod.newObject(DvmMethod.java:224)
|
直接把 new 出来的 Throwable 对象的实例传过去
1
2
3
4
5
6
7
8
9
|
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "java/lang/Throwable-><init>()V":{
return vm.resolveClass("java/lang/Throwable").newObject(new Throwable());
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}
|
又报错了
1
2
3
4
5
|
java.lang.UnsupportedOperationException: java/lang/Throwable->getStackTrace()[Ljava/lang/StackTraceElement;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:128)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
这一块看之前龙哥的文章他是用 jnitrace 出来的数据给补上的,但是我发现 jnitrace 这个 app 现在得到的数据好少,也没有StackTraceElement 这个栈的数据,后来又查资料,看到 fashion 哥是这么补的,fashion 哥这么补肯定是有他的道理的
1
2
3
|
case "java/lang/Throwable->getStackTrace()[Ljava/lang/StackTraceElement;":{
return new ArrayObject(vm.resolveClass("java/lang/StackTraceElement").newObject(null),vm.resolveClass("java/lang/StackTraceElement").newObject(null),vm.resolveClass("java/lang/StackTraceElement").newObject(null));
}
|
那我也就这么补了吧
又又又又报错了
1
2
3
4
5
|
java.lang.UnsupportedOperationException: java/lang/StackTraceElement->getClassName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:132)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
我返回空
1
2
3
|
case "java/lang/StackTraceElement->getClassName()Ljava/lang/String;":{
return new StringObject(vm,"");
}
|
接着,报错
1
2
3
4
|
java.lang.UnsupportedOperationException: java/lang/String->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)
|
直接按要求补即可
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature){
case "java/lang/String->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
String strObj = dvmObject.getValue().toString();
String regex = vaList.getObjectArg(0).getValue().toString();
String replace_str = vaList.getObjectArg(1).getValue().toString();
String result = strObj.replaceAll(regex,replace_str);
return new StringObject(vm,result);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
|
又在报错了
1
2
3
4
5
|
java.lang.UnsupportedOperationException: java/io/ByteArrayOutputStream-><init>()V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:753)
at com.pinduoduo.Pdd.newObject(Pdd.java:184)
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:733)
at com.github.unidbg.linux.android.dvm.DvmMethod.newObject(DvmMethod.java:224)
|
马上补,这个感觉是用了什么压缩算法,会不会说 app 也会使用压缩算法,我记得 web 的就是一堆指纹,然后 gzip 压缩之后进行魔改 base64 算法
1
2
3
4
|
case "java/io/ByteArrayOutputStream-><init>()V":{
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
return vm.resolveClass("java/io/ByteArrayOutputStream").newObject(byteArrayOutputStream);
}
|
接着补
1
2
3
4
5
|
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream-><init>(Ljava/io/OutputStream;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:753)
at com.pinduoduo.Pdd.newObject(Pdd.java:189)
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:733)
at com.github.unidbg.linux.android.dvm.DvmMethod.newObject(DvmMethod.java:224)
|
如法炮制
1
2
3
4
5
6
7
8
9
|
case "java/util/zip/GZIPOutputStream-><init>(Ljava/io/OutputStream;)V":{
ByteArrayOutputStream byteArrayOutputStream = (ByteArrayOutputStream) varArg.getObjectArg(0).getValue();
try {
GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream);
return vm.resolveClass("java/util/zip/GZIPOutputStream").newObject(gzipOutputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
|
报错,接着补
1
2
3
4
|
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream->write([B)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:985)
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:980)
at com.github.unidbg.linux.android.dvm.DvmMethod.callVoidMethod(DvmMethod.java:149)
|
直接补
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Override
public void callVoidMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/util/zip/GZIPOutputStream->write([B)V":{
byte[] arr = (byte[])varArg.getObjectArg(0).getValue();
GZIPOutputStream gzipOutputStream = (GZIPOutputStream)dvmObject.getValue();
try {
gzipOutputStream.write(arr);
} catch (IOException e) {
throw new RuntimeException(e);
}
return;
}
}
super.callVoidMethod(vm, dvmObject, signature, varArg);
}
|
再补再补,感觉快出来了
1
2
3
4
|
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream->write([B)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:985)
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:980)
at com.github.unidbg.linux.android.dvm.DvmMethod.callVoidMethod(DvmMethod.java:149)
|
这个,直接补,没啥说的
1
2
3
4
5
6
7
8
9
|
case "java/util/zip/GZIPOutputStream->finish()V":{
GZIPOutputStream gzipOutputStream = (GZIPOutputStream)dvmObject.getValue();
try {
gzipOutputStream.finish();
}catch (IOException e){
e.printStackTrace();
}
return;
}
|
接着补
1
2
3
4
5
|
java.lang.UnsupportedOperationException: java/io/ByteArrayOutputStream->toByteArray()[B
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
at com.pinduoduo.Pdd.callObjectMethod(Pdd.java:138)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)
|
直接补
1
2
3
4
5
|
case "java/io/ByteArrayOutputStream->toByteArray()[B":{
ByteArrayOutputStream byteArrayOutputStream = (ByteArrayOutputStream)dvmObject.getValue();
byte[] bytes = (byte[])byteArrayOutputStream.toByteArray();
return new ByteArray(vm,bytes);
}
|
我严重怀疑有 gzip 压缩,又来了
1
2
3
4
|
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream->close()V
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:985)
at com.pinduoduo.Pdd.callVoidMethod(Pdd.java:246)
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:980)
|
直接补
1
2
3
4
5
6
7
8
9
|
case "java/util/zip/GZIPOutputStream->close()V":{
GZIPOutputStream gzipOutputStream = (GZIPOutputStream)dvmObject.getValue();
try {
gzipOutputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
return;
}
|
补了之后,就出值了

算法分析
首先看一下 newStringUTF 这个函数的返回地址,这个是最直接的方法,直接跳转到 0x1f0d4,改一下变量名

首先可以看到v11 是返回的 anti-token 的值,接着就需要追溯 v11 这个值的来源,然后往上顺着这个链路,就发现 v11 参数的生成需要用到 sub_1EFF8 这个子函数的参数,那就可以直接先 hook 一下 sub_1EFF8 这个子函数,看一下这个子函数的参数是什么
1
|
emulator.attach().addBreakPoint(module.base+0x1EFF8);
|
参数 1:

这个是 env 对象,继续看一下参数 2,参数 3 是一个 16 进制数,应该是参数 2 的长度
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
|
mr1 0x14b
>-----------------------------------------------------------------------------<
[15:01:14 057]r1=RW@0x124b0180, md5=8d176f877a75656480564aa0eebf9d5e, hex=1f8b08000000000000ff6364606060cc6010666414303034354f32363136324c363732353065e06464624bcfcf4fcf4965e06664e6482acdc9c9484d4c01725838fc522b4a8b154c2318d81859997ddc5d81826c08159c8cec6cbe2e8e1696ae0cca8c1c0a10a6be9191a59999a5915569716a917e516a4e6a6271aa6e766a6531d0104e66333d030616462e46710676466e96301edb5540060f0b03083033f282083e10c10f220440842003508310232b033fa330cf931d6b9fcedefba271cacb86590cac8c224cc9794025a20c40b3c5987d425c81b438b3899901504e82c9c010a847125d8f14480f0ba33423139094616406fa498ee382dfda596b6f599c010ac9333200851438802136e706cffa2e204791a3f888e72aab530fbf013daaa4609a9a94666a926c686694946c629e689c68696268626e90666692986c98649902005826521f6e010000
size: 331
0000: 1F 8B 08 00 00 00 00 00 00 FF 63 64 60 60 60 CC ..........cd```.
0010: 60 10 66 64 14 30 30 34 35 4F 32 36 31 36 32 4C `.fd.0045O26162L
0020: 36 37 32 35 30 65 E0 64 64 62 4B CF CF 4F CF 49 67250e.ddbK..O.I
0030: 65 E0 66 64 E6 48 2A CD C9 C9 48 4D 4C 01 72 58 e.fd.H*...HML.rX
0040: 38 FC 52 2B 4A 8B 15 4C 23 18 D8 18 59 99 7D DC 8.R+J..L#...Y.}.
0050: 5D 81 82 6C 08 15 9C 8C EC 6C BE 2E 8E 16 96 AE ]..l.....l......
0060: 0C CA 8C 1C 0A 10 A6 BE 91 91 A5 99 99 A5 91 55 ...............U
0070: 69 71 6A 91 7E 51 6A 4E 6A 62 71 AA 6E 76 6A 65 iqj.~QjNjbq.nvje
0080: 31 D0 10 4E 66 33 3D 03 06 16 46 2E 46 71 06 76 1..Nf3=...F.Fq.v
0090: 46 6E 96 30 1E DB 55 40 06 0F 0B 03 08 30 33 F2 Fn.0..U@.....03.
00A0: 82 08 3E 10 C1 0F 22 04 40 84 20 03 50 83 10 23 ..>...".@. .P..#
00B0: 2B 03 3F A3 30 CF 93 1D 6B 9F CE DE FB A2 71 CA +.?.0...k.....q.
00C0: CB 86 59 0C AC 8C 22 4C C9 79 40 25 A2 0C 40 B3 ..Y..."L.y@%..@.
00D0: C5 98 7D 42 5C 81 B4 38 B3 89 99 01 50 4E 82 C9 ..}B\..8....PN..
00E0: C0 10 A8 47 12 5D 8F 14 48 0F 0B A3 34 23 13 90 ...G.]..H...4#..
00F0: 94 61 64 06 FA 49 8E E3 82 DF DA 59 6B 6F 59 9C .ad..I.....YkoY.
0100: 01 0A C9 33 32 00 85 14 38 80 21 36 E7 06 CF FA ...32...8.!6....
0110: 2E 20 47 91 A3 F8 88 E7 2A AB 53 0F BF 01 3D AA . G.....*.S...=.
0120: A4 60 9A 9A 94 66 6A 92 6C 68 66 94 94 6C 62 9E .`...fj.lhf..lb.
0130: 68 9C 68 69 62 68 62 6E 90 66 66 92 98 6C 98 64 h.hibhbn.ff..l.d
0140: 99 02 00 58 26 52 1F 6E 01 00 00 ...X&R.n...
^-----------------------------------------------------------------------------^
|
这个我不知道是啥数据,问了下 ai,说这个是 gzip 的数据,那就对上了,前面进行了 gzip 的压缩,先解压缩看一下明文是啥
看起来有很多指纹啊,这个就跟 web 有点类似了

接着看第四个参数

这个就是最后结果 anti-token 的前缀啊,倒着往前面分析吧
1
2
|
v11[sub_1CFCF8((int)v9, v12, v8, v8 >> 31, (int)(v11 + 3)) + 3] = 0;
v13 = (*a1)->NewStringUTF(a1, v11);
|
v11 上面还会经过一个函数 sub_1CFCF8, v11 + 3 应该是添加的那个固定值 2af,进去看一下

它里面又会经过两个函数 sub_1CFADC 和 sub_1CFC24 这两个子函数,先看 sub_1CFADC 这个子函数吧,进去之后发现就是一个 base64 编码,首先看一下 base64 编码大致的公式吧
1
2
3
4
|
idx0 = b0 >> 2;
idx1 = ((b0 << 4) & 0x30) | (b1 >> 4);
idx2 = ((b1 << 2) & 0x3C) | (b2 >> 6);
idx3 = b2 & 0x3F;
|
发现就是 base64 编码

这个是标准的 base64,接着去看一下 sub_1CFC24 这个子函数干了什么

他就是对 base64 编码过后的数据进行补位的函数,hook 一下外层函数,看看传进来的参数是什么?
1
|
emulator.attach().addBreakPoint(module.base+0x1CFCF8);
|
断住了

参数 1 大概率就是经过上面加密函数得到的密文数据,主要是他不是 gzip 之后的数据,参数 2 是缓冲区,用于存放结果,参数 3 是缓冲区的长度,参数 4 不知道,参数 5 竟然出现了个 aes 的字眼

你说参数 1 可不可能就是经过 aes 得到的数据啊,这完全有可能啊,那参数 5 有没有可能就是密钥或者 iv 啊,这也完全有可能啊,别猜啊,继续往上面分析啊
先去 cyberchef 验证一下,是不是标准的 base64,虽然分析出来是标准的 base64,但是万一有魔改呢?万一跟 web 一样给我魔改一下呢?pdd 毕竟坏心眼子多的很


确实他没有魔改,接着我就要看一下这块明文是从哪儿来的了

v9 参与了上面 sub_1CEC28 这个子函数中的运算,同时参与的还有 gzip 压缩过后的数据,那就可以推测出 sub_1CEBDC 应该是在做补位之类的什么操作,然后分配空间,下面的 sub_1CFF8 的最后一个参数出现了 aes 字眼,那这个 sub_1CEC28 应该就是 aes 算法了,明文数据就是 gzip 压缩过后的数据,貌似很合理的样子,进去分析一下,让他更合理

这就一个跳板函数,先给内存清空,然后调用 sub_1CEC8C 这个子函数
进去瞄一眼看看

我去,这代码混淆的有点头大,不知道啊,先 hook 一下吧
1
|
emulator.attach().addBreakPoint(module.base+0x1CEC8C);
|
参数 1 还是 pdd_aes_180121_1,可能是 key 的这个数据,参数 2 前 16 个字节是 0,暂时看不出来是啥,看不出来是啥,猜一下呢?不是 iv 能是啥,但是你也不能光靠猜啊,你去看看啊

参数 3 是明文数据,参数 4 是明文的长度
接着看这个函数干了啥事情吧,v5 这个是个数组,先从索引 0 起吧,首先会有一个 sub_1ECDC4 这个子函数,进去看一看

额,不知道啊,看汇编,把汇编抠出来慢慢看
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
|
MOV R0, R10
ADD R7, R0, #0x10
MOV R0, R7 ; size // size = r10+0x10
BL malloc // buf = malloc(r10+0x10)分配空间
MOV R1, R8
MOV R2, R10
MOV R5, R0
BL __aeabi_memcpy // memcpy(buf,r8,r10)
MOVW R0, #0x9FAB
CMP R7, R10
MOVT R0, #0x751C
MOVW R1, #0x5413
MOVWGT R0, #0xF5A2
MOVT R1, #0x5836
MOVTGT R0, #0x66F3 // 赋值一堆常量,不太清楚是在干啥
LDR R3, [SP,#arg_1C]
CMP R7, R10
LDR R2, =(sub_1CEDC4 - 0x1CEE48)
ADDGT R1, R1, #2
EOR R0, R3, R0
EOR R1, R3, R1
STR R0, [SP,#arg_1C]
ADD R0, PC, R2 ; sub_1CEDC4
LDR R1, [R9,R1,LSL#2]
ADD R0, R0, R1
BX R0
|
看了一会儿,只能很明显的看到这是 memcpy 的逻辑,但是是不是 aes 算法,我还看不出来,也许他就是 aes 算法前面的准备,继续往下面看吧,这一块代码看着没太大意义貌似,继续往后面看到 sub_1CEF6C 这个子函数,进去瞄一眼

去 sub_1CE948 这个子函数看一下

这一块感觉有点像 aes 的味道了,首先分配内存,0xB0 转换成 10 进制刚好是 176,而 aes-128 的扩展密钥长度刚好是 176, sub_1CD680 这个子函数的入参还有个 10,你说会不会就是代表 aes-128 的加密轮数啊,太有可能了,如果真是这样,那 a1 就很有可能就是 aes-128 的密钥,管他的,先来 hook 一下
1
|
emulator.attach().addBreakPoint(module.base+0x1CD680);
|
我去,这哥们儿有点眼熟啊,这不是下面的那个 base64 包装函数里面还见过吗

如果是 aes-128,那么密钥长度是 16 个字节,即密钥就是 pdd_aes_180121_1,先进去 sub_1CD680 这个子函数看一眼他干了啥事情
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
|
void __fastcall sub_1CD680(int a1, int a2, int a3, int a4)
{
int v5; // r3
_DWORD v6[26]; // [sp+50h] [bp-88h]
v6[15] = 0;
v6[10] = 0;
v6[22] = 0;
v6[2] = 0;
v6[4] = (char *)sub_1CDA64 - (char *)sub_1CD9C0;
v6[20] = (char *)sub_1CDA9C - (char *)sub_1CDA64;
v6[19] = (char *)sub_1CDCC8 - (char *)sub_1CDB24;
v6[13] = (char *)sub_1CD8F8 - (char *)&loc_1CD7F4;
v6[12] = (char *)sub_1CD880 - (char *)&loc_1CD7F4;
v6[11] = (char *)sub_1CDCC8 - (char *)&loc_1CDC90;
v6[0] = &loc_1CDBA4 - (_UNKNOWN *)sub_1CD9C0;
v6[18] = &loc_1CDC48 - &loc_1CDC00;
v6[6] = (char *)sub_1CD94C - (char *)sub_1CD8F8;
v6[5] = (char *)sub_1CD8F8 - (char *)sub_1CD880;
v6[14] = (char *)sub_1CD9C0 - (char *)sub_1CD94C;
v6[3] = (char *)sub_1CDD90 - (char *)sub_1CD8F8;
v6[9] = &loc_1CDC90 - &loc_1CDC48;
v6[21] = (char *)sub_1CDB24 - (char *)&loc_1CDAE4;
v6[8] = (char *)sub_1CD9C0 - (char *)sub_1CDCC8;
v6[17] = &loc_1CDC00 - &loc_1CDBA4;
v6[7] = (char *)sub_1CDCC8 - (char *)&loc_1CDBA4;
v6[16] = &loc_1CDAE4 - (_UNKNOWN *)sub_1CDA9C;
v6[1] = (char *)sub_1CDD90 - (char *)sub_1CDCC8;
v5 = 14090141;
if ( a4 > 0 )
v5 = 14090140;
__asm { BX R5 }
}
|
我去,这个混淆得不咋好看啊,或者我还是跟上面一样,根据 v6 这个数组的索引顺序进行分析吧,首先看 sub_1CD9C0 这个子函数

通过 hook 可以得到 v25 等于 0x4,这里是在做判断长度,看走密钥扩展的哪一个分支,不是主要逻辑,继续看下面的

这里有一个很规整的结构:
1
2
3
4
|
*(_BYTE *)(a14 + 4 * v27) = a16 ^ *(_BYTE *)(a14 + 4 * (v27 - v25));
*(_BYTE *)(a14 + (a17 | 1)) = a15 ^ *(_BYTE *)(a14 + ((4 * (v27 - v25)) | 1));
*(_BYTE *)(a14 + (a17 | 2)) = a3 ^ *(_BYTE *)(a14 + ((4 * (v27 - v25)) | 2));
*(_BYTE *)(a14 + (a17 | 3)) = v26 ^ *(_BYTE *)(a14 + ((4 * (v27 - v25)) | 3));
|
有点像密钥扩展的公式,有点忘记了,去问了下 ai,它说这是密钥扩展的核心公式
好吧,那就可以确定说这个就是 aes 算法了,之前都只是猜测,目前已经知道的信息,aes-128,密钥已知,是 ECB/CBC?iv 是什么?明文的填充方式是什么?这就是接下来需要分析的问题了,接着在这个函数里面打转转意义不大了,我继续往后面看,全是在做密钥扩展的操作,但是跳转到 sub_1CDA9C 的时候,发现 byte_1EC05F 就是 S 盒,而且还是标准的 S 盒

跳出来,来到 sub_1CDE38 这个函数
1
2
3
4
5
6
7
8
9
10
11
|
int __fastcall sub_1CE948(int a1, int a2, int a3)
{
void *v6; // r7
int v8; // [sp+4h] [bp-1Ch]
v6 = malloc(0xB0u);
sub_1CD680(a1, v6, 10, 4);
sub_1CDE38(a2, a3, v6, 10);
free(v6);
return _stack_chk_guard - v8;
}
|
这个函数在密钥扩展的下面,将扩展之后的密钥传了进去,这个函数有很大可能就是 aes 的加密函数了,先来 hook 一下吧
1
|
emulator.attach().addBreakPoint(module.base+0x1CDE38);
|
先看参数 1,他就是需要加密的明文数据,参数 2,前 16 个字节全是 0 的家伙,我现在愈加怀疑他就是 iv 了,参数 3 就是密钥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
mr2 176
>-----------------------------------------------------------------------------<
[11:27:11 072]r2=RW@0x124b3000, md5=e3c730e26f090ed168938cee747b7b06, hex=7064645f6165735f3138303132315f31b6aba37cd7ced023e6f6e012d4c7bf2372a38534a56d5517439bb505975c0a263cc472bc99a927abda3292ae4d6e9888ab82b65f322b91f4e819035aa5779bd24e9603597cbd92ad94a491f731d30a2508f13c9e744cae33e0e83fc4d13b35e1aa67c4a0de2b6a933ec35557eff860b66bb78a7fb59ce0ec8b5fb5bb64a7d50d2cb45d3c9928bdd01277086b76d0dd666a756e04f35dd3d4e12adbbf97fa06d9
size: 176
0000: 70 64 64 5F 61 65 73 5F 31 38 30 31 32 31 5F 31 pdd_aes_180121_1
0010: B6 AB A3 7C D7 CE D0 23 E6 F6 E0 12 D4 C7 BF 23 ...|...#.......#
0020: 72 A3 85 34 A5 6D 55 17 43 9B B5 05 97 5C 0A 26 r..4.mU.C....\.&
0030: 3C C4 72 BC 99 A9 27 AB DA 32 92 AE 4D 6E 98 88 <.r...'..2..Mn..
0040: AB 82 B6 5F 32 2B 91 F4 E8 19 03 5A A5 77 9B D2 ..._2+.....Z.w..
0050: 4E 96 03 59 7C BD 92 AD 94 A4 91 F7 31 D3 0A 25 N..Y|.......1..%
0060: 08 F1 3C 9E 74 4C AE 33 E0 E8 3F C4 D1 3B 35 E1 ..<.tL.3..?..;5.
0070: AA 67 C4 A0 DE 2B 6A 93 3E C3 55 57 EF F8 60 B6 .g...+j.>.UW..`.
0080: 6B B7 8A 7F B5 9C E0 EC 8B 5F B5 BB 64 A7 D5 0D k........_..d...
0090: 2C B4 5D 3C 99 28 BD D0 12 77 08 6B 76 D0 DD 66 ,.]<.(...w.kv..f
00A0: 6A 75 6E 04 F3 5D D3 D4 E1 2A DB BF 97 FA 06 D9 jun..]...*......
^-----------------------------------------------------------------------------^
|
参数 4 就是加密的轮数,看一下这块的返回值,这样更加确定一点这个函数就是 aes 加密,毕竟进去分析代码它混淆的有点厉害,不怎么好看
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
|
debugger.addBreakPoint(func, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Arm32RegisterContext ctx = emulator.getContext();
Pointer in = ctx.getPointerArg(0);
Pointer out = ctx.getPointerArg(1);
Pointer roundKeys = ctx.getPointerArg(2);
int rounds = ctx.getIntArg(3);
outPtrHolder[0] = UnidbgPointer.nativeValue(out);
long lr = ctx.getLR() & 0xFFFFFFFFL;
System.out.println("=== AES sub_1CDE38 ENTRY ===");
System.out.println("in = " + in);
System.out.println("out = " + out);
System.out.println("rk = " + roundKeys);
System.out.println("rounds = " + rounds);
Inspector.inspect(in.getByteArray(0, 16),"input");
System.out.println("lr = 0x" + Long.toHexString(lr));
debugger.addBreakPoint(lr & ~1L, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer outPtr = UnidbgPointer.pointer(emulator, outPtrHolder[0]);
byte[] cipher = outPtr.getByteArray(0, 16);
System.out.println("=== AES sub_1CDE38 RETURN ===");
System.out.println("cipher = " + Hex.encodeHexString(cipher));
return true;
}
});
return true;
}
});
|
cyberchef 和 unidbg 互相验证一下,发现就是 aes 加密

而且从 hook 的结果来看,明文的填充是 pkcs7 的填充方式

iv 应该是在前面的混淆代码中产生的,接着就可以继续去 cyberchef 中进行验证,iv 是不是那 16 个 0 字节了


是的,不用往前面分析了
至于这个 gzip 的指纹数据,这块不是很熟悉啊安卓的,就知道之前龙哥的文章使用过 SystemPropertyHook 来进行补环境
1
2
3
4
5
6
7
8
9
|
SystemPropertyHook systemPropertyHook = new SystemPropertyHook(emulator);
systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() {
@Override
public String getProperty(String key) {
System.out.println("需要获取的指纹的键值::"+key);
return "";
}
});
memory.addHookListener(systemPropertyHook);
|
来看一下这里面需要补哪些指纹吧
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
|
systemPropertyHook.setPropertyProvider(new SystemPropertyProvider() {
@Override
public String getProperty(String key) {
System.out.println("需要获取的指纹的键值::"+key);
switch (key){
case "ro.kernel.qemu":{
return null; // 判断你是不是模拟器,那我肯定不是啊
}
case "libc.debug.malloc":{
return null;
}
case "ro.serialno":{ // 获取安卓设备序列号
return "自己获取";
}
case "ro.product.brand":{
return "google";
}
case "ro.product.device":{
return "walleye";
}
case "ro.product.model":{
return "Pixel 2";
}
case "ro.product.manufacturer":{
return "Google";
}
case "ro.product.board":{
return "walleye";
}
case "ro.build.display.id":{
return "自己获取";
}
case "ro.build.id":{
return "自己获取";
}
case "ro.build.version.incremental":{
return "自己获取";
}
case "ro.build.type":{
return "user";
}
case "ro.build.tags":{
return "release-keys";
}
case "ro.build.version.release":{
return "10";
}
case "ro.build.version.sdk":{
return "29";
}
case "ro.build.date.utc":{
return "自己获取";
}
}
return "";
}
});
|
补了之后发现就是这鸟样了,那再把那个明文数据解压缩一下,发现能够提取到的就是下面这些信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
FA79E1A06399
google
walleye
Pixel 2
Google
walleye
QQ3A.200805.001
QQ3A.200805.001/6578210:user/release-keys
10
中国联通
cn
LTE
460
01
中国联通
cn
3272d3b46dd4494c80c8bc916f638472
|
但是 pdd 风控强的一批,算法知道了也就那么回事,风控过不去