frida教程

frida环境搭建

  1. 根据机器cpu架构选择下载frida: frida sever

  2. 解压获得frida-server-x.x.x-x,将该文件上传至Android /data/local/tmp下

  3. 赋予该文件执行权限,chmod 777 frida-server-x.x.x-x

  4. 运行该文件, ./frida-server-x.x.x-x

  5. python安装frida,安装的版本一定与下载的文件版本对应

    1
    pip3 install frida-tools  //最好挂个梯子,要不容易一直卡在setup

frida工具介绍

安装完成后在python的Scripts目录下会有frida ,frida-trace,frida-ps,frida-discover ,frida-kill ,frida-ls-devices,6个EXE可执行文件。

frida

1. frida -U     包名                     ---调试连接到电脑上设备中的应用
2. frida  进程名                        ---打开本地进程
3. frida -U -f 进程名 -l 脚本   --- 进程注入js脚本

frida–ps

用于列出进程的命令行工具
1. frida-ps -U    ---列出设备上的所有进程
2. frida-ps -Ua   ---列出设备上的所有应用名称
3. frida-ps -Uai  ---列举出来设备上的所有已安装应用程序和对应的名字
4. frida-ps -D PID ---连接指定设备
5. frida-ps -D <DEVICE-ID> -a ---检查进程是否被杀死

frida-trace

 动态跟踪函数调用的工具
 1. frida-trace -i "recv" 包名  跟踪recv方法
 2. frida-trace -m "-[NSString isEqualToString]" 包名   ---追踪oc isEqualToString方法

frida-discover

 1. frida-discover -n 包名/frida-discover -p pid ---发现程序内部函数

frida-kill

 终止进程的命令行工具
 1. frida-kill -D <DEVICE-ID> <PID> ---杀死进程

frida-ls-devices

  1. 列出所有连接到电脑上的设备 注:
    1. frida-ps -Uai 查看所有安装的应用。
    2. frida-ps -D cca1b9055 -a -i 可以指定查看某个设备的进程。
    3. frida-trace 用于跟踪函数或者 Objective-C 方法的调用-i 跟踪某个函数,-x 排除某个函数。-m 跟踪某个 Objective-C 方法,-M 排除某个 Objective-C 方法。-a 跟踪某一个地址,需要指名模块的名称。

frida编译

用于学习frida原理及去特征

编译环境

  • ubuntu
  • python3
  • node(v14.14.0)
  • NDK

编译过程

  1. 安装依赖

    以下是编译需要的环境,如果有则不需要在安装,node推荐去官网下载(https://nodejs.org)

    1
    sudo apt-get install build-essential curl git lib32stdc++-9-dev libc6-dev-i386 nodejs npm python3-dev python3-pip
  2. 拉源码

    1
    git clone --recurse-submodules https://github.com/frida/frida.git
  3. 设置环境变量

    1
    2
    3
    4
    androidNdk="/Users/cola/Library/Android/sdk/ndk/android-ndk-r22b"
    export PATH=$androidNdk:$PATH
    export ANDROID_NDK_ROOT=$androidNdk
    export NDK_HOME=$androidNdk
  4. 编译(第一次编译会下载编译工具,用梯子快一些不会出现奇怪的问题)

    1
    make core-android-arm64
  5. 编译完成

    编译后的文件在build目录下

    1
    2
    3
    build/frida-android-arm64/bin/frida-server
    build/frida-android-arm64/lib/frida-gadget.so
    .......

API

hexdump

1
2
3
4
5
6
7
8
9
10
11
12
13
var libc = Module.findBaseAddress('libc.so');
console.log(hexdump(libc, {
offset: 0,
length: 64,
header: true,
ansi: true
}));
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
00000010 03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00 ..(.........4...
00000020 34 a8 04 00 00 00 00 05 34 00 20 00 08 00 28 00 4.......4. ...(.
00000030 1e 00 1d 00 06 00 00 00 34 00 00 00 34 00 00 00 ........4...4...

send

send是在python层定义的on_message回调函数,jscode内所有的信息都被监控script.on(‘message’, on_message),当输出信息的时候on_message函数会拿到其数据再通过format转换, 其最重要的功能也是最核心的是能够直接将数据以json格式输出,当然数据是二进制的时候也依然是可以使用send,十分方便.

声明变量类型

索引 API 含义
1 new Int64(v) 定义一个有符号Int64类型的变量值为v,参数v可以是字符串或者以0x开头的的十六进制值
2 new UInt64(v) 定义一个无符号Int64类型的变量值为v,参数v可以是字符串或者以0x开头的的十六进制值
3 new NativePointer(s) 定义一个指针,指针地址为s
4 ptr(“0”) 定义一个指针

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function () {
console.log("");
console.log("new Int64(1):"+new Int64(1));
console.log("new UInt64(1):"+new UInt64(1));
console.log("new NativePointer(0xEC644071):"+new NativePointer(0xEC644071));
console.log("new ptr('0xEC644071'):"+new ptr(0xEC644071));
});
输出效果如下:
new Int64(1):1
new UInt64(1):1
new NativePointer(0xEC644071):0xec644071
new ptr('0xEC644071'):0xec644071

索引 API 含义
1 add(rhs)、sub(rhs)、and(rhs)、or(rhs)、xor(rhs) 加、减、逻辑运算
2 shr(N)、shl(n) 向右/向左移位n位生成新的Int64
3 Compare(Rhs) 返回整数比较结果
4 toNumber() 转换为数字
5 toString([radix=10]) 转换为可选基数的字符串(默认为10)

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
 Java.perform(function () {
console.log("");
console.log("new Int64(1):"+new Int64(1));
console.log("new UInt64(1):"+new UInt64(1));
console.log("new NativePointer(0xEC644071):"+new NativePointer(0xEC644071));
console.log("new ptr('0xEC644071'):"+new ptr(0xEC644071));
});
输出效果如下:
new Int64(1):1
new UInt64(1):1
new NativePointer(0xEC644071):0xec644071
new ptr('0xEC644071'):0xec644071

Process对象

Process.id

返回附加目标进程的PID

Process.isDebuggerAttached()

检测当前是否对目标程序已经附加

Process.enumerateModules()

枚举当前加载的模块,返回模块对象的数组。

Process.enumerateModules()会枚举当前所有已加载的so模块,并且返回了数组Module对象.

example:

在js中能够直接使用Process对象的所有api,调用了Process.enumerateModules()方法之后会返回一个数组,数组中存储N个叫Module的对象,for循环遍历,使用下标的方式调用了Module对象的name属性,name是so模块的名称

1
2
3
4
5
6
7
8
9
function frida_Process() {
Java.perform(function () {
var process_Obj_Module_Arr = Process.enumerateModules();
for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
console.log("",process_Obj_Module_Arr[i].name);
}
});
}
setImmediate(frida_Process,0);

Process.enumerateThreads()

枚举当前所有的线程,返回包含以下属性的对象数组:

索引 属性 含义
1 id 线程id
2 state 当前运行状态有running, stopped, waiting, uninterruptible or halted
3 context 带有键pc和sp的对象,它们是分别为ia32/x64/arm指定EIP/RIP/PC和ESP/RSP/SP的NativePointer对象。也可以使用其他处理器特定的密钥,例如eax、rax、r0、x0等。

examlpe:

1
2
3
4
5
6
7
8
9
10
11
12
function frida_Process() {
Java.perform(function () {
var enumerateThreads = Process.enumerateThreads();
for(var i = 0; i < enumerateThreads.length; i++) {
console.log("");
console.log("id:",enumerateThreads[i].id);
console.log("state:",enumerateThreads[i].state);
console.log("context:",JSON.stringify(enumerateThreads[i].context));
}
});
}
setImmediate(frida_Process,0);

Process.getCurrentThreadId()

获取此线程的操作系统特定 ID 作为数字

Module对象

Module对象的属性

索引 属性 含义
1 name 模块名称
2 base 模块地址,其变量类型为NativePointer
3 size 大小
4 path 完整文件系统路径

Module对象的API

索引 API 含义
1 Module.load() 加载指定so文件,返回一个Module对象
2 enumerateImports() 枚举所有Import库函数,返回Module数组对象
3 enumerateExports() 枚举所有Export库函数,返回Module数组对象
4 enumerateSymbols() 枚举所有Symbol库函数,返回Module数组对象
5 Module.findExportByName(exportName)、Module.getExportByName(exportName) 寻找指定so中export库中的函数地址
6 Module.findBaseAddress(name)、Module.getBaseAddress(name) 返回so的基地址

Module.load()

主要用于加载指定so文件,返回一个Module对象。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function frida_Module() {
Java.perform(function () {
//参数为so的名称 返回一个Module对象
const hooks = Module.load('libhello.so');
//输出
console.log("模块名称:",hooks.name);
console.log("模块地址:",hooks.base);
console.log("大小:",hooks.size);
console.log("文件系统路径",hooks.path);
});
}
setImmediate(frida_Module,0);

输出如下:
模块名称: libhello.so
模块地址: 0xdf2d3000
大小: 24576
文件系统路径 /data/app/com.roysue.roysueapplication-7adQZoYIyp5t3G5Ef5wevQ==/lib/arm/libhello.so

Process.EnumererateModules()

对上面的的Process.EnumererateModules()对象输出进行补全,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function frida_Module() {
Java.perform(function () {

var process_Obj_Module_Arr = Process.enumerateModules();
for(var i = 0; i < process_Obj_Module_Arr.length; i++) {
if(process_Obj_Module_Arr[i].path.indexOf("hello")!=-1)
{
console.log("模块名称:",process_Obj_Module_Arr[i].name);
console.log("模块地址:",process_Obj_Module_Arr[i].base);
console.log("大小:",process_Obj_Module_Arr[i].size);
console.log("文件系统路径",process_Obj_Module_Arr[i].path);
}
}
});
}
setImmediate(frida_Module,0);

输出如下:
模块名称: libhello.so
模块地址: 0xdf2d3000
大小: 24576
文件系统路径 /data/app/com.roysue.roysueapplication-7adQZoYIyp5t3G5Ef5wevQ==/lib/arm/libhello.so

enumerateImports()

该API会枚举模块中所有中的所有Import函数,示例代码如下。

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
function frida_Module() {
Java.perform(function () {
const hooks = Module.load('libhello.so');
var Imports = hooks.enumerateImports();
for(var i = 0; i < Imports.length; i++) {
//函数类型
console.log("type:",Imports[i].type);
//函数名称
console.log("name:",Imports[i].name);
//属于的模块
console.log("module:",Imports[i].module);
//函数地址
console.log("address:",Imports[i].address);
}
});
}
setImmediate(frida_Module,0);

输出如下:
[Google Pixel::com.roysue.roysueapplication]-> type: function
name: __cxa_atexit
module: /system/lib/libc.so
address: 0xf58f4521
type: function
name: __cxa_finalize
module: /system/lib/libc.so
address: 0xf58f462d
type: function
name: __stack_chk_fail
module: /system/lib/libc.so
address: 0xf58e2681
...

enumerateExports()

该API会枚举模块中所有中的所有Export函数,示例代码如下。

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 frida_Module() {
Java.perform(function () {
const hooks = Module.load('libhello.so');
var Exports = hooks.enumerateExports();
for(var i = 0; i < Exports.length; i++) {
//函数类型
console.log("type:",Exports[i].type);
//函数名称
console.log("name:",Exports[i].name);
//函数地址
console.log("address:",Exports[i].address);
}
});
}
setImmediate(frida_Module,0);

输出如下:
[Google Pixel::com.roysue.roysueapplication]-> type: function
name: Java_com_roysue_roysueapplication_hellojni_getSum
address: 0xdf2d411b
type: function
name: unw_save_vfp_as_X
address: 0xdf2d4c43
type: function
address: 0xdf2d4209
type: function
...

enumerateSymbols()

该API会枚举模块中所有中的符号表,示例代码如下。

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 frida_Module() {
Java.perform(function () {
const hooks = Module.load('libc.so');
var Symbol = hooks.enumerateSymbols();
for(var i = 0; i < Symbol.length; i++) {
console.log("isGlobal:",Symbol[i].isGlobal);
console.log("type:",Symbol[i].type);
console.log("section:",JSON.stringify(Symbol[i].section));
console.log("name:",Symbol[i].name);
console.log("address:",Symbol[i].address);
}
});
}
setImmediate(frida_Module,0);

输出如下:
isGlobal: true
type: function
section: {"id":"13.text","protection":"r-x"}
name: _Unwind_GetRegionStart
address: 0xf591c798
isGlobal: true
type: function
section: {"id":"13.text","protection":"r-x"}
name: _Unwind_GetTextRelBase
address: 0xf591c7cc
...

Module.findExportByName(exportName),Module.getExportByName(exportName)

返回so文件中Export函数库中函数名称为exportName函数的绝对地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
function frida_Module() {
Java.perform(function () {
Module.getExportByName('libhello.so', 'c_getStr')
console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.findExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));
console.log("Java_com_roysue_roysueapplication_hellojni_getStr address:",Module.getExportByName('libhello.so', 'Java_com_roysue_roysueapplication_hellojni_getStr'));
});
}
setImmediate(frida_Module,0);

输出如下:
Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d
Java_com_roysue_roysueapplication_hellojni_getStr address: 0xdf2d413d

Module.findBaseAddress(name)、Module.getBaseAddress(name)

返回name模块的基地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
function frida_Module() {
Java.perform(function () {
var name = "libhello.so";
console.log("so address:",Module.findBaseAddress(name));
console.log("so address:",Module.getBaseAddress(name));
});
}
setImmediate(frida_Module,0);

输出如下:
so address: 0xdf2d3000
so address: 0xdf2d3000

Memory对象

Memory的一些API通常是对内存处理

Memory.scan搜索内存数据

其主要功能是搜索内存中以address地址开始,搜索长度为size,需要搜是条件是pattern,callbacks搜索之后的回调函数;此函数相当于搜索内存的功能。

我们来直接看例子,然后结合例子讲解,如下图。

如果我想搜索在内存中112A地址的起始数据要怎么做,代码示例如下

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
function frida_Memory() {
Java.perform(function () {
//先获取so的module对象
var module = Process.findModuleByName("libhello.so");
//??是通配符
var pattern = "03 49 ?? 50 20 44";
//基址
console.log("base:"+module.base)
//从so的基址开始搜索,搜索大小为so文件的大小,搜指定条件03 49 ?? 50 20 44的数据
var res = Memory.scan(module.base, module.size, pattern, {
onMatch: function(address, size){
//搜索成功
console.log('搜索到 ' +pattern +" 地址是:"+ address.toString());
},
onError: function(reason){
//搜索失败
console.log('搜索失败');
},
onComplete: function()
{
//搜索完毕
console.log("搜索完毕")
}
});
});
}
setImmediate(frida_Memory,0);

onMatch:function(address,size):使用包含作为NativePointer的实例地址的address和指定大小为数字的size调用,此函数可能会返回字符串STOP以提前取消内存扫描。

onError:Function(Reason):当扫描时出现内存访问错误时使用原因调用。

onComplete:function():当内存范围已完全扫描时调用。

执行流程:搜索libhello.so文件在内存中的数据,搜索以pattern条件的在内存中能匹配的数据。搜索到之后根据回调函数返回数据。

我们来看看执行结果如下:

1
2
搜索到 03 49 ?? 50 20 44 地址是:0xdf2d412a
搜索完毕

我们要如何验证搜索到底是不是图1-5中112A地址,其实很简单。so的基址是0xdf2d3000,而搜到的地址是0xdf2d412a,我们只要df2d412a-df2d3000=112A。就是说我们已经搜索到了!

搜索内存数据Memory.scanSync

功能与Memory.scan一样,只不过它是返回多个匹配到条件的数据。
代码示例如下。

1
2
3
4
5
6
7
8
9
10
11
12
function frida_Memory() {
Java.perform(function () {
var module = Process.findModuleByName("libhello.so");
var pattern = "03 49 ?? 50 20 44";
var scanSync = Memory.scanSync(module.base, module.size, pattern);
console.log("scanSync:"+JSON.stringify(scanSync));
});
}
setImmediate(frida_Memory,0);

输出如下,可以看到地址搜索出来是一样的
scanSync:[{"address":"0xdf2d412a","size":6}]

内存分配Memory.alloc

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

使用案例如下

1
2
3
4
5
6
7
8
9
10
11
12
function frida_Memory() {
Java.perform(function () {
const r = Memory.alloc(10);
console.log(hexdump(r, {
offset: 0,
length: 10,
header: true,
ansi: false
}));
});
}
setImmediate(frida_Memory,0);

以上代码在目标进程中申请了10字节的空间~输出如下

1
2
3
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
e8142070 00 00 00 00 00 00 00 00 00 00 .ELF......

可以看到在0xdfe4cd40处申请了10个字节内存空间~

也可以使用:

Memory.allocUtf8String(str) 分配utf字符串

Memory.allocUtf16String 分配utf16字符串

Memory.allocAnsiString 分配ansi字符串

Memory.copy内存复制

如同c api memcp一样调用,使用案例如下。

从module.base中复制10个字节的内存到新年申请的r内

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 frida_Memory() {
Java.perform(function () {
//获取so模块的Module对象
var module = Process.findModuleByName("libhello.so");
//条件
var pattern = "03 49 ?? 50 20 44";
//搜字符串 只是为了将so的内存数据复制出来 方便演示~
var scanSync = Memory.scanSync(module.base, module.size, pattern);
//申请一个内存空间大小为10个字节
const r = Memory.alloc(10);
//复制以module.base地址开始的10个字节 那肯定会是7F 45 4C 46...因为一个ELF文件的Magic属性如此。
Memory.copy(r,module.base,10);
console.log(hexdump(r, {
offset: 0,
length: 10,
header: true,
ansi: false
}));
});
}
setImmediate(frida_Memory,0);

输出如下。
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
e8142070 7f 45 4c 46 01 01 01 00 00 00 .ELF......

写入内存Memory.writeByteArray

将字节数组写入一个指定内存,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function frida_Memory() {     
Java.perform(function () {
//定义需要写入的字节数组 这个字节数组是字符串"roysue"的十六进制
var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
//申请一个新的内存空间 返回指针 大小是arr.length
const r = Memory.alloc(arr.length);
//将arr数组写入R地址中
Memory.writeByteArray(r,arr);
//输出
console.log(hexdump(r, {
offset: 0,
length: arr.length,
header: true,
ansi: false
}));
});
}
setImmediate(frida_Memory,0);

输出如下。
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 72 6f 79 73 75 65 roysue

读取内存Memory.readByteArray

将一个指定地址的数据,代码示例如下:

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
function frida_Memory() {     
Java.perform(function () {
//定义需要写入的字节数组 这个字节数组是字符串"roysue"的十六进制
var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
//申请一个新的内存空间 返回指针 大小是arr.length
const r = Memory.alloc(arr.length);
//将arr数组写入R地址中
Memory.writeByteArray(r,arr);
//读取r指针,长度是arr.length 也就是会打印上面一样的值
var buffer = Memory.readByteArray(r, arr.length);
//输出
console.log("Memory.readByteArray:");
console.log(hexdump(buffer, {
offset: 0,
length: arr.length,
header: true,
ansi: false
}));
});
});
}
setImmediate(frida_Memory,0);

输出如下。
[Google Pixel::com.roysue.roysueapplication]-> Memory.readByteArray:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 72 6f 79 73 75 65 roysue

Java对象

Java.available

该函数一般用来判断当前进程是否加载了JavaVM,Dalvik或ART虚拟机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function frida_Java() {
Java.perform(function () {
//作为判断用
if(Java.available)
{
//注入的逻辑代码
console.log("hello java vm");
}else{
//未能正常加载JAVA VM
console.log("error");
}
});
}
setImmediate(frida_Java,0);

输出如下。
hello java vm

Java.androidVersion

显示android系统版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function frida_Java() {
Java.perform(function () {
//作为判断用
if(Java.available)
{
//注入的逻辑代码
console.log("",Java.androidVersion);
}else{
//未能正常加载JAVA VM
console.log("error");
}
});
}
setImmediate(frida_Java,0);

输出如下。
9
因为我的系统版本是9版本~

枚举类Java.enumerateLoadedClasses

该API枚举当前加载的所有类信息,它有一个回调函数分别是onMatch、onComplete函数,我们来看看代码示例以及效果!

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 frida_Java() {
Java.perform(function () {
if(Java.available)
{
//console.log("",Java.androidVersion);
//枚举当前加载的所有类
Java.enumerateLoadedClasses({
//每一次回调此函数时其参数className就是类的信息
onMatch: function (className)
{
//输出类字符串
console.log("",className);
},
//枚举完毕所有类之后的回调函数
onComplete: function ()
{
//输出类字符串
console.log("输出完毕");
}
});
}else{
console.log("error");
}
});
}
setImmediate(frida_Java,0);

枚举类加载器Java.enumerateLoadedClasses

该api枚举Java VM中存在的类加载器,其有一个回调函数,分别是onMatch: function (loader)与onComplete: function ()
它也有一个好兄弟叫Java.enumerateClassLoadersSync()也是返回的数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function frida_Java() {
Java.perform(function () {
if(Java.available)
{
//枚举当前加载的Java VM类加载器
Java.enumerateClassLoaders({
//回调函数,参数loader是类加载的信息
onMatch: function (loader)
{
console.log("",loader);
},
//枚举完毕所有类加载器之后的回调函数
onComplete: function ()
{
console.log("end");
}
});
}else{
console.log("error");
}
});
}
setImmediate(frida_Java,0);

附加调用Java.perform

Java.perform(fn)主要用于当前线程附加到Java VM并且调用fn方法
它也有一个好兄弟。Java.performNow(fn)~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function frida_Java() {
//运行当前js脚本时会对当前线程附加到Java VM虚拟机,并且执行function方法
Java.perform(function () {
//判断是否Java VM正常运行
if(Java.available)
{
//如不意外会直接输出 hello
console.log("hello");
}else{
console.log("error");
}
});
}
setImmediate(frida_Java,0);

输出如下。
[Google Pixel::com.roysue.roysueapplication]-> hello

获取类Java.use

Java.use(className),动态获取className的类定义,通过对其调用$new()来调用构造函数,可以从中实例化对象。当想要回收类时可以调用$Dispose()方法显式释放,当然也可以等待JavaScript的垃圾回收机制,当实例化一个对象之后,可以通过其实例对象调用类中的静态或非静态的方法,官方代码示例定义如下。

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function () {
//获取android.app.Activity类
var Activity = Java.use('android.app.Activity');
//获取java.lang.Exception类
var Exception = Java.use('java.lang.Exception');
//拦截Activity类的onResume方法
Activity.onResume.implementation = function () {
//调用onResume方法的时候,会在此处被拦截并且调用以下代码抛出异常!
throw Exception.$new('Oh noes!');
};
});

扫描实例类Java.choose

在堆上查找实例化的对象,示例代码如下!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(function () {
//查找android.view.View类在堆上的实例化对象
Java.choose("android.view.View", {
//枚举时调用
onMatch:function(instance){
//打印实例
console.log(instance);
},
//枚举完成后调用
onComplete:function() {
console.log("end")
}});
});

输出如下:
android.view.View{2292774 V.ED..... ......ID 0,1794-1080,1920 #1020030 android:id/navigationBarBackground}
android.view.View{d43549d V.ED..... ......ID 0,0-1080,63 #102002f android:id/statusBarBackground}
end

类型转换器Java.cast

Java.cast(handle, klass),就是将指定变量或者数据强制转换成你所有需要的类型;创建一个 JavaScript 包装器,给定从 Java.use() 返回的给定类klas的句柄的现有实例。此类包装器还具有用于获取其类的包装器的类属性,以及用于获取其类名的字符串表示的$className属性,通常在拦截so层时会使用此函数将jstring、jarray等等转换之后查看其值。

定义任意数组类型Java.array

frida提供了在js代码中定义java数组的api,该数组可以用于传递给java API,代码示例如下

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function () {
//定义一个int数组、值是1003, 1005, 1007
var intarr = Java.array('int', [ 1003, 1005, 1007 ]);
//定义一个byte数组、值是0x48, 0x65, 0x69
var bytearr = Java.array('byte', [ 0x48, 0x65, 0x69 ]);
for(var i=0;i<bytearr.length;i++)
{
//输出每个byte元素
console.log(bytearr[i])
}
});

我们通过上面定义int数组和byte的例子可以知道其定义格式为Java.array(‘type’,[value1,value2,….]);那它都支持type呢?

|

索引 type 含义
1 Z boolean
2 B byte
3 C char
4 S short
5 I int
6 J long
7 F float
8 D double
9 V void

注册类Java.registerClass(spec)

Java.registerClass:创建一个新的Java类并返回一个包装器,其中规范是一个包含:

name:指定类名称的字符串。

superClass:(可选)父类。要从 java.lang.Object 继承的省略。

implements:(可选)由此类实现的接口数组。

fields:(可选)对象,指定要公开的每个字段的名称和类型。

methods:(可选)对象,指定要实现的方法。

注册一个类,返回类的实例,实例化目标类对象并且调用类中的方法代码如下

1
2
3
4
5
6
7
Java.perform(function () {
//注册一个目标进程中的类,返回的是一个类对象
var hellojni = Java.registerClass({
name: 'com.roysue.roysueapplication.hellojni'
});
console.log(hellojni.addInt(1,2));
});

官方案例:

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
50
51
52
53
54
//获取目标进程的SomeBaseClass类
var SomeBaseClass = Java.use('com.example.SomeBaseClass');
//获取目标进程的X509TrustManager类
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');

var MyWeirdTrustManager = Java.registerClass({
//注册一个类是进程中的MyWeirdTrustManager类
name: 'com.example.MyWeirdTrustManager',
//父类是SomeBaseClass类
superClass: SomeBaseClass,
//实现了MyWeirdTrustManager接口类
implements: [X509TrustManager],
//类中的属性
fields: {
description: 'java.lang.String',
limit: 'int',
},
//定义的方法
methods: {
//类的构造函数
$init: function () {
console.log('Constructor called');
},
//X509TrustManager接口中方法之一,该方法作用是检查客户端的证书
checkClientTrusted: function (chain, authType) {
console.log('checkClientTrusted');
},
//该方法检查服务器的证书,不信任时。在这里通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
checkServerTrusted: [{
//返回值类型
returnType: 'void',
//参数列表
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String'],
//实现方法
implementation: function (chain, authType) {
//输出
console.log('checkServerTrusted A');
}
}, {
returnType: 'java.util.List',
argumentTypes: ['[Ljava.security.cert.X509Certificate;', 'java.lang.String', 'java.lang.String'],
implementation: function (chain, authType, host) {
console.log('checkServerTrusted B');
//返回null会信任所有证书
return null;
}
}],
// 返回受信任的X509证书数组。
getAcceptedIssuers: function () {
console.log('getAcceptedIssuers');
return [];
},
}
});

主要实现了证书类的javax.net.ssl.X509TrustManager类,,这里就是相当于自己在目标进程中重新创建了一个类,实现了自己想要实现的类构造,重构造了其中的三个接口函数、从而绕过证书校验

Java.vm对象

Java.vm对象十分常用,比如想要拿到JNI层的JNIEnv对象,可以使用getEnv();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function frida_Java() {     
Java.perform(function () {
//拦截getStr函数
Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getStr"), {
onEnter: function(args) {
console.log("getStr");
},
onLeave:function(retval){
//它的返回值的是retval 在jni层getStr的返回值的jstring
//我们在这里做的事情就是替换掉结果
//先获取一个Env对象
var env = Java.vm.getEnv();
//通过newStringUtf方法构建一个jstirng字符串
var jstring = env.newStringUtf('roysue');
//replace替换掉结果
retval.replace(jstring);
console.log("getSum方法返回值为:roysue")
}
});
}
setImmediate(frida_Java,0);

Interceptor对象

该对象功能十分强大,函数原型是Interceptor.attach(target, callbacks):

参数target是需要拦截的位置的函数地址,也就是填某个so层函数的地址即可对其拦截

target是一个NativePointer参数,用来指定你想要拦截的函数的地址,NativePointer我们也学过是一个指针。

需要注意的是对于Thumb函数需要对函数地址+1,callbacks则是它的回调函数,分别是以下两个回调函数:

Interceptor.attach

onEnter:函数(args):回调函数,给定一个参数args,可用于读取或写入参数作为 NativePointer 对象的数组。

onLeave:函数(retval):回调函数给定一个参数 retval,该参数是包含原始返回值的 NativePointer 派生对象。可以调用 retval.replace(0) 以整数 0 替换返回值,或者调用 retval.replace(ptr(”0x1234”))以替换为指针。

请注意,此对象在 OnLeave调用中回收,因此不要将其存储在回调之外并使用它。如果需要存储包含的值,请制作深副本,例如:ptr(retval.toString())。

1
2
3
4
5
6
7
8
9
10
11
12
13
//使用Module对象getExportByNameAPI直接获取libc.so中的导出函数read的地址,对read函数进行附加拦截
Interceptor.attach(Module.getExportByName('libc.so', 'read'), {
//每次read函数调用的时候会执行onEnter回调函数
onEnter: function (args) {
this.fileDescriptor = args[0].toInt32();
},
//read函数执行完成之后会执行onLeave回调函数
onLeave: function (retval) {
if (retval.toInt32() > 0) {
/* do something with this.fileDescriptor */
}
}
});

通过我们对Interceptor.attach函数有一些基本了解了~它还包含一些属性。

索引 属性 含义
1 returnAddress 返回地址,类型是NativePointer
2 context 上下文:具有键pc和sp的对象,它们是分别为ia32/x64/arm指定EIP/RIP/PC和ESP/RSP/SP的NativePointer对象。其他处理器特定的键也可用,例如eax、rax、r0、x0等。也可以通过分配给这些键来更新寄存器值。
3 errno 当前errno值
4 lastError 当前操作系统错误值
5 threadId 操作系统线程ID
6 depth 相对于其他调用的调用深度
我们来看看示例代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function frida_Interceptor() {
Java.perform(function () {
//对So层的导出函数getSum进行拦截
Interceptor.attach(Module.findExportByName("libhello.so" , "Java_com_roysue_roysueapplication_hellojni_getSum"), {
onEnter: function(args) {
//输出
console.log('Context information:');
//输出上下文因其是一个Objection对象,需要它进行接送、转换才能正常看到值
console.log('Context : ' + JSON.stringify(this.context));
//输出返回地址
console.log('Return : ' + this.returnAddress);
//输出线程id
console.log('ThreadId : ' + this.threadId);
console.log('Depth : ' + this.depth);
console.log('Errornr : ' + this.err);
},
onLeave:function(retval){
}
});
});
}
setImmediate(frida_Interceptor,0);

Interceptor.detachAll

作用就是让之前所有的Interceptor.attach附加拦截的回调函数失效。

Interceptor.replace

相当于替换掉原本的函数,用替换时的实现替换目标处的函数。如果想要完全或部分替换现有函数的实现,则通常使用此函数。,我们也看例子,例子是最直观的!代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function frida_Interceptor() {
Java.perform(function () {
//这个c_getSum方法有两个int参数、返回结果为两个参数相加
//这里用NativeFunction函数自己定义了一个c_getSum函数
var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'),
'int',['int','int']);
//输出结果 那结果肯定就是 3
console.log("result:",add_method(1,2));
//这里对原函数的功能进行替换实现
Interceptor.replace(add_method, new NativeCallback(function (a, b) {
//h不论是什么参数都返回123
return 123;
}, 'int', ['int', 'int']));
//再次调用 则返回123
console.log("result:",add_method(1,2));
});
}

NativePointer对象

同等与C语言中的指针

new NativePointer(s)

声明定义NativePointer类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function frida_NativePointer() {
Java.perform(function () {
//第一种字符串定义方式 十进制的100 输出为十六进制0x64
const ptr1 = new NativePointer("100");
console.log("ptr1:",ptr1);
//第二种字符串定义方式 直接定义0x64 同等与定义十六进制的64
const ptr2 = new NativePointer("0x64");
console.log("ptr2:",ptr2);
//第三种定数值义方式 定义数字int类型 十进制的100 是0x64
const ptr3 = new NativePointer(100);
console.log("ptr3:",ptr3);
});
}
setImmediate(frida_NativePointer,0);

输出如下,都会自动转为十六进制的0x64
ptr1: 0x64
ptr2: 0x64
ptr3: 0x64

运算符以及指针读写API

它也能调用以下运算符

看完API含义之后,我们来使用他们,下面该脚本是readByteArray()示例~

1
2
3
4
5
6
7
8
9
10
11
12
13
function frida_NativePointer() {
Java.perform(function () {
console.log("");
//拿到libc.so在内存中的地址
var pointer = Process.findModuleByName("libc.so").base;
//读取从pointer地址开始的16个字节
console.log(pointer.readByteArray(0x10));
});
}
setImmediate(frida_NativePointer,0);
输出如下:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............

首先我先来用readByteArray函数来读取libc.so文件在内存中的数据,这样我们方便测试,我们从libc文件读取0x10个字节的长度,肯定会是7F 45 4C 46…因为ELF文件头部信息中的Magic属性。

readPointer()

从此内存位置读取NativePointer,示例代码如下。省略function以及Java.perform~

1
2
3
4
5
var pointer = Process.findModuleByName("libc.so").base;
console.log(pointer.readByteArray(0x10));
console.log("readPointer():"+pointer.readPointer());
输出如下。
readPointer():0x464c457f

也就是将readPointer的前四个字节的内容转成地址产生一个新的NativePointer。

writePointer(ptr)

读取ptr指针地址到当前指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
     //先打印pointer指针地址
console.log("pointer :"+pointer);
//分配四个字节的空间地址
const r = Memory.alloc(4);
//将pointer指针写入刚刚申请的r内
r.writePointer(pointer);
//读取r指针的数据
var buffer = Memory.readByteArray(r, 4);
//r指针内放的pointer指针地址
console.log(buffer);

输出如下。
//console.log("pointer :"+pointer); 这句打印的地址 也就是libc的地址
pointer :0xf588f000
//console.log(buffer); 输出buffer 0xf588f000在内存数据会以00 f0 88 f5方式显示
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 00 f0 88 f5 ....

readS32()、readU32()

从该内存位置读取有符号或无符号8/16/32/etc或浮点数/双精度值,并将其作为数字返回。这里拿readS32()、readU32()作为演示.

1
2
3
4
5
6
7
8
9
10
11
   //从pointer地址读4个字节 有符号
console.log(pointer.readS32());
//从pointer地址读4个字节 无符号
console.log(pointer.readU32());


输出如下。
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
1179403647 == 0x464c457f
1179403647 == 0x464c457f

writeS32()、writeU32()

1
2
3
4
5
6
7
8
9
10
    //申请四个字节的内存空间
const r = Memory.alloc(4);
//将0x12345678写入r地址中
r.writeS32(0x12345678);
//输出
console.log(r.readByteArray(0x10));
// writeS32()、writeU32()输出的也是一样的,只是区别是有符号和无符号
输出如下。
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 78 56 34 12 00 00 00 00 00 00 00 00 00 00 00 00 xV4.............

readByteArray(length))、writeByteArray(bytes)

readByteArray(length))连续读取内存length个字节,、writeByteArray连续写入内存bytes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    //先定义一个需要写入的字节数组
var arr = [ 0x72, 0x6F, 0x79, 0x73, 0x75, 0x65];
//这里申请以arr大小的内存空间
const r = Memory.alloc(arr.length);
//将arr数组字节写入r
Memory.writeByteArray(r,arr);
//读取arr.length大小的数组
var buffer = Memory.readByteArray(r, arr.length);
console.log("Memory.readByteArray:");
console.log(hexdump(buffer, {
offset: 0,
length: arr.length,
header: true,
ansi: false
}));

输出如下。
Memory.readByteArray:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 72 6f 79 73 75 65 roysue

readCString([size = -1])、writeUtf8String(str)

readCString功能是读取指针地址位置的字节字符串,对应的writeUtf8String是写入指针地址位置的字符串处。(这里的r是接着上面的代码的变量)。

1
2
3
4
5
6
//在这里直接使用readCString读取会把上面的'roysue'字符串读取出来
console.log("readCString():"+r.readCString());
//这里是写入字符串 也就是 roysue起始位置开始被替换为haha
const newPtrstr = r.writeUtf8String("haha");
//替换完了之后再继续输出 必然是haha
console.log("readCString():"+newPtrstr.readCString());

NativeFunction对象

创建新的NativeFunction以调用address处的函数(用NativePointer指定),其中rereturn Type指定返回类型,argTypes数组指定参数类型。如果不是系统默认值,还可以选择指定ABI。对于可变函数,添加一个‘.’固定参数和可变参数之间的argTypes条目,我们来看看官方的例子。

1
2
3
4
5
6
7
8
// LargeObject HandyClass::friendlyFunctionName();
//创建friendlyFunctionPtr地址的函数
var friendlyFunctionName = new NativeFunction(friendlyFunctionPtr,
'void', ['pointer', 'pointer']);
//申请内存空间
var returnValue = Memory.alloc(sizeOfLargeObject);
//调用friendlyFunctionName函数
friendlyFunctionName(returnValue, thisPtr);

我来看看它的格式,函数定义格式为new NativeFunction(address, returnType, argTypes[, options]),参照这个格式能够创建函数并且调用!returnType和argTypes[,]分别可以填void、pointer、int、uint、long、ulong、char、uchar、float、double、int8、uint8、int16、uint16、int32、uint32、int64、uint64这些类型,根据函数的所需要的type来定义即可。

在定义的时候必须要将参数类型个数和参数类型以及返回值完全匹配,假设有三个参数都是int,则new NativeFunction(address, returnType, [‘int’, ‘int’, ‘int’]),而返回值是int则new NativeFunction(address, ‘int’, argTypes[, options]),必须要全部匹配,并且第一个参数一定要是函数地址指针。

NativeCallback对象

new NativeCallback(func,rereturn Type,argTypes[,ABI]):创建一个由JavaScript函数func实现的新NativeCallback,其中rereturn Type指定返回类型,argTypes数组指定参数类型。

您还可以指定ABI(如果不是系统默认值)。

有关支持的类型和Abis的详细信息,请参见NativeFunction。注意,返回的对象也是一个NativePointer,因此可以传递给Interceptor#replace。当将产生的回调与Interceptor.replace()一起使用时,将调用func,并将其绑定到具有一些有用属性的对象,就像Interceptor.Attach()中的那样。我们来看一个例子。如下,利用NativeCallback做一个函数替换。

1
2
3
4
5
6
7
8
9
10
Java.perform(function () {
var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'),
'int',['int','int']);
console.log("result:",add_method(1,2));
//在这里new一个新的函数,但是参数的个数和返回值必须对应
Interceptor.replace(add_method, new NativeCallback(function (a, b) {
return 123;
}, 'int', ['int', 'int']));
console.log("result:",add_method(1,2));
});

HOOK

HOOK java层方法模版

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
import frida, sys

#接收js脚本send方法传入的数据
#@message数据类型为字典
#{'type':'send','payload':'js脚本发送的数据'}
#@data数据类型为byte
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

jscode = """

Java.perform(function () {
var MainActivity = Java.use('类名');
MainActivity.方法.implementation = function (入参) {
//打印入参数据
console.log(入参)
var result=this.方法(入参)
//打印出参
console.log(出参)
//数据返回
return result
};
});
"""
#重启应用并附加到运行中的应用的进程中
#pid=frida.get_usb_device().spawn(['包名'])
#process = frida.get_usb_device().attach(pid)

#附加到运行中的应用的进程中
process = frida.get_usb_device().attach("HOOK包名")
#创建js脚本
script = process.create_script(jscode)
#注入js脚本
script.on('message', on_message)
print('[*] Running')
script.load()
sys.stdin.read()

HOOK native层方法模版

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
import frida, sys

#接收js脚本send方法传入的数据
#@message数据类型为字典
#{'type':'send','payload':'js脚本发送的数据'}
#@data数据类型为byte
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

jscode = """

setImmediate(function() {
#Interceptor.attach方法需要两个参数 1.NativePointer指针 2.回调方
Interceptor.attach(Module.findExportByName(".so文件" , "导出方法"), {
onEnter: function(args) {

send('此消息发送给on_message方法');
},
onLeave:function(retval){

}
});

});

"""

#重启应用并附加到运行中的应用的进程中
#pid=frida.get_usb_device().spawn(['包名'])
#process = frida.get_usb_device().attach(pid)

#附加到运行中的应用的进程中
process = frida.get_usb_device().attach("HOOK包名")
#创建js脚本
script = process.create_script(jscode)
#注入js脚本
script.on('message', on_message)
print('[*] Running')
script.load()
sys.stdin.read()

HOOK重载方法

1. apply arguments

1
2
3
MyClass.MyFunc.overload("java.util.List").implementation = function() {
this.MyFunc.overload("java.util.List").apply(this, arguments);
}

2. argments下标

1
2
3
MyClass.MyFunc.overload("java.util.List").implementation = function () {
this.MyFunc(arguments[0]);
};

3. 使用具体参数

1
2
3
MyClass.MyFuncs.overload("int", "int").implementation = function (s1, s2) {
var ret = this.MyFuncs(s1, s2);
}

4. call

1
2
3
4
5
6
7
8
9
10
11
12
13
var Handler = classFactory.use("android.os.Handler");
var Looper = classFactory.use("android.os.Looper");

var looper = Looper.getMainLooper();
var handler = Handler.$new.overload("android.os.Looper").call(Handler, looper);


MyClass.MyFunc.overload("java.lang.String;").implementation = {
this.MyFunc.overload("java.lang.String").call(this, args[1])
MyClass.MyFunc.overload("java.lang.String").call()
}


拦截系统fopen函数

1
FILE *fopen(const char *filename, const char *mode)

使用给定的模式 mode 打开 filename 所指向的文件。文件如果打开成功,会返回一个指针,相当于句柄,如果文件打开失败则返回 0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Interceptor.attach(Module.findExportByName(null, "fopen"), {

onEnter: function(args) {
if (!args[0].isNull()){
var path = args[0].readUtf8String();
console.log("fopen " + path);
}

},
onLeave: function(retval) {

retval.replace(0); //将返回值替换成0

},
})

拦截自定义函数

自定义一个 getEncryptKey 函数,返回的参数是一个字符串指针,我们在 onLeave 函数中添加下面的代码,分配内存并填充字符串 123456,最终替换返回值

1
2
3
4
5
6
7
8
9
10
11
Interceptor.attach(Module.findExportByName(null, "getEncryptKey"), {

onLeave: function(retval) {

console.log("getEncryptKey onLeave");
console.log("[*] retval:" + retval.readUtf8String());
var string = Memory.allocUtf8String("1234567"); //分配内存并填充数据
retval.replace(string); //替换

},
})

修改C函数执行流程

以修改open方法为例

1
2
3
4
5
6
7
8
9
var openPtr = Module.getExportByName(null, 'open');  //获取open方法地址
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']); //创建方法
Interceptor.replace(openPtr, new NativeCallback(function (pathPtr, flags) {
var path = pathPtr.readUtf8String();
console.log('[*]path: ' + path);
var fd = open(pathPtr, flags);
console.log('[*]fd:' + fd);
return fd;
}, 'int', ['pointer', 'int'])); //替换方法

以修改fopen方法为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var fopenAddr = Module.findExportByName("libc.so","fopen");

//FILE* fopen(const char* __path, const char* __mode);

var func_c_fopen = new NativeFunction(fopenAddr,"pointer",["pointer","pointer"]);

Interceptor.replace(fopenAddr,new NativeCallback(function (path,mode) {

console.log("替换");
console.error(path,Memory.readCString(path),"mode:",Memory.readCString(mode));

var newPtr = Memory.allocUtf8String("/data/local/tmp/js/app.js");

return func_c_fopen(newPtr,mode)

},"pointer",["pointer","pointer"]));

拦截 sub_xxxx 函数

通过指针去HOOK,首先通过IDA获取到方法的偏移地址,也就是sub后面的十六进制

1
2
3
4
5
//如果thumb指令需要将偏移地址+1
Interceptor.attach(Module.findBaseAddress("libc.so").add(0x1A45), {
onEnter: function (args) {
}
});

HOOK OC

1. 拦截自定义方法

1
2
3
4
5
6
7
var method = ObjC.classes.类['+/- 方法名'];
var origImp = method.implementation;
method.implementation = ObjC.implement(method, function(self,selector){

return origImp(self,selector);

});

2. 拦截NSString

1
frida-trace -U -m "+[NSURL URLWithString:]" xxx

frida会自动在当前文件夹__handlers__ 下生成js脚本,编辑并填写以下代码

1
2
3
4
5
6
onEnter: function (log, args, state) {
log("+[NSURL URLWithString:" + args[2] + "]");
var objcNSString = ObjC.Object(args[2]); //将指针转为Objective-C 对象 NSString
var strObjcNSString = objcNSString.UTF8String();
log("+[NSURL URLWithString] str: " + strObjcNSString);
}

3.拦截NSMutableURLRequest

1
frida-trace -U -m "-[NSURLRequest setHTTPBody:]" -m "-[NSMutableURLRequest setHTTPBody:]" xxx

找到 NSMutableURLRequest_setHTTPBody.js 修改成如下代码,可以看到调用 NSData 的 bytes 函数得到内存地址,然后再调用 readUtf8String 读取内存中的数据,即可得到字符串。

1
2
3
4
5
6
onEnter: function (log, args, state) {
log("-[NSMutableURLRequest setHTTPBody:" + args[2] + "]");
var objcData = ObjC.Object(args[2]); //NSData
var strBody = objcData.bytes().readUtf8String(objcData.length()); //NSData 转换成 string
log("[NSMutableURLRequest setHTTPBody data: " + strBody);
},

4. 遍历 NSArray

1
2
3
4
5
var array = new ObjC.Object(args[2]);
var count = array.count().valueOf();
for (var i = 0; i !== count; i++) {
var element = array.objectAtIndex_(i);
}

5. 遍历 NSDictionary

1
2
3
4
5
6
var dict = new ObjC.Object(args[2]);
var enumerator = dict.keyEnumerator();
var key;
while ((key = enumerator.nextObject()) !== null) {
var value = dict.objectForKey_(key);
}

6. 通配符跟踪方法

1
frida-trace -U -m "-[Home* *]" Twitter

7. 跟踪CCCrypt

1
frida-trace -U -i CCCrypt xxx
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
{
onEnter: function (log, args, state) {
/**
operation = args[0]
CCAlgorithm = args[1]
CCOptions = args[2]
keyBytes = args[3]
keyLength = args[4]
ivBuffer = args[5]
inBuffer = args[6]
inLength = args[7]
outBuffer = args[8]
outLength = args[9]
outCountPtr = args[10]
*/
if (this.operation == 0) { //0代表加密
//解密前明文
console.log("input:")
console.log(hexdump(ptr(args[6]), {
length: args[7].toInt32(),
header: true,
ansi: true
}))
//密钥
console.log("Key: ")
console.log(hexdump(ptr(args[3]), {
length: args[4].toInt32(),
header: true,
ansi: true
}))
//IV
console.log("IV: ")
console.log(hexdump(ptr( args[5]), {
length: args[4].toInt32(),
header: true,
ansi: true
}))
}
}
}

拦截URWithString

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
var className = "NSURL";
var funcName = "+ URLWithString:";
var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
Interceptor.attach(hook.implementation, {


onEnter: function(args){

var className = ObjC.Object(args[0]);
var methodName = args[1];
var urlString = ObjC.Object(args[2]);

console.log("className: " + className.toString());
console.log("methodName: " + methodName.readUtf8String());
console.log("urlString: " + urlString.toString());
console.log("-----------------------------------------");

urlString = ObjC.classes.NSString.stringWithString_("http://www.baidu.com")
console.log("newUrlString: " + urlString.toString());
console.log("-----------------------------------------");

},

onLeave: function(retval) {

console.log("[*] Class Name: " + className);
console.log("[*] Method Name: " + funcName);
console.log("\t[-] Type of return value: " + typeof retval);
console.log("\t[-] Original Return Value: " + retval);
}
});

有时候被拦截的函数调用的次数较多,打印的信息也会较多,我们需要保存成文件

1
2
3
4
var file = new File("/var/mobile/log.txt","a+");//a+表示追加内容,和c语言的fopen函数模式类似
file.write("logInfo");
file.flush();
file.close();

8. API查找器和拦截器的组合使用实现批量HOOK

API查找器支持对 Objective-C 方法和 C 函数的查找,比如我们来写一个找到 NSFileManager 这个类名下的所有方法。新建一个 ApiResolver,如果查找 Objective-C,ApiResolver 参数中填写 objc,如果是查找 C 函数,ApiResolver 参数中填写 module,然后调用 enumerateMatches 枚举函数,每次枚举到一个函数会调用一次 onMatch,回调参数 match 包含 name 和 address 两个属性,分别代表名称和地址。整个枚举过程完成之后会调用 onComplete,代码如下:

1
2
3
4
5
6
7
8
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[NSFileManager *]', {
onMatch: function(match) {
console.log(match['name'] + ":" + match['address']);
},
onComplete: function() {}
});

附加 Safari 浏览器并加载脚本,会打印出 NSFileManager 所有的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
frida -U -l hook_objc.js "Safari 浏览器"
-[NSFileManager dealloc]:0x1e2324034
-[NSFileManager setDelegate:]:0x1e23b2744
-[NSFileManager delegate]:0x1e2333ab8
-[NSFileManager _info]:0x1e2333ae0
-[NSFileManager removeItemAtURL:error:]:0x1e235583c
-[NSFileManager copyItemAtURL:toURL:error:]:0x1e23b2ab0
-[NSFileManager fileExistsAtPath:isDirectory:]:0x1e2301d50
-[NSFileManager fileExistsAtPath:]:0x1e22e5398
-[NSFileManager setAttributes:ofItemAtPath:error:]:0x1e23180bc
-[NSFileManager removeItemAtPath:error:]:0x1e23336bc
-[NSFileManager enumeratorAtPath:]:0x1e2326024
-[NSFileManager isWritableFileAtPath:]:0x1e23240dc
-[NSFileManager moveItemAtPath:toPath:uniquePath:error:]:0x1ec6f4ec4
-[NSFileManager copyItemAtPath:toPath:uniquePath:error:]:0x1ec6f4ed4
......

得到方法名称之后,我们可以把感兴趣的找出来,然后调用拦截器(Interceptor)进行 Hook ,比如我们需要做一个简易的文件监控的功能,监控某个进程操作了哪些文件,文件操作的相关的方法会调用 removeItemAtPath、moveItemAtPath、copyItemAtPath、createFileAtPath、createDirectoryAtPath、enumeratorAtPath、contentsOfDirectoryAtPath,分别代表删除文件、移动文件、复制文件、创建文件、创建目录、枚举目录(包括子目录),枚举目录(不包括子目录),具体代码如下:

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
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[NSFileManager *]', {
onMatch: function(match) {
var method = match['name'];
var implementation = match['address'];

// 过滤需要拦截的方法
if (//(method.indexOf("fileExistsAtPath") != -1)
(method.indexOf("removeItemAtPath") != -1)
|| (method.indexOf("moveItemAtPath") != -1)
|| (method.indexOf("copyItemAtPath") != -1)
|| (method.indexOf("createFileAtPath") != -1)
|| (method.indexOf("createDirectoryAtPath") != -1)
|| (method.indexOf("enumeratorAtPath") != -1)
|| (method.indexOf("contentsOfDirectoryAtPath") != -1)) {

console.log(match['name'] + ":" + match['address']);
try {
Interceptor.attach(implementation, {
onEnter: function(args) {

var className = ObjC.Object(args[0]);
var methodName = args[1];
var filePath = ObjC.Object(args[2]);

console.log("className: " + className.toString());
console.log("methodName: " + methodName.readUtf8String());
console.log("filePath: " + filePath.toString());

},
onLeave: function(retval) {

}
});
} catch (err) {
console.log("[!] Exception: " + err.message);
}
}

},
onComplete: function() {
}
});

附加 SpringBoard 并加载脚本,然后我们操作卸载某个应用,此时会打印出相关的文件操作的路径,从下面打印的信息中我们可以看到卸载某个应用时,SpringBoard 进程对文件的操作过程,会删除 /var/mobile/Library/UserNotifications 和应用的沙盒目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
frida -U -l hook_objc.js SpringBoard
[iPhone::SpringBoard]->
className: <NSFileManager: 0x282954250>
methodName: removeItemAtPath:error:
filePath: /var/mobile/Library/UserNotifications/com.360buy.jdmobile/Attachments
className: <NSFileManager: 0x282954250>
methodName: filesystemItemRemoveOperation:shouldRemoveItemAtPath:
filePath: <NSFilesystemItemRemoveOperation: 0x283674f40>
className: <NSFileManager: 0x282954250>
methodName: enumeratorAtPath:
filePath: /private/var/mobile/Containers/Data/Application/C8A23CD4-6857-4BF4-876E-7FD104B0E799/Library/Caches/Snapshots/com.360buy.jdmobile
className: <NSFileManager: 0x282954250>
methodName: removeItemAtPath:error:
filePath: /private/var/mobile/Containers/Data/Application/C8A23CD4-6857-4BF4-876E-7FD104B0E799/Library/Caches/Snapshots/com.360buy.jdmobile
className: <NSFileManager: 0x282954250>
methodName: filesystemItemRemoveOperation:shouldRemoveItemAtPath:
filePath: <NSFilesystemItemRemoveOperation: 0x283677e80>
className: <NSFileManager: 0x282954250>
methodName: removeItemAtPath:error:
filePath: /private/var/mobile/Containers/Data/Application/C8A23CD4-6857-4BF4-876E-7FD104B0E799/Library/Caches/Snapshots
className: <NSFileManager: 0x282954250>
methodName: filesystemItemRemoveOperation:shouldRemoveItemAtPath:
filePath: <NSFilesystemItemRemoveOperation: 0x283677e80>
.......

比如需要拦截 NSFileManager 类的所有方法,但是排除某些方法,代码可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
var resolver = new ApiResolver('objc');
resolver.enumerateMatches('*[NSFileManager *]', {
onMatch: function(match) {
var method = match['name'];
var implementation = match['address'];

// 过滤不需要拦截的方法,其他方法全部拦截
if (method.indexOf("dealloc") == -1) {
//进行 Hook
//......
}
},
});

接下来我们来学习 API 查找器如何查找 C 函数,有 3 种方式,格式参考如下:

1
format is: exports:*!open*, exports:libc.so!* or imports:notepad.exe!*

比如我们查找 fopen 函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
var resolver = new ApiResolver('module');
resolver.enumerateMatches('exports:*!fopen*', {
onMatch: function(match) {

var name = match['name'];
var address = match['address'];
console.log(name + ":" + address);

},
onComplete: function() {}
});

附加 SpringBoard 加载脚本之后,打印如下信息,名称包括了动态库的路径,此时得到函数地址,就可以调用拦截器进行 Hook。

1
2
3
4
5
6
7
8
frida -U -l hook_objc.js SpringBoard
[iPhone::SpringBoard]->
/usr/lib/libSystem.B.dylib!fopen:0x1e145d4d0
/usr/lib/libSystem.B.dylib!fopen$DARWIN_EXTSN:0x1e1495d04
/usr/lib/system/libsystem_c.dylib!fopen$DARWIN_EXTSN:0x1e1495d04
/usr/lib/system/libsystem_c.dylib!fopen:0x1e145d4d0
/usr/lib/libSystem.B.dylib!fopen:0x1e145d4d0
/usr/lib/libSystem.B.dylib!fopen$DARWIN_EXTSN:0x1e1495d04

Objective-C 除了调用拦截器进行 Hook,还可以使用替换 implementation 的方式,类似于苹果提供的一套 Method Swizzling Hook 的方式,比如 Hook +[NSURL URLWithString:] 的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var method = ObjC.classes.NSURL['+ URLWithString:'];
var origImp = method.implementation;
method.implementation = ObjC.implement(method, function (self, sel, url){

console.log("+ [NSURL URLWithString:]");
var urlString = ObjC.Object(url);
console.log("url: " + urlString.toString());

return origImp(self, sel, url); //调用原方法,如果不调用则原方法得不到执行

//替换参数,将 URL 替换成 http://www.ioshacker.net
//var newUrl = ObjC.classes.NSString.stringWithString_("http://www.ioshacker.net");
//return origImp(self, sel, newUrl);

});

9. NSData与BASE64互转

NSData转BASE64

1
2
3
4
5
var NSData = new ObjC.Object(args[3]);
var BASE64NSData = NSData['- base64EncodedDataWithOptions:'].call(NSData,0)
//NSString *baseString = [[NSString alloc]initWithData:base64Data encoding:NSUTF8StringEncoding];
var NSString = ObjC.classes.NSString.alloc();
var ret =NSString['- initWithData:encoding:'].call(NSString,BASE64NSData,4);

BASE64转NSData

1
2
3
4
5
var base64String="/9j/4AAQSkZJR"
var BASE64NSString = ObjC.classes.NSString.stringWithString_(base64String);
var NSData = ObjC.classes.NSData.alloc();
//NSData *data = [[NSData alloc]initWithBase64EncodedString:BASE64NSString options:NSDataBase64DecodingIgnoreUnknownCharacters];
var ret = NSData['- initWithBase64EncodedString:options:'].call(NSData,BASE64NSString,1);

10.修改NSDictionary数据

假如NSDictionary数据为

1
{"status":"fail","code":500}

修改code字段500为200

1
2
3
4
5
6
//result为传入的NSDictionary对象
var NSMutableDictionaryResult = ObjC.classes.NSMutableDictionary.dictionaryWithDictionary_(result);//转为NSMutableDictionary
var code = ObjC.classes.NSString.stringWithString_("code");
var codeNum = ObjC.classes.NSString.stringWithString_("200");
NSMutableDictionaryResult.setValue_forKey_(codeNum,code); //更改500->200
console.log(NSMutableDictionaryResult)

11.UIImage图片与Base64互转

OC实现

1
2
3
4
5
6
7
8
9
//UIImage图片转成Base64字符串:
UIImage *originImage = [UIImage imageNamed:@"originImage.png"];
NSData *data = UIImageJPEGRepresentation(originImage, 1.0f);
NSString *encodedImageStr = [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];

//Base64字符串转UIImage图片:
NSData *decodedImageData = [[NSData alloc]
initWithBase64EncodedString:encodedImageStr options:NSDataBase64DecodingIgnoreUnknownCharacters];
UIImage *decodedImage = [UIImage imageWithData:decodedImageData];

UIImage转Base64

1
2
3
4
5
6
var UIImageJPEGRepresentation = new NativeFunction(Module.findExportByName(null,"UIImageJPEGRepresentation"),"pointer",["pointer","float"]);
var uiimage = ""; //uiimage对象
var NSData = new ObjC.Object(UIImageJPEGRepresentation(uiimage,1.0));
var BASE64NSData = NSData['- base64EncodedDataWithOptions:'].call(NSData,0)
var NSString = ObjC.classes.NSString.alloc();
var ret =NSString['- initWithData:encoding:'].call(NSString,BASE64NSData,4);

Base64转UIImage

1
2
3
4
5
var base64String = ObjC.classes.NSString.stringWithString_("/9j/4AAQ");
var BASE64NSString = ObjC.classes.NSString.stringWithString_(base64String);
var NSData = ObjC.classes.NSData.alloc();
var data = NSData['- initWithBase64EncodedString:options:'].call(NSData,BASE64NSString,1);
var uiimage = ObjC.classes.UIImage.imageWithData_(data);

RPC

远程过程调用(RPC)对于应用逆向起到了很便捷的作用,比如目标应用有一个加解密的函数内部实现非常复杂,想分析整个加解密的实现过程的工作量较大,此时可以使用 frida 的 RPC 功能,只要知道加解密函数的名称或地址,还有相应的参数,即可直接调用得到加解密后的结果。
下面我们来做一个实例,编写一个 CrackMe 程序,定义一个 coreClass 类,里面有 4 个比较重要的方法,分别是 getDeviceId、httpPost、encrypt、decrypt,从它们的名称可以看出大概的意思,具体的定义如下:

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
@interface coreClass : NSObject
@end

@implementation coreClass

- (NSString*)getDeviceId{
return @"e10adc3949ba59abbe56e057f20f883e";
}

+(NSString*)httpPost:(NSString*)url{
NSLog(@"URL: %@", url);
return @"test";
}

- (NSString*)encrypt:(NSData*)data :(NSString*)key{

NSLog(@"data: %@", data);
NSLog(@"key: %@", key);
return @"encrypt successed!";
}

- (NSString*)decrypt:(NSString*)str :(NSString*)key{
NSLog(@"str: %@", str);
NSLog(@"key: %@", key);
return @"decrypt successed!";
}
@end

下面我们要做的事情是在 JS 脚本里去调用这 4 个方法。Objective-C 方法有两种,一种是 - 号开头,称为对象方法,还有一种是 + 号开头,称为类方法,前者需要初始化一个实例才能调用,后者不需要初始化实例,可以直接调用。先来看看对象方法在 JS 脚本里如何调用,比如我们要调用 -[coreClass getDeviceId] 方法,调用的代码如下:

1
2
3
var coreClass = ObjC.classes.coreClass.alloc();
var deviceId = coreClass['- getDeviceId'].call(coreClass);
console.log("deviceId: " + deviceId.toString());

再来看看 +[coreClass httpPost:] 方法的调用,从下面的代码可以看出,类方法是不需要初始化实例,在方法名称后面加上一个 _ 号就可以调用了

1
2
3
4
var coreClass = ObjC.classes.coreClass;
var url = ObjC.classes.NSURL.URLWithString_("http://www.ioshacker.net");
var retString = coreClass.httpPost_(url);
console.log("retString: " + retString);

-[coreClass encrypt::] 的调用代码如下,输入的参数有两个,一个参数是 NSData 类型的,一个是 NSString 类型。

1
2
3
4
5
6
7
8
9
var coreClass = ObjC.classes.coreClass.alloc();
var str = ObjC.classes.NSString.stringWithString_("test");

//NSData *data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
var data = str.dataUsingEncoding_(4);
var key = ObjC.classes.NSString.stringWithString_("key");

var encryptString = coreClass['- encrypt::'].call(coreClass, data, key);
console.log("encryptString: " + encryptString);

-[coreClass decrypt::] 的调用代码如下,两个参数都是 NSString 类型。

1
2
3
4
5
var coreClass = ObjC.classes.coreClass.alloc();
var str = ObjC.classes.NSString.stringWithString_("test");
var key = ObjC.classes.NSString.stringWithString_("key");
var decryptString = coreClass['- decrypt::'].call(coreClass, str, key);
console.log("decryptString: " + decryptString);

在上面我们已经学会如何在 JS 脚本里调用目标应用的 Objective-C 方法,接下面要做的就是将功能代码导出提供给 Python 使用。在 JS 代码里定义4个函数,分别是 getDeviceId、httpPost、encrypt、decrypt,这 4 个函数分别代表了调用目标进程的 4 个 Objective-C 方法,然后再使用 rpc.exports 将这 4 个函数导出,代码如下:

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
50
51
52
53
54
55
56
57
function getDeviceId(){
var coreClass = ObjC.classes.coreClass.alloc();
var deviceId = coreClass['- getDeviceId'].call(coreClass);
console.log("deviceId: " + deviceId.toString());
console.log("--------------------");
}

function httpPost(inputurl){
var coreClass = ObjC.classes.coreClass;
var url = ObjC.classes.NSURL.URLWithString_(inputurl);
var retString = coreClass.httpPost_(url);
console.log("retString: " + retString);
console.log("--------------------");
}

function encrypt(inputstr, inputkey){
var coreClass = ObjC.classes.coreClass.alloc();
var str = ObjC.classes.NSString.stringWithString_(inputstr);

//NSData *data = [@"data" dataUsingEncoding:NSUTF8StringEncoding];
var data = str.dataUsingEncoding_(4);
var key = ObjC.classes.NSString.stringWithString_(inputkey);

var encryptString = coreClass['- encrypt::'].call(coreClass, data, key);
console.log("encryptString: " + encryptString);
console.log("--------------------");
}

function decrypt(inputstr, inputkey){
var coreClass = ObjC.classes.coreClass.alloc();
var str = ObjC.classes.NSString.stringWithString_(inputstr);
var key = ObjC.classes.NSString.stringWithString_(inputkey);
var decryptString = coreClass['- decrypt::'].call(coreClass, str, key);
console.log("decryptString: " + decryptString);
console.log("--------------------");
}

//getDeviceId();
//httpPost("http://www.ioshacker.net");
//encrypt("123", "456");
//decrypt("123", "456");

// 导出RPC函数
rpc.exports = {
deviceid: function () {
getDeviceId();
},
httppost: function(urlstr) {
httpPost(urlstr);
},
encrypt: function (inputstr, inputkey) {
encrypt(inputstr, inputkey);
},
decrypt: function(inputstr, inputkey) {
decrypt(inputstr, inputkey);
},
};

接着我们再编写 python 代码,首先调用 get_usb_device().attach 附加到目标进程,附加成功会返回一个 Session 实例,然后读取 call.js 文件里的内容,这里的内容就是上面我们编写的 JS 代码,然后调用 session.create_script 创建脚本,再调用 load 加载脚本,最后调用 script.exports 获取 JS 导出的 RPC 函数,此时就可以调用 RPC 函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# -*- coding: utf-8 -*-
import codecs
import frida

if __name__ == '__main__':
#附加到目标进程
session = frida.get_usb_device().attach(u'CrackMe')

#读取JS脚本
with codecs.open('./call.js', 'r', 'utf-8') as f:
source = f.read()

script = session.create_script(source)
script.load()

rpc = script.exports

#调用导出的函数
rpc.deviceid()
rpc.httpPost("http://www.ioshacker.net")
rpc.encrypt("123", "456")
rpc.decrypt("123", "456")
session.detach()

上面我们讲到了如何将 Objective-C 方法导出给 Python 使用,那如何导出 C 函数呢?比如调用 NSHomeDirectory 这个系统提供的函数,知道它所在的模块是 Foundation,找到它的地址在 JS 脚本里就可以调用,然后在 rpc.exports 里添加一个导出函数,这样 Python 就可以调用了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getHomeDir(){
var NSHomeDirectory = new NativeFunction(ptr(Module.findExportByName("Foundation", "NSHomeDirectory")), 'pointer', []);
var path = new ObjC.Object(NSHomeDirectory());
console.log('homeDir: ' + path);
}

// 导出RPC函数
rpc.exports = {
//......
homedir: function(){
getHomeDir();
}
};

b2s and s2b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    //b2s
function b2s(array) {
var result = "";
for (var i = 0; i < array.length; i++) {
result += String.fromCharCode(modulus(array[i], 256));
}
return result;
}

function modulus(x, n) {
return ((x % n) + n) % n;
}

//s2b

var StringClass=Java.use("java.lang.String");
var b2sString = b2s('字节数组')
console.log(b2sString)
var jstring = StringClass.$new(b2sString)
var jbyte = jstring.getBytes("UTF-8")

插件化 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
Java.perform(function () {
var hook_class_name = "Test";
Java.enumerateClassLoaders({
onMatch: function (loader) {
console.log(loader)
try {
if (loader.findClass(hook_class_name)) {
var factory = Java.ClassFactory.get(loader)
var myClass = factory.use("myClass");
var test = Java.classFactory.use(hook_class_name);
test.DexClass.overload().implementation=function(){
var ret = this.DexClass();
console.log(ret);
return ret;
}

}
} catch (error){

}
},
onComplete: function () {

}
});

});

创建字节数组

1
2
3
4
5
6
var myArray=new Array(1024);
var i = 0
for (i = 0; i < myArray.length; i++) {
myArray[i]= 0x0;
}
var buffer = Java.array('byte',myArray);

创建String数组

1
Java.array("Ljava.lang.String;", ["111","222","333"]);

打印字节数组

1
2
3
4
5
6
7
var size = 100;  //字节数组长度
var bytearr= Java.array("byte",arg1); //方便for循环
var content = "";
for (var i = 0;i<size;i++){
content = content + String.fromCharCode(bytearr[i]);
}
console.log(content)

load dex打印对象

1
2
3
Java.openClassFile("xxxxx.dex").load();
const gson = Java.use("com.xxxx.xxxx");
console.log(gson.$new().toJson(xxxx));

打印堆栈

  • Android

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

    1
    console.log('\tBacktrace:\n\t' + Thread.backtrace(this.context,Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n\t')); //Backtracer.ACCURATE

SUB方法主动调用

1
2
3
4
5
6
7
8
9
10
11
function  hooksub(content) {

var offset = 0x835; //sub方法偏移
var nativeLibModule = Process.getModuleByName("libnative-lib.so"); //获取so模块
var func_sub = nativeLibModule.base.add(offset); //获取sub方法内存地址
var arg0 = Memory.alloc(10); //申请内存
ptr(arg0).writeUtf8String(content); //写入自定义内容
var sub_method = new NativeFunction(func_sub,'pointer',['pointer']); //新建该方法,pointer为返回值(指针类型),['pointer']为入参(指针类型)
var result = sub_method(arg0) //调用该方法
console.log("result",hexdump(result));
}

SO导出方法-主动调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// char* cncrypt(char* data,  int num)
// NativeFunction(address, returnType, argTypes[, abi])

function hookMethod(str_data, n_num)
{
var soName = "libnative-lib.so"; //要hook的so名
var funcName = "encrypt"; //要hook的函数名

//获取函数的地址
var n_addr_func = Module.findExportByName(soName , funcName);


var func_c_enc = new NativeFunction(n_addr_func , 'pointer', ['pointer', 'int']);

//调用frida的api申请空间 填入字符串
var str_data_arg = Memory.allocUtf8String(str_data);

//调用so层的c函数
var p_str_ret = func_c_enc(str_data_arg, n_num);

//读取字符串
var str_ret = Memory.readCString(p_str_ret);
return str_ret;
}

NativeFunction中frida支持的数据类型和abi

SUPPORTED TYPES

  • void
  • pointer
  • int
  • uint
  • long
  • ulong
  • char
  • uchar
  • size_t
  • ssize_t
  • 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

keypatch

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
function dis(address,number) {
console.log("\n");
for (var i = 0; i < number; i++) {
var ins = Instruction.parse(address);
console.error("address:" + address + " dis:" + ins.toString());
address = ins.next;
}
}

function nopKill() {
//nop kill
var libnativemodule = Process.getModuleByName("libnative-lib.so");
var base = libnativemodule.base;
Memory.patchCode(base.add(0x1564),4,patchaddr =>{
var arm64Writer = new Arm64Writer(patchaddr);
arm64Writer.putNop();
arm64Writer.flush();
})
}

function editTbnz() {

// Tbnz => Tbz
var libnativemodule = Process.getModuleByName("libnative-lib.so");
var base = libnativemodule.base;
Memory.protect(base.add(0x150C), 4, 'rwx');
base.add(0x150C).writeByteArray([0xD1, 0x01, 0x00, 0x36]);



}

function main() {
// hook();
var libnativemodule = Process.getModuleByName("libnative-lib.so");
var base = libnativemodule.base;
dis(base.add(0x150C),30);
//nopKill();
//editTbnz();
}

setImmediate(main);

ssl_pinning

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
Java.perform(function() {
var androidVersion = parseInt(Java.androidVersion, 10)
if (androidVersion > 6){
console.log(111)
try{
// Generic SSL Pinning Bypass tested on Android 7, 7.1, 8, and 9
// https://android.googlesource.com/platform/external/conscrypt/+/1186465/src/platform/java/org/conscrypt/TrustManagerImpl.java#391
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl.checkTrustedRecursive.implementation = function(certs, host, clientAuth, untrustedChain, trustedChain, used) {
send('[SSL Pinning Bypass] checkTrustedRecursive() bypassed');
return Java.use("java.util.ArrayList").$new();
}
}catch (err) {
send('[SSL Pinning Bypass] TrustManagerImpl.checkTrustedRecursive() not found');
}
try {
var TrustManagerImpl2 = Java.use('com.android.org.conscrypt.TrustManagerImpl');
TrustManagerImpl2.verifyChain.implementation = function (untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {
send('[SSL Pinning Bypass] verifyChain() bypassed for: ' + host);
return untrustedChain;
}
} catch (err) {
send('[SSL Pinning Bypass] TrustManagerImpl.verifyChain() not found');
}
try {
var ConscryptFileDescriptorSocket = Java.use('com.android.org.conscrypt.ConscryptFileDescriptorSocket');
ConscryptFileDescriptorSocket.verifyCertificateChain.implementation = function (certChain, authMethod) {
send('[SSL Pinning Bypass] verifyCertificateChain() bypassed');
return;
}
} catch (err) {
send('[SSL Pinning Bypass] ConscryptFileDescriptorSocket.verifyCertificateChain() not found');
}
} else if (androidVersion > 4 && androidVersion < 7) {
console.log(111)
// Generic SSL Pinning Bypass tested on Android 5, 5,1, 6
// https://codeshare.frida.re/@akabe1/frida-universal-pinning-bypasser/
try {
var OpenSSLSocketImpl = Java.use('com.android.org.conscrypt.OpenSSLSocketImpl');
OpenSSLSocketImpl.verifyCertificateChain.implementation = function (certRefs, authMethod) {
send('[SSL Pinning Bypass] OpenSSLSocketImpl.verifyCertificateChain() bypassed');
return;
}
} catch (err) {
send('[SSL Pinning Bypass] OpenSSLSocketImpl.verifyCertificateChain() not found');
}
}
// 3rd Party Pinning
try{
var OkHttpClient = Java.use("com.squareup.okhttp.OkHttpClient");
OkHttpClient.setCertificatePinner.implementation = function(certificatePinner){
send('[SSL Pinning Bypass] OkHttpClient.setCertificatePinner() bypassed');
return this;
};
// Invalidate the certificate pinnet checks (if "setCertificatePinner" was called before the previous invalidation)
var CertificatePinner = Java.use("com.squareup.okhttp.CertificatePinner");
CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;').implementation = function(p0, p1){
send('[SSL Pinning Bypass] CertificatePinner.check() 1 bypassed');
return;
};
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function(p0, p1){
send('[SSL Pinning Bypass] CertificatePinner.check() 2 bypassed');
return;
};
} catch(err) {
send('[SSL Pinning Bypass] okhttp CertificatePinner not found');
}
try {
var CertificatePinner2 = Java.use('okhttp3.CertificatePinner');
CertificatePinner2.check.overload('java.lang.String', 'java.util.List').implementation = function (str) {
send('[SSL Pinning Bypass] okhttp3.CertificatePinner.check() bypassed for ' + str);
return;
};
} catch(err) {
send('[SSL Pinning Bypass] okhttp3 CertificatePinner not found');
}
try {
// https://gist.github.com/cubehouse/56797147b5cb22768b500f25d3888a22
var dataTheorem = Java.use("com.datatheorem.android.trustkit.pinning.OkHostnameVerifier");
dataTheorem.verify.overload('java.lang.String', 'javax.net.ssl.SSLSession').implementation = function (str) {
send('[SSL Pinning Bypass] DataTheorem trustkit.pinning.OkHostnameVerifier.verify() 1 bypassed for ' + str);
return true;
};

dataTheorem.verify.overload('java.lang.String', 'java.security.cert.X509Certificate').implementation = function (str) {
send('[SSL Pinning Bypass] DataTheorem trustkit.pinning.OkHostnameVerifier.verify() 2 bypassed for ' + str);
return true;
};
} catch(err) {
send('[SSL Pinning Bypass] DataTheorem trustkit not found');
}
try {
var PinningTrustManager = Java.use('appcelerator.https.PinningTrustManager');
PinningTrustManager.checkServerTrusted.implementation = function () {
send('[SSL Pinning Bypass] Appcelerator appcelerator.https.PinningTrustManager.checkServerTrusted() bypassed');
}
} catch (err) {
send('[SSL Pinning Bypass] Appcelerator PinningTrustManager not found');
}
try {
var SSLCertificateChecker = Java.use('nl.xservices.plugins.SSLCertificateChecker');
SSLCertificateChecker.execute.overload('java.lang.String', 'org.json.JSONArray', 'org.apache.cordova.CallbackContext').implementation = function (action, args, callbackContext) {
send('[SSL Pinning Bypass] Apache Cordova - SSLCertificateChecker.execute() bypassed');
callbackContext.success("CONNECTION_SECURE");
return;
};
} catch(err) {
send('[SSL Pinning Bypass] Apache Cordova SSLCertificateChecker not found');
}
try {
var wultra = Java.use('com.wultra.android.sslpinning.CertStore');
wultra.validateFingerprint.overload('java.lang.String', '[B').implementation = function (commonName, fingerprint) {
send('[SSL Pinning Bypass] Wultra com.wultra.android.sslpinning.CertStore.validateFingerprint() bypassed');
var ValidationResult = Java.use('com.wultra.android.sslpinning.ValidationResult');
return ValidationResult.TRUSTED;
};
} catch(err) {
send('[SSL Pinning Bypass] Wultra CertStore.validateFingerprint not found');
}
}, 0);

android加密

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
Java.perform(function () {
/**
* 加密算法加解密
*/

var complete_bytes = new Array();
var index = 0;
var secretKeySpecDef = Java.use('javax.crypto.spec.SecretKeySpec');
var ivParameterSpecDef = Java.use('javax.crypto.spec.IvParameterSpec');
var cipherDef = Java.use('javax.crypto.Cipher');
var Arrays = Java.use('java.util.Arrays');

var jAndroidLog = Java.use("android.util.Log");
var jException = Java.use("java.lang.Exception");

var threadef = Java.use('java.lang.Thread');
var threadinstance = threadef.$new();
function Where(stack){
for(var i = 0; i < stack.length; ++i){
console.log(stack[i].toString());
}
}
var cipherDoFinal_1 = cipherDef.doFinal.overload();
var cipherDoFinal_2 = cipherDef.doFinal.overload('[B');
var cipherDoFinal_3 = cipherDef.doFinal.overload('[B', 'int');
var cipherDoFinal_4 = cipherDef.doFinal.overload('[B', 'int', 'int');
var cipherDoFinal_5 = cipherDef.doFinal.overload('[B', 'int', 'int', '[B');
var cipherDoFinal_6 = cipherDef.doFinal.overload('[B', 'int', 'int', '[B', 'int');
var cipherUpdate_1 = cipherDef.update.overload('[B');
var cipherUpdate_2 = cipherDef.update.overload('[B', 'int', 'int');
var cipherUpdate_3 = cipherDef.update.overload('[B', 'int', 'int', '[B');
var cipherUpdate_4 = cipherDef.update.overload('[B', 'int', 'int', '[B', 'int');
var secretKeySpecDef_init_1 = secretKeySpecDef.$init.overload('[B', 'java.lang.String');
var secretKeySpecDef_init_2 = secretKeySpecDef.$init.overload('[B', 'int', 'int', 'java.lang.String');
var ivParameterSpecDef_init_1 = ivParameterSpecDef.$init.overload('[B');
var ivParameterSpecDef_init_2 = ivParameterSpecDef.$init.overload('[B', 'int', 'int');

secretKeySpecDef_init_1.implementation = function (arr, alg) {
var key = b2s(arr);
var byteArr = Arrays.toString(arr);
console.log("\n[*] "+alg + " secret key, plaintext: " + key+" byte[]: "+byteArr);
return secretKeySpecDef_init_1.call(this, arr, alg);
}

secretKeySpecDef_init_2.implementation = function (arr, off, len, alg) {
var key = b2s(arr);
var byteArr = Arrays.toString(arr);
console.log("\n[*] "+alg + " secret key, plaintext: " + key+" byte[]: "+byteArr);
return secretKeySpecDef_init_2.call(this, arr, off, len, alg);
}

/*
ivParameterSpecDef_init_1.implementation = function(arr)
{
var iv = b2s(arr);
var byteArr = Arrays.toString(arr);
console.log("[*] Creating IV, plaintext: : "+iv+" byte[]: "+byteArr)
return ivParameterSpecDef_init_1.call(this, arr);
}

ivParameterSpecDef_init_2.implementation = function(arr, off, len)
{
var iv = b2s(arr);
var byteArr = Arrays.toString(arr);
console.log("[*] Creating IV, plaintext: " + iv +" byte[]: "+byteArr);
return ivParameterSpecDef_init_2.call(this, arr, off, len);
}
*/



cipherDoFinal_1.implementation = function () {
var ret = cipherDoFinal_1.call(this);
info(this.getIV(), this.getAlgorithm(), complete_bytes, ret);
return ret;
}

cipherDoFinal_2.implementation = function (arr) {
addtoarray(arr);
var ret = cipherDoFinal_2.call(this, arr);
info(this.getIV(), this.getAlgorithm(), complete_bytes, ret);
return ret;
}

cipherDoFinal_3.implementation = function (arr, a) {
addtoarray(arr);
var ret = cipherDoFinal_3.call(this, arr, a);
info(this.getIV(), this.getAlgorithm(), complete_bytes, ret);
return ret;
}

cipherDoFinal_4.implementation = function (arr, a, b) {
addtoarray(arr);
var ret = cipherDoFinal_4.call(this, arr, a, b);
info(this.getIV(), this.getAlgorithm(), complete_bytes, ret);
return ret;
}

cipherDoFinal_5.implementation = function (arr, a, b, c) {
addtoarray(arr);
var ret = cipherDoFinal_5.call(this, arr, a, b, c);
info(this.getIV(), this.getAlgorithm(), complete_bytes, ret);
return ret;
}

cipherDoFinal_6.implementation = function (arr, a, b, c, d) {
addtoarray(arr);
var ret = cipherDoFinal_6.call(this, arr, a, b, c, d);
info(this.getIV(), this.getAlgorithm(), complete_bytes, c);
return ret;
}

cipherUpdate_1.implementation = function (arr) {
addtoarray(arr);
return cipherUpdate_1.call(this, arr);
}

cipherUpdate_2.implementation = function (arr, a, b) {
addtoarray(arr);
return cipherUpdate_2.call(this, arr, a, b);
}

cipherUpdate_3.implementation = function (arr, a, b, c) {
addtoarray(arr);
return cipherUpdate_3.call(this, arr, a, b, c);
}

cipherUpdate_4.implementation = function (arr, a, b, c, d) {
addtoarray(arr);
return cipherUpdate_4.call(this, arr, a, b, c, d);
}

function info(iv, alg, plain, encoded) {
console.log("\n[*] ------------------------------------------"+alg+"---------------------------------------------")
var stack = threadinstance.currentThread().getStackTrace();
Where(stack)
if (iv) {
var byteArr = Arrays.toString(iv);
console.log("[*] Initialization Vector: " + b2s(iv) + " byte[]: "+byteArr);
} else {
console.log("[*] Initialization Vector: " + iv);
}

console.log("[*] Algorithm: " + alg);
console.log("[*] In: " + b2s(plain));
console.log("[*] Out: " + b2s(encoded));
complete_bytes = [];
index = 0;
console.log("[*] -----------------------------------------------------------------------------------------")

}

function hexdump(buffer, blockSize) {
blockSize = blockSize || 16;
var lines = [];
var hex = "0123456789ABCDEF";
for (var b = 0; b < buffer.length; b += blockSize) {
var block = buffer.slice(b, Math.min(b + blockSize, buffer.length));
var addr = ("0000" + b.toString(16)).slice(-4);
var codes = block.split('').map(function (ch) {
var code = ch.charCodeAt(0);
return " " + hex[(0xF0 & code) >> 4] + hex[0x0F & code];
}).join("");
codes += " ".repeat(blockSize - block.length);
var chars = block.replace(/[\\x00-\\x1F\\x20]/g, '.');
chars += " ".repeat(blockSize - block.length);
lines.push(addr + " " + codes + " " + chars);
}
return lines.join("\\n");
}

function b2s(array) {
var result = "";
for (var i = 0; i < array.length; i++) {
result += String.fromCharCode(modulus(array[i], 256));
}
return result;
}

function modulus(x, n) {
return ((x % n) + n) % n;
}

function addtoarray(arr) {
for (var i = 0; i < arr.length; i++) {
complete_bytes[index] = arr[i];
index = index + 1;
}
}
});

android哈希

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137

/**
* HASH/HMAC
*/
Java.perform(function () {
var hash_complete_bytes = new Array();
var hash_index = 0;
var MessageDigestDef = Java.use('java.security.MessageDigest');
var MacDef = Java.use('javax.crypto.Mac');
var MessageDigestDef_init_1 = MessageDigestDef.$init.overload('java.lang.String');
var MessageDigestUpdate_1 = MessageDigestDef.update.overload('[B');
var MessageDigestUpdate_2 = MessageDigestDef.update.overload('[B', 'int', 'int');
var MessageDigestUpdate_3 = MessageDigestDef.update.overload('java.nio.ByteBuffer');
var MessageDigestdigest_1 = MessageDigestDef.digest.overload();
var MessageDigestdigest_2 = MessageDigestDef.digest.overload('[B');
var MessageDigestdigest_3 = MessageDigestDef.digest.overload('[B','int','int');
var MacdoFinal_1 = MacDef.doFinal.overload();
var MacdoFinal_2 = MacDef.doFinal.overload('[B');
var MacdoFinal_3 = MacDef.doFinal.overload('[B','int');




MessageDigestDef_init_1.implementation = function (alg) {
// console.log("[*] "+alg);
return MessageDigestDef_init_1.call(this,alg);
}

MessageDigestUpdate_1.implementation = function (arr) {
hashAddtoarray(arr);
// console.log("MessageDigestUpdate_1 "+b2s(arr))
return MessageDigestUpdate_1.call(this, arr);
}

MessageDigestUpdate_2.implementation = function (arr,offset,len) {
hashAddtoarray(arr);
// console.log("MessageDigestUpdate_2 "+b2s(arr))
return MessageDigestUpdate_2.call(this, arr,offset,len);
}

MessageDigestUpdate_3.implementation = function (arr) {
hashAddtoarray(arr);
// console.log("MessageDigestUpdate_3 "+b2s(arr))
return MessageDigestUpdate_3.call(this, arr);
}


MacdoFinal_1.implementation = function () {
var ret = MacdoFinal_1.call(this);
hashInfo(this.getAlgorithm(),hash_complete_bytes,ret,'MacdoFinal_1')
return ret
}

MacdoFinal_2.implementation = function (arr) {
hashAddtoarray(arr)
var ret = MacdoFinal_2.call(this,arr);
hashInfo(this.getAlgorithm(),hash_complete_bytes,ret,'MacdoFinal_2')
return ret
}

MacdoFinal_3.implementation = function (arr,outOffset) {
hashAddtoarray(arr)
var ret = MacdoFinal_3.call(this,arr,outOffset);
hashInfo(this.getAlgorithm(),hash_complete_bytes,ret,'MacdoFinal_3')
return ret
}

function hashInfo(alg, plain, encoded,method) {
console.log("\n[*] -----------------------------------------"+alg+"--------------------------------------------")
console.log("[*] method : "+method)
console.log("[*] Hash Algorithm : " + alg);
console.log("[*] Hash In : " + b2s(plain));
console.log("[*] Hash Out : " + toHexString(encoded));
hash_complete_bytes = [];
hash_index = 0;
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
console.log("[*] --------------------------------------------------------------------------------------------")
}

MessageDigestdigest_1.implementation = function () {
var ret = MessageDigestdigest_1.call(this);
hashInfo(this.getAlgorithm(),hash_complete_bytes,ret,'MessageDigestdigest_1')
// console.log("[*] Algorithm: "+ this.getAlgorithm()+" hash result: "+toHexString(ret))
return ret
}

MessageDigestdigest_2.implementation = function (arr) {
var ret = MessageDigestdigest_2.call(this,arr);
var hash = b2s(arr);
hashInfo(this.getAlgorithm(),hash_complete_bytes,ret,'MessageDigestdigest_2')
// console.log("[*] hash plaintext: "+hash+" hash result: "+toHexString(ret));
return ret;
}

MessageDigestdigest_3.implementation = function (arr,offset,len) {
var ret = MessageDigestdigest_3.call(this,offset,len);
var hash = b2s(arr);
hashInfo(this.getAlgorithm(),hash_complete_bytes,ret,'MessageDigestdigest_3')
// console.log("[*] hash plaintext: "+hash+" hash result: "+toHexString(ret));
return ret;
}



function hashAddtoarray(arr) {
for (var i = 0; i < arr.length; i++) {
hash_complete_bytes[hash_index] = arr[i];
hash_index = hash_index + 1;
}
}

function toHexString(byteArray) {
var HEX_DIGITS = "0123456789abcdef";
var HEX_DIGIT_BITS = 4
var HEX_DIGIT_MASK = 0xF
var sb='';
for (var i = 0; i < byteArray.length; i++) {
var b = byteArray[i] & 0xFF;
sb += HEX_DIGITS.charAt(b >>> HEX_DIGIT_BITS)+HEX_DIGITS.charAt(b & HEX_DIGIT_MASK);
}
return sb;
}


function b2s(array) {
var result = "";
for (var i = 0; i < array.length; i++) {
result += String.fromCharCode(modulus(array[i], 256));
}
return result;
}

function modulus(x, n) {
return ((x % n) + n) % n;
}

});

内存搜索字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function scan() {
var result = [];
Process.enumerateRanges('r--').forEach(function (range) {
if (range.file && range.file.path) {
try {

Memory.scanSync(range.base, range.size, "31 38 38 34 36").forEach(function
(match) {
console.log("[*] search : "+range.file.path)
console.log(hexdump(match.address))
});
} catch (e) {
}
}
});
console.log("No search")
}

rootbypass

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
// Based on  https://codeshare.frida.re/@dzonerzy/fridantiroot/
Java.perform(function () {
var RootPackages = [
"com.noshufou.android.su",
"com.noshufou.android.su.elite",
"eu.chainfire.supersu",
"com.koushikdutta.superuser",
"com.thirdparty.superuser",
"com.yellowes.su",
"com.koushikdutta.rommanager",
"com.koushikdutta.rommanager.license",
"com.dimonvideo.luckypatcher",
"com.chelpus.lackypatch",
"com.ramdroid.appquarantine",
"com.ramdroid.appquarantinepro",
"com.devadvance.rootcloak",
"com.devadvance.rootcloakplus",
"de.robv.android.xposed.installer",
"com.saurik.substrate",
"com.zachspong.temprootremovejb",
"com.amphoras.hidemyroot",
"com.amphoras.hidemyrootadfree",
"com.formyhm.hiderootPremium",
"com.formyhm.hideroot",
"me.phh.superuser",
"eu.chainfire.supersu.pro",
"com.kingouser.com"
];
var RootBinaries = ["mu", ".su", "su", "busybox", "supersu", "Superuser.apk", "KingoUser.apk", "SuperSu.apk","mounts"]; //,"status""cmdline"
var RootProperties = {
"ro.build.selinux": "1",
"ro.debuggable": "0",
"service.adb.root": "0",
"ro.secure": "1"
};
var RootPropertiesKeys = [];
for (var k in RootProperties) RootPropertiesKeys.push(k);



/**
* 通过判断是否存在Superuser.apk或su等命令来判断环境
* HOOK File.exists()
* File.getName() 获取输入的文件路径
* 与RootBinaries特征进行匹配
*/

var NativeFile = Java.use('java.io.File');
NativeFile.exists.implementation = function () {

var name = NativeFile.getName.call(this);
console.log("HOOK exists: "+name)
if (RootBinaries.indexOf(name) > -1) {
console.log("[*] hook:exists() parameter: "+name);
return false;
} else {
return this.exists.call(this);
}
};



/**
* 通过判断系统是否为发行版本来检测环境
* HOOK String.contains
* 如果对比的字符串为test-keys,意味者检测系统是否是为发行版本release-keys
* 此处过滤不够严格,代码有可能会通过 String.contains("release-keys")进行检测
*/
var String = Java.use('java.lang.String');
String.contains.implementation = function (name) {
if (name == "test-keys") {
console.log("[*] test-keys check Bypass");
return false;
}
return this.contains.call(this, name);
};

/**
* 根据入参判断是否可以匹配设置的黑名单
*/
function isRootCheck(cmd) {
var fakeCmd;
console.log("cmd isRootCheck: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd == "sh" || cmd.indexOf("which") != -1 ) {
fakeCmd = "grep";
console.log("[*] cmd Bypass parameter:"+cmd);
return fakeCmd;
}
if (cmd == "su") {
fakeCmd = "justalled";
console.log("[*] cmd Bypass parameter:"+cmd);
return fakeCmd;
}
return false;
}
/**
* 获取重载方法所有入参类型,从而可以实现hook所有重载方法
* @param {class}} toHook
*/
function get_implementations(toHook) {
var imp_args = []
toHook.overloads.forEach(function (impl, _) {
if (impl.hasOwnProperty('argumentTypes')) {
var args = [];
var argTypes = impl.argumentTypes
argTypes.forEach(function (arg_type, __) {
args.push(arg_type.className)
});
imp_args.push(args);
}
});
return imp_args;
}


/**
* 通过java.lang.Runtime.exec执行命令的方式来检测环境
* HOOK java.lang.Runtime.exec下的所有重载方法
*/
var Runtime = Java.use('java.lang.Runtime');
var execImplementations = get_implementations(Runtime.exec)
var exec = Runtime.exec.overload('java.lang.String')

execImplementations.forEach(function (args, _) {
Runtime.exec.overload.apply(null, args).implementation = function () {
var fakeCmd;
//此处也可以使用 Array.prototype.slice作用是获取arguments数组里的所有参数
//注arguments里就是重载方法的入参值,arguments为隐含变量由frida提供
var argz = [].slice.call(arguments);
var cmd = argz[0]
if (typeof cmd === 'string') {
fakeCmd = isRootCheck(cmd);
if (fakeCmd) {
console.log("[*] hook Runtime.exec string cmd Bypass parameter:"+cmd);
return exec.call(this, fakeCmd);
}
//判断cmd是否为String数组
} else if (typeof cmd === 'object') {
for (var i = 0; i < cmd.length; i = i + 1) {
var tmp_cmd = cmd[i];
fakeCmd = isRootCheck(tmp_cmd);
if (fakeCmd) {
console.log("[*] hook Runtime.exec object cmd Bypass parameter:"+cmd);
return exec.call(this, 'aba');
}
}
}
return this['exec'].apply(this, argz);
};
});

// BufferedReader checkLine check
/**
* 通过读取/system/build.prop中ro.build.tags的值来检测环境
* HOOK java.io.BufferedReader.readLine如果读取内容包含ro.build.tags=test-keys则替换为ro.build.tags=release-keys
*/
// var BufferedReader = Java.use('java.io.BufferedReader');
// BufferedReader.readLine.overload().implementation = function () {
// var text = this.readLine.call(this);
// console.log(text)
// if (text === null) {
// // just pass , i know it's ugly as hell but test != null won't work :(
// } else {
// var shouldFakeRead = (text.indexOf("ro.build.tags=test-keys") > -1);
// if (shouldFakeRead) {
// send("[RootDetection Bypass] build.prop file read");
// console.log("[*] build.prop file read parameter:"+text);
// text = text.replace("ro.build.tags=test-keys", "ro.build.tags=release-keys");
// }
// }
// return text;
// }

/**
* 启动一个进程来使用命令如:getprop、id、mount等来检测环境
* HOOK java.lang.ProcessBuilder.start方法
* 通过hook start方法调用该对象内的command方法返回即将执行的命令:List<String> command
* 遍历command是否与黑名单匹配
*/
var ProcessBuilder = Java.use('java.lang.ProcessBuilder');
ProcessBuilder.start.implementation = function () {
var cmd = this.command.call(this);
var shouldModifyCommand = false;
for (var i = 0; i < cmd.size(); i = i + 1) {
var tmp_cmd = cmd.get(i).toString();
if (tmp_cmd.indexOf("getprop") != -1 || tmp_cmd.indexOf("mount") != -1 || tmp_cmd.indexOf("build.prop") != -1 || tmp_cmd.indexOf("id") != -1) {
shouldModifyCommand = true;
}
}
if (shouldModifyCommand) {

console.log("[*] hook ProcessBuilder cmd Bypass parameter:"+cmd);
this.command.call(this, ["grep"]);
return this.start.call(this);
}
if (cmd.indexOf("su") != -1) {
console.log("[*] hook ProcessBuilder cmd Bypass parameter:"+cmd);
this.command.call(this, ["juslled"]);
return this.start.call(this);
}

return this.start.call(this);
}

// Patch other libraries after the above ones
/**
* 判断isInsideSecureHardware 硬件安全区检测
* 通过getPackageInfo检测已安装的app的包名是否包含de.robv.android.xposed.installer、magsik等应用以此来检测环境
* 通过android.os.SystemProperties.get方法获取ro.build.selinux、ro.debuggable等来检测系统环境
*/
var RootBypass = [{
class: 'android.security.keystore.KeyInfo',
method: 'isInsideSecureHardware',
func: function () {
send("[RootDetection Bypass] isInsideSecureHardware");
console.log("isInsideSecureHardware Bypass");
return true;
},
target: 6
}, {
class: 'android.app.ApplicationPackageManager',
method: 'getPackageInfo',
arguments: ['java.lang.String', 'int'],
func: function (pname, flags) {
var shouldFakePackage = (RootPackages.indexOf(pname) > -1);
if (shouldFakePackage) {
console.log("[*] hook ApplicationPackageManager check for package:"+pname);
pname = "com.cola.zzzz";
}
return this.getPackageInfo.call(this, pname, flags);
}
}, {
class: 'android.os.SystemProperties',
method: 'get',
arguments: ['java.lang.String'],
func: function (name) {
if (RootPropertiesKeys.indexOf(name) != -1) {
console.log("[*] hook SystemProperties parameter:"+name);
return RootProperties[name];
}
return this.get.call(this, name);
}
}
]

RootBypass.forEach(function (bypass, _) {
var toHook;
try {
if (bypass.target && parseInt(Java.androidVersion, 10) < bypass.target) {

console.log("[*] unavailable class/method - "+bypass.class + '.' + bypass.method);
return
}
toHook = Java.use(bypass.class)[bypass.method];
if (!toHook) {

console.log("[*] Cannot find- "+bypass.class + '.' + bypass.method);
return
}
} catch (err) {
console.log("[*] Error- "+bypass.class + '.' + bypass.method+err);
return
}
if (bypass.arguments) {
toHook.overload.apply(null, bypass.arguments).implementation = bypass.func;
} else {
toHook.overload.implementation = bypass.func;
}
})



/**
* 通过C/C++代码来检测环境
* HOOK fopen和system方法
* 原方法使用Memory.writeUtf8String进行替换要查找的文件路径
* 因为 *fopen(const char * __restrict __filename, const char * __restrict __mode) 的入参数被const修饰无法修改
* 所以更改为 args[0] = Memory.alloc(1)来达到替换入参的目的
*/

Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
onEnter: function (args) {

var path = Memory.readCString(args[0]);

path = path.split("/");
var executable = path[path.length - 1];
var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1)
if (shouldFakeReturn) {
console.log("[*] hook:fopen() parameter: "+path)
args[0] = Memory.alloc(1)
// Memory.writeUtf8String(args[0], "/notex ss");
// send("[RootDetection Bypass] native fopen");

}
},
onLeave: function (retval) {

}
});

/**
* HOOK open
*/
// Interceptor.attach(Module.findExportByName("libc.so", "open"), {
// onEnter: function (args) {

// var path = Memory.readCString(args[0]);
// console.log("[*] hook:open() parameter: "+path)
// path = path.split("/");
// var executable = path[path.length - 1];
// var shouldFakeReturn = (RootBinaries.indexOf(executable) > -1)
// if (shouldFakeReturn) {
// args[0] = Memory.alloc(1)

// }
// },
// onLeave: function (retval) {

// }
// });

/**
* Exec Family
*/
Interceptor.attach(Module.findExportByName("libc.so", "system"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:system() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

Interceptor.attach(Module.findExportByName("libc.so", "execl"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execl() parameter: "+cmd)


console.log("[*] hook:execl() parameter1: "+ Memory.readCString(args[1]))
console.log("[*] hook:execl() parameter2: "+ Memory.readCString(args[2]))
console.log("[*] hook:execl() parameter3: "+ Memory.readCString(args[3]))
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd.indexOf("netstat") != -1 ) {
Memory.writeUtf8String(args[0], "grepx");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

Interceptor.attach(Module.findExportByName("libc.so", "execlp"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execlp() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

Interceptor.attach(Module.findExportByName("libc.so", "execve"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execve() parameter: "+cmd)
console.log("[*] hook:execl() parameter: "+ Memory.readCString(args[1]))
console.log("[*] hook:execl() parameter: "+ Memory.readCString(args[2]))
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id" || cmd.indexOf("netstat") != -1) {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

Interceptor.attach(Module.findExportByName("libc.so", "execle"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execle() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});


Interceptor.attach(Module.findExportByName("libc.so", "execlpe"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execlpe() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});


Interceptor.attach(Module.findExportByName("libc.so", "execv"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execv() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

Interceptor.attach(Module.findExportByName("libc.so", "execvp"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execvp() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

Interceptor.attach(Module.findExportByName("libc.so", "execvpe"), {
onEnter: function (args) {
var cmd = Memory.readCString(args[0]);
console.log("[*] hook:execvpe() parameter: "+cmd)
if (cmd.indexOf("getprop") != -1 || cmd == "mount" || cmd.indexOf("build.prop") != -1 || cmd == "id") {
Memory.writeUtf8String(args[0], "grep");
}
if (cmd == "su") {
Memory.writeUtf8String(args[0], "justafaled");
}
},
onLeave: function (retval) {

}

});

});

SSL双向认证

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
function hook_KeyStore_load() {
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
var myArray=new Array(1024);
var i = 0
for (i = 0; i < myArray.length; i++) {
myArray[i]= 0x0;
}
var buffer = Java.array('byte',myArray);

var StringClass = Java.use("java.lang.String");
var KeyStore = Java.use("java.security.KeyStore");
KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

console.log("KeyStore.load1:", arg0);
this.load(arg0);
};
KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));

console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);

if (arg0){
var file = Java.use("java.io.File").$new("/sdcard/Download/"+ String(arg0)+".p12");
var out = Java.use("java.io.FileOutputStream").$new(file);
var r;
while( (r = arg0.read(buffer)) > 0){
out.write(buffer,0,r)
}
console.log("save success!")
out.close()
}
this.load(arg0, arg1);
};

console.log("hook_KeyStore_load...");


});
}

function hook_ssl() {
Java.perform(function() {
var ClassName = "com.android.org.conscrypt.Platform";
var Platform = Java.use(ClassName);
var targetMethod = "checkServerTrusted";
var len = Platform[targetMethod].overloads.length;
console.log(len);
for(var i = 0; i < len; ++i) {
Platform[targetMethod].overloads[i].implementation = function () {
console.log("class:", ClassName, "target:", targetMethod, " i:", i, arguments);
//printStack(ClassName + "." + targetMethod);
}
}
});
}


function main(){
hook_KeyStore_load()
// hook_ssl()
}
setImmediate(main);

参考文章