使用Xposed对native进行hook

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

使用方法

  1. 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);
    
  2. 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的路径,并通过上面介绍的方式进行注入。

需要声明的权限:





组件类的代码:

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是一个很不错的选择。

附录

Utils.h和Obfuscate.h