qimao小说sign字段逆向及unidbg实现

qimao小说sign字段逆向及unidbg实现

Java层

image-20220123212829789

apk是加固的,这次脱壳使用的是frida_dump

frida -U --no-pause -f com.kmxs.reader -l dump_dex.js

然后再重新打包

import pathlib
import zipfile


def get_files(dex_dir):
    fdir = pathlib.Path(dex_dir)
    infos = {}
    for item in fdir.glob('*.dex'):
        size = item.stat().st_size
        infos[size] = item
    
    fdict = {}
    for idx, key in enumerate(sorted(infos, reverse=True)):
        name = 'classes{}.dex'.format(str(idx) if idx else '')
        fdict[name] = infos[key]

    return fdict

def pack(apk_path, dex_dir):
    dst = apk_path + '.pack.apk'
    with zipfile.ZipFile(apk_path) as zf, zipfile.ZipFile(dst, 'w') as zout:
        for item in zf.infolist():
            if item.filename.startswith('classes') and item.filename.endswith('.dex'):
                print('Ignore:', item.filename)
            else:
                buffer = zf.read(item.filename)
                zout.writestr(item, buffer)

        for filename, fpath in get_files(dex_dir).items():
            print('Add:', fpath)
            zinfo = zipfile.ZipInfo(filename)
            with open(fpath, 'rb') as fin:
                zout.writestr(zinfo, fin.read())

if __name__ == '__main__':
    pack(r"E:\workspace\qimao\qimao613.apk", r"E:\workspace\qimao\dump_dex_com.kmxs.reader")

然后jadx打开搜索"sign"

image-20220122235915472

可以看到url和header里面的sign都是调用同一个加密函数。

com.km.repository.net.config.interceptor.HeaderInterceptor.b

image-20220123000109559

com.qimao.qmsdk.tools.encryption.Encryption.sign

image-20220123000150079

com.km.encryption.api.Security.sign

image-20220123000236135

先hook看看

android hooking watch class_method com.km.encryption.api.Security.sign --dump-args --dump-return
image-20220123140659715
image-20220123140752489

虽然找到了native函数,但是看不出是在哪个so里面注册的。

a函数查找用例

image-20220123141149646

com.qimao.qmsdk.tools.encryption.Encryption.init

image-20220123141229042

init函数查找用例

image-20220123141321652

defpackage.qf.run

image-20220123141406108

看来就是在libcommon-encryption.so注册的。

so层

由于手机和app都支持64位指令,所以分析的是64位so

函数窗口搜索java

image-20220123002352558

有点奇怪,其他几个函数都有了,唯独少了sign函数。每个都点进去看看

image-20220123003710978

Java_com_km_encryption_api_Security_token这个函数里看到了Java_com_km_encryption_api_Security_sign,难道它们是同一个函数?看看这个函数

image-20220123003830781

从它的实现来看,就是在Java层的输入后面加了个keyData,然后做个MD5,这很大概率就是sign函数,因为签名就是32位长度的。

看看MessageDigestAlgorithm::MessageDigestAlgorithm函数

image-20220123004312636

看看MessageDigestAlgorithm::init

image-20220123004426099

hook一下MessageDigestAlgorithm::init函数

function dump(name, addr, length) {
    console.log("======================== " + name + " ============");
    console.log(hexdump(addr, {length:length||32}));
}

function hook_key(){
    var bptr = Module.findBaseAddress("libcommon-encryption.so");
    Interceptor.attach(bptr.add(0x19394), {
        onEnter: function(args) {
            console.log(args[0], args[1], args[2]);
            dump("input", args[1], parseInt(args[2]));
        },
        onLeave: function(retval) {
        }
    })
}
image-20220123142052258

只有第一次的是输入,其他的是算法的填充。cyberchef上验证一下是不是标准MD5

image-20220123142115586

没有问题,是对的。

header的sign调用的也是这个函数,只是输入不一样。

unidbg实现

习惯性选择调用32位的so,按照惯例,搭个框架

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

    public static String pkgName = "com.kmxs.reader";
    public static String apkPath = "unidbg-android/src/test/java/com/qimao/qimao613.apk";
    public static String soPath = "";

    public Qimao() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File(apkPath));
        vm.setJni(this);
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary("common-encryption", true);
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator);
    }
    
    public static void main(String[] args) {
        Qimao test = new Qimao();
    }
}

然后就是报错和补环境

image-20220123194410678
@Override
public DvmObject callObjectMethodV(BaseVM vm, DvmObject dvmObject, String signature, VaList vaList) {
    switch (signature) {
        case "java/lang/Class->getClassLoader()Ljava/lang/ClassLoader;": {
            return new ClassLoader(vm, signature);
        }
    }
    return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
image-20220123195042845

开始正式调用

public void call_sign() {
    DvmClass clz = vm.resolveClass("com/km/encryption/api/Security");
    String methodSign = "sign([B)Ljava/lang/String;";
    StringObject ret = clz.callStaticJniMethodObject(emulator, methodSign, new ByteArray(vm, "book_privacy=1cache_ver=1642759975gender=2read_preference=2tab_type=2".getBytes(StandardCharsets.UTF_8)));
}
public static void main(String[] args) {
    Qimao test = new Qimao();
    test.call_sign();
}
image-20220123214354010

日志里可以看到sign函数的地址,跳转过去也是Java_com_km_encryption_api_Security_token这个函数

image-20220123214536950

继续补环境

@Override
public DvmObject getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
    switch (signature) {
        case "com/km/encryption/generator/KeyGenerator->assetManager:Landroid/content/res/AssetManager;": {
            return new AssetManager(vm, signature);
        }
    }
    return super.getStaticObjectField(vm, dvmClass, signature);
}
image-20220123195727498

报错了,但看不出什么。不过如果对AssetManager在native层的实现有所了解的话,就知道它是通过libandroid.so实现的。可惜的是unidbg并没有实现这个so,不过它提供了一个Android VirtualModule,实现了libandroid.so中的几个函数。

image-20220123202647299

从打印的日志也可以看到,libcommon-encryption.so依赖了libandroid.so

public Qimao() {
    emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
    Memory memory = emulator.getMemory();
    memory.setLibraryResolver(new AndroidResolver(23));
    vm = emulator.createDalvikVM(new File(apkPath));
    vm.setJni(this);
    vm.setVerbose(true);
    new AndroidModule(emulator, vm).register(memory);  // Load AndroidModule
    DalvikModule dm = vm.loadLibrary("common-encryption", true);
    module = dm.getModule();
    dm.callJNI_OnLoad(emulator);
}

再次运行

image-20220123203430887

还是报错了,这次跳转到0xfd29看看。

image-20220123203814508

可以看到调用了几个AAsset_*函数,可惜的是目前unidbg并没有实现其中的AAsset_seek函数

image-20220123204048124

所以需要自己实现一下,在unidbg-android/src/main/java/com/github/unidbg/virtualmodule/android/AndroidModule.java添加代码

@Override
protected void onInitialize(Emulator emulator, final VM vm, Map symbols) {
    // ..
    symbols.put("AAsset_seek", svcMemory.registerSvc(is64Bit ? new Arm64Svc() {
        @Override
        public long handle(Emulator emulator) {
            return seek(emulator, vm);
        }
    } : new ArmSvc() {
        @Override
        public long handle(Emulator emulator) {
            return seek(emulator, vm);
        }
    }));
}

private static int seek(Emulator emulator, VM vm) {
    RegisterContext context = emulator.getContext();
    UnidbgPointer pointer = context.getPointerArg(0);
    int offset = context.getIntArg(1);
    int whence = context.getIntArg(2);
    if (log.isDebugEnabled()) {
        log.debug("AAset_seek pointer=" + pointer + ", offset=" + offset + ", whence=" + whence + ", LR=" + context.getLRPointer());
    }
    final int SEEK_SET = 0;
    final int SEEK_CUR = 1;
    final int SEEK_END = 2;
    if ((whence == SEEK_SET && offset >= 0) || whence == SEEK_CUR || whence == SEEK_END) {
        Asset asset = vm.getObject(pointer.toIntPeer());
        return asset.seek(offset, whence);
    }
    throw new BackendException("offset=" + offset + ", whence=" + whence + ", LR=" + context.getLRPointer());
}

unidbg-android/src/main/java/com/github/unidbg/linux/android/dvm/api/Asset.java添加代码

public int seek(int offset, int whence) {
    Pointer pointer = memoryBlock.getPointer();
    int index = pointer.getInt(0);
    int length = pointer.getInt(4);

    final int SEEK_SET = 0;
    final int SEEK_CUR = 1;
    final int SEEK_END = 2;

    if (whence == SEEK_SET) {
        index = offset;
    }
    else if (whence == SEEK_CUR) {
        index = index + offset;
    }
    else if (whence == SEEK_END) {
        index = length + offset;
    }
    pointer.setInt(0, index);
    return index;
}

重新运行

image-20220123205804729

恢复正常的报错了,getKey()需要返回一个字符串,jadx看看这个类。

image-20220123205952471

可以看到是返回成员变量key,可以使用objection + Wallbreaker查看

plugin wallbreaker classdump com.km.encryption.generator.KeyGenerator
image-20220123210219431

所以返回"8w1"

@Override
public DvmObject callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
    switch (signature) {
        case "com/km/encryption/generator/KeyGenerator->getKey()Ljava/lang/String;": {
            return new StringObject(vm, "8w1");
        }
    }
    return super.callStaticObjectMethodV(vm, dvmClass,  signature, vaList);
}
image-20220123210350433

和抓包结果一样。

完整代码

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

    public static String pkgName = "com.kmxs.reader";
    public static String apkPath = "unidbg-android/src/test/java/com/qimao/qimao613.apk";
    public static String soPath = "";

    public Qimao() {
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName(pkgName).build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File(apkPath));
        vm.setJni(this);
        vm.setVerbose(true);
        new AndroidModule(emulator, vm).register(memory);
        DalvikModule dm = vm.loadLibrary("common-encryption", true);
        module = dm.getModule();
        dm.callJNI_OnLoad(emulator);
    }

    @Override
    public DvmObject callObjectMethodV(BaseVM vm, DvmObject dvmObject, String signature, VaList vaList) {
        switch (signature) {
            case "java/lang/Class->getClassLoader()Ljava/lang/ClassLoader;": {
                return new ClassLoader(vm, signature);
            }
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

    @Override
    public DvmObject getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        switch (signature) {
            case "com/km/encryption/generator/KeyGenerator->assetManager:Landroid/content/res/AssetManager;": {
                return new AssetManager(vm, signature);
            }
        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }

    @Override
    public DvmObject callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature) {
            case "com/km/encryption/generator/KeyGenerator->getKey()Ljava/lang/String;": {
                return new StringObject(vm, "8w1");
            }
        }
        return super.callStaticObjectMethodV(vm, dvmClass,  signature, vaList);
    }

    public void call_sign() {
        DvmClass clz = vm.resolveClass("com/km/encryption/api/Security");
        String methodSign = "sign([B)Ljava/lang/String;";
        StringObject ret = clz.callStaticJniMethodObject(emulator, methodSign, new ByteArray(vm, "book_privacy=1cache_ver=1642759975gender=2read_preference=2tab_type=2".getBytes(StandardCharsets.UTF_8)));
//        System.out.println("sign:" + ret.getValue());
    }

    public static void main(String[] args) {
        Qimao test = new Qimao();
        test.call_sign();
    }
}

你可能感兴趣的:(qimao小说sign字段逆向及unidbg实现)