4.3 快手

本文主要介绍怎么使用 unidbg 模拟快手 sig3 的算法调用,本文选取的快手版本为:9.10.30.22832 ,读者可以自行从豌豆荚中找到历史版本进行下载!快手的包名为:com.smile.gifmaker

入口定位

首先我们抓包知道该加密参数的名称为 :__NS_sig3, 那么我们先使用 frida 反编译 app ,然后尝试搜索一下这个名字

发现了两个位置,我们可以随便选取一个,然后跟踪代码

发现该加密参数其实来源

于:mXSec.getWrapper().atlasSign("azeroth", Azeroth2.B.q(), 0, a2);, 此时如果你点进去看,你就会发现这其实是一个接口,实现的地方在别的地方

那么实现的地方在哪里呢?首先我们需要先定位一下 mXSec.getWrapper() 得到的是什么,只要拿到这个 Wrapper 之后,那么加密函数不就是他的 atlasSign 方法了吗?

那么我们先点进去看看这个 mXSec.getWrapper 方法

发现他先拿的 this.mWrapper,如果拿不到再去拿 a 这个对象,点到 a 里面发现他的加密方法直接返回了一个空字符串

那就意味着肯定不是 a 这个对象,肯定拿到了 mWrapper, 那么为了寻找这个 mWrapper, 我们就要寻找他是在什么时候设置的,我们可以点击查找用例

发现第三个位置有设置,进去跟进去看

也就是有人调用了 setWrapper 进行设置了,继续查找用例跟进

所以那个 mWrapper 应该就是 com.kuaishou.android.security.bridge.middleware.a() 这个对象了,看一下他的 atlasSign 方法,发现确实有点像

上面的什么 PatchProxy 就选不管了,可能是缓存啥的(具体我也不清楚),我们的关注点是在 b(str3, true, str2);,因为这个就是返回值,那么我们点进去看

又是一堆复杂的代码,没关系,我们继续只关注返回值:return new com.kuaishou.android.security.base.util.c(b.i().j().c()).l() ? d().b(str, z, str2) : d().c(str, z, str2);

可以看到这里是一个三元表达式,至于走的是哪一个,我们没有 hook 也不是很清楚,所以先碰碰运气点第一个进去看看

这个函数可真够长的,但是我们先不关心逻辑,先只关注返回值,也就是 return str5;,我们发现这个 str5 生成规则为:String str5 = new String(a2.f().a());,那么继续跟进看看这个 a 函数是啥

他返回的是 this.f14492a; ,那么这个 this.f14492a; 又是怎么来的呢 ? 直接查看引用 / 或者你往上面瞟一眼就知道这个是通过上面那个 a 函数设置的,那我们继续分析上面这个 a 函数呗, 看看是什么时候设置上去的,结果发现了一堆代码,那只能继续碰碰运气,随便选一个继续分析了

我选的是第 7 个,因为个人对 getBytes 很有好感,点进去发现以下代码,arr 来自于 str3

str3 又是来自另一个 a 函数,点到 a 函数里面看发现又是一个接口,我们可以尝试查找用例和搜索关键字寻找,关键字就是 Object a(int i, Object... objArr)

只找到了两个,并且第一个就是我们之前的接口,那我们点进去第二个看看!

发现了这样一个东西,Nice!感觉就是这里了,点进去发现也是一个 native 函数

我觉得大概率就是这里了!可以使用 frida 验证一下,事实证明猜测没错!

入参也可以看出来了,参数就不分析了,这篇文章主要内容是加密定位以及 unidbg 算法模拟调用,参数本文会用 hook 到的参数写死

so 文件定位

知道了加密入口,要定位 so 文件就更简单了,我们只需要 hook 一下 registNatives 就可以了,脚本在:https://raw.githubusercontent.com/lasting-yang/frida_hook_libart/master/hook_RegisterNatives.js

搜索一下就可以看到 so 文件为 libkwsgmain.so

unidbg 模拟调用

1. 模板搭建

package com.kuaishou;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
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.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class ks910Sig3 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final String dirPath = "unidbg-android/src/test/resources";
    private final Module module;

    ks910Sig3() {
        // 模拟器,设置进程名为 com.smile.gifmaker
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.smile.gifmaker").build();
        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 23版本的sdk
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File(dirPath + "/ks910.apk"));
        // 打印日志
        vm.setVerbose(true);
        // 设置JNI,类必须extends AbstractJni -> 用于补环境
        vm.setJni(this);
        // 加载 so 文件, 因为传入 app, 所以可以直接写 so 文件的名字, 去掉 开头的 lib 和 结尾的 .so
        DalvikModule dm = vm.loadLibrary("kwsgmain", true);
        module = dm.getModule(); // 获取so模块的句柄
        dm.callJNI_OnLoad(emulator); // 调用JNI_OnLoad
    }

    public static void main(String[] args) {
        ks910Sig3 ins = new ks910Sig3();
    }

}

搭完架子之后运行发现其报错了

通过在网上搜索可以找到一个 issue, 地址为:[main]W/libc: pthread_create failed: clone failed: Out of memory · Issue #398 · zhkl0228/unidbg (github.com)

只有添加以下两行代码即可解决:

        emulator.getSyscallHandler().setVerbose(false);
        emulator.getSyscallHandler().setEnableThreadDispatcher(true);

再次运行发现已经运行成功了!

但是有一个地方是需要注意的,我们查看日志发现其实他依赖了一个 libandroid.so ,但是找不到失败了,所有后面需要补一下!

可以将上述的代码改为:

        new JniGraphics(emulator, vm).register(memory);
        emulator.getSyscallHandler().setVerbose(false);
        emulator.getSyscallHandler().setEnableThreadDispatcher(true);
        new AndroidModule(emulator, vm).register(memory);

可以看到,刚刚那个错误已经不见了!

2. 入参构造

入参构造主要思路就是使用 frida 调试一下参数,再根据代码构造参数,本文不做过多的解释,直接使用现成的参数

先使用以下脚本进行 hook

        let JNICLibrary = Java.use("com.kuaishou.android.security.internal.dispatch.JNICLibrary");
        JNICLibrary["doCommandNative"].implementation = function (i, objArr) {

            console.log("\n===== JNICLibrary.doCommandNative START=====\n")
            console.log(`>> i=${i}`);
            console.log(`>> objArr=${objArr}`);
            let result = this["doCommandNative"](i, objArr);
            console.log(`>> result=${result}`);
            console.log("\n===== JNICLibrary.doCommandNative END=====\n")
            return result;
        };

可以看到 objArr 里面的参数构成

  • 参数 1: 一个 string 数组

  • 参数 2:d7b7d042-d4f2-4012-be60-d97ff2429c17

  • 参数 3:-1

  • 参数 4:false

  • 参数 5:com.yxcorp.gifshow.App@b489a45

  • 参数 6:``

  • 参数 7:false

继续完善脚本,看看参数 1 里面究竟是什么

let JNICLibrary = Java.use("com.kuaishou.android.security.internal.dispatch.JNICLibrary");
var ArrayClz = Java.use("java.lang.reflect.Array");

JNICLibrary["doCommandNative"].implementation = function (i, objArr) {

    console.log("\n===== JNICLibrary.doCommandNative START=====\n")
    console.log(`>> i=${i}`);
    console.log(`>> objArr=${objArr}`);

    try {
        var len = ArrayClz.getLength(objArr[0]);
        for (let i = 0; i != len; i++) {

            console.log(`>> objArr[${i}]=${ArrayClz.get(objArr[0], i).toString()}`);
        }

    } catch (e) {

    }



    let result = this["doCommandNative"](i, objArr);
    console.log(`>> result=${result}`);
    console.log("\n===== JNICLibrary.doCommandNative END=====\n")
    return result;
};

可以看到其实就一个元素,就是 url 相关,因此就可以构造入参了,需要注意的是这边看源码的时候发现参数 5 是一个 context, 而这里面的 context 不是 android.context.Context ,而是 com.yxcorp.gifshow.App,如果我们后面需要 context 的时候直接补这个可以省去很多部环境的步骤,也可以避免很多坑

3. 模拟调用

先编写一个调用方法

    public void callNsSig3() {
        DvmClass clazz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary");

        // 参数 2
        DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
        vm.addLocalObject(context);
        StringObject urlObj = new StringObject(vm, "/rest/n/feed/selectionbb9caf23ee1fda57a6c167198aba919f");
        vm.addLocalObject(urlObj);
        ArrayObject arrayObject = new ArrayObject(urlObj);
        StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
        vm.addLocalObject(appkey);
        DvmInteger intergetobj = DvmInteger.valueOf(vm, -1);
        vm.addLocalObject(intergetobj);
        DvmBoolean boolobj = DvmBoolean.valueOf(vm, false);
        vm.addLocalObject(boolobj);
        DvmObject<?> ret = clazz.callStaticJniMethodObject(
                emulator,
                "doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;",
                10418,
                vm.addLocalObject(new ArrayObject(arrayObject, appkey, intergetobj, boolobj, context, null, boolobj))
        );
        System.out.println("================================================");
        System.out.println(ret.getValue());
        System.out.println("================================================");
    }

编写完调⽤代码之后,我们尝试运⾏,会发现其会继续报错,接下来就开始缺啥补啥,进⾏补环境了!

4. 环境补充

Invalid memory read (UC_ERR_READ_UNMAPPED)

因为读取文件的时候也会遇到 memory 的问题,所以我们先补一下文件访问的,这边我们什么都不做

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

发现还是报错,所以应该不是没有补文件的问题,问了大佬最后告知这个函数依赖于另一个函数的执行,因为我们没有执行所有才会报错!至于如何知道他是不是依赖了别的,那就只能看 so 了。这边可能需要大佬补充一下怎么看!

但是我们也可以使用 fridahook 一下就可以看出来

在程序启动的时候就调用了 JNICLibrary.doCommandNative ,此时 i 的值为 10412objArr[null,"d7b7d042-d4f2-4012-be60-d97ff2429c17", null, null, com.yxcorp.gifshow.App@fa286af, null, null ],这边补一下依赖的函数调用

    public void callInit() {
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // 第一个参数是env
        DvmObject<?> thiz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary").newObject(null);
        list.add(vm.addLocalObject(thiz)); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
        DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
        vm.addLocalObject(context);
        list.add(10412); //参数1
        StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
        vm.addLocalObject(appkey);
        list.add(vm.addLocalObject(new ArrayObject(null, appkey, null, null, context, null, null)));
        // 直接通过地址调用
        Number number = module.callFunction(emulator,
                0x53129,
                list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
//        return result;
//        Number[] numbers = module.callFunction(emulator, 0x53129, list.toArray());
//        DvmObject<?> object = vm.getObject(numbers[0].intValue());
//        String result = (String) object.getValue();
        System.out.println("result:" + result);
    }

调用完这个函数之后再次调用我们的函数,后面就是枯燥的补环境了

com/yxcorp/gifshow/App->getPackageCodePath()Ljava/lang/String;

获取 apk 文件地址,去手机上找一下,直接返回

case "com/yxcorp/gifshow/App->getPackageCodePath()Ljava/lang/String;": {
    return new StringObject(vm, "/data/app/com.smile.gifmaker-oyRnT1esU1Pf5iDY6JKtjA==/base.apk");
}

com/yxcorp/gifshow/App->getAssets()Landroid/content/res/AssetManager;

获取一个 AssetManager 对象,可以自己实例化一个返回

case "com/yxcorp/gifshow/App->getAssets()Landroid/content/res/AssetManager;": {
    return new AssetManager(vm, signature);
}

com/yxcorp/gifshow/App->getPackageName()Ljava/lang/String;

获取包名,直接返回

case "com/yxcorp/gifshow/App->getPackageName()Ljava/lang/String;": {
    return new StringObject(vm, "com.smile.gifmaker");
}

com/yxcorp/gifshow/App->getPackageManager()Landroid/content/pm/PackageManager;

获取一个 PackageManager,使用反射拿到之后返回

case "com/yxcorp/gifshow/App->getPackageManager()Landroid/content/pm/PackageManager;": {
    return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
}

java/lang/Boolean->booleanValue()Z

要获取 bool 值,理论上应该直接将值拿出来然后转换为 bool 返回,但是这里居然会报错

(boolean) dvmObject.getValue();

那么我们就打断点看看是啥

发现就是一个 false,那就直接返回吧

case "java/lang/Boolean->booleanValue()Z": {
    return false;
}

然后发现结果出来了!

附上完整代码:

package com.kuaishou;

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.linux.android.dvm.api.AssetManager;
import com.github.unidbg.linux.android.dvm.array.ArrayObject;
import com.github.unidbg.linux.android.dvm.wrapper.DvmBoolean;
import com.github.unidbg.linux.android.dvm.wrapper.DvmInteger;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.github.unidbg.virtualmodule.android.JniGraphics;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class ks910Sig3 extends AbstractJni implements IOResolver {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final String dirPath = "unidbg-android/src/test/resources";
    private final Module module;

    ks910Sig3() {
        // 模拟器,设置进程名为 com.smile.gifmaker
        emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.smile.gifmaker").build();

        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File(dirPath + "/ks910.apk"));

        new JniGraphics(emulator, vm).register(memory);
        emulator.getSyscallHandler().setVerbose(false);
        emulator.getSyscallHandler().setEnableThreadDispatcher(true);
        new AndroidModule(emulator, vm).register(memory);


        // 打印日志
        vm.setVerbose(true);
//        SyscallHandler<AndroidFileIO> handler = emulator.getSyscallHandler();
//        handler.addIOResolver(this);
        // 设置JNI,类必须extends AbstractJni -> 用于补环境
        vm.setJni(this);
        // 加载 so 文件, 因为传入 app, 所以可以直接写 so 文件的名字, 去掉 开头的 lib 和 结尾的 .so
        DalvikModule dm = vm.loadLibrary("kwsgmain", true);
        module = dm.getModule(); // 获取so模块的句柄
        dm.callJNI_OnLoad(emulator); // 调用JNI_OnLoad
    }

    public static void main(String[] args) {
//        Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG);
//        Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG);
//        Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG);
//        Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG);
//        Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG);
//        Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG);
        ks910Sig3 ins = new ks910Sig3();
        ins.callInit();
        ins.callNsSig3();
    }


    public void callNsSig3() {
        DvmClass clazz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary");

        // 参数 2
        DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
        vm.addLocalObject(context);
        StringObject urlObj = new StringObject(vm, "/rest/n/feed/selectionbb9caf23ee1fda57a6c167198aba919f");
        vm.addLocalObject(urlObj);
        ArrayObject arrayObject = new ArrayObject(urlObj);
        StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
        vm.addLocalObject(appkey);
        DvmInteger intergetobj = DvmInteger.valueOf(vm, -1);
        vm.addLocalObject(intergetobj);
        DvmBoolean boolobj = DvmBoolean.valueOf(vm, false);
        vm.addLocalObject(boolobj);
        DvmObject<?> ret = clazz.callStaticJniMethodObject(
                emulator,
                "doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;",
                10418,
                vm.addLocalObject(new ArrayObject(arrayObject, appkey, intergetobj, boolobj, context, null, boolobj))
        );

        System.out.println("================================================");
        System.out.println(ret.getValue());
        System.out.println("================================================");
    }


    public void callInit() {
        List<Object> list = new ArrayList<>(10);
        list.add(vm.getJNIEnv()); // 第一个参数是env
        DvmObject<?> thiz = vm.resolveClass("com/kuaishou/android/security/internal/dispatch/JNICLibrary").newObject(null);
        list.add(vm.addLocalObject(thiz)); // 第二个参数,实例方法是jobject,静态方法是jclass,直接填0,一般用不到。
        DvmObject<?> context = vm.resolveClass("com/yxcorp/gifshow/App").newObject(null); // context
        vm.addLocalObject(context);
        list.add(10412); //参数1
        StringObject appkey = new StringObject(vm, "d7b7d042-d4f2-4012-be60-d97ff2429c17");
        vm.addLocalObject(appkey);
        list.add(vm.addLocalObject(new ArrayObject(null, appkey, null, null, context, null, null)));
        // 直接通过地址调用
        Number number = module.callFunction(emulator,
                0x53129,
                list.toArray());
        String result = vm.getObject(number.intValue()).getValue().toString();
//        return result;
//        Number[] numbers = module.callFunction(emulator, 0x53129, list.toArray());
//        DvmObject<?> object = vm.getObject(numbers[0].intValue());
//        String result = (String) object.getValue();
        System.out.println("result:" + result);
    }

    @Override
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature) {
            case "com/yxcorp/gifshow/App->getPackageCodePath()Ljava/lang/String;": {
                return new StringObject(vm, "/data/app/com.smile.gifmaker-oyRnT1esU1Pf5iDY6JKtjA==/base.apk");
            }
            case "com/yxcorp/gifshow/App->getAssets()Landroid/content/res/AssetManager;": {
                return new AssetManager(vm, signature);
            }
            case "com/yxcorp/gifshow/App->getPackageName()Ljava/lang/String;": {
                return new StringObject(vm, "com.smile.gifmaker");
            }
            case "com/yxcorp/gifshow/App->getPackageManager()Landroid/content/pm/PackageManager;": {
                return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
            }
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

    @Override
    public boolean callBooleanMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature) {
            case "java/lang/Boolean->booleanValue()Z": {
                return false;
            }
        }
        return super.callBooleanMethodV(vm, dvmObject, signature, vaList);
    }


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

最后更新于

这有帮助吗?