安卓frida笔记
连接:[[../Android连接#流程|Android连接]]
Frida 使用教程和错误汇总
Frida工作原理学习(1)-Android安全-看雪-安全社区|安全招聘|kanxue.com
frida注入的主要思路就是找到目标进程,使用ptrace跟踪目标进程获取mmap,dlpoen,dlsym等函数库的便宜获取mmap在目标进程申请一段内存空间将在目标进程中找到存放[frida-agent-32/64.so]的空间启动执行各种操作由agent去实现。
补充:frida注入之后会在远端进程分配一段内存将agent拷贝过去并在目标进程中执行代码,执行完成后会 detach 目标进程,这也是为什么在 frida 先连接上目标进程后还可以用gdb/ida等调试器连接,而先gdb连接进程后 frida 就无法再次连上的原因(frida在注入时只会ptrace一下下注入完毕后就会结束ptrace所以ptrace占坑这种反调试使用spawn方式启动即可)。
frida-agent 注入到目标进程并启动后会启动一个新进程与 host 进行通信,从而 host 可以给目标进行发送命令,比如执行代码,激活/关闭 hook,同时也能接收到目标进程的执行返回以及异步事件信息等

可以通过命令行来注入js脚本:
frida -U -f com.example.myapplication -l AndroidFridaHook.js
也可以通过python来注入,由于可拓展性,以下均以python做note:
测试连接:
在[[../Android连接|Android连接]]连接上frida后,使用frida-ps -Uai找到 相关包名、app名信息
inject.py
from struct import pack
import frida
import os
def on_message(message, data):
if message['type'] == 'send':
print(f"[JS] {message['payload']}")
else:
print(message)
def spawn_or_attach(device, package_name, app_name, switch_mode=1):
"""
附加到进程或启动新进程
switch_mode: 1=Spawn模式, 0=Attach模式
返回: (pid, session)元组
"""
pid = None
# 1. Spawn启动新进程
if switch_mode == 1:
print(f"使用Spawn模式启动: {package_name}")
pid = device.spawn(package_name)
# 2. Attach已存在进程
else:
print(f"使用Attach模式查找: {app_name}")
processes = device.enumerate_processes()
for process in processes:
if process.name == app_name:
pid = process.pid
print(f"找到进程: {process.name} (PID: {pid})")
break
if pid is None:
raise Exception(f"未找到应用: {app_name}")
try:
print(f"附加到进程: PID {pid}")
session = device.attach(pid)
return pid, session
except Exception as e:
print(f"附加进程失败: {e}")
exit(1)
def load_script(session, js_script_file):
"""
加载并执行JS脚本
返回: script对象
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
js_path = os.path.join(script_dir, js_script_file)
try:
with open(js_path, "r", encoding="utf-8") as f:
js_code = f.read()
script = session.create_script(js_code)
script.on('message', on_message)
script.load()
return script
except Exception as e:
print(f"加载脚本失败: {e}")
exit(1)
if __name__ == "__main__":
# 配置参数
package_name = "com.example.myapplication"
app_name = "My Application"
js_script_file = "AndroidFridaHook.js"
spawn_mode = 1 # 1=Spawn, 0=Attach
try:
# 连接设备
device = frida.get_remote_device()
# 附加到进程
pid, session = spawn_or_attach(device, package_name, app_name, spawn_mode)
# 加载脚本
script = load_script(session, js_script_file)
# 如果是Spawn模式,需要恢复执行
if spawn_mode == 1:
print("恢复进程执行...")
device.resume(pid)
# 保持连接
print("脚本运行中,按Enter键退出...")
input()
except Exception as e:
print(f"发生错误: {e}")
AndroidFridaHook.js
使用setImmediate确保在主线程执行
console.log("Frida脚本已成功注入!");
setImmediate(function () {
setTimeout(function () {
Java.perform(function () {
console.log("Java VM已附加");
// var BtnActivity = Java.use("com.example.myapplication.BtnActivity");
// BtnActivity.Sub.implementation = function(a, b) {...}
// console.log("[*] Sub函数被调用");
});
}, 1000);
});
或者第二种格式:
console.log("frida hook start");
function diy_func(){
Java.perform(function () {
console.log("diy_func")
});
}
function main() {
diy_func();
}
setImmediate(function(){
setTimeout(main, 1000);
});
Java层Hook
Hook java方法 方法接口
通过 Frida 的 Java.use API 获取类引用,然后替换方法实现,属于被动调用hook
用implementation调用方法的接口
var BtnActivity = Java.use("com.example.myapplication.BtnActivity");
BtnActivity.Sub.implementation = function(a, b) {...}
调用原方法:
var result = this.方法名如add(a, b, c);
修改方法返回值:
return hookedResult;
也有第二种hook写法 使用Java.choose:
通过Java.choose动态查找实例,
找到实例后执行onMatch,然后调用实例方法
调用完实例方法后执行onComplete
Java.perform(function () {
console.log("start")
var FridaActivity = Java.use("com.example.study")
FridaActivity.static_bool_var.value = true
Java.choose("com.example.study", {
onMatch: function (instance) {
instance.bool_var.value = true
instance._same_name_bool_var.value = true
},
onComplete: function () {
console.log("end")
}
})
})
Hook 内部类 重载 构造 实例
用$符号连接外部类和内部类
静态内部类:
var StaticUsed = Java.use("com.example.myapplication.BtnActivity$StaticUsed");
StaticUsed.getName.implementation = function() {...}
StaticUsed.$init.implementation = function(name2) {...}
[!NOTE]
非静态内部类:(不过在这个例子中去掉overload也可以,那么就和静态内部类一样,这里做个拓展。反正我不太懂这里)var CheckUsed = Java.use("com.example.myapplication.BtnActivity$CheckUsed"); CheckUsed.check.implementation = function(value) {...} CheckUsed.$init.overload('com.example.myapplication.BtnActivity').implementation = function(btnActivity) {...}
用overload指明 重载方法 的参数类型
StaticUsed.mul.overload('int', 'int').implementation = function(a, b) {...}
用$init指明hook的方法为构造方法
this.$init(name2); // 使用$init调用构造函数
用.value获取内部类的字段值
var name1 = StaticUsed.name1.value;
用$new创建新的内部类实例,tips:StaticNotUsed从来没有创建和调用过实例
var StaticNotUsed = Java.use("com.example.myapplication.BtnActivity$StaticNotUsed");
var instance = StaticNotUsed.$new("hello"); //创建实例并传参进构造方法
var result = instance.getName(); //调用普通方法
内部类(Inner Class)是指在一个类的内部定义的类,如以下的非静态内部类
public class Outer {
class Inner { // 成员内部类
// ...
}
}
举例一个静态内部类:
public class BtnActivity extends AppCompatActivity {
public static class StaticNotUsed {
private String name = "成员变量名字_StaticNotUsed"; //name字段,想要获取这个值需要使用.value
public StaticNotUsed(String name) { // 构造函数
this.name = name; // 像这种如果是方法的传参直接拿就行,就不需要用value
}
public String getName() { // 普通方法
return this.name;
}
// 重载函数略
}
}
var StaticUsed = Java.use("com.example.myapplication.BtnActivity$StaticUsed");
// Hook构造函数
StaticUsed.$init.implementation = function(name2) {
console.log("[*] StaticUsed构造函数被调用");
name2 = "Hook修改的名字";
this.$init(name2); // 使用$init调用构造函数
// 构造函数没有返回值
};
// Hook普通方法
StaticUsed.getName.implementation = function() {
console.log("[*] StaticUsed.getName被调用");
var name1 = StaticUsed.name1.value; // 使用.value获取字段值
return hookedResult;
};
// Hook重载方法
StaticUsed.mul.overload('int', 'int').implementation = function(a, b) {
console.log("[*] StaticUsed.mul(a,b)被调用");
var result = StaticUsed.mul(a, b); // 传入想hook的a、b参数,想要重载就修改上面的overload的传参
var hookedResult = 12345;
return hookedResult;
};
Android逆向学习笔记——使用Python库调用Frida | Whitebird's Home
Frida JavaScript 常用 API - 简书
【FridaHook整理】Frida安装及Hook安卓常用脚本_frida hook-CSDN博客
Frida用法详解【附用例】_frida -u -f-CSDN博客
frida的用法--Hook Java代码篇 - luoyesiqiu - 博客园
[原创]Frida-Hook-Java层操作大全
[原创]Frida-Hook-Native层操作大全
java.use找不到类?
枚举类加载器
关于多classloader的hook和原理探究-Android安全-看雪
在 Android 应用中,某些类可能不是通过默认的 dalvik.system.PathClassLoader 加载的,而是通过其他类加载器(如 dalvik.system.InMemoryDexClassLoader)加载。Frida 默认情况下只能挂钩由默认类加载器加载的类。
使用 enumerateClassLoaders 可以找到这些非默认的类加载器,并通过它们来挂钩所需的类。enumerateClassLoaders 是 Frida 提供的一个 API,用于枚举 Java 虚拟机中当前存在的所有类加载器。这个功能在处理动态加载的类或非默认类加载器加载的类时特别有用。
或者在某些情况下,类可能还没有被加载。使用 enumerateClassLoaders 方法可以查找类加载器并手动加载类,从而进行调试和分析。
Java.perform(function () {
Java.enumerateClassLoaders({ // 枚举类加载器
onMatch: function (loader) {
try {
if (loader.findClass("com.example.TargetClass")) {
console.log("Class loader found: " + loader);
Java.classFactory.loader = loader; // 切换类加载器
var TargetClass = Java.use("com.example.TargetClass");
// 进行挂钩操作
TargetClass.someMethod.implementation = function () {
console.log("Hooked method called!");
return this.someMethod();
};
}
} catch (e) {
console.log(e);
}
},
onComplete: function () {
console.log("Enumeration complete");
}
});
});
在这个示例中,enumerateClassLoaders 用于枚举所有类加载器,并查找包含 com.example.TargetClass 的类加载器。一旦找到该类加载器,就将其设置为当前的类加载器,并对 someMethod 方法进行挂钩。
枚举所有已加载的类
Java.perform(function () {
Java.enumerateLoadedClasses({// 枚举所有已加载的类,包括系统类,所以需要过滤
onMatch: function (name, handle) {
if (name.indexOf("com.example.myapplication") < 0) { // 匹配类名,用于定位目标类
return;
}
else if (name.indexOf("$$ExternalSynthetic") >= 0) {
// 过滤掉编译时自动生成的合成类,但它们在运行时确实存在,并且Frida能够检测到它们
return;
}
console.log("name: " + name + "\n\thandle: " + handle);
// Java.use(name).check.implementation = function () {
// return true;
// };
},
onComplete: function () {
console.log("类枚举完成");
}
});
});
打印:
name: com.example.myapplication.BtnActivity$Calculator
handle: 0x506b6
name: com.example.myapplication.R$string
handle: 0x50d46
name: com.example.myapplication.BtnActivity
handle: 0x51a96
name: com.example.myapplication.utils.NavigationHelper
handle: 0x52226
name: com.example.myapplication.BtnActivity$CheckUsed
handle: 0x52bc6
name: com.example.myapplication.R$layout
handle: 0x53ba6
name: com.example.myapplication.SaveDataAndFunc
handle: 0x53bc6
name: com.example.myapplication.BtnActivity$StaticUsed
handle: 0x53d26
name: com.example.myapplication.BtnActivity$SimpleCalculator
handle: 0x54406
name: com.example.myapplication.JumpTo
handle: 0x54696
name: com.example.myapplication.R$id
handle: 0x54ae6
类枚举完成
枚举所有方法
Java.perform(function(){
var Demo = Java.use("com.zj.wuaipojie.Demo");//Demo是一个Activity
//getDeclaredMethods枚举所有方法
var methods =Demo.class.getDeclaredMethods();
for(var j=0; j < methods.length; j++){
var methodName = methods[j].getName();
console.log(methodName);
for(var k=0; k<Demo[methodName].overloads.length;k++){
Demo[methodName].overloads[k].implementation = function(){
for(var i=0;i<arguments.length;i++){
console.log(arguments[i]);
}
return this[methodName].apply(this,arguments);
}
}
}
})
枚举接口
Java.perform(function () {
Java.enumerateLoadedClasses({
onMatch: function (class_name) {
if (class_name.indexOf("com.example.myapplication") < 0) {
return;
} else if (class_name.indexOf("$$ExternalSynthetic") >= 0) {
// 过滤掉编译时自动生成的合成类,但它们在运行时确实存在,并且Frida能够检测到它们
return;
} else {
var hook_cls = Java.use(class_name);
var interfaces = hook_cls.class.getInterfaces();
if (interfaces.length > 0) {
console.log(class_name + ": ");
for (var i in interfaces) {
console.log("\t", interfaces[i].toString());
}
}
}
},
onComplete: function () {
console.log("接口枚举完成");
}
});
});
打印:
com.example.myapplication.BtnActivity$SimpleCalculator:
interface com.example.myapplication.BtnActivity$Calculator
[Ljava.lang.Class;@ea8102
function d() {
[native code]
}
枚举so导出符号
Java.perform(() => {
let libnb = Module.enumerateExportsSync("libhoudini.so");
libnb.forEach(exp => console.log(exp.name));
});
打印:
S_000035
s_000037
stack chk_fail
s_000061
s_000039
s_000036
NativeBridgeItf
s_000038
...
打印栈回溯
function printStack(name) {
Java.perform(function () {
var Exception = Java.use("java.lang.Exception");
var ins = Exception.$new("Exception");
var straces = ins.getStackTrace();
if (straces != undefined && straces != null) {
var strace = straces.toString();
var replaceStr = strace.replace(/,/g, "\\n");
console.log("=============================" + name + " Stack strat=======================");
console.log(replaceStr);
console.log("=============================" + name + " Stack end=======================\r\n");
Exception.$dispose();
}
});
}
输出:
[Google Pixel::com.tlamb96.spetsnazmessenger]-> 2131558449 111 02:27 下午 false
=============================com.tlamb96.kgbmessenger.b.a Stack strat=======================
com.tlamb96.kgbmessenger.b.a.<init>(Native Method)
com.tlamb96.kgbmessenger.MessengerActivity.onSendMessage(Unknown Source:40)
java.lang.reflect.Method.invoke(Native Method)
android.support.v7.app.m$a.onClick(Unknown Source:25)
android.view.View.performClick(View.java:6294)
android.view.View$PerformClick.run(View.java:24770)
android.os.Handler.handleCallback(Handler.java:790)
android.os.Handler.dispatchMessage(Handler.java:99)
android.os.Looper.loop(Looper.java:164)
android.app.ActivityThread.main(ActivityThread.java:6494)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
=============================com.tlamb96.kgbmessenger.b.a Stack end=======================
手动加载dex并调用
自己手搓一个java文件,编译出dex之后,通过Java.openClassFile("xxx.dex").load()加载,这样我们就可以正常通过Java.use调用里面的方法了
深入Native函数并主动调用
特点:
- 直接hook so库中的函数
- 可以hook任何native函数,不限于JNI函数
- 可以访问函数内部实现细节
- 可以监控函数内部的执行流程
- 可以监控函数调用参数和返回值
- 可以修改函数返回值
- 可以读取和修改寄存器状态
- 可以访问原始内存
- 需要知道函数的确切地址
列出so文件中的所有导出函数:
打印模块基址
var so_addr = Process.findModuleByName("libwolf.so");
if (so_addr) {
console.log("so_addr: ", so_addr.base);
}
列出导出函数
// console.log("[*] 导出函数列表:");
var exports = so_addr.enumerateExports();
exports.forEach(function(exp) {
console.log(" - 名称: " + exp.name);
console.log(" 地址: " + exp.address);
console.log(" 类型: " + exp.type);
console.log(" ---");
});
寻找指定函数
var func_addr = Module.findExportByName("libwolf.so", "ds_func");
console.log("func_addr: ", func_addr);
主动调用偏移处的函数
so内的函数根据函数性质可以分为两种:
- JNI函数,绝大部分都是导出函数,调用函数传参时需要传入
JNIEnv- 动态注册
- 静态注册
- [[../安卓IDA使用经验#注册Native方法|如何区分JNI函数是动态注册还是静态注册]]
- 在寻找他们地址时通用方法是通过
基地址 + 偏移直接计算地址。 - 静态注册的函数(如
Java_前缀)可通过findExportByName查找
- C/C++普通函数,可以是导出函数也可以是内部函数,不过没关系,对Frida来说,只要知道偏移地址就都可以hook
JNI函数
确定函数偏移
var so_addr = Process.findModuleByName("libnative-lib.so");
var func_addr = Module.findExportByName("libnative-lib.so", "Java_com_example_myapplication_BtnActivity_sub");
var targetAddr = so_addr.base.add(0x620);//注意这里的add是加偏移
// func_addr和targetAddr这两者相同,通过函数名查找和通过偏移查找都可以
调用函数
需要传入:JNIEnv*和jobject
// 获取有效的JNIEnv*和jobject
const env = Java.vm.getEnv();
const activity = Java.use("com.example.BtnActivity").$new();
var targetFunc = new NativeFunction(targetAddr, 'int', ['pointer', 'pointer', 'int', 'int', 'int']);// 运行时地址,返回类型,传参
var result = targetFunc(env.handle, activity.handle, 1, 2, 3);// 传参:JNIEnv*, jobject, 和三个整数

- targetAddr : 目标函数的内存地址
- 'int' : 函数返回值类型
['pointer', 'pointer', 'int', 'int', 'int']: 函数参数列表,依次为:- 第一个 pointer : JNIEnv* 指针(JNI 环境)
- 第二个 pointer : jobject 指针(Java 对象实例)
- 第一个 int : 函数的第一个实际参数 a
- 第二个 int : 函数的第二个实际参数 b
- 第三个 int : 函数的第三个实际参数 c
C++的普通函数
- 如果函数是C++的普通函数(如 wolf_de ),则不需要JNI参数

确定函数偏移
var so_addr = Process.findModuleByName("libwolf.so");
var wolf_de_addr = Module.findExportByName("libwolf.so", "_Z7wolf_dePKc");// 通过函数名找地址,函数名得看汇编,而不是伪代码的函数名
wolf_de_addr = so_addr.base.add(0x1a520); // 通过偏移找地址,这里的偏移是找IDA Pro的函数首地址
调用函数
var wolf_de = new NativeFunction(wolf_de_addr, 'pointer', ['pointer']);
var str = Memory.allocUtf8String("636D55B2AA8609CB");
var result = wolf_de(str);
console.log("[+] wolf_de函数返回值: ", result);
console.log("[+] 返回字符串: ", Memory.readCString(result));// 读取内存地址处的内容并转化为字符串
调用函数时 不同类型参数的传递
在Frida中,根据不同的参数类型,有不同的传递方法:
字符串类型
var str = Memory.allocUtf8String("636D55B2AA8609CB");
整型参数
直接传递数值即可:
// 整型参数
var intArg = 123;
var result = someFunction(intArg);
// 如果需要指定不同大小的整型
var int8Arg = 123 & 0xFF; // 8位整数
var int16Arg = 12345 & 0xFFFF; // 16位整数
var int32Arg = 123456789 & 0xFFFFFFFF; // 32位整数
指针类型参数
使用 ptr 函数创建指针:
// 空指针
var nullPtr = ptr(0);
// 特定地址的指针
var somePtr = ptr("0xabcd1234");
// 从基址偏移创建指针
var offsetPtr = moduleBase.add(0x1000);
数组类型参数
使用 Memory.alloc 分配内存并写入数据:
// 分配内存
var arrayBuffer = Memory.alloc(10); // 分配10字节
// 写入数据
Memory.writeByteArray(arrayBuffer, [0x01, 0x02, 0x03, 0x04, 0x05]);
// 或者写入Int32数组
Memory.writeInt32Array(arrayBuffer, [123, 456, 789]);
// 传递数组指针
var result = someFunction(arrayBuffer);
结构体参数
创建结构体并填充数据:
// 分配结构体内存
var structSize = 16; // 结构体大小
var structPtr = Memory.alloc(structSize);
// 填充结构体字段
Memory.writeInt32(structPtr, 123); // 偏移0处写入int
Memory.writePointer(structPtr.add(4), ptr(0x1234)); // 偏移4处写入指针
Memory.writeUtf8String(structPtr.add(8), "test"); // 偏移8处写入字符串
// 传递结构体指针
var result = someFunction(structPtr);
frida参数类型——NativePointer
大部分参数,如var so_addr = Process.getModuleByName("libnative-lib.so");或者是onMatch: function (address, size)中的address,一般都是NativePointer类型的
读取
so_addr.base.toInt32().toString(16) //转为整型,再转为十六进制
return this.toString().substring(0, 10); // 示例:返回前 10 个字符
修改
so_addr.base.add(0x620);
address.sub(0x32);
初始化
ptr('...')是new NativePointer(...)的简写,所以一般用ptr就行
var address = ptr('0x12345678');
var hook_addr = ptr(so_addr.base.add(0x10984))
插桩函数内偏移处的地址的快照
// 找so基址和ds偏移略
var ds_internalAddr = ds_addr.add(0x15442 - 0x153F0);
console.log("[*] 插桩内部地址: ", ds_internalAddr);
Interceptor.attach(ds_internalAddr, {
onEnter: function (args) {
console.log("[+] 执行到函数内部地址");
// 读取寄存器状态
console.log("寄存器状态:");
console.log(" EAX: " + this.context.eax);
console.log(" EBX: " + this.context.ebx);
console.log(" ECX: " + this.context.ecx);
console.log(" EDX: " + this.context.edx);
console.log(" ESI: " + this.context.esi);
console.log(" EDI: " + this.context.edi);
// 读取特定内存地址的值
// 假设EAX寄存器中存放了一个指针
if (this.context.eax != 0) {
var memValue = Memory.readByteArray(ptr(this.context.eax), 16);
console.log(" EAX指向的内存: " + hexdump(memValue));
// 尝试读取字符串
console.log(" EAX指向的字符串: " + Memory.readCString(ptr(this.context.eax)));
}
// 读取栈上的数据
var stackData = Memory.readByteArray(this.context.esp, 32);
console.log("栈数据:");
console.log(hexdump(stackData));
}
});
插桩函数进入和返回的参数
和上面的frida主动调用不同,这个是等待程序主动调用
var so_addr = Process.findModuleByName("libwolf.so");
var ds_addr = so_addr.base.add(0x153F0);
Interceptor.attach(ds_addr, {
onEnter: function(args) {
this.arg0=arg[0]; // 保存参数,便于在onLeave中使用
args[1] = ptr("0x12345678");// 修改参数
console.log("[+] ds函数被调用,参数: ", args[0], args[1], args[2]);
},
onLeave: function(retval) {
console.log("[+] ds函数原始返回值: ", retval);
if (this.args0.toInt32() > 10) {
console.log("[+] 修改返回值为 1234");
retval.replace(1234); // 替换返回值
}
}
});
Interceptor.attach(Module.findExportByName(null, "dlopen"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
}
}
},
onLeave: function (retval) {
if (this.is_can_hook) {
console.log("hook start...");
hook_func()
}
}
}
Interceptor的底层实现原理
Interceptor 是 inline-hook 的封装
GumInterceptor * interceptor;
GumInvocationListener * listener;
// 初始化 Gum 库
gum_init();
// 创建拦截器和监听器
GumInterceptor *interceptor = gum_interceptor_obtain();
GumInvocationListener *listener = g_object_new(EXAMPLE_TYPE_LISTENER, NULL);
// 开始事务并附加 Hook
gum_interceptor_begin_transaction(interceptor);
gum_interceptor_attach_listener(interceptor,
GSIZE_TO_POINTER(gum_module_find_export_by_name(NULL, "open")),
listener,
GSIZE_TO_POINTER(EXAMPLE_HOOK_OPEN));
gum_interceptor_end_transaction(interceptor);
// 触发 Hook 并测试
close(open("/etc/hosts", O_RDONLY));
// 清理资源
gum_interceptor_detach_listener(interceptor, listener);
g_object_unref(listener);
g_object_unref(interceptor);
插桩函数并替换函数
上面的attach插桩只能实现进入和返回参数的修改和打印,但是并不能实现插桩函数逻辑的改变。而replace插桩就能够实现函数逻辑的替换。
注意!只能监听函数地址!不能监听内存地址!监听内存地址在后面会写到。
// 假设我们要hook一个名为"add"的函数
var addPtr = Module.findExportByName("libnative-lib.so", 'add');
var add = new NativeFunction(addPtr, 'int', ['int', 'int']);
Interceptor.replace(addPtr, new NativeCallback(function (a, b) {
console.log('原始add函数被调用,参数: ' + a + ', ' + b);
console.log('将执行减法操作而不是加法');
// 可选项:是否调用原始add函数
var callOriginal = true; // 设置为true以调用原始函数
if (callOriginal) {
var originalResult = add(a, b);
console.log('原始add函数返回值: ' + originalResult);
}
// 不调用原始的add函数,而是直接实现减法逻辑
var result = a - b; // 减法操作
console.log('返回减法结果: ' + result);
return result;
}, 'int', ['int', 'int']));
批量hook
[0x1c544, 0x1b8d4, 0x26e5c]是一个偏移数组,表示libmsaoaidsec.so模块中某些函数的偏移地址。libmsaoaidsec.base.add(offset)计算出这些函数的实际地址。Interceptor.replace用于替换这些函数的实现:newNativeCallback创建一个新的原生回调函数,这里定义了一个空的函数,仅打印一条日志。"void"表示替换函数的返回类型。[]表示替换函数的参数列表为空。
[0x1c544, 0x1b8d4, 0x26e5c].forEach(offset => {
Interceptor.replace(libmsaoaidsec.base.add(offset),
newNativeCallback(() => console.log(`Interceptor.replace: 0x${offset.toString(16)}`), "void", [])
);
});
trace指定so的JNI调用
GitHub - chame1eon/jnitrace: A Frida based tool that traces usage of the JNI API in Android apps.
hook指定库
jnitrace -l libwolf.so com.wolf.ndktest
hook所有库,注意要加""不然*会翻译成路径
jnitrace -l "*" com.wolf.ndktest
命令行参数,加载-l前面:
-m attach或者spawn :(默认spawn),attach对应Name,spawn对应包名
-i <函数名> :指定特定JNI函数 (例如指定 RegisterNatives 函数)
-e <函数名> :排除特定JNI函数 (例如排除 GetEnv 函数)
-I <函数名> :仅追踪指定导出函数 (如 stringFromJNI 函数)
-E <函数名> :排除指定导出函数 (如 JNI_OnLoad)
-o output.json :(保存结果到 JSON 文件)
-p <脚本路径> :在 JNItrace 加载前执行(用于绕过反调试)。
-a <脚本路径> :在 JNItrace 加载后执行。
-b fuzzy|accurate|none :显示堆栈回溯,fuzzy 为模糊回溯,accurate 为精确回溯(默认),none 禁用回溯
插桩内存地址
MemoryAccessMonitor
但是注意,该API只能实现一次性的监控,不能持久化
本质上是将目标内存页设置为不可读写,这样在发生读写行为时会触发事先注册好的中断处理函数,其中会调用到用户使用 gum_memory_access_monitor_new 注册的回调方法中
如何实现持久性监控呢?网上有相关项目:
ARM架构下的
GitHub - asmjmp0/fridaMemoryAccessTrace: android memory access trace utility powered by frida framework
python "C:\Users\lenovo\Desktop\Code\frida_study\fridaMemoryAccessTrace-master\fridaMemoryAccessTrace-master\main.py" -h
usage: main.py [-h] -l LENGTH [-n NAME] [-lp] (-b BREAK | -o OFFSET | -s SYMBOL)
options:
-h, --help show this help message and exit
-l LENGTH, --length LENGTH
breakpoint length can not over pagesize
-n NAME, --name NAME process name
-lp, --listproc list process
-b BREAK, --break BREAK
absolute address, eg. 0x12345678
-o OFFSET, --offset OFFSET
relative address, eg. libxxx.so@0x1234
-s SYMBOL, --symbol SYMBOL
get address by symbol eg. libxxx.so@test_value
const path="cd ~/Desktop/Code/frida_study/fridaMemoryAccessTrace-master/fridaMemoryAccessTrace-master";
console.log(path)
let str = "python ./main.py "
for (let i = 0; i < search_addr.length; i++) {
str += "-b " + search_addr[i].toString() + " ";
}
str += " -l 4 -n Crackme01"
console.log(str)
X86架构下的
GitHub - revercc/fbkpt: frida 设置软件断点和内存读写断点
插桩栈回溯
Interceptor.attach(f, {
onEnter: function (args) {
console.log('RegisterNatives called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n');
}
});
内存修改
读取内存
读取的是指针类型的值
var address = ptr('0x12345678'); // 0x12345678 指向 'HelloWorld'
var pointer = Memory.readPointer(address); // 读取出 0x12345678
读取指针指向的字符串
如果不确定字符串的编码方式,优先使用 readUtf8String,它可以兼容大多数情况。
readUtf8String:适用于需要读取包含多语言字符(如中文、日文等)的字符串。readCString:适用于读取纯 ASCII 字符串,或者在目标程序明确指定使用 ASCII 编码的情况下使用。
有两种写法,readCString同理
var pointer_str = Memory.readUtf8String(pointer); // 读取出'HelloWorld'
var pointer_str = ptr(pointer).readUtf8String();
有时读取不出来,就需要使用类型转换:
// 方法一
var jString = Java.cast(args[2], Java.use('java.lang.String'));
console.log("参数:", jString.toString());
// 方法二
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);
读取的字符串判断是否包含某子串
if (arg0.indexOf(":69A2") >= 0) {...}
读取指针指向的字节数组
var read_bytes = Memory.readByteArray(address, 32);
console.log("read_bytes", hexdump(read_bytes));
读取so文件
var so_data = Memory.readByteArray(so_addr.base, so_addr.size);
修改内存
在patch code的字节码时,程序运行的字节码很可能与jeb导出的不一样,需要自己dump so文件出来,再拖进IDA找需要搜索或者patch的字节码
主要的内存修改方法包括:
- Memory.writeByteArray() : 写入字节数组
- Memory.writePointer() : 写入指针
- Memory.writeUtf8String() : 写入字符串
// 1. 写入多个字节
Memory.writeByteArray(address, [0x90, 0x90, 0x90]);
// 2. 写入指针
var address = ptr('0x12345678');// 指定要写入的内存地址
var pointer = ptr('0x87654321');// 指定要写入的指针值
Memory.writePointer(address, pointer);// 将指针写入到指定地址
// 3. 写入字符串
Memory.writeUtf8String(address, "Hello");
假如address有偏移的话,则:
Memory.writeByteArray(address.add(3), [0x2B]);
Memory.writeByteArray(address.sub(3), [0x2B]);
ARM64 patch
示例:把启动线程检查的地方nop掉
var lib_handler = Process.findModuleByName("libnative-lib.so")
//0x10984 为 pthread create 那一行
var dst_addr = ptr(lib_handler.base.add(0x10984))
Memory.patchCode(dst_addr, 4, function (code) { // 修改4字节的代码
// 创建ARM64汇编写入器
var cw = new Arm64Writer(code, { pc: dst_addr });
cw.putNop() // 写入NOP指令(空操作)
cw.flush(); // 调用`flush`方法将更改应用到内存中
});
针对不同 CPU 架构的 Writer
Arm64Writer(针对 ARM64)ArmWriter(针对 ARM 32 )X86Writer(针对 x86/x64)MipsWriter(针对部分路由器、嵌入式设备)ThumbWriter(针对Thumb/Thumb-2 指令集(16/32 位混合))
写入汇编代码:
Decrypted iOS App Store & ARM Converter
上面的网站可以将汇编转为字节码,然后再通过patch代码注入字节码
Memory.patchCode(dst_addr, 4, function (code) { // 修改4字节的代码
var writer = new Arm64Writer(code, { pc: dst_addr });
writer.putBytes(hexToBytes("20008052")) // 写入ARM指令
writer.flush(); // 调用`flush`方法将更改应用到内存中
});
在patch之前还需要修改读写权限,不过太麻烦了,建议先直接patch,有报错再看是不是权限的原因
try {
// 获取内存页信息
var pageInfo = Process.findRangeByAddress(address);
console.log("内存页权限:", pageInfo.protection);
// 如果内存不可写,修改权限
if (!pageInfo.protection.includes('w')) {
Memory.protect(pageInfo.base, pageInfo.size, 'rwx');
console.log("已修改内存页权限为rwx");
}
Memory.writeByte(address, 0x2B);
Memory.writeByte(address.add(3), 0x2B);
// 如果是代码段,可能需要恢复原始权限
if (pageInfo.protection !== 'rwx') {
Memory.protect(pageInfo.base, pageInfo.size, pageInfo.protection);
console.log("已恢复原始内存页权限");
}
// 读取修改后的值
var newBytes = Memory.readByteArray(address, 16);
console.log("修改后的内存:", hexdump(newBytes));
} catch(e) {
console.log("修改内存失败:", e);
}
dump内存
console.log("===============================");
var so_addr = Process.getModuleByName("libnative-lib.so");
// dump so文件
console.log("[*] 开始dump so文件...");
console.log("[*] so基址: " + so_addr.base);
console.log("[*] so大小: " + so_addr.size);
// 读取整个so文件的内存
var so_data = Memory.readByteArray(so_addr.base, so_addr.size);
// 将数据写入Android设备的可写目录
var file = new File("/data/local/tmp/dump_lib.so", "wb");
file.write(so_data);
file.flush();
file.close();
console.log("[+] SO文件已保存到: /data/local/tmp/dump_lib.so");
console.log("请输入命令以传输文件: adb pull /data/local/tmp/dump_lib.so c:\\Users\\lenovo\\Desktop\\dump_lib.so")
内存扫描搜索
通过Memory.scan实现
在搜索匹配时,程序运行的字节码很可能与jeb导出的不一样,需要自己dump so文件出来,再拖进IDA找需要搜索或者patch的字节码
// so_addr.base: so文件在内存中的起始地址
// so_addr.size: so文件在内存中占用的大小(字节数)
// address: Memory.scan回调中匹配到的内存地址
// size: 匹配到的内存块大小
Java.perform(function () {
console.log("===============================");
var so_addr = Process.getModuleByName("libnative-lib.so");
var pattern = "03 45 ?? 03 45"; // 这里的??表示匹配任意一个字节
Memory.scan(so_addr.base, so_addr.size, pattern, {// 如果要改搜索大小,就修改把so_addr.size的替换自定义的变量
// 匹配到的内存块大小
onMatch: function (address, size) {//这两个参数名固定
console.log("在地址 " + address + " 找到匹配");
var bytes = Memory.readByteArray(address, 16);
console.log(hexdump(bytes));
return true;// 返回true继续查找,返回false停止查找
},
onComplete: function () {
console.log("内存扫描完成");
}
});
});
遇到的情况如下,因为程序是运行在x86-64的app模拟器中,中间的x86可以忽略。
但是可以看出,jeb导出的so和frida导出的运行时的so的字节码是不一样的,只有函数首地址相同,所以以后在计算函数内部某一行代码偏移时,建议还是用dump出来的偏移地址来调用。
jeb导出的x86-64:

jeb导出的x86:

frida dump的x86-64:

硬件断点
在Frida 16.5.0 Released的发布中,frida添加了硬件断点的实现,其说明如下:
Some of you may have found yourself in a situation where a piece of data in memory looks interesting, and you want to locate the code responsible for it.
You may have tried Frida’s MemoryAccessMonitor API, but found the page granularity hard to work with.
To address this, hsorbo and I implementing support for hardware breakpoints and watchpoints. The long story short is that thread objects returned by Process.enumerateThreads() now have setHardwareBreakpoint(), setHardwareWatchpoint(), and corresponding methods to unset them at a later point. These are then combined with Process.setExceptionHandler() where you call the unset method and return true to signal that the exception was handled, and execution should be resumed.
翻译:
你们中的一些人可能遇到过这样的情况:内存中的一段数据看起来很有趣,而你想要找到生成它的代码。你可能尝试过使用Frida的MemoryAccessMonitor API,但发现其页面粒度难以操作。
为了解决这个问题,hsorbo和我正在实现对硬件断点和监视点的支持。简单来说,现在由Process.enumerateThreads()返回的线程对象有了setHardwareBreakpoint()、setHardwareWatchpoint()以及相应的取消设置方法,这些方法可以在稍后使用。然后,这些功能与Process.setExceptionHandler()结合使用,在其中调用取消方法并返回true,以表明异常已被处理,程序应该继续执行。
相关示例也在发布的链接上有实现。
其过程如下:
- 使用
Process.enumerateMallocRanges()扫描堆内存中的“50”,打出子弹后,匹配扫描结果中的“49”,确定子弹存储地址 - 对子弹存储地址下硬件断点,当有汇编对子弹存储的地址rwx时则会抛出异常,打印该处汇编的地址
- 找到修改子弹的汇编地址之后就方便了,在native层的汇编插桩读取rax
- 或者进一步实现patch功能
设置设置硬件断点:
thread.setHardwareWatchpoint(index, address, size, conditions);
// `index`:硬件寄存器的索引(如0-3,受架构限制)
// `address`:内存地址(需用`ptr()`转换,如`ptr('0x1fbf5191884')`)
// `size`:监控的字节长度(如1、2、4、8字节)
// `conditions`:触发条件,如`'w'`表示写操作,`'r'`读操作,`'x'`执行操作
示例:
function installWatchpoint(address, size, conditions) {
const thread = Process.enumerateThreads()[0];// 选择第一个线程
// 如果断点命中则会抛出异常,从而禁用硬件断点
Process.setExceptionHandler(e => {
console.log(`=== Handler got ${e.type} at ${e.context.pc}`);
// 检查当前线程是否为目标线程 且 异常类型是断点和单步执行
if (Process.getCurrentThreadId() === thread.id && ['breakpoint', 'single-step'].includes(e.type)) {
thread.unsetHardwareWatchpoint(0); // 卸载索引0的监视点
console.log('Watchpoint disabled');
return true;
}
return false;
});
thread.setHardwareWatchpoint(0, address, size, conditions);// 硬件寄存器[0]
console.log('Watchpoint ready');
}
// 调用示例:监控地址0x1fbf5191884的4字节写操作
installWatchpoint(ptr('0x1fbf5191884'), 4, 'w');
学习链接:GitHub - hackcatml/frida-watchpoint-tutorial: Frida's setHardwareWatchpoint tutorial
RPC
Frida-RPC 会自动处理底层的 JNI 环境和参数传递,灵活性稍低,但更适合快速开发和自动化测试,特别是在需要从外部调用函数时。
适合需要从外部(如 Python 脚本)调用函数的场景,特别适合动态调试和自动化任务,例如说爬虫等需要重复使用js脚本中的函数。
而之前提到的[[#主动调用偏移处的函数]]需要手动获取 JNIEnv* 和 jobject,并使用 NativeFunction 来调用目标函数。这种方式需要对目标函数的内存地址和签名有明确的了解,使用起来复杂些。
示例:
my_rfc.js
function js_add(a, b) {
let result = 0;
Java.perform(function () {
// 调用Java层的Calculator类中的add方法
result = Java.use("com.example.Calculator").add(a, b);
})
return result;
}
function js_multiply(x) {
let result = 0;
Java.perform(function () {
// 调用Java层的Calculator类中的multiply方法
result = Java.use("com.example.Calculator").multiply(x, 2);
})
return result;
}
rpc.exports = { // js导出表,提供给python脚本使用
py_add: js_add,
py_multiply: js_multiply
};
python
import frida
# 读取JS文件
with open('c:\\Users\\lenovo\\Desktop\\Code\\py\\my_rfc.js', 'r', encoding='utf-8') as f:
jsCode = f.read()
# 准备工作
process = frida.get_usb_device().attach('com.example.calculator')
script = process.create_script(jsCode)
script.load()
# 测试RPC调用
a = 10
b = 20
# 调用js中导出的加法方法
add_result = script.exports.py_add(a, b)
print("加法结果:", add_result)
# 调用js中导出的乘法方法
multiply_result = script.exports.py_multiply(a, b)
print("乘法结果:", multiply_result)
[!tips]
var的重复声明:允许在同一个作用域内多次声明同一个变量,后面的声明会覆盖前面的声明
尽量避免重复声明变量,使用let或const来声明变量,以避免潜在问题
frida-attach
emm暂时用不到,其实和上面差不多感觉。
frida-trace
Frida-Trace:应用程序函数与方法跟踪指南-CSDN博客
frida-trace -U -f YOUR_APP_NAME_OR_PACKAGE_NAME -m
//跟踪应用程序中调用的所有方法并将其输出"*"
frida-trace -U -f YOUR_APP_NAME_OR_PACKAGE_NAME -i "FUNCTION_NAME"
//追踪名为“FUNCTION_NAME”的函数"*"
frida-trace -U -f YOUR_APP_NAME_OR_PACKAGE_NAME -I "CLASS_NAME.*"
//“CLASS_NAME”的类追踪所有方法"*"
frida-trace -U -f YOUR_APP_NAME_OR_PACKAGE_NAME -I "CLASS_NAME.METHOD_NAME"
//仅追踪名为“METHOD_NAME”的特定方法,其中“CLASS_NAME”是该方法所属的类。
frida-trace -U -f YOUR_APP_NAME_OR_PACKAGE_NAME -L "LIBRARY_NAME.*"
//仅追踪名为“LIBRARY_NAME”的库中的所有方法
frida-trace -U -f YOUR_APP_NAME_OR_PACKAGE_NAME -i "FUNCTION_OR_METHOD_NAME" -a "PARAMETER_NAME"
//此命令将仅追踪带有名为“PARAMETER_NAME”的特定参数的函数或方法
$ frida-trace -U -i "*Activity.onCreate" -m '-*myapp*' -f my.package.name
//.跟踪应用程序打开某个 Activity 时创建的类
$ frida-trace -U -i "*:*" -m '-*myapp*' -f my.package.name
//输出应用程序启动时调用的所有函数
$ frida-trace -U -i "*Http*:*" -f my.package.name
//跟踪所有使用 HTTP 进行通信的函数:
$ frida-trace -U -i "*" -m '-*myapp*' -f my.package.name
//输出应用程序启动后调用的所有类和函数
$ frida-trace -U -m "-*com.my.package*" -i "Java.use('com.my.package.SomeClass').*"
//跟踪特定类中所有的函数
$ frida-trace -U -m '-*myapp*' -i "Java.use('my.package.name').myFunction"
//跟踪某一特定函数的调用
$ frida-trace -U -i "*Activity.onCreate" -f my.package.name | grep "\-> Java Class: " | cut -d' ' -f4 | sort -u
//输出应用程序启动时调用的所有类
$ frida-trace -U -i "Java.use('my.package.name.SomeClass').$init"
//追踪某个类的构造函数
$ frida-trace -U -i "Java.use('my.package.name.SomeClass').myFunction" -I "return"
//追踪某个函数并输出其返回值
$ frida-trace -U -i "*" -m "-android.*" -f my.package.name | grep "\-> J " | cut -d' ' -f4 | sort -u
//跟踪应用程序使用的所有类,不包含系统类
$ frida-trace -i "Java.use('my.package.name.SomeClass').*"
//追踪类的所有方法,包括从父类继承的方法
$ frida-trace -i "Java.use('my.package.name.SomeClass').myFunction" -I "args, ret"
//追踪某个函数并打印出其调用时的参数和返回值
$ frida-trace -m "-android.*" -i "exports:*" -f my.package.name
//跟踪应用程序使用的所有 JNI 函数
$ frida-trace -m "-android.*" -i "Java.*" -f my.package.name
//追踪应用程序使用的所有 Java API
$ frida-trace -i "Java.use('my.package.name.SomeClass').myFunction" -m "+-*" -I "syscall"
//追踪某个方法中的所有系统调用
$ frida-trace -m "-android.*" -i "Java.use('java.net.*')" -f my.package.name
//跟踪应用程序使用的所有网络类
$ frida-trace -m "-android.*" -i "Java.use('javax.*').*" -f my.package.name
//跟踪应用程序使用的所有加密类
$ frida-trace -m "-android.*" -i "Java.use('android.*Service').*"
//跟踪应用程序使用的所有 Android 系统服务
$ frida-trace -U -i "Java.use('my.package.name.SomeClass').myFunction" -I "stack"
//追踪某个函数并输出其调用堆栈
$ frida-trace -m "-android.*" -i "exports:*" -f my.package.name
//追踪应用程序使用的所有 native 函数
$ frida-trace -m "-android.*" -i "Java.use('android.content.BroadcastReceiver').onReceive"
//跟踪应用程序中所有广播的接收
$ frida-trace -U -i "Java.use('my.package.name.SomeClass').myFunction" -I "duration"
//追踪某个函数的执行时间
$ frida-trace -U -i "*" -m "-android.*" -f my.package.name > trace.log
//追踪应用程序使用的所有类和函数,并将结果输出到文件中
$ frida-trace -U -i "Java.use('okhttp3.mockwebserver.MockWebServer').*(..)" -I "args, ret" -I "mockWebServer.getHostName, mockWebServer.getPort, request.method, request.url, response.code, response.body.string, response.headers.size()"
//跟踪应用程序的网络请求,输出请求的 URL 和响应
$ frida-trace -U -m "-android.*" -i "exports:*" -f my.package.name > jni-trace.log
//追踪应用程序中的 JNI 函数并将其输出到文件中
$ frida-trace -m "-android.*" -i "Java.vm.*" -f my.package.name
//跟踪应用程序中 JNI 函数的调用:
$ frida-trace -U -i "Java.use('javax.crypto.Cipher').getInstance" -I "args"
//追踪应用程序中使用的所有常见加密算法:
$ frida-trace -U -i "*Activity.onCreate" -f my.package.name
//跟踪应用程序启动时创建的类
$ frida-trace -U -i "*:*" -f my.package.name
//输出应用程序启动时调用的所有函数
$ frida-trace -U -i "Java.use('java.net.Socket').connect"
//所有的网络通信

浙公网安备 33010602011771号