这篇文章上次修改于 911 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

原文点此,这里仅记录一些复现知识点和心得

复现笔记

主要围绕几个要点展开

基于签名调用nativeGenerate2是静态注册的方法,基于签名调用传参代码会相对简洁

public void nativeGenerate2() {
    System.out.println("start call nativeGenerate2");
    DvmClass SecureNative_cls = vm.resolveClass("com/xunmeng/pinduoduo/secure/SecureNative");
    DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);
    int context_ptr = vm.addLocalObject(context);
    int str1_ptr = vm.addLocalObject(new StringObject(vm, ""));
    int str2_ptr = vm.addLocalObject(new StringObject(vm, "Ck5UqWFzreofeABcWvkAAg=="));
    int str3_ptr = vm.addLocalObject(new StringObject(vm, "1Hdy4cQW"));
    int str4_ptr = vm.addLocalObject(new StringObject(vm, "/storage/emulated/0"));
    int str5_ptr = vm.addLocalObject(new StringObject(vm, "version=134&info=g6iUSuzNlWeDi%2FxPng%2FN%2B8ZyQEP%2FnQuHC42hkmSWvCOg79IqfkRW5Lu3jsAh0QwizbgZZSg1FOEI%0Ao4R%2F6pw6XXsv%2FxH%2FzUDXzxJ5UXUYGMSYhF%2BULFIhbWMihyiUWSRA%2FamuTFPOOd17oppNLL6QvlSp%0A9rC2BHcgOMfMaYgq0uuiVDJB4cXNREX10fgGf20jz56kh%2B6ejh1iHIEYffs3OKbtp9M7FqmSpiQY%0AuAHzn7rCorHuZDP8tyvStvBqpdDxO92eeEt%2BprLDqsM1HfA%2BX3ItGURbaT4%2BQSMCOA2dU2JVAv8V%0A4%2Fs%2BAghe%2BdfHFWF6Uy1GuRr%2BSZyh6WI0OLTdc10lh0N0t1cykN9qzyI3ybzIvZcqXXRw9AbyyahJ%0AkN8EluJRR4eTqZiM0DjRM6OOB4YComr6gWm7i8OSjQ41yqFhiA9%2BA0An%2BqSpsZWsXPPIcmQYI8Wi%0AmkZ4zBRKc8CPQ4EB6IzqeDfAz%2F7eWLhfEPr0Mbm1pVG0FKqTSPThBKFWVfcOi2SGWFFZZD2izyUw%0Ax1FJOB%2BPxlb2IMLXNU8AG5dkjxhyV6J8QgkQ4re1bY4vfZ2ti3FGHHT%2Bjv6he65vwzMTS5rX7DKs%0ARhM%2BfVR%2BybfcSz3hg277lYzeqx%2BSVTj7LYaC13XkFwIiBcN%2BD5ME%2BKcVOrlF4V8d1LBnl6RbnxA2%0AIF47j2zKcYFGya8ifgrxP3UoTnoPND5DZLQ%2BFXrzv9%2BvzB68A3N2NsuvgphKPg46gqAg76OvmuO2%0AZ6vw2CCfFx2LjQrEqA%2FzIAuW7clNBHpKg5Po0VfFYEHq6UHtIvIbbI49XVGIH7I7q0Y7UeAKgr9m%0AUXsLGDkwzduSuULTQzIttfxWG2HcRzPARV3LBLXjjcjit6F%2BiBcYC47rCax3MN4SQRz8%2BiVX5Cpr%0AN2GYoZs9zamIRJ3oYi828X8r5HucPyMjPDtybvoLQKDIcQMKWyqBj1IH5DW%2BDbR1tFFlJpCMUHqo%0A1wTCQs7qu9mBHJ5iz9WQTEclLCxgUYh484V1AvcKJmOtawNVL5yUOZJOC4yacTMi0Mc2Mt8ZHqRV%0ASk5JgtflVr94tlrW17bc3%2B9HZsd8tOXKrg7aW8RshsycQZXoqBMqHsGWLWzhVg9gBZsCR5%2F68Q6a%0A1mvizrOLX6V2%2Bv8D5z%2BZ1KOKE%2FnNYajg8Dqltk1abE%2BjL4wELUvkVSO18SvqK1YgBUVZ9NHp2t%2BC%0ALVaNxuFkaEEmpqOKTMzpQXJ23b%2FQR4KLtVC8xx%2BZ0Uy1HPYAYsFejtWwxj9%2B%2BNwvDTiZsC8mQ6IJ%0ADPUk4h6IC9s9yYqmq53qYxL7onc2gpCYS0G37XkXCxxNwDT8pIpCgP%2F2NTb1X8PGBtsil6lwnpew%0APdAduDVsF5axEOTkEH0oDjQqBWvVu%2BQwFNVt43oVYNoyk%2B3R3IPowz9qZ88rjBXg8UNiSFPic%2Fta%0Ad%2B6KMR52cM2BXW%2BRk9QWkhRiHr2s8lWBWmOKOZJBsVl3tUj%2FyEwDZqsXyZmAXM2xfX7SNMqQ9RLp%0AhsXXf%2Fqef0k8IMPvfTgzWuDKXLzIcUpqGISWf3n1G22jRZ8XnpClY5NgBeHz7XRFbdE87sb6tGok%0ALHGY8UTSX30y9WhyoSaxDrWk3L8McnFLQ3FD6SDRXU4zYIx447dLsSqLbMdeM6wnLF9lk3BtO9PF%0AXuZMkw7OA0P1vNSndalOljXJZPgj%2F0yjKXdUWdKDfpo9%2B%2FA7ZPm4ll9TVLrD%2FRFaX5VXs80lo44F%0AOun1b7F3NxGUEpGufe7WgC5gUpWuDusuLPJOuJzRGRLMBsfxKkMHCWGB1AaMY8uK8js6pQomg4WI%0AJZZkigafqU3N0is1%2FSJ6Fwefq%2FVFde8%2B2k%2FDbi%2FduShxbF594RscSFfsJ4mL4Q6AZkUo0SKzOqmi%0AfhlpocB0BYS1JhG68%2FmzIVvq%2FU3Y8%2Bw1L0aGA8D1qO0IVkS6Jsbj79rWemq2tERTJnpPm0WRIxfi%0A8ofM81HNYhGh9eh2b5fnpbalisbHnq546QWAglFo6qtNvFfISQmWXwpuyIP1chRB5am6SIqQNpG8%0ANbPPhbqi1%2FdfPNC%2FXEJkhHBIk%2B8XAOEECY5bBbIuj1%2B5%2B4tayrk%2BD4%2F3V%2FIb%2BReBi%2F8Y68Ha0JXJ%0ACekgkktuwtgfc9i0RvTFfA8hYz7YvP0sSHSHJk0ZVAO%2BMZJiUXX5QepmO7jlQ9ozafZgdArNnqFn%0AkBbW4RkGFs1ttIxXHjaXhH9qOjA341Zid7tAMeDCZWoskpScqMb0b5FA5szQfnQ%2BtOs33HChmdjW%0AFnq4wvCZHLJoPhKl1H1KNsGWyl8M2ihIAM3LMlM8jIQo%2FuFO8WmLorKxd9KeiidnRQ8b4S44tTT5%0A40uSb3fvO%2BGh7FfQFjIDwOEoGvasL71wGluVOL9QoHvrQp6BP9nsu%2FaktdEsTsit%2BanUHhRwQsvN%0AE7rU49S3QuQsqQeB%2B1MRezHD0nqWdqJpaTSCO9u7pfwJUUcLgv7arYjncMyPM7fjHjriKwmRdHMm%0A%2FRJF%2FmURbDZ0nzvJMGRGP8z8JM9T9Taw4cyBanPao0eomIxEdK6UEzArezqp17hGk51ag42dVXQe%0AF9fxEKcy1OYZbgdrJuZtBEVM8aUGRXyFXUW6EZ6EkV3bLI56DOEpPrQtF9Qiz6PfRe9OIeUdAbTG%0AG3buXJUGPEc1ZEQs8Vpmu%2BbE0xkfl1sav7pORpCibmhc2PfPxmzLCtpmSdvGfPsaw44SPM8IceTY%0AivKjxgHy75PwP%2BTRg8%2B56%2FFIVp%2FKJgb4Gqf32iqetgNOkU5nnQWGwM%2FwZajGOAhhdB5Eaa45SvDR%0AN%2Bg9fOOKB3E1kjyBSDnkGRrsf66GBsSM0yzKCqbfeoOxSaEUCOYOTrbPbR%2BAiknNML7P9tX6T0Yu%0A3npozYNHYRuOlVTk17s9A1MGMIYhscPMdHKetU8JCMoPd3M49NcH3qyoD%2FR4a6ixNKR%2B3s10pU%2Fm%0AVhjj8LkEL6DBmBXdDKFK3Yem2f1xAlbEPmvUG3%2BMp%2B9WCz9BbYKF4fuYFBrgWrXpIdvJDlkRC9Kw%0AkErw1oBBrWpCyUXIE0%2FX2H3u8iFWPvxseTd%2BhZarM77VfXjj02DfmzVLWU9DGij%2BL9qzgoGw%2FCj%2B%0ALDGoPUweTrWXmev%2FWpnXCzIj9PnoIqDmS3qa2W7q1nAxTmrEIKjkzy%2F%2B6dcn7jz8Z%2BXqAjtjuGFW%0APi6rT8HAmj4uf%2B%2FdbG1bfztKXHftkEo%2Bxnl37QFEeJ2OJPz5jpCZh9EickNrMyNJTQ3cO0OMuK45%0ADTJnb7Xc0xpxqJ54XxY%3D%0A"));
    long num = 0x17b44082347L;

    StringObject result = SecureNative_cls.callStaticJniMethodObject(
        emulator,
        "nativeGenerate2(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)Ljava/lang/String;",
        context_ptr, str1_ptr, str2_ptr, str3_ptr, str4_ptr, str5_ptr, num
    );
    System.out.println("result:" + result);
}

文件访问,虽然可以手动实现IOResolverresolve,在函数内进行文件进行操作

但我更倾向于在setRootDir设置好根目录的中构造对应的目录和文件

重写的resolve打印出信息,文件则直接从手机中pull一份,再放到对应的目录中去

@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
    System.out.println("evil elf want to access:" + pathname);
    return null;
}

例如访问cpuinfo文件

  • target/rootfs/proc/cpuinfo

自定义SyscallHandler,正常情况下,大家都是像下面这样来初始化模拟器,但跟着复现后,会在补完某个java环境后出现异常

emulator = AndroidEmulatorBuilder.for32Bit().setRootDir(new File("target/rootfs")).setProcessName("com.xunmeng.pinduoduo").build();

如图

WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:469) - handleInterrupt intno=2, NR=190, svcNumber=0x0, PC=RX@0x401c6b5c[libc.so]0x41b5c, LR=RX@0x401b45cb[libc.so]0x2f5cb, syscall=null

可以看到这里的syscall=null

这是因为unidbg还没有实现ARM下编号为190的syscall

具体可以看这个网站

可以知道ARM下编号为190的syscall是vfork,相关说明如下

  • vfork会产生一个新的子进程.但是vfork创建的子进程与父进程共享数据段,而且由vfork创建的

不太理解,但是可以知道它返回值是新子进程的pid,那么可以具体实现如下(FROM 龙哥

private void vfork(Emulator<?> emulator) {
    EditableArm32RegisterContext context = (EditableArm32RegisterContext) emulator.getContext();
    int childPid = emulator.getPid() + ThreadLocalRandom.current().nextInt(256);
    int r0 = childPid;
    System.out.println("vfork pid=" + r0);
    context.setR0(r0);
}

也就是返回一个新的pid,是不是要真的新产生一个子进程呢?根据测试这里不用也可以接着走下去

如何完全模拟vfork呢?不知道TAT

然后现在是197也就是fstat64,具体和fstat差不多,但是unidbg确实也没有具体实现

WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:469) - handleInterrupt intno=2, NR=197, svcNumber=0x0, PC=RX@0x401c61f8[libc.so]0x411f8, LR=RX@0x401bdf3f[libc.so]0x38f3f, syscall=null

可以看到报错具体在BaseAndroidFileIO.java:17

既然差不多,这里似乎在访问su文件?那直接返回0大小好了,

然后就能顺利跑后续了,如果出现memory failed那么升级一下unidbg即可

另外使用了自定义的SyscallHandler后,就不能再加上for32Bit了,否则自定义SyscallHandler无法生效

至此,应该就可以跑通结果了

其他,龙哥增加了一个UserEnvModule,以及对通过重写SystemPropertyProvider对一些属性进行设定,这样模拟的参数计算的结果被风控的概率更小

后面fstat64这里的错误,龙哥是通过修改pipe2实现修复的


这部分是最开始写的,后续发现java层环境太多要补充了,就没有记录了,请忽略

步骤

so从apk中提取,文件结构如下

之前跟着复现了一遍info2的调用,这里照着先写个模板

package com.xunmeng.pinduoduo.secure;

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.*;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class SecureNative extends AbstractJni implements IOResolver {

    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    SecureNative() {
        emulator = AndroidEmulatorBuilder
                .setRootDir(new File("target/rootfs"))
                .setProcessName("com.xunmeng.pinduoduo")
                .build();
        System.out.println("当前进程PID -> " + emulator.getPid());
        emulator.getSyscallHandler().addIOResolver(this);

        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));

        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/20211023/拼多多官方.apk"));
        vm.setVerbose(true);
        vm.setJni(this);

        DalvikModule dm_libUserEnv = vm.loadLibrary(new File("unidbg-android/src/test/resources/20211023/libUserEnv.so"),true);
        dm_libUserEnv.callJNI_OnLoad(emulator);

        DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/20211023/libpdd_secure.so"), true);
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator);
    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("evil elf want to access:" + pathname);
        return null;
    }

    public static void main(String[] args) {
        SecureNative mSecureNative = new SecureNative();
        mSecureNative.nativeGenerate2();
    }

    public void nativeGenerate2() {
        System.out.println("start call nativeGenerate2");
    }
}

很好,是要补java环境

@Override
public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature){
        case "android/provider/Settings$Secure->ANDROID_ID:Ljava/lang/String;":{
            return new StringObject(vm, "android_id");
        }
    }
    return super.getStaticObjectField(vm, dvmClass, signature);
}

补上,然后下一步

@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
    switch (signature){
        case "android/app/ActivityThread->getApplication()Landroid/app/Application;":
            DvmClass context = vm.resolveClass("android/content/Context");
            DvmClass contextwrapper = vm.resolveClass("android/content/ContextWrapper", context);
            return vm.resolveClass("android/app/Application", contextwrapper).newObject(signature);
        case "android/content/Context->getContentResolver()Landroid/content/ContentResolver;":
            return vm.resolveClass("android/content/ContentResolver").newObject(signature);
        case "java/util/UUID->toString()Ljava/lang/String;":
            return new StringObject(vm, dvmObject.getValue().toString());
        case "java/lang/String->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
            StringObject str = (StringObject) dvmObject;
            StringObject s1 = vaList.getObjectArg(0);
            StringObject s2 = vaList.getObjectArg(1);
            assert s1 != null;
            assert s2 != null;
            return new StringObject(vm, str.getValue().replaceAll(s1.getValue(), s2.getValue()));
        }
    }
    return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

补上(这里是之前info2的代码,直接一步到位),然后下一步

@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    switch (signature){
        case "android/provider/Settings$Secure->getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;":
            return new StringObject(vm, "");
        case "java/util/UUID->randomUUID()Ljava/util/UUID;":
            return vm.resolveClass("java/util/UUID").newObject(UUID.randomUUID());
    }
    return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

补上,下一步

中间这里出现了报错,暂时先放一边,先把java环境的报错解决

@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    switch (signature){
        case "com/tencent/mars/xlog/PLog->i(Ljava/lang/String;Ljava/lang/String;)V":
            return;
    }
    super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
}

补上,然后现在没有其他异常了

这里的日志看起来好像没有nativeGenerate2相关的,不要慌用grep看一下


补java环境ing


N分钟后,补完下面的环境,出现一个异常,不知道怎么处理

龙哥说到

  • com.github.unidbg.linux.android.dvm.api.SystemService.getObjectType(SystemService.java:44)

一看便知


依葫芦画瓢,现在接着补java环境


然后在getNetworkInterfaces我觉得返回一个就行了

java.lang.UnsupportedOperationException: java/net/NetworkInterface->getNetworkInterfaces()Ljava/util/Enumeration;
    at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:410)

...

大量java层的环境...

略...