Xposed框架可谓是“家喻户晓”的神器,它具有着frida所不具备的持久性(虽然frida也可以通过frida-gadget实现持久化,但没有Xposed使用方便)。当我们需要hook java层的代码时,Xposed使用起来得心应手,但是随着软件开发者的安全意识越来越高,放在java层的核心代码也就越来少,这就导致Xposed使用起来有点力不从心,逆向分析者也就面临着如何使用Xposed对native进行hook的问题,下面的文章就对该问题提供一个解决思路。
Dobby框架的介绍
使用Xposed注入so
结语
附录
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_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函数获取函数的地址了,(注:文件在文末分享)
使用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;
@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是一个很不错的选择。文章来源:https://www.toymoban.com/news/detail-784716.html
附录
Utils.h和Obfuscate.h文章来源地址https://www.toymoban.com/news/detail-784716.html
到了这里,关于使用Xposed对native进行hook的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!