0%


title:So层Hook

So层Hook

示例应用的so文件加载时机是在一些按钮被点击后才加载,注意开启应用后注入,然后点击按钮,才会输出相关信息

像那种一开始就有加载so的代码,先点击CADD(其他能加载so的按钮也行)一下,再注入,防止找不到so文件。

最基本的,加载so

1
2
3
4
var module = Process.findModuleByName("libxiaojianbang.so");
if(module!=NULL){
console.log(JSON.stringify(module));//顺带输出一些基础信息,模块名,加载到内存后的首地址
}

这里也可以用getModuleByName,不过该函数如果找不到对应的模块会抛出异常,前者找不到会返回NULL。据说frida里,find和get开头的一样的函数都是这样。

Module对象的属性可以通过JSON.stringfy方法来打印

通过地址获取模块;主要通过以下的api

1
2
findModuleByAddress(addres:NativePointerValue):Module | NULL;
getModuleByAddress(addres:NativePointerValue):Module;

枚举导入表

1
2
var imports = Process.getModuleByName("libxiaojianbang.so").enumerateImports();
console.log(JSON.stringfy(imports[0]));

得到模块中指定函数的地址,进行一个遍历

1
2
3
4
5
6
7
8
9
10
var improts = Process.findModuleByName("libxiaojianbang.so").enumerateImports();
var sprintf_addr = null;
for(let i = 0; i < improts.length; i++){
let _import = improts[i];
if(_import.name.indexOf("sprintf") != -1){
sprintf_addr = _import.address;
break;
}
}
console.log("sprintf_addr: ", sprintf_addr);

枚举导出表

1
2
var exports = Process.getModuleByName("libxiaojianbang.so").enumerateExports();
console.log(JSON.stringify(exports[0]));

同样的,得到模块中指定函数的地址,进行一个遍历

1
2
3
4
5
6
7
8
9
10
var exports = Process.findModuleByName("libxiaojianbang.so").enumerateExports();
var MD5Final_addr = null;
for(let i = 0; i < exports.length; i++){
let _export = exports[i];
if(_export.name.indexOf("_Z8MD5FinalP7MD5_CTXPh") != -1){
MD5Final_addr = _export.address;
break;
}
}
console.log("MD5Final_addr: ", MD5Final_addr);

枚举模块中的符号表

将导入表导出表都给遍历了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function findFuncInWitchSo(funcName) {
var modules = Process.enumerateModules();
for (let i = 0; i < modules.length; i++) {
let module = modules[i];
let _symbols = module.enumerateSymbols();
for (let j = 0; j < _symbols.length; j++) {
let _symbol = _symbols[i];
if(_symbol.name == funcName){
return module.name + " " + JSON.stringify(_symbol);
}
}
let _exports = module.enumerateExports();
for (let j = 0; j < _exports.length; j++) {
let _export = _exports[j];
if(_export.name == funcName){
return module.name + " " + JSON.stringify(_export);
}
}
}
return null;
}
console.log(findFuncInWitchSo('strcat'));

Module的源码声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
declare class Module {
name: string; //模块名
base: NativePointer; //模块基址
size: number; //模块大小
path: string; //模块所在路径
enumerateImports(): ModuleImportDetails[]; //枚举导入表
enumerateExports(): ModuleExportDetails[]; //枚举导出表
enumerateSymbols(): ModuleSymbolDetails[]; //枚举符号表
findExportByName(exportName: string): NativePointer | null; //获取导出函数地址
getExportByName(exportName: string): NativePointer; //获取导出函数地址
static load(name: string): Module; //加载指定模块
static findBaseAddress(name: string): NativePointer | null; //获取模块基址
static getBaseAddress(name: string): NativePointer; //获取模块基址
//获取导出函数地址
static findExportByName(moduleName: string | null, exportName: string): NativePointer | null;
//获取导出函数地址
static getExportByName(moduleName: string | null, exportName: string): NativePointer;
}

正式开始Hook so函数

第一步,得到目标函数的地址

除了上述方法外,用Frida的API也可以

首先是静态方法和实例方法

静态方法可以直接通过 类名.方法名 的方式来访问,并传入两个参数,第一个是string类型的模块名,第二个是string类型的导出函数名(以汇编界面显示的名称位准,如上文的MD5_Final)

实例方法可以先获取Module对象,再通过 对象.方法名的方式来访问,只需要传入string类型的导出函数名即可,并返回NativePointer类型的函数地址。得到NativePointer类型的函数地址后,可以使用Interceptor的attach函数来进行Hook,也可以使用Interceptor的detachAll函数来解除Hook。

1
2
3
4
5
6
declare namespace Interceptor{
function attach(target:NativePointerValue,callbacksOrProbe:InvocationListenerCallbacks | InstructionProbeCallback,data?:NativePointerValue):
InvocationListener;
function detatchAll():void;
....
}

可以看到,detachAll不需要任何参数,而attach则需要传入函数地址和被Hook函数触发时执行的回调函数。

Interceptor通过inlinehook的方式拦截代码执行,会修改被Hook处的16个字节

需要注意的时,Java native层的函数,前两个参数是 JNIEnv*,jcalss(静态)/jobject(实例),后面的参数就是Java层中声明时对应的函数。

arm64中使用x0-x7来传递参数,如果参数数量多于8个,则需要从栈中获取(实际上传参还得考虑浮点寄存器,w开头的32位寄存器;另外arm32中是使用r0-r3寄存器来传递参数,超出的同样入栈)

arm64使用x0或w0寄存器存放函数返回值,arm32使用r0,r0如果放不下,则会使用r1

接下来来看看hexdump的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
declare function hexdump(target: ArrayBuffer | NativePointerValue, options?: HexdumpOptions): string;
interface HexdumpOptions {
offset?: number; //从给定的target偏移一定字节数开始dump,默认为0
length?: number; //指定dump的字节数,注意需要十进制的数值,默认16*16
header?: boolean; //返回的string中是否包含标题,默认为true
ansi?: boolean; //返回的string是否带(颜色?),默认为false
}

//使用实例

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var data = hexdump(soAddr, {length: 16, header: false});
console.log(data);
// 74c6c39000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var data = hexdump(soAddr, {offset: 4, length: 16, header: false});
console.log(data);
// 74c6c39004 02 01 01 00 00 00 00 00 00 00 00 00

Hook 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
declare class Module {
......
static findBaseAddress(name: string): NativePointer | null;
static getBaseAddress(name: string): NativePointer;
}

//使用实例

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log(soAddr);
//Module.getBaseAddress("libxiaojianbang.so")
//soAddr: 0x7b2e6c0000

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var funcAddr = soAddr.add(0x1ACC);

//NativePointer源码

declare class NativePointer {
constructor(v: string | number | UInt64 | Int64 | NativePointerValue);
add(v: NativePointerValue | UInt64 | Int64 | number | string): NativePointer;
...... //constructor是NativePointer的构造函数,可以用new NativePointer(...)的方式吧数值,字符串等类型转为NativePointer类型;ptr <=> new NativePointer
}

var soAddr = 0x77ab999000;
console.log( ptr(soAddr).add(0x1A0C) ); // ptr <=> new NativePointer

var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var sub_1A0C = soAddr.add(0x1ACC);
Interceptor.attach(sub_1ACC, {
onEnter: function (args) {
console.log("sub_1ACC onEnter args[0]: ", args[0]);
console.log("sub_1ACC onEnter args[1]: ", args[1]);
console.log("sub_1ACC onEnter args[2]: ", args[2]);
console.log("sub_1ACC onEnter args[3]: ", args[3]);
console.log("sub_1ACC onEnter args[4]: ", args[4]);
}, onLeave: function (retval) {
console.log("sub_1ACC onLeave retval: ", retval);
}
});
//sub_1ACC onEnter args[0]: 0x7bc3bd66c0
//sub_1ACC onEnter args[1]: 0x7fda079fb4
//sub_1ACC onEnter args[2]: 0x5
//sub_1ACC onEnter args[3]: 0x6
//sub_1ACC onEnter args[4]: 0x7
//sub_1ACC onLeave retval: 0x12

当然了,函数地址也是可以手算的:

  1. thumb指令下:so文件基址+函数地址相对so文件的偏移量+1
  2. ARM指令下:so文件基址+函数地址相对so文件的偏移量

如何判断是thumb还是arm:

thumd的opcode是两个字节,arm是4个字节

当然了,其实32位的基本是thumb,64位基本是arm

获取指针参数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var MD5Final = soAddr.add(0x3A78);
Interceptor.attach(MD5Final, {
onEnter: function (args) {
this.args1 = args[1];
}, onLeave: function (retval) {
console.log(hexdump(this.args1));
}
});
/*
7ffc689cc8 41 be f1 ce 7f dc 3e 42 c0 e5 d9 40 ad 74 ac 00 A.....>B...@.t..
//logcat中的输出结果
//CMD5 md5Result: 41bef1ce7fdc3e42c0e5d940ad74ac00
*/

Frida inlineHook获取函数执行结果(hook精确到某一条指令,这个需要找到在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
var hookAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x1AF4);
Interceptor.attach(hookAddr, {
onEnter: function (args) {
console.log("onEnter x8: ", this.context.x8.toInt32());
console.log("onEnter x9: ", this.context.x9.toInt32());
}, onLeave: function (retval) {
console.log("onLeave x0: ", this.context.x0.toInt32());
}
});
/*
onEnter x8: 11
onEnter x9: 7
onLeave x0: 18
*/

var hookAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x1FF4);
Interceptor.attach(hookAddr, {
onEnter: function (args) {
console.log("onEnter: ", this.context.x1);
console.log("onEnter: ", hexdump(this.context.x1));
}, onLeave: function (retval) {
}
});
/*
onEnter: 0x7d9016ae80
7d9016ae80 78 69 61 6f 6a 69 61 6e 62 61 6e 67 00 00 c0 41 xiaojianbang...A
*/

Frida修改函数参数与返回值

  1. 修改函数参数和返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var addFunc = soAddr.add(0x1ACC);
Interceptor.attach(addFunc, {
onEnter: function (args) {
args[2] = ptr(100);
//this.context.x2 = 100;
console.log(args[2].toInt32());
}, onLeave: function (retval) {
console.log(retval.toInt32());
retval.replace(100);
//this.context.x0 = 100;
}
});
/*
args[2]: 100
retval: 113
//logcat中的输出为
//CADD addResult: 100
*/

需要注意的是args[2] = ptr(100);这一条语句,因为args是NativePointer类型的数组,故只接受NativePinter类型的值,除次方法外,也可以用this.context.x2=100的直接修改寄存器的值方式来修改。

onLeave中新增了replace方法,直接修改返回值

  1. 字符串修改
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
var MD5Update = Module.findExportByName("libxiaojianbang.so", "_Z9MD5UpdateP7MD5_CTXPhj");
Interceptor.attach(MD5Update, {
onEnter: function (args) {
console.log(hexdump(args[1])); //hexdump用于从给定的地址开始,dump一段内存
console.log(args[2].toInt32());
}, onLeave: function (retval) {
}
});
/*
7ad0ca9f40 78 69 61 6f 6a 69 61 6e 62 61 6e 67 00 00 c0 41 xiaojianbang...A
12

7ad042e000 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
44

7fda079e50 60 00 00 00 00 00 00 00 ed 17 ae 39 cf 5d 07 be `..........9.]..
8
//logcat中的输出结果
//CMD5 md5Result: 41bef1ce7fdc3e42c0e5d940ad74ac00
*/

function stringToBytes(str){
return hexToBytes(stringToHex(str));
}
function stringToHex(str) {
return str.split("").map(function(c) {
return ("0" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}

//修改char*指向的地址内存数据

var MD5Update = Module.findExportByName("libxiaojianbang.so", "_Z9MD5UpdateP7MD5_CTXPhj");
Interceptor.attach(MD5Update, {
onEnter: function (args) {
if(args[1].readCString() == "xiaojianbang"){
let newStr = "xiaojian\0";
args[1].writeByteArray(stringToBytes(newStr));
console.log(hexdump(args[1]));
args[2] = ptr(newStr.length - 1);//减一是因为"\0"占了一个,特别注意修改的字符串长度别大于原字符串
console.log(args[2].toInt32());
}
}, onLeave: function (retval) {
}
});
/*
7b2e35bf50 78 69 61 6f 6a 69 61 6e 00 61 6e 67 00 00 c0 41 xiaojian.ang...A
8
//logcat中的输出结果
//CMD5 md5Result: 66b0451b7a00d82790d4910a7a3a4162
*/

//将内存中已有的字符串赋值给参数
var MD5Update = Module.findExportByName("libxiaojianbang.so", "_Z9MD5UpdateP7MD5_CTXPhj");
var strAddr = Module.findBaseAddress("libxiaojianbang.so").add(0x3CFD);
Interceptor.attach(MD5Update, {
onEnter: function (args) {
if(args[1].readCString() == "xiaojianbang"){
args[1] = strAddr;
console.log(hexdump(args[1]));
args[2] = ptr(strAddr.readCString().length);
console.log(args[2].toInt32());
}
}, onLeave: function (retval) {
}
});
/*
7ae6787cfd 63 6f 6d 2f 78 69 61 6f 6a 69 61 6e 62 61 6e 67 com/xiaojianbang
7ae6787d0d 2f 6e 64 6b 2f 4e 61 74 69 76 65 48 65 6c 70 65 /ndk/NativeHelpe
7ae6787d1d 72 00 65 6e 63 6f 64 65 00 28 29 4c 6a 61 76 61 r.encode.()Ljava
33
//logcat中的输出结果
//CMD5 md5Result: f6190c61b22ec8efe63fade2c47d8a49
*/

//stringToBytes函数的定义,参考上一小节

//修改MD5_CTX结构体中的buffer和count
var MD5Update = Module.findExportByName("libxiaojianbang.so", "_Z9MD5UpdateP7MD5_CTXPhj");
Interceptor.attach(MD5Update, {
onEnter: function (args) {
this.args0 = args[0];
this.args1 = args[1];
}, onLeave: function (retval) {
if(this.args1.readCString() == "xiaojianbang"){
let newStr = "jianbang";
this.args0.add(24).writeByteArray(stringToBytes(newStr));
//这个add(24)是因为MD5_CTX结构体前8个字节用于记录原始明文的bit长度,后16个字节是初始化的魔数,所以从第24个字节后就是用writeByteArray写入明文数据等一系列操作
console.log(hexdump(this.args0.writeInt(newStr.length * 8)));
}
}
});
/*
7fda079f08 40 00 00 00 00 00 00 00 01 23 45 67 89 ab cd ef @........#Eg....
7fda079f18 fe dc ba 98 76 54 32 10 6a 69 61 6e 62 61 6e 67 ....vT2.jianbang
7fda079f28 62 61 6e 67 00 00 00 00 d0 a0 07 da 7f 00 00 00 bang............
7fda079f38 78 b2 2f 3e 7b 00 00 00 4c b2 2f 3e 7b 00 00 00 x./>{...L./>{...
7fda079f48 00 00 00 00 00 00 00 00 06 00 00 00 00 00 00 00 ................
7fda079f58 63 01 63 01 00 00 00 00 10 00 00 00 10 00 00 00 c.c.............
//logcat中的输出结果
//CMD5 md5Result: ea54ded1bd8a592dd826fb919687f13f
*/

var MD5Update = Module.findExportByName("libxiaojianbang.so", "_Z9MD5UpdateP7MD5_CTXPhj");
var newStr = "xiaojianbang&liruyi";
var newStrAddr = Memory.allocUtf8String(newStr);
Interceptor.attach(MD5Update, {
onEnter: function (args) {
if(args[1].readCString() == "xiaojianbang"){
args[1] = newStrAddr;
console.log(hexdump(args[1]));
args[2] = ptr(newStr.length);
console.log(args[2].toInt32());
}
}, onLeave: function (retval) {
}
});
/*
7b34a80060 78 69 61 6f 6a 69 61 6e 62 61 6e 67 26 6c 69 72 xiaojianbang&lir
7b34a80070 75 79 69 00 00 00 00 00 23 00 00 00 00 00 00 00 uyi.....#.......
19
//logcat中的输出结果
//CMD5 md5Result: 8f1968f06a1e62bb3d83119352cc26cc
*/

readCString是NativePointer的方法,读取C字符串,返回JavaScript的string类型的字符串,可以传入一个参数作为指定读取的字节数,没有的话就读到结束标识符”\0”