Lazy loaded image
Frida学习笔记(六):Native 层 API 全解
Words 9206Read Time 24 min
2026-7-8
2026-7-8
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 不是平的,而是分四层依赖:
notion image
Frida Native 层 API 的四层依赖架构:底层 NativePointer 是基础载体,中层是 Memory/NativeFunction/NativeCallback/hexdump 工具,上层是 Interceptor.attach/replace 拦截器,旁路是 Process/Module/Thread/DebugSymbol 环境查询层
本篇的定位是:把每个 API 讲透,让你在后续实战中知道该用什么工具、怎么用。

二、NativePointer:Native 世界的通用货币

2.1 什么是 NativePointer

在 Java 层,你操作的是有类型的对象——Stringintbyte[],Frida 会自动做类型转换。在 Native 层,一切都回归到最原始的形式:内存地址和字节
NativePointer 是 Frida 对「64 位整数值」的封装。它可以代表一个内存地址(比如 0x7a8b004a90),也可以代表一个普通的整数值(比如函数的 int 参数)。在 Interceptor.attachargs 数组中,每个元素都是 NativePointerretval 也是 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。先用一张图把它们的关系定位清楚,再分别讲:
notion image
NativeFunction 与 NativeCallback 的双向调用对称关系图

3.1 为什么需要主动调用

第05篇讲了 Java 层的「主动调用」——直接从 Frida 脚本调用 App 的 Java 方法。Native 层同样需要这个能力。
场景一:你在 IDA 中发现 SO 内部有一个 decrypt(data, key, len) 函数。你想传入不同的密文测试它的解密行为——但这个函数不通过 JNI 暴露给 Java 层,App 自己也只在特定操作时才调用它。你需要直接从 Frida 脚本调用这个 Native 函数。
场景二:你想调用 libc 的 openreadwrite 来读写目标进程中的文件——而不是从你自己的电脑上操作。因为目标进程可能有一些特殊的文件权限(如 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 步顺序流程,任何一步漏掉或者错位都会导致进程崩溃或被检测
notion image
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 有两个核心方法 ── attachreplace,它们的区别是:
Interceptor.attach:在函数入口和出口安装监听器(onEnter / onLeave)。原始函数正常执行,你只是旁观和记录。类比:在高速公路上加装摄像头,车照常通行,你只是拍照。
Interceptor.replace:用你提供的实现完全替换原始函数。原始函数不再执行(除非你在替换实现中主动调用它)。类比:把高速公路的一个出口封掉,换成你自己修的一条路。
把两者的调用流并排画出来,差异一目了然:
notion image
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.attachonEnteronLeave 回调中,this.context 提供了对当前 CPU 寄存器状态的完整访问。这在参数不通过标准方式传递(比如自定义调用约定)或者你需要读取特殊寄存器时很有用。
下图先把 ARM64 上 Frida Hook 实战常读的子集梳理清楚(完整 AAPCS64 调用约定本系列会有专篇展开):
notion image
ARM64 寄存器在 this.context 中的实战速查图

8.2 修改寄存器

你也可以直接修改寄存器的值:

8.3 onEnter 和 onLeave 之间传递数据

this 对象在同一次函数调用的 onEnteronLeave 之间是共享的。你可以用它来在进入和退出时传递数据:
💡
不要用全局变量传递数据:在多线程环境中(Android App 几乎都是多线程的),如果两个线程同时调用被 Hook 的函数,全局变量会互相覆盖,导致数据错乱。this 对象是每次调用独立的,线程安全。

九、Process 与 Module:进程级信息查询

9.1 Process 对象

9.2 枚举内存范围

Process.enumerateRanges 返回进程中符合指定保护属性的所有内存映射区域。这在搜索内存、分析内存布局时很有用:
在全进程内存中搜索:结合 Process.enumerateRangesMemory.scan,可以在整个进程的所有可读内存中搜索特征数据 ── SO dump、内存取证、密钥定位都靠这个组合。

9.3 枚举线程

9.4 Module 对象


十、实战:监控文件访问——Hook libc 核心函数

综合运用本篇学到的 API,写一个实用工具:监控目标 App 的所有文件访问操作。这在分析 App 行为(读了什么配置文件、写了什么缓存、检测了哪些 Root 特征文件)时非常有价值。
运行这个脚本后,当 App 进行 Root 检测时,你会看到类似这样的输出:
通过这些日志,你可以清楚地看到 App 的 Root/Frida 检测都在检查什么文件,调用链指向了 SO 中的具体函数——为后续的绕过工作提供了精准的目标。
notion image
文件监控脚本工作原理

十一、实用速查: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 检测溯源。
上一篇
Frida学习笔记(七):调用栈、日志与 Hook 排错
下一篇
Frida学习笔记(五):Java 层 API 全解

Comments
Loading...