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”

Objection

常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
android hooking search classes 关键字 #(关键字可选,最后的classes是可替换为其他东西的,如methods)
android hooking list class_methods <路径.类名> #(同样可替换,例如activity
android hooking list classes #列出所有已加载的类
对指定方法进行hook : android hooking watch class_method <路径.类名.方法名> #(这个命令可以在后面加参数)
对指定类的所有函数hook :android hooking watch class 类名
Hook类的所有方法(不含构造方法):android hooking watch class <路径.类名>
Hook类的所有构造方法: android hooking watch class_method <路径.类名.$init>
Hook方法的所有重载: android hooking watch class_method <路径.类名.方法名>
Hook方法的参数,返回值和调用栈: android hooking watch class_method <路径.类名.方法名> --dump-args --dump-return --dump-backtrace
查看Hook了多少个类:jobs list
取消Hook:jobs kill <taskID>
搜索堆中的实例: android heap search instances <类名>
在应用启动之前就Hook: objection -g <进程名> explore --startup-command "android hooking watch class '<路径.类名>' "

JNI函数的Hook与快速定位

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

第一步也是要先得到JNI函数的地址,有两种方式

  1. 枚举libart的符号表,得到对应的JNI函数地址后Hook
  2. 通过计算地址的方式来Hook,比较麻烦:先得到JNEnv结构体的地址,再通过偏移得到对应JNI函数指针的地址,最后通过指针得到JNI函数的地址。需要注意的是**32位(4字节)64位(8字节)**的指针长度不同会导致偏移地址不同

如果用C/C++开发so文件,JNIEnv*指针变量最终都指向JNINativePoniter结构体。

要获取JNIEnv*指针变量的内存地址,Frida中提供了相应的方法来获取,部分源码如下

1
2
3
4
5
6
7
8
9
declare namespace Java {
...
const vm: VM;
interface VM {
...
getEnv():Env;
tryGetEnv():Env | null;
}
}

getEnv和tryGetEnv都可以返回Frida包装后的JNIEnv对象,如果没有找到,前者会抛出异常错误,后者会返回一个NULL

1
2
console.log("env:",JSON.stringify(Java.vm.tryGetEnv()));
//env: {"handle":"0x76c5e1af30","vm":{"handle":"0x76d5e07c10"}}

handle属性记录的是原始JNIEnv* 指针变量的内存地址,所存放的就是JNIEnv结构体的地址。利用readPointer可以将改内存的数据转化为地址

所以说,获得JNIEnv结构体的地址的最直接的方式

1
Java.vm.tryGetEnv().handle.readPointer()

1

1
2
3
4
struct JNINativeInterface{
...
jstring (*NewStringUTF)(JNIEnv* ,const char*);
}

env.handle的类型是NativePointer,但env的类型不是,所以不能直接env.NativePointer。但可以通过Memory.readPointer(env)或者ptr(env).NativePointer()来达到目的,推荐使用后者

枚举libart符号表来Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook_jni() {
var _symbols = Process.getModuleByName("libart.so").enumerateSymbols();
var newStringUtf = null;
for (let i = 0; i < _symbols.length; i++) {
var _symbol = _symbols[i];
if(_symbol.name.indexOf("CheckJNI") == -1 && _symbol.name.indexOf("NewStringUTF") != -1){
newStringUtf = _symbol.address;
}
}
Interceptor.attach(newStringUtf, {
onEnter: function (args) {
console.log("newStringUtf args: ", args[1].readCString());
}, onLeave: function (retval) {
console.log("newStringUtf retval: ", retval);
}
});
}
hook_jni();

这些api在so层hook都讲过的,都能看懂,只是这过滤了一下包含CheckJNI的符号

再来介绍一下Process.pointerSize这个方法,返回当前进程的指针大小,这样就不用可以去区别32位和64位指针大小的区别了,也解决了因指针大小不一样而导致偏移量不一样的问题。

1
2
3
4
5
6
var envAddr = Java.vm.tryGetEnv().handle.readPointer();
var NewStringUTF = envAddr.add(167 * Process.pointerSize);
var NewStringUTFAddr = envAddr.add(167 * Process.pointerSize).readPointer();
console.log(hexdump(NewStringUTF));//NewStringUTF函数指针的内存地址
console.log(hexdump(NewStringUTFAddr));//NewStringUTFAddr就是NewStringUTF函数的地址(不是指针的)
console.log(Instruction.parse(NewStringUTFAddr).toString());

当得到了函数地址后,可以通过Frida的API,Instruction.parse(NewStringUTFAddr).toString()方法,可以将指令转化为汇编代码。

Hook代码

1
2
3
4
5
6
7
8
9
10
11
12
function hook_jni2() {
var envAddr = Java.vm.tryGetEnv().handle.readPointer();
var NewStringUTFAddr = envAddr.add(167 * Process.pointerSize).readPointer();
Interceptor.attach(NewStringUTFAddr, {
onEnter: function (args) {
console.log("FindClass args: ", args[1].readCString());
}, onLeave: function (retval) {
console.log("FindClass retval: ", retval);
}
});
}
hook_jni2();

利用Frida API主动调用JNI函数

前文提到过,通过Java.vm.tryGetEnv()就可以得到Frida包装过的JNIEnv对象,然后就可以接着通过Frida封装的API来调用JNI函数。

这里需要注意,JNI的函数命名采用大驼峰命名法,Frida封装的采用小驼峰命名法,并且UTF只有首字母大写。

举两个例子:

  1. JNI-NewStringUTF;Frida-newStringUtf;
  2. JNI-GetStringUTFChars;Frida-getStringUtfChars

其余的到下面的链接看

https://github.com/frida/frida-java-bridge

另外两者的参数也不同,JNI的参数第一个都是JNIEnv*,frida不需要传这个参数

so层文件打印函数栈

在Frida中可以通过Thread.backtrace来获取函数栈,看看源码

1
2
3
4
5
6
7
delcare namespace Thread{

function backtrace(context?:CpuContext,backtracer?:Backtracer):NativePointer[];

function sleep(delay:number):void;

}

backtrace用于获取函数栈,掺入两个可省略的参数,返回NativePointer类型的地址数组,但两个参数不推荐省略。

sleep接受一个数值用于挂起当前线程,并在多少秒后恢复

backtrace参数需要传入BackTracer类型

1
2
3
4
declare class BackTracer{
static ACCURATE: Baktracer;//这个获取到的信息比较准确,但不是所有情况都适用
static FUZZY: Backtracer;//这个可能不是很准确,但对任何二进制文件都有效
}

实际情况可以两个都试一下。

DebugSymbol.fromAddress。该方法用于获取对应地址的调试信息

其源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
declare class DebugSymbol{
address: NativePointer; //符号所在地址
name: string | null; //符号名
moduleName: string | null; //符号所在模块名
fileName: string | null; //符号所在的文件名
lineNumber: number | null; //符号在文件里的行号
static fromAddress(address: NativePointerValue): DebugSymbol; //通过地址获取符号
static fromName(name: string):DebugSymbol; //通过名字获取符号
//传入函数名,返回该函数所在地址;如果有多个同名函数,则返回第一个,没找到则报错。
static getFunctionByName(name: string): NativePointer;
//传入函数名,以数组形式返回函数地址。
static findFunctionsNamed(name: string): NativePointer[];
static findFunctionsMatching(glod:string): NativePointer[];
static load(path:string):void; //加载指定模块中的符号
toString():string;
}

来个简单测试

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 debsym = DebugSymbol.fromName("strcat");
console.log("address: ", debsym.address);
console.log("name: ", debsym.name);
console.log("moduleName: ", debsym.moduleName);
console.log("fileName: ", debsym.fileName);
console.log("lineNumber: ", debsym.lineNumber);
console.log("toString: ", debsym.toString());

console.log("getFunctionByName: ", DebugSymbol.getFunctionByName("strcat"));
console.log("findFunctionsNamed: ", DebugSymbol.findFunctionsNamed("JNI_OnLoad"));
console.log("findFunctionsMatching: ", DebugSymbol.findFunctionsMatching("JNI_OnLoad"));

/*

address: 0x7896f75230
name: strcat
moduleName: libc.so
fileName:
lineNumber: 0
toString: 0x7896f75230 libc.so!strcat
getFunctionByName: 0x7896f75230
findFunctionsNamed: 0x75fd9e1910,0x75fd9a2ee8,0x75fd957fb0,0x75fc0abd90,0x75f0be3070,0x75ec6ad2a0,0x75ecb22020,0x75ec6380b0,0x75ec5c8020,0x75e8b69208,0x75f4620c48
findFunctionsMatching: 0x75e8b69208,0x75ec5c8020,0x75ec6380b0,0x75ec6ad2a0,0x75ecb22020,0x75f0be3070,0x75f4620c48,0x75fc0abd90,0x75fd957fb0,0x75fd9a2ee8,0x75fd9e1910

*/

so层主动调用任意函数

首先介绍一个Frida的API

1
new NativeFunction(address,returnType,argTypes[,abi])

该API需要传入函数地址,返回值类型,参数类型数组和可省略的abi数组。

returnTypes和argTypes支持很多种类型: void,pointer,int,uint,(u)long,(u)char,float,double,int8,uint8,(u)int16,(u)int32,(u)int64,bool,size_t,ssize_t

其实传给new NativeFunction的返回值和参数类型不用非常准确也是可以调用函数的

1
2
3
4
5
6
7
8
9
10
11
12
Java.perform(function () {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var funAddr = soAddr.add(0x16BC);
var jstr2cstr = new NativeFunction(funAddr, 'pointer', ['pointer','pointer']);
var env = Java.vm.tryGetEnv();
//主动调用jni函数newStringUtf,将JavaScript的字符串转为Java字符串
var jstring = env.newStringUtf("xiaojianbang");
var retval = jstr2cstr(env.handle, jstring);
//var retval = jstr2cstr(env, jstring);
console.log(retval.readCString());
});
//xiaojianbang

[,abi]的个数为除了JNI特有的参数外(如JNIEnv*,jclass/jobject),函数本身有的参数加一个返回值

通过NativeFunction主动调用JNI函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var symbols = Process.getModuleByName("libart.so").enumerateSymbols();
var NewStringUTFAddr = null;
var GetStringUTFCharsAddr = null;
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if(symbol.name.indexOf("CheckJNI") == -1 && symbol.name.indexOf("NewStringUTF") != -1){
NewStringUTFAddr = symbol.address;
}else if (symbol.name.indexOf("CheckJNI") == -1 && symbol.name.indexOf("GetStringUTFChars") != -1){
GetStringUTFCharsAddr = symbol.address;
}
}
var NewStringUTF = new NativeFunction(NewStringUTFAddr, 'pointer', ['pointer', 'pointer']);
var GetStringUTFChars = new NativeFunction(GetStringUTFCharsAddr, 'pointer', ['pointer', 'pointer', 'pointer']);

var jstring = NewStringUTF(Java.vm.tryGetEnv().handle, Memory.allocUtf8String("xiaojianbang"));
console.log(jstring);

var cstr = GetStringUTFChars(Java.vm.tryGetEnv(), jstring, ptr(0));
console.log(cstr.readCString());
/*
0x1
xiaojianbang
*/

查看上面两个函数NewStringUTF;GetStringUTFChars在jni中的声明

1
2
3
4
5
struct JNINativeInterface{
...
jstring (*NewStringUTF)(JNIEnv*,const char*);
const char* (*getStringUTFChars)(JNIEnv*, jstring,jboolean*);
};

在C语言中,jboolean可以用一个字节的数值代替,故相关代码也可改为以下形式

1
2
3
var cstr = GetStringUTFChars(Java.vm.tryGetEnv(),  jstring,  Memory.alloc(1).writeS8(1));
console.log(cstr.readCString());
//xiaojianbang

而且在使用new NativeFunction声明函数指针时,某些参数与jni.h中不一致也是可以调用的,如GetStringUTFChars,可以只传两个参数

1
2
3
4
5
//......
var GetStringUTFChars = new NativeFunction(GetStringUTFCharsAddr, 'pointer', ['pointer', 'pointer']);
var cstr = GetStringUTFChars(Java.vm.tryGetEnv(), jstring);
console.log(cstr.readCString());
//xiaojianbang

JNI函数注册的快速定位

  1. JNI静态注册可以Hook dlsym 静态注册的方式在一开始并没有绑定so层的函数,当Java的native函数首次被调用,系统会按规则构建出对应的函数名,通过dlsym去每一个so文件中寻找符号,找到后进行绑定。
  2. JNI函数动态注册可以Hook RegisterNatives,这个函数就是用来动态注册的,不必多说
  3. Hook其他关键的JNI函数,如NewStringUTF,再打印函数栈。当然也可以试试jnitrace

Hook dlsym

dlsym的声明

1
2
void * dlsym(void * handle,const char* symbol);
//第一个参数是dlopen函数之后返回的句柄,第二个是要求获取的函数的名称,返回值是void *,指向函数的地址。

handle时使用dlopen函数之后返回的句柄,symbol是要求获取函数的名称,返回值是void* 指向函数的地址。具体看

dlsym()

1
2
3
4
5
6
7
8
9
10
11
12
13
var dlsymAddr = Module.findExportByName("libdl.so", "dlsym");
Interceptor.attach(dlsymAddr, {
onEnter: function (args) {
this.args1 = args[1];
}, onLeave: function (retval) {
var module = Process.findModuleByAddress(retval);
if(module == null) return;
console.log(this.args1.readCString(), module.name, retval, retval.sub(module.base));
}
});
//Java_com_xiaojianbang_ndk_NativeHelper_add 0x75072acacc单击cadd
//Java_com_xiaojianbang_ndk_NativeHelper_md5 0x75072acf2c单机cmd5

经过测试发现,上面那两条信息只能输出一次,就是在那两个按钮在被初次点击的时候。因为静态注册的native函数首次被调用才会经过dlsym函数,之后就不再触发。

而在jni动态注册中,在系统加载完so文件后,才去获取JNI_Onload函数地址,使用的也是dlsym

Hook RegisterNatives获取函数地址

RegisterNatives定义在libart.so中,可以通过枚举符号来获取地址。

1
2
3
4
5
6
7
8
9
var RegisterNativesAddr = null;
var _symbols = Process.findModuleByName("libart.so").enumerateSymbols();
for (var i = 0; i < _symbols.length; i++) {
var _symbol = _symbols[i];
if (_symbol.name.indexOf("CheckJNI") == -1 && _symbol.name.indexOf("RegisterNatives") != -1) {
RegisterNativesAddr = _symbols[i].address;
}
}
console.log(RegisterNativesAddr);

RegisterNatives在jni.h,我觉得已经不用再看了,就是一个结构体,其成员:第0个是在函数java层的名字,第二个是签名,第三个是函数在cpp文件中的指针一般为(void )开头,参数的话有四个JNIEnv * ,jcalss,JNINativeMethod ,jint

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
28
var RegisterNativesAddr = null;
var _symbols = Process.findModuleByName("libart.so").enumerateSymbols();
for (var i = 0; i < _symbols.length; i++) {
var _symbol = _symbols[i];
if (_symbol.name.indexOf("CheckJNI") == -1 && _symbol.name.indexOf("RegisterNatives") != -1) {
RegisterNativesAddr = _symbols[i].address;
}
}

Interceptor.attach(RegisterNativesAddr, {
onEnter: function (args) {
var env = Java.vm.tryGetEnv();
var className = env.getClassName(args[1]);
var methodCount = args[3].toInt32();

for (let i = 0; i < methodCount; i++) {
var methodName = args[2].add(Process.pointerSize * 3 * i)
.readPointer().readCString();
var signature = args[2].add(Process.pointerSize * 3 * i)
.add(Process.pointerSize).readPointer().readCString();
var fnPtr = args[2].add(Process.pointerSize * 3 * i)
.add(Process.pointerSize * 2).readPointer();
var module = Process.findModuleByAddress(fnPtr);
console.log(className, methodName, signature, fnPtr, module.name, fnPtr.sub(module.base));
}
}, onLeave: function (retval) {
}
});

这里JNINativeMethod是一个结构体数组,每个成员占3个指针长度,在不知道有多少个成员的情况下,使用循环进行遍历,没循环一次,再偏移3个指针长度。

jnitrace使用

1
jnitrace -m attach - xxxx.so com.xxxxx.xxxxx //包名以frida-ps -U显示的为优先

-l xxx.so用于指定要跟踪的库,此参数可多次使用,或用*来跟踪所有库

e.g:-l libnativ-lib.so -l libanother-lib.so 或-l *

貌似遇到有壳的就不弹信息了。


title:Java层Hook

Java层Hook

  1. 脚本编写:

    先用Java.use定位到类,定位类使用的所属的package.类名的方式,之后可以通过implementation方法直接覆写静态方法。另外,再次强调,hook的代码的函数的参数,只需要数量上与原型相同,类型可不做要求。

  2. 几个常用的frida框架命令行参数:

    -U:链接USB设备。

    -F:附加最前面的应用。

    -f:主动启动进程。

    -l:加载script脚本文件

    -o:输出日志。

    —no-pauese:启动主线程运行应用。

  3. 基本上new 后面跟随的都是构造方法。

  4. 在js脚本中,使用$init来指代构造方法的名字。

  5. 中间遇到几次什么main thread 什么的,导致注入失败,目前原因不明,过了几天就能注入了,离谱。

  6. 迟来的脚本注入命令:

1
2
3
frida -U -F 进程名 -l xxx.js    //使用-U 是不需要进行端口转发的,-R才需要的.
//然后由于frida在15.2.2之后,默认app在启动时不再暂停,所以--no-pause参数没用了,想让程序暂停,将--no-pause替换为--pause即可
//-F是附加启动,后跟进程名,-f是spawn方式启动,后跟包名

从这开始是关键代码快速定位:

  1. showStacks()显示函数调用栈,主要使用的是Log类的getStackTraceString方法。
1
2
3
4
5
6
function showStacks(){
Java.perform(function(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
})
}
//调用的时候最好做个过滤,防止崩溃.
  1. app处理,提交数据通常会将数据放在集合中,HashMap又是这其中最为常用的。
1
2
3
4
5
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation=function(a,b){
console.log("HashMap.put",a,b);
return this.put(a,b);
}
  1. Hook Toast
1
2
3
4
5
6
7
8
9
10
11
12
var toast = Java.use("android.widget.Toast");
toast.show.implementation=function(){
showstacks();
console.log("Toast.show");
return this.show();
}

function showstacks(){
Java.perform(function(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
})
}
  1. Hook 组件(按钮,输入框那些,此处以登录按钮为例)
1
2
3
4
Java.perform(function(){
var btn_login_id=Java.use("com.dodonew.online.R$id").btn_login.value;
console.log("btn_lgin_id",btn_login_id);
})
  1. Hook用户输入
1
2
3
4
5
6
var textUtils = Java.use("android.text.TextUtils");
textUtils.isEmpty.implementation = function (a) {
showStacks();
console.log("TextUtils.isEmpty: ", a);
return this.isEmpty(a);
}

据说在协议分析中,客户端与服务端进行数据交换时,通常会使用JSON数据作为中间数据进行交互。通常会用到一些JSON类,如JSONObject,Gson等。后者用的相对较多,且可以被混淆。

1
2
3
4
5
6
7
8
9
10
11
12
var jSONObject = Java.use("org.json.JSONObject");
jSONObject.put.overload('java.lang.String', 'java.lang.Object').implementation = function (a, b) {
showStacks();
console.log("JSONObject.put: ", a, b);
return this.put(a, b);
}
jSONObject.getString.implementation = function (a) {
showStacks();
var result = this.getString(a);
console.log("JSONObject.getString: ", a, result);
return result;
}

Hook StringBuilder

Java中,字符串是只读的,对字符串进行修改,拼接等操作都是会创建新的字符串来返回

当有大量的字符串操作时,一般都会用到StringBuilder,Hook其toString方法来定位关键字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java.perform(function(){
var stringBuilder = Java.use("java.lang.StringBuilder");
stringBuilder.toString.implementation = function () {
var result = this.toString.apply(this, arguments);
if(result == "username=13866668888"){
showStacks();
console.log("stringBuilder.toString is called!", result);
}
return result;
}
})

function showStacks(){
Java.perform(function(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
})
}

MD5 Hook

MAC Hook

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,需要剔除。

Dalvik,smali学习笔记

基础了解

说到smali(对Dalvik虚拟机字节码的一种解释),首先应该提一下Google专门为Android平台涉及的用于运行Android程序的虚拟机–Dalvik虚拟机

Dalvik与Jvm主要在字节码是有差别。

在程序运行时,Jvm会频繁对栈进行操作,而Dalvik则是对寄存器

用一组各自的字节码做对比

这是原程序(通过异或交换两个数的值)

1
2
3
4
5
6
7
8
9
public class math{
public static void main(String[] args){
int a=5,b=3,c=a^b;
a=c^a;
b=c^b;
System.out.println(a);
System.out.println(b);
}
}

Jvm下的字节码

1
javap -c -classpath . math.class

image-20220629214610348

重点看main函数的代码:

可以看到,Jvm的字节码明显要比Dalvik的字节码复杂iconst_5表示申明一个整型常量5,istore_0表示栈顶int数值存入第1局部变量,等…

Dalvik下的字节码(此图不准确,请看下面的smali)

(该工具通常在Android SDK的build-tools目录下,使用的时候需要注意jdk版本问题,写这文章的时候用的jdk18,不能成功执行下面的命令,换成jdk8或者其他版本就行)

1
2
3
dx --dex --output=math.dex math.class
如果用的是d8.bat则用d8 --ouput 指定文件夹名称 math.class
dexdump -d math.dex

smali一般的指令格式为: [op]-[type] (可选) / [位宽,默认4位] [目标寄存器], [源寄存器] (可选)

诸多命令可以到这篇文章查找

(41条消息) 【Android安全】Dalvik字节码含义查询表_Walter_Jia的博客-CSDN博客

也或者是官方文档

Opcodes | Android Developers

可以看到Dalvik指令后面,都是用寄存器保存返回值,对寄存器进行频繁操作,而Jvm是对栈进行频繁操作。

实现相同的功能其,相比于Jvm,Dalvik所用到的指令更少,自然也更简洁。

我们将刚得到的dex文件用baksmali编译为smali文件

1
2
java -jar baksmali.jar d math.dex
java -jar smali.jar a -o math1.dex math.smali(这个是把smali转为dex)

只需要把baksmali和smali的jar包放在SDK的tools目录下,然后切换到该目录下执行命令就可以编译出smali文件了(默认在执行后生成的output文件夹里)。(注意以上过程都需要在同一个jdk版本的前提下进行,否则会出现一些错误)

记事本打开生成的smali文件(这里我是用IDEA的插件(java2smali)直接生成的,跟上面那张图不一样,上面是用的另一台电脑的java8编译出来的,下面这个是java18的结果)

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
.class public Lmath;
.super Ljava/lang/Object;
.source "math.java"


# direct methods
.method public constructor <init>()V
.registers 1

.prologue
.line 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V

return-void
.end method

.method public static main([Ljava/lang/String;)V
.registers 5
.param p0, "args" # [Ljava/lang/String;

.prologue
.line 3
const/4 v0, 0x5

.local v0, "a":I
const/4 v1, 0x3

.local v1, "b":I
xor-int v2, v0, v1

.line 4
.local v2, "c":I
xor-int/lit8 v0, v0, 0x6

.line 5
xor-int/lit8 v1, v1, 0x6

.line 6
sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;

invoke-virtual {v3, v0}, Ljava/io/PrintStream;->println(I)V

.line 7
sget-object v3, Ljava/lang/System;->out:Ljava/io/PrintStream;

invoke-virtual {v3, v1}, Ljava/io/PrintStream;->println(I)V

.line 8
return-void
.end method

上面 Ljava/lang/System;->out:Ljava/io/PrintStream 表示Java中的System类的out字段的PrintStream字段类型

这里因为我baksmali和smali版本(都是2.5.2)的原因,指令会跟老版本不一样,老版本可能是这样的

image-20220629211611482

再贴一张Dalvik的两种寄存器命名法

image-20220630095915424

对于有M个寄存器,N个参数的函数来说

在P命名法中p0是参数寄存器,v0是变量寄存器

在V命名法中,局部变量寄存器是v0–vn 参数寄存器则为vn–v(n+m)

(在Dalvik汇编代码较长的,且使用寄存器较多的时候,P命名法可以更清楚的阅读代码。)

对于math的代码来说,参数寄存器就是用来存放参数的,即存储println引用的字段,即被交换值过后的a (=3)。v0则是用来存放变量值3,5。

Smali简单编写尝试

首先说下,有些指令助记符后添加了jumbo后缀,这是在 Android 4.0 开始的扩展指令,增加了寄存器和指令索引的取值范围。另外,除去加了jumbo后缀的扩展指令,每个指令的字节码只占1字节范围是0x0~0xff。

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
.class public Lmath;
.super Ljava/lang/Object;

.method public constructor <init>()V
.prologue
.registers 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static main([Ljava/lang/String;)V
.registers 4

const v0, 0x5
const v1, 0x3
const v2,0x0
xor-int v2 ,v0,v1

sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
xor-int v0, v0, v2
invoke-virtual {p0,v0}, Ljava/io/PrintStream;->println(I)V

sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
xor-int v1, v1, v2
invoke-virtual {p0,v1}, Ljava/io/PrintStream;->println(I)V
return-void
.end method

我们用上面将smali转化为dex的命令,将其转化为math2.dex,再用d2jdex将dex转化为jar包

1
d2j-dex2jar math2.dex

然后用jd-gui打开生成的jar包,查看代码

image-20220701100839722

可以看到虽然长相不同,但其实现的功能与原来的math还是差不多的。(雾)

socket编程尝试(Windows)

首先是对于socket这一技术,贴一篇文章

socket技术详解

用一张图概括整个过程(linux下的,与windows不同之处在于函数名和一些细小差别)

image-20220520081743268

针对详细过程,请参阅上述文章,此处不再赘述

我们着重讲编程部分。

首先是客户端

Client

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
#include <iostream>
#include <cstdio>
#include <Winsock2.h>
#include <string>


#pragma comment(lib, "ws2_32.lib") //表示链接Ws2_32.lib这个库。

using namespace std;

#define xPort 8000 //端口
#define xIP "127.0.0.1" // ip

int main()
{
const int xBUF_SIZE = 64;
WSADATA wsd; //WSADATA变量
SOCKET xServer; //服务器套接字
SOCKET xClient; //客户端套接字
char input[xBUF_SIZE]; //接受数据缓冲区
char str[xBUF_SIZE]; //返回数据缓冲区
int retVal; //返回值

if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) //初始化
{
cout << "WSAStartup failed!" << endl;
return 1;
}

xClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个socket
if (INVALID_SOCKET == xClient)
{
cout << "socket failed!" << endl;
WSACleanup();
return -1;
}

struct sockaddr_in sockAddr;
memset(&sockAddr, 0, sizeof(sockAddr)); //每个字节都用0填充
sockAddr.sin_family = PF_INET;
sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
sockAddr.sin_port = htons(8000);

retVal = connect(xClient, (LPSOCKADDR)&sockAddr, sizeof(sockAddr)); //连接
if (SOCKET_ERROR == retVal)
{
cout << "connect failed!" << endl;
closesocket(xClient);
WSACleanup();
return -1;
}
//客户端初始化完成

while (true)
{
memset(input, 0, xBUF_SIZE);
cout << "Input:" ;
scanf("%s",input);
retVal = send(xClient, input, xBUF_SIZE, 0);
recv(xClient, str, xBUF_SIZE, 0);
cout << str << endl;
break;
}

closesocket(xClient);
WSACleanup();

return 0;

}

上述sockaddr_in的结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
struct sockaddr_in {

  short int sin_family; /* 通信类型 */

  unsigned short int sin_port; /* 端口 */

  struct in_addr sin_addr; /* Internet 地址 */

  unsigned char sin_zero[8]; /* 与sockaddr结构的长度相同*/

};

服务端

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
#include <iostream>
#include <cstdio>
#include <Winsock2.h>
#include <string>


#pragma comment(lib, "ws2_32.lib")

using namespace std;

#define xPort 8000
#define xIP "127.0.0.1"


int main()
{
const int xBUF_SIZE = 64;
WSADATA wsd; //WSADATA变量
SOCKET xServer; //服务器套接字
SOCKET xClient; //客户端套接字
SOCKADDR_IN xaddrServ;
char string[xBUF_SIZE]; //接受数据缓冲区
char sendinput[xBUF_SIZE]; //返回数据缓冲区
int retVal; //返回值


if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
cout << "WSAStartup failed!" << endl;
return 1;
}
//创建一个socket
xServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == xServer)
{
cout << "socket failed" << endl;
WSACleanup();
return -1;
}
//服务器socket地址
xaddrServ.sin_family = AF_INET;
xaddrServ.sin_port = htons(xPort);
xaddrServ.sin_addr.s_addr = INADDR_ANY;

//绑定socket
retVal = bind(xServer, (SOCKADDR*)&xaddrServ, sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == retVal)
{
cout << "bind failed" << endl;
closesocket(xServer);
WSACleanup();
return -1;
}

retVal = listen(xServer, 1);
if (SOCKET_ERROR == retVal)
{
cout << "listen failed" << endl;
closesocket(xServer);
WSACleanup();
return -1;
}

sockaddr_in addrClient;
int addrClientLen = sizeof(addrClient);
xClient = accept(xServer, (sockaddr FAR*) & addrClient, &addrClientLen);//创建一个等待链接的客户端socket
if (INVALID_SOCKET == xClient)
{
cout << "accept failed" << endl;
closesocket(xServer);
WSACleanup();
return -1;
}

//初始化完成

while (true)
{
memset(sendinput, 0, xBUF_SIZE);
recv(xClient,sendinput,xBUF_SIZE,0);
cout << sendinput;
memset(string,0,xBUF_SIZE);
strcpy(string, "good :)");
send(xClient,string,xBUF_SIZE,0);
}

closesocket(xServer);
closesocket(xClient);
WSACleanup();

return 0;

}

原本想在服务端加一个RC4加密的,结果有一些问题,也不太想改,就让客户端输入,服务端返回一个字符串了。

这只是单线程的socket编程。

参考文章

https://www.cnblogs.com/kefeiGame/p/7246942.html

https://www.cnblogs.com/L-hq815/archive/2012/07/09/2583043.html(linux下)

推介结合winapi看

(后续找个时间试试多线程)

image-20220415133333747

拖进IDA打开,发现让程序运行起来就是一个问题。

image-20220415133421969

查看汇编代码,发现0x4010DE处的代码有问题,需要nop掉才能顺利反汇编

但是直接用IDA的Patch program-Assemble不行,尝试用OD打开进行nop,但是发现OD现实的地址跟IDA不一样,这是由于IDA加载的是静态的文件,而OD加载的动态的内存里的(其实这也是为什么打开修改文件属性的软件时,IDA能够同时打开该文件,而OD不能打开的原因),由于可能受到ASLR的影响,使得地址不一样。所以我们需要将其关闭,在OPTION_HEADER里找到DLLCharacteristics,将原本的8140改为8100或者0000即可。

(关于ASLR:ASLR

image-20220415163903500

需要注意的是,从0x4010D7开始,就是执行跳转的0x4010DE的代码,故,需要将0x4010D7到0x4010DE的都给nop掉,然后保存,再用IDA打开即可

如下是伪代码:

image-20220415164426611

那个if通过调试来看其实就是计算字符串长度并且检测字符串中是否含有不可见字符,只要没有不可见字符,这块都不会跳到wrong那儿

image-20220415170345535

image-20220415170354222

image-20220415170403903

再往下进入sub_4013F0函数

image-20220415170858424

image-20220415173316263

看着一片粉红的,几乎都是老面孔啊,逆向工程核心原理的反调试部分,复习一下。

首先,IsDebuggerPresent是一个检测是否处于调试状态,调试状态其值为1;CheckRemoteDebuggerPresent,这个和前者类似,甚至可以说一模一样,pbDebuggerPresent的值是TURE(调试状态)还是FALSE;

然后是SetLastError,OutputDebugString和GetLastError的组合拳了,OutputDebugString可以检查测进程是否处于调试状态,并且在调试状态下输出字符串,前者会设置一个错误码,如果OutputDebugString顺利执行,那么错误码不会改变,GetLastError就会获取错误码从而触发异常,如果OutputDebugString没有顺利执行,那错误码会被重置,使后者无法触发异常;

然后是NtQueryInformationProcess,核心原理那本书有详细的解释,简要说明其中一处就是其第二个成员ProcessDebugPort(0x7),如果不在调试状态,其值为0;

下一组,SetLastError(上文设置错误码的函数),CloseHandle、GetLastError,跟上面介绍的同样,类似的还有利用CloseWindow,都是产生异常,使错误码改变

DebugActiveProcess,将活动进程附加到调试器,如果成功则返回1(非零数),否则为0,换言之,当前进程处在非调试状态返回0;

GetStartupInfoA,该函数用来检索创建调用进程时指定的STARTUPINFO结构体的内容。其参数为指向STARTUPINFO结构体的指针。STARTUPINFO结构体用来保存程序启动的信息,通过其中结构体参数的改变来检测程序是正常运行还是在调试器中运行的。一般双击运行的进程的父进程都是explorer.exe,但是如果进程被调试父进程则是调试器进程。也就是说如果父进程不是explorer.exe则可以认为程序正在被调试。以上即为其反调试原理,若STARTUPINFO结构体成员中有一项不为0,则检测到反调试。

下面两篇文章都是很好的学习材料。

反调试

反调试

再往下走

image-20220415174816956

这个函数是查找进程信息,并将其与几个常用的调试工具作比较,可以看到没有IDA,那这个函数就可以放心过,但还是进去看看,毕竟里面还有两个赋值操作,事关flag。

最后一个,RDTSC,这个就是时间检测法来实现反调试了

旧图新用

image-20220415175143219

综上,总共十处反调试,属于是在”堆怪”了。

回到main函数中,进入sub_4012A0函数

image-20220415175727744

就是一个普通的base64,没什么好说的。

再回到主函数的最后

1
if ( *(&v18 + v8) != v9[2 * v8 + 1] || v9[2 * v8] + 2 != (off_404018[v8] ^ 3) )

显而易见,将变化后的输入的字符串的奇数位和偶数位分别加工。

至此,整体流程分析完毕。

在过反调试的时候,我们注意al寄存器的值,其中会有不同的字符赋值给它,正确过掉反调试后所赋的值才是我们需要的数据。中间比较麻烦的可能就是错误码的两处。耐心点多试试,就能过掉了,尤其注意跳转指令的条件(可以结合伪代码来看,更容易理解)。我也调了好几次才全部过掉:(原罪的菜

在这之后,al所被赋值的字符串为2TVBnx0lnn

那这题难点也就结束了,直接贴上解题脚本

1
2
3
4
5
6
7
8
9
10
str1 = "LKd8gPYWS["
str2 = "2TVBnx0lnn"
arr = [0 for i in range(20)]

for i in range(10):
arr[i*2] = (ord(str1[i])^3)-2'''偶数位变化'''
arr[i*2+1] = ord(str2[i])'''奇数位变化'''

cipher = ''.join(map(chr,arr))
print cipher.decode('base64') '''python2版本的脚本'''

python3用这个吧

1
2
3
4
5
6
7
8
9
10
11
12
import base64
str1 = "LKd8gPYWS["
str2 = "2TVBnx0lnn"
arr = [0 for i in range(20)]

for i in range(10):
arr[i*2] = (ord(str1[i])^3)-2
arr[i*2+1] = ord(str2[i])

cipher = ''.join(map(chr,arr))
cipher = base64.b64decode(cipher)
print(cipher)

flag:D0g3{3aSy_Ant1_De6ug}

附件

PE文件解析器

检测是否为PE文件

首先先检测该文件是不是PE文件,根据PE文件特性

PE文件的第一个字节位于一个传统的MS-DOS头部,称作IMAGE_DOS_HEADER,该结构体第一个成员的值可用作判断文件是否为PE文件。其值应被设置为4D5A(但是实际存储在文件中,是以小端序形式存储的,如下所示),

image-20220412211923140

这才是PE文件的标识。

在《逆向工程核心原理》中,我们可以知道PE文件类型大概有以下几种

image-20220412211706773

那么,如何访问该成员呢?

利用结构体访问成员的方式即可

1
2
3
4
5
PIMAGE_DOS_HEADER P_DosHeader;////定义指向_IMAGE_DOS_HEADER结构体的指针
P_DosHeader = (PIMAGE_DOS_HEADER)buffer;//在编写这一步的时候,我忽视了buffer的字节大小和指针的字节大小不对等(因为我刚开始定义buffer疏忽了将其作为指针来定义),从而触发warning: cast to pointer from integer of different size [-Wint-to-pointer-cast],buffer改为指针类型即可解决该warning
printf("Info as follow:\n");
printf("The Flag bit of this file is %X\n", P_DosHeader->e_magic);
printf("The offset of the DOS header is:%X\n", P_DosHeader->e_lfanew);

输出如下图所示

image-20220412231706106

可以看到,正确的。

当然我们也可以为MZ加个判断

1
2
3
4
5
if(P_DosHeader->e_magic != IMAGE_DOS_SIGNATURE)//5A4D-"ZM",4D5A-"MZ"
{
printf("This file is not a PE file!\n");
return 0;
}

输入的路径格式可以是如下图所示的

image-20220412234003731

顺带一提上述被检测的文件就是记事本这个应用程序。

NT头偏移量(上图的0x000000E0)根据程序不同,其值也不同

接下来可以正式开始解析PE文件

NT_HEADER

首先是NT头包含哪些信息

  1. signature
  2. FILE_HEADER
  3. OPTIONAL_HEADER

后面两个其实都包含大量数据。我们以PEview工具查看做示例,如下图所示

image-20220413073714649

image-20220413073726016

东西很多,但写起来难度不是很大

FILE_HEADER

在签名里,一个PE有效文件其字段应为0x00004550,ASCII码为PE00

访问PE文件头的方式为:

P_NtHeader=Imagebase+dosHeader->e_lfanew;

然后是机器码和节区数

image-20220413102414066

因为COFF符号表在PE文件中比较少见,所以这里有关COFF符号表的两个成员就不输出信息了

Characteristics,普通的.exe文件这个字段的值一般是10F,DLL文件则一般是2102

1
2
3
4
5
6
7
//解析PE文件头
PIMAGE_NT_HEADERS32 P_NtHeader; //定义指向_IMAGE_NT_HEADERS32结构体的指针
P_NtHeader = (PIMAGE_NT_HEADERS32)(buffer + P_DosHeader->e_lfanew);
printf("The Flag bit of this file is %X\n", P_NtHeader->Signature);
printf("Machine: %X\n", P_NtHeader->FileHeader.Machine);
printf("Number of Sections: %d\n", P_NtHeader->FileHeader.NumberOfSections);
printf("Characteristics: %X\n", P_NtHeader->FileHeader.Characteristics);

OPTIONAL_HEADER

由于成员众多,我仅列出了在逆向分析时,我经常用到的一些成员

printf即可,没什么技术含量

稍微提一下Subsystem,它的值用来区分系统文件和普通的可执行文件。

image-20220413111626984

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
printf("The follow is the info of PE file optional header:\n");
printf("Magic: %X\n", P_NtHeader->OptionalHeader.Magic); //这个值是0x10B,表示PE32,0x20B表示PE32+(64位)
printf("AddressOfEntryPoint: %X\n", P_NtHeader->OptionalHeader.AddressOfEntryPoint);//这项可以告诉我们程序的入口地址
printf("ImageBase: %X\n", P_NtHeader->OptionalHeader.ImageBase);//这个值是文件的起始地址,也就是我们要解析的文件的起始地址
printf("SectionAlignment: %X\n", P_NtHeader->OptionalHeader.SectionAlignment);//节区对齐值
printf("FileAlignment: %X\n", P_NtHeader->OptionalHeader.FileAlignment);//文件对齐值
printf("SizeOfImage: %X\n", P_NtHeader->OptionalHeader.SizeOfImage);//文件大小
printf("SizeOfHeaders: %X\n", P_NtHeader->OptionalHeader.SizeOfHeaders);//头部大小
printf("CheckSum: %X\n", P_NtHeader->OptionalHeader.CheckSum);//映像校验和,用处就是用来检验文件是否被修改过。
printf("Subsystem: %X\n", P_NtHeader->OptionalHeader.Subsystem);//子系统
printf("NumberOfRvaAndSizes: %X\n", P_NtHeader->OptionalHeader.NumberOfRvaAndSizes);//RVA和Sizes的数量
printf("Number Of Data Directories: %X\n", P_NtHeader->OptionalHeader.NumberOfRvaAndSizes);//数据目录的数量
printf("------------------------Thats's basic info of OPTIONAL_HEADER----------------------------\n");
printf("\n");
printf("------------------------Next are some tables----------------------------\n");
printf("\n");
//解析DATA_DIRECTORY
printf("The follow is the info of PE file data directory:\n");
printf("Address of Export Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[0].VirtualAddress);
printf("Size of Export Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[0].Size);
printf("Address of Import Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[1].VirtualAddress);
printf("Size of Import Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[1].Size);
printf("Address of Resource Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[2].VirtualAddress);
printf("Size of Resource Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[2].Size);
printf("Address of TLS Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[9].VirtualAddress);
printf("Size of TLS Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[9].Size);
printf("Address of IAT: %X\n", P_NtHeader->OptionalHeader.DataDirectory[12].VirtualAddress);
printf("Size of IAT: %X\n", P_NtHeader->OptionalHeader.DataDirectory[12].Size);
printf("------------------------Thats's basic info of DATA_DIRECTORY----------------------------\n");
printf("\n");

在上面的各种表中,需要特别关注IMPORT和EXPORT这两个,反调试相关的话,TLS的地址其实是保存在FS寄存器的,实际访问也是通过FS寄存器。

至此,NT头的解析算是完成了。

节区头

这个直接构造循环输出其相关信息即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
printf("The follow is the info of PE file section header:\n");
PIMAGE_SECTION_HEADER P_SectionHeader;
P_SectionHeader = (PIMAGE_SECTION_HEADER)(buffer + P_DosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS32));
for(int i = 0; i < P_NtHeader->FileHeader.NumberOfSections; i++)
{
printf("Section Name: %s\n", P_SectionHeader->Name);
printf("Virtual Address: %X\n", P_SectionHeader->VirtualAddress);
printf("Size of Raw Data: %X\n", P_SectionHeader->SizeOfRawData);
printf("Pointer to Raw Data: %X\n", P_SectionHeader->PointerToRawData);
printf("Pointer to Relocations: %X\n", P_SectionHeader->PointerToRelocations);
printf("Pointer to Linenumbers: %X\n", P_SectionHeader->PointerToLinenumbers);
printf("Number of Relocations: %X\n", P_SectionHeader->NumberOfRelocations);
printf("Number of Linenumbers: %X\n", P_SectionHeader->NumberOfLinenumbers);
printf("Characteristics: %X\n", P_SectionHeader->Characteristics);
printf("------------------------Thats's basic info of SECTION----------------------------\n");
printf("\n");

image-20220413115002184

至此,一些基本的信息得以展现,现在main函数已经非常臃肿了,我们将还没实现的导入/出表,重定位表,和TLS表分别用对应的函数来输出相关信息。

IMPORT_TABLE

首先是导入表的一些基本信息

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
typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union{
DWORD Characteristics;
DWORD OriginalFirstThunk;//导入名称表INT
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain;
DWORD Name; //dll名称
DWORD FirstThunk; //导入地址表IAT
}IMAGE_IMPORT_DESCRIPTOR;
//OriginalFirstThunk和FirstThunk都指向的是_IMAGE_THUNK_DATA32结构体
//导入名称表最高位是0,就是名称导入
//最高位是1,就是序号导入
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD,导入函数的地址,在加载到内存后,这里才起作用
DWORD Ordinal; // 假如是序号导入的,会用到这里
DWORD AddressOfData; //PIMAGE_IMPORT_BY_NAME,假如是函数名导入的,用到这 里 ,它指向另外一个结构体:PIMGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;

typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint; //序号
BYTE NAME[1]; //函数名
}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

如下是关于函数以何种方式输入的更详细的说明

image-20220413185242749

还要提一嘴,exe文件的默认加载及地址是0x004000000

dll文件的则是0x10000000

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
void ImportTable(char* buffer)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
PIMAGE_DATA_DIRECTORY pDataDirectory = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT);//
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToOffset(pDataDirectory->VirtualAddress, buffer) + buffer);//指向导入表的第一个描述符
//遍历导入表
while(pImportDescriptor->Name)
{
char * szDllname = (char*)(RvaToOffset(pImportDescriptor->Name, buffer) + buffer);//指向DLL名字
printf("DLL Name: %s\n", szDllname);
PIMAGE_THUNK_DATA32 PIAT = (PIMAGE_THUNK_DATA32)(RvaToOffset(pImportDescriptor->FirstThunk, buffer) + buffer);//指向IAT
PIMAGE_THUNK_DATA32 PINT = (PIMAGE_THUNK_DATA32)(RvaToOffset(pImportDescriptor->OriginalFirstThunk, buffer) + buffer);//指向INT
//解析IAT,IAT是一个结构体数组,其结尾为0
while(PIAT->u1.Ordinal)
{
if(PIAT->u1.Ordinal & 0x80000000)//如果是序号
{
printf("Ordinal: %2d\n", PIAT->u1.Ordinal & 0xFFFF);
printf("\n");
}
else//如果是函数名
{
PIMAGE_IMPORT_BY_NAME szFuncName = (PIMAGE_IMPORT_BY_NAME)(RvaToOffset(PIAT->u1.AddressOfData, buffer) + buffer);
printf("Function Name: %s\n", szFuncName->Name);
printf("Hint: %X\n", szFuncName->Hint);
printf("\n");
}
PIAT++;
}
pImportDescriptor++;
}
}

效果图

image-20220414191127255

image-20220414191140997

有些DLL函数名称会乱码,就很离谱

EXPORT_TABLE

image-20220414130435339

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY { 
DWORD Characteristics; // (1) 保留,恒为0x00000000
DWORD TimeDateStamp; // (2) 时间戳
WORD MajorVersion; // (3) 主版本号,一般不赋值
WORD MinorVersion; // (4) 子版本号,一般不赋值
DWORD Name; // (5) 模块名称*
DWORD Base; // (6) 索引基数*
DWORD NumberOfFunctions; // (7) 导出地址表中成员个数*
DWORD NumberOfNames; // (8) 导出名称表中成员个数*
DWORD AddressOfFunctions; // (9) 导出地址表(EAT)*
DWORD AddressOfNames; // (10) 导出名称表(ENT)*
DWORD AddressOfNameOrdinals; // (11) 指向导出序号数组*
}IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

其结构如上。

image-20220414131910912

image-20220414132204351

EAT: 导出地址表 ENT: 导出名称表(与EOT一一对应)

EOT:导出序号表

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
void ExportTable(char* buffer)
{
//Dos头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
//PE
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
//定位数据目录表中的导出表
PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
//填充导出表结构
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToOffset(pExportDir->VirtualAddress, buffer) + buffer);
char* szName = (char*)(RvaToOffset(pExport->Name, buffer) + buffer);
if (pExport->AddressOfFunctions == 0)
{
printf("Without EXPORT_TABLE!\n");
return;
}
printf("Name:%s\n", szName);
printf("Number of Functions:%08X\n", pExport->NumberOfFunctions);
printf("Number of Names:%08X\n", pExport->NumberOfNames);
printf("Func_addr:%08X\n", pExport->AddressOfFunctions);
printf("\n");
//获取函数数量
DWORD dwNumOfFUN = pExport->NumberOfFunctions;
//函数名数量
DWORD dwNumOfNames = pExport->NumberOfNames;
//基
DWORD dwBase = pExport->Base;
//导出地址表
PDWORD pEat32 = (PDWORD)(RvaToOffset(pExport->AddressOfFunctions, buffer) + buffer);
//导出名称表
PDWORD pEnt32 = (PDWORD)(RvaToOffset(pExport->AddressOfNames, buffer) + buffer);
//导出序号表
PWORD pId = (PWORD)(RvaToOffset(pExport->AddressOfNameOrdinals, buffer) + buffer);
for (int i = 0; i < dwNumOfFUN; i++)
{
if (pEat32[i] == 0)
continue;
DWORD Id = 0;
for (; Id < dwNumOfNames; Id++)
{
if (pId[Id] == i)
break;
}
if (Id == dwNumOfNames)
{
printf("Name:%X Address:0x%08X Name[NuLL]\n", i + dwBase, pEat32[i]);
printf("\n");
}
else
{
char* szFunName = (char*)(RvaToOffset(pEnt32[Id], buffer) + buffer);
printf("Name:%X Address:0x%08X Name[%s]\n", i + dwBase, pEat32[i],szFunName);
printf("\n");
}
printf("-------------------------------------\n");
}
printf("----------------------------------------------------------\n");
}

image-20220414191329512

这个解析的其实是老头环的导出表 :)

TLS_TABLE

image-20220414190838236

1
2
3
4
5
6
7
8
9
10
11
12
void TlsTable(char* buffer)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
PIMAGE_DATA_DIRECTORY pTLSDir = (pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_TLS);//定位数据目录表中的TLS表
PIMAGE_TLS_DIRECTORY pTLS = (PIMAGE_TLS_DIRECTORY)(RvaToOffset(pTLSDir->VirtualAddress, buffer) + buffer);//填充TLS结构
printf("StartAddressOfRawData: %08X\n", pTLS->StartAddressOfRawData);
printf("EndAddressOfRawData: %08X\n", pTLS->EndAddressOfRawData);
printf("TLS_Callback: %08X\n", pTLS->AddressOfCallBacks);//tls回调函数
printf("TLS_Size: %08X\n", pTLS->SizeOfZeroFill);
printf("TLS_Characteristics: %08X\n", pTLS->Characteristics);
}

全篇

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
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>


DWORD RvaToOffset(DWORD dwRva, char* buffer); //计算数据目录表起始位置到文件头的偏移(RVAtoRAW)
void ImportTable(char* buffer); //解析导入表函数
void ExportTable(char* buffer); //解析导出表函数
void TlsTable(char* buffer); //解析TLS表的函数

int main()
{
int nFileLength = 0;
FILE* pFile = NULL;
char path[100];
char *buffer;

printf("Please input the path of the file you want to look by my parser of PE:\n");
gets(path);
fopen_s(&pFile,path, "rb");
if(pFile == NULL)
{
printf("Open file failed,maybe it's doesn't exist?\n");
return 0;
}
fseek(pFile, 0, SEEK_END); //允许文件读写位置移动到文件尾,返回文件长度
nFileLength = ftell(pFile); //获取文件长度
if(nFileLength == 0)
{
printf("The file is empty!\n");
return 0;
}
rewind(pFile); //将文件指针重新指向文件开头

//接下来为文件分配空间
int ImageBase = nFileLength * sizeof(char) + 1;
buffer = (char*)malloc(ImageBase);
memset(buffer, 0, nFileLength * sizeof(char) + 1);
//读取文件内容
fread(buffer, 1, ImageBase, pFile);
//至此,文件读取操作完成

PIMAGE_DOS_HEADER P_DosHeader; //定义指向_IMAGE_DOS_HEADER结构体的指针
P_DosHeader = (PIMAGE_DOS_HEADER)buffer;
printf("Basic Info is as follow:\n");
printf("The Flag bit of this file is %X\n", P_DosHeader->e_magic);
printf("The offset of the DOS header is:%X\n", P_DosHeader->e_lfanew);
printf("------------------------Thats's basic info of DosHeaderStruct----------------------------\n");
printf("\n");
if(P_DosHeader->e_magic != IMAGE_DOS_SIGNATURE)//判断是否为PE文件
{
printf("This file is not a PE file!\n");
return 0;
}
//解析PE文件头
//解析FILE_HEADER
PIMAGE_NT_HEADERS32 P_NtHeader; //定义指向_IMAGE_NT_HEADERS32结构体的指针
P_NtHeader = (PIMAGE_NT_HEADERS32)(buffer + P_DosHeader->e_lfanew);
printf("The follow is the info of PE file header:\n");
printf("The Flag bit of this file is %X\n", P_NtHeader->Signature);
printf("Machine: %X\n", P_NtHeader->FileHeader.Machine);
printf("Number of Sections: %d\n", P_NtHeader->FileHeader.NumberOfSections);
printf("Characteristics: %X\n", P_NtHeader->FileHeader.Characteristics);
/*printf("TimeDateStamp: %X\n", P_NtHeader->FileHeader.TimeDateStamp);//这项可以告诉我们编译器创建文件的时间,注释掉吧,感觉没用过*/
printf("------------------------Thats's basic info of FILE_HEADER----------------------------\n");
printf("\n");
//解析OPTIONAL_HEADER
printf("The follow is the info of PE file optional header:\n");
printf("Magic: %X\n", P_NtHeader->OptionalHeader.Magic); //这个值是0x10B,表示PE32,0x20B表示PE32+(64位)
printf("AddressOfEntryPoint: %X\n", P_NtHeader->OptionalHeader.AddressOfEntryPoint);//这项可以告诉我们程序的入口地址
printf("ImageBase: %X\n", P_NtHeader->OptionalHeader.ImageBase);//这个值是文件的起始地址,也就是我们要解析的文件的起始地址
printf("SectionAlignment: %X\n", P_NtHeader->OptionalHeader.SectionAlignment);//节区对齐值
printf("FileAlignment: %X\n", P_NtHeader->OptionalHeader.FileAlignment);//文件对齐值
printf("SizeOfImage: %X\n", P_NtHeader->OptionalHeader.SizeOfImage);//文件大小
printf("SizeOfHeaders: %X\n", P_NtHeader->OptionalHeader.SizeOfHeaders);//头部大小
printf("CheckSum: %X\n", P_NtHeader->OptionalHeader.CheckSum);//映像校验和,用处就是用来检验文件是否被修改过。
printf("Subsystem: %X\n", P_NtHeader->OptionalHeader.Subsystem);//子系统
printf("NumberOfRvaAndSizes: %X\n", P_NtHeader->OptionalHeader.NumberOfRvaAndSizes);//RVA和Sizes的数量
printf("Number Of Data Directories: %X\n", P_NtHeader->OptionalHeader.NumberOfRvaAndSizes);//数据目录的数量
printf("------------------------Thats's basic info of OPTIONAL_HEADER----------------------------\n");
printf("\n");
printf("------------------------Next are some tables----------------------------\n");
printf("\n");
//解析DATA_DIRECTORY
printf("The follow is the info of PE file data directory:\n");
printf("Address of Export Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[0].VirtualAddress);
printf("Size of Export Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[0].Size);
printf("Address of Import Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[1].VirtualAddress);
printf("Size of Import Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[1].Size);
printf("Address of Resource Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[2].VirtualAddress);
printf("Size of Resource Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[2].Size);
printf("Address of TLS Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[9].VirtualAddress);
printf("Size of TLS Table: %X\n", P_NtHeader->OptionalHeader.DataDirectory[9].Size);
printf("Address of IAT: %X\n", P_NtHeader->OptionalHeader.DataDirectory[12].VirtualAddress);
printf("Size of IAT: %X\n", P_NtHeader->OptionalHeader.DataDirectory[12].Size);
printf("------------------------Thats's basic info of DATA_DIRECTORY----------------------------\n");
printf("\n");
//解析SECTION_HEADER
printf("The follow is the info of PE file section header:\n");
PIMAGE_SECTION_HEADER P_SectionHeader;
P_SectionHeader = (PIMAGE_SECTION_HEADER)(buffer + P_DosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS32));
for(int i = 0; i < P_NtHeader->FileHeader.NumberOfSections; i++)
{
printf("Section Name: %s\n", P_SectionHeader->Name);
printf("Virtual Address: %X\n", P_SectionHeader->VirtualAddress);
printf("Size of Raw Data: %X\n", P_SectionHeader->SizeOfRawData);
printf("Pointer to Raw Data: %X\n", P_SectionHeader->PointerToRawData);
printf("Pointer to Relocations: %X\n", P_SectionHeader->PointerToRelocations);
printf("Pointer to Linenumbers: %X\n", P_SectionHeader->PointerToLinenumbers);
printf("Number of Relocations: %X\n", P_SectionHeader->NumberOfRelocations);
printf("Number of Linenumbers: %X\n", P_SectionHeader->NumberOfLinenumbers);
printf("Characteristics: %X\n", P_SectionHeader->Characteristics);
printf("------------------------Thats's basic info of SECTION----------------------------\n");
printf("\n");
P_SectionHeader++;
}
printf("Please input which information you want to look :\n");
printf("1.Export Table\n");
printf("2.Import Table\n");
printf("3.TLS Table\n");
printf("4.exit\n");
int choice;
while(1)
{
printf("Input a number: ");
scanf("%d", &choice);
if (choice == 1)
ExportTable(buffer);
else if (choice == 2)
ImportTable(buffer);
else if (choice == 3)
TlsTable(buffer);
else if (choice == 4)
return 0;
else
printf("Please input again and make sur that's valid\n");
}
system("pause");
free(buffer);//走之前别忘了清空缓存区
return 0;
}

DWORD RvaToOffset(DWORD dwRva, char* buffer)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;//Dos头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);//PE头
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);//区段表
//判断是否落在头部当中
if (dwRva < pSection[0].VirtualAddress)
{
return dwRva;
}
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
if (dwRva >= pSection[i].VirtualAddress && dwRva <= pSection[i].VirtualAddress + pSection[i].Misc.VirtualSize)
{
//dwRva-pSection[i].VirtualAddress是数据目录表到区段起始地址的偏移(OFFSET)
// pSection[i].PointerToRawData区段到文件头的偏移(OFFSET)
return dwRva - pSection[i].VirtualAddress + pSection[i].PointerToRawData;//返回虚拟地址对应的文件偏移地址(RAW),这个公式一定得掌握
}
}
}
void ImportTable(char* buffer)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
PIMAGE_DATA_DIRECTORY pDataDirectory = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT);//
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToOffset(pDataDirectory->VirtualAddress, buffer) + buffer);//指向导入表的第一个描述符
//遍历导入表
while(pImportDescriptor->Name)
{
char * szDllname = (char*)(RvaToOffset(pImportDescriptor->Name, buffer) + buffer);//指向DLL名字
printf("DLL Name: %s\n", szDllname);
PIMAGE_THUNK_DATA32 PIAT = (PIMAGE_THUNK_DATA32)(RvaToOffset(pImportDescriptor->FirstThunk, buffer) + buffer);//指向IAT
PIMAGE_THUNK_DATA32 PINT = (PIMAGE_THUNK_DATA32)(RvaToOffset(pImportDescriptor->OriginalFirstThunk, buffer) + buffer);//指向INT
//解析IAT,IAT是一个结构体数组,其结尾为0
while(PIAT->u1.Ordinal)
{
if(PIAT->u1.Ordinal & 0x80000000)//如果是序号
{
printf("Ordinal: %2d\n", PIAT->u1.Ordinal & 0xFFFF);
printf("\n");
}
else//如果是函数名
{
PIMAGE_IMPORT_BY_NAME szFuncName = (PIMAGE_IMPORT_BY_NAME)(RvaToOffset(PIAT->u1.AddressOfData, buffer) + buffer);
printf("Function Name: %s\n", szFuncName->Name);
printf("Hint: %X\n", szFuncName->Hint);
printf("\n");
}
PIAT++;
}
pImportDescriptor++;
}
}
void ExportTable(char* buffer)
{
//Dos头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
//PE
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
//定位数据目录表中的导出表
PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
//填充导出表结构
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToOffset(pExportDir->VirtualAddress, buffer) + buffer);
char* szName = (char*)(RvaToOffset(pExport->Name, buffer) + buffer);
if (pExport->AddressOfFunctions == 0)
{
printf("Without EXPORT_TABLE!\n");
return;
}
printf("Name:%s\n", szName);
printf("Number of Functions:%08X\n", pExport->NumberOfFunctions);
printf("Number of Names:%08X\n", pExport->NumberOfNames);
printf("Func_addr:%08X\n", pExport->AddressOfFunctions);
printf("\n");
//获取函数数量
DWORD dwNumOfFUN = pExport->NumberOfFunctions;
//函数名数量
DWORD dwNumOfNames = pExport->NumberOfNames;
//基
DWORD dwBase = pExport->Base;
//导出地址表
PDWORD pEat32 = (PDWORD)(RvaToOffset(pExport->AddressOfFunctions, buffer) + buffer);
//导出名称表
PDWORD pEnt32 = (PDWORD)(RvaToOffset(pExport->AddressOfNames, buffer) + buffer);
//导出序号表
PWORD pId = (PWORD)(RvaToOffset(pExport->AddressOfNameOrdinals, buffer) + buffer);
for (int i = 0; i < dwNumOfFUN; i++)
{
if (pEat32[i] == 0)
continue;
DWORD Id = 0;
for (; Id < dwNumOfNames; Id++)
{
if (pId[Id] == i)
break;
}
if (Id == dwNumOfNames)
{
printf("Name:%X Address:0x%08X Name[NuLL]\n", i + dwBase, pEat32[i]);
printf("\n");
}
else
{
char* szFunName = (char*)(RvaToOffset(pEnt32[Id], buffer) + buffer);
printf("Name:%X Address:0x%08X Name[%s]\n", i + dwBase, pEat32[i],szFunName);
printf("\n");
}
printf("-------------------------------------\n");
}
printf("----------------------------------------------------------\n");
}
void TlsTable(char* buffer)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
PIMAGE_DATA_DIRECTORY pTLSDir = (pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_TLS);//定位数据目录表中的TLS表
PIMAGE_TLS_DIRECTORY pTLS = (PIMAGE_TLS_DIRECTORY)(RvaToOffset(pTLSDir->VirtualAddress, buffer) + buffer);//填充TLS结构
printf("StartAddressOfRawData: %08X\n", pTLS->StartAddressOfRawData);
printf("EndAddressOfRawData: %08X\n", pTLS->EndAddressOfRawData);
printf("TLS_Callback: %08X\n", pTLS->AddressOfCallBacks);//tls回调函数
printf("TLS_Size: %08X\n", pTLS->SizeOfZeroFill);
printf("TLS_Characteristics: %08X\n", pTLS->Characteristics);
}

小结

写这玩意儿还是花费了很长时间的,当初学PE的时候对导入表和导出表没有仔细学习,写的时候还去补遗了,但其实对这些知识点熟悉,写起来还是很容易的,只要能正确访问目标就行了

写完能对PE文件的结构有更深刻的了解和体会。

(边看代码边阅读助于理解,:) ,还有就是尽量解析32文件,64位可能会有显示不出节区名之类的错误)