Frida Hook Android Native 函数

版权归作者所有,如有转发,请注明文章出处:cyrus-studio.github.io/blog/

RegisterNatives

RegisterNatives 是 JNI(Java Native Interface)的一部分,用于在 Java 类和本地 C/C++ 代码之间注册本地方法。其原型如下:

kotlin 复制代码
jint RegisterNatives(JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint method_count);

参数说明:

  • env:JNIEnv 指针。

  • clazz:Java 类的 jclass 句柄。

  • methods:指向 JNINativeMethod 结构体数组的指针。

  • method_count:要注册的方法数量。

其中,methods 结构体的定义如下:

arduino 复制代码
typedef struct {
    const char* name;       // 方法名称(指向字符串)
    const char* signature;  // 方法签名(指向字符串)
    void* fnPtr;            // 方法的本地实现(指向本地函数)
} JNINativeMethod;

可以看到,每个 JNINativeMethod 结构体由 三个指针 组成:

  1. name(方法名指针)

  2. signature(方法签名指针)

  3. fnPtr(本地方法指针)

一般会有两个 RegisterNatives 函数,CheckJNI 版本只有在调试选项打开时才会调用,我们一般用 JNI 的那个就行。

ini 复制代码
[+] Found RegisterNatives symbol: _ZN3art12_GLOBAL__N_18CheckJNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x780d7757a8
[+] Found RegisterNatives symbol: _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x780d7eed10

通过 hook RegisterNatives 实现监控 app 中动态注册的 JNI 函数。代码如下:

RegisterNatives.js

ini 复制代码
// 查找 libart.so 中的 RegisterNatives 地址
function findRegisterNativesAddr() {
    let symbols = Module.enumerateSymbolsSync("libart.so");
    let addrRegisterNatives = null;

    // 遍历所有符号,查找非 CheckJNI 版本的 RegisterNatives
    for (let i = 0; i < symbols.length; i++) {
        let symbol = symbols[i];

        // 确认符合 RegisterNatives 标准的符号(非 CheckJNI 版本)
        if (symbol.name.indexOf("CheckJNI") < 0 &&
            symbol.name.indexOf("RegisterNatives") >= 0) {
            addrRegisterNatives = symbol.address;
            console.log("[+] Found RegisterNatives symbol: " + symbol.name + " at " + symbol.address);
            break;  // 找到第一个匹配的符号即可
        }
    }

    // 如果没有找到 RegisterNatives 地址,返回 null
    if (addrRegisterNatives === null) {
        console.log("[!] No non-CheckJNI RegisterNatives symbol found!");
    }
    return addrRegisterNatives;
}

// Hook RegisterNatives 函数,打印方法相关信息
function hookRegisterNatives(addrRegisterNatives) {
    if (addrRegisterNatives !== null) {
        // 使用 Interceptor 附加到 RegisterNatives 地址
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {

                // 获取 java 类和方法列表
                let javaClass = args[1];
                let className = Java.vm.tryGetEnv().getClassName(javaClass); // 获取类名

                // 打印 RegisterNatives 的方法参数
                let methodsPtr = ptr(args[2]);

                let methodCount = args[3].toInt32();
                console.log("[RegisterNatives] method_count:", methodCount);

                // 遍历注册的每个 JNI 方法
                for (let i = 0; i < methodCount; i++) {
                    let methodPtr = methodsPtr.add(i * Process.pointerSize * 3); // 获取每个方法的指针

                    // 读取每个方法的名称、签名和函数指针
                    let namePtr = Memory.readPointer(methodPtr);
                    let sigPtr = Memory.readPointer(methodPtr.add(Process.pointerSize));
                    let fnPtr = Memory.readPointer(methodPtr.add(Process.pointerSize * 2));

                    let name = Memory.readCString(namePtr); // 方法名称
                    let sig = Memory.readCString(sigPtr);   // 方法签名
                    let symbol = DebugSymbol.fromAddress(fnPtr); // 函数符号信息

                    // 打印每个 JNI 方法的详细信息
                    console.log("[RegisterNatives] Class:", className);
                    console.log("  Method: " + name);
                    console.log("  Signature: " + sig);
                    console.log("  Function Pointer: " + fnPtr);
                    console.log("  Function Symbol: " + symbol);
                }
            }
        });
    } else {
        console.log("[!] Cannot hook RegisterNatives because the address is null.");
    }
}

// 执行查找并 hook RegisterNatives
setImmediate(function() {
    let addrRegisterNatives = findRegisterNativesAddr();
    hookRegisterNatives(addrRegisterNatives);
});

启动指定 app 并执行脚本

复制代码
frida -H 127.0.0.1:1234 -l register_natives.js -f packageName

输出如下:

yaml 复制代码
[+] Found RegisterNatives symbol: _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi at 0x780d7eed10
Spawned `com.shizhuang.duapp`. Use %resume to let the main thread start executing!
[Remote::com.shizhuang.duapp]-> %resume
[Remote::com.shizhuang.duapp]-> [RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: dI
  Signature: (I)I
  Function Symbol: 0x77a435693c libGameVMP.so!0xb93c
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: dS
  Signature: (Ljava/lang/String;)Ljava/lang/String;
  Function Symbol: 0x77a4356aec libGameVMP.so!0xbaec
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: dL
  Signature: (J)J
  Function Symbol: 0x77a4356ad0 libGameVMP.so!0xbad0
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IV
  Signature: ([Ljava/lang/Object;)V
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IZ
  Signature: ([Ljava/lang/Object;)Z
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IB
  Signature: ([Ljava/lang/Object;)B
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IC
  Signature: ([Ljava/lang/Object;)C
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IS
  Signature: ([Ljava/lang/Object;)S
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: II
  Signature: ([Ljava/lang/Object;)I
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IF
  Signature: ([Ljava/lang/Object;)F
  Function Symbol: 0x77a4358fe8 libGameVMP.so!0xdfe8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IJ
  Signature: ([Ljava/lang/Object;)J
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: ID
  Signature: ([Ljava/lang/Object;)D
  Function Symbol: 0x77a4359028 libGameVMP.so!0xe028
[RegisterNatives] method_count: 1
[RegisterNatives] Class: lte.NCall
  Method: IL
  Signature: ([Ljava/lang/Object;)Ljava/lang/Object;
  Function Symbol: 0x77a4358fa8 libGameVMP.so!0xdfa8

JNIEnv

常用的 JNI 函数在 frida 的 env.js 中都已经定义好了

github.com/frida/frida...

通过下面代码获取 JNIEnv 引用,就可以调用相关的 JNI 函数

bash 复制代码
let env = Java.vm.tryGetEnv()

文档:frida.re/docs/javasc...

读取 String 对象内容

通过 env.js 中定义的 stringFromJni 函数可以直接获取到字符串对象的值

示例代码:

ini 复制代码
let str = env.stringFromJni(stringObj)
console.log(str);

dlopen

dlopen 函数 在 linker 模块,是 Android 系统上的动态库加载函数,用于在运行时加载共享库(.so 文件)。

你可以在 bionic/linker/dlfcn.cpp 中找到 dlopen 的实现:

arduino 复制代码
void* dlopen(const char* filename, int flags) {
    return do_dlopen(filename, flags, nullptr);
}

aospxref.com/android-11....

android_dlopen_ext 是 Android 特有的 dlopen 扩展版本,允许开发者在加载共享库时使用额外的选项,比如 指定库的加载路径 或 共享库的保护标志。

android_dlopen_ext 函数原型

arduino 复制代码
void* android_dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo);

参数

  • filename:共享库路径(如 "libnative-lib.so")。

  • flags:同 dlopen(如 RTLD_LAZY)。

  • extinfo:额外的加载选项,传 NULL 表示默认行为。

通过 frida hook android app 的 so 加载过程,代码如下:

dlopen.js

javascript 复制代码
function hook_dlopen(package_name) {
    // Hook dlopen 函数
    const dlopenAddr = Module.findExportByName(null, "dlopen");
    const dlopen = new NativeFunction(dlopenAddr, 'pointer', ['pointer', 'int']);
    Interceptor.attach(dlopen, {
        onEnter(args) {
            // 获取传递给 dlopen 的参数(SO 文件路径)
            const soPath = Memory.readUtf8String(args[0]);
            // 如果是目标 app SO 文件,则打印路径
            if (package_name) {
                if (soPath.includes(package_name)) {
                    // 打印信息
                    console.log("dlopen() - Loaded SO file:", soPath);
                }
            }else{
                console.log("dlopen() - Loaded SO file:", soPath);
            }
        }, onLeave(retval) {
        }
    });

    // Hook android_dlopen_ext 函数
    const android_dlopen_extAddr = Module.findExportByName(null, "android_dlopen_ext");
    const android_dlopen_ext = new NativeFunction(android_dlopen_extAddr, 'pointer', ['pointer', 'int', 'pointer']);
    Interceptor.attach(android_dlopen_ext, {
        onEnter(args) {
            // 获取传递给 android_dlopen_ext 的参数(SO 文件路径)
            const soPath = Memory.readUtf8String(args[0]);
            // 如果是目标 app SO 文件,则打印路径
            if (package_name) {
                if (soPath.includes(package_name)) {
                    // 打印信息
                    console.log("android_dlopen_ext() - Loaded SO file:", soPath);
                }
            }else{
                console.log("android_dlopen_ext() - Loaded SO file:", soPath);
            }
        }, onLeave(retval) {
        }
    });
}


setImmediate(function () {
    hook_dlopen("com.shizhuang.duapp")
});

执行脚本

复制代码
frida -H 127.0.0.1:1234 -l  dlopen.js -f packageName

输出如下:

typescript 复制代码
     ____
    / _  |   Frida 14.2.18 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://www.frida.re/docs/home/
Spawned `com.shizhuang.duapp`. Use %resume to let the main thread start executing!
[Remote::com.shizhuang.duapp]-> %resume
[Remote::com.shizhuang.duapp]-> android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/oat/arm64/base.odex
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libGameVMP.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libmmkv.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libxcrash.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libdulog.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libduhook.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libheif.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libdewuffmpeg.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libduplayer.so
android_dlopen_ext() - Loaded SO file: /data/app/com.shizhuang.duapp-fTxemmnM8l6298xbBELksQ==/lib/arm64/libstatic-webp.so
android_dlopen_ext() - Loaded SO file: /data/user/0/com.shizhuang.duapp/files/soloader_x64/libxyvodsdk.so
android_dlopen_ext() - Loaded SO file: /data/user/0/com.shizhuang.duapp/files/soloader_x64/libijmdetect_drisk.so
android_dlopen_ext() - Loaded SO file: /data/user/0/com.shizhuang.duapp/files/soloader_x64/libdu_security.so

文件读写

Frida 文件读写相关 api

frida.re/docs/javasc...

写一个字符串到文件:

ini 复制代码
function writeStringToFile() {
    try {
        var appContext = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();
        var privateDir = appContext.getExternalFilesDir(null).getAbsolutePath();

        var path = privateDir + "/cyrus_studio.dat";
        var file = new File(path, "w");
        file.write("CYRUS STUDIO");
        file.flush();
        file.close();

        console.log("[+] Successfully wrote to " + path);
    } catch (e) {
        console.log("[!] Error: " + e.message);
    }
}

setImmediate(function () {
    Java.perform(writeStringToFile);
});

执行脚本:

r 复制代码
frida -H 127.0.0.1:1234 -l write_string_to_file.js -F

输出如下:

bash 复制代码
[+] Successfully wrote to /storage/emulated/0/Android/data/com.shizhuang.duapp/files/cyrus_studio.dat

调用 C 函数

在 Frida 中,NativeFunction 用于调用 native 函数。它允许我们在 JavaScript 中调用 C 语言函数,就像普通 JavaScript 函数一样。

基本语法

go 复制代码
var func = new NativeFunction(address, returnType, argTypes);
  • address:函数地址,可以用 Module.findExportByName 或 Module.findExportByName(null, "func_name") 获取。

  • returnType:函数的返回类型,如 "void"、"int"、"pointer" 等。

  • argTypes:一个数组,指定函数的参数类型,如 ["pointer", "int"]。

libc.so 文件操作相关函数原型如下:

arduino 复制代码
// 打开文件
FILE *fopen(const char *filename, const char *mode);
/*
参数:
- filename: 需要打开的文件路径
- mode: 文件打开模式,如 "r"(只读),"w"(写入),"rb"(二进制只读)等
返回值:
- 成功返回 FILE* 指针,失败返回 NULL
*/

// 移动文件指针
int fseek(FILE *stream, long offset, int whence);
/*
参数:
- stream: 已打开的文件指针
- offset: 偏移量,以字节为单位
- whence: 参考位置
  - SEEK_SET (0): 文件开头
  - SEEK_CUR (1): 当前指针位置
  - SEEK_END (2): 文件末尾
返回值:
- 成功返回 0,失败返回非零值
*/

// 获取当前文件指针位置
long ftell(FILE *stream);
/*
参数:
- stream: 已打开的文件指针
返回值:
- 返回当前文件指针的偏移量(从文件开头算起)
- 失败返回 -1L
*/

// 读取文件内容
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
/*
参数:
- ptr: 读取数据的存储缓冲区
- size: 每个数据块的大小(单位:字节)
- count: 读取数据块的个数
- stream: 已打开的文件指针
返回值:
- 返回成功读取的数据块数目
- 失败或到文件末尾返回小于 count 的值
*/

// 关闭文件
int fclose(FILE *stream);
/*
参数:
- stream: 需要关闭的文件指针
返回值:
- 成功返回 0,失败返回 EOF(通常为 -1)
*/

在 Android 系统中,这些函数的原型通常可以在 Bionic(Android 的 C 库)中找到。

路径:/bionic/libc/include/stdio.h // Android (Bionic)

aospxref.com/android-11....

通过 NativeFunction 封装 系统中 libc.so 文件相关的函数,并调用读取文件内容

ini 复制代码
function readFileToString() {
    // 定义 C 语言标准库函数的 Frida 封装
    var fopen = new NativeFunction(Module.findExportByName("libc.so", "fopen"), "pointer", ["pointer", "pointer"]);
    var fseek = new NativeFunction(Module.findExportByName("libc.so", "fseek"), "int", ["pointer", "int", "int"]);
    var ftell = new NativeFunction(Module.findExportByName("libc.so", "ftell"), "long", ["pointer"]);
    var fread = new NativeFunction(Module.findExportByName("libc.so", "fread"), "int", ["pointer", "long", "int", "pointer"]);
    var fclose = new NativeFunction(Module.findExportByName("libc.so", "fclose"), "int", ["pointer"]);

    try {
        // 获取 Android 应用的上下文
        var appContext = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();
        var privateDir = appContext.getExternalFilesDir(null).getAbsolutePath();
        var path = privateDir + "/cyrus_studio.dat"; // 目标文件路径

        // 分配内存存储文件路径和打开模式
        var mode = Memory.allocUtf8String("rb"); // 以二进制只读模式打开文件
        var filePath = Memory.allocUtf8String(path);

        // 调用 fopen 打开文件
        var fp = fopen(filePath, mode);

        if (fp.isNull()) {
            console.log(`[!] Failed to open file ${path} for reading`);
        } else {
            // 移动文件指针到文件末尾,计算文件大小
            fseek(fp, 0, 2); // SEEK_END
            var size = ftell(fp);
            fseek(fp, 0, 0); // SEEK_SET(回到文件开头)

            // 分配缓冲区存储文件内容
            var buffer = Memory.alloc(size + 1);
            fread(buffer, size, 1, fp); // 读取文件内容到 buffer
            fclose(fp); // 关闭文件

            // 读取缓冲区内容并转换为字符串
            var content = Memory.readUtf8String(buffer);
            console.log("[+] File content: " + content);
        }
    } catch (e) {
        console.log("[!] Error: " + e.message);
    }
}


setImmediate(function () {
    Java.perform(readFileToString);
});

执行脚本

r 复制代码
frida -H 127.0.0.1:1234 -l read_file_to_string.js -F

输出如下:

css 复制代码
[+] File content: CYRUS STUDIO
相关推荐
程序员JerrySUN6 分钟前
Jetson边缘嵌入式实战课程第二讲:JetPack 和 SDK Manager 是什么
c语言·开发语言·网络·udp·音视频
我不是懒洋洋10 分钟前
布谷鸟过滤器:比布隆过滤器更优雅的判重方案
c语言·经验分享
忡黑梨10 分钟前
eNSP_从直连到BGP全网互通
c语言·网络·数据结构·python·算法·网络安全
andr_gale26 分钟前
04_rc文件语法规则
android·framework·aosp
祖国的好青年1 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴2 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭2 小时前
Android ADB 完全使用指南
android·adb
handler012 小时前
Git 核心指令速查
linux·c语言·c++·笔记·git·学习
学会去珍惜2 小时前
学会C语言可以做什么
c语言·网络编程·游戏开发·嵌入式系统·系统编程