0%

Frida so层进阶应用

Frida so层进阶应用

示例应用的so文件加载时机是在一些按钮被点击后才加载,注意

内存读写

内存页是分权限的,权限通常用rwx来表示,r-可读,w-可写,x-可执行

报错acess violation即为非法访问

先来看看Memory的protect方法的源码

1
2
3
4
5
type PageProtection = string;
declare namespace Memory {
function protect(address: NativePointerValue, size: number | UInt64, protection: PageProtection): boolean;
......
}
  1. 对指定内存修改权限
1
2
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
Memory.protect(soAddr.add(0x3DED), 16, 'rwx');
  1. 读取指定地址的字符串
1
2
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log(soAddr.add(0x3DED).readCString());
  1. 导出指定地址的内存数据
1
2
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log(hexdump(soAddr.add(0x3DED)));
  1. 读取指定地址的内存数据
1
2
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log(soAddr.add(0x3DED).readByteArray(16));
  1. 写入数据到指定内存地址

    在计算出指定的内存地址后,可以使用NativePointer的writeByteArray方法,从该地址往后写入数据,源码声明如下

1
2
3
declare class NativePointer {
writeByteArray(value: ArrayBuffer | number[]): NativePointer;
}

writeByteArray接受一个参数,ArrayBuffer或者是数值数组(想要写入的字节数组)。先来几个转换进制的工具函数。这些就单独放吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 将字符串转为字节数组
function stringToBytes(str){
return hexToBytes(stringToHex(str));
}
// 将字符串进行hex编码
function stringToHex(str) {
return str.split("").map(function(c) {
return ("0" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
// 将hex编码的数据转为字节数组
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
// 将hex编码的数据转为字符串
function hexToString(hexStr) {
var hex = hexStr.toString();
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}

写入字符串(跟上面的一起食用)

1
2
3
4
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var tmpAddr = soAddr.add(0x3DED);
Memory.protect(tmpAddr, 16, 'rwx');
console.log(hexdump( tmpAddr.writeByteArray(stringToBytes("xiaojianbang\0")) ));
  1. 分配内存

Memory方法的alloc的源码声明如下

alloc会在Frida私有堆上分配指定大小的内存,返回NativePointer类型的首地址,接着用NativePointer类的writeByteArray方法写入内存即可

1
2
3
4
declare namespace Memory {
function alloc(size: number | UInt64, options?: MemoryAllocOptions): NativePointer;
function allocUtf8String(str: string): NativePointer;
}

e.g.:

1
2
3
var addr = Memory.alloc(8);
addr.writeByteArray(hexToBytes("eeeeeeeeeeeeeeee"));
console.log(addr.readByteArray(8));

Frida修改so函数代码

首先是Instruction的parse方法,可以将16进制转化为汇编代码,该方法参数仅有一个,即NativePointer类型的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var funcAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x1ACC);
var asm = Instruction.parse(funcAddr);
console.log(asm);
for (var i = 0; i < 3; i++) {
asm = Instruction.parse(asm.next);
console.log(asm);
}

/*
sub sp, sp, #0x20
str x0, [sp, #0x18]
str x1, [sp, #0x10]
str w2, [sp, #0xc]
*/

然后

opcode与arm汇编的转换网址

https://armconverter.com/

修改代码需要写入的是opcode,并且需要用到上面的向指定内存中地址写入数据的方式。

1
2
3
4
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
soAddr.add(0x1AF4).writeByteArray(hexToBytes("0001094B"));
console.log(Instruction.parse(soAddr.add(0x1AF4)));
// sub w0, w8, w9

另一种方式是利用frida的api-ArmWriter

1
2
3
4
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
new Arm64Writer(soAddr.add(0x1AEC)).putNop();
console.log(Instruction.parse(soAddr.add(0x1AEC)).toString());
// nop

再或者说是patchCode,它的第0个参数是修改的其实地址,第一个参数是要修改的字节数,第二个参数是回调函数,当函数执行到起始地址是,会调用回调函数

1
2
3
4
5
6
7
8
var codeAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x1AF4);
Memory.patchCode(codeAddr, 4, function (code) {
var writer = new Arm64Writer(code, { pc: codeAddr });
writer.putBytes(hexToBytes("0001094B"));
writer.flush();
});
console.log(Instruction.parse(codeAddr));
//sub w0, w8, w9

Frida在进行so层hook是,需要修改函数的前16个字节,这也是函数被Hook的检测点之一

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook_func() {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var MD5Final = soAddr.add(0x3A78);
console.log(hexdump(MD5Final.readByteArray(20)));
Interceptor.attach(MD5Final, {
onEnter: function (args) {
console.log(hexdump(MD5Final.readByteArray(20)));
}, onLeave: function (retval) {
}
});
}
hook_func();

/*
00000000 ff 43 01 d1 fd 7b 04 a9 fd 03 01 91 48 d0 3b d5 .C...{......H.;.
00000010 08 15 40 f9 ..@.

00000000 50 00 00 58 00 02 1f d6 00 96 33 3e 74 00 00 00 P..X......3>t...
00000010 08 15 40 f9 ..@.
*/

Frida从内存中导出so函数

通过传入模块名,找到对应的Module,得到模块基址,模块大小等必要信息,然后保存生成路径,可能由于目标APP的权限影响,最好把路径设在APP本身的私有目录,防止保存失败。然后用Memory.protect修改内存权限,再使用NativePointer的readByteArray方法读取整个so文件对应的内存数据。最后使用Frida的API将文件写出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function dump_so(so_name) {
Java.perform(function () {
var module = Process.getModuleByName(so_name);
console.log("[name]:", module.name);
console.log("[base]:", module.base);
console.log("[size]:", module.size);
console.log("[path]:", module.path);
var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
var path = dir + "/" + module.name + "_" + module.base + "_" + module.size + ".so";
var file = new File(path, "wb");
if (file) {
Memory.protect(module.base, module.size, 'rwx');
var buffer = module.base.readByteArray(module.size);
file.write(buffer);
file.flush();
file.close();
console.log("[dump]:", path);
}
});
}
dump_so("libxiaojianbang.so");

Frida dump dex

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
//为了读写并dump文件的函数
function get_self_process_name() {
var openPtr = Module.getExportByName('libc.so', 'open');
var open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);

var readPtr = Module.getExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, "int", ["int", "pointer", "int"]);

var closePtr = Module.getExportByName('libc.so', 'close');
var close = new NativeFunction(closePtr, 'int', ['int']);

var path = Memory.allocUtf8String("/proc/self/cmdline");
var fd = open(path, 0);
if (fd != -1) {
var buffer = Memory.alloc(0x1000);

var result = read(fd, buffer, 0x1000);
close(fd);
result = ptr(buffer).readCString();
return result;
}

return "-1";
}

//这玩意儿跟dump so差不多
function dump_dex() {
var libart = Process.findModuleByName("libart.so");
var addr_DefineClass = null;
var symbols = libart.enumerateSymbols();
for (var index = 0; index < symbols.length; index++) {
var symbol = symbols[index];
var symbol_name = symbol.name;
//这个DefineClass的函数签名是Android9的
//_ZN3art11ClassLinker11DefineClassEPNS_6ThreadEPKcmNS_6HandleINS_6mirror11ClassLoaderEEERKNS_7DexFileERKNS9_8ClassDefE
if (symbol_name.indexOf("ClassLinker") >= 0 &&
symbol_name.indexOf("DefineClass") >= 0 &&
symbol_name.indexOf("Thread") >= 0 &&
symbol_name.indexOf("DexFile") >= 0 ) {
console.log(symbol_name, symbol.address);
addr_DefineClass = symbol.address;
}
}
var dex_maps = {};

console.log("[DefineClass:]", addr_DefineClass);
if (addr_DefineClass) {
Interceptor.attach(addr_DefineClass, {
onEnter: function (args) {
var dex_file = args[5];
//ptr(dex_file).add(Process.pointerSize) is "const uint8_t* const begin_;"
//ptr(dex_file).add(Process.pointerSize + Process.pointerSize) is "const size_t size_;"
var base = ptr(dex_file).add(Process.pointerSize).readPointer();
var size = ptr(dex_file).add(Process.pointerSize + Process.pointerSize).readUInt();

if (dex_maps[base] == undefined) {
dex_maps[base] = size;
var magic = ptr(base).readCString();
if (magic.indexOf("dex") == 0) {
var process_name = get_self_process_name();
if (process_name != "-1") {
var dex_path = "/data/data/" + process_name + "/files/" + base.toString(16) + "_" + size.toString(16) + ".dex";
console.log("[find dex]:", dex_path);
var fd = new File(dex_path, "wb");
if (fd && fd != null) {
var dex_buffer = ptr(base).readByteArray(size);
fd.write(dex_buffer);
fd.flush();
fd.close();
console.log("[dump dex]:", dex_path);

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

var is_hook_libart = false;

//这个也是将dlopen和android_dlopen_ext都给Hook了
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "dlopen"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
//console.log("dlopen:", path);
if (path.indexOf("libart.so") >= 0) {
this.can_hook_libart = true;
console.log("[dlopen:]", path);
}
}
},
onLeave: function (retval) {
if (this.can_hook_libart && !is_hook_libart) {
dump_dex();
is_hook_libart = true;
}
}
})

Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
//console.log("android_dlopen_ext:", path);
if (path.indexOf("libart.so") >= 0) {
this.can_hook_libart = true;
console.log("[android_dlopen_ext:]", path);
}
}
},
onLeave: function (retval) {
if (this.can_hook_libart && !is_hook_libart) {
dump_dex();
is_hook_libart = true;
}
}
});
}

setImmediate(hook_dlopen);

加密的字符串解密

这个emmm

  1. 直接打印内存中的字符串,以某一偏移地址为例

    1
    2
    3
    4
    5
    var soAddr = Module.findBaseAddress("liblogin_encrypt.so");
    console.log(soAddr.add(0xD060).readCString());
    // java/security/KeyFactory
    //这个用的不是HookDemo
    //在这链接:https://pan.baidu.com/s/1qsdNfpmOMVjfpw3pQfYGTA?pwd=GAME 提取码:GAME
  2. 使用jnitrace

    这种的话感觉不太灵光,参照前面写的

  3. 从内存中导出so。需要注意的是,导出的so是需要经过修复的,这种方法脱壳也经常用。具体方式参考上面的。

构造二级指针

这块主要用到new NativeFunction声明函数指针

xiugaiStr函数在so文件的偏移地址为0x1B00,主动调用它,需要先得到它的地址,然后用new NativeFunction声明函数指针;再接着构建实参,使用Memory的allocUtf8String方法构建一个字符串,也就是char* 类型,将返回的地址保存在strAddr中,再将strAddr存入到指针变量中,就完成了二级指针的构建。最后调用函数直接传入二级指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var soAddr = Module.findBaseAddress("liblogin_encrypt.so");
console.log(soAddr.add(0xD060).readCString());
// java/security/KeyFactory

var xiugaiStrAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x1B00);
var xiugaiStr = new NativeFunction(xiugaiStrAddr, 'int64', ['pointer']);

var strAddr = Memory.allocUtf8String("dajianbang");
console.log(hexdump(strAddr));
/*7d95814930 64 61 6a 69 61 6e 62 61 6e 67 00 98 7d 00 00 00 dajianbang..}...
*/

var finalAddr = Memory.alloc(8).writePointer(strAddr);
console.log(hexdump(finalAddr));
/*
7d95142920 30 49 81 95 7d 00 00 00 22 6c 6f 67 22 2c 22 6c 0I..}..."log","l
*/

xiugaiStr(finalAddr);
console.log(hexdump(strAddr));
/*
7d9582dfe0 64 61 6a 69 61 6e 62 61 6e 67 20 51 51 32 34 33 dajianbang QQ243
7d9582dff0 35 38 37 35 37 00 00 00 03 04 00 00 00 00 00 00 58757...........
*/

读写文件

写出文件的方法:使用fopen打开文件,fputs写入数据,fclose关闭文件。

主动调用so函数还是得先拿到地址,上代码,没什么好说的,就是注意f那三个函数的源码声明,看看参数类型,返回值类型啥的

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

// FILE *fopen(const char *filename, const char *mode)
// int fputs(const char *str, FILE *stream)
// int fclose(FILE *stream)
var fopen = new NativeFunction(fopenAddr, "pointer", ["pointer", "pointer"]);
var fputs = new NativeFunction(fputsAddr, "int", ["pointer", "pointer"]);
var fclose = new NativeFunction(fcloseAddr, "int", ["pointer"]);

var fileName = Memory.allocUtf8String("/data/data/com.xiaojianbang.app/xiaojianbang.txt");
var openMode = Memory.allocUtf8String("w");
var buffer = Memory.allocUtf8String("111111111");

var file = fopen(filename, open_mode);
fputs(buffer, file);
fclose(file);

fopen和fputs接受的都是指针类型的参数,不能将JS的字符串直接传递过去,需要使用Memory的allocUtf8String在内存中构建相应字符串后,再传递地址。

读取文件:使用libc.so的fgets。几乎一样,就不说了

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

// char *fgets(char *str, int n, FILE *stream)
var fopen = new NativeFunction(fopenAddr, "pointer", ["pointer", "pointer"]);
var fgets = new NativeFunction(fgetsAddr, "pointer", ["pointer", "int", "pointer"]);
var fclose = new NativeFunction(fcloseAddr, "int", ["pointer"]);

var fileName = Memory.allocUtf8String("/data/data/com.xiaojianbang.app/xiaojianbang.txt");
var openMode = Memory.allocUtf8String("r");
var buffer = Memory.alloc(60);

var file = fopen(fileName, openMode);
var data = fgets(buffer, 60, file);
console.log(data.readCString());
fclose(file);

Frida其他常用API介绍

NativePointer类的常用方法

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
declare class NativePointer {
// NativePointer构造函数,通过new NativePointer(...) 或者ptr(...) 来使用
constructor(v: string | number | UInt64 | Int64 | NativePointerValue);
// 判断是否空指针
isNull(): boolean;
// 创建新指针,值等于this + v
add(v: NativePointerValue | UInt64 | Int64 | number | string): NativePointer;
// 创建新指针,值等于this - v
sub(v: NativePointerValue | UInt64 | Int64 | number | string): NativePointer;
// 更多用于指针计算的方法,请自行查阅相关文档和源码
......
// 比较两者是否相等
equals(v: NativePointerValue | UInt64 | Int64 | number | string): boolean;
// 比较两者大小,返回1、-1、0
compare(v: NativePointerValue | UInt64 | Int64 | number | string): number;
// 指针转32位有符号数
toInt32(): number;
// 指针转32位无符号数
toUInt32(): number;
// NativePointer类型转string类型,参数可以指定进制,默认16进制
toString(radix?: number): string;
toJSON(): string;

// 读取4/8字节数据,转指针
readPointer(): NativePointer;
// 读8bit数据,也就是1字节数据,转有符号数
readS8(): number;
// 读8bit数据,也就是1字节数据,转无符号数
readU8(): number;
// 更多读取数据转数值的方法依此类推
......
// 读指定字节数内存数据,返回ArrayBuffer
readByteArray(length: number): ArrayBuffer | null;
// 读指定长度的C语言char*字符串,或者读取到遇字节0为止
readCString(size?: number): string | null;
// 读指定长度的Utf8字符串(可以是中文),或者读取到遇字节0为止
readUtf8String(size?: number): string | null;
readUtf16String(length?: number): string | null;
// 仅限Windows平台使用
readAnsiString(size?: number): string | null;

// 将4/8字节指针写入内存,后续基本都是与读取类似的操作,不再赘述
writePointer(value: NativePointerValue): NativePointer;
writeS8(value: number | Int64): NativePointer;
writeU8(value: number | UInt64): NativePointer;
......
writeByteArray(value: ArrayBuffer | number[]): NativePointer;
writeUtf8String(value: string): NativePointer;
writeUtf16String(value: string): NativePointer;
writeAnsiString(value: string): NativePointer;
}

Memory的常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
declare namespace Memory {
// 异步,在内存中搜索指定数据
// 可以搜索指定指令,然后patch。可以搜索指定文件头,然后dump等等
function scan(address: NativePointerValue, size: number | UInt64, pattern: string, callbacks: MemoryScanCallbacks): void;
// scan的同步版本
function scanSync(address: NativePointerValue, size: number | UInt64, pattern: string): MemoryScanMatch[];
// 在frida私有堆上分配指定大小的内存,返回首地址
function alloc(size: number | UInt64, options?: MemoryAllocOptions): NativePointer;
// 在frida私有堆上,将str作为UTF-8字符串进行分配、编码和写出
//(可以是中文)
function allocUtf8String(str: string): NativePointer;
function allocUtf16String(str: string): NativePointer;
// 仅限Windows平台使用
function allocAnsiString(str: string): NativePointer;
// 内存拷贝,参数为目标地址、源地址、要复制的字节数
function copy(dst: NativePointerValue, src: NativePointerValue, n: number | UInt64): void;
// 先分配内存,然后进行拷贝
function dup(address: NativePointerValue, size: number | UInt64): NativePointer;
// 修改内存页权限,之前小节中有介绍,这里不再赘述
function protect(address: NativePointerValue, size: number | UInt64, protection: PageProtection): boolean;
// 可以用来patch指令,本书后续内容中单独介绍
function patchCode(address: NativePointerValue, size: number | UInt64, apply: MemoryPatchApplyCallback): void;
}

替换函数

介绍一个新的FridaAPi:Interceptor.replace方法

其源码声明如下

1
2
3
4
declare namespace Interceptor {
function replace(target: NativePointerValue, replacement: NativePointerValue,
data?: NativePointerValue): void;
}

第0个参数需要传入NativePointer类型的目标地址,第一个参数掺入用于替换的函数(通常由new NativeCallback()构建),也得是NativePointer型的

NativeCallback源码声明如下

1
2
3
4
declare class NativeCallback extends NativePointer {
constructor(func: NativeCallbackImplementation, retType: NativeType, argTypes:
NativeType[], abi?: NativeABI);
}

NativeCallback在实例化时,需要传入函数的实现,返回值类型,参数类型数组。该类继承了NativePointer,所以可传给replace函数的replacement参数。

简单案例如下

1
2
3
4
5
6
7
8
9
function hook_func() {
var md5Func = Module.findBaseAddress("libxiaojianbang.so").add(0x1F2C);
Interceptor.replace(md5Func, new NativeCallback(function () {

}, "void", [ ]));
}
hook_func();
//logcat中的输出为
//CMD5 md5Result: null

值得注意的是,如果替换成空函数,NativeCallback的参数类型数组可以随便写或者为空数组。但是返回值类型最好和原函数返回值类型一致,而且要合理,别整太抽象。如果新韩淑影响了原函数的代码逻辑,APP是有可能崩溃的

稍作修改如下

1
2
3
4
5
6
7
8
function hook_func() {
var md5Func = Module.findBaseAddress("libxiaojianbang.so").add(0x1F2C);
Interceptor.replace(md5Func, new NativeCallback(function () {
return 100;
}, "int", []));
}
hook_func();
**//该函数触发后,app崩溃**

替换时打印原函数实参(需要将参数类型写对)

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_func() {
var md5Func = Module.findBaseAddress("libxiaojianbang.so").add(0x1F2C);
Interceptor.replace(md5Func, new NativeCallback(function (env, jclass, data) {
console.log(env);
console.log(jclass);
console.log(Java.vm.tryGetEnv().getStringUtfChars(data).readCString());
return Java.vm.tryGetEnv().newStringUtf("this is return value");
}, "pointer", ["pointer", "int", "pointer"]));//这一句是有错的,jcalss的类型是指针,但这里声明的是int,会导致取出的结果错误
}
hook_func();
//0x7627ad5610
//-22369900
//xiaojianbang

这个才是正确的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook_func() {
var md5Addr = Module.findBaseAddress("libxiaojianbang.so").add(0x1F2C);
var md5Func = new NativeFunction(md5Addr, "pointer", ["pointer", "pointer", "pointer"]);
Interceptor.replace(md5Addr, new NativeCallback(function (env, jclass, data) {
var fridaEnv = Java.vm.tryGetEnv();
console.log(env, jclass, fridaEnv.getStringUtfChars(data).readCString());
var retval = md5Func(env, jclass, data);
console.log(fridaEnv.getStringUtfChars(retval).readCString());
return retval;
}, "pointer", ["pointer", "pointer", "pointer"]));
}
hook_func();
/*
0x7a4fb206c0 0x7fe6e9a5a4 xiaojianbang
41bef1ce7fdc3e42c0e5d940ad74ac00
//logcat中的输出为
CMD5 md5Result: 41bef1ce7fdc3e42c0e5d940ad74ac00
*/

Frida的一些进阶Hook

Hook系统函数dlopen

dlopen()

Hook so层,时机很重要,比如想要在so函数加载完成后,而so中的某个函数还没有被调用之前(或者在JNI_Onload被调用之前-因为有可能该函数被放在JNI_Onload的第一行)对该函数进行Hook。这时候,dlopen就派上用场了。系统加载的so文件和自己加载的so文件,一般都要通过这个函数(最好以spawn方式注入)

dlopen的第0个参数是被加载的so文件路径,第一个参数用于指定加载模式。Hook代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_func() {
var myInit = Module.findBaseAddress("libxiaojianbang.so").add(0x1DE8);
Interceptor.replace(myInit, new NativeCallback(function () {
console.log("replace myInit success");
}, "void", [ ]));
}
function hook_dlopen() {
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
Interceptor.attach(android_dlopen_ext, {
onEnter: function (args) {
var soPath = args[0].readCString();
if(soPath.indexOf("libxiaojianbang.so") != -1) this.hook = true;
}, onLeave: function (retval) {
if(this.hook) hook_func();
}
});
}
hook_dlopen();
// replace myInit success

android_dlopen_ext用于加载so文件,__filename 和 __flags 参数与 dlopen(3) 相同,通过 __info 的 flags 成员提供特定于 Android 的flag。

源码声明如下

1
2
3
4
5
6
7
void * android_dlopen_ext(
const char *__filename,
int __flags,
const android_dlextinfo *__info
)

//The __filename and __flags arguments are the same as for dlopen(3), with the Android-specific flags supplied via the flags member of __info.Available since API level 21.

低android版本中,dlopen用于加载so文件,这里改一下啊,两个都Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_func() {
var myInit = Module.findBaseAddress("libxiaojianbang.so").add(0x1DE8);
Interceptor.replace(myInit, new NativeCallback(function () {
console.log("replace myInit success");
}, "void", [ ]));
}
function hook_dlopen(addr, soName, callback) {
Interceptor.attach(addr, {
onEnter: function(args){
var name = args[0].readCString();
if(name.indexOf(soName) != -1) this.hook = true;
}, onLeave: function(retval){
if(this.hook) callback();
}
});
}
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(dlopen, "libxiaojianbang.so", hook_func);
hook_dlopen(android_dlopen_ext, "libxiaojianbang.so", hook_func);
// replace myInit success

Hook JNI_Onload

在so文件被加载后,系统会自动调用JNI_Onload,但也是在dlopen(或android_dlopen_ext)之后。所以Hook方法与普通函数一致,直接Hook dlopen即可

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 hook_JNIOnload() {
var JNI_OnLoad = Module.findExportByName("libxiaojianbang.so", "JNI_OnLoad");
Interceptor.attach(JNI_OnLoad, {
onEnter: function(args){
console.log(args[0]);
}, onLeave: function(retval){
console.log(retval);
}
});
}
function hook_dlopen(addr, soName, callback) {
Interceptor.attach(addr, {
onEnter: function(args){
var name = args[0].readCString();
if(name.indexOf(soName) != -1) this.hook = true;
}, onLeave: function(retval){
if(this.hook) callback();
}
});
}
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(dlopen, "libxiaojianbang.so", hook_JNIOnload);
hook_dlopen(android_dlopen_ext, "libxiaojianbang.so", hook_JNIOnload);
/*
0x7a4faaf1c0
0x10006 // JNIOnload函数必须返回一个合理的jni版本号,这里返回的是1.6
*/

Hook initarray

在so文件加载过程中,系统会自动调用so文件中的init,init_array,和JNI_Onload函数。前两个实在dlopen函数执行过程中调用的。JNI_Onload是在dlopen函数执行之后调用的。

此时就有一个问题。如果将Hook initarray的代码放在dlopen的onEnter里面,so文件还没有加载,自然无法Hook initarray,onLeave就不用想了。故,需要在dlopen函数内部找一个时机。so已经加载,但init,init_array未被调用。linker (这玩意儿有32位和64位的区别) 的call_constructors满足这些需求。该函数用于调用so文件的init和init_array函数。且all_constructors在linker的符号表中,不需要额外计算偏移地址。但还是要Hook dlopen来监控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
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
function hook_dlopen(addr, soName, callback) {
Interceptor.attach(addr, {
onEnter: function (args) {
var soPath = args[0].readCString();
if(soPath.indexOf(soName) != -1) callback();
}, onLeave: function (retval) {
}
});
}
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(dlopen, "libxiaojianbang.so", hook_call_constructors);
hook_dlopen(android_dlopen_ext, "libxiaojianbang.so", hook_call_constructors);

//枚举符号Hook call_constructors
function hook_call_constructors() {
var _symbols = Process.getModuleByName("linker64").enumerateSymbols();
var call_constructors_addr = null;
for (let i = 0; i < _symbols.length; i++) {
var _symbol = _symbols[i];
if(_symbol.name.indexOf("call_constructors") != -1){
call_constructors_addr = _symbol.address;
}
}
Interceptor.attach(call_constructors_addr, {
onEnter: function (args) {
hook_initarray();
}, onLeave: function (retval) {
}
});
}

//这才是业务代码,上面的都是为这个做铺垫
function hook_initarray(){
var xiaojianbangAddr = Module.findBaseAddress("libxiaojianbang.so");
var func1_addr = xiaojianbangAddr.add(0x1D14);
var func2_addr = xiaojianbangAddr.add(0x1D3C);
var func3_addr = xiaojianbangAddr.add(0x1CEC);
Interceptor.replace(func1_addr, new NativeCallback(function () {
console.log("func1 is replaced!!!");
}, 'void', []));
Interceptor.replace(func2_addr, new NativeCallback(function () {
console.log("func2 is replaced!!!");
}, 'void', []));
Interceptor.replace(func3_addr, new NativeCallback(function () {
console.log("func3 is replaced!!!");
}, 'void', []));
Interceptor.detachAll();
}

Hook pthread_create

一些用于检测的函数通常需要实时运行,就有可能用到pthrea_create开启一个子线程。通过Hook这个函数,可以查看App应用程序为哪些函数开启了线程,就可以有针对性的去分析这些函数的代码逻辑是否与检测相关

其源码声明如下

1
int pthread_create(pthread * tidp,const pthread_attr_t * attr,void * (* start_rtn)(void*),void * arg);

第0个参数为指向线程标识符的指针,第一个参数用来设置线程属性,第二个参数是线程运行函数的起始地址,最后一个参数是传递给线程运行函数的参数

实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
var pthread_create_addr = Module.findExportByName("libc.so", "pthread_create");
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
console.log(args[0], args[1], args[2], args[3]);
var Module = Process.findModuleByAddress(args[2]);
if(Module != null) console.log(Module.name, args[2].sub(Module.base));
},onLeave:function(retval){
}
});

//0x7ffeaa98f8 0x0 0x7557c08d8c 0x0
//libxiaojianbang.so 0x1d8c

监控内存读写

使用Process.setExceptionHandler(callback)可以设置异常处理回调函数。当异常触发时,会调用该函数。在该回调函数中,可通过修改寄存器和内存来让程序从异常中恢复。如果处理了异常,函数最后需要返回true,Frida会立即恢复线程。

直接上代码

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
function hook_dlopen(addr, soName, callback) {
Interceptor.attach(addr, {
onEnter: function (args) {
var soPath = args[0].readCString();
if(soPath.indexOf(soName) != -1) this.hook = true;
}, onLeave: function (retval) {
if (this.hook) {callback()}
}
});
}
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(dlopen, "libxiaojianbang.so", set_read_write_break);
hook_dlopen(android_dlopen_ext, "libxiaojianbang.so", set_read_write_break);

function set_read_write_break(){
Process.setExceptionHandler(function(details) {
console.log(JSON.stringify(details, null, 2));
Memory.protect(details.memory.address, Process.pointerSize, 'rwx');
return true;
});
var addr = Module.findBaseAddress("libxiaojianbang.so").add(0x3DED);
Memory.protect(addr, 8, '---');
}
/*
{
"message": "access violation accessing 0x75ecd31e6b",
"type": "access-violation",
"address": "0x767e9e75c4",
"memory": {
"operation": "read",
"address": "0x75ecd31e6b"
},
"context": {
"pc": "0x767e9e75c4",
"sp": "0x7fc2481e50",
"x0": "0x7681a52188",
"x1": "0x75ecd31e6b",
......
"lr": "0x767e9e73f8"
},
"nativeContext": "0x7fc2480c70"
}
*/

上述代码先Hook乐dlopen函数,然后在onLeave函数中执行set_read_write_break,该函数中,首先设置异常回调,接着把需要监控的内存区域权限设置为”—-”,当异常发生时,会调用异常回调函数,该回调函数接受一个参数,details。该参数是一个对象,记录了发生异常的消息描述message,异常的类型-type,发生异常的地址,发生异常时访问的内存地址以及发生异常时的寄存器信息。还可以在回调函数中打印函数栈,获取地址对应的符号信息等

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
function hook_dlopen(addr, soName, callback) {
Interceptor.attach(addr, {
onEnter: function (args) {
var soPath = args[0].readCString();
if(soPath.indexOf(soName) != -1) this.hook = true;
}, onLeave: function (retval) {
if (this.hook) {callback()}
}
});
}
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(dlopen, "libxiaojianbang.so", set_read_write_break);
hook_dlopen(android_dlopen_ext, "libxiaojianbang.so", set_read_write_break);

function set_read_write_break(){
Process.setExceptionHandler(function(details) {
console.log(JSON.stringify(details, null, 2));
console.log("lr", DebugSymbol.fromAddress(details.context.lr));
console.log("pc", DebugSymbol.fromAddress(details.context.pc));
Memory.protect(details.memory.address, Process.pointerSize, 'rwx');
console.log(Thread.backtrace(details.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
return true;
});
var addr = Module.findBaseAddress("libxiaojianbang.so").add(0x3DED);
Memory.protect(addr, 8, '---');
}
/*
{
"message": "access violation accessing 0x75eaff1e6b",
......
"nativeContext": "0x7fc2480c70"
}
lr 0x767e9e73f8 libc.so!__vfprintf+0x3c
pc 0x767e9e75c4 libc.so!__vfprintf+0x208
0x767e9e73f8 libc.so!__vfprintf+0x3c
......
0x75eafefe08 libxiaojianbang.so!_Z6myInitv+0x20
0x75eafefe34 libxiaojianbang.so!JNI_OnLoad+0x24
......
*/

但是由于Memory.protect修改的时内存页的权限,并不只是修改给定字节数的权限,所以会存在误差,导致监控内存读写的位置不是很精确。监控内存读写最推荐使用的是使用unidbg,这是一个基于unicorn开发的,在PC端模拟执行so文件的框架。

先插个眼

unidbg

函数追踪工具frida-trace

IDA插件:trace_natives

https://github.com/Pr0214/trace_natives

用该插件获取so文件代码段中的所有函数的偏移地址后,再配合frida-trace就可以打印函数内部调用流程。

通过修改traceNatives.py中的代码,可以自行调整汇编指令多余几行的函数地址记录在txt文件中

trace_natives用来生成frida-trace运行时所需的参数,而真正的Hook由frida-trace来完成

并且,frida-trace还支持正则表达式模糊匹配来批量Hook java方法,Hook所有静态注册的jni函数等。代码如下

1
2
frida-trace -UF -j '*!*certificate*/isu'
frida-trace -UF -i "Java_*"

Frida API的简单封装

eeeee,这个感觉没啥好特别注意的。直接上示例代码吧

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
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
hookAddr(soAddr.add(0x1ACC), 5); // Java_com_xiaojianbang_ndk_NativeHelper_add
hookAddr(soAddr.add(0x22A0), 3); // MD5Update

function hookAddr(funcAddr, paramsNum){
var module = Process.findModuleByAddress(funcAddr);
Interceptor.attach(funcAddr, {
onEnter: function(args){
this.logs = [];
this.params = [];
this.logs.push("call " + module.name + "!" + ptr(funcAddr).sub(module.base) + "\n");
for(let i = 0; i < paramsNum; i++){
this.params.push(args[i]);
this.logs.push("this.args" + i + " onEnter: " + printAddr(args[i]));
}
}, onLeave: function(retval){
for(let i = 0; i < paramsNum; i++){
this.logs.push("this.args" + i + " onLeave: " + printAddr(this.params[i]));
}
this.logs.push("retval onLeave: " + printAddr(retval) + "\n");
console.log(this.logs);
}
});
}

function printAddr(addr){
var module = Process.findRangeByAddress(addr);
if(module != null) return hexdump(addr) + "\n";
return ptr(addr) + "\n";
}

/*

call libxiaojianbang.so!0x1acc
......
,this.args2 onEnter: 0x5
,this.args3 onEnter: 0x6
,this.args4 onEnter: 0x7
......
,this.args2 onLeave: 0x5
,this.args3 onLeave: 0x6
,this.args4 onLeave: 0x7
,retval onLeave: 0x12

call libxiaojianbang.so!0x22a0
......
,this.args1 onEnter:
7590c56390 78 69 61 6f 6a 69 61 6e 62 61 6e 67 00 00 c0 41 xiaojianbang...A
,this.args2 onEnter: 0xc
......
,this.args1 onLeave:
7590c56390 78 69 61 6f 6a 69 61 6e 62 61 6e 67 00 00 c0 41 xiaojianbang...A
,this.args2 onLeave: 0xc
,retval onLeave:
7fc209c890 78 69 61 6f 6a 69 61 6e 62 61 6e 67 76 00 00 00 xiaojianbangv...

*/

代码跟踪引擎stalker

Stalker是Frida的代码跟踪引擎,允许跟踪线程,捕获每个调用,每个块,甚至每个执行的指令。它支持AArch64架构以及Intel64和IA-32

示例如下

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
var md5Addr = Module.getExportByName("libxiaojianbang.so", "Java_com_xiaojianbang_ndk_NativeHelper_md5");
Interceptor.attach(md5Addr, {
onEnter: function () {
this.tid = Process.getCurrentThreadId();
Stalker.follow(this.tid, {
events: {
call: true,
},
onReceive(events) {
var _events = Stalker.parse(events);
for (var i = 0; i < _events.length; i++) {
console.log(_events[i]);
}
},
});
}, onLeave: function () {
Stalker.unfollow(this.tid);
}
});
/*
call,0x7590e77054,0x7590d63f60,0
call,0x7590e035a8,0x7590e0b24c,0
......
这个脚本容易崩。。。
*/

Stalker.follow源码声明如下

1
2
3
4
5
6
declare namespace Stalker {
function follow(threadId?: ThreadId, options?: StalkerOptions): void;
function unfollow(threadId?: ThreadId): void;
function parse(events: ArrayBuffer, options?: StalkerParseOptions): StalkerEventFull[] | StalkerEventBare[];
......
}

第0个参数是threaID,用于指定跟踪的线程(默认是当前线程)。用Process.getCurrentThreadId()来获取当前线程id,赋值给this.id,并传递给Stalker.unfollow,用于解除跟踪,函数第一个参数是options,类型为StalkerOptions,用于自定义跟踪选项。跟踪选项源码声明如下

1
2
3
4
5
6
7
8
9
10
11
12
interface StalkerOptions {
events?: {
call?: boolean;
ret?: boolean;
exec?: boolean;
block?: boolean;
compile?: boolean;
};
onReceive?: (events: ArrayBuffer) => void;
onCallSummary?: (summary: StalkerCallSummary) => void;
......
}

events用于指定生成哪些事件,传递给onReceive和onCallSummary。比如当events里面的call为true,Stalker在跟踪代码时,遇到函数调用,就会记录一些信息传递给onReceive和onCallSummary。events还支持在执行ret指令,所有指令exec,基本块block时生成事件。

events参数可以用Stalker.parse来解析。返回结果为StalkerEventFull数组或者StalkerEventBare数组。故使用循环即可遍历这些数组。并且Stalker.parse可以指定选项,StalkerParseOptions,源码声明如下

1
2
3
4
5
6
7
8
9
10
11
12
interface StalkerParseOptions {
annotate?: boolean;
stringify?: boolean;
}
type StalkerEventFull = StalkerCallEventFull | StalkerRetEventFull | StalkerExecEventFull |
StalkerBlockEventFull | StalkerCompileEventFull;
type StalkerEventBare = StalkerCallEventBare | StalkerRetEventBare | StalkerExecEventBare |
StalkerBlockEventBare | StalkerCompileEventBare;

type StalkerCallEventFull = [ "call", NativePointer | string, NativePointer | string, number ];
type StalkerCallEventBare = [ NativePointer | string, NativePointer | string, number ];
......

在示例代码的输出中 call,0x7590e77054,0x7590d63f60,0。对应上面来看。

从左到右分别表示事件类型为call,发生call时的地址,call所调用的函数地址。将示例代码稍作修改如下

onReceive事件通常用于查看函数流程,onCallSummary事件用于查看对应地址被调用次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onReceive(events) {
var _events = Stalker.parse(events);
for (var i = 0; i < _events.length; i++) {
var addr1 = _events[i][1];
var module1 = Process.findModuleByAddress(addr1);
if (module1 && module1.name == "libxiaojianbang.so") {
var addr2 = _events[i][2];
var module2 = Process.findModuleByAddress(addr2);
console.log(module1.name, addr1.sub(module1.base), module2.name, addr2.sub(module2.base));
}
}
}
/*
libxiaojianbang.so 0x1f64 libxiaojianbang.so 0x1440
libxiaojianbang.so 0x1710 libxiaojianbang.so 0x1630
libxiaojianbang.so 0x187c libart.so 0x34f218 这个表示call的地址在libxiaojianbang.so的地址为0x187c处 ,被调用的函数定义在libart.so的0x34f128
......
*/

注意Stalker.follow会跟踪所有so文件的调用,包括一些系统so文件,所以容易崩,做好过滤和注释掉一些不必要的。

再来介绍一下onCallSummary的使用,该回调函数接受一个类型为StalkerCallSummary的参数,源码声明如下

1
2
3
interface StalkerCallSummary {
[target: string]: number;
}

summary是一个对象,属性名时被调用的函数地址,属性值是被调用的次数

看看它的写法

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 md5Addr = Module.getExportByName("libxiaojianbang.so", "Java_com_xiaojianbang_ndk_NativeHelper_md5");
Interceptor.attach(md5Addr, {
onEnter: function () {
this.tid = Process.getCurrentThreadId();
Stalker.follow(this.tid, {
events: {
call: true,
},
onCallSummary(summary) {
for (const addr in summary) {
var module = Process.findModuleByAddress(addr);
if (module && module.name == "libxiaojianbang.so") {
const num = summary[addr];
console.log(module.name, ptr(addr).sub(module.base), num);
}
}
},
});
}, onLeave: function () {
Stalker.unfollow(this.tid);
}
});
/*
libxiaojianbang.so 0x14a0 2
libxiaojianbang.so 0x15e0 1
libxiaojianbang.so 0x1610 1
libxiaojianbang.so 0x1460 1
libxiaojianbang.so 0x15d0 16
......
哇超,真的容易崩
*/

找到目标so的信息,并将那些地址全部Hook,打印参数和返回值,以此来缩小关键函数范围,甚至对于封装的比较好的加密算法,可以直接得到密钥等关键信息。

但是需要注意Stalker记录的call函数地址,包含plt表中的地址,这些地址无法被Hook,需要剔除。