固定参数-以京东sign逆向为例

固定参数-以京东sign逆向为例

前言

在逆向过程中,需要结合frida或unidbg,对整个算法进行一步步的分析,有时候我们分析完某一部分,想要继续往下分析时,需要重新发起请求,这时候的参数往往都已经改变了,这样会打断我们的节奏,影响效率。此外,有些算法除了我们外部传进去的参数外,还有一些其他的参数参与了加密,比如时间戳,随机数,一旦这些参与了算法,那么即使每次的传入参数不变,加密的结果还是会变。

外部输入参数的固定

frida

在京东app的hook中,我们选择编写一个函数,能够固定的调用Java层的native函数。

var bptr = Module.findBaseAddress('libjdbitmapkit.so')
console.log(bptr);

function hook_12ECC() {
    Interceptor.attach(bptr.add(0x12ECC+1), {
        onEnter: function(args) {
            this.arg0 = args[0];
            this.arg3 = args[3];
            console.log(args[0], args[1], args[2], args[3], args[4]);
            dump("arg0-ptr0", args[0].readPointer());
            dump("arg0-ptr1", args[0]);
            dump("arg0-ptr1", args[0].add(4).readPointer(), 64);
            console.log("arg1", args[1].readCString());
            // dump("arg2", args[2]);
            console.log("arg3", args[3].readCString(parseInt(args[4])));
        },
        onLeave: function(){
            console.log("arg0-ret", this.arg0);
            dump("ret-arg0-ptr1", this.arg0);
            // dump("ret-arg0-ptr2", this.arg0.add(8).readPointer());
            dump("12ecc-ret-arg2", this.arg3)
        }
    })
}

function callBitmapkitUtils() {
    var BitmapkitUtils = Java.use('com.jingdong.common.utils.BitmapkitUtils');

    var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
    var context = currentApplication.getApplicationContext();

    var b = 'clientImage';
    var c = '{"moduleParams":{"18":"1565611060638","19":"1565229712150","25":"1567478504636","27":"1602488415048","28":"1631069159956","30":"1567404005627","32":"1567997588476","34":"1593508185597","35":"1568708316462","37":"1630293538664","42":"1623741761542","44":"1569247647090","46":"1588839806224","47":"1571295610042","61":"1582091758495","70":"1585279774645","74":"1586781606615"}}';
    var d = 'd5a585639f505b18';
    var e = 'android';
    var f = '10.2.0';

    var res = BitmapkitUtils.getSignFromJni(context, b, c, d, e, f);
    console.log('res: ', res);
}

Java.perform(function() {
    hook_12ECC();
})

然后启动frida之后,可以在shell中输入callBitmapkitUtils()来调用函数。

image-20211213192143481

这样一来不像在手机上滑动页面、点击页面那样,有时会有多个请求发出,会多次调用加密的方法。这样的好处是,再也不用在手机上操作了,而且请求的内容和个数是可控的。不过我们注意到京东的参数里有stsv这两个参数,这可不是由我们传入的,属于内部输入参数,接下来我们要固定它们。

unidbg

对于unidbg而言,在我们编写代码的时候,一般都是固定输入的

内部输入参数的固定

frida

对于内部输入参数而言,可能有时间戳,随机数,常量(数字或字符),其中前2个是会改变的,这会影响逆向分析,所以需要固定这两个参数。时间戳一般是调用libc.sogettimeofday函数,随机数则是调用libc.solrand48srand48

function hook_libc(){
    var ptr_t = Module.findExportByName("libc.so", "gettimeofday");
    Interceptor.attach(ptr_t, {
        onEnter: function(args){
            this.arg0 = args[0];
        },
        onLeave: function() {
            this.arg0.writeU32(1639393559);
            this.arg0.add(4).writeU32(0);
        }
    });

    Interceptor.attach(Module.findExportByName("libc.so" , "lrand48"), {
        onEnter: function(args) {
        },
        onLeave:function(retval){
            retval.replace(1);
        }
    });
}
Java.perform(function() {
    hook_libc();
    hook_12ECC();
})
image-20211213194004045

这样一来,即使多次运行,stsv也不会改变,有利于我们的分析。

不过,固定时间戳有个问题,其他函数在获取时间戳的时候发现不对,可能会导致frida环境崩溃,所以我们希望只在调用的时候固定时间戳。所以我们更新如下代码

var logvar = 0;

function callBitmapkitUtils() {
    var BitmapkitUtils = Java.use('com.jingdong.common.utils.BitmapkitUtils');

    var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
    var context = currentApplication.getApplicationContext();

    var b = 'clientImage';
    var c = '{"moduleParams":{"18":"1565611060638","19":"1565229712150","25":"1567478504636","27":"1602488415048","28":"1631069159956","30":"1567404005627","32":"1567997588476","34":"1593508185597","35":"1568708316462","37":"1630293538664","42":"1623741761542","44":"1569247647090","46":"1588839806224","47":"1571295610042","61":"1582091758495","70":"1585279774645","74":"1586781606615"}}';
    var d = 'd5a585639f505b18';
    var e = 'android';
    var f = '10.2.0';

    logvar = 1;
    var res = BitmapkitUtils.getSignFromJni(context, b, c, d, e, f);
    logvar = 0;
    console.log('res: ', res);
}

function hook_libc(){
    var ptr_t = Module.findExportByName("libc.so", "gettimeofday");
    Interceptor.attach(ptr_t, {
        onEnter: function(args){
            this.arg0 = args[0];
        },
        onLeave: function() {
            if (logvar) {
                this.arg0.writeU32(1639393559);
                this.arg0.add(4).writeU32(0);
            }
        }
    });

    Interceptor.attach(Module.findExportByName("libc.so" , "lrand48"), {
        onEnter: function(args) {
        },
        onLeave:function(retval){
            if (logvar){
                retval.replace(1);
            }
        }
    });
}

这样一来,只有在logvar值为1时才会固定参数。而logvar的默认值为0,只有在调用callBitmapkitUtils方法的时候才会改为1,调用完成后又会改为0。

unidbg
image-20211213195344286

对于unidbg,我们运行多次后会发现,时间戳st会变,sv一直是111,好像和frida上的表现不一样,难道出了什么问题?

其实如果研究了京东libjdbitmapkit.so就会发现sv的后两位都是随机数余3。

image-20211213195752629

而在unidbg对libc.so的随机数生成的实现中,种子是固定的(我猜的,没深究源码),导致生成的随机数的顺序是固定的,继而导致余数是固定的。

public void hook_libc() {
    IHookZz hookZz = HookZz.getInstance(emulator);
    hookZz.wrap(module.findSymbolByName("lrand48"), new WrapCallback() {
        @Override
        public void preCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
        }

        @Override
        public void postCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
            int old = ctx.getIntArg(0);
            System.out.println("Origin rand:" + old);
            ctx.setR0(1);
        }
    });

    hookZz.wrap(module.findSymbolByName("gettimeofday"), new WrapCallback() {
        @Override
        public void preCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
            Pointer pointer = ctx.getR0Pointer();
            ctx.push(pointer);
        }

        @Override
        public void postCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
            Pointer pointer = ctx.pop();
            pointer.setLong(0, 1639388888);
            pointer.setLong(4, 0);
        }
    });
}
public static void main(String[] args) {
    JingDong test = new JingDong();
    test.hook_libc();
    test.callSign();
}
image-20211213200606140
image-20211213200516589

可以看到,时间戳已经被固定下来了,而打印出来的两个随机数,他们的除以3的余数都为1,这也说明了为什么固定参数之前sv的值一直是111。

unidbg完整代码
package com.jingdong;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.hook.hookzz.*;
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.linux.android.dvm.array.ByteArray;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.memory.Memory;
import com.sun.jna.Pointer;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.ParsingException;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.security.cert.X509Certificate;

public class JingDong extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    public static String pkgName = "com.jingdong.app.mall";
    public static String apkPath = "unidbg-android/src/test/java/com/jingdong/jingdong9.2.2.apk";
    public static String soPath = "unidbg-android/src/test/java/com/jingdong/libjdbitmapkit.so";
    private static final String APK_PATH = "/data/app/com.jingdong.app.mall.apk";

    JingDong() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File(apkPath));
        DalvikModule dm = vm.loadLibrary(new File(soPath), false);
        vm.setJni(this);
        vm.setVerbose(true);
        dm.callJNI_OnLoad(emulator);
        module = dm.getModule();
    }

    @Override
    public DvmObject getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        switch (signature) {
            case "com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;": {
                return vm.resolveClass("android/app/Activity", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);
            }
        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }

    @Override
    public DvmObject getObjectField(BaseVM vm, DvmObject dvmObject, String signature) {
        switch (signature) {
            case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;": {
                return new StringObject(vm, APK_PATH);
            }
        }
        return super.getObjectField(vm, dvmObject, signature);
    }

    @Override
    public DvmObject callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
        switch (signature) {
            case "com/jingdong/common/utils/BitmapkitZip->unZip(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B": {
                StringObject apkPath = varArg.getObjectArg(0);
                StringObject directory = varArg.getObjectArg(1);
                StringObject filename = varArg.getObjectArg(2);
                if (APK_PATH.equals(apkPath.getValue()) &&
                        "META-INF/".equals(directory.getValue()) &&
                        ".RSA".equals(filename.getValue())) {
                    byte[] data = vm.unzip("META-INF/JINGDONG.RSA");
                    return new ByteArray(vm, data);
                }
            }
            case "com/jingdong/common/utils/BitmapkitZip->objectToBytes(Ljava/lang/Object;)[B": {
                DvmObject obj = varArg.getObjectArg(0);
                byte[] bytes = objectToBytes(obj.getValue());
                return new ByteArray(vm, bytes);
            }
        }
        return super.callStaticObjectMethod(vm ,dvmClass, signature, varArg);
    }

    @Override
    public DvmObject newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
        switch (signature) {
            case "sun/security/pkcs/PKCS7->([B)V": {
                ByteArray array = varArg.getObjectArg(0);
                try {
                    return vm.resolveClass("sun/security/pkcs/PKCS7").newObject(new PKCS7(array.getValue()));
                } catch (ParsingException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
        return super.newObject(vm, dvmClass, signature, varArg);
    }

    @Override
    public DvmObject callObjectMethod(BaseVM vm, DvmObject dvmObject, String signature, VarArg varArg) {
        switch (signature) {
            case "sun/security/pkcs/PKCS7->getCertificates()[Ljava/security/cert/X509Certificate;": {
                PKCS7 pkcs7 = (PKCS7) dvmObject.getValue();
                X509Certificate[] certificates = pkcs7.getCertificates();
                return ProxyDvmObject.createObject(vm, certificates);
            }
        }
        return super.callObjectMethod(vm, dvmObject, signature, varArg);
    }

    @Override
    public DvmObject newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature) {
            case "java/lang/StringBuffer->()V": {
                return vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());
            }
            case "java/lang/Integer->(I)V": {
                return DvmInteger.valueOf(vm, vaList.getIntArg(0));
            }
        }
        return super.newObjectV(vm, dvmClass, signature, vaList);
    }

    @Override
    public DvmObject callObjectMethodV(BaseVM vm, DvmObject dvmObject, String signature, VaList vaList) {
        switch (signature) {
            case "java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;": {
                StringBuffer buffer = (StringBuffer) dvmObject.getValue();
                StringObject str = vaList.getObjectArg(0);
                buffer.append(str.getValue());
                return dvmObject;
            }
            case "java/lang/Integer->toString()Ljava/lang/String;": {
                return new StringObject(vm, ((Integer)dvmObject.getValue()).toString());
            }
            case "java/lang/StringBuffer->toString()Ljava/lang/String;": {
                return new StringObject(vm, ((StringBuffer)dvmObject.getValue()).toString());
            }
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

    private static byte[] objectToBytes(Object obj) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(obj);
            oos.flush();
            byte[] array = baos.toByteArray();
            oos.close();
            baos.close();
            return array;
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    public void hook_libc() {
        IHookZz hookZz = HookZz.getInstance(emulator);
        hookZz.wrap(module.findSymbolByName("lrand48"), new WrapCallback() {
            @Override
            public void preCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
            }

            @Override
            public void postCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                int old = ctx.getIntArg(0);
                System.out.println("Origin rand:" + old);
                ctx.setR0(1);
            }
        });

        hookZz.wrap(module.findSymbolByName("gettimeofday"), new WrapCallback() {
            @Override
            public void preCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                Pointer pointer = ctx.getR0Pointer();
                ctx.push(pointer);
            }

            @Override
            public void postCall(Emulator emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                Pointer pointer = ctx.pop();
                pointer.setLong(0, 1639388888);
                pointer.setLong(4, 0);
            }
        });
    }

    public void callSign() {
        DvmClass cBitmapkitUtils = vm.resolveClass("com/jingdong/common/utils/BitmapkitUtils");
        StringObject ret = cBitmapkitUtils.callStaticJniMethodObject(emulator, "getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",
                vm.resolveClass("android/content/Context").newObject(null),
                "clientImage",
                "{\"moduleParams\":{\"18\":\"1565611060638\",\"19\":\"1565229712150\",\"25\":\"1567478504636\",\"27\":\"1602488415048\",\"28\":\"1631069159956\",\"30\":\"1567404005627\",\"32\":\"1567997588476\",\"34\":\"1593508185597\",\"35\":\"1568708316462\",\"37\":\"1630293538664\",\"42\":\"1623741761542\",\"44\":\"1569247647090\",\"46\":\"1588839806224\",\"47\":\"1571295610042\",\"61\":\"1582091758495\",\"70\":\"1585279774645\",\"74\":\"1586781606615\"}}",
                "d5a585639f505b18",
                "android",
                "10.2.0");
        System.out.println(ret.getValue());
    }

    public static void main(String[] args) {
        JingDong test = new JingDong();
        test.hook_libc();
        test.callSign();
    }
}

关于京东加密

image-20211213201043795

其实京东app有3套加密方案,会根据随机数不同来选择不同的方案,而unidbg生成的随机数和京东cv的生成方案导致sv一直是固定的,从而一直调用其中一套方案。我们在逆向的时候,其实只需要逆向出其中一套方案即可,那个简单选哪个,如果刚好是调用的那套方案的话那还好。如果不是,一时之间又不知道怎么处理的话就有点倒霉了。

你可能感兴趣的:(固定参数-以京东sign逆向为例)