Frida-Native层Hook

概述

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//在Frida脚本中实现native层Hook的API函数是Interceptor.attach(addr, callbacks)
Interceptor.attach(addr, {
    onEnter(args) {
        //在函数调用前产生的回调,在这个函数中可以处理函数参数的相关内容.
        //被Hook的函数参数内容是以数组的方式存储在args中.

    },
    onLeave(retval) {
        //在被Hook的目标函数执行完成后执行的函数.
        //被Hook的函数返回值用retval变量来表示.

    }
});

要实现对一个native层函数的Hook,最重要的就是找到该函数的首地址.

Native层函数导出

如果是导出函数,可以通过下述API函数获得相应函数的首地址.

1
2
3
4
5
6
//当第一个参数为null时,会在内存加载的所有模块中搜索导出符号名.

//未找到相应的导出符号名时,会抛出一个异常.
Module.getExportByName(moduleName | null, exportName)
//未找到相应的导出符号名时,会直接返回一个null值.
Module.findExportByName(moduleName | null, exportName)

测试Apk

java代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.luonative;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.luonative.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'luonative' library on application startup.
    static {
        System.loadLibrary("luonative");
    }

    private ActivityMainBinding binding;

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

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

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());

        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Log.d("LuoHun", stringFromJNI());
        }

    }

    public native String stringFromJNI();
}

native代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//libluonative.so
#include <jni.h>
#include <string>

//JNI函数的命名规则:Java_PackageName_ClassName_MethodName
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_luonative_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

Hook示例

本例以Hook libluonative.so库中的Java_com_example_luonative_MainActivity_stringFromJNI函数为例.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function hook_native() {
    var addr = Module.getExportByName("libluonative.so", "Java_com_example_luonative_MainActivity_stringFromJNI")

    Interceptor.attach(addr, {
        onEnter: function (args) {
            console.log("jnienv pointer => ", args[0])
            console.log("jobj pointer => ", args[1])
        },
        onLeave: function (retval) {
            //获取当前线程的JNIEnv结构
            var env = Java.vm.getEnv()
            //打印日志
            console.log("retval is => ", env.getStringUtfChars(retval, 0).readCString())
            //构造一个新字符串
            var jstrings = env.newStringUtf("Hello XiaLuoHun");
            //替换返回值
            retval.replace(jstrings); 

        }
    })
}

function main() {
    hook_native()
}

setImmediate(main)

Native层函数未导出

在每次App重新运行后native函数加载的绝对地址是会变化的,唯一不变的是函数相对于所在模块基地址的偏移,因此我们可以在获取模块的基地址后加上固定的偏移地址获取相应函数的地址.Frida通过以下API来获取函数的绝对地址.

1
2
3
4
5
//获取对应模块的基地址
Module.findBaseAddress(name)
Module.getBaseAddress(name)
//传入固定的偏移offset,获取最后的函数绝对地址
add(offset)

动态注册的函数

为了介绍上述获取函数地址的方式,下面先介绍一下动态注册的JNI函数.

静态注册的JNI函数:可以通过一定的命名方式从Java层找到对应native层函数名.

动态注册的JNI函数:在native层实现的函数名称不定,并且不一定要求相应函数是导出类型.

只需调用以下API函数就可以完成动态注册.

1
2
3
4
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,jint nMethods)
//第一个参数clazz, native函数所在的类,可通过FindClass这个JNI函数获取(将类名的"."符号换成"/")
//第二个参数methods, 是一个数组,其中包含函数的一些签名信息以及对应在native层的函数指针
//第三个参数nMethods, 是methods数组的数量

测试Apk

java代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.example.luonative;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.luonative.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'luonative' library on application startup.
    static {
        System.loadLibrary("luonative");
    }

    private ActivityMainBinding binding;

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

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

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());

        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Log.d("LuoHun", stringFromJNI());
        }

    }

    public native String stringFromJNI();
}

native代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//libluonative.so
#include <jni.h>
#include <string>

jstring JNICALL LuoDynamicNative(   JNIEnv* env,
                                    jobject /* this */){

    std::string hello = "Hello from C++ LuoHun";
    return env->NewStringUTF(hello.c_str());
}

JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env;
    vm->GetEnv((void**)&env, JNI_VERSION_1_6);

    JNINativeMethod methods[] = {
            "stringFromJNI", "()Ljava/lang/String;", (void*)LuoDynamicNative
    };

    env->RegisterNatives(env->FindClass("com/example/luonative/MainActivity"), methods, 1);

    return JNI_VERSION_1_6;
}

Hook示例

上述apk编译运行后,使用Objection遍历libluonative.so模块的导出函数,会发现再也找不到stringFromJNI()字符串相关的函数了.此时就可以通过先找模块基址,再根据偏移获取函数绝对地址.那么函数的偏移如何获取呢? 通过Hook实现动态注册的函数RegisterNatives()来获取动态注册后的LuoDynamicNative()函数地址.

这里介绍一个项目frida_hook_libart,其仓库地址为https://github.com/lasting-yang/frida_hook_libart.这个项目中包含着对一些JNI函数和art相关函数的Frida Hook脚本,这里要使用的Hook脚本是hook_RegisterNatives.js

在本案例中,RegisterNatives()函数是在JNI_OnLoad()函数中完成的,即模块一加载就会自动运行的函数,因此为了能够顺利地在RegisterNatives()函数未被调用前Hook到,就需要使用Frida在注入时,选择spwan模式运行.

NativeHook动态注册1

从上图中可以看到LuoDynamicNative()函数相对于libluonative.so模块的偏移为0xf11c

Hook动态注册的LuoDynamicNative()函数的脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function hook_native() {
    var libluonative_addr = Module.findBaseAddress("libluonative.so")
    console.log("libluonative_addr is => ", libluonative_addr)
    var stringFromJNI = libluonative_addr.add(0xf11c)
    console.log("stringFromJNI address is => ", stringFromJNI)
    
    Interceptor.attach(stringFromJNI, {
        onEnter: function (args) {
            console.log("jnienv pointer => ", args[0])
            console.log("jobj pointer => ", args[1])
        },
        onLeave: function (retval) {
            //获取当前线程的JNIEnv结构
            var env = Java.vm.getEnv()
            //打印日志
            console.log("retval is => ", env.getStringUtfChars(retval, 0).readCString())
            //构造一个新字符串
            var jstrings = env.newStringUtf("Hello XiaLuoHun");
            //替换返回值
            retval.replace(jstrings); 
        }
    })
}

function main() {
    hook_native()
}

setImmediate(main)
注意
虽然每次重新运行App时,函数的偏移地址是不会改变的,但是这是建立在App未被重新编译的基础上.如果在一次运行后App代码发生修改,并重新在AndroidStudio中编译运行,那么新的App函数的偏移地址是会发生改变的,此时应重新使用hook_RegisterNatives.js脚本获取相应函数的新偏移值并修改Hook脚本,然后再进行注入.

相关内容

0%