任务描述 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); } }