从实践中学习unidbg使用(二)

  1. 从实践中学习unidbg使用(二)
    1. 一、引言
    2. 二、任务描述
    3. 三、初始化
      1. 3.1创建虚拟机
      2. 3.2加载SO
      3. 3.3加载依赖模块
    4. 四、发起调用
      1. 参考

从实践中学习unidbg使用(二)

一、引言

本篇中的APK详见参考文章

目标APK: sinaInternational.apk

目标方法实现:libutility.so

二、任务描述

apk放到JADX中进行反编译,并找到com.sina.weibo.security类。目标函数是其中的calculateS函数

package com.sina.weibo.security;

import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.taobao.accs.utl.UtilityImpl;
import com.umeng.message.MsgConstant;
import com.weico.international.WApplication;
import permissions.dispatcher.PermissionUtils;

/* loaded from: classes.dex */
public class WeiboSecurityUtils {
    public static WeiboSecurityUtils instance;
    private static Object mCalculateSLock;
    private static String sIValue;
    private static String sImei;
    private static String sMac;
    private static String sSeed;
    private static String sValue;

    public native String calculateS(Context context, String str, String str2);

    public native String getIValue(Context context, String str);

    static {
        System.loadLibrary("utility");
        instance = null;
        sImei = "";
        sMac = "";
        mCalculateSLock = new Object();
    }

    private WeiboSecurityUtils() {
    }

    public static synchronized WeiboSecurityUtils getInstance() {
        WeiboSecurityUtils weiboSecurityUtils;
        synchronized (WeiboSecurityUtils.class) {
            if (instance == null) {
                instance = new WeiboSecurityUtils();
            }
            weiboSecurityUtils = instance;
        }
        return weiboSecurityUtils;
    }

    public static String calculateSInJava(Context context, String srcArray, String pin) {
        String str;
        synchronized (mCalculateSLock) {
            if (srcArray.equals(sSeed) && !TextUtils.isEmpty(sValue)) {
                str = sValue;
            } else if (context != null) {
                sSeed = srcArray;
                sValue = getInstance().calculateS(context.getApplicationContext(), srcArray, pin);
                str = sValue;
            } else {
                str = "";
            }
        }
        return str;
    }

    public static String getIValue(Context context) {
        if (TextUtils.isEmpty(sIValue)) {
            String deviceSerial = getImei(context);
            if (TextUtils.isEmpty(deviceSerial)) {
                deviceSerial = getWifiMac(context);
            }
            if (TextUtils.isEmpty(deviceSerial)) {
                deviceSerial = "000000000000000";
            }
            if (context != null && !TextUtils.isEmpty(deviceSerial)) {
                String iValue = getInstance().getIValue(context.getApplicationContext(), deviceSerial);
                sIValue = iValue;
                return iValue;
            }
            return "";
        }
        return sIValue;
    }

    public static String getImei(Context context) {
        if (TextUtils.isEmpty(sImei) && context != null) {
            if (PermissionUtils.hasSelfPermissions(WApplication.cContext, MsgConstant.PERMISSION_READ_PHONE_STATE)) {
                TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService("phone");
                sImei = telephonyManager.getDeviceId();
            } else {
                sImei = null;
            }
        }
        return sImei;
    }

    public static String getWifiMac(Context context) {
        if (TextUtils.isEmpty(sMac) && context != null) {
            WifiManager wifi = (WifiManager) context.getApplicationContext().getSystemService(UtilityImpl.NET_TYPE_WIFI);
            WifiInfo mac = wifi.getConnectionInfo();
            sMac = mac != null ? mac.getMacAddress() : "";
        }
        return sMac;
    }
}

改类的大体分析如下

功能 方法 说明
加密计算 calculateS / calculateSInJava 基于种子和 PIN,生成加密签名
获取设备标识 getImei, getWifiMac 尝试获取唯一设备标识(IMEI 或 MAC)
获取加密设备标识 getIValue 基于设备信息调用 native 加密逻辑生成值
单例管理 getInstance 保证类唯一实例,控制资源和缓存

接下来就是使用Frida hook这个方法,获取其入参和返回值,以下是hook脚本

setTimeout(function () {
  Java.perform(function () {
    let WeiboSecurityUtils = Java.use(
      "com.sina.weibo.security.WeiboSecurityUtils"
    );
    WeiboSecurityUtils["calculateS"].implementation = function (
      context,
      str,
      str2
    ) {
      console.log(
        `WeiboSecurityUtils.calculateS is called: context=${context}, str=${str}, str2=${str2}`
      );
      let result = this["calculateS"](context, str, str2);
      console.log(`WeiboSecurityUtils.calculateS result=${result}`);
      return result;
    };
  });
}, 5000);

然后还是和上一篇文章**从实践中学习unidbg使用(一)**一样,主动使用获取到的入参进行附加进程主动调用该方法,然后获取到其返回值,再使用unidbg复现Frida hook的结果。主动调用的hook脚本可参考以下

function callcalculateS() {
  Java.perform(function () {
    let WeiboSecurityUtils = Java.use(
      "com.sina.weibo.security.WeiboSecurityUtils"
    );
    let current_application = Java.use(
      "android.app.ActivityThread"
    ).currentApplication();
    let arg1 = current_application.getApplicationContext();
    let arg2 = "hello world";
    let arg3 = "123456";
    let ret = WeiboSecurityUtils.$new().calculateS(arg1, arg2, arg3);
    console.log("ret: " + ret);
  });
}
callcalculateS();

结果为 d74a75bb

三、初始化

package com.test1;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.arm.backend.Unicorn2Factory;
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.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;

import java.io.File;

public class TEST1 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final DvmClass WeiboSecurityUtils;

    public TEST1() {
        emulator = AndroidEmulatorBuilder
                .for32Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .setProcessName("com.weico.international")
                .build();
        Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File("D:\\Compilation_Enviroment\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\test1\\sinaInternational.apk"));
        //设置当前类为虚拟机的JNI接口实现,即告诉虚拟机,如果 native 层调用 Java 方法、访问 Java 字段,使用我(这个类)来处理。
        vm.setJni(this);
        //打开 unidbg 的详细日志输出(verbose mode)
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary("utility", true);
        WeiboSecurityUtils = vm.resolveClass("com/sina/weibo/security/WeiboSecurityUtils");
        dm.callJNI_OnLoad(emulator);
    }

    public static void main(String[] args) {
        TEST1 test1 = new TEST1();
    }


}

apk lib 目录下只包含armeabi动态库,所以是ARM32。这篇讨论创建虚拟机对象、加载 SO 这两件事。

3.1创建虚拟机

Unidbg的VM主要用于处理JNI逻辑,这里我们通过如下方式创建。

vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/weibo/sinaInternational.apk"));

createDalvikVM有两个重载方法,建议使用第二个。

//创建虚拟机
VM dalvikVM = emulator.createDalvikVM();
//创建虚拟机并指定APK文件
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"));

加载APK这一行行为让很多人对Unidbg产生了误解,觉得它不是SO模拟器,而是应用级的模拟器,就像雷电或者夜神模拟器那样。

事实上,Unidbg加载APK并非要做执行DEX甚至是运行APK这样的大事,相反,它只是在做一些小事,主要包括下面两部分

  1. 解析APK基本信息,减少使用者在补JNI环境上的工作量。Unidbg会解析APK的版本名、版本号、包名、APK签名等信息。如果样本通过JNI调用获取这些信息、Unidbg会替我们做处理。如果没有加载APK,这些逻辑就需要我们去补环境,平添了不少工作量。
  2. 解析管理APK资源文件。加载APK后可以通过openAsset获取APK assets目录下的文件。如果样本通过AAssetManager_open等函数访问APK的assets,Unidbg会替我们做处理。

综上所述,创建虚拟机时加载APK会更省事。除此之外,Unidbg使用apk-parser这个开源项目完成APK的解析工作,读者可以在自己的项目里使用它。

<dependency>
    <groupId>net.dongliu</groupId>
    <artifactId>apk-parser</artifactId>
    <version>2.6.10</version>
</dependency>

3.2加载SO

我们通过loadLibrary API将SO加载到Unidbg中,它有数个重载方法,下面两个使用最多。

//参数一: 动态库或可执行ELF文件
//参数二: 是否必须执行 init_proc、init_array 这些初始化函数
DalvikModule loadLibrary(File elfFile, boolean forceCallInit);

//参数一:动态库或可执行ELF的文件名
//参数二: 是否必须执行 init_proc、init_array 这些初始化函数
DalvikModule loadLibrary(String libname, boolean forceCallInit);

可以发现区别在于第一个参数,前者传入文件后者则传入动态库的名字。

后者在使用上近似于Java的System.loadLibrary(soName),名字要掐头去尾,如libkwsgmain.so对应为kwsgmain。

vm.loadLibrary("kwsgmain", true);

loadLibrary内部会为libname再添头添尾。

@Override
public final DalvikModule loadLibrary(String libname, boolean forceCallInit) {
    String soName = "lib" + libname + ".so";
    LibraryFile libraryFile = findLibrary(soName);
    if (libraryFile == null) {
        throw new IllegalStateException("load library failed: " + libname);
    }
    Module module = emulator.getMemory().load(libraryFile, forceCallInit);
    return new DalvikModule(this, module);
}

只传入名字如何找到对应的SO文件并进行加载?这就依赖于3.1中所提到的加载APK,Unidbg会去APK的lib目录下寻找目标SO文件,如果不加载就没办法处理,下面看看具体代码。

首先是32位的处理,去找armeabi-v7a以及armeabi

byte[] loadLibraryData(Apk apk, String soName) {
    byte[] soData = apk.getFileData("lib/armeabi-v7a/" + soName);
    if (soData != null) {
        if (log.isDebugEnabled()) {
            log.debug("resolve armeabi-v7a library: " + soName);
        }
        return soData;
    }
    soData = apk.getFileData("lib/armeabi/" + soName);
    if (soData != null && log.isDebugEnabled()) {
        log.debug("resolve armeabi library: " + soName);
    }
    return soData;
}

再看看64位,去arm64-v8a下找。

byte[] loadLibraryData(Apk apk, String soName) {
    byte[] soData = apk.getFileData("lib/arm64-v8a/" + soName);
    if (soData != null) {
        if (log.isDebugEnabled()) {
            log.debug("resolve arm64-v8a library: " + soName);
        }
        return soData;
    } else {
        return null;
    }
}

loadLibrary还有一个重载,直接传入字节数组。它的主要应用场景是加载从真实环境中Dump出的内存。

loadLibrary(String libname, byte[] raw, boolean forceCallInit)

这个API缺少维护和优化,并不好用。

在一般情况下,选择哪一个加载SO文件?我建议使用传入动态库名字的那一个,因为它能更好的处理SO的依赖模块。

3.3加载依赖模块

几乎没有SO文件可以不依赖任何外部库就正常工作,比如各种各样的C标准库函数、Android FrameWork提供的一些库,以及样本的自定义库等等。

可以在IDA反汇编界面的头部查看依赖库信息,又或者使用objdump、readelf等工具也可以。
image-20250603102021157

尽管都会依赖外部SO文件,但在这两篇文章里,我们似乎没有用到loadLibrary加载所需的这些SO文件。这是因为在SO的装载逻辑里,往往会解析依赖库,检查依赖库是否加载到当前内存中,如果未加载就尝试加载等等。

Android linker会这么做,Unidbg的ELF Loader当然也会这么做。尝试加载外部SO时,Android Linker会查找**/verdor/lib这样的系统库目录以及/data/app/packageName/base.apk!/lib/armeabi-v7a**这样的用户库目录。

Unidbg的ELF Loader也做了类似的处理,但逻辑上要简单很多。

对于系统库SO,Unidbg存在着一个系统库环境,包含着以libc.so为代表的常见SO文件。用户需要通过setLibraryResolver确认使用哪一个系统库文件,参数可选是23和19.

// 模拟器的内存操作接口
final Memory memory = emulator.getMemory();
// 设置系统类库
memory.setLibraryResolver(new AndroidResolver(23));

23 和 19 分别对应于 sdk23(Android 6.0) 和 sdk19(Android 4.4)的运行库环境。
image-20250603102806733

处理64位的SO文件时只能选择SDK23,因为Android4.4以上没有对应的64位系统库。即使处理32位SO文件,我们一般都是用SDK23,因为它相对“新”一些。

如果读者希望使用更高版本的运行时库,可以参考unidbg-android/pull.sh,使用自己测试机的类库。但是,并非真机的SO放到Unidbg里就大功告成了,Unidbg为这两个环境做了一些优化和兼容性处理,比如多线程部分,你也需要做对应的处理才能让新环境可用。

接下来看看Unidbg寻找依赖SO的代码逻辑

LibraryFile neededLibraryFile = libraryFile.resolveLibrary(emulator, neededLibrary);
if (libraryResolver != null && neededLibraryFile == null) {
    neededLibraryFile = libraryResolver.resolveLibrary(emulator, neededLibrary);
}

第二处的resolveLibrary用于寻找系统库路径下是否有对应的SO,如下所示:

protected static LibraryFile resolveLibrary(Emulator<?> emulator, String libraryName, int sdk, Class<?> resClass) {
    final String lib = emulator.is32Bit() ? "lib" : "lib64";
    String name = "/android/sdk" + sdk + "/" + lib + "/" + libraryName.replace('+', 'p');
    URL url = resClass.getResource(name);
    if (url != null) {
        return new URLibraryFile(url, libraryName, sdk, emulator.is64Bit());
    }
    return null;
}

第一处resolveLibrary用于寻找用户库路径下是否有对应SO文件,如下所示。上一小节说用libname比libFile更适合处理依赖模块的原因就在这里。

如果采用libFile方法加载,那就会在libFile的统计目录下寻找对应SO文件。

@Override
public LibraryFile resolveLibrary(Emulator<?> emulator, String soName) {
    File file = new File(elfFile.getParentFile(), soName);
    return file.canRead() ? new ElfLibraryFile(file, is64Bit) : null;
}

如果采用libname方法加载,那就会在lib目录下寻找对应的SO文件。

@Override
public LibraryFile resolveLibrary(Emulator<?> emulator, String soName) {
    byte[] libData = baseVM.loadLibraryData(apk, soName);
    return libData == null ? null : new ApkLibraryFile(baseVM, this.apk, soName, libData, packageName, is64Bit);
}

// 32 位
byte[] loadLibraryData(Apk apk, String soName) {
    byte[] soData = apk.getFileData("lib/armeabi-v7a/" + soName);
    if (soData != null) {
        if (log.isDebugEnabled()) {
            log.debug("resolve armeabi-v7a library: " + soName);
        }
        return soData;
    }
    soData = apk.getFileData("lib/armeabi/" + soName);
    if (soData != null && log.isDebugEnabled()) {
        log.debug("resolve armeabi library: " + soName);
    }
    return soData;
}

那么使用libFile加载时,使用者不仅要在lib下拷贝出目标SO文件,最好也将其他的SO拷贝出来,以防存在依赖,通过libname加载则不需要考虑这个问题。
当然也可以采用手动加载多个SO文件,像下面这样。

vm.loadLibrary("kwsgmain", true);
vm.loadLibrary("abc", true);

四、发起调用

public String callcalculateS(){
	DvmObject<?> context = vm.resolveClass("android/app/Application", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);
	String arg2 = "hello world";
    String arg3 = "123456";
    String ret = WeiboSecurityUtils.newObject(null).callJniMethodObject(emulator, "calculateS", context, arg2, arg3).getValue().toString();
    return ret;
}

参数2、3都是字符串,按照前篇所述直接传入就行。

参数1时Android中最常见的Context,为什么要这么处理?这样处理相当微妙,我们在后面的文章里讨论它。

运行报错如下

JNIEnv->FindClass(android/content/pm/PackageManager) was called from RX@0x12002c79[libutility.so]0x2c79
[15:58:55 618]  WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:538) - handleInterrupt intno=2, NR=0, svcNumber=0x11e, PC=unidbg@0xfffe0274, LR=RX@0x12002c8d[libutility.so]0x2c8d, syscall=null
java.lang.UnsupportedOperationException: android/content/ContextWrapper->getPackageManager()Landroid/content/pm/PackageManager;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:867)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethod(DvmMethod.java:69)

报错原因就是我们在模拟执行的时候没有实现getPackageManager函数,因此我们需要在callObjectMethod函数中实现这个函数,简单的实现则是返回它所需要的值(一般用于占位即可),补环境代码如下。函数签名已经在报错中了,可以直接使用。

@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
    switch (signature){
        case "android/content/ContextWrapper->getPackageManager()Landroid/content/pm/PackageManager;":{
            return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
        }
    }
    return super.callObjectMethod(vm, dvmObject, signature, varArg);
}

继续运行,出现报错
image-20250603160922255

这个意思是传递给free函数的地址0x12175000是无效的,导致内存释放失败。遇到这个报错,最简单的处理办法就是 hook free 函数,替换它的实现,让它返回 0,即释放成功。当然也可以对报错的待释放内存做处理,比如只有指针地址是 0x12175000 释放失败时。

patch代码如下

public void patchFree(){
        IWhale whale = Whale.getInstance(emulator);
        Symbol free = memory.findModule("libc.so").findSymbolByName("free");

        whale.inlineHookFunction(free, new ReplaceCallback() {
            @Override
            public HookStatus onCall(Emulator<?> emulator, long originFunction) {
                System.out.println("WInlineHookFunction free = " + emulator.getContext().getPointerArg(0));
                long addr = emulator.getContext().getPointerArg(0).peer;
                if (addr == 0x12175000 || addr == 0x12176000){
                    return HookStatus.LR(emulator, 0);
                }
                else {
                    return HookStatus.RET(emulator,originFunction);
                }

            }
        });
    }

完整代码如下。

package com.test1;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.whale.IWhale;
import com.github.unidbg.hook.whale.Whale;
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.memory.Memory;
import unicorn.ArmConst;

import java.io.File;

public class TEST1 extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final DvmClass WeiboSecurityUtils;
    private final Memory memory;

    public TEST1() {
        emulator = AndroidEmulatorBuilder
                .for32Bit()
                .addBackendFactory(new Unicorn2Factory(true))
                .setProcessName("com.weico.international")
                .build();
        memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        vm = emulator.createDalvikVM(new File("D:\\Compilation_Enviroment\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\test1\\sinaInternational.apk"));
        //设置当前类为虚拟机的JNI接口实现,即告诉虚拟机,如果 native 层调用 Java 方法、访问 Java 字段,使用我(这个类)来处理。
        vm.setJni(this);
        //打开 unidbg 的详细日志输出(verbose mode)
        vm.setVerbose(true);
        DalvikModule dm = vm.loadLibrary("utility", true);

        WeiboSecurityUtils = vm.resolveClass("com/sina/weibo/security/WeiboSecurityUtils");
        dm.callJNI_OnLoad(emulator);

        patchFree();
    }

    public String callS(){
        DvmObject<?> context = vm.resolveClass("android/app/Application", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);
        String arg2 = "hello world";
        String arg3 = "123456";
        // String ret =
        return  WeiboSecurityUtils.newObject(null).callJniMethodObject(emulator, "calculateS", context, arg2, arg3).getValue().toString();
    }

    public static void main(String[] args) {
        TEST1 test1 = new TEST1();
        String result = test1.callS();
        System.out.println("call s result:"+result);
    }

    @Override
    public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
        switch (signature){
            case "android/content/ContextWrapper->getPackageManager()Landroid/content/pm/PackageManager;":{
                return vm.resolveClass("android/content/pm/PackageManager").newObject(null);
            }
        }
        return super.callObjectMethod(vm, dvmObject, signature, varArg);
    }

    public void patchFree(){
        IWhale whale = Whale.getInstance(emulator);
        Symbol free = memory.findModule("libc.so").findSymbolByName("free");

        whale.inlineHookFunction(free, new ReplaceCallback() {
            @Override
            public HookStatus onCall(Emulator<?> emulator, long originFunction) {
                System.out.println("WInlineHookFunction free = " + emulator.getContext().getPointerArg(0));
                long addr = emulator.getContext().getPointerArg(0).peer;
                if (addr == 0x12175000 || addr == 0x12176000){
                    return HookStatus.LR(emulator, 0);
                }
                else {
                    return HookStatus.RET(emulator,originFunction);
                }

            }
        });
    }


}

运行结果

call s result:d74a75bb

参考

[Unidbg 的基本使用(二)](https://www.yuque.com/lilac-2hqvv/xdwlsg/vy2p4ts1oq8sxxgb?# 《Unidbg 的基本使用(二)》)

[补库函数(五)](https://www.yuque.com/lilac-2hqvv/xdwlsg/bzoykwvuim3hkz2o?# 《补库函数(五)》)


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1621925986@qq.com

💰

×

Help us with donation