任务描述 apk 反编译,找到com.sina.weibo.security,我们的目标是其中的 calculateS 函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 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;public class WeiboSecurityUtils { public static WeiboSecurityUtils instance = null ; private static Object mCalculateSLock = new Object (); 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" ); } 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)) { return sIValue; } String deviceSerial = getImei(context); if (TextUtils.isEmpty(deviceSerial)) { deviceSerial = getWifiMac(context); } if (TextUtils.isEmpty(deviceSerial)) { deviceSerial = "000000000000000" ; } if (context == null || TextUtils.isEmpty(deviceSerial)) { return "" ; } String iValue = getInstance().getIValue(context.getApplicationContext(), deviceSerial); sIValue = iValue; return iValue; } public static String getImei (Context context) { if (TextUtils.isEmpty(sImei) && context != null ) { if (PermissionUtils.hasSelfPermissions(WApplication.cContext, MsgConstant.PERMISSION_READ_PHONE_STATE)) { sImei = ((TelephonyManager) context.getSystemService("phone" )).getDeviceId(); } else { sImei = null ; } } return sImei; } public static String getWifiMac (Context context) { if (TextUtils.isEmpty(sMac) && context != null ) { WifiInfo mac = ((WifiManager) context.getApplicationContext().getSystemService(UtilityImpl.NET_TYPE_WIFI)).getConnectionInfo(); sMac = mac != null ? mac.getMacAddress() : "" ; } return sMac; } }
使用frida Hook 目标函数的返回值,代码如下。
1 2 3 4 5 6 7 8 9 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); })
结果为 d74a75bb,本篇目标是使用 Unidbg 复现对 calculateS 的调用。
初始化 在 Unidbg 的 unidbg-android/src/test/java 下新建包和类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package com.weibo;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 WeiBo extends AbstractJni { private final AndroidEmulator emulator; private final DvmClass WeiboSecurityUtils; private final VM vm; public WeiBo () { 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 ("unidbg-android/src/test/resources/weibo/sinaInternational.apk" )); vm.setJni(this ); 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) { WeiBo wb = new WeiBo (); } }
apk lib 目录下只包含armeabi动态库,所以没得选,只能是ARM32
创建虚拟机 Unidbg 中的 VM 类代表了 Android 虚拟机环境,用于模拟和执行 JNI 方法及与底层库的交互。它不仅模拟了 Android 上的 Java 层,还提供了一些便捷的工具来加载、解析 APK 文件及其资源。主要用于模拟应用在 JNI 层的交互,执行 Android 动态链接库(SO)的操作。
createDalvikVM有两个重载方法,建议使用第二个。
1 2 3 4 VM dalvikVM = emulator.createDalvikVM ();VM dalvikVM = emulator.createDalvikVM (new File ("apk file path" ));
Unidbg 加载 Apk 并非要做执行 DEX 甚至是运行 Apk 这样的大事,相反,它只是在做一些小事,主要包括下面两部分
解析 Apk 基本信息,减少使用者在补 JNI 环境上的工作量。Unidbg 会解析 Apk 的版本名、版本号、包名、 Apk 签名等信息。如果样本通过 JNI 调用获取这些信息,Unidbg 会替我们做处理。如果没有加载 Apk,这些逻辑就需要我们去补环境,平添了不少工作量。
解析和管理 Apk 资源文件,加载 Apk 后可以通过 openAsset获取 APK assets目录下的文件。如果样本通过AAssetManager_open等函数访问 apk 的assets,Unidbg 会替我们做处理。
Unidbg 通过依赖开源项目 apk-parser 来完成 APK 文件的解析工作。
加载 SO loadLibrary API 概述我们通过 loadLibrary API 将 SO 加载到 Unidbg 中,它有数个重载方法,下面两个使用最多。
1 2 3 4 5 6 7 DalvikModule loadLibrary (File elfFile, boolean forceCallInit) ; DalvikModule loadLibrary (String libname, boolean forceCallInit) ;
**loadLibrary(String libname, boolean forceCallInit)**:该方法接受库文件的名字(不包含 lib 前缀和 .so 后缀),并且会自动处理文件的加载。这个方法的使用类似于 Java 中的 System.loadLibrary(),它会根据传入的库名自动查找并加载相应的动态库。
使用 loadLibrary 加载 SO 文件 对于使用库名而非文件路径的 loadLibrary 方法,Unidbg 会自动为库名添加前缀(lib)和后缀(.so),然后尝试加载对应的 SO 文件。具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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 ); }
**findLibrary(soName)**:这个方法会查找目标 SO 文件。如果文件路径是正确的,它会返回一个 LibraryFile 实例,代表 SO 文件的内容。如果找不到文件,抛出异常。
**emulator.getMemory().load(libraryFile, forceCallInit)**:该方法将 SO 文件加载到模拟器的内存中,确保 JNI 调用能够正常执行。forceCallInit 参数决定是否强制执行初始化函数,如 init_proc、init_array 等。
如何根据库名找到 SO 文件 当你传入一个动态库的名字(如 kwsgmain)时,Unidbg 会根据以下逻辑自动查找 SO 文件:
从 APK 文件加载 SO
如果加载 APK 时已经解析了其内容,Unidbg 会从 APK 文件中的 lib/armeabi-v7a/ 或 lib/arm64-v8a/ 等目录查找 SO 文件。
SO 文件依赖关系的处理
Unidbg 在加载 SO 文件时,能够自动处理 SO 文件的依赖关系。即,如果一个 SO 文件依赖于其他动态库,Unidbg 会递归加载这些依赖库,确保所有的函数调用都能正确执行。这也是为什么推荐使用库名加载方法(loadLibrary(libname, true))的原因之一。
加载从真实环境中 Dump 出的内存 除了通过文件路径或库名加载 SO 文件外,Unidbg 还支持直接从字节数组加载 SO 文件:
1 DalvikModule loadLibrary (String libname, byte [] raw, boolean forceCallInit) ;
**raw**:这是从真实环境中 dump 出来的 SO 文件的字节数据。这个重载方法目前还缺少优化,因此使用上可能存在一定的限制和不稳定性,特别是对于复杂的依赖关系和初始化函数。
推荐的加载方式
在大多数情况下,推荐使用 loadLibrary(String libname, boolean forceCallInit) 方法来加载动态库。因为它能够处理 SO 文件的依赖关系,且在加载 APK 时会自动查找并加载 APK 中 lib 目录下的动态库。只传入库名(如 kwsgmain),即可自动处理前缀和后缀。
加载依赖模块 如何查看依赖库 在分析 SO 文件时,通常可以使用工具如 IDA 、objdump 或 readelf 来查看动态库的依赖项。这些工具会列出库文件中使用的所有外部符号,并显示它们依赖的其他 SO 文件。
Unidbg 自动解析依赖库 Unidbg 具有自动解析和加载依赖库的能力。它会解析一个 SO 文件的依赖关系,并尝试自动加载这些依赖项。Unidbg 使用 ElfLoader 来处理 ELF 格式的 SO 文件,它会检查是否已经加载所依赖的库。如果库尚未加载,它会尝试加载依赖库。
Android Linker (Android 系统中的动态链接器)会在系统库路径(如 /vendor/lib)和应用库路径(如 /data/app/packageName/base.apk!/lib/armeabi-v7a)中查找依赖库。
Unidbg ELF Loader 也会做类似的处理,但其实现更加简化。Unidbg 会通过以下方式来查找和加载依赖库:
如何解决依赖库的加载 Unidbg 会在加载一个动态库时,检查该库是否依赖于其他库,并且尝试自动加载这些依赖库。
1 2 3 4 5 LibraryFile neededLibraryFile = libraryFile.resolveLibrary(emulator, neededLibrary);if (libraryResolver != null && neededLibraryFile == null ) { neededLibraryFile = libraryResolver.resolveLibrary(emulator, neededLibrary); }
resolveLibrary:这是 Unidbg 用于查找库文件的函数。它首先会查找库文件是否已经加载,如果没有加载,它会尝试通过 libraryResolver 来解析该库。
系统库的加载
Unidbg 为系统库(如 libc.so)提供了一个默认的系统库环境。你可以通过 setLibraryResolver 来指定使用哪一版本的 Android 系统库(如 SDK 19 或 SDK 23)。
SDK 23 (Android 6.0)和 SDK 19 (Android 4.4)是两种常用的 Android 系统库环境。
对于 64 位 SO 文件,必须选择 SDK 23,因为 Android 4.4 不支持 64 位架构。
加载依赖库的具体实现 在 Unidbg 中,resolveLibrary 函数用于查找依赖库。它会根据库名和 SDK 版本查找库的路径。
1 2 3 4 5 6 7 8 9 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**:通过指定的 SDK 版本和库名,Unidbg 会构建库文件的路径并尝试从资源文件中加载。如果找到了该资源,它会返回一个 LibraryFile 实例。
用户库路径下的依赖 除了系统库,Unidbg 还可以在用户的库路径下查找依赖库,通常是在 APK 文件的 lib 目录下。例如,32 位库通常位于 lib/armeabi-v7a/ 或 lib/armeabi/ 目录下,64 位库则位于 lib/arm64-v8a/ 目录下。采用 libname 方式加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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); } 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; }
**loadLibraryData**:此方法从 APK 文件中加载指定的 SO 文件。如果找到该文件,返回其字节数据,供后续加载使用。
手动加载多个依赖库 有时,Unidbg 无法自动加载某些依赖库,用户可以手动加载多个 SO 文件:
1 2 vm.loadLibrary("kwsgmain" , true ); vm.loadLibrary("abc" , true );
通过这种方式,用户可以显式加载多个库,确保所有依赖都得到满足。
发起调用 1 2 3 4 5 6 7 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,为什么要这么处理?
Context 作为 Android 中最常见的参数 :Context 是 Android 系统中访问系统服务和资源的核心组件,因此许多 JNI 函数会需要 Context 参数。在 unidbg 中,为了模拟 Android 环境并让这些函数正常运行,需要通过加载 Application 类来创建一个模拟的 Context 对象。
为什么要特殊处理 Context 参数 :由于 unidbg 运行的是一个模拟环境,它并不具备完整的 Android 系统框架。因此,unidbg 通过 resolveClass 加载 Context 相关的类,创建了一个虚拟的 Context 对象,以便正确地模拟 JNI 调用。
报错:
1 2 3 JNIEnv->FindClass(android/content/pm/PackageManager) was called from RX@0x12002c79[libutility.so]0x2c79 [10:22:12 989] 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;
Unidbg 并不完全模拟 Android 框架的所有功能。很多 Android 系统提供的类(如 PackageManager)和方法(ContextWrapper.getPackageManager())默认是不可用的。
在这种情况下,当目标库或应用试图调用这些系统类的函数时,Unidbg 无法直接提供其实现,从而抛出 UnsupportedOperationException 异常。因此我们需要在callObjectMethod函数中实现这个函数,简单的实现则是返回它所需要的值(一般用于占位即可),补环境代码如下。
1 2 3 4 5 6 7 8 9 @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); }
继续运行,出现报错
1 2 3 4 5 6 7 JNIEnv->CallObjectMethod(android.content.pm.Signature@3ac3fd8b, toByteArray() => [B@6a5fc7f7) was called from RX@0x12002d71 [libutility.so]0x2d71 Invalid address 0x12175000 passed to free: value not allocated [crash]A/libc: Invalid address 0x12175000 passed to free: value not allocated exit with code: 1 Exception in thread "main" java.lang.NullPointerException at com.weibo.Weibo.callS(Weibo.java:36 ) at com.weibo.Weibo.main(Weibo.java:41 )
这个意思是传递给free函数的地址0x12175000是无效的,导致内存释放失败。遇到这个报错,最简单的处理办法就是 hook free 函数,替换它的实现,让它返回 0,即释放成功。当然也可以对报错的待释放内存做处理,比如只有指针地址是 0x12175000 释放失败时。补环境代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 emulator.attach().addBreakPoint(dm.getModule().findSymbolByName("free" ).getAddress(), new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { Arm32RegisterContext registerContext = emulator.getContext(); emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0 ); emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLR()); return true ; } }); 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); } } }); }
完整:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package com.weibo;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Emulator;import com.github.unidbg.arm.backend.Unicorn2Factory;import com.github.unidbg.arm.context.Arm32RegisterContext;import com.github.unidbg.debugger.BreakPointCallback;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 Weibo extends AbstractJni { private final AndroidEmulator emulator; private final DvmClass WeiboSecurityUtils; private final VM vm; public Weibo () { 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 ("unidbg-android/src/test/resources/weibo/sinaInternational.apk" )); vm.setJni(this ); vm.setVerbose(true ); DalvikModule dm = vm.loadLibrary("utility" , true ); emulator.attach().addBreakPoint(dm.getModule().findSymbolByName("free" ).getAddress(), new BreakPointCallback () { @Override public boolean onHit (Emulator<?> emulator, long address) { Arm32RegisterContext registerContext = emulator.getContext(); emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0 ); emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLR()); return true ; } }); WeiboSecurityUtils = vm.resolveClass("com/sina/weibo/security/WeiboSecurityUtils" ); dm.callJNI_OnLoad(emulator); } 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" ; return WeiboSecurityUtils.newObject(null ).callJniMethodObject(emulator, "calculateS" , context, arg2, arg3).getValue().toString(); } public static void main (String[] args) { Weibo wb = new Weibo (); String result = wb.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); } }