抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

介绍

frida 是平台原生 appGreasemonkey,说的专业一点,就是一种动态插桩工具,可以插入一些代码到原生 app 的内存空间去,(动态地监视和修改其行为),这些原生平台可以是 WinMacLinuxAndroid 或者 iOS。而且 frida 还是开源的。

Greasemonkey 可能大家不明白,它其实就是 firefox 的一套插件体系,使用它编写的脚本可以直接改变 firefox 对网页的编排方式,实现想要的任何功能。而且这套插件还是外挂的,非常灵活机动。

使用 frida 可以“看到”平时看不到的东西。出于编译型语言的特性,机器码在 CPU 和内存上执行的过程中,其内部数据的交互和跳转,对用户来讲是看不见的。当然如果手上有源码,甚至哪怕有带调试符号的可执行文件包,也可以使用 gdblldb 等调试器连上去看。

frida 注入的主要思路就是找到目标进程, 使用 ptrace 跟踪目标进程获取 mmap,dlpoen,dlsym 等函数库的便宜获取 mmap 在目标进程申请一段内存空间将在目标进程中找到存放 [frida-agent-32/64.so] 的空间启动执行各种操作由 agent 去实现。

inlinehook 思路

  1. 动态替换需要 Hook 的指令片段为一段经过设计的跳板指令,即 trampoline ,目标为我们设计好的一段 shellCode
  2. 在内存中设计并生成一段 shellCode ,这是我们的可控 shellCode ,在该 shellCode 中需要实现 Hook 的功能函数(即打印/替换-参数/结果)
  3. shellCode 的设计原则是保持 Hook 前后的栈平衡,并保护寄存器状态(即 Hook 结束后,保持与 Hook 开始前一致的栈布局与寄存器状态)
  4. 在 shellCode 中完成原函数的执行工作,被替换的掉的指令中若包含计算 PC-relative address ( 如 Branch 指令 ),需要对其正确解析执行
组件名称 功能描述
frida-gum 提供了 inline-hook 的核心实现,还包含了代码跟踪模块 Stalker,用于内存访问监控的 MemoryAccessMonitor,以及符号查找、栈回溯实现、内存扫描、动态代码生成和重定位等功能
frida-core fridahook 的核心,具有进程注入、进程间通信、会话管理、脚本生命周期管理等功能,屏蔽部分底层的实现细节并给最终用户提供开箱即用的操作接口。包含了 frida-server、frida-gadget、frida-agent、frida-helper、frida-inject 等关键模块和组件,以及之间的互相通信底座
frida-gadget 本身是一个动态库,可以通过重打包修改动态库的依赖或者修改 smali 代码去实现向三方应用注入 gadget,从而实现 Frida 的持久化或免 root
frida-server 本质上是一个二进制文件,类似于前面学习到的 android_server,需要在目标设备上运行并转发端口,在 Frida hook 中起到关键作用

基础

操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Usage: frida [options] target

Options:
--version show program's version number and exit
-h, --help show this help message and exit
-D ID, --device=ID connect to device with the given ID
-U, --usb connect to USB device
-R, --remote connect to remote frida-server
-H HOST, --host=HOST connect to remote frida-server on HOST
-f FILE, --file=FILE spawn FILE
-F, --attach-frontmost
attach to frontmost application
-n NAME, --attach-name=NAME
attach to NAME
-p PID, --attach-pid=PID
attach to PID
--stdio=inherit|pipe stdio behavior when spawning (defaults to “inherit”)
--aux=option set aux option when spawning, such as “uid=(int)42”
(supported types are: string, bool, int)
--realm=native|emulated
realm to attach in
--runtime=qjs|v8 script runtime to use
--debug enable the Node.js compatible script debugger
--squelch-crash if enabled, will not dump crash report to console
-O FILE, --options-file=FILE
text file containing additional command line options
-l SCRIPT, --load=SCRIPT
load SCRIPT
-P PARAMETERS_JSON, --parameters=PARAMETERS_JSON
parameters as JSON, same as Gadget
-C CMODULE, --cmodule=CMODULE
load CMODULE
--toolchain=any|internal|external
CModule toolchain to use when compiling from source
code
-c CODESHARE_URI, --codeshare=CODESHARE_URI
load CODESHARE_URI
-e CODE, --eval=CODE evaluate CODE
-q quiet mode (no prompt) and quit after -l and -e
--no-pause automatically start main thread after startup
-o LOGFILE, --output=LOGFILE
output to log file
--eternalize eternalize the script before exit
--exit-on-error exit with code 1 after encountering any exception in
the SCRIPT


frida-ps -U 查看当前手机运行的进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
frida-ps --help
使用方式: frida-ps [选项]

选项:
-h, --help 显示帮助信息并退出
-D ID, --device ID 连接到具有给定ID的设备
-U, --usb 连接到USB设备
-R, --remote 连接到远程frida-server
-H HOST, --host HOST 连接到HOST上的远程frida-server
--certificate CERTIFICATE
与HOST进行TLS通信,期望的CERTIFICATE
--origin ORIGIN 连接到设置了"Origin"头为ORIGIN的远程服务器
--token TOKEN 使用TOKEN验证HOST
--keepalive-interval INTERVAL
设置心跳包间隔(秒),或设置为0以禁用(默认为-1,根据传输方式自动选择)
--p2p 与目标建立点对点连接
--stun-server ADDRESS
设置与--p2p一起使用的STUN服务器地址
--relay address,username,password,turn-{udp,tcp,tls}
添加与--p2p一起使用的中继
-O FILE, --options-file FILE
包含额外命令行选项的文本文件
--version 显示程序版本号并退出
-a, --applications 只列出应用程序
-i, --installed 包括所有已安装的应用程序
-j, --json 以JSON格式输出结果

操作模式

操作模式 描述 优点 主要用途
CLI(命令行)模式 通过命令行直接将 JavaScript 脚本注入进程中,对进程进行操作 便于直接注入和操作 在较小规模的操作或者需求比较简单的场景中使用
RPC 模式 使用 Python 进行 JavaScript 脚本的注入工作,实际对进程进行操作的还是 JavaScript 脚本,可以通过 RPC 传输给 Python 脚本来进行复杂数据的处理 在对复杂数据的处理上可以通过 RPC 传输给 Python 脚本来进行,有利于减少被注入进程的性能损耗 在大规模调用中更加普遍,特别是对于复杂数据处理的需求

注入模式与启动命令:

注入模式 描述 命令或参数 优点 主要用途
Spawn 模式 将启动 App 的权利交由 Frida 来控制,即使目标 App 已经启动,在使用 Frida 注入程序时还是会重新启动 App 在 CLI 模式中,Frida 通过加上 -f 参数指定包名以 spawn 模式操作 App 适合于需要在 App 启动时即进行注入的场景,可以在 App 启动时即捕获其行为 当需要监控 App 从启动开始的所有行为时使用
Attach 模式 在目标 App 已经启动的情况下,Frida 通过 ptrace 注入程序从而执行 Hook 的操作 在 CLI 模式中,如果不添加 -f 参数,则默认会通过 attach 模式注入 App 适合于已经运行的 App,不会重新启动 App,对用户体验影响较小 在 App 已经启动,或者我们只关心特定时刻或特定功能的行为时使用
1
frida -U -f 进程名 -l hook.js

attach 模式 :

1
frida -U 进程名 -l hook.js

frida_server 自定义端口

1
2
3
4
5
frida server 默认端口:27042

taimen:/ $ su
taimen:/ # cd data/local/tmp/
taimen:/data/local/tmp # ./fs1280 -l 0.0.0.0:6666

logcat |grep "D.zj2595" 日志捕获
adb connect 127.0.0.1:62001 模拟器端口转发

语法

API 名称 描述
Java.use(className) 获取指定的 Java 类并使其在 JavaScript 代码中可用。
Java.perform(callback) 确保回调函数在 Java 的主线程上执行。
Java.choose(className, callbacks) 枚举指定类的所有实例。
Java.cast(obj, cls) 将一个 Java 对象转换成另一个 Java 类的实例。
Java.enumerateLoadedClasses(callbacks) 枚举进程中已经加载的所有 Java 类。
Java.enumerateClassLoaders(callbacks) 枚举进程中存在的所有 Java 类加载器。
Java.enumerateMethods(targetClassMethod) 枚举指定类的所有方法。

日志输出语法区别

日志方法 描述 区别
console.log() 使用 JavaScript 直接进行日志打印 多用于在 CLI 模式中,console.log() 直接输出到命令行界面,使用户可以实时查看。在 RPC 模式中,console.log() 同样输出在命令行,但可能被 Python 脚本的输出内容掩盖。
send() Frida 的专有方法,用于发送数据或日志到外部 Python 脚本 多用于 RPC 模式中,它允许 JavaScript 脚本发送数据到 Python 脚本,Python 脚本可以进一步处理或记录这些数据。

Hook 框架模板

1
2
3
4
5
6
function main(){
Java.perform(function(){
hookTest1();
});
}
setImmediate(main);//

三板斧:

  1. 先 hook、看参数和返回值:定位:命令行
  2. 再构造参数、主动调用:利用:命令行
  3. 最后配 RPC 导出结果:规模化利用:PYTHON

js 脚本

JavaScript API | Frida • A world-class dynamic instrumentation toolkit

Hook 普通方法、打印参数和修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义一个名为 hookTest1 的函数
function hookTest1(){
//获取一个名为 "类名" 的 Java 类,并将其实例赋值给 JavaScript 变量 utils
var utils = Java.use("类名");
//修改 "类名" 的 "method" 方法的实现。这个新的实现会接收两个参数(a 和 b)
utils.method.implementation = function(a, b){
//将参数 a 和 b 的值改为 123 和 456。
a = 123;
b = 456;
//调用修改过的 "method" 方法,并将返回值存储在 `retval` 变量中
var retval = this.method(a, b);
//在控制台上打印参数 a,b 的值以及 "method" 方法的返回值
console.log(a, b, retval);
//返回 "method" 方法的返回值
return retval;
}
}

打印堆栈的脚本:

1
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

Hook 重载参数

1
2
3
4
5
6
7
8
9
10
11
12
// .overload()
// .overload('自定义参数')
// .overload('int')
function hookTest2(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//overload 定义重载函数,根据函数的参数类型填
utils.func.overload('com.zj.wuaipojie.Demo$Animal','java.lang.String').implementation = function(a,b){
b = "aaaaaaaaaa";
this.func(a,b);
console.log(b);
}
}

Hook 构造函数

1
2
3
4
5
6
7
8
9
function hookTest3(){
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的构造函数的实现,$init 表示构造函数
utils.$init.overload('java.lang.String').implementation = function(str){
console.log(str);
str = "52";
this.$init(str);
}
}

Hook 字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hookTest5(){
Java.perform(function(){
//静态字段的修改
var utils = Java.use("com.zj.wuaipojie.Demo");
//修改类的静态字段 "flag" 的值
utils.staticField.value = "我是被修改的静态变量";
console.log(utils.staticField.value);
//非静态字段的修改
//使用 `Java.choose()` 枚举类的所有实例
Java.choose("com.zj.wuaipojie.Demo", {
onMatch: function(obj){
//修改实例的非静态字段 "_privateInt "的值为" 123456 ",并修改非静态字段" privateInt " 的值为 9999。
obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线
obj.privateInt.value = 9999;
},
onComplete: function(){

}
});
});

}

Hook 内部类

1
2
3
4
5
6
7
8
9
10
11
function hookTest6(){
Java.perform(function(){
//内部类
var innerClass = Java.use("com.zj.wuaipojie.Demo$innerClass");
console.log(innerClass);
innerClass.$init.implementation = function(){
console.log("eeeeeeee");
}

});
}

枚举所有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hookTest8(){
Java.perform(function(){
var Demo = Java.use("com.zj.wuaipojie.Demo");
//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);
}
}
}
})
}

主动调用

静态方法

1
2
var ClassName=Java.use("com.zj.wuaipojie.Demo");
ClassName.privateFunc();

非静态方法

1
2
3
4
5
6
7
8
9
10
11
12
var ret = null;
Java.perform(function () {
Java.choose("com.zj.wuaipojie.Demo",{ //要 hook 的类
onMatch:function(instance){ //onmatch 循环打印实例 instance
ret=instance.privateFunc("aaaaaaa"); //要 hook 的方法
},
onComplete:function(){ //搜索结果
console.log("result: " + ret);
}
});
})
//return ret;

python 脚本

包名加载

1
2
3
4
5
6
7
import frida, sys
jsCode = """ ...... """
script.exports.rpcfunc()
process = frida.get_usb_device().attach('包名') # 获取 USB 设备并附加到应用
script = process.create_script(jsCode) # 创建并加载脚本
script.load()# 执行脚本
sys.stdin.read()# 保持脚本运行状态,防止它执行完毕后立即退出

PID 加载

先通过 ps -A|grep XXX,获取 PID

1
2
3
4
process = frida.get_usb_device().attach(pid)
script=process.create_script(jscode);
script.load();
sys.stdin.read();

spawn 方式附加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: UTF-8 -*-
import frida,sys
jscode=""" """
#get_remote_device
device = frida.get_usb_device()
print("device:",device);
pid = device.spawn(["com."])#以挂起的方式创建进程
print("pid:",pid);
process = device.attach(pid);
print("process:",process);
script = process.create_script(jscode)
script.load()
device.resume(pid)#加载完脚本,恢复进程运行
sys.stdin.read()

连接非标准端口

先查看手机的 ip,然后 frida-server 用指定端口的方法启动

1
2
3
4
5
process = frida.get_device_manager().add_remote_device('ip:port').attach('name');
script = process.create_script(jscode);
script.load()
print("开始hook")
sys.stdin.read()

frida 与 Python 的交互(send)

我们用 frida 去做 hook 的时候,js 代码中通过 console.log 打印出想要的值,但是这个值无法交给 python 继续使用,因此需要用到 send 函数来将值传递给 python 中,具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# -*- coding: UTF-8 -*-
import frida,sys
jscode=""" """
def messageFunc(message, data): //回调函数,对 message 进行处理
print(message)
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
else:
print(message)
process = frida.get_usb_device().attach('name')
script = process.create_script(jscode)
script.load()
script.on('message', messageFunc) //接受 js 里发来的消息,类似监听
print("开始hook")
sys.stdin.read()

frida 与 Python 的交互(recv)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# -*- coding: UTF-8 -*-
import time
import frida,sys
jscode="""
recv( //js中通过recv函数接受python发来的数据并作处理。
function(obj){
console.log(obj.data)
retval=obj.data
}
).wait();
"""
def messageFunc(message, data):
print(message)
if message["type"] == 'send':
print(u"[*] {0}".format(message['payload']))
else:
print(message)
process = frida.get_usb_device().attach('name')
script = process.create_script(jscode)
script.load()
script.on('message', messageFunc)
time.sleep(10)
script.post({'data': '12345678'})
print("开始hook")
sys.stdin.read()

rpc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import time
import frida,sys
jscode="""
function test(data){
var result = "";
Java.perform(function(){
result = Java.use('m').md5(data);
console.log('result1: ', result);
});
console.log('result2: ', result);
return result;
}
rpc.exports = { //通过rpc.exports导出函数
rpcfunc: test
};
"""
device = frida.get_usb_device()
print("device:",device);
pid = device.spawn(["name"])#以挂起的方式创建进程
print("pid:",pid);
process = device.attach(pid);
print("process:",process);
script = process.create_script(jscode)
script.load()
device.resume(pid)#加载完脚本,恢复进程运行
result = script.exports_sync.rpcfunc('') //通过script.exports_sync.rpcfunc调用函数
print("开始hook")
print("result3:"+result)
sys.stdin.read()

native-hook

准备

hook 关键点就是找到函数地址

注意事项

如果需要手动计算函数地址,请注意安卓 32bit、64bit 的计算区别:

安卓位数 指令 计算方式
32 位 thumb so 基址 + 函数在 so 中的偏移 + 1
64 位 arm so 基址 + 函数在 so 中的偏移
  1. 安卓里一般 32 位的 so 中都是 thumb 指令,64 位的 so 中都是 arm 指令
  2. 通过 IDA 里的 opcode bytes 来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入 4)
  3. thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1
    arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移

Process

Process 对象代表当前被 Hook 的进程,能获取进程的信息,枚举模块,枚举范围等

API 含义
Process.id 返回附加目标进程的 PID
Process.isDebuggerAttached() 检测当前是否对目标程序已经附加
Process.enumerateModules() 枚举当前加载的模块,返回模块对象的数组
Process.enumerateThreads() 枚举当前所有的线程,返回包含 id, state, context 等属性的对象数组

Module

Module 对象代表一个加载到进程的模块(例如,在 Windows 上的 DLL,或在 Linux/Android 上的 .so 文件), 能查询模块的信息,如模块的基址、名称、导入/导出的函数等

API 含义
Module.load() 加载指定 so 文件,返回一个 Module 对象
enumerateImports() 枚举所有 Import 库函数,返回 Module 数组对象
enumerateExports() 枚举所有 Export 库函数,返回 Module 数组对象
enumerateSymbols() 枚举所有 Symbol 库函数,返回 Module 数组对象
Module.findExportByName(exportName)、Module.getExportByName(exportName) 寻找指定 so 中 export 库中的函数地址
enumerateModules() 用于列出当前进程中加载的所有模块(即共享库或动态链接库)的方法
Module.findBaseAddress(name)、Module.getBaseAddress(name) 返回 so 的基地址
module.name.indexOf("libc.so") 检查一个模块的名称是否包含特定的 so

Memory

Memory 是一个工具对象,提供直接读取和修改进程内存的功能,能够读取特定地址的值、写入数据、分配内存等

方法 功能
Memory.copy() 复制内存
Memory.scan() 搜索内存中特定模式的数据
Memory.scanSync() 同上,但返回多个匹配的数据
Memory.alloc() 在目标进程的堆上申请指定大小的内存,返回一个 NativePointer
Memory.writeByteArray() 将字节数组写入一个指定内存
Memory.readByteArray 读取内存
Memory.allocUtf8String(contents) Frida 提供的内存管理函数,用于在内存中分配一个包含指定字符串的缓冲区,并以 UTF-8 编码存储

frida 的 Interceptor:

基本概念

  1. 动态插桩Interceptor 允许用户在应用程序运行时插入代码,而不需要修改源代码或重新编译。这种动态插桩技术使得分析应用程序的行为变得更加灵活和高效。
  2. 目标函数:可以监控和修改的函数。目标函数可以是本地库(如 .so 文件)中的导出函数,也可以是 Java 方法(在 Android 应用中)等。

主要功能

  • 监控函数调用:用户可以在函数调用前后执行特定的代码,这允许开发者查看输入参数和返回值。
  • 修改参数和返回值:可以在函数被调用时修改传入的参数或函数的返回值,从而影响程序的执行流。
  • 获取堆栈信息:可以在函数调用时获取调用堆栈信息,帮助调试和分析。

思路:

1 利用函数名,通过 Module.findExportByName 函数获取函数地址。

2 通过 Module.findBaseAddress 函数获取所在库函数地址,然后加上函数偏移量即为函数地址

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const libName = 'libnative-lib.so'
const funcName = 'Java_com_example_demoso1_MainActivity_stringFromJNI'
const funcAddr = Module.findExportByName(libName, funcName)
console.log(funcAddr)
//或者
const offset = 0x10018
const baseAddr = Module.findBaseAddress(libName)
const funcAddr = baseAddr.add(offset)
console.log(funcAddr)

// 通过 Interceptor.attach 来对函数进行 hook
Interceptor.attach(funcAddr, {
onEnter: function (args) {
//onEnter 里可以打印和修改参数
},
onLeave: function (retval) {
//onLeave 里可以打印和修改返回值
},
})

地址相关 API

  1. 导出表(Export Table):列出了库中可以被其他程序或库访问的所有公开函数和符号的名称。
  2. 导入表(Import Table):列出了库需要从其他库中调用的函数和符号的名称。
  3. 符号表(Symbol Table):编程语言编译过程中的一种数据结构,主要用于存储与程序中各种标识符(如变量名、函数名、类名等)相关的信息。

枚举导出表

1
2
3
4
5
const imports = Module.enumerateImports('libart.so');//同目录下的动态库
for (const iterator of imports) {
console.log(JSON.stringify(iterator));//转成 json 格式

}

枚举导入表

1
2
3
4
5
const exports = Module.enumerateExports('libart.so');//同目录下的动态库
for (const iterator of exports) {
console.log(JSON.stringify(iterator));//转成 json 格式
// {"type": "function", "name":"_ZN3art3jit3Jit13JitAtFirstUseEv "," address ":" 0x7fff7229e1b0 "}
}

枚举符号表

1
2
3
4
5
const exports = Module.enumerateSymbols('libart.so');//同目录下的动态库
for (const iterator of exports) {
console.log(JSON.stringify(iterator));//转成 json 格式
// {"isGlobal": true, "type": "function", "section":{"id": "12.text", "protection": "r-x"}, "name": "art_l2d", "address": "0x7fff724bb4d0", "size": 6}
}

枚举进程中已加载的模块

1
2
3
4
5
const modules = Process.enumerateModules();
for (const iterator of exports) {
console.log(JSON.stringify(iterator));//转成 json 格式
// {"name": "libmtp.so", "base": "0x7fff6e6c4000", "size": 200704, "path": "/system/lib64/libmtp.so"}
}

通过函数名找导出函数

1
2
const funcAddr = Module.findExportByName('libart.so', '_ZN9unix_file6FdFile5WriteEPKcll');//getExportByName 相同
console.log(funcAddr);

Module.findBaseAddress

1
2
3
const baseAddr = Module.findBaseAddress('libencryptlib.so')//同 Module.getBaseAddress
console.log(baseAddr)
// "0x7ffff6013000"

通过模块名找基地址

1
2
3
const module = Process.findModuleByName('libpiex.so')
console.log(JSON.stringify(module))
//{"name": "libpiex.so", "base": "0x7ffff6013000", "size": 114688, "path": "/system/lib64/libpiex.so"}

Process.findModuleByAddress()

1
2
const module = Process.findModuleByAddress(addr)//addr 可以是该模块中的任何一个地址
console.log(JSON.stringify(module))

hook 函数

native 函数

new NativeFunction(address, returnType, argTypes[, options]): 创建一个新的 NativeFunction 用于调用位于指定地址的函数,其中 returnType 指定返回类型,argTypes 数组指定参数类型。

1
2
3
4
5
6
function main(){
const addAddr = Module.findExportByName('libleidian.so', 'Java_com_example_leidian_MainActivity_add');
const addFunction = new NativeFunction(addAddr, 'int64', ['int64', 'int64', 'int', 'int']);
const result = addFunction(0, 0, 1, 2);
console.log("result: ", result);
}

Interceptor.attach 函数

Interceptor.attach(target, callbacks[, data]):拦截 target 处的函数调用,callbacks 包括 onEnteronLeave 的回调函数实现。

先等待应用程序启动,再 attach。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//hook 修改参数值以及返回值
function replaceHook(addr) {

//拦截目标函数调用
Interceptor.attach(addr,
{
onEnter: function(args){//目标函数执行之前需要做的事
console.log("come into add function");
console.log("arg1: ", args[0]);
console.log("arg2: ", args[1]);
console.log("arg3: ", args[2]);
console.log("arg4: ", args[3]);
console.log('Context : ' + JSON.stringify(this.context));
//context 包含各种寄存器的值
//Context : {"pc": "0x7fff588cf950", "sp": "0x7fff6bbf9828", "rax": "0x0", "rcx": "0x2", "rdx": "0x1", "rbx": "0x0", "rsp": "0x7fff6bbf9828", "rbp": "0x7fff6bbf9830", "rsi": "0x0", "rdi": "0x0", "r8": "0x18", "r9": "0x7fff5a0f512c", "r10": "0x0", "r11": "0x7fff588cf950", "r12": "0x7fff6bbf9990", "r13": "0x1", "r14": "0x4", "r15": "0xfffffffffffffffc", "rip": "0x7fff588cf950"}
},
onLeave: function(retval){//目标函数执行之后需要做的事
console.log("return value: ", retval);
//修改返回值
retval.replace(0x9)//修改成整数
//retval.replace(ptr(0x14792))//修改成指针
}
}
)

//调用 hook 后的函数查看效果
const addFunction = new NativeFunction(addr, 'int64', ['int64', 'int64', 'int', 'int']);
var result = addFunction(0, 0, 1, 2);
console.log("result: ", result);

}
  • onEnter:在目标函数被调用时执行,可以访问传入的参数。你可以在这里添加逻辑,比如日志记录或条件检查。
  • onLeave:在目标函数返回时执行,可以访问返回值。你可以修改返回值,也可以执行其他操作。

打印整数型、布尔值类型、char 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
console.log(helloAddr);
if(helloAddr != null){
//Interceptor.attach 是 Frida 里的一个拦截器
Interceptor.attach(helloAddr,{
//onEnter 里可以打印和修改参数
onEnter: function(args){ //args 传入参数
console.log(args[0]); //打印第一个参数的值
console.log(this.context.x1); // 打印寄存器内容
console.log(args[1].toInt32()); //toInt32()转十进制
console.log(args[2].readCString()); //读取字符串 char 类型
console.log(hexdump(args[2])); //内存 dump

},
//onLeave 里可以打印和修改返回值
onLeave: function(retval){ //retval 返回值
console.log(retval);
console.log("retval",retval.toInt32());
}
})
}
})
}

字符串类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function hookTest2(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
if(helloAddr != null){
Interceptor.attach(helloAddr,{
//onEnter 里可以打印和修改参数
onEnter: function(args){ //args 传入参数
// 方法一
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);
},
//onLeave 里可以打印和修改返回值
onLeave: function(retval){ //retval 返回值
var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
console.log("返回值:", returnedJString.toString());
}
})
}
})
}

Interceptor.replace 函数

Interceptor.replace(target, replacement[, data]):将 target 处的函数替换为 replacement 的实现(使用 NativeCallback 实现替换)。如果您想要完全或部分替换现有函数的实现,通常会使用此方法。需先启动应用,然后再 attach,而非 spawn,否者找不到函数地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//hook 修改参数值以及返回值
function replaceHook(addr) {
//参数个数和类型可通过 IDA 查看
//创建一个新的 native 函数,用于调用指定地址(第一个参数决定)的函数,
const addFunction = new NativeFunction(addr, 'int64', ['int64', 'int64', 'int', 'int']);
// 重实现目标函数地址的功能实现
Interceptor.replace(addr, new NativeCallback(
function(arg1, arg2, arg3, arg4){
console.log("come into add function");
console.log("arg1: ", arg1);
console.log("arg2: ", arg2);
console.log("arg3: ", arg3);
console.log("arg4: ", arg4);
//保持原函数功能不变
var result = addFunction(arg1, arg2, arg3, arg4);//主动调用
return result
}, 'int64', ['int64', 'int64', 'int', 'int'])
)

//调用 hook 后的函数查看效果
var result = addFunction(0, 0, 1, 2);
console.log("result: ", result);

}

function hookAdd(){
var addAddr = Module.findExportByName('libleidian.so', 'Java_com_example_leidian_MainActivity_add');
replaceHook(addAddr);
}

setImmediate(hookAdd)

hook 修改函数的参数和返回值

修改数值

1
2
3
4
5
6
7
8
9
Interceptor.attach(helloAddr, {
onEnter: function (args) {
args[2] = ptr(1000) // 指针类型参数 和 整数类型参数 修改
},
onLeave: function (retval) {
retval.replace(1337) // 整数类型返回值替换
retval.replace(ptr("0x1234"))//指针类型返回值替换 (指针其实仍然是数值)
},
})

利用 so 层原有字符串进行地址替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Interceptor.attach(funcAddr, {
onEnter: function (args) {
const strAddr = baseAddr.add(0x142D7) // 字符串的文件偏移量 0x142D7
args[1] = strAddr // 替换字符串地址参数
const str = strAddr.readCString()
console.log("target string: ", str);

args[2] = ptr(str.length) // 替换字符串长度参数
console.log("target string length: ", args[2]);
},
onLeave: function (retval) {

},
})

创建新字符串并进行地址替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const newStr = "some strings";
const newStrAddr = Memory.allocUtf8String(newStr); // 使用 Frida 的 Memory 来申请内存区域 返回的是一个指针

Interceptor.attach(funcAddr, {
onEnter: function (args) {
// const newStrAddr = Memory.allocUtf8String(newStr); // 如果在这里申请的话, 到 onLeave 将会回收 可以在全局定义或使用 this.newStrAddr 附加到自身
args[1] = newStrAddr
console.log("target string: ", args[1].readCString())

args[2] = ptr(newStr.length)
console.log("target string length: ", args[2]);
},
onLeave: function (retval) {},
})

原地址上修改字符

1
2
3
4
5
6
7
8
9
10
11
12
Interceptor.attach(funcAddr, {
onEnter: function (args) {
let newStr = 'some strings'
// 需要写入字节数组的方式来写入字符串
args[1].writeByteArray(hexToBytes(stringToHex(newStr) + '00')) // c 语言字符串结尾为 0 字节
console.log("target string: ", args[1].readCString())

args[2] = ptr(newStr.length)
console.log("target string length: ", args[2]);
},
onLeave: function (retval) {},
})

内存读写

https://frida.re/docs/javascript-api/#memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 读取指定地址的字符串
const baseAddr = Module.findBaseAddress('libleidian.so')
console.log(baseAddr.add(0x1234).readCString())

// 2. dump 指定地址的内存
console.log(hexdump(baseAddr.add(0x1234)))

// 3. 读指定地址的内存
console.log(baseAddr.add(0x1234).readByteArray(16))
console.log(Memory.readByteArray(baseAddr.add(0x2c00), 16)) //原先的 API

// 4. 写指定地址的内存
baseAddr.add(0x1234).writeByteArray(stringToBytes('xiaojianbang'))

// 5. 申请新内存写入
Memory.alloc()
Memory.allocUtf8String()

// 6. 修改内存权限
Memory.protect(ptr(libso.base), libso.size, 'rwx')

修改 native 层代码

需注意架构区别!!!arm 架构使用 Arm64Writer 修改,x64x86 架构使用 X86Writer 修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function hexToBytes(hex) {
let bytes = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16));
}
return bytes;
}

function alterCode(){
const addFuncAddr = Module.findExportByName("libleidian.so", "Java_com_example_leidian_MainActivity_add")
const addInsAddr = addFuncAddr.add(0x15)
console.log("origin instruction => ", Instruction.parse(addInsAddr));//将对应地址的硬编码解析成汇编指令
// console.log(Process.arch);

// 修改内存保护为可写
// Memory.protect(addInsAddr, 0x1000, 'rwx');
//修改对应地址的硬编码
// addInsAddr.writeByteArray(hexToBytes(0x8B45EC))//无效

Memory.patchCode(addInsAddr, 1, function (code) {
// let Writer = new Arm64Writer(code, {pc: addInsAddr});
let Writer = new X86Writer(code, {pc: addInsAddr});
Writer.putBytes(hexToBytes("2b"));
Writer.flush();
});
console.log("current instruction => ", Instruction.parse(addInsAddr));//将对应地址的硬编码解析成汇编指令
}

函数堆栈打印

1
2
3
4
5
6
console.log(
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
//.map(symbol => `${symbol.name} (${symbol.address})`)
.join('\n')
)

寻找 native 函数所在的动态库

native 函数根据注册时机可分为静态注册函数、动态注册函数,它们对应的 hook 点不同。

  • 动态注册函数:动态注册函数 hook RegisterNatives 函数。
  • 静态注册函数:可以 hook dlsym 函数(在动态加载的场景下)。

动态注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function hook_RegisterNatives() {
let RegisterNatives_addr = null
let symbols = Process.findModuleByName('libart.so').enumerateSymbols()
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i].name
if (symbol.indexOf('CheckJNI') == -1 && symbol.indexOf('JNI') >= 0) {//适合所有的 Android 系统版本
if (symbol.indexOf('RegisterNatives') >= 0) {
RegisterNatives_addr = symbols[i].address
console.log('RegisterNatives_addr: ', RegisterNatives_addr)
}
}
}
Interceptor.attach(RegisterNatives_addr, {
onEnter: function (args) {
let env = args[0]
let jclass = args[1]
let class_name = Java.vm.tryGetEnv().getClassName(jclass)
let methods_ptr = ptr(args[2])
let method_count = args[3].toInt32()
console.log('RegisterNatives method counts: ', method_count)
/*
JNINativeMethod jmethods [] = {
{"DynamicRegistration", "()V", (void *)(DynamicRegistrationNative)}//DynamicRegistrationNative 为 native 方法
};
*/
for (let i = 0; i < method_count; i++) {
let name = methods_ptr.add(i * Process.pointerSize * 3)
.readPointer()
.readCString()
let sig = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)
.readPointer()
.readCString()
let fnPtr_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2)
.readPointer()
let find_module = Process.findModuleByAddress(fnPtr_ptr)
console.log(
'RegisterNatives java_class: ', class_name,
'name: ', name,
'sig: ', sig,
'fnPtr: ', fnPtr_ptr,
'module_name: ', find_module.name,
'module_base: ', find_module.base,
'offset: ', ptr(fnPtr_ptr).sub(find_module.base),
)
}
},
onLeave: function (retval) {},
})
}

静态注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_dlsym(dlsymFuncAddr){
Interceptor.attach(dlsymFuncAddr, {
onEnter: function (args) {
// do something
this.libAddr = ptr(args[0]) // args [0] 是库基址
this.funcName = ptr(args[1]) // args [1] 是函数名
},
onLeave: function (retval) {
// do something
let module = Process.findModuleByAddress(retval)//this.libAddr
console.log("function name: ", this.funcName.readCString(),
"lib name: ", module.name, //是否可以直接用 libAddr.name?
"function's memory address: "retval,
"function offset: ", retval.sub(module.base)
)
},
})
}

let dlsymFuncAddr = Module.findExportByName('libdl.so', 'dlsym')
hook_dlsym(dlsymFuncAddr)

hook 运行时动态加载的库与函数

  • dlopen 函数用于运行时动态加载一个共享库,并返回一个句柄。这个句柄可以用于后续的符号解析。
  • dlsym 函数用于获取运行时动态加载的库中符号的地址。该符号可以是函数、变量等。

hook dlopen 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//hook_dlopen
function hook_dlopen(dlopenFuncAdrr, libName, targetFuncOffset, callback) {
Interceptor.attach(addr, {
onEnter: function (args) {
let libPath = args[0].readCString()
// 是否是目标动态库
if (libPath.indexOf(libName) != -1) {
this.hook = true
}
},
onLeave: function (retval) {
//得对应的 lib 库加载完了才能 hook 目标函数
if (this.hook){
//dlopen 返回值即 lib 库的指针
callback(retval.add(targetFuncOffset))
}
}
})
}

function hook_targetFunc(targetFuncAddr) {
Interceptor.attach(targetFuncAddr, {
onEnter: function (args) {
// do something
},
onLeave: function (retval) {
// do something
},
})
}

let dlopenFuncAdrr = Module.findExportByName('libdl.so', 'dlopen') // 低版本安卓系统
hook_dlopen(dlopenFuncAdrr, 'libxiaojianbang.so', hook_targetFunc)

//let dlopenFuncAdrr = Module.findExportByName('libdl.so', 'android_dlopen_ext') // 高版本安卓系统
//hook_dlopen(dlopenFuncAdrr, 'libxiaojianbang.so', hook_targetFunc)

hook dlsym 函数

获取目标函数所在的 so 库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_dlsym(dlsymFuncAddr){
Interceptor.attach(dlsymFuncAddr, {
onEnter: function (args) {
// do something
this.libAddr = ptr(args[0]) // args [0] 是库基址
this.funcName = ptr(args[1]) // args [1] 是函数名
},
onLeave: function (retval) {
// do something
let module = Process.findModuleByAddress(retval)//this.libAddr
console.log("function name: ", this.funcName.readCString(),
"lib name: ", module.name, //是否可以直接用 libAddr.name?
"function's memory address: "retval,
"function offset: ", retval.sub(module.base)
)
},
})
}

let dlsymFuncAddr = Module.findExportByName('libdl.so', 'dlsym')
hook_dlsym(dlsymFuncAddr)

inlineHook(针对寄存器的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//查看任意地址处的寄存器值
function inlineHook() {
var nativePointer = Module.findBaseAddress('libleidian.so')
var hookAddr = nativePointer.add(0x2080C)
Interceptor.attach(hookAddr, {
onEnter: function (args) {
console.log('onEnter: ', JSON.stringify(this.context))
// console.log('rax: ', this.context.rax)
},
onLeave: function (retval) {},
})

var funcAddr = Module.findExportByName('libleidian.so', 'Java_com_example_leidian_MainActivity_stringFromJNI');
var func = new NativeFunction(funcAddr, "int64", ["pointer"])
//主动调用该函数,以查看效果
func(Java.vm.tryGetEnv())

}

相关工具

其他

Android 逆向学习笔记——使用 Python 库调用 Frida | Whitebird’s Home (whitebird0.github.io)

实用 FRIDA 进阶:内存漫游、hook anywhere、抓包-安全客 - 安全资讯平台 (anquanke.com)

一些工具/脚本

dqzg12300/fridaUiTools: frida 工具的缝合怪 (github.com)

siyujie/OkHttpLogger-Frida: Frida 实现拦截 okhttp 的脚本 (github.com)

frida-dexdump

环境:venv 搭建不同版本的 frida 环境 - 冻 L1 的博客 (wutongliran.top)

IDA&Frida 学习 - 吾爱破解 - 52pojie.cn

frida 16

frida16的命令行跟以前有哪些不一样了?

答:仨变化 前俩主要是spawn的时候用包名,attach的时候用App名 1. https://t.zsxq.com/16wx9Q8Fg 2. https://t.zsxq.com/16QyRqxh4 3. 还有一个变化就是以前是默认启动时就会暂停的,得加–no-pause才能启动时不暂停。现在相反了,默认启动不暂停,得加–pause才会启动暂停。 4. objection的变化如贴:https://t.zsxq.com/16QvfwtGB

最新的frida16,在spawn模式下,不能用APP名,只能用包名,包名有多种获取方式: 1. adb查看最顶端前台应用的包名和activity名 adb shell dumpsys window | grep mCurrentFocus 2. ps -e 显示全部进程,新点开的app进程一般靠最后几个 3. apk里的AndroidManifest.xml里的package字段 同理r0capture在-f时要用包名。

最新的frida16,在attach模式下,可以 1. 直接-UF 2. 也可以-U APP名 APP名必须是点开app后,frida-ps -U显示的那个app名字。

同理#r0capture 在-U后面接APP名

评论