Featured image of post 某多多app逆向分析

某多多app逆向分析

样本地址: 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_1CFADCsub_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 风控强的一批,算法知道了也就那么回事,风控过不去