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-ps
,attach
上对应程序后其会返回一个session
,后续对于程序的Hook之类的操作均是通过session
来进行完成
消息处理
假如我们想对一个程序进行注入,那么需要准备的便是对于Hook的地址,我们构建一个简单的程序为例:
1 |
|
将其编译并运行,我们可以拿到f函数
的地址信息以及看到程序一直打印数字
获取参数
我们利用之前获取到的session
,来创建一个Hook
脚本
1 |
|
其中的xxx为我们所编写的 js
利用上述我们可以构建一个完整脚本:
1 |
|
上述Interceptor.attach
代表我们需要附加上的地址信息,onEnter
表示当到达对应地址后进行的处理
同时我们使用send
进行发送消息,而接收利用script.on
来进行,当收到消息后丢入on_message
进行处理
相似的我们还有recv
来处理消息,下面使用一个简单的例子
1 |
|
上述中当我们进入到对应地址时会将参数信息进行发送到on_message
进行处理,我们获取到字典中payload
的值后将其使用post
方式对其进行传入到我们的脚本中,对于recv
存在有两个参数,第一个是消息类型,第二个是消息回调函数,可以看到其将原有的值乘以二后填入到原来的地方进行输出
recv() 函数本身是一个异步方式(并不会产生阻塞信息)
如果我们加入一个阻塞函数op.wait()
,那么会出现如果JS没有收到Python发送的消息就一直等待接收
1 |
|
调用程序函数
调用程序时我们需要创建一个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 |
|
构建调用函数的脚本如下:
1 |
|
可以看到对应地址处的函数成功的被我们进行了调用,程序中间出现了几次打印 1,后面的x
增加值为 1
字符串注入
我们将原来的代码进行一些修改,让其原本打印数字修改为打印对应的字符串
1 |
|
我们想要修改对应的字符串将其进行打印,我们构建的对应脚本如下:
1 |
|
使用类似的方法,比如Memory.alloc()
和Memory.protect()
很容就能操作目标进程的内存,可以创建Python
的ctypes
和其他内存结构比如structs
,然后以字节数组的方式传递给目标函数。
JS部分API
Module.findExportByName —— 获取导出函数信息
调用Windows API
其参数有两个Module.findExportByName(moduleName|null, exportName);
第一个参数指明对应的模块名称,第二个则是相应的导出函数名信息,其返回值对应导出函数的绝对地址值,在寻找对应的导出函数时会花费较大时间来进行寻找
我们以notpad.exe
来进行处理MessageBoxW()
,我们首先可以使用frida-trace
来构建一个基本的Hook
框架
1 |
|
-f 指定对应程序
-i 指定对应的函数名称
-X 指定对应模块
之后我们可以在对应运行上述指令的位置处找到一个__handlers__
的文件夹,里面便是我们对应的JS
基本Hook
框架信息,我们可以简单的将其中的一些信息进行修改来达到一个Hook
。对应的启动命令如下:
1 |
|
但是我们此次是通过Module.findExportByName
来找到对应API
的地址,我们可以构建如下脚本
1 |
|
当我们触发对应的函数调用时其便会在控制台打印对应的窗体信息
hexdump —— 将对应数据按一定格式输出
我们同样的以notepad
为例子
1 |
|
我们可以拿到如下输出
1 |
|
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)
来监控任何来自目标进程的消息,消息监控可以来自script
和session
两个方面。比如,如果你想要监控目标进程的退出,可以使用下面这个语句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参数包含 :
-
- id,线程ID
- state,线程状态,取之范围是 running, stopped, waiting, uninterruptible, halted
- context, 包含 pc, sp,分别代表 EIP/RIP/PC 和 ESP/RSP/SP,分别对应于 ia32/x64/arm平台,其他的寄存器也都有,比如 eax, rax, r0, x0 等。
- 函数可以直接返回 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对象包含如下字段:
-
- name, 模块名
-
- base, 基地址
-
- size,模块大小
-
- path,模块路径
-
- 函数可以返回 stop 来停止枚举 。
-
- 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对象包含如下属性:
-
base:基地址 26
-
size:内存块大小
-
protection:保护属性
-
file:(如果有的话)内存映射文件:
4.1 path,文件路径
4.2 offset,文件内偏移 -
如果要停止枚举过程,直接返回 stop 即可
-
-
onComplete: function(): 所有内存块枚举完成之后会回调
Process.enumerateRangesSync(protection | specifier)
: enumerateRanges()函数的同步版本,返回内存块数组
Process.enumerateMallocRanges(callbacks)
: 用于枚举在系统堆上申请的内存块
Process.enumerateMallocRangesSync(protection)
: Process.enumerateMallocRanges()
的同步版本
Process.setExceptionHandler(callback)
: 在进程内安装一个异常处理函数(Native Exception),回调函数会在目标进程本身的异常处理函数之前调用,回调函数只有一个参数 details,包含以下几个属性:
-
type,取值为下列之一:
-
- abort
-
- access-violation
-
- guard-page
-
- illegal-instruction
-
- stack-overflow
-
- arithmetic
-
- breakpoint
-
- single-step
-
- system
-
-
address,异常发生的地址,NativePointer
-
memory,如果这个对象不为空,则会包含下面这些属性:
-
- operation: 引发一场的操作类型,取值范围是 read, write 或者 execute
-
- address: 操作发生异常的地址,NativePointer
-
-
context,包含 pc 和 sp 的 NativePointer,分别代表指令指针和堆栈指针
-
nativeContext,基于操作系统定义的异常上下文信息的 NativePointer,在 context 里面的信息不够用的时候,可以考虑用这个指针,但是一般不建议使用
估计是考虑到可移植性或者稳定性
-
捕获到异常之后,怎么使用就看你自己了,比如可以把异常信息写到日志里面,然后发送个信息给主控端,然后同步等待主控端的响应之后 处理,或者直接修改异常信息里面包含的寄存器的值,尝试恢复掉异常,继续执行。如果你处理了异常信息,那么这个异常回调里面你要返回 true,Frida会把异常交给进程异常处理函数处理,如果到最后都没人去处理这个异常,就直接结束目标进程。
Module
Module.emuerateImports(name, callbacks)
: 枚举模块 name 的导入表,枚举到一个导入项的时候回调callbacks, callbacks包含下面2个回调:
-
onMatch: function(imp): 枚举到一个导入项到时候会被调用,imp包含如下的字段:
-
- type,导入项的类型, 取值范围是 function或者variable
-
- name,导入项的名称
-
- module,模块名称
-
- address,导入项的绝对地址
-
- 以上所有的属性字段,只有 name 字段是一定会有,剩余的其他字段不能保证都有,底层会尽量保证每个字段都能给出数据,但是不 能保证一定能拿到数据,onMatch 函数可以返回字符串 stop 表示要停止枚举。
-
-
onComplete: function(): 当所有的导入表项都枚举完成的时候会回调
Module.eumerateImportsSync(name)
: enumerateImports()的同步版本
Module.emuerateExports(name, callbacks)
: 枚举指定模块 name 的导出表项,结果用 callbacks 进行回调:
- onMatch: function(exp): 其中 exp 代表枚举到的一个导出项,包含如下几个字段:
-
- type,导出项类型,取值范围是 function或者variable
-
- name,导出项名称
-
- address,导出项的绝对地址,NativePointer
-
- 函数返回 stop 的时候表示停止枚举过程
-
- onComplete: function(): 枚举完成回调
Module.enumerateExportsSync()
: Module.enumerateExports()
的同步版本
Module.enumerateSymbols(name, callbacks)
: 枚举指定模块中包含的符号,枚举结果通过回调进行通知:
-
onMatch: function(sym): 其中 sym 包含下面几个字段: isGlobal,布尔值,表示符号是否全局可见 type,符号的类型,取值是下面其中一种:
-
- unknown
-
- undefined
-
- absolute
-
- section
-
- prebound-undefined
-
- indirect
-
-
section,如果这个字段不为空的话,那这个字段包含下面几个属性:
-
- id,小节序号,段名,节名
-
- 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 是扫描函数回调对象:
-
- onMatch: function(address, size): 扫描到一个内存块,起始地址是address,大小size的内存块,返回 stop 表示停止扫描
-
- onError: function(reason): 扫描内存的时候出现内存访问异常的时候回调
-
- 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是同一个地址,因为在有的系统上是不允许直接写入代码段的,需要先写入到一个临时的地方,然后在影射到响应代码段上