type
Post
status
Published
date
Jul 8, 2026
slug
summary
本篇目标:系统掌握 Frida 在 Native 层的全部核心 API。第05篇讲完了 Java 层的工具箱,本篇进入 Native 层。Native 层的 Frida API 围绕三个核心概念展开:NativePointer(一切数据的基础载体)、Memory(读、写、搜索、分配内存)、Interceptor(拦截和替换函数)。掌握了这些,你就拥有了在目标进程中「读任何数据、改任何数据、截获任何函数」的完整能力。
tags
frida
Android
逆向工程
category
公众号
icon
password
网址
上次编辑时间
Jul 8, 2026 03:12 AM
comment
AI 总结
本篇目标:系统掌握 Frida 在 Native 层的全部核心 API。第05篇讲完了 Java 层的工具箱,本篇进入 Native 层。Native 层的 Frida API 围绕三个核心概念展开:NativePointer(一切数据的基础载体)、Memory(读、写、搜索、分配内存)、Interceptor(拦截和替换函数)。掌握了这些,你就拥有了在目标进程中「读任何数据、改任何数据、截获任何函数」的完整能力。
一、Native 层 API 全景图
先看全局。Frida 的 Native 层 API 按功能分为五组:
功能分组 | 核心 API | 一句话说明 |
指针与数值 | NativePointer | 64 位地址值,支持算术运算和内存读写 |
ㅤ | NativeFunction | 从 JS 主动调用 Native 函数 |
ㅤ | NativeCallback | 创建可被 Native 代码调用的 JS 回调函数 |
ㅤ | ptr(value) | 创建 NativePointer 的快捷方式 |
ㅤ | NULL | 空指针常量,等同于 ptr(0) |
内存操作 | Memory.read* / Memory.write* | 读写指定地址的数据 |
ㅤ | Memory.alloc(size) | 在目标进程中分配内存 |
ㅤ | Memory.allocUtf8String(str) | 分配内存并写入 UTF-8 字符串 |
ㅤ | Memory.scan(addr, size, pattern, callbacks) | 在内存范围中搜索字节模式 |
ㅤ | Memory.scanSync(addr, size, pattern) | 同步版内存搜索 |
ㅤ | Memory.copy(dst, src, n) | 复制内存 |
ㅤ | Memory.protect(addr, size, prot) | 修改内存页的保护属性 |
ㅤ | hexdump(target, options) | 以十六进制+ASCII格式 dump 内存 |
函数拦截 | Interceptor.attach(addr, callbacks) | Hook 函数入口和出口 |
ㅤ | Interceptor.replace(addr, replacement) | 完全替换函数实现 |
ㅤ | Interceptor.revert(addr) | 撤销 Hook/替换 |
ㅤ | Interceptor.detachAll() | 撤销所有 Hook |
ㅤ | Interceptor.flush() | 确保所有挂起的 Hook 修改已生效 |
进程与模块 | Process.id / Process.arch / Process.platform | 进程基本信息 |
ㅤ | Process.enumerateModules() | 枚举所有已加载的 SO |
ㅤ | Process.enumerateRanges(prot) | 枚举符合保护属性的内存范围 |
ㅤ | Process.findModuleByName(name) | 查找指定名称的模块 |
ㅤ | Process.getModuleByName(name) | 同上,找不到时抛异常 |
ㅤ | Module.findExportByName(mod, name) | 查找导出符号地址 |
ㅤ | Module.enumerateExports() | 枚举模块的导出符号 |
ㅤ | Module.enumerateImports() | 枚举模块的导入符号 |
线程与调试 | Thread.backtrace(ctx, backtracer) | 获取 Native 调用堆栈 |
ㅤ | DebugSymbol.fromAddress(addr) | 将地址解析为符号信息 |
ㅤ | Process.enumerateThreads() | 枚举所有线程 |
表给"清单",下面这张图给"位置感"——5 类 API 不是平的,而是分四层依赖:

Frida Native 层 API 的四层依赖架构:底层 NativePointer 是基础载体,中层是 Memory/NativeFunction/NativeCallback/hexdump 工具,上层是 Interceptor.attach/replace 拦截器,旁路是 Process/Module/Thread/DebugSymbol 环境查询层
本篇的定位是:把每个 API 讲透,让你在后续实战中知道该用什么工具、怎么用。
二、NativePointer:Native 世界的通用货币
2.1 什么是 NativePointer
在 Java 层,你操作的是有类型的对象——
String、int、byte[],Frida 会自动做类型转换。在 Native 层,一切都回归到最原始的形式:内存地址和字节。NativePointer 是 Frida 对「64 位整数值」的封装。它可以代表一个内存地址(比如 0x7a8b004a90),也可以代表一个普通的整数值(比如函数的 int 参数)。在 Interceptor.attach 的 args 数组中,每个元素都是 NativePointer;retval 也是 NativePointer。类比理解:如果说 Java 层的 Frida 像在用高级语言操作数据库(有 ORM、有类型映射、有自动转换),那 Native 层的 Frida 就像在用 C 语言直接操作裸内存——你拿到的每个值都是一个「地址/数字」,需要你自己决定如何解读它。
2.2 创建 NativePointer
2.3 算术运算
NativePointer 支持地址算术——这在计算偏移、遍历数组、访问结构体字段时必不可少:
常见场景:在 IDA 中你看到一个结构体的字段布局是
offset 0x00: int type; offset 0x04: int size; offset 0x08: char* data;。用 Frida 读取时就是 Memory.readInt(structPtr.add(0x00))、Memory.readInt(structPtr.add(0x04))、Memory.readPointer(structPtr.add(0x08))——每次 .add(offset) 都是 NativePointer 的算术运算。
2.4 类型转换
NativePointer 本质上是一个 64 位整数,你需要根据上下文把它转换为合适的类型:
64 位精度陷阱:JavaScript 的 Number 类型只能精确表示 2^53 以内的整数。ARM64 的地址空间是 64 位的,地址值可能超过 2^53。因此,永远不要用
parseInt(ptr.toString(), 16) 来获取地址的数值——对于大地址会丢失精度。如果需要比较两个地址,用 .equals() 或 .compare();如果需要做算术,用 .add() / .sub()。它们内部都是用 64 位整数运算的,不经过 JavaScript Number。
2.5 通过 NativePointer 读写内存
NativePointer 对象自身就提供了读写内存的快捷方法——和
Memory.read*/write* 等价,但语法更链式化:Memory.read* vs NativePointer.read*:自 Frida 14(2021)起,官方推荐使用 addr.readX() / addr.writeX() 形式,旧的 Memory.readS32(addr) 静态方法虽仍可用但已标记弃用,未来主版本可能移除。链式写法在读取结构体字段时更天然(addr.add(4).readS32() 一气呵成)。本篇 第五节仍会列出 Memory.* 形式,主要用于对照——新代码请优先用 NativePointer 实例方法。
64 位算术是安全的:
add / sub / and / or / shr / shl 都在 NativePointer 内部以 64 位整数实现,不经过 JS Number,所以即使地址 >2^53 也不会丢精度。只有当你显式 toInt32() / toUInt32() / Number(...) 时才会落到 JS Number 上。
三、NativeFunction:从 JS 主动调用 Native 函数
NativeFunction 和下一节的 NativeCallback 是一对对称 API ── 一个负责 JS→Native,一个负责 Native→JS。先用一张图把它们的关系定位清楚,再分别讲:
NativeFunction 与 NativeCallback 的双向调用对称关系图
3.1 为什么需要主动调用
第05篇讲了 Java 层的「主动调用」——直接从 Frida 脚本调用 App 的 Java 方法。Native 层同样需要这个能力。
场景一:你在 IDA 中发现 SO 内部有一个
decrypt(data, key, len) 函数。你想传入不同的密文测试它的解密行为——但这个函数不通过 JNI 暴露给 Java 层,App 自己也只在特定操作时才调用它。你需要直接从 Frida 脚本调用这个 Native 函数。场景二:你想调用 libc 的
open、read、write 来读写目标进程中的文件——而不是从你自己的电脑上操作。因为目标进程可能有一些特殊的文件权限(如 App 私有目录 /data/data/包名/ 下的文件),从进程内部访问比从外部访问更方便。场景三:调用
dlopen / dlsym 来手动加载一个 SO 并获取其中的函数地址。3.2 基本语法
3.3 类型系统
NativeFunction 支持的类型标识符:
类型标识符 | C 类型 | 大小 |
"void" | void | 0(仅用于返回类型) |
"bool" | bool | 1 字节 |
"int" | int | 4 字节 |
"uint" | unsigned int | 4 字节 |
"int8" | int8_t | 1 字节 |
"uint8" | uint8_t | 1 字节 |
"int16" | int16_t | 2 字节 |
"uint16" | uint16_t | 2 字节 |
"int32" | int32_t | 4 字节 |
"uint32" | uint32_t | 4 字节 |
"int64" | int64_t | 8 字节 |
"uint64" | uint64_t | 8 字节 |
"long" | long | LP64(Linux/Android/macOS ARM64)= 8 字节;LLP64(Windows)= 4 字节 |
"ulong" | unsigned long | 同上 |
"size_t" | size_t | LP64 = 8 字节;LLP64 = 8 字节(Win64)/ 4 字节(Win32) |
"ssize_t" | ssize_t | 同 size_t |
"float" | float | 4 字节 |
"double" | double | 8 字节 |
"pointer" | void* | 8 字节(ARM64) |
3.4 实用示例
调用约定(ABI):Android ARM64 默认使用 AAPCS64,本系列实战不需关心。仅在跨平台(Windows x86 上的
stdcall)才需要在 new NativeFunction(addr, ret, args, { abi: 'stdcall' }) 中指定。
四、NativeCallback:让 Native 代码调用你的 JS 函数
4.1 NativeCallback 是什么
NativeFunction 的方向是「JS → Native」(你主动调用 Native 函数)。NativeCallback 的方向是反过来——「Native → JS」。它创建一个 Native 层可见的函数指针,当 Native 代码调用这个指针时,实际执行的是你写的 JavaScript 代码。4.2 基本语法
4.3 实战:替换函数指针
NativeCallback 最常见的用途是与
Interceptor.replace 配合——用你的 JS 函数完全替换一个 Native 函数的实现:4.4 NativeCallback 在回调注册中的应用
有些 Native API 接受函数指针作为回调参数。你可以用 NativeCallback 创建一个 JS 回调,注入到 Native 代码中:
生命周期注意:
NativeCallback 创建的回调函数指针只在 JS 的 NativeCallback 对象存活期间有效。如果这个 JS 对象被垃圾回收了,Native 代码再调用那个函数指针就会崩溃。确保将 NativeCallback 对象保存在全局变量中,避免被 GC。
五、Memory:读、写、搜索、分配
5.1 静态读写 API(与 NativePointer 实例方法等价)
2.5 节已经讲过 NativePointer 的链式读写方法。
Memory.* 命名空间下还有一组等价的静态方法——两者功能完全相同,只是写法不同。下表是对照速查(推荐左列写法,右列只在需要对齐多列代码时使用):推荐:实例方法 | 等价:静态方法 |
addr.readS8() / readU8() | Memory.readS8(addr) / readU8(addr) |
addr.readS16() / readU16() | Memory.readS16(addr) / readU16(addr) |
addr.readS32() / readU32() | Memory.readS32(addr) / readU32(addr) |
addr.readS64() / readU64() | Memory.readS64(addr) / readU64(addr)(返回 Int64/UInt64) |
addr.readFloat() / readDouble() | Memory.readFloat(addr) / readDouble(addr) |
addr.readPointer() | Memory.readPointer(addr) |
addr.readUtf8String(n?) / readUtf16String(n?) | Memory.readUtf8String(addr, n?) / readUtf16String(addr, n?) |
addr.readByteArray(n) | Memory.readByteArray(addr, n) |
addr.writeS32(v) / writeU32(v) / ... | Memory.writeS32(addr, v) / writeU32(addr, v) / ... |
addr.writeUtf8String(s) | Memory.writeUtf8String(addr, s) |
addr.writeByteArray([...]) | Memory.writeByteArray(addr, [...]) |
addr.writePointer(p) | Memory.writePointer(addr, p) |
写入保护:你不能往只读内存(如 .text 代码段)直接写入数据,会触发段错误(SIGSEGV)。需要先用Memory.protect修改保护属性——见 5.4 节。
5.2 Memory.alloc:在目标进程中分配内存
Memory.alloc(size) 在目标进程的堆上分配指定大小的内存,返回一个 NativePointer 指向分配的地址。这块内存会被零初始化。内存生命周期:
Memory.alloc 分配的内存的生命周期与返回的 JS 变量绑定。只要 JS 变量还被引用(不被 GC),内存就不会被释放。如果你把分配的地址传给 Native 代码长期使用(比如注册的回调中引用了这块内存),确保将 JS 变量保存在全局作用域中。
5.3 Memory.scan:在内存中搜索字节模式
Memory.scan 在指定的内存范围中搜索匹配特定字节模式的位置。这是内存取证和动态分析中的核心能力。同步版本
Memory.scanSync 直接返回匹配结果数组:5.4 Memory.protect:修改内存保护属性
直接 patch 代码段是 4 步顺序流程,任何一步漏掉或者错位都会导致进程崩溃或被检测:

Memory.protect 修改代码段的 4 步时序图
风险提醒:修改内存保护属性和直接 patch 代码是非常底层的操作。错误的修改可能导致进程崩溃,且 patch 不会随脚本卸载而恢复——必须自己保存和恢复原始字节。在大部分检测绕过场景中,优先使用
Interceptor.attach / Interceptor.replace(卸载脚本时自动还原);只有在 attach/replace 因调用约定怪异、热路径开销过大或被反 Hook 检测时,才回退到 Memory.protect + 直接 patch。
5.5 Memory.copy:复制内存
Memory.copy 在把内存映像(SO 段、堆缓冲、共享映射等)转移到可控缓冲区时很常用,是 dump 类工具的基础操作。六、hexdump:内存可视化分析的瑞士军刀
hexdump 不是一个「Memory」命名空间下的函数,而是 Frida 提供的全局工具函数。但它在 Native 层分析中几乎每个脚本都会用到。6.1 基本用法
输出:
左侧是地址,中间是十六进制的字节值,右侧是 ASCII 可视化(不可打印字符显示为
.)。6.2 配置选项
6.3 在 Hook 中使用 hexdump
七、Interceptor.replace vs Interceptor.attach
7.1 两者的区别
Interceptor 有两个核心方法 ── attach 和 replace,它们的区别是:Interceptor.attach:在函数入口和出口安装监听器(onEnter / onLeave)。原始函数正常执行,你只是旁观和记录。类比:在高速公路上加装摄像头,车照常通行,你只是拍照。
Interceptor.replace:用你提供的实现完全替换原始函数。原始函数不再执行(除非你在替换实现中主动调用它)。类比:把高速公路的一个出口封掉,换成你自己修的一条路。
把两者的调用流并排画出来,差异一目了然:

Interceptor.attach vs Interceptor.replace 调用流对比
7.2 Interceptor.replace 用法
注意:在
Interceptor.replace 之前,先用 new NativeFunction(addr, ...) 保存原始函数的引用。replace 之后再创建 NativeFunction 会得到替换后的版本——调用它会递归调用你的替换函数,导致无限循环。
7.3 什么时候用 replace
场景 | 推荐 |
想看参数和返回值,不改变行为 | attach |
想修改参数或返回值 | attach(在 onEnter/onLeave 中修改) |
想完全跳过函数执行 | replace |
想绕过检测(总是返回特定值) | replace |
想用自己的逻辑替换原始实现 | replace |
Hook 后 App 崩溃(attach 改变了栈帧等) | 试试 replace |
7.4 撤销 Hook
八、this.context:访问 CPU 寄存器
8.1 在 Interceptor 回调中读取寄存器
在
Interceptor.attach 的 onEnter 和 onLeave 回调中,this.context 提供了对当前 CPU 寄存器状态的完整访问。这在参数不通过标准方式传递(比如自定义调用约定)或者你需要读取特殊寄存器时很有用。下图先把 ARM64 上 Frida Hook 实战常读的子集梳理清楚(完整 AAPCS64 调用约定本系列会有专篇展开):

ARM64 寄存器在 this.context 中的实战速查图
8.2 修改寄存器
你也可以直接修改寄存器的值:
8.3 onEnter 和 onLeave 之间传递数据
this 对象在同一次函数调用的 onEnter 和 onLeave 之间是共享的。你可以用它来在进入和退出时传递数据:不要用全局变量传递数据:在多线程环境中(Android App 几乎都是多线程的),如果两个线程同时调用被 Hook 的函数,全局变量会互相覆盖,导致数据错乱。
this 对象是每次调用独立的,线程安全。
九、Process 与 Module:进程级信息查询
9.1 Process 对象
9.2 枚举内存范围
Process.enumerateRanges 返回进程中符合指定保护属性的所有内存映射区域。这在搜索内存、分析内存布局时很有用:在全进程内存中搜索:结合
Process.enumerateRanges 和 Memory.scan,可以在整个进程的所有可读内存中搜索特征数据 ── SO dump、内存取证、密钥定位都靠这个组合。
9.3 枚举线程
9.4 Module 对象
十、实战:监控文件访问——Hook libc 核心函数
综合运用本篇学到的 API,写一个实用工具:监控目标 App 的所有文件访问操作。这在分析 App 行为(读了什么配置文件、写了什么缓存、检测了哪些 Root 特征文件)时非常有价值。
运行这个脚本后,当 App 进行 Root 检测时,你会看到类似这样的输出:
通过这些日志,你可以清楚地看到 App 的 Root/Frida 检测都在检查什么文件,调用链指向了 SO 中的具体函数——为后续的绕过工作提供了精准的目标。

文件监控脚本工作原理
十一、实用速查:Native 层常用操作代码片段
总结
把本篇浓缩成几条最容易踩坑、且不显而易见的要点:
- NativePointer 是 64 位安全的——add/sub/and/or/shr 都不会丢精度,只有显式 toInt32() / Number(...) 时才落到 JS Number。从 Frida 14 起,所有读写优先用 addr.readX() 链式写法,Memory.read* 静态形式已弃用。
- NativeCallback 与 Memory.alloc 都受 JS GC 控制——把它们交给 Native 长期持有时(注册回调、传给后台线程),必须在 JS 端用全局变量保活,否则随时会崩。
this是 onEnter/onLeave 间唯一线程安全的传递通道——多线程 App 用全局变量传参一定会串。
- Interceptor.replace 之前要先
new NativeFunction(addr, ...)保存原函数引用,否则 replace 后再创建会得到自己,导致无限递归。
- **Memory.protect 改完代码段后请改回 r-x**——长期保留 rwx 是最常见的反 Frida 检测特征之一;并自己备份原始字节,patch 不会随脚本卸载自动还原。
文件监控实战脚本(
file_monitor.js)把以上要点全部综合在一个真实工作流里——它不是教学玩具,可直接用于 Root/Frida 检测溯源。- Author:24th
- URL:https://24th.top/article/397e5b08-46db-8047-8279-f367c1f23c38
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!








