Xposed框架可谓是“家喻户晓”的神器,它具有着frida所不具备的持久性(虽然frida也可以通过frida-gadget实现持久化,但没有Xposed使用方便)。当我们需要hook java层的代码时,Xposed使用起来得心应手,但是随着软件开发者的安全意识越来越高,放在java层的核心代码也就越来少,这就导致Xposed使用起来有点力不从心,逆向分析者也就面临着如何使用Xposed对native进行hook的问题,下面的文章就对该问题提供一个解决思路。
Dobby框架的介绍
简介
Dobby是一个轻量级、多平台、多架构的inline hook框架,它使用起来轻快便捷,支持Windows/macOS/iOS/Android/Linux平台,且支持X86, X86-64, ARM, ARM64架构,因此我选择它作为inline hook的框架
环境准备
首先在Dobby的仓库中下载最新发布的版本
下载完成后解压,会看到里面有一个头文件和对应着四个架构的文件夹,文件夹中放着静态链接库文件,之后需要把这些文件添加到android studio的项目中
下面使用android studio创建一个native工程,然后把需要的文件导入到工程中,我的目录结构如下(重点看红框中的,不相干的文件暂时忽略)
然后编写CMakeLists.txt对我们导入的静态链接库做声明(只展示需要改动的部分),cmake的命令可以参看文档cmake-commands(7) — CMake 3.25.1 Documentation
使用方法
- DobbyCodePatch该方法的作用是修改内存中的数据,通常用来修改指令,在使用过程中要注意的是大小端的问题,在安卓平台是小端模式,所以要注意调整顺序,下面展示nop一个指令的示例 (注:getAbsoluteAddress是自定义的函数,在下面说明)
uint8_t nop[4] = {0xD5,0x3,0x20,0x1F};uint8_t * nop_ptr = nop;DobbyCodePatch((void*)getAbsoluteAddress("libxgVipSecurity.so", 0x20710),nop_ptr,4);
- DobbyHook该方法的作用是修改或者替换一个函数,下面给出一个替换函数的示例代码 (注:getAbsoluteAddress是自定义的函数,在下面说明)
char *(*old_sub_1FCCC)(char *, char *) = nullptr;char *new_sub_1FCCC(char *a1, char *a2) { char *result = old_sub_1FCCC(a1, a2); __android_log_print(6, "guagua", "data decrypt value is %s", result); if((strstr(result,"moreOtherData") - result) < 5){ char moreOtherData[93] = {""}; strncpy(moreOtherData,result+1,92); char *data_value = (char *) malloc(0x200); sprintf(data_value, "{%s%s", moreOtherData, "\"token\":\"35151312554131451445345314\""); __android_log_print(6, "guagua", "modified data decrypt value is %s", data_value); return data_value; } return result;}// hook sub_1FCCCDobbyHook((void*)getAbsoluteAddress("libxgVipSecurity.so", 0x1FCCC), (dobby_dummy_func_t)new_sub_1FCCC, (dobby_dummy_func_t *) &old_sub_1FCCC);
自定义的工具函数
在平时使用Dobby的时候还会遇到一个问题,当在so中有符号名时hook起来会方便一些,但很多函数在IDA中都是以sub_xxx命名的,这时候怎么获取函数的地址是一个难题,但好在有人造了轮子,我们可以省点力气,我在github上找到了两个文件,分别是Utils.h和Obfuscate.h,在使用时只需要导入Utils.h,然后就可以使用getAbsoluteAddress函数获取函数的地址了,(注:文件在文末分享)
使用Xposed注入so
上面介绍了Dobby的基本情况,这里还需要补充一点,我们需要把hook的代码写进JNI_OnLoad中,这样当so注入的时候才能自动执行我们的hook代码,JNI_OnLoad的示例代码如下:
jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(6, "guagua", "插件so注入成功");
JNIEnv *env = nullptr;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) == JNI_OK) {
// nop 0x20710
uint8_t nop[4] = {0xD5,0x3,0x20,0x1F};
__android_log_print(6, "guagua", "nop 0x20710 success");
uint8_t * nop_ptr = nop;
DobbyCodePatch((void*)getAbsoluteAddress("libxgVipSecurity.so", 0x20710),nop_ptr,4);
return JNI_VERSION_1_6;
}
return 0;
}
下面要做的是把so注入到目标程序中,这里要注意的是我把native代码和Xposed代码放在一个项目中,而不是单独生成的so文件。
一般我们要注入的程序都是被加固的,因此需要先对classLoader进行切换,不然找不到应用程序的类,这里我以360加固为例,注意我选择的时机点,当然也可以根据自己的理解选择其它的时机点
ClassLoader mclassloader =null;@OverridepublicvoidhandleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam)throwsThrowable{XposedBridge.log(lpparam.packageName);if(lpparam.packageName.equals("com.tencent.rilp")){XposedHelpers.findAndHookMethod("com.stub.StubApp", lpparam.classLoader,"onCreate",newXC_MethodHook(){@OverrideprotectedvoidafterHookedMethod(MethodHookParam param)throwsThrowable{super.afterHookedMethod(param);// 获取classloaderClass activitythreadclass = lpparam.classLoader.loadClass("android.app.ActivityThread");Object activityobj =XposedHelpers.callStaticMethod(activitythreadclass,"currentActivityThread");Object mInitialApplication =XposedHelpers.getObjectField(activityobj,"mInitialApplication");Object mLoadedApk =XposedHelpers.getObjectField(mInitialApplication,"mLoadedApk");
mclassloader =(ClassLoader)XposedHelpers.getObjectField(mLoadedApk,"mClassLoader");XposedBridge.log("guagua classloader change success");}});}}
在注入我们的hook so之前,也需要选择时机,如果hook的是系统的so,那么我们的hook so一定要在目标程序的so加载之前就注入进去,如果hook的是目标程序的so,那么我们的hook so的加载时机可以选择目标程序so加载完成之后的任意一个时机
在安卓8以下可以主动调用doLoad加载so,在安卓9以上可以主动调用nativeLoad加载so,下面是加载so的代码
int version =android.os.Build.VERSION.SDK_INT;if(!path.equals("")){if(version >=28){XposedBridge.log("guagua start inject libguagua.so");XposedHelpers.callMethod(Runtime.getRuntime(),"nativeLoad", path, mclassloader);}else{XposedHelpers.callMethod(Runtime.getRuntime(),"doLoad", path, mclassloader);}}
其中path是我们要加载so的路径,mclassloader是类加载器,类加载器很容易就能拿到,那么so的路径该怎么获取?
既然我的native代码写在了Xposed项目里面,那我只需要拿到Xposed模块自身的so的路径不就行了吗。在低版本的系统中,我们可以直接把so的路径写死,但在高版本的系统中是不行的,因为在路径中会有如
~~cFiynmB1ZhW3l4ffMY7duw==
一样的字符串,不过可以由自身进程获取。但在这之前,我们需要明白一件事情,xposed_init里面声明的类的代码是运行在目标程序里面的,可以理解为是目标程序自身运行的代码,而目标程序和我们写的Xposed模块的应用是两个进程,所以我们需要利用IPC机制来获取Xposed模块中so的路径。
安卓实现IPC的方式有很多,有Bundle、文件共享、Messenger、AIDL、ContentProvider和Socket等,我这里选择文件共享来实现IPC。
首先在组件类中拿到应用的so路径,并把路径保存到一个文件中,运行在目标程序的代码负责从文件中读取so的路径,并通过上面介绍的方式进行注入。
需要声明的权限:
<!--文件读写权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
组件类的代码:
packagecom.mdcg.guaguaxposed;importandroidx.appcompat.app.AppCompatActivity;importandroidx.core.app.ActivityCompat;importandroid.Manifest;importandroid.content.Intent;importandroid.content.pm.PackageInfo;importandroid.content.pm.PackageManager;importandroid.os.Build;importandroid.os.Bundle;importandroid.os.Environment;importandroid.provider.Settings;importandroid.view.View;importandroid.widget.Button;importandroid.widget.Toast;importjava.io.File;importjava.io.FileOutputStream;importjava.io.IOException;importjava.util.List;publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button button =findViewById(R.id.init_button);
button.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(View view){initSoPath();}});}privatestaticfinalintREQUEST_EXTERNAL_STORAGE=1;privatestaticString[]PERMISSIONS_STORAGE={"android.permission.READ_EXTERNAL_STORAGE","android.permission.WRITE_EXTERNAL_STORAGE"};privatevoidinitSoPath(){int sdk =Build.VERSION.SDK_INT;if(sdk <=29){//检查权限(NEED_PERMISSION)是否被授权 PackageManager.PERMISSION_GRANTED表示同意授权if(ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){if(ActivityCompat.shouldShowRequestPermissionRationale(this,Manifest.permission
.WRITE_EXTERNAL_STORAGE)){Toast.makeText(this,"请开通相关权限,否则无法正常使用本应用!",Toast.LENGTH_SHORT).show();}//申请权限ActivityCompat.requestPermissions(this,PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE);}else{writeSdcard();}}else{if(!Environment.isExternalStorageManager()){Intent intent =newIntent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);startActivity(intent);return;}else{writeSdcard();}}}privatevoidwriteSdcard(){String text ="";PackageManager pm =getPackageManager();List<PackageInfo> pkgList = pm.getInstalledPackages(0);if(pkgList.size()>0){for(PackageInfo pi : pkgList){// /data/app/~~cFiynmB1ZhW3l4ffMY7duw==/com.mdcg.guaguaxposed-zyuZcPG2uq6jw8Lc7DT40A==/base.apkif(pi.applicationInfo.publicSourceDir.indexOf("com.mdcg.guaguaxposed")!=-1){
text = pi.applicationInfo.publicSourceDir.replace("base.apk","lib/arm64/libguagua.so");}}}if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){File file1=newFile("/sdcard/","guaguaSoPath.txt");if(!file1.exists()){try{
file1.createNewFile();}catch(IOException e){
e.printStackTrace();}}FileOutputStream fileOutputStream =null;try{
fileOutputStream =newFileOutputStream(file1);
fileOutputStream.write(text.getBytes());Toast.makeText(this,"初始化成功!",Toast.LENGTH_SHORT).show();}catch(Exception e){
e.printStackTrace();}finally{if(fileOutputStream !=null){try{
fileOutputStream.close();}catch(IOException e){
e.printStackTrace();}}}}}}}
Xposed获取so路径的代码:
privateStringgetSoPath(){if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){InputStream inputStream =null;Reader reader =null;BufferedReader bufferedReader =null;try{File file=newFile("/sdcard/","guaguaSoPath.txt");
inputStream =newFileInputStream(file);
reader =newInputStreamReader(inputStream);
bufferedReader =newBufferedReader(reader);StringBuilder result =newStringBuilder();String temp;while((temp = bufferedReader.readLine())!=null){
result.append(temp);}XposedBridge.log("read so path is "+ result.toString());return result.toString();}catch(Exception e){
e.printStackTrace();}finally{if(reader !=null){try{
reader.close();}catch(IOException e){
e.printStackTrace();}}if(inputStream !=null){try{
inputStream.close();}catch(IOException e){
e.printStackTrace();}}if(bufferedReader !=null){try{
bufferedReader.close();}catch(IOException e){
e.printStackTrace();}}}}}return"";}
结语
使用Xposed去hook native的原理并不难理解,无非就是使用一些native hook框架写成一个so文件,然后使用Xposed对so文件进行加载,只不过一些细节的部分有点繁琐。使用frida去hook native会简单许多,但如果是要实现持久化的话,Xposed是一个很不错的选择。
附录
Utils.h和Obfuscate.h
版权归原作者 马到成功~ 所有, 如有侵权,请联系我们删除。