枚举导入表

通过枚举导入表,可以得到出现在so模块导入表中的函数地址

1
2
3
4
5
6
7
枚举导入表
var improts = Module.enumerateImports("libencryptlib.so");
for(let i = 0; i < improts.length; i++){
console.log(JSON.stringify(improts[i]));
console.log(improts[i].name + " " + improts[i].address);
}
上面的方法得到的是函数在内存中的实际加载地址,不是相对地址

枚举导出表

通过枚举导出表,可以得到出现在so模块导出表中的函数地址

1
2
3
4
5
//枚举导出表
var exports = Module.enumerateExports("libencryptlib.so");
for(let i = 0; i < exports.length; i++){
console.log(exports[i].name + " " + exports[i].address);
}

枚举符号表

通过枚举符号表,可以得到出现在符号表中的函数地址

1
2
3
4
5
枚举符号表
var symbols = Module.enumerateSymbols("libencryptlib.so");
for(let i = 0; i < symbols.length; i++){
console.log(symbols[i].name + " " + symbols[i].address);
}

符号表和导出表的区别

exports解析的SHT_DYNSYM sectionsymbols解析的是SHT_SYMTAB section

SYMTAB的范围比DYNSYM更大,不是so运行必须的,一般会被去掉

枚举模块

通过枚举模块,再枚举模块中的导出表(导出表的函数一般会被其他so文件作为导入函数使用),可以快速找到某个导入函数来自哪个so文件

1
2
3
4
//枚举进程中已加载的模块
var modules = Process.enumerateModules();
console.log(JSON.stringify(modules[0].enumerateExports()[0]));
会返回模块加载的基址、模块大小、path等信息

Hook导出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 导出函数的hook
// findExportByName第一个参数是so文件名,第二个参数是要hook的函数名(是汇编里的函数名,不是F5反编译出来的函数名)
// 因为F5的函数名都是修饰过的,汇编里的才是准确的
var funcAddr = Module.findExportByName("libencryptlib.so", "_ZN7MD5_CTX11MakePassMD5EPhjS0_");
console.log(funcAddr);
// 得到函数地址后,可以使用Interceptor.attach来hook函数
// 先执行onEnter里的代码,再执行真正的函数,再执行onLeave里的代码
Interceptor.attach(funcAddr, {
onEnter: function (args) {
// args[1]如果是地址,可以使用hexdump来得到地址中存放的数据
console.log("funcAddr onEnter args[1]: ", hexdump(args[1]));
// 使用toInt32来转换成十进制
console.log("funcAddr onEnter args[2]: ", args[2].toInt32());
// args[3]存放的是返回值
// 想要读取返回值,不能在onLeave里直接打印args[3],因为args是onEnter函数的局部变量
// 需要把args[3]作为一个属性赋值给当前对象,属性名随意
this.args3 = args[3];
}, onLeave: function (retval) {
console.log("funcAddr onLeave args[3]: ", hexdump(this.args3));
}
});

计算函数地址

在导入表、导出表、符号表里找不到的函数,地址需要自己计算

计算方式:so基址+函数在so中的偏移 [+1] (是否加一得看情况)

基址的获取

需要先得到so的基址,也就是模块基址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 各种方式得到so基址
var module1 = Process.findModuleByName("libencryptlib.so");
//console.log(JSON.stringify(module1));
console.log("module1", module1.base);

var module2 = Process.getModuleByName("libencryptlib.so");
console.log("module2", module2.base);

var soAddr = Module.findBaseAddress("libencryptlib.so");
console.log("soAddr", soAddr);

var modules = Process.enumerateModules();
for(let i = 0; i < modules.length; i++){
if(modules[i].name == "libencryptlib.so"){
console.log(modules[i].name + " " + modules[i].base);
}
}

// findModuleByAddress传入的是一个地址,返回地址对应的模块
var module = Process.findModuleByAddress(Module.findBaseAddress("libencryptlib.so"));
console.log("module " + module.name + " " + module.base);

函数地址的计算

如果是thumb指令,so基址+函数在so中的偏移 +1

如果是arm指令,so基址+函数在so中的偏移

在Android中,32位so中的函数,基本都是thumb指令,64位的so中的函数,基本都是arm指令

可以通过显示汇编指令对应的opcode bytes来判断,options->general->Number of opcode bytes (non-graph) 设置为4

arm指令为4字节,如果函数中有些指令是2个字节(就是4字节和2字节的指令都有的情况下),那么函数地址计算需要+1

其实加不加一都试一下就行

1
2
3
4
5
var soAddr = Module.findBaseAddress("libencryptlib.so");
// 如果想直接用数值来加上偏移,需要把数值转换一下,如下
// var so = 0x77ab999000;
// console.log(ptr(so).add(0x1FA38)); // new NativePointer()
var funcAddr = soAddr.add(0x1FA38);

Hook任意函数

1
2
3
4
5
6
7
8
9
10
11
12
13
var soAddr = Module.findBaseAddress("libencryptlib.so");
// var so = 0x77ab999000;
// console.log(ptr(so).add(0x1FA38)); // new NativePointer()
var funcAddr = soAddr.add(0x1FA38);
Interceptor.attach(funcAddr, {
onEnter: function (args) {
console.log("funcAddr onEnter args[1]: ", hexdump(args[1]));
console.log("funcAddr onEnter args[2]: ", args[2].toInt32());
this.args3 = args[3];
}, onLeave: function (retval) {
console.log("funcAddr onLeave args[3]: ", hexdump(this.args3));
}
);

so层hook其实很简单

就是把上面的代码的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
29
30
31
32
33
34
// 有手就行的so hook
function print_arg(addr){
var module = Process.findRangeByAddress(addr);
if(module != null) return hexdump(addr) + "\n";
return ptr(addr) + "\n";
}
function hook_native_addr(funcPtr, paramsNum){
// 通过函数地址定位到函数所属哪个so模块
var module = Process.findModuleByAddress(funcPtr);
Interceptor.attach(funcPtr, {
onEnter: function(args){
this.logs = [];
this.params = [];
this.logs.push("call " + module.name + "!" + ptr(funcPtr).sub(module.base) + "\n");
for(let i = 0; i < paramsNum; i++){
// 不管函数是不是把参数当返回值用的,反正都打印一遍
this.params.push(args[i]);
this.logs.push("this.args" + i + " onEnter: " + print_arg(args[i]));
}
}, onLeave: function(retval){
for(let i = 0; i < paramsNum; i++){
this.logs.push("this.args" + i + " onLeave: " + print_arg(this.params[i]));
}
// retval是目标函数的返回值
this.logs.push("retval onLeave: " + print_arg(retval) + "\n");
// 执行完毕的时候把执行前和执行后的log一起打印
console.log(this.logs);
}
});
}

var soAddr = Module.findBaseAddress("libencryptlib.so");
var funcAddr = soAddr.add(0x1FA38);
hook_native_addr(funcAddr, 4);

修改函数参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//修改函数数值参数和返回值
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log("soAddr", soAddr);
var add = soAddr.add(0x165C);
Interceptor.attach(add, {
onEnter: function (args) {
args[2] = ptr(1000); // new NativePointer,也不能直接使用=1000赋值
console.log(args[2].toInt32());
console.log(args[3]);
console.log(args[4]);
}, onLeave: function (retval) {
retval.replace(1000); // 不能使用=直接赋值
console.log(retval.toInt32());
}
});

修改函数字符串参数

1
2
3
4
5
6
7
8
9
10
11
如下方法:

把char *指向的字符串修改掉,新字符串一般不超出原字符串长度

把so中已有的字符串地址传给函数

修改MD5_CTX结构体中存放数据的内容

构建新的字符串,需要注意构建的字符串变量的作用域

替换函数

以如下函数为例

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
__int64 __fastcall Java_com_xiaojianbang_ndk_NativeHelper_md5(_JNIEnv *a1, __int64 a2, __int64 a3)
{
__int64 result; // x0
int i; // [xsp+34h] [xbp-12Ch]
const char *v5; // [xsp+38h] [xbp-128h]
size_t v8; // [xsp+70h] [xbp-F0h]
char v9[88]; // [xsp+98h] [xbp-C8h] BYREF
char v10[32]; // [xsp+F0h] [xbp-70h] BYREF
__int128 v11[2]; // [xsp+110h] [xbp-50h] BYREF
char v12; // [xsp+130h] [xbp-30h]
char v13[16]; // [xsp+138h] [xbp-28h] BYREF
__int64 v14; // [xsp+148h] [xbp-18h]

v14 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
v5 = (const char *)jstring2cstr(a1, a3);
v12 = 0;
memset(v11, 0, sizeof(v11));
MD5Init(v9);
v8 = strlen(v5);
MD5Update(v9, v5, v8);
MD5Final(v9, v13);
for ( i = 0; i <= 15; ++i )
{
sub_1848(v10, 32LL, "%02x", (unsigned __int8)v13[i]);
__strncat_chk(v11, v10, 2LL, 33LL);
}
_JNIEnv::ReleaseStringUTFChars(a1, a3, v5);
result = _JNIEnv::NewStringUTF(a1, (const char *)v11);
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return result;
}

MD5Update(v9, v5, v8); ,一个参数是CTX,第二个参数是char*,第三个参数是长度,hook这个函数

方法一:采取把char *指向的字符串修改掉,新字符串一般不超出原字符串长度的方法,一般不用,因为这个地址的内容可能会被其他代码去访问

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 stringToBytes(str){
return hexToBytes(stringToHex(str));
}

// Convert a ASCII string to a hex string
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;
}

// Convert a hex string to a ASCII string
function hexToString(hexStr) {
var hex = hexStr.toString();//force conversion
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}

// 修改函数字符串参数
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log("soAddr", soAddr);
var MD5Update = soAddr.add(0x1D68);
Interceptor.attach(MD5Update, {
onEnter: function (args) {
if(args[1].readCString() == "xiaojianbang"){
var newStr = "xiaojian";
// args[1]是一个指针,往这个指针所指向的地址里写入数据
args[1].writeByteArray(hexToBytes(stringToHex(newStr) + "00"));
console.log(hexdump(args[1]));
args[2] = ptr(newStr.length);
console.log(args[2].toInt32());
}
}, onLeave: function (retval) {

}
});

方法二:把so中已有的字符串地址传给函数以及操作C语言结构体,其实也不怎么用,虽然不会破坏指针所指向的内存数据,但是不一定每次都能找到符合条件的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// frida操作C语言结构体
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log("soAddr", soAddr);
var MD5Update = soAddr.add(0x1D68);
Interceptor.attach(MD5Update, {
onEnter: function (args) {
this.args0 = args[0];
this.args1 = args[1];
if(args[1].readCString() == "xiaojianbang"){
// 把so中已有的字符串地址传给函数
args[1] = soAddr.add(0x38A1);
console.log(hexdump(args[1]));
args[2] = ptr(soAddr.add(0x38A1).readCString().length);
console.log(args[2].toInt32());
}
}, onLeave: function (retval) {
if(this.args1.readCString() == "xiaojianbang"){
//操作C语言结构体,其实就是访问内存然后写入内存
console.log(hexdump(this.args0.add(24).writeByteArray(stringToBytes("dadajianbang"))));
}
}
});

方法三:构建新的字符串,需要注意构建的字符串变量的作用域

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
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log("soAddr", soAddr);
var MD5Update = soAddr.add(0x1D68);
var newStr = "gdfgdhfgjghjgkhjkh;kl;k;";
// allocUtf8String是申请内存并同时写入
var newStrAddr = Memory.allocUtf8String(newStr);
Interceptor.attach(MD5Update, {
onEnter: function (args) {
this.args0 = args[0];
this.args1 = args[1];
if(args[1].readCString() == "xiaojianbang"){
// 不能把字符串声明写在这里
// var newStr = "gdfgdhfgjghjgkhjkh;kl;k;";
// var newStrAddr = Memory.allocUtf8String(newStr);
// 因为属于局部变量,onEnter执行结束之后变量被回收了
args[1] = newStrAddr;
console.log(hexdump(args[1]));
args[2] = ptr(newStr.length);
console.log(args[2].toInt32());
}
}, onLeave: function (retval) {
if(this.args1.readCString() == "xiaojianbang"){
console.log(hexdump(this.args0));
}
}
});

hook dlopen

有些函数,只在so文件首次被加载的时候才会调用,比如initarray或者jnionload等等,就没办法点击按钮之后再去hook了,这个时候需要通过hook来知道so文件什么时候被加载,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
//hook_dlopen
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();
}
});
}

function hook_func() {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
console.log("soAddr", soAddr);
var MD5Final = soAddr.add(0x3540);
Interceptor.attach(MD5Final, {
onEnter: function (args) {
this.args1 = args[1];
}, onLeave: function (retval) {
console.log(hexdump(this.args1));
}
});
}
// 7.0以上是android_dlopen_ext,以下是dlopen
// 如果不知道so文件的名字,使用Module.findExportByName(null,"dlopen");,frida会自己根据函数名找so文件
var dlopen = Module.findExportByName("libdl.so", "dlopen");
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
//console.log(JSON.stringify(Process.getModuleByAddress(dlopen)));
hook_dlopen(dlopen, "libxiaojianbang.so", hook_func);
hook_dlopen(android_dlopen_ext, "libxiaojianbang.so", hook_func);0

内存读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 内存读写
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
// dump指定地址的内容
console.log(hexdump(soAddr.add(0x38A1)));
// 读取指定地址字符串
console.log(soAddr.add(0x38A1).readCString());
// 往指定地址写入数据
console.log(soAddr.add(0x38A1).writeByteArray(stringToBytes("0123456789abcdef")));
// 从指定地址开始读,读取33个字节
// soAddr.add(0x38A1).readPointer()是读取4个字节或者8个字节,取决于32位还是64位,返回native pointer类型
console.log(soAddr.add(0x38A1).readByteArray(33));

// alloc返回native pointer类型,这个类型在js里可以直接用,可以直接调用writeByteArray写数据
var addr = Memory.alloc(13);
addr.writeByteArray(stringToBytes("xiaojianbang\0"));
console.log(addr.readByteArray(13));
var str = Memory.allocUtf8String("xiaojianbang");
console.log("Memory.allocUtf8String: ", str.readByteArray(13));
// 修改内存权限
Memory.protect(ptr(libso.base), libso.size, 'rwx');

操作汇编代码

arm汇编

32位下传参 ro-r3寄存器,不够的话栈传参,63位下x0-x7传递,不够的话栈传参(x0-x7其实就是w0-w7,都是寄存器)

STR指令保存数据,STR X1, [SP, #0x20+var_10] ,表示把X1的值给放到SP+0x20+var_10的位置处

LDR指令保存数据,LDR W9,[SP, #0x20+var_11],表示把SP, #0x20+var_11位置处的值赋值给W9

在线ARM与Hex机器码转换 https://armconverter.com

修改汇编指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function changeCode() {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");

var codeAddr = soAddr.add(0x1684);
Memory.protect(codeAddr, 4, 'rwx');
// 修改地址对应的指令
codeAddr.writeByteArray(hexToBytes("0001094B")); //sub w0, w8, w9
// 将对应地址的指令解析成汇编
console.log(Instruction.parse(codeAddr).toString());
// 利用frida提供的API来写汇编代码
new Arm64Writer(soAddr.add(0x167C)).putNop();
console.log(Instruction.parse(soAddr.add(0x167C)).toString());

var codeAddr = soAddr.add(0x1684);
// 第一个参数是地址,第二个参数是大小,第三个参数是回调函数
// 第一个参数就是要往哪个地址写指令
// 第二个参数是指令的字节大小,32位就是4个字节
// patchCode实际上就是当程序执行到这个地址的函数的时候才会生效
Memory.patchCode(codeAddr, 4, function (code) {
var writer = new Arm64Writer(code, { pc: codeAddr });
writer.putBytes(hexToBytes("0001094B")); //sub w0, w8, w9
writer.putBytes(hexToBytes("FF830091")); //ADD SP, SP, #0x20
writer.putRet();
writer.flush();
});

}

so层主动调用任意函数

声明函数指针

1
2
3
4
文档:https://frida.re/docs/javascript-api/#NativeFunction

语法:new NativeFunction(address, returnType, argTypes[, abi])
abi在Android端一般不写,returnType是函数返回值,参数类型的话,如果有jstring类型,也是pointer

支持的returnTypeargTypes

1
2
void pointer int uint long ulong char uchar float double int8 uint8 int16
uint16 int32 uint32 int64 uint64 bool size_t ssize_t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// so层主动调用任意函数
// 假设函数是(const char*)jstring2cstr(JNIEnv *a1,_int64 a3);
// _int64在这里就是jstring 类型(在 IDA 中显示为 _int64,因为它是一个指针)
function call_so_func() {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var funAddr = soAddr.add(0x124C);
var jstr2cstr = new NativeFunction(funAddr, 'pointer', ['pointer','pointer']);
// 构建第一个参数
var env = Java.vm.tryGetEnv();
console.log("env: ", JSON.stringify(env));
// 构建第二个参数,将js类型的字符串转为java类型的字符串也就是jstring类型,不然无法识别
var jstring = env.newStringUtf("xiaojianbang");
var retval = jstr2cstr(env, jstring);
// 返回值是地址,需要调用readCString来读取地址中的内容
console.log(retval.readCString());
}

ps:如果上面的env获取不到的话,可以试试把函数代码封装到Java.perform里面去

hook libc读写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// hooklibc读写文件
// 实际上就是主动调用这些函数来完成文件读写
function writeTxt() {
var fopenAddr = Module.findExportByName("libc.so", "fopen");
var fputsAddr = Module.findExportByName("libc.so", "fputs");
var fcloseAddr = Module.findExportByName("libc.so", "fclose");
console.log(fopenAddr, fputsAddr, fcloseAddr);
// 查一下函数原型
var fopen = new NativeFunction(fopenAddr, 'pointer', ['pointer', 'pointer']);
var fputs = new NativeFunction(fputsAddr, 'int', ['pointer', 'pointer']);
var fclose = new NativeFunction(fcloseAddr, 'int', ['pointer']);
// allocUtf8String会返回一个native pointer
var fileName = Memory.allocUtf8String("/data/data/com.xiaojianbang.app/xiaojianbang.txt");
var openMode = Memory.allocUtf8String("w");
var data = Memory.allocUtf8String("QQ24358757\n");
// 不能直接用fopen("test.txt","w");
// 因为js里的string直接传的话,这不是c能识别的指针类型
var file = fopen(fileName, openMode);
// 如果报错,可能是目录没有写权限,一般放到app里的私有目录没有问题
console.log(file);
fputs(data, file);
fclose(file);
}

JNI函数的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
29
30
31
32
33
34
35
36
37
// hook libart 来hook jni相关函数
// 方法一:编译之后的jni函数在libart.so里
// jni函数在libart.so中不属于导出表,在符号表里,需要用enumerateSymbols()来获取符号表内容
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){
console.log(symbol.name, symbol.address);
newStringUtf = symbol.address;
}
}
Interceptor.attach(newStringUtf, {
onEnter: function (args) {
console.log("newStringUtf args: ", args[1].readCString());
}, onLeave: function (retval) {
console.log("newStringUtf retval: ", retval);
}
});
}

// 方法二:jni函数本质上是通过jnienv调用的,jnienv本质是个JNINativeInterface的结构体,定位出这个结构体即可
// 一般不用这个方法
function hook_jni2() {
// JNIEnv保存的是JNINativeInterface类型的指针,还需要再readPointer()访问指针指向的地址
var envAddr = Java.vm.tryGetEnv().handle.readPointer();
var funAddr = envAddr.add(48).readPointer();
//console.log(Instruction.parse(funAddr).toString());
Interceptor.attach(funAddr, {
onEnter: function (args) {
console.log("FindClass args: ", args[1].readCString());
}, onLeave: function (retval) {
console.log("FindClass retval: ", retval);
}
});
}

主动调用jni函数

可以使用封装好的API调用jni函数,比声明NativeFunction更简单一点

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
// 直接调用API去调用jni函数
function call_jni() {
var retval = Java.vm.tryGetEnv().newStringUtf("xiaojianbang");
// 直接打印retval会返回例如0x1这种JVM句柄
// jstring类型的retval
console.log(retval);
// GetStringUTFChars方法会将JVM内部管理的字符串数据复制到C的内存空间,并返回一个指针(char*)
console.log(Java.vm.tryGetEnv().getStringUtfChars(retval).readCString());
}

// 声明NativeFunction的方法去调用
function call_jni2() {
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){
console.log(symbol.name, symbol.address);
newStringUtf = symbol.address;
}
}
var newStringUtf_func = new NativeFunction(newStringUtf, 'pointer', ['pointer', 'pointer']);
var jstring = newStringUtf_func(Java.vm.tryGetEnv().handle, Memory.allocUtf8String("xiaojianbang"));
console.log(jstring);

var envAddr = Java.vm.tryGetEnv().handle.readPointer();
var GetStringUTFChars = envAddr.add(0x548).readPointer();
var GetStringUTFChars_func = new NativeFunction(GetStringUTFChars, 'pointer', ['pointer', 'pointer', 'pointer']);
// ptr(0)表示传入指针类型,ptr()函数返回native pointer类型
var cstr = GetStringUTFChars_func(Java.vm.tryGetEnv().handle, jstring, ptr(0));
console.log(cstr.readCString());

}

so层打印函数栈

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

二级指针的构造

函数

1
2
3
void xiugaiStr(char** str){
strcat(*str, "testtesttest");
}

demo

1
2
3
4
5
6
7
8
9
10
11
// 二级指针的构造
function call_func() {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var xiugaiStr = soAddr.add(0x17D0);
var xiugaiStr_func = new NativeFunction(xiugaiStr, 'int64', ['pointer']);
var strAddr = Memory.allocUtf8String("dajianbang");
console.log(hexdump(strAddr));
var finalAddr = Memory.alloc(8).writePointer(strAddr);
xiugaiStr_func(finalAddr);
console.log(hexdump(strAddr));
}

如何确认native函数在哪个so文件

静态分析查看静态代码块中加载的so,并不靠谱

有可能native函数声明在一个类中,so加载(System.LoadLibrary函数)在其他的类中,还可以在另外的类中,一次性加载所有的so

或者可以hook系统函数来得到绑定的native函数地址,然后再得到so地址

jni函数动态注册的话,可以hook RegisterNatives,得到methods结构体,从而找到函数的地址、函数名等信息,然后定位出模块

jni函数静态注册的话,可以hook dlsym ,通过dlsym去找对应的函数,静态注册的函数名是不能混淆的,不然app找不到函数

有的app会在某个类中一次性把所有so加载了,而且如果是动态注册,JNI_Onload函数可能也是加密的,字符串也是加密的,这个时候静态分析就很难

快速定位jni静态注册函数

dlsym第一个参数是dlopen的返回值(用于标识打开的共享库),第二个参数是char*,即symbol,通常是函数名,返回值是函数的地址,通过函数的地址判断地址处于哪个so文件的空间中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook_dlsym() {
var dlsymAddr = Module.findExportByName("libdl.so", "dlsym");
console.log(dlsymAddr);
Interceptor.attach(dlsymAddr, {
onEnter: function (args) {
this.args1 = args[1];
}, onLeave: function (retval) {
// retval实际上也是native pointer类型
var module = Process.findModuleByAddress(retval);
if(module == null) return;
console.log(this.args1.readCString(), module.name, retval, retval.sub(module.base));
}
});
}
hook_dlsym();

快速定位jni动态注册函数

RegisterNatives是jni函数,首先要找到RegisterNatives的地址,RegisterNatives的第三个参数是JNINativeMethod数组的指针

1
2
3
4
5
typedef struct {
const char* name; // Java 方法名
const char* signature; // 方法签名(表示参数和返回值类型)
void* fnPtr; // 指向本地实现函数的指针
} JNINativeMethod;
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
function hook_RegisterNatives() {
var symbols = Process.getModuleByName("libart.so").enumerateSymbols();
var RegisterNatives_addr = null;
for (let i = 0; i < symbols.length; i++) {
var symbol = symbols[i];
if(symbol.name.indexOf("CheckJNI") == -1 && symbol.name.indexOf("RegisterNatives") != -1) {
RegisterNatives_addr = symbol.address;
}
}
console.log("RegisterNatives_addr: ", RegisterNatives_addr);

Interceptor.attach(RegisterNatives_addr, {
onEnter: function (args) {

var env = Java.vm.tryGetEnv();
// 函数的第二个参数是jclazz类型,要获取类名需要调用JNIEnv的getClassName
// 注意第一个参数是JniEnv* ,所以下标是1
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) {
}
});


}
hook_RegisterNatives();

建议使用--no-pause启动frida来hook更多函数

inline hook

想hook某一行汇编代码,其实也是基址+偏移的方式去hook,和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
function inlineHook() {
// 目标汇编代码是ADD W8,W8,W9
var nativePointer = Module.findBaseAddress("libxiaojianbang.so");
var hookAddr = nativePointer.add(0x17BC);
Interceptor.attach(hookAddr, {
onEnter: function (args) {
// args只是摆设,因为没有参数和返回值的概念了
// w8其实就是x8的低32位,硬要取w8的话,和0xffffffff位与就行了
console.log("onEnter: ", this.context.x8);
}, onLeave: function (retval) {
console.log("onLeave: ", this.context.x8.toInt32());
// 取x8的低3位,7就是0x111
console.log(this.context.x8 & 7);
}
});

var nativePointer = Module.findBaseAddress("libxiaojianbang.so");
var hookAddr = nativePointer.add(0x1B70);
Interceptor.attach(hookAddr, {
onEnter: function (args) {
console.log("onEnter: ", this.context.x1);
console.log("onEnter: ", hexdump(this.context.x1));
}, onLeave: function (retval) {

}
});
}

inline hook在32位so下并不稳定,可能会崩溃,32位下的话要么多试几次或者换unidbg

打印so层函数栈

env和env.handle

这两个在一定程度上可以通用,或者可以说会完成自动转换,使用Frida封装的JNI相关API,必须使用env,当参数需要JNIEnv*时,可以使用envenv.handle

1
2
3
var env = Java.vm.tryGetEnv();
// handle可以理解为env的内存地址
env.handle;

打印so层函数栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var updateAddr = soAddr.add(0x21B0);
Interceptor.attach(updateAddr, {
onEnter: function (args) {
// .map是js里的方法,相当于把Thread.backtrace(this.context, Backtracer.ACCURATE的结果丢给.map去操作
// .map的参数是一个函数,和下面不写注释的写法是一样的
//console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log("==================================================");
console.log(soAddr);
console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(function (value) {
// 对map的参数函数加以改造,可以打印出目标so文件的函数栈以及相对偏移
var symbol = DebugSymbol.fromAddress(value);
console.log(symbol.moduleName);
if(symbol.moduleName === "libxiaojianbang.so") {
return symbol + " offset: " + value.sub(soAddr);
}
return symbol;
}).join('\n'));
}, onLeave: function (retval) {

}
});

注意打印出的函数地址是实际执行的汇编代码的下一行指令的地址

替换函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var addAddr = soAddr.add(0x1A0C);
// NativeCallback的第一个参数是函数逻辑,第二个参数是返回值,第三个参数是参数列表
// 并不推荐把返回值类型和原函数类型对不上,可能程序会崩
// 参数个数和类型没有那么需要严格一致对上
Interceptor.replace(addAddr, new NativeCallback(function () {
console.log(100);
}, 'void', []));

// 假设原函数有5个参数,返回int
// 如果想获取传入原函数的参数的话,NativeCallback的参数列表最好和原函数一致
// 而且NativeCallback的返回值最好和原函数返回值一致
// 替换函数并不是真正的替换掉了函数,想要在replace中调用原函数可以可以的,通过主动调用即new NativeFunction
var add = new NativeFunction(addAddr, 'int', ['pointer','pointer', 'int','int','int']);
Interceptor.replace(addAddr, new NativeCallback(function (a, b, c, d,e) {
console.log(a, b, c, d, e);
var oldResult = add(a, b, c, d, e);
console.log(oldResult);
return 100;
}, 'int', ['pointer', 'pointer', 'int','int','int']));

hexdump方法

打印指定地址的内存空间

1
2
3
4
5
// offset是从距离多少的位置开始打印内存
// length是打印多少字节的内存(length是数值,args[2]是native pointer类型,需要转换,而且一般是十进制)
// 注意length是距离args[1]的长度
// 比如hexdump(1,{offset:6, length:10}),只会打印从地址7开始的3个值,也就是说length不受offset影响
console.log(hexdump(args[1], {offset: 6, length: args[2].toUInt32(), header: false}));

Frida trace

tracenatives是IDA中的插件,运行之后会生成一个txt,使用frida trace和该txt结合可以打印出函数栈

加载txt的时候会生成或者加载一些以偏移命名的js文件,可以改这些js文件,让js文件打印出我们想要的结果,比如加上hexdump这些

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
/*
* Auto-generated by Frida. Please modify to match the signature of sub_21b0.
* This stub is currently auto-generated from manpages when available.
*
* For full API reference, see: https://frida.re/docs/javascript-api/
*/

{
/**
* Called synchronously when about to call sub_21b0.
*
* @this {object} - Object allowing you to store state for use in onLeave.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {array} args - Function arguments represented as an array of NativePointer objects.
* For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
* It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
* @param {object} state - Object allowing you to keep state across function calls.
* Only one JavaScript function will execute at a time, so do not worry about race-conditions.
* However, do not use this to store function arguments across onEnter/onLeave, but instead
* use "this" which is an object for keeping state local to an invocation.
*/
onEnter(log, args, state) {
var soAddr = Module.findBaseAddress("libxiaojianbang.so");
var funcAddr = soAddr.add(0x21b0);
log(DebugSymbol.fromAddress(funcAddr).name, hexdump(args[1], {length: 12, header: false}));
},

/**
* Called synchronously when about to return from sub_21b0.
*
* See onEnter for details.
*
* @this {object} - Object allowing you to access state stored in onEnter.
* @param {function} log - Call this function with a string to be presented to the user.
* @param {NativePointer} retval - Return value represented as a NativePointer object.
* @param {object} state - Object allowing you to keep state across function calls.
*/
onLeave(log, retval, state) {
}
}

内存读写监控

不推荐这种方式,一般用unidbg去监控,了解即可

逻辑就是在首次加载目标so文件的时候调用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
29
30
31
32
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(0x3CFD);
Memory.protect(addr, 8, '---'); //修改内存页的权限,不是字节级别的内存权限,是一整个页

}