网站首页 > 技术文章 正文
1 root权限
frida-server 在 Android 设备上运行时需要 root 权限,这主要是由于它的工作涉及到操作其他应用程序的内存、修改进程状态、拦截系统调用等功能。而在 Android 系统(以及其他类 Unix 系统)中,操作系统的权限管理机制决定了只有具有 root 权限的进程才可以进行这些敏感的操作。
1.1为什么需要 root 权限?
frida-server 需要 root 权限的主要原因是:
- 访问其他进程的内存:
- 在 Linux/Android 系统中,每个进程都有自己独立的虚拟内存空间。普通用户进程不能随意访问或修改其他进程的内存,以保证系统的安全性和稳定性。
- frida-server 通过使用 ptrace 系统调用来注入和操作目标进程。ptrace 是一个用于监视和控制其他进程的系统调用,常用于调试器(如 gdb)来控制被调试进程。只有 root 用户或有特定权限的用户才能对其他具有不同用户权限的进程调用 ptrace。
- 如果没有 root 权限,Frida 将无法附加到其他进程,也无法读取或修改其内存。
- 注入动态库和修改进程行为:
- Frida 需要将其引擎(动态库)注入到目标进程的内存空间中,这需要对目标进程的内存进行修改和写入。注入后,Frida 的引擎会加载并执行 JavaScript 代码,操控目标进程的行为。
- 修改进程的地址空间(如在目标进程中注入动态库)是一种敏感操作,普通权限的用户进程是无法完成的。
- 挂钩系统 API 和函数:
- Frida 通过其动态库注入技术,能够在目标进程中挂钩函数、拦截函数调用。这些操作涉及到修改函数指针或改变进程的执行流。通常,操作系统只允许 root 权限或管理员权限的进程进行这种操作,以防止恶意程序篡改其他进程的行为。
- 与其他进程建立调试连接:
- 在类 Unix 系统中,调试进程(如使用 ptrace 调试)和被调试进程必须具有相同的用户权限,或者调试进程具有更高的权限(如 root)。
- frida-server 需要充当调试器,才能向目标进程发送命令、读取寄存器和内存状态,因此它需要具有 root 权限。
1.2底层原理
frida-server 使用的一些底层技术依赖于系统的低级功能,这些功能通常只有 root 权限才能访问。以下是 frida-server 依赖的一些底层原理:
1.ptrace 系统调用
ptrace 是 Linux 中的一个系统调用,用于允许一个进程(通常是调试器)控制另一个进程的执行。它提供了调试和进程监控的功能,例如:
- 读取和写入目标进程的内存。
- 检查和修改目标进程的寄存器。
- 拦截和修改系统调用。
- 控制目标进程的执行流(如单步执行)。
权限要求:在 Android 中,一个进程只有在以下条件之一下才能对另一个进程调用 ptrace:
- 调试进程和目标进程是同一个用户,且处于同一个 Android 应用的 UID 下。
- 调试进程具有 root 权限。
对于 Android 系统中的大多数用户进程,由于它们运行在不同的 UID 下,因此普通用户是没有权限使用 ptrace 来调试其他应用进程的。
2.访问/proc文件系统
/proc 文件系统包含了每个进程的各种信息(如内存映射、寄存器状态等)。frida-server 需要读取 /proc 文件系统中的信息来确定目标进程的内存布局、加载的动态库等。
权限要求:在 Android 中,访问 /proc 中的其他进程的信息通常需要 root 权限,尤其是当你试图读取进程的内存映射(如 /proc/<pid>/maps)或修改其状态时。
3.修改进程地址空间
frida-server 的主要功能之一是将 Frida 的核心引擎(动态库)注入到目标进程中。这需要以下操作:
- 分配内存:在目标进程的地址空间中分配内存来存储 Frida 引擎。
- 写入动态库路径:将 Frida 动态库的路径写入目标进程的地址空间,以便目标进程能够加载它。
- 调用 dlopen:在目标进程中执行 dlopen 调用来加载动态库。
这些操作都涉及到对目标进程地址空间的直接操作,而这在操作系统的安全机制下是被严格限制的。通常情况下,这些操作需要 root 权限。
4.动态 Hook 和函数拦截
frida-server 通过 Hook 技术可以拦截目标进程的函数调用。为了实现这一点,Frida 会:
- 修改目标函数的入口地址,使其跳转到 Frida 的处理代码。
- 修改目标进程的堆栈和寄存器来执行 Hook 逻辑。
这些底层操作需要对目标进程的指令流和内存进行修改,因此也需要 root 权限。
2 Hook 过程
Frida 的 Hook 技术主要依赖于其动态库注入和内存修改的能力,通过动态注入自己的动态库(Frida 的引擎库)来修改目标进程的函数指针或方法,从而实现对目标进程的函数调用进行拦截。下面通过具体的例子来详细说明 Frida 如何在目标进程中挂钩(Hook)函数。
示例:在 Android 应用中 Hookopen系统调用
我们以在 Android 应用中 Hook open 系统调用为例进行讲解。open 是一个常见的文件操作函数,它的原型是:
int open(const char *pathname, int flags);
我们希望通过 Frida 来拦截对 open 函数的调用,打印出每次打开的文件路径和标志参数。
2.1 在目标进程中 Hookopen函数
我们将使用 Frida 的 JavaScript 脚本来实现对目标进程的 Hook:
// hook_open.js
Java.perform(function () {
// 从目标进程中获取 libc.so 动态库的基址
var libc = Module.findBaseAddress('libc.so');
if (libc === null) {
console.log('libc.so not found');
return;
}
// 获取 open 函数的实际地址
var openPtr = Module.findExportByName('libc.so', 'open');
if (openPtr === null) {
console.log('open function not found');
return;
}
console.log('open function address:', openPtr);
// 使用 Interceptor.attach 来 Hook open 函数
Interceptor.attach(openPtr, {
onEnter: function (args) {
// 打印出打开的文件路径
var path = Memory.readCString(args[0]);
console.log('open called with path:', path);
// 打印 open 函数的 flags 参数
var flags = args[1].toInt32();
console.log('open called with flags:', flags);
},
onLeave: function (retval) {
// 打印 open 函数的返回值
console.log('open returned:', retval.toInt32());
}
});
});
2.2 启动 Frida Server
- 在目标 Android 设备上启动 frida-server,通常需要 root 权限。
- adb push frida-server /data/local/tmp/ adb shell su chmod +x /data/local/tmp/frida-server /data/local/tmp/frida-server &
2.3 使用 Frida 客户端运行脚本
在 PC 上,通过 Frida 客户端将脚本注入到目标进程中(假设目标进程的 PID 为 1234):
frida -U -n <package_name> -l hook_open.js
或者你可以直接使用目标应用的包名进行 Hook:
frida -U -f <package_name> -l hook_open.js --no-pause
2.4 Hook 过程的实现原理
1.查找目标函数地址
Frida 使用 Module.findExportByName 来查找目标进程中导出的 open 函数的地址。这个步骤的背后涉及到读取目标进程的内存,并解析动态库的符号表,找到目标函数的实际地址。
2.插入 Hook
Frida 的核心原理之一是通过 Interceptor.attach 来插入 Hook。这个方法会在目标函数的入口地址上插入一个跳转指令,使得每次调用目标函数时,都会先执行 Frida 插入的 Hook 逻辑。
- onEnter:在目标函数被调用之前执行。
- onLeave:在目标函数返回之后执行。
3.操作函数参数和返回值
在 onEnter 回调中,我们可以读取传递给 open 函数的参数(路径名和标志),并进行相应的操作。Frida 提供了丰富的 API 访问内存,例如 Memory.readCString 来读取指针指向的字符串。
在 onLeave 回调中,我们可以读取目标函数的返回值,并根据需要进行修改。
5. 更高级的 Hook:修改函数参数或返回值
除了打印函数调用的参数和返回值,Frida 还允许我们修改这些值。例如,我们可以修改传递给 open 函数的文件路径,使得目标进程总是尝试打开一个不同的文件:
Interceptor.attach(openPtr, {
onEnter: function (args) {
// 修改文件路径
var originalPath = Memory.readCString(args[0]);
console.log('Original open path:', originalPath);
var newPath = '/new/fake/path.txt';
args[0] = Memory.allocUtf8String(newPath);
console.log('Modified open path:', newPath);
},
onLeave: function (retval) {
console.log('open returned:', retval.toInt32());
}
});
Hook 实现背后的技术原理
- 动态库注入:在 Android 系统中,通过 frida-server 将 Frida 的引擎动态库注入到目标应用的进程空间中。这允许 Frida 在目标进程内执行 JavaScript 代码,并访问和修改进程的内存。
- 拦截函数调用:通过 Frida 的 Interceptor.attach 方法,在目标函数的入口处插入一个跳转指令,这个跳转指令会跳到 Frida 预先注入的代码位置。通过这个跳转机制,Frida 可以在目标函数被调用之前或之后执行自己的逻辑。
- 访问进程内存:Frida 提供了一些内存操作函数(如 Memory.readCString 和 Memory.allocUtf8String)来读取或修改目标进程的内存。这些操作基于 Frida 的内存访问 API,允许我们在目标进程中安全地操作内存。
- 修改返回值和参数:Frida 可以在 onEnter 和 onLeave 回调中修改函数参数和返回值。通过直接修改目标进程的内存或寄存器值,Frida 可以改变函数的执行逻辑。
3 注入ELF 文件
Module.findExportByName 是 Frida 提供的一个非常强大的 API,它能够在运行时查找目标进程中某个模块(如动态库)导出的函数或符号的地址。这个功能是 Frida 动态注入和 Hook 技术的基础。
3.1理解 ELF 文件格式
在 Android 和大多数 Linux 系统中,应用程序和动态库的文件格式都是 ELF(Executable and Linkable Format)。ELF 文件包含了以下几部分:
- ELF 头部:存储文件的基本信息(如类型、字节序、入口地址等)。
- 程序头表:描述了进程加载 ELF 文件时所需的内存布局。
- 节头表(Section Header Table):描述了文件中每个节的属性(如 .text、.data、.symtab 等)。
- 符号表:存储了文件中所有符号的名称、类型、绑定属性和地址信息。
当 ELF 文件被加载到内存中(即进程运行时),动态链接器会将这些符号表加载进来,并解析符号的实际地址。
3.2动态链接和符号解析
在应用程序运行时,操作系统会加载可执行文件和所有需要的动态库。在这个过程中,动态链接器(如 Android 上的 ld.so)负责:
- 加载 ELF 文件和所有依赖的动态库。
- 解析符号表,并将符号名称映射到实际的内存地址。
符号解析的结果会存储在进程的内存空间中。在运行时,所有已解析的符号可以通过符号表(如 .dynsym、.symtab)来访问。
3.3Frida 如何查找导出的符号
Module.findExportByName 这个函数的核心工作是:读取目标进程中指定模块的符号表,并在其中查找特定名称的符号,获取该符号在内存中的地址。
具体步骤如下:
- 获取模块的基地址
- 当 Frida 调用 Module.findBaseAddress('libc.so') 时,它实际上是查询了当前进程中所有加载的模块,并找到指定模块的基地址。Frida 通过读取 /proc/<pid>/maps 文件,来获取所有模块的加载信息。
- cat /proc/<pid>/maps
- /proc/<pid>/maps 文件包含了每个进程的内存映射信息,例如:
- 7f83a2a000-7f83a3b000 r-xp 00000000 fd:01 1719337 /lib/x86_64-linux-gnu/libc-2.27.so
- 通过解析这个文件,Frida 可以获取 libc.so 的基地址。例如,libc.so 的基地址可能是 0x7f83a2a000。
- 读取模块的符号表
- 当获取了模块的基地址后,Frida 需要解析 ELF 文件中的符号表。对于一个加载在内存中的 ELF 文件,Frida 通过读取并解析 .dynsym(动态符号表)或者 .symtab(静态符号表) 来查找导出的符号。
- Frida 使用的底层原理类似于解析 ELF 文件格式中的符号表。符号表中的每个条目(Elf32_Sym 或 Elf64_Sym 结构)包括了以下信息:
- 符号名称的索引(在 .strtab 字符串表中)。
- 符号的地址。
- 符号的类型(如函数、对象)。
- 符号的大小。
- Frida 通过在这些条目中查找匹配的符号名称(如 open),并返回其对应的地址。
- 在符号表中查找匹配的符号
- 当 Frida 调用 Module.findExportByName('libc.so', 'open') 时,它会执行以下步骤:
- 使用模块的基地址和 ELF 文件格式信息来找到符号表的位置(通常是 .dynsym)。
- 遍历符号表中的所有符号,并检查符号的名称是否匹配 open。
- 如果找到匹配的符号,计算其在内存中的实际地址,并返回。
- 这个过程中,Frida 需要读取目标进程的内存,并解析 ELF 文件的结构。这通常需要依赖 Frida 的内存访问能力和对 ELF 文件格式的深度理解。
3.4底层细节:解析 ELF 和内存访问
在 Frida 的实现中,它使用了一些底层技术来解析和访问目标进程的内存。具体包括:
- 内存读取(Memory Access)
- Frida 可以通过 ptrace 系统调用(或其他操作)读取目标进程的内存数据。这是因为在 Linux 系统中,ptrace 可以允许一个进程读取另一个进程的内存。Frida 可以通过这种方式读取 ELF 文件的符号表和字符串表。
- ELF 文件解析(ELF Parsing)
- Frida 内部实现了对 ELF 文件格式的解析逻辑。它能够根据模块的基地址、ELF 头、程序头和节头表等信息,找到符号表和字符串表的位置,并遍历这些表来查找导出的符号。
- 计算符号的内存地址
- 在解析 ELF 文件的符号表时,Frida 获取了符号的偏移地址。结合模块的基地址,Frida 可以计算出符号在内存中的实际地址。
4 Java 层的函数
在 Android 应用中,Java 层的函数并不像 C/C++ 函数那样直接存储在 ELF 文件的符号表中。Java 层的函数主要由 Android 虚拟机(Dalvik/ART)管理。为了查找并 Hook Java 层的函数,Frida 提供了对 Java 虚拟机的直接操作接口。下面我将详细解释在 Android 的 Java 层查找并 Hook 一个函数的过程。
我们以一个常见的 Android Java 层的示例为例:假设目标应用中有一个 Java 类 com.example.MyClass,其中有一个方法 void myMethod(String arg),我们希望使用 Frida 来查找并 Hook 这个方法。
4.1 Frida 查找 Java 层函数的原理
- Java.perform:这是 Frida 提供的 API,用于确保在 Java 虚拟机(Dalvik/ART)环境中执行代码。它会确保所有的 Java 类和方法都已经加载并初始化完成。
- Java.use:通过类名来加载并获取一个 Java 类的引用。
- 替换方法:使用 Frida 提供的 overload 和 implementation 接口来替换 Java 方法的实现,从而实现 Hook。
4.2 示例代码:Hook Java 层的函数
假设我们有以下 Android 应用中的 Java 类和方法:
// com/example/MyClass.java
package com.example;
public class MyClass {
public void myMethod(String arg) {
System.out.println("Original myMethod called with arg: " + arg);
}
}
我们希望通过 Frida 来 Hook 这个 myMethod 方法,打印出调用时的参数,并在函数执行后修改参数或返回值。
4.3 使用 Frida 来 Hook Java 层的函数
1. 编写 Frida 的 JavaScript 脚本
// hook_java_method.js
Java.perform(function () {
// 使用 Java.use 来获取目标类
var MyClass = Java.use("com.example.MyClass");
// Hook 目标方法 myMethod
MyClass.myMethod.overload("java.lang.String").implementation = function (arg) {
// 打印调用时的参数
console.log("Hooked myMethod called with arg: " + arg);
// 调用原始方法
var result = this.myMethod(arg);
// 打印方法执行后的结果
console.log("Original myMethod executed");
// 可以根据需要修改参数或返回值
return result;
};
});
2. 启动目标 Android 应用并加载脚本
在 PC 上使用 Frida 客户端将脚本注入到目标应用中,假设目标应用的包名为 com.example.targetapp:
frida -U -f com.example.targetapp -l hook_java_method.js --no-pause
-U 表示连接到 USB 设备,-f 表示启动目标应用,-l 表示加载指定的脚本文件,--no-pause 表示注入后不暂停应用。
详细解释
1.Java.perform的作用
Java.perform 是 Frida 提供的一个非常重要的函数,它会确保你在访问 Java 类和方法时,Java 虚拟机已经准备好所有的类和方法。这个函数的实现原理是,Frida 在底层通过 JNI(Java Native Interface)与 Android 虚拟机(Dalvik/ART)进行交互。
2.Java.use查找并使用 Java 类
Java.use 函数会根据指定的类名来查找并返回一个 Java 类的引用。这个过程的底层原理是:
- Frida 通过 JNI 调用 Android 虚拟机提供的 FindClass 函数来查找指定的 Java 类。
- 找到类之后,Frida 会通过 JNI 进一步查询类中的所有方法,并在内部维护一个方法表(Method Table),方便后续进行方法的替换或调用。
3.使用 overload 和 implementation 替换方法
在 Frida 中,每个 Java 方法都有一个 overload 属性,它表示这个方法的不同重载版本。通过指定参数类型(如 java.lang.String),我们可以精确找到我们希望 Hook 的具体方法。
implementation 属性用于替换方法的实现,Frida 在底层通过 JNI 操作 Dalvik/ART 虚拟机的内部结构来完成这个替换过程:
- Frida 使用 JNI 调用来获取目标方法的原始地址。
- 然后,它将一个自定义的回调函数(在 JavaScript 中编写的)替换为原始方法的实现地址。
- 当目标应用调用这个方法时,它会跳转到 Frida 注入的回调函数中,从而实现 Hook。
4.4 原理细节
1.JNI(Java Native Interface)调用
Frida 与 Android 虚拟机的交互主要依赖于 JNI。JNI 是 Java 虚拟机与原生 C/C++ 代码之间的桥梁,Frida 通过 JNI 函数来实现对 Java 类和方法的查找和操作。
- FindClass:查找指定名称的 Java 类。
- GetMethodID:获取某个类中的方法 ID。
- CallMethod:调用 Java 方法。
- SetMethodID:设置或修改某个 Java 方法的实现地址。
2.方法表的修改(Method Table Manipulation)
在 Android 虚拟机(Dalvik/ART)中,每个 Java 类都有一个方法表(Method Table),用于存储类中所有方法的相关信息,包括方法的名称、参数类型、返回值类型以及方法的实际内存地址。
Frida 通过 JNI 和虚拟机内部 API 读取并修改这个方法表,将某个方法的入口地址替换为自己的回调函数的地址,从而实现方法的拦截。
Frida 在 Android 系统中 Hook Java 层的函数时,主要是通过 JNI 与 ART(Android Runtime) 或 Dalvik 进行交互。JNI 是 Java 虚拟机(JVM)与本地(Native)代码交互的一种接口标准,允许本地代码(如 C/C++)访问 Java 虚拟机中的类、方法和对象。
3.主要流程
Frida 注入到目标应用进程: Frida 的 frida-server 通过进程注入技术,将自己的动态库注入到目标应用的进程空间中。这个注入过程通常通过低级系统调用(如 ptrace)实现。
在目标进程中启动 Frida 引擎: 一旦注入成功,Frida 会启动一个嵌入的 JavaScript 引擎(如 V8)来执行用户编写的 JavaScript Hook 脚本。
通过 JNI 与 ART/Dalvik 交互: Frida 内部实现了对 JNI 的一系列封装。
通过 JNI,Frida 可以调用 Android 系统提供的底层接口来:
查找 Java 类(通过 FindClass)。
获取方法 ID(通过 GetMethodID 或 GetStaticMethodID)。
调用 Java 方法(通过 CallMethod)。
修改方法的实现(通过修改虚拟机的 Method Table)。Java 方法的存储与管理不同于本地方法:在 Java 层,所有的 Java 类和方法是由 Android 的 ART/Dalvik 虚拟机来管理的,而不是像本地方法那样直接存储在 ELF 文件的符号表中。因此,Java 层的方法是虚拟机内部的结构,而非 ELF 文件中的符号。
Frida 的 JNI 调用与虚拟机交互:Frida 的 Java Hook 主要是通过 JNI 来操作 ART/Dalvik 虚拟机。Frida 使用 JNI 来查找 Java 类和方法,调用和替换方法的实现,而这些操作不需要解析 ELF 文件。
- 上一篇: C语言小技巧两个感叹号(两个!)妙用
- 下一篇: 37.文件操作
猜你喜欢
- 2024-11-22 正点原子I.MX6U嵌入式Linux C应用编程:第二章《文件I/O基础》
- 2024-11-22 如何将Python函数输出内容同时打印到屏幕和文件
- 2024-11-22 3个重点,20个函数分析,浅析FFmpeg转码过程
- 2024-11-22 Linux驱动基础篇:hello驱动
- 2024-11-22 Python自带的库(open函数)读写txt、csv、json、XML、Excel文件
- 2024-11-22 UG NX OPEN二次开发实例:UF,C语言编程,创建圆柱体,API文档翻译
- 2024-11-22 openGauss SEQUENCE函数
- 2024-11-22 Python文件操作的步骤
- 2024-11-22 Python读取与写入Excel模块:openpyxl
- 2024-11-22 PHP imap_open函数任意命令执行漏洞
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)