Frida Learning

前言

本文章主要整理Frida的一些用法,方便后续回顾时知道如何使用,本文中的例子基于Windows环境进行相应编写对应代码并进行测试,未对其他环境进行测试。同时因为个人水平以及知识有限,难免过程中会出现理解上的一些问题,进而解释错误等,还请师傅们指正。

其他学习链接:

Frida Tutorial for Reverse Engineers 4 of 10: Getting Your hands Dirty with Frida’s Python Binding

Welcome | Frida • A world-class dynamic instrumentation toolkit

相关应用

加载一个程序

1
frida.attach("XXX")

获取程序列表可以在控制台中使用frida-psattach上对应程序后其会返回一个session,后续对于程序的Hook之类的操作均是通过session来进行完成

消息处理

假如我们想对一个程序进行注入,那么需要准备的便是对于Hook的地址,我们构建一个简单的程序为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <unistd.h>

void f(int n)
{
printf ("Number: %d\n", n);
}

int main (int argc, char * argv[])
{
int i = 0;
printf ("f() is at %p\n", f);
while (1)
{
f (i++);
sleep (1);
}
}

将其编译并运行,我们可以拿到f函数的地址信息以及看到程序一直打印数字

获取参数

我们利用之前获取到的session,来创建一个Hook脚本

1
2
3
4
5
session.script(
"""
xxx
"""
)

其中的xxx为我们所编写的 js

利用上述我们可以构建一个完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import frida
import sys

session = frida.attach("hello.exe")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
send(args[0].toInt32());
}
});
""" % int(sys.argv[1], 16))

def on_message(message,data): # 消息处理
print(message)

script.on('message',on_message) # 接收消息类型,后面为消息处理的回调函数
script.load() # 加载脚本
sys.stdin.read()

上述Interceptor.attach代表我们需要附加上的地址信息,onEnter表示当到达对应地址后进行的处理

同时我们使用send进行发送消息,而接收利用script.on来进行,当收到消息后丢入on_message进行处理

相似的我们还有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
import frida
import sys

session = frida.attach("hello.exe")
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
send(args[0].toString());
var op = recv('input', function(value) {
args[0] = ptr(value.payload);
});
}
});
""" % int(sys.argv[1], 16))

def on_message(message, data):
print(message)
val = int(message['payload'], 16)
script.post({'type': 'input', 'payload': str(val * 2)})

script.on('message', on_message)
script.load()
sys.stdin.read()

上述中当我们进入到对应地址时会将参数信息进行发送到on_message进行处理,我们获取到字典中payload的值后将其使用post方式对其进行传入到我们的脚本中,对于recv存在有两个参数,第一个是消息类型,第二个是消息回调函数,可以看到其将原有的值乘以二后填入到原来的地方进行输出

recv() 函数本身是一个异步方式(并不会产生阻塞信息)

如果我们加入一个阻塞函数op.wait(),那么会出现如果JS没有收到Python发送的消息就一直等待接收

1
2
3
4
5
6
7
8
9
10
11
script = session.create_script("""
Interceptor.attach(ptr("%s"), {
onEnter: function(args) {
send(args[0].toString());
var op = recv('input', function(value) {
args[0] = ptr(value.payload);
});
op.wait();
}
});
""" % int(sys.argv[1], 16))

调用程序函数

调用程序时我们需要创建一个NativeFunction(),其构建参数为:new NativeFunction(address, returnType, argTypes[, abi])

address指明了对应的Hook函数的地址信息,returnType为返回值类型,argTypes为对应的传入参数,abi为可选参数

Supported Types

  • void
  • pointer
  • int
  • uint
  • long
  • ulong
  • char
  • uchar
  • float
  • double
  • int8
  • uint8
  • int16
  • uint16
  • int32
  • uint32
  • int64
  • uint64
  • bool

Supported ABIs

  • default

  • Windows 32-bit:

    • sysv
    • stdcall
    • thiscall
    • fastcall
    • mscdecl
  • Windows 64-bit:

    • win64
  • UNIX x86:

    • sysv
    • unix64
  • UNIX ARM:

    • sysv
    • vfp

我们将原来的程序进行些修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <unistd.h>
void f (int n, int* x) {
(*x) += n;
printf ("Number: %d %d\n", n,*x);
}
int main(int argc, char * argv[]) {
int i = 0,x = 0;
printf ("f() is at %p\n", f);
while (1) {
f (i++,&x);
sleep (1);
}
}

构建调用函数的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import frida
import sys

session = frida.attach("hello.exe")
script = session.create_script(
"""
var f = new NativeFunction(ptr(%d), 'void', ['int', 'pointer']);
f(1, ptr(0x62fe18));
f(1, ptr(0x62fe18));
"""
% int(sys.argv[1], 16))

script.load()
sys.stdin.read()

可以看到对应地址处的函数成功的被我们进行了调用,程序中间出现了几次打印 1,后面的x增加值为 1

字符串注入

我们将原来的代码进行一些修改,让其原本打印数字修改为打印对应的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>
void f (char* n){
printf ("String: %s\n", n);
}
int main(int argc, char * argv[]) {
char* array = "HelloWorld";
printf ("f() is at %p\n", f);
while (1) {
f (array);
sleep (1);
}
}

我们想要修改对应的字符串将其进行打印,我们构建的对应脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import frida
import sys

session = frida.attach("hello.exe")
script = session.create_script(
"""
var array = Memory.allocUtf8String("WOW,You are so good!");
var f = new NativeFunction(ptr(%d), 'void', ['pointer']);
f(array);
"""
% int(sys.argv[1], 16))

script.load()
sys.stdin.read()

使用类似的方法,比如Memory.alloc()Memory.protect()很容就能操作目标进程的内存,可以创建Pythonctypes和其他内存结构比如structs,然后以字节数组的方式传递给目标函数。

JS部分API

Module.findExportByName —— 获取导出函数信息

调用Windows API其参数有两个Module.findExportByName(moduleName|null, exportName);

第一个参数指明对应的模块名称,第二个则是相应的导出函数名信息,其返回值对应导出函数的绝对地址值,在寻找对应的导出函数时会花费较大时间来进行寻找

我们以notpad.exe来进行处理MessageBoxW(),我们首先可以使用frida-trace来构建一个基本的Hook框架

1
frida-trace -f C:\windows\system32\notepad.exe -i MessageBoxW -X user32.dll

-f 指定对应程序

-i 指定对应的函数名称

-X 指定对应模块

之后我们可以在对应运行上述指令的位置处找到一个__handlers__的文件夹,里面便是我们对应的JS基本Hook框架信息,我们可以简单的将其中的一些信息进行修改来达到一个Hook。对应的启动命令如下:

1
2
3
frida -n [正在运行文件名] -l [js脚本路径]
比如我要用当前目录的a.js脚本 去hook一个正在执行的文件b
frida -n b -l "./a.js"

但是我们此次是通过Module.findExportByName来找到对应API的地址,我们可以构建如下脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import frida
import sys

session = frida.attach("notepad.exe")
script = session.create_script(
"""
var baseFunc = Module.findExportByName("user32.dll", "MessageBoxW");
Interceptor.attach(baseFunc, {
onEnter: function(args, state) {
console.log("MessageBoxW called");
console.log("lpText: " + args[1].readUtf16String());
console.log("lpCaption: " + args[2].readUtf16String());
}
});
"""
)

script.load()
sys.stdin.read()

当我们触发对应的函数调用时其便会在控制台打印对应的窗体信息

hexdump —— 将对应数据按一定格式输出

我们同样的以notepad为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import frida
import sys

session = frida.attach("notepad.exe")
script = session.create_script(
"""
var baseFunc = Module.findExportByName("user32.dll", "MessageBoxW");
var buf = Memory.alloc(256);
buf = Memory.readByteArray(baseFunc, 256);
console.log(hexdump(buf, {
offset: 0,
length: 256,
header: true,
ansi: false
}));
"""
)

script.load()
sys.stdin.read()

我们可以拿到如下输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
00000000 48 83 ec 38 45 33 db 44 39 1d 92 7b 03 00 74 2e H..8E3.D9..{..t.
00000010 65 48 8b 04 25 30 00 00 00 4c 8b 50 48 33 c0 f0 eH..%0...L.PH3..
00000020 4c 0f b1 15 78 86 03 00 4c 8b 15 79 86 03 00 41 L...x...L..y...A
00000030 8d 43 01 4c 0f 44 d0 4c 89 15 6a 86 03 00 83 4c .C.L.D.L..j....L
00000040 24 28 ff 66 44 89 5c 24 20 e8 d2 fd ff ff 48 83 $(.fD.\$ .....H.
00000050 c4 38 c3 cc cc cc cc cc cc cc cc cc cc cc cc cc .8..............
00000060 40 55 53 56 57 41 54 41 55 41 56 41 57 48 8d 6c @USVWATAUAVAWH.l
00000070 24 f8 48 81 ec 08 01 00 00 65 48 8b 04 25 30 00 $.H......eH..%0.
00000080 00 00 45 33 ed 48 8b 79 10 48 8b f1 45 8b fd 4c ..E3.H.y.H..E..L
00000090 89 6d b8 4c 89 6d c0 4c 39 68 78 75 19 41 8d 4d .m.L.m.L9hxu.A.M
000000a0 0e 48 ff 15 f0 78 01 00 0f 1f 44 00 00 48 85 c0 .H...x....D..H..
000000b0 0f 84 8d 08 00 00 44 8b 66 28 41 8b dc c1 eb 07 ......D.f(A.....
000000c0 81 e3 00 10 00 00 48 f7 46 20 00 00 ff ff 75 68 ......H.F ....uh
000000d0 48 85 ff 74 4e 48 8b 0d f4 77 03 00 ba 08 00 00 H..tNH...w......
000000e0 00 41 b8 00 02 00 00 48 ff 15 ea 71 01 00 0f 1f .A.....H...q....
000000f0 44 00 00 48 89 45 b8 48 85 c0 74 27 0f b7 56 20 D..H.E.H..t'..V

setTimeout —— 延迟函数调用

setTimeout(fn, delay),在延迟delay毫秒之后,调用 fn,这个调用会返回一个ID,这个ID可以传递给clearTimeout用来进行调用取消

clearTimeout —— 取消延迟函数调用

clearTimeout(id),取消通过setTimeout发起的延迟调用

setInterval —— 设置间隔时间调用函数

setInterval(fn, delay),每隔delay毫秒调用一次 fn,返回一个ID,这个ID可以传给clearInterval进行调用取消

clearInterval —— 取消间隔时间调用函数

clearInterval(id),取消通过setInterval发起的调用

Memory.readPointer —— 数据读取

Memory.readPointer(address)其中 address 为对应读地址信息

Memory.writePointer —— 内存写入

Memory.writePointer(address, ptr)其中 address 为对应写地址信息,ptr 为对应写内容,如我需要写入123,则相应构造为ptr("123")

Interceptor.attach —— 函数调用拦截

Interceptor.attach(target, callbacks): 在target指定的位置进行函数调用拦截,target 是一个NativePointer参数,用来指定你想要拦截的函数的地址,有一点需要注意,在32位 ARM 机型上,ARM函数地址末位一定是 0 ( 2字节对齐 ),Thumb函数地址末位一定 1 ( 单字节对齐 ),如果使用的函数地址是用 Frida API 获取的话,那么API内部会自动处理这个细节 ( 比如:Module.findExportByName())。

其中callbacks参数是一个对象,大致结构如下:

  • onEnter: function(args): 被拦截函数调用之前回调,其中原始函数的参数使用args数组 ( NativePointer 对象数组 ) 来表示,可以在这里修改函数的调用参数。

  • onLeave: function(retval): 被拦截函数调用之后回调,其中 retval 表示原始函数的返回值,retval 是从 NativePointer 继承来的,是对原始返回值的一个封装,可以使用 retval.replace(1337) 调用来修改返回值的内容。需要注意的一点是,retval 对象只在 onLeave 函数作用域范围 内有效,因此如果你要保存这个对象以备后续使用的话,一定要使用深拷贝来保存对象,比如:ptr(retval.toString())

拦截器的attach调用返回一个监听对象,后续取消拦截的时候,可以作为 Interceptor.detach() 的参数使用。

还有一个比较方便的地方,那就是在回调函数里面,包含了一个隐藏的 this 的线程 tls 对象,方便在回调函数中存储变量,比如可以在 onEnter 中保存值,然后在 onLeave 中使用

this 对象还包含了一些额外的比较有用的属性:

  • returnAddress: 返回 NativePointer 类型的 address 对象

  • context: 包含 pc,sp,以及相关寄存器比如 eax, ebx等,可以在回调函数中直接修改

  • errno: (UNIX)当前线程的错误值

  • lastError: (Windows) 当前线程的错误值

  • threadId: 操作系统线程Id depth: 函数调用层次深度

Python部分API

消息监控

使用script.on('message', on_message)来监控任何来自目标进程的消息,消息监控可以来自scriptsession两个方面。比如,如果你想要监控目标进程的退出,可以使用下面这个语句session.on('detached', my_function)

API参考

此部分主要搬运与官方的API文档,不对以下API做过多的举例分析

Process

Process.arch: CPU架构信息,取值范围:ia32、x64、arm、arm64

Process.platform: 平台信息,取值范围:windows、darwin、linux、qnx

Process.pageSize: 虚拟内存页面大小,主要用来辅助增加脚本可移植性

Process.pointerSize: 指针占用的内存大小,主要用来辅助增加脚本可移植性

Process.codeSigningPolicy: 取值范围是 optional 或者 required,后者表示Frida会尽力避免修改内存中的代码,并且不会执行未签名的代码。默认值是 optional,除非是在 Gadget 模式下通过配置文件来使用 required,通过这个属性可以确定 Interceptor API 是否有限制,确定代码修改或者执行未签名代码是否安全。

这个目前没有实验清楚,可以参考原文

Process.isDebuggerAttached(): 确定当前是否有调试器附加

Process.getCurrentThreadId(): 获取当前线程ID

Process.enumerateThreads(callbacks): 枚举所有线程,每次枚举到一个线程就执行回调类callbacks:

  • onMatch: function(thread): 当枚举到一个线程的时候,就调用这个函数,其中thread参数包含 :

      1. id,线程ID
      2. state,线程状态,取之范围是 running, stopped, waiting, uninterruptible, halted
      3. context, 包含 pc, sp,分别代表 EIP/RIP/PC 和 ESP/RSP/SP,分别对应于 ia32/x64/arm平台,其他的寄存器也都有,比如 eax, rax, r0, x0 等。
      4. 函数可以直接返回 stop 来停止枚举。
  • onComplete: function(): 当所有的线程枚举都完成的时候调用。

Process.enumerateThreadSync(): enumerateThreads()的同步版本,返回线程对象数组

Process.findModuleByAddress(address), Process.getModuleByAddress(address), Process.findModuleByName(name), Process.getModuleByName(name): 根据地址或者名称来查找模块,如果找不到这样的模块,find开头的函数返回 null,get开头的函数会抛出异常。

Process.enumerateModules(callbacks): 枚举已经加载的模块,枚举到模块之后调用回调对象:

  • onMatch: function(module): 枚举到一个模块的时候调用,module对象包含如下字段:
      1. name, 模块名
      1. base, 基地址
      1. size,模块大小
      1. path,模块路径
      1. 函数可以返回 stop 来停止枚举 。
      1. onComplete: function(): 当所有的模块枚举完成的时候调用。

Process.enumerateModulesSync(): enumerateModules() 函数的同步版本,返回模块对象数组

Process.findRangeByAddress(address), Process.getRangeByAddress(address): 返回一个内存块对象,如果在这个address找不到内存块对象,那么 findRangeByAddress() 返回 null 而 getRangeByAddress 则抛出异常。

Process.numerateRanges(protection | specifier, callbacks): 枚举指定 protection 类型的内存块,以指定形式的字符串给出:rwx,而 rw 表示最少是可读可写,也可以用分类符,里面包含 protection 这个Key,取值就是前面提到的rwx,还有一个 coalesce 这个Key,表示是否要把位置相邻并且属性相同的内存块合并给出结果,枚举过程中回调 callbacks 对象:

  • onMatch: function(range): 每次枚举到一个内存块都回调回来,其中Range对象包含如下属性:

    1. base:基地址 26

    2. size:内存块大小

    3. protection:保护属性

    4. file:(如果有的话)内存映射文件:
      4.1 path,文件路径
      4.2 offset,文件内偏移

    5. 如果要停止枚举过程,直接返回 stop 即可

  • onComplete: function(): 所有内存块枚举完成之后会回调

Process.enumerateRangesSync(protection | specifier): enumerateRanges()函数的同步版本,返回内存块数组

Process.enumerateMallocRanges(callbacks): 用于枚举在系统堆上申请的内存块

Process.enumerateMallocRangesSync(protection): Process.enumerateMallocRanges() 的同步版本

Process.setExceptionHandler(callback): 在进程内安装一个异常处理函数(Native Exception),回调函数会在目标进程本身的异常处理函数之前调用,回调函数只有一个参数 details,包含以下几个属性:

  • type,取值为下列之一:

      1. abort
      1. access-violation
      1. guard-page
      1. illegal-instruction
      1. stack-overflow
      1. arithmetic
      1. breakpoint
      1. single-step
      1. system
  • address,异常发生的地址,NativePointer

  • memory,如果这个对象不为空,则会包含下面这些属性:

      1. operation: 引发一场的操作类型,取值范围是 read, write 或者 execute
      1. address: 操作发生异常的地址,NativePointer
  • context,包含 pc 和 sp 的 NativePointer,分别代表指令指针和堆栈指针

  • nativeContext,基于操作系统定义的异常上下文信息的 NativePointer,在 context 里面的信息不够用的时候,可以考虑用这个指针,但是一般不建议使用

    估计是考虑到可移植性或者稳定性

  • 捕获到异常之后,怎么使用就看你自己了,比如可以把异常信息写到日志里面,然后发送个信息给主控端,然后同步等待主控端的响应之后 处理,或者直接修改异常信息里面包含的寄存器的值,尝试恢复掉异常,继续执行。如果你处理了异常信息,那么这个异常回调里面你要返回 true,Frida会把异常交给进程异常处理函数处理,如果到最后都没人去处理这个异常,就直接结束目标进程。

Module

Module.emuerateImports(name, callbacks): 枚举模块 name 的导入表,枚举到一个导入项的时候回调callbacks, callbacks包含下面2个回调:

  • onMatch: function(imp): 枚举到一个导入项到时候会被调用,imp包含如下的字段:

      1. type,导入项的类型, 取值范围是 function或者variable
      1. name,导入项的名称
      1. module,模块名称
      1. address,导入项的绝对地址
      1. 以上所有的属性字段,只有 name 字段是一定会有,剩余的其他字段不能保证都有,底层会尽量保证每个字段都能给出数据,但是不 能保证一定能拿到数据,onMatch 函数可以返回字符串 stop 表示要停止枚举。
  • onComplete: function(): 当所有的导入表项都枚举完成的时候会回调

Module.eumerateImportsSync(name): enumerateImports()的同步版本

Module.emuerateExports(name, callbacks): 枚举指定模块 name 的导出表项,结果用 callbacks 进行回调:

  • onMatch: function(exp): 其中 exp 代表枚举到的一个导出项,包含如下几个字段:
      1. type,导出项类型,取值范围是 function或者variable
      1. name,导出项名称
      1. address,导出项的绝对地址,NativePointer
      1. 函数返回 stop 的时候表示停止枚举过程
  • onComplete: function(): 枚举完成回调

Module.enumerateExportsSync(): Module.enumerateExports()的同步版本

Module.enumerateSymbols(name, callbacks): 枚举指定模块中包含的符号,枚举结果通过回调进行通知:

  • onMatch: function(sym): 其中 sym 包含下面几个字段: isGlobal,布尔值,表示符号是否全局可见 type,符号的类型,取值是下面其中一种:

      1. unknown
      1. undefined
      1. absolute
      1. section
      1. prebound-undefined
      1. indirect
  • section,如果这个字段不为空的话,那这个字段包含下面几个属性:

      1. id,小节序号,段名,节名
      1. protection,保护属性类型, rwx 这样的属性
  • name,符号名称

  • address,符号的绝对地址,NativePointer 这个函数返回 stop 的时候,表示要结束枚举过程

Module.enumerateSymbolsSync(name): Module.enumerateSymbols() 的同步版本

Module.enumerateRanges(name, protection, callbacks): 功能基本等同于 Process.enumerateRanges(),只不过多了一个模块名限定了枚 举的范围

Module.enumerateRangesSync(name, protection): Module.enumerateRanges() 的同步版本

Module.findBaseAddress(name): 获取指定模块的基地址

Module.findExportByName(module | null, exp): 返回模块 module 内的导出项的绝对地址,如果模块名不确定,第一个参数传入 null,这种情 况下会增大查找开销,尽量不要使用。

Memory

Memory.scan(address, size, pattern, callbacks): 在 address 开始的地址,size 大小的内存范围内以 pattern 这个模式进行匹配查找,查找到一个内存块就回调callbacks,各个参数详细如下:

  • pattern 比如使用13 37 ?? ff来匹配 0x13开头,然后跟着0x37,然后是任意字节内容,接着是0xff这样的内存块
  • callbacks 是扫描函数回调对象:
      1. onMatch: function(address, size): 扫描到一个内存块,起始地址是address,大小size的内存块,返回 stop 表示停止扫描
      1. onError: function(reason): 扫描内存的时候出现内存访问异常的时候回调
      1. onComplete: function(): 内存扫描完毕的时候调用

Memory.scanSync(address, size, pattern): 内存扫描 scan() 的同步版本

Memory.alloc(size): 在目标进程中的堆上申请size大小的内存,并且会按照Process.pageSize对齐,返回一个NativePointer,并且申请的内存 如果在JavaScript里面没有对这个内存的使用的时候会自动释放的。也就是说,如果你不想要这个内存被释放,你需要自己保存一份对这个内存块的引用。

Memory.copy(dust, src, n): 就像是memcpy

Memory.dup(address, size): 等价于 Memory.alloc()和Memory.copy()的组合。

Memory.protect(address, size, protection): 更新address开始,size大小的内存块的保护属性,protection 的取值参考 Process.enumerateRanges(),比如:Memory.protect(ptr("0x123", 4096, 'rw-'));

Memory.patchCode(address, size, apply): apply是一个回调函数,这个函数是用来在 address 开始的地址和 size 大小的地方开始Patch的时 候调用,回调参数是一个NativePointer的可写指针,需要在 apply 回调函数里面要完成 patch 代码的写入,注意,这个可写的指针地址不一定和上 面的address是同一个地址,因为在有的系统上是不允许直接写入代码段的,需要先写入到一个临时的地方,然后在影射到响应代码段上


Frida Learning
https://equinox-shame.github.io/2023/07/21/Frida 学习/
作者
梓曰
发布于
2023年7月21日
许可协议