本文主要介绍怎么使用 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
文件为 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();
}
}
搭完架子之后运行发现其报错了
只有添加以下两行代码即可解决:
复制 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;
}
}