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
2
3
4
5
6
7
8
9
10
-U    通过USB连接设备
-R 连接远程设备
-H 连接指定的IP地址和端口的frida-sever,可以连接多台设备,通过netstat -nltp查看

-f spawn模式,启动时hook,启动一个新的进程并挂起 # 有必要带上'--no-pause'参数
-F attach模式,直接注入前端进程
-n 通过包名注入
-p 以PID进程码来注入 同名进程时用
-l 脚本路径
-o 指定内容输出到哪个文件

Frida API

Java.perform(fn)

主要用于当前线程附加到Java VM并且调用fn方法。

Java.use(className)

动态获取className的类定义,通过对其调用$new()来调用构造函数,可以从中实例化对象。

Hook onCreate方法和stringFromJNI方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setImmediate(function(){
Java.perform(function(){
var mainactivity = Java.use("com.example.easyso.MainActivity");
mainactivity.onCreate.implementation=function(x){
console.log("this is MainActivity.onCreate");
return this.onCreate(x);
}

mainactivity.stringFromJNI.implementation=function(){
var result = this.stringFromJNI();
console.log("stringFromJNI: ",result);
return result;
}
})
})

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
2
3
4
5
6
7
setImmediate(function(){
var moudles = Process.enumerateModules();
for(var i=0;i<moudles.length;i++){
console.log(JSON.stringify(moudles[i]))
}
console.log(moudles);
})

枚举导入表

1
2
3
4
5
6
7
8
setImmediate(function(){
// 获取包含导入表信息的数组
var imports = Module.enumerateImports("libeasyso.so");
console.log(JSON.stringify(imports));
for(var i=0;i<imports.length;i++){
console.log("name:",imports[i].name+" address:"+imports[i].address);
}
})

枚举符号表

1
2
3
4
5
6
7
8
setImmediate(function(){
// 获取包含符号表信息的数组
var symbols = Module.enumerateSymbols("libencryptlib.so");
console.log(JSON.stringify(symbols))
for(var i=0;i<imports.length;i++){
console.log(symbols[i]);
}
})

枚举导出表

1
2
3
4
5
6
7
8
setImmediate(function(){
// 获取包含导出表信息的数组
var exports = Module.enumerateExports("libencryptlib.so");
console.log(JSON.stringify(exports))
for(var i=0;i<exports.length;i++){
console.log(exports[i]);
}
})

根据模块名称和函数名称获取函数地址

1
2
3
4
5
setImmediate(function(){
// 根据模块名称和函数名称获取函数地址,这里的函数名称是IDA里获取到的符号名
var funaddr = Module.findExportByName("libencryptlib.so","_ZN7MD5_CTX11MakePassMD5EPhjS0_");
console.log(funaddr);
})

Memory

  • Memory.patchCode(address, size, apply)
    安全地修改由 address 指定的内存位置(类型为 NativePointer)处的size大小的字节。提供的 JavaScript 函数 apply 会被调用,并传入一个可写指针,在返回之前,你必须在该指针处写入所需的修改。
    • address:指定要修改的内存区域的起始地址。
    • size:指定要修改的内存区域的大小(通常以字节为单位)。
    • apply:指定要应用的修改内容。
      1
      2
      3
      4
      5
      6
      7
      8
      Memory.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
2
3
4
5
6
7
8
9
10
11
12
13
14
setImmediate(function(){
var funaddr = Module.findExportByName("libencryptlib.so","_ZN7MD5_CTX11MakePassMD5EPhjS0_");
console.log(funaddr);
Interceptor.attach(funaddr,{
onEnter:function (args){
console.log("funAddr onEnter args[1]:",args[1]);
console.log("funAddr onEnter args[2]:",args[2]);
console.log("funAddr onEnter args[3]:",args[3]);
},
onLeave:function (retval){

}
})
})


这里参数1和参数3打印出来的是地址,可以通过hexdump()函数来获取地址对应的内存,修改一下,再此执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setImmediate(function(){
var funaddr = Module.findExportByName("libencryptlib.so","_ZN7MD5_CTX11MakePassMD5EPhjS0_");
console.log(funaddr);
Interceptor.attach(funaddr,{
onEnter:function (args){
console.log("funAddr onEnter args[1]:",hexdump(args[1]));
console.log("funAddr onEnter args[2]:",args[2].toInt32()); // 默认显示16进制,这里转为10进制
console.log("funAddr onEnter args[3]:",hexdump(args[3]));
},
onLeave:function (retval){

}
})
})

参数3的地址处是空的,是因为还没计算出结果,结果在onLeave函数里

1
2
3
onLeave:function (retval){
console.log("funAddr onLeave args[3]:",this.arg3);
}

结果是一个地址,用hexdump来获取内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setImmediate(function(){
var funaddr = Module.findExportByName("libencryptlib.so","_ZN7MD5_CTX11MakePassMD5EPhjS0_");
console.log(funaddr);
Interceptor.attach(funaddr,{
onEnter:function (args){
console.log("funAddr onEnter args[1]:",hexdump(args[1]));
console.log("funAddr onEnter args[2]:",args[2].toInt32()); // 默认显示16进制,这里转为10进制
console.log("funAddr onEnter args[3]:",hexdump(args[3]));
this.arg3 = args[3];
},
onLeave:function (retval){
console.log("funAddr onLeave args[3]:",hexdump(this.arg3));
}
})
})

就可以看到参数3的值了

CPU Instruction

Instruction

  • Instruction.parse(target)
    解析内存中 target 地址处的指令。
    返回的对象具有的字段:
    • address: 此指令的地址(EIP),类型为 NativePointer
    • next: 指向下一条指令的指针,您可以使用 parse() 解析它
    • size: 此指令的大小
    • mnemonic: 指令助记符的字符串表示
    • opStr: 指令操作数的字符串表示
    • operands: 描述每个操作数的对象数组,每个对象至少指定类型和值,但可能还包含取决于架构的其他属性
    • regsRead: 此指令隐式读取的寄存器名称数组
    • regsWritten: 此指令隐式写入的寄存器名称数组
    • groups: 此指令所属的组名称数组
    • toString(): 转换为人类可读的字符串

使用Instruction来获取stringFromJNI指令信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setImmediate(function(){
{
// 根据模块名称和导出函数的名称来查找并返回对应的函数地址
var stringFromJniaddr = Module.findExportByName("libeaso.so","Java_com_example_easyso_MainActivity_stringFromJNI");
console.log(stringFromJniaddr);
// 解析指定地址处的机器码指令,返回一个 Instruction对象
var instruction = Instruction.parse(stringFromJniaddr);
console.log("ins =>",instruction.toString());
var address;
for(var i=0;i<10;i++){
// ".next 的作用是获取当前指令的下一个地址或相关信息"
address=instruction.next;
instruction = Instruction.parse(address);
console.log("ins =>",instruction.toString());
}
}
})

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指令