本文主要介绍怎么使用 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
里面的参数构成
参数 2:d7b7d042-d4f2-4012-be60-d97ff2429c17
参数 5:com.yxcorp.gifshow.App@b489a45
继续完善脚本,看看参数 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
了。这边可能需要大佬补充一下怎么看!
但是我们也可以使用 frida
先 hook
一下就可以看出来
在程序启动的时候就调用了 JNICLibrary.doCommandNative
,此时 i
的值为 10412
, objArr
为 [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;
}
}