JNIHook

xiaoeryu Lv5
  • JNI函数符号hook
  • JNI函数参数、返回值打印和替换
  • 动态注册JNI_OnLoad
  • hook RegisterNatives
  • jnitrace

本章对系统框架层的函数进行了hook、以及hook Registernatives获取其中的信息

Frida相对于Xposed的优势在于可以对Native层进行hook、对内存进行搜索

除了对用户层native代码进行hook之外也可以对系统框架层进行hook,而对更深层进行hook有时候可以更容易得到需要的信息。

jnitrace脚本是hook了所有的JNI函数,app经过了JNI的任何函数都可以打印出操作流程。

这里还是以之前写的demo中的JNI函数为例

  • 例如这里hook env->GetStringUTFChars
  • 要hook这个函数还是首先要找到它的地址,查找函数的地址可以用上一章的方法从导出表或者导出符号中找到函数地址,但是首先要知道这个函数在哪个so中,如果对源码不熟悉不确定在哪个so中的话可以上网搜索一下这个函数所在的so

hook env->GetStringUTFChars

获取函数地址
function hook_JNI() {
    Java.perform(function() {
        var GetStringUTFChars_addr = null;
        var symbols = Module.enumerateSymbols("libart.so");

        symbols.forEach(function(symbol) {
            // 排除包含 "CheckJNI" 的符号,并且包含 "JNI" 的符号
            if (symbol.name.includes("JNI") && !symbol.name.includes("CheckJNI")) {
                // 查找包含 "GetStringUTFChars" 的符号
                if (symbol.name.includes("GetStringUTFChars")) {
                    console.log("GetStringUTFChars: " + symbol.name);
                    GetStringUTFChars_addr = symbol.address;
                }
            }
        });
    });
}
  • 因为这个so库中有两个GetStringUTFChars,需要把CheckJNI给筛选掉。最后得到的结果是:**_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh**

  • 对这个结果进行demangler 可以看到获取到完整的函数名:art::JNI::GetStringUTFChars(_JNIEnv, _jstring, unsigned char*)**

  • 使用attach对它进行hook就可以获取到所有调用这个函数的参数和返回值了

function hook_JNI() {
    Java.perform(function () {
        var GetStringUTFChars_addr = null;
        var symbols = Module.enumerateSymbols("libart.so");

        symbols.forEach(function (symbol) {
            // 排除包含 "CheckJNI" 的符号,并且包含 "JNI" 的符号
            if (symbol.name.includes("JNI") && !symbol.name.includes("CheckJNI")) {
                // 查找包含 "GetStringUTFChars" 的符号
                if (symbol.name.includes("GetStringUTFChars")) {
                    console.log("GetStringUTFChars: " + symbol.name);
                    GetStringUTFChars_addr = symbol.address;
                }
            }
        });
        Interceptor.attach(GetStringUTFChars_addr, {
            onEnter: function (args) {
                console.log("art::JNI::GetStringUTFChars(_JNIEnv*, _jstring*, unsigned char*)=> " + args[0], Java.vm.getEnv().getStringUtfChars(args[1], null).readCString(), args[2]);
                // console.log('CCCryptorCreate called from:\n' +
                //     Thread.backtrace(this.context, Backtracer.ACCURATE)
                //         .map(DebugSymbol.fromAddress).join('\n') + '\n');
            },
            onLeave: function (retval) {
                console.log("GetStringUTFChars retval: " + retval.readCString());
            }
        })
    });
}
运行结果
  • 参数和返回值总共变了三次,说明这个函数也被调用了三次
  • 尝试了打印调用栈,不过调用栈打印的结果不太准确

hook env->NewStringUTF

替换env->NewStringUTF()的参数和返回值,流程跟刚才差不多区别在于这里是一个带参数的函数,尝试替换它的参数看返回值是否被改变

function replace_JNI() {
    Java.perform(function () {
        var NewStringUTF_addr = null;
        var symbols = Module.enumerateSymbols("libart.so");

        symbols.forEach(function (symbol) {
            // 排除包含 "CheckJNI" 的符号,并且包含 "JNI" 的符号
            if (symbol.name.includes("JNI") && !symbol.name.includes("CheckJNI")) {
                // 查找包含 "NewStringUTF" 的符号
                if (symbol.name.includes("NewStringUTF")) {
                    console.log("NewStringUTF name: " + symbol.name);
                    NewStringUTF_addr = symbol.address;
                }
            }
        });

        // 定义原始 NewStringUTF 函数
        var original_NewStringUTF = new NativeFunction(NewStringUTF_addr, "pointer", ["pointer", "pointer"]);

        // 替换原始 NewStringUTF 函数
        Interceptor.replace(NewStringUTF_addr, new NativeCallback(function (env, str) {
            console.log("NewStringUTF args: ", env, str.readCString());
            
            // 调用原始 NewStringUTF 函数
            var original_result = original_NewStringUTF(env, Memory.allocUtf8String("hooked_NewStringUTF"));
            console.log("NewStringUTF result: ", original_result);

            // 创建新的字符串并返回
            var newStr = Memory.allocUtf8String("hooked_NewStringUTF");
            var newRet = original_NewStringUTF(env, newStr);
            return newRet;
        }, "pointer", ["pointer", "pointer"]));
    });
}
执行结果
  • hook脚本执行后,NewStringUTF()的值就被替换为了脚本中传入的值

hook动态绑定的RegisterNatives

function hook_RegisterNatives() {
    Java.perform(function () {
        var RegisterNatives_addr = null;  // 初始化 RegisterNatives 函数的地址变量
        var symbols = Module.enumerateSymbols("libart.so");  // 枚举 libart.so 中的所有符号

        symbols.forEach(function (symbol) {  // 遍历所有的符号
            // 排除包含 "CheckJNI" 的符号,并且包含 "JNI" 的符号
            if (symbol.name.includes("JNI") && !symbol.name.includes("CheckJNI")) {
                // 查找包含 "RegisterNatives" 的符号
                if (symbol.name.includes("RegisterNatives")) {
                    console.log("RegisterNatives name: " + symbol.name);  // 打印找到的 RegisterNatives 函数名
                    RegisterNatives_addr = symbol.address;  // 将找到的 RegisterNatives 函数地址赋给 RegisterNatives_addr 变量
                }
            }
        });

        if (RegisterNatives_addr != null) {  // 如果 RegisterNatives 函数地址不为 null
            // 在 RegisterNatives 函数上创建一个 Interceptor
            Interceptor.attach(RegisterNatives_addr, {
                onEnter: function (args) {  // 进入函数时的回调
                    console.log("RegisterNative method counts => ", args[3]);  // 打印注册的方法数量
                    var env = args[0];  // 获取 JNIEnv 指针
                    var clazz = args[1];  // 获取 jclass 对象
                    var class_name = Java.vm.getEnv().getClassName(clazz);  // 获取类名
                    var methods_ptr = ptr(args[2]);  // 获取方法数组指针
                    var method_count = args[3].toInt32();  // 将方法数量转换为整数

                    // 遍历方法数组
                    for (var i = 0; i < method_count; i++) {
                        // 获取方法名、方法签名和函数指针
                        var name_ptr = methods_ptr.add(i * Process.pointerSize * 3).readPointer();
                        var sig_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize).readPointer();
                        var fnPtr_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2).readPointer();

                        // 查找函数指针所在的模块
                        var find_module = Process.findModuleByAddress(fnPtr_ptr);

                        // 打印注册的方法信息
                        console.log("RegisterNative class_name => ", class_name);
                        console.log("RegisterNative name => ", Memory.readCString(name_ptr));
                        console.log("RegisterNative sig => ", Memory.readCString(sig_ptr));
                        console.log("RegisterNative fnPtr_ptr => ", JSON.stringify(DebugSymbol.fromAddress(fnPtr_ptr)));
                        console.log("RegisterNative find_module => ", JSON.stringify(find_module));
                        console.log("callee => ", DebugSymbol.fromAddress(this.returnAddress));
                        console.log("offset => ", ptr(fnPtr_ptr).sub(find_module.base));
                    }
                },
                onLeave: function () { }  // 离开函数时的回调
            });
        } else {
            console.log("RegisterNatives_addr is null");  // 如果 RegisterNatives 函数地址为 null,则打印提示信息
        }
    });
}
  • 这段稍微长一点,分为两个部分,第一部分还是获取*RegisterNatives()*的地址,第二部分对其中的参数进行解析
  • 可以解析出来的内容包括:类名、注册前的函数名、sig、注册函数在内存中的信息、所在模块的信息、 RegisterNative()的返回地址和所在库名以及在库中的偏移、最后还获取了一下注册函数所在的偏移(其实已经知道了函数地址和库的基址已经可以直接算出来了)
附件

使用的被hook demo还是上一章中使用的demo

本章完整的hook脚本

  • 标题: JNIHook
  • 作者: xiaoeryu
  • 创建于 : 2024-03-24 10:56:34
  • 更新于 : 2024-03-26 09:17:36
  • 链接: https://github.com/xiaoeryu/2024/03/24/JNI框架层Hook/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论