frida
官网
官网链接https://github.com/frida/frida/releases
安装
frida-tools
- 安装
frida-tools
使用pip install frida-tools
会自动安装最新版frida全系列产品
frida
迭代更新的速度很快,可以选择安装指定版本的frida
1
pip install frida-tools==9.2.4
- 查看frida版本
frida --version
frida-sever
- 在测试机上安装对应版本的
frida-sever
frida-sever
的版本要和frida
的版本一致,同时frida-sever
的架构要和测试机的系统以及架构保持一致
可以使用adb shell getprop ro.product.cpu.abi
查看系统架构信息 - 下载完
frida-sever
后,将解压后的frida-sever
通过adb push
推送到测试机的/data/local/tmp
目录下,通过chmod
命令为其赋予权限chmod 777 frida-sever
- 运行后通过
frida-ps -U
查看其进程
frida使用
frida --help
1 | -U 通过USB连接设备 |
Frida API
Java.perform(fn)
主要用于当前线程附加到Java VM并且调用fn方法。
Java.use(className)
动态获取className的类定义,通过对其调用$new()来调用构造函数,可以从中实例化对象。
Hook onCreate方法和stringFromJNI方法
1 | setImmediate(function(){ |
frida -U -f com.example.easyso -l demo.js --no-pause
这里的参数--no-pause
是在脚本加载完成后,不暂停应用程序的执行,不加这个参数的话,Frida 会在脚本加载后暂停应用程序的执行
Process
Process.enumerateModules()
列出进程中所有加载的模块。
Process.findModuleByName(“moduleName”)
通过模块名来查找模块,返回Module对象,如果未找到,返回null或undefined。
Process.getModuleByName(“moduleName”)
通过模块名来获取模块地址,返回Module对象,如果未找到,会抛出异常。
Process.findModuleByAddress(NativePointer)
通过地址查找包含该地址的模块,返回Module对象,如果未找到,返回null。
Process.getModuleByAddress(NativePointer)
通过地址获取包含该地址的模块,返回Module对象,如果未找到,会抛出异常。
Process.findRangeByAddress(NativePointer)
查找包含指定地址的内存范围。
如果找到包含指定地址的内存范围,则返回一个对象,包含内存范围的基址、大小、保护属性和文件信息等
Module
Module.enumerateImports(“moduleName”)
列出指定模块的所有导入函数,返回一个包含导入函数信息的数组。
Module.enumerateExports(“moduleName”)
列出指定模块的所有导出函数,返回一个包含导出函数信息的数组。
Module.enumerateSymbols(“moduleName”)
列出指定模块的所有符号,返回一个包含符号信息的数组。
Module.findBaseAddress(“moduleName”)
查找指定模块的基址,返回NativePointer对象,如果找不到模块则返回 null。
Module.findExportByName(“moduleName”,”exportName”)
查找指定模块中指定导出函数的地址,返回NativePointer对象。
枚举模块
1 | setImmediate(function(){ |
枚举导入表
1 | setImmediate(function(){ |
枚举符号表
1 | setImmediate(function(){ |
枚举导出表
1 | setImmediate(function(){ |
根据模块名称和函数名称获取函数地址
1 | setImmediate(function(){ |
Memory
- Memory.patchCode(address, size, apply)
安全地修改由 address 指定的内存位置(类型为 NativePointer)处的size大小的字节。提供的 JavaScript 函数 apply 会被调用,并传入一个可写指针,在返回之前,你必须在该指针处写入所需的修改。address
:指定要修改的内存区域的起始地址。size
:指定要修改的内存区域的大小(通常以字节为单位)。apply
:指定要应用的修改内容。1
2
3
4
5
6
7
8Memory.patchCode(address,size,function(code){
var writer =new Arm64Writer(code);
writer.putNop();
console.log(Instruction.parse(add));
console.log(writer.base);
console.log(writer.code);
console.log(writer.pc);
})
Interceptor
Interceptor.attach
当我们有了函数地址时,可以通过Interceptor.attach来对函数进行hook
1 | setImmediate(function(){ |
这里参数1和参数3打印出来的是地址,可以通过hexdump()函数来获取地址对应的内存,修改一下,再此执行
1 | setImmediate(function(){ |
参数3的地址处是空的,是因为还没计算出结果,结果在onLeave函数里
1 | onLeave:function (retval){ |
结果是一个地址,用hexdump来获取内存
1 | setImmediate(function(){ |
就可以看到参数3的值了
CPU Instruction
Instruction
- Instruction.parse(target)
解析内存中 target 地址处的指令。
返回的对象具有的字段:address
: 此指令的地址(EIP),类型为NativePointer
next
: 指向下一条指令的指针,您可以使用parse()
解析它size
: 此指令的大小mnemonic
: 指令助记符的字符串表示opStr
: 指令操作数的字符串表示operands
: 描述每个操作数的对象数组,每个对象至少指定类型和值,但可能还包含取决于架构的其他属性regsRead
: 此指令隐式读取的寄存器名称数组regsWritten
: 此指令隐式写入的寄存器名称数组groups
: 此指令所属的组名称数组toString()
: 转换为人类可读的字符串
使用Instruction来获取stringFromJNI指令信息
1 | setImmediate(function(){ |
Arm64Writer
创建一个新的代码写入器,用于生成直接写入内存的 AArch64 机器代码
构造函数
new Arm64Writer(codeAddress[, { pc: ptr(‘0x1234’) }]`
- codeAddress: 代码将被写入的内存地址,指定为 NativePointer 类型。
- pc (可选): 初始程序计数器,当在临时缓冲区中生成代码时非常有用。
方法
- reset(codeAddress[, { pc: ptr(‘0x1234’) }])
回收实例,重新初始化Arm64Writer
实例。 - dispose()
立即清理内存。 - flush()
解析标签引用并将待处理的数据写入内存。在生成代码完成后,应始终调用此方法。在一次生成多个函数时,通常也需要在不相关的代码片段之间调用此方法。
属性
- base
输出的第一个字节的内存位置,作为 NativePointer 类型。 - code
下一个字节的输出内存位置,作为 NativePointer 类型。 - pc
下一个字节的输出程序计数器,作为 NativePointer 类型。 - offset
当前偏移量,作为 JavaScript 数字类型。
代码生成方法
- skip(nBytes)
跳过指定字节数。 - putRet()
生成一个 RET 指令。 - putNop()
生成一个NOP指令