赞
踩
Xposed框架可谓是“家喻户晓”的神器,它具有着frida所不具备的持久性(虽然frida也可以通过frida-gadget实现持久化,但没有Xposed使用方便)。当我们需要hook java层的代码时,Xposed使用起来得心应手,但是随着软件开发者的安全意识越来越高,放在java层的核心代码也就越来少,这就导致Xposed使用起来有点力不从心,逆向分析者也就面临着如何使用Xposed对native进行hook的问题,下面的文章就对该问题提供一个解决思路。
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_1FCCC DobbyHook((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函数获取函数的地址了,(注:文件在文末分享)
上面介绍了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; @Override public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable { XposedBridge.log(lpparam.packageName); if (lpparam.packageName.equals("com.tencent.rilp")) { XposedHelpers.findAndHookMethod("com.stub.StubApp", lpparam.classLoader, "onCreate", new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super.afterHookedMethod(param); // 获取classloader Class 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"/>
组件类的代码:
package com.mdcg.guaguaxposed; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import android.Manifest; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.provider.Settings; import android.view.View; import android.widget.Button; import android.widget.Toast; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.List; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = findViewById(R.id.init_button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { initSoPath(); } }); } private static final int REQUEST_EXTERNAL_STORAGE = 1; private static String[] PERMISSIONS_STORAGE = {"android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE"}; private void initSoPath() { 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 = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); startActivity(intent); return; } else { writeSdcard(); } } } private void writeSdcard() { 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.apk if (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=new File("/sdcard/","guaguaSoPath.txt"); if (!file1.exists()){ try { file1.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(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路径的代码:
private String getSoPath() { 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=new File("/sdcard/", "guaguaSoPath.txt"); inputStream = new FileInputStream(file); reader = new InputStreamReader(inputStream); bufferedReader = new BufferedReader(reader); StringBuilder result = new StringBuilder(); 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是一个很不错的选择。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。