一、引言 目标 APK:sinaInternational.apk
目标方法:com.sina.weibo.security.calculateS
目标方法实现:libutility.so
二、任务描述 jadx反编译apk,得到目标函数 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 10 11 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); }) }
结果为 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 40 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; private final Memory memory; public WeiBo () { 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 ("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
。这篇讨论创建虚拟机对象、加载 SO 这两件事。
3.1 创建虚拟机 Unidbg 的VM
主要用于处理 JNI 逻辑,这里我们通过如下方式创建。
1 vm = emulator.createDalvikVM(new File ("unidbg-android/src/test/resources/weibo/sinaInternational.apk" ));
createDalvikVM
有两个重载方法,建议使用第二个。
1 2 3 4 VM dalvikVM = emulator.createDalvikVM();VM dalvikVM = emulator.createDalvikVM(new File ("apk file path" ));
加载 APK 这一行为让很多人对 Unidbg 产生了误解,觉得它不是 SO 模拟器,而是应用级的模拟器,就像雷电或夜神模拟器那样。
事实上,Unidbg 加载 Apk 并非要做执行 DEX 甚至是运行 Apk 这样的大事,相反,它只是在做一些小事,主要包括下面两部分
解析 Apk 基本信息,减少使用者在补 JNI 环境上的工作量。 Unidbg 会解析 Apk 的版本名、版本号、包名、 Apk 签名等信息。如果样本通过 JNI 调用获取这些信息,Unidbg 会替我们做处理。如果没有加载 Apk,这些逻辑就需要我们去补环境,平添了不少工作量。
解析和管理 Apk 资源文件 ,加载 Apk 后可以通过 openAsset
获取 APK assets
目录下的文件。如果样本通过AAssetManager_open
等函数访问 apk 的assets
,Unidbg 会替我们做处理。
综上所述,创建虚拟机时加载 Apk 更省事。除此之外,Unidbg 使用 apk-parser 这个开源项目完成 Apk 的解析工作,读者可以在自己的项目里使用它。
1 2 3 4 5 <dependency> <groupId>net.dongliu</groupId> <artifactId>apk-parser</artifactId> <version>2.6 .10 </version> </dependency>
3.2 加载 SO 我们通过 loadLibrary API 将 SO 加载到 Unidbg 中,它有数个重载方法,下面两个使用最多。
1 2 3 4 5 6 7 DalvikModule loadLibrary (File elfFile, boolean forceCallInit) ; DalvikModule loadLibrary (String libname, boolean forceCallInit) ;
可以发现区别在于第一个参数,前者传入文件后者传入动态库的名字。
后者在使用上近似于 JAVA 的System.loadLibrary(soName)
,名字要掐头去尾,如 libkwsgmain.so 对应为 kwsgmain。
1 vm.loadLibrary("kwsgmain" , true );
loadLibrary
内部会为libname
再添头添尾。
1 2 3 4 5 6 7 8 9 10 @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,如果不加载 APK 就没法处理 ,下面看看具体代码。
首先是 32 位的处理,去找armeabi-v7a
以及armeabi
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
下找。
1 2 3 4 5 6 7 8 9 10 11 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 出的内存 。
1 loadLibrary(String libname, byte [] raw, boolean forceCallInit)
这个 API 缺少维护和优化,并不好用。
在一般情况下,建议使用传入动态库名字的那一个,因为它能更好的处理 SO 的依赖模块。
3.3 加载依赖模块 几乎没有 SO 可以不依赖任何外部库就正常工作,比如各种各样的 C 标准库函数、Android FrameWork 提供的一些库,以及样本的自定义库等等。
可以在 IDA 反汇编界面的头部查看依赖库信息 ,又或者使用 objdump、readelf 等工具也可。
尽管都会依赖外部 SO,但在这两篇文章里,我们似乎都没有用loadlibrary
加载所需的这些 SO。这是因为在 SO 的装载逻辑里,往往会解析依赖库,检查依赖库是否加载到当前内存中,如果未加载就尝试加载等等。
Android linker 会这么做,Unidbg 的 ElfLoader 当然也会这么做。尝试加载外部 SO 时,Android Linker 会查找/vendor/lib
这样的系统库目录以及/data/app/packageName/base.apk/lib/armeabi-v7a
这样的用户库目录。
Unidbg 的 ELF Loader 也做了类似处理,但逻辑上要简单很多。
对于系统库 SO,Unidbg 存在着一个系统库环境,包含着以 libc.so 为代表的常见 SO。用户需要通过setLibraryResolver
确认使用哪一个系统库文件,参数可选是 23 和 19。
1 2 3 4 final Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 ));
23 和 19 分别对应于 sdk23(Android 6.0 支持64位) 和 sdk19(Android 4.4 不支持64位)的运行库环境,它们都在unidbg-android/src/main/resources/android
目录下。如果希望使用更高版本的运行时库,可以参考unidbg-android/pull.sh
,使用自己测试机的类库。
接下来看看 Unidbg 寻找依赖 SO 的代码逻辑
1 2 3 4 LibraryFile neededLibraryFile = libraryFile.resolveLibrary(emulator, neededLibrary);if (libraryResolver != null && neededLibraryFile == null ) { neededLibraryFile = libraryResolver.resolveLibrary(emulator, neededLibrary); }
第二处 resolveLibrary 用于寻找系统库路径下是否有对应 SO
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 用于寻找用户库路径下是否有对应 SO。上一小节说用 libname 比 libFile 更适合处理依赖模块的原因就在这里。
如果采用 libFile 方式加载,那就会在 libFile 的同级目录下寻找对应 SO。
1 2 3 4 5 @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。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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; }
那么使用 libFile 加载时,使用者不仅要在 lib 下拷贝出目标 SO,最好也将其他 SO 拷贝出来,以防存在依赖,通过 libname 加载则不需要考虑这个问题。
当然读者也可以手动加载多个 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,为什么要这么处理???
运行后报错信息如下
1 2 3 4 5 6 JNIEnv->FindClass(android / content / pm / PackageManager) was called from RX@0x12002c79 [libutility .so ] 0x2c79 WARN [com .github .unidbg .linux .ARM32SyscallHandler ] ... 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)
显然,我们定义的Application类没有实现getPackageManager函数,因此我们需要在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 Invalid address 0 x12175000 passed to free: value not allocated[crash] A/libc: Invalid address 0 x12175000 passed to free: value not allocated Exception in thread "main" java.lang .NullPointerException : Cannot invoke "com.github.unidbg.linux.android.dvm.DvmObject.getValue()" because the return value of "com.github.unidbg.linux.android.dvm.DvmObject.callJniMethodObject(com.github.unidbg.Emulator, String, Object[])" is null at com.sina .weibo .security .WeiboSecurityUtils .calculateS (WeiboSecurityUtils.java :47 ) at com.sina .weibo .security .WeiboSecurityUtils .main (WeiboSecurityUtils.java :64 )
这个意思是传递给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); } } }); }
运行出结果。
五、参考 Unidbg 的基本使用(二)
[补库函数(五)](https://www.yuque.com/lilac-2hqvv/xdwlsg/bzoykwvuim3hkz2o?# 《补库函数(五)》)