安卓逆向小案例——阿里系某电影票务APP加密参数还原-Unidbg篇
一、前期准备
使用unidbg还原参数时,首先需要找到指定的native方法和对应的so文件。而锁定生成加密参数的native方法和相应的so文件可以通过frida相关hook找到,这里不做解释。
1. app版本
dt专业版app_6.3.0.apk
2. so文件
libsgmainso-6.5.21.so
3. native方法
```
.class public Lcom/taobao/wireless/security/adapter/JNICLibrary;
.super Ljava/lang/Object;
.source ""
# direct methods
.method public static varargs native doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;
.end method
```
4. 待生成的加密参数
x-sign、x-sgext、x-umt、x-mini-wua
二、unidbg流程
1. 第一步——判断是否存在初始化
native方法是否需要先初始化后才能获取数据,怎么判断?
a:hook产生加密参数的native和该JAVA类中所有native函数
使用spwan模式启动app,查看已经hook的native函数的执行顺序
b:编写目标native的执行代码(这里的传参就使用刚才hook到的具体数据)
在每一个hook的native方法调用前先执行这段目标native的执行代码,再次使用spwan模式启动APP,再次查看输出
这里的逻辑是:如果目标代码在最初就可以成功执行,那就说明不需要初始化,若目标代码在第n-1个native方法前都是报错的,到第n个方法前能够执行出正确结果,那说明第n个方法前的所有hook的native方法都可能是初始化。(这里用可能,是因为有少部分函数不是初始化,但是确实在目标函数前执行,这类函数明显特征是他的执行不固定,可能这次在a方法前面,下一次在a方法后面,而且方法的传参没有实际意义或者没有传参。)
当前案例:当前案例只有一个native方法,但是不代表他没有初始化方法,该是得按照上述的两步操作来观察。
- hook代码
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if (android_dlopen_ext != null) {
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
this.hook = false;
var soName = args[0].readCString();
if (soName.indexOf("libsgmainso-6.5.21.so") != -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
console.log('找到so,开始加载JNI_OnLoad');
var JNIOnload = Module.findExportByName("libsgmainso-6.5.21.so", "JNI_OnLoad");
Interceptor.attach(JNIOnload, {
onEnter: function (args) {
console.log("进到 JNI_OnLoad");
},
onLeave: function (retval){
console.log("JNI_Onload 执行完成");
// 这里开始hook
hookSignNative();
}
})
}
}
})
}
function hookSignNative() {
var base_address = Module.findBaseAddress('libsgmainso-6.5.21.so')
if (base_address != null) {
Java.perform(function () {
var obj = Java.use("java.lang.Object");
Interceptor.attach(base_address.add(0xc049), {
onEnter: function (args) {
console.log("hook success");
console.log("arg0:" + args[0]); //JNIENV
console.log("arg1:" + Java.cast(args[1], obj)); //jclazz
console.log("arg2:" + args[2].toInt32()); // 参数1
console.log("arg3:" + args[3]); // 参数1
var arg3 = Java.cast(args[3], obj);
console.log("arg3:" + arg3); // 参数1
printArray(arg3);
},
onLeave: function (retval) {
console.log("result ==> :" + retval);
}
});
});
}
}
function printArray(objects) {
// 这里是输出Object数组
var ArrayClz = Java.use("java.lang.reflect.Array");
var len = ArrayClz.getLength(objects);
console.log("Array ==> length = "+ len);
for(let i=0;i!=len;i++) {
var nowData = ArrayClz.get(objects, i);
if (nowData != null) {
console.log("Array ==> " + nowData.toString());
}
}
}
- spwan启动APP,输出结果
- 这里正常输出结果,接下来执行下一步。执行目标函数(这里也是同一个native)。
function docommandnative2 () {
Java.perform(
function() {
var integerclass= Java.use("java.lang.Integer");
var stringclass= Java.use("java.lang.String");
var booleanclass= Java.use("java.lang.Boolean");
var str1 = stringclass.$new('23632979');
var data = stringclass.$new('Yl7Bnq3bwrgDACsiIRXKipG+&&&23632979&202309ce8a8fdfa2a155b604f7c65d9d&1650634740&mtop.alipictures.gravitywave.global.search.list&1.2&&10005894&AvH8ID-LlSsyBuvgEtWSp0OGnvcpTMm66qRu16fKJFxl&&&&27&&&&&&&');
var z = booleanclass.$new(false);
var i = integerclass.$new(0);
var api = stringclass.$new('mtop.alipictures.gravitywave.global.search.list');
var str2 = stringclass.$new('pageId=&pageName=');
var str3 = stringclass.$new('');
var str4 = stringclass.$new('');
var str5 = stringclass.$new('');
var objArr =Java.array('Ljava.lang.Object;',[str1, data, z, i, api, str2, str3, str4, str5]);
Java.enumerateClassLoaders({
"onMatch": function(loader) {
if (loader.toString().indexOf("libsgmain.so") >= 0 ) {
Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的
console.log("loader = ",loader.toString());
}
},
"onComplete": function() {
console.log("success");
}
});
var JNICLibrary = Java.classFactory.use('com.taobao.wireless.security.adapter.JNICLibrary');
var resultMap = JNICLibrary.doCommandNative(70102, objArr)
if (resultMap != null) {
console.log('result == ' + resultMap)
}
}
)
}
- 将执行目标函数的代码添加到hook代码之前,spwan模式启动APP,可以发现在执行1个10101和3个10102之后,目标函数可以成功执行。
2. 搭建unidbg
第一步初始化判断已经完成,且通过hook获取到具体的参数。如下:
- 10101: 参数0 和 1可以不管。
参数0:0xf21a7230 :这是JNIEnv
参数1:class com.taobao.wireless.security.adapter.JNICLibrary对象
参数2:10101
参数3:[“com.alipictures.moviepro.MovieproApplication@3ca0131”, 3, “”, “/data/user/0/com.alipictures.moviepro/app_SGLib”, “”] :对象数组
- 10102-第一次
参数2:10102
参数3:[“main”, “6.5.21”, “/data/app/com.alipictures.moviepro-lYhmlkDhtgvVNT1zH6qZIQ==/lib/arm/libsgmainso-6.5.21.so”]
- 10102-第二次
参数2:10102
参数3:[“securitybody”, “6.5.26”, “/data/app/com.alipictures.moviepro-lYhmlkDhtgvVNT1zH6qZIQ==/lib/arm/libsgsecuritybodyso-6.5.26.so”]
- 10102-第三次
参数2:10102
参数3:[“middletier”, “6.5.23”, “/data/app/com.alipictures.moviepro-lYhmlkDhtgvVNT1zH6qZIQ==/lib/arm/libsgmiddletierso-6.5.23.so”]
- 接下来搭建unidbg模板
package com.qking.al.taopiaopiao;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
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.AbstractJni;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import java.io.File;
public class TppSign extends AbstractJni implements IOResolver {
private final AndroidEmulator emulator;
private final VM vm;
final String PACKAGE_NAME = "com.alipictures.moviepro";
final String APK_NAME = "unidbg-android/src/test/resources/ali/taopiaopiao/dt630.apk";
final String SO_SMAIN = "unidbg-android/src/test/resources/ali/taopiaopiao/libsgmainso-6.5.21.so";
public DvmClass JNICLibrary;
TppSign() {
// 创建模拟器实例,进程名填写实际的进程名,避免针对进程名的校验
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName(PACKAGE_NAME)
.build();
// 绑定IO重定向接口,没有这句,下面的resolve方法不会被调用
emulator.getSyscallHandler().addIOResolver(this);
// 获取模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建虚拟机,传入apk,让unidbg为我们做部分签名校验的工作(最好填绝对路径)
vm = emulator.createDalvikVM(new File(APK_NAME));
// 设置JNI
vm.setJni(this);
// 打印日志
vm.setVerbose(true);
new AndroidModule(emulator, vm).register(memory);
JNICLibrary = vm.resolveClass("com/taobao/wireless/security/adapter/JNICLibrary");
}
public static void main(String[] args) {
TppSign tppSign = new TppSign();
}
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("QKING ==> " + pathname);
return null;
}
}
3. 开始第一次初始化
// 这个方法重复调用,写成成员变量方便调用
public String methodSign = "doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;";
public void initMain() {
// 加载so
DalvikModule dm = vm.loadLibrary(new File(SO_SMAIN), true);
dm.callJNI_OnLoad(emulator);
DvmObject<?> context = vm.resolveClass("com/alipictures/moviepro/MovieproApplication", vm.resolveClass("android/content/Context")).newObject("taobao");
DvmObject<?> ret = JNICLibrary.callStaticJniMethodObject(
emulator, methodSign, 10101,
new ArrayObject(
context,
DvmInteger.valueOf(vm, 3),
new StringObject(vm, ""),
new StringObject(vm, "/data/user/0/" + PACKAGE_NAME + "/app_SGLib"),
new StringObject(vm, "")
));
System.out.println("QKING, initMain.ret-10101: " + ret.getValue().toString());
}
- 接下来就是认真补环境了
- 这里getPackageCodePath方法是获取/data/app/包名下的base.apk的绝对路径(不懂可以百度、谷歌)
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "com/alipictures/moviepro/MovieproApplication->getPackageCodePath()Ljava/lang/String;": {
// APK_INSTALL_PATH : /data/app/com.alipictures.moviepro-lYhmlkDhtgvVNT1zH6qZIQ\==/base.apk
return new StringObject(vm, APK_INSTALL_PATH);
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
- 继续运行
- 这里返回值是一个File对象,直接new一个对象返回,在后续再做处理
case "com/alipictures/moviepro/MovieproApplication->getFilesDir()Ljava/io/File;": {
return vm.resolveClass("java/io/File").newObject(signature);
}
- 继续运行
- 这里就是对刚才我们直接返回的File对象的处理
case "java/io/File->getAbsolutePath()Ljava/lang/String;": {
String sig = dvmObject.getValue().toString();
System.out.println("qking sig:" + sig);
if(sig.equals("com/alipictures/moviepro/MovieproApplication->getFilesDir()Ljava/io/File;")){
return new StringObject(vm, "/data/user/0/com.alipictures.moviepro/files");
}
break;
}
- 继续运行,这需要一个ApplicationInfo对象。
- 直接new一个ApplicationInfo对象返回
case "com/alipictures/moviepro/MovieproApplication->getApplicationInfo()Landroid/content/pm/ApplicationInfo;": {
return new ApplicationInfo(vm);
}
- 继续向下
- nativeLibraryDir变量是app的lib文件夹目录,为/data/app/包名下的lib/arm文件夹
case "android/content/pm/ApplicationInfo->nativeLibraryDir:Ljava/lang/String;": {
// dataAppPath:/data/app/com.alipictures.moviepro-lYhmlkDhtgvVNT1zH6qZIQ==
return new StringObject(vm, dataAppPath + "/lib/arm");
}
- 继续运行, registerAppLifeCyCleCallBack是一个没有传参没有返回值的方法,这里直接返回空
- 继续运行,这里通过方法名readFromSPUnified可以看出,这里是去读一个什么东西。这里直接去看源码
- jadx查看源码,发现这个类没有反编译出来,直接换jeb来看。
- jeb打开,找到对应的方法。这里他去调用了SPUtility2.a(arg3, arg4, arg5)方法,之间进去看。
- 发现这里他去调用SPUtility2.a(“SGMANAGER_DATA2”),再进去看。
- 发现这里是去读文件:SPUtility2.a.getFilesDir().getAbsolutePath() + 刚才传进来的文件名
- 直接去手机里查找:
- 发现SGMANAGER_DATA2文件中就是一个key-value的对象
- 然后获取数据是用 arg1 + “_” + arg2去取
case "com/taobao/wireless/security/adapter/common/SPUtility2->readFromSPUnified(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": {
String arg1 = varArg.getObjectArg(0).getValue().toString();
String arg2 = varArg.getObjectArg(1).getValue().toString();
String key = arg1 + "_" + arg2;
System.out.println("KEY==> "+ key);
JSONObject data = JSONObject.parseObject("{\"dynamicreid_dynamicreid\":\"8d8ae5b64573be6\",\"dynamicrsid_dynamicrs这里还有很多,直接省略了\"}");
String result = data.getString(key);
System.out.println("data ==> " + result);
if (result != null) {
return new StringObject(vm, result);
}
return null;
}
- 下一步
- 直接看源码,这里做了一个判断,如果f==0,去赋值e = 1,f = 1。如果f!= 0,直接返回e,这里就明显了,e==1,所以直接返回1即可
- 继续向下
- 看源码,这里返回值v9=v3=r17(第二个参数)
- 直接返回第二个参数
- 把int数据格式转换一下返回
case "java/lang/Integer-><init>(I)V": {
int input = varArg.getIntArg(0);
return DvmInteger.valueOf(vm, input);
}
- 跑出结果了
4. 第二个初始化函数
- 10102第一次,这个函数运行时没有加载别的so文件,直接可以运行成功,所以可以加到刚才问方法里顺序执行。
5. 第三个初始化函数
- 10102第二次,这里会去加载libsgsecuritybodyso-6.5.26.so文件
final String SO_BODY = "unidbg-android/src/test/resources/ali/taopiaopiao/libsgsecuritybodyso-6.5.26.so";
public void initSecurityBody() {
DalvikModule sb = vm.loadLibrary(new File(SO_BODY), true);
sb.callJNI_OnLoad(emulator);
DvmObject<?> ret = JNICLibrary.callStaticJniMethodObject(
emulator, methodSign, 10102,
new ArrayObject(
new StringObject(vm, "securitybody"),
new StringObject(vm, "6.5.26"),
new StringObject(vm, dataAppPath + "/lib/arm/libsgsecuritybodyso-6.5.26.so")
));
System.out.println("QKING, initMain.ret-10102-2: " + ret.getValue().toString());
}
- 直接运行
- 直接new一个ClassLoader对象并返回
case "com/alibaba/wireless/security/securitybody/SecurityGuardSecurityBodyPlugin->getPluginClassLoader()Ljava/lang/ClassLoader;":{
return new ClassLoader(vm, signature);
}
- 继续
- 直接newObject
case "com/taobao/dp/util/CallbackHelper->getInstance()Lcom/taobao/dp/util/CallbackHelper;": {
return dvmClass.newObject(signature);
}
- 这里设置slot的值,这里添加一个成员变量slot
- 添加代码
private long slot;
@Override
public void setStaticLongField(BaseVM vm, DvmClass dvmClass, String signature, long value) {
switch (signature) {
case "com/alibaba/wireless/security/framework/SGPluginExtras->slot:J": {
this.slot = value;
return;
}
}
super.setStaticLongField(vm, dvmClass, signature, value);
}
- 这里是获取刚才设置的值,直接返回
@Override
public long getStaticLongField(BaseVM vm, DvmClass dvmClass, String signature) {
switch (signature) {
case "com/alibaba/wireless/security/framework/SGPluginExtras->slot:J": {
return this.slot;
}
}
return super.getStaticLongField(vm, dvmClass, signature);
}
- 继续运行
- 直接返回类名
case "dalvik/system/PathClassLoader->findClass(Ljava/lang/String;)Ljava/lang/Class;": {
String clazzName = varArg.getObjectArg(0).getValue().toString();
System.out.println("findclass:" + clazzName);
return vm.resolveClass(clazzName);
}
- 继续, 这里去输出他的传参。传参为:accessibility,这里的意思是去获取AccessibilityManager对象,用来管理无障碍模式的。这里直接返回空,正常人谁开无障碍呀。。
- 继续,这里调用callStaticVoidMethod无传参无返回值,直接返回null
- 至此,第三次初始化完成
6. 最后一次初始化
- 这里还是回去加载一个so文件
final String SO_MIDD = "unidbg-android/src/test/resources/ali/taopiaopiao/libsgmiddletierso-6.5.23.so";
public void initMidd() {
DalvikModule midd = vm.loadLibrary(new File(SO_MIDD), true);
midd.callJNI_OnLoad(emulator);
DvmObject<?> ret = JNICLibrary.callStaticJniMethodObject(
emulator, methodSign, 10102,
new ArrayObject(
new StringObject(vm, "middletier"),
new StringObject(vm, "6.5.23"),
new StringObject(vm, dataAppPath + "/lib/arm/libsgmiddletierso-6.5.23.so")
));
System.out.println("QKING, initMain.ret-10102-3: " + ret.getValue().toString());
}
- 运行,SDK_INT直接返回23
- 直接结束
7. 正式执行目标函数
- 这里不需要再加载so,直接把hook到的数据传入进行调用
public void getXSignAll() {
DvmObject<?> ret = JNICLibrary.callStaticJniMethodObject(
emulator, methodSign, 70102,
new ArrayObject(
new StringObject(vm, "23632979"),
new StringObject(vm, "Yl7Bnq3bwrgDACsiIRXKipG+&&&23632979&88bb23b6258b4ec22f9a4779b2b1c83c&1661343561&mtop.common.gettimestamp&*&&10005894&AvH8ID-LlSsyBuvgEtWSp0OGnvcpTMm66qRu16fKJFxl&&&&27&&&&&&&"),
DvmBoolean.valueOf(vm, false),
DvmInteger.valueOf(vm, 0),
new StringObject(vm, "mtop.common.gettimestamp"),
new StringObject(vm, "pageId=&pageName="),
null, null, null ));
System.out.println("QKING, getXSign.ret-70102: " + ret.getValue().toString());
}
- 运行报错,百度了一堆,说的是环境没有补全,最后发现其实是文件加载没有补
- 补一个apk
- 继续运行
- 这里和之前一样,先直接返回一个File对象
- 继续补一个文件路径
- 继续往下
- 这里需要网络接口的列表,返回值是一个枚举对象,这里列出常见的名称
case "java/net/NetworkInterface->getNetworkInterfaces()Ljava/util/Enumeration;":{
String[] NetworkInterfaceNameList = new String[]{"dummy0","r_rmnet_data2","r_rmnet_data3","ip_vti0","wlan0","wlan1"};
int length = NetworkInterfaceNameList.length;
List<DvmObject<?>> NetworkInterfacelist = new ArrayList<>();
for (int i = 0; i < length; i++) {
NetworkInterfacelist.add(vm.resolveClass("java/net/NetworkInterface").newObject(NetworkInterfaceNameList[i]));
}
return new Enumeration(vm, NetworkInterfacelist);
}
- 继续运行
- 补一个Enumeration的方法调用
@Override
public boolean callBooleanMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/util/Enumeration->hasMoreElements()Z": {
System.out.println("hasMoreElements ===> " + dvmObject);
return ((Enumeration)dvmObject).hasMoreElements();
}
}
return super.callBooleanMethod(vm, dvmObject, signature, varArg);
}
- 继续,这里补的方法同上
- 直接返回false
- 继续运行
- 这里补base.apk路径
case "android/content/pm/ApplicationInfo->sourceDir:Ljava/lang/String;": {
return new StringObject(vm, APK_INSTALL_PATH);
}
- doAdapter, 这里最好的方式是通过hook传入具体的参数,返回真实的结果
- hook代码如下:
function callDoAdapter(i) {
Java.perform(
function() {
Java.enumerateClassLoaders({
"onMatch": function (loader) {
if (loader.toString().indexOf("libsgsecuritybody.so") >= 0) {
// 将当前class factory中的loader指定为我们需要的
Java.classFactory.loader = loader;
console.log("loader = ", loader.toString());
let SecurityBodyAdapter = Java.classFactory.use("com.alibaba.wireless.security.securitybody.SecurityBodyAdapter");
var ret = SecurityBodyAdapter.doAdapter(i);
console.log("libsgsecuritybody.so return ==> ", ret);
}
},
"onComplete": function () {
console.log("success");
}
});
})
}
- 补一个Thread.currentThread()
- 这需要返回当前的堆栈
case "java/lang/Thread->getStackTrace()[Ljava/lang/StackTraceElement;": {
StackTraceElement[] elements = ((Thread) dvmObject.getValue()).getStackTrace();
DvmObject[] objs = new DvmObject[elements.length];
for (int i = 0; i < elements.length; i++) {
System.out.println(elements[i]);
objs[i] = vm.resolveClass("java/lang/StackTraceElement").newObject(elements[i]);
}
return new ArrayObject(objs);
}
- 这里直接调用StackTraceElement对象的getClassName方法
- 这里明显是检测一些设备信息,直接hook查看对应的值
- hook代码
function doCom(i) {
Java.perform(function() {
Java.enumerateClassLoaders({
"onMatch": function(loader) {
if (loader.toString().indexOf("libsgmain.so") >= 0 ) {
Java.classFactory.loader = loader; // 将当前class factory中的loader指定为我们需要的
console.log("loader = ",loader.toString());
}
},
"onComplete": function() {
console.log("success");
}
});
let DeviceInfoCapturer = Java.classFactory.use("com.taobao.wireless.security.adapter.datacollection.DeviceInfoCapturer");
var result = DeviceInfoCapturer.doCommandForString(i);
console.log("result ==> " + result)
})
}
- 设备信息补完后继续运行
- 这里直接new一个hashMap即可
- 这里是map的put方法,直接帮他实现一下就行
- 终于跑出结果了。。。