[Android] 在不需要Root权限且只使用Java的情况下,钩取Java函数

首先

我认为如果在Java中也能够实现函数钩子,那将会非常有趣,所以我试着做了一下。

环境

・Android 11(真机)
・非Root

挂钩前的准备工作。

首先,我们要创建一个简单的库。
文件/文件夹的结构是这样的。

...
jinterceptor
   EvacuatedMethodStorage.java
   Interceptor.java
   Memory.java
   ArtMethod.java
   Reflection.java
   Unsafe.java
...
MainActivity.java
...

以下的说明和来源

public class EvacuatedMethodStorage {

    private static int mReferencedCount = 0;

    public static int referencedCount(){
        return mReferencedCount++;
    }
    public static Object method0(Object receiver, Object...params){
        return null;
    }

    public static Object method1(Object receiver, Object...params){
        return null;
    }

    public static Object method2(Object receiver, Object...params){
        return null;
    }

    public static Object method3(Object receiver, Object...params){
        return null;
    }

    public static Object method4(Object receiver, Object...params){
        return null;
    }

    ...

    public static Object method255(Object receiver, Object...params){
        return null;
    }
}

这个课程中实现了一组用于保存钩子函数的原始函数的方法。

public class Interceptor {

    private static final Class<?> class_EvacuatedMethodStorage = EvacuatedMethodStorage.class;

    private static Map<Pair<String, String>, Integer> mBackupMap = new ConcurrentHashMap<>();

    public static void override(Method original, Method replace) throws NoSuchMethodException {
        long originalAddress = Unsafe.getMethodAddress(original), replaceAddress = Unsafe.getMethodAddress(replace);
        int c = backupMethod(originalAddress); //cは保存先の関数の番号
        Memory.copy(originalAddress, replaceAddress, ArtMethod.getSize());
        mBackupMap.put(new Pair<>(replace.getDeclaringClass().getName(), replace.getName()), c);
        //...<>(original.getDeclaringClass().getName(), original.getName())... ではないのは、元の関数が上書きされるため
    }

    public static Object callOriginal(Object receiver, Object...params){
        StackTraceElement current = Thread.currentThread().getStackTrace()[3]; //どのフックされた関数を通過したのかのトレースを取得
        int originalNum = mBackupMap.get(Pair.create(current.getClassName(), current.getMethodName())); //保存先の関数の番号を取得
        switch (originalNum){
            case 0:
                return EvacuatedMethodStorage.method0(receiver, params);
            case 1:
                return EvacuatedMethodStorage.method1(receiver, params);
            case 2:
                return EvacuatedMethodStorage.method2(receiver, params);

            ...

            case 255:
                return EvacuatedMethodStorage.method255(receiver, params); 
        }
        return null;
    }

    private static int backupMethod(long original) throws NoSuchMethodException {
        int count = EvacuatedMethodStorage.referencedCount();
        Method evacuationDst = class_EvacuatedMethodStorage.getDeclaredMethod("method" + count, Object.class, Object[].class);
        Memory.copy(Unsafe.getMethodAddress(evacuationDst), original, ArtMethod.getSize()); //退避先の関数をoriginaに上書き
        return count; //保存先の番号
    }
}

这个类实现了钩子函数。
顺便提一下,在callOriginal函数中,不使用反射并且使用switch语句来判断和调用已经存储的原始函数,是有原因的。
后面会说明原因。

public class Memory {

    private static byte peekByte(long address){
        return (Byte) Reflection.call(null, "libcore.io.Memory", "peekByte", null, new Class[]{long.class}, new Object[]{address});
    }

    private static void pokeByte(long address, byte value) {
        Reflection.call(null, "libcore.io.Memory", "pokeByte", null, new Class[]{long.class, byte.class}, new Object[]{address, value});
    }

    public static void copy(long dst, long src, long length) {
        for (long i = 0; i < length; i++) {
            pokeByte(dst, peekByte(src));
            dst++;
            src++;
        }
    }
}

这是一个用于进行内存操作的类。

public class ArtMethod {

    private static long mSize;

    static {
        try {
            Method r1 = ArtMethod.class.getDeclaredMethod("r1"),
                    r2 = ArtMethod.class.getDeclaredMethod("r2");
            mSize = Unsafe.getMethodAddress(r2) - Unsafe.getMethodAddress(r1);
        } catch (Throwable throwable){
                throwable.printStackTrace();
        }
    }

    public static long getSize(){
        return mSize;
    }

    private static void r1(){}

    private static void r2(){}

}

这个类提供了ArtMethod的大小。

首先,ArtMethod 是什么意思?

ArtMethod 是 Android 的运行时系统(ART:Android Runtime)内部的一个类的组成部分。这个类用于表示 Java 方法,在 Android 应用程序的运行过程中存储和操作方法的信息。

ChatGPT告诉我,
也就是说,它类似于用于存储Java方法的各种信息的包装器。
链接到ArtMethod的头文件
链接到ArtMethod的源代码

现在,我想要谈谈获取关键大小的代码。
在这里获取大小的是这段代码。

mSize = Unsafe.getMethodAddress(r2) - Unsafe.getMethodAddress(r1);

Unsafe.getMethodAddress的实现如下所示。

public class Unsafe {

    public static long getMethodAddress(Method method){
        return (Long) Reflection.get(method.getClass().getSuperclass(), null, "artMethod", method);
    }

}

通过反射,我们可以获取到Method类的地址。这个地址存储在Method类的超类Executable中的名为artMethod的long型变量中。确切来说,变量artMethod中存储的是将该方法封装的ArtMethod的地址或指针。

スクリーンショット 2023-09-10 190148.png

如果图表也不太清楚的话,真的非常抱歉。

public class Reflection {

    public static Object call(Class<?> clazz, String className, String methodName, Object receiver,
                              Class[] types, Object[] params) {
        try {
            if (clazz == null) clazz = Class.forName(className);
            Method method = clazz.getDeclaredMethod(methodName, types);
            method.setAccessible(true);
            return method.invoke(receiver, params);
        } catch (Throwable throwable) {
            throw new RuntimeException("reflection error:", throwable);
        }
    }

    public static Object get(Class<?> clazz, String className, String fieldName, Object receiver) {
        try {
            if (clazz == null) clazz = Class.forName(className);
            Field field = clazz.getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(receiver);
        } catch (Throwable e) {
            throw new RuntimeException("reflection error:", e);
        }
    }
}

这个班级是为了方便使用反射功能而设计的。

确认动作

主活动写成了这样。

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        try {

            //original関数をreplace関数に上書き
            Interceptor.override(MainActivity.class.getDeclaredMethod("original"), MainActivity.class.getDeclaredMethod("replace"));

        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        original(); //呼び出し

        TextView tv = binding.sampleText;
        tv.setText("あいうえお");
    }

    void original(){
        System.out.println("original");
    }

    void replace() {
        System.out.println("replace");
    }
}

执行结果

更换

原本应该调用的是original函数,但是却调用了replace函数。钩子函数执行成功。
当然,仍然可以调用原本的函数。

    void replace() {
        System.out.println("replace");
        Interceptor.callOriginal(this, new Object[]{}); //元の関数呼び出し
    }

执行结果

用中文翻译以下英文句子,只需一种选项:

替换
原文

顺便一提,在连接后,请确认MainActivity类中声明的方法,会发生有趣的事情。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        try {

            Interceptor.override(MainActivity.class.getDeclaredMethod("original"), MainActivity.class.getDeclaredMethod("replace"));

        } catch (Throwable e) {
            throw new RuntimeException(e);
        }

        System.out.println(" ==== Declared Methods ==== ");
        for (Method method : MainActivity.class.getDeclaredMethods()){
            System.out.println(method.getName());
        }
        System.out.println(" ========================== ");

        TextView tv = binding.sampleText;
        tv.setText("あいうえお");
    }

执行结果

====声明的方法====
onCreate
替换
替换
=====================

スクリーンショット 2023-09-10 232833.png
Memory.copy(originalAddress, replaceAddress, ArtMethod.getSize());

因为我们还没有试过这个方法,所以需要进行调查以了解可能产生的副作用。

请留意

我将介绍我目前所了解到的注意事项和副作用。

・JNI不支持调用被挂钩的函数

由于将C ++等原始函数的前几个字节重写为jmp而不是使用trampoline技巧,因此不得不完全覆盖函数,所以没办法。请注意,无法钩住从本地调用的函数。

将保存了原始方法的函数通过反射调用会导致进入无限循环的状态。

我对这个原因不清楚。
举个例子,假设我们之前的例子中,original函数的备份被保存在EvacuatedMethodStorage::method0中,但是当通过反射调用method0函数时,调用了replace方法而不是原始函数,导致发生了无限循环。
我在hook之前进行了备份,所以我认为不会受到影响,但是即使尝试获取了hook后的method0的地址,也和original和replace的地址不一致,所以更加不清楚了。
正如之前提到的,在callOriginal函数内,通过switch语句判断并调用反射前备份的原始函数,是因为这个原因。

如果有很多勾连函数,就需要逐个在EvacuatedMethodStorage中添加方法。

这真的很麻烦啊。要为Interceptor::callOriginal函数的switch语句添加新的选项…

结束

我从来没有想过通过Java就能实现函数钩子,通常我会认为这方面可能会涉及JNI。
我非常喜欢这种实验,所以如果有机会的话,我还想写类似这次内容的东西。
如果有任何错误,请毫不犹豫地指出来。

赠品

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        try {

            Interceptor.override(TextView.class.getDeclaredMethod("setText", CharSequence.class), MainActivity.class.getDeclaredMethod("setText_hook", CharSequence.class));

        } catch (Throwable throwable){
            throwable.printStackTrace();
        }

        TextView tv = binding.sampleText;
        tv.setText("あいうえお");
    }

    void setText_hook(CharSequence text){
        System.out.println("setText_hook -> text : " + text);
        Interceptor.callOriginal(this, text);
    }
}

執行結果 (zhí jié guǒ)

致命信号11(SIGSEGV),代码1(SEGV_MAPERR),错误地址0x83efd50fdc在线程13503(ec.javahooktest)中,进程13503(ec.javahooktest)。

我觉得如果钩住了View系列的函数,大概就会导致崩溃。

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        try {

            Interceptor.override(System.class.getDeclaredMethod("loadLibrary", String.class), MainActivity.class.getDeclaredMethod("loadLibrary_hook", String.class));

        } catch (Throwable throwable){
            throwable.printStackTrace();
        }

        TextView tv = binding.sampleText;
        tv.setText("あいうえお");

        System.loadLibrary("javahooktest");
    }

    static void loadLibrary_hook(String libname){
        System.out.println("System::loadLibrary -> libname : " + libname);
        //Interceptor.callOriginal(null, libname);
    }

}

虽然不需要展示执行结果,但是我发现调用callOriginal时会导致崩溃。不过我认为这种方法非常实用,可以在加载游戏等应用程序所需库后,通过钩子技术拦截loadLibrary函数,防止外部库的注入等,以加强对作弊的防范作用。只要不从内存中读取,这种方法可能对防作弊有所帮助。

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        try {

            Interceptor.override(TextView.class.getDeclaredMethod("setText", CharSequence.class), MainActivity.class.getDeclaredMethod("setText_hook", CharSequence.class));

        } catch (Throwable throwable){
            throwable.printStackTrace();
        }

        Log.d("ABC", "YO");

        TextView tv = binding.sampleText;
        tv.setText("あいうえお");
    }

    void d_hook(String tag, String msg){
        System.out.println("Log.d -> hook : tag | " + tag + ", msg | " + msg);
        Interceptor.callOriginal(null, tag, msg);
    }
}

执行结果 (shí jié guǒ)

运行时.cc:663] JNI检测到应用程序中的错误:jstring具有错误类型:java.lang.Object[]

不行。

顺便提一下,如果不调用原始文件的话

    void d_hook(String tag, String msg){
        System.out.println("Log.d -> hook | tag : " + tag + ", msg : " + msg);
        //Interceptor.callOriginal(null, tag, msg);
    }

执行结果

打印.d -> 钩子 | 标签: ABC,消息: 嘿

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        try {

            Interceptor.override(ContextWrapper.class.getDeclaredMethod("getPackageName"), MainActivity.class.getDeclaredMethod("getPackageName_hook"));

        } catch (Throwable throwable){
            throwable.printStackTrace();
        }

        System.out.println("Package : " + getPackageName());

        TextView tv = binding.sampleText;
        tv.setText("あいうえお");
    }

    String getPackageName_hook(){
        return "ai.ue.o";
    }

执行结果

包装:ai.ue.o

可能有各种用途的吗….?

广告
将在 10 秒后关闭
bannerAds