Frida Native层Hook学习笔记

Native层的hook

如何hook

知道函数地址就可以hook和主动调用

如何获取函数地址

  1. 利用函数名,通过Module.findExportByName函数获取函数地址。
  2. 通过Module.findBaseAddress函数获取所在库函数地址,然后加上函数偏移量即为函数地址。

hook示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const libName = 'libnative-lib.so'
const funcName = 'Java_com_example_demoso1_MainActivity_stringFromJNI'
const funcAddr = Module.findExportByName(libName, funcName)
console.log(funcAddr)
//或者
const offset = 0x10018
const baseAddr = Module.findBaseAddress(libName)
const funcAddr = baseAddr.add(offset)
console.log(funcAddr)

// 通过Interceptor.attach来对函数进行hook
Interceptor.attach(funcAddr, {
onEnter: function (args) {
// do something
},
onLeave: function (retval) {
// do something
},
})

Frida Native层的API

枚举导出表

1
2
3
4
5
const imports = Module.enumerateImports('libart.so');//同目录下的动态库
for (const iterator of imports) {
console.log(JSON.stringify(iterator));//转成json格式

}

枚举导入表

1
2
3
4
5
const exports = Module.enumerateExports('libart.so');//同目录下的动态库
for (const iterator of exports) {
console.log(JSON.stringify(iterator));//转成json格式
// {"type":"function","name":"_ZN3art3jit3Jit13JitAtFirstUseEv","address":"0x7fff7229e1b0"}
}

枚举符号表

1
2
3
4
5
const exports = Module.enumerateSymbols('libart.so');//同目录下的动态库
for (const iterator of exports) {
console.log(JSON.stringify(iterator));//转成json格式
// {"isGlobal":true,"type":"function","section":{"id":"12.text","protection":"r-x"},"name":"art_l2d","address":"0x7fff724bb4d0","size":6}
}

枚举进程中已加载的模块

1
2
3
4
5
const modules = Process.enumerateModules();
for (const iterator of exports) {
console.log(JSON.stringify(iterator));//转成json格式
// {"name":"libmtp.so","base":"0x7fff6e6c4000","size":200704,"path":"/system/lib64/libmtp.so"}
}

通过函数名找导出函数

findExportByName

1
2
const funcAddr = Module.findExportByName('libart.so', '_ZN9unix_file6FdFile5WriteEPKcll');
console.log(funcAddr);

getExportByName

findExportByName

模块(库)基址获取方式

Module.findBaseAddress

1
2
3
const baseAddr = Module.findBaseAddress('libencryptlib.so')
console.log(baseAddr)
// "0x7ffff6013000"

Module.getBaseAddress

Module.findBaseAddress

Process.findModuleByName

通过模块名找。

1
2
3
const module = Process.findModuleByName('libpiex.so')
console.log(JSON.stringify(module))
//{"name":"libpiex.so","base":"0x7ffff6013000","size":114688,"path":"/system/lib64/libpiex.so"}

Process.getModuleByName

Process.findModuleByName

Process.findModuleByAddress()

1
2
const module = Process.findModuleByAddress(addr)//addr可以是该模块中的任何一个地址
console.log(JSON.stringify(module))

Process.getModuleByAddress()

Process.findModuleByAddress()

调用native函数

new NativeFunction(address, returnType, argTypes[, options]): 创建一个新的 NativeFunction 用于调用位于指定地址的函数,其中 returnType 指定返回类型,argTypes 数组指定参数类型。

支持的参数类型:

void、pointer、int、uint、long、ulong、char、uchar、size_t、ssize_t、float、double、int8、uint8、int16、uint16、int32、uint32、int64、uint64、bool

示例:

1
2
3
4
5
6
function main(){
const addAddr = Module.findExportByName('libleidian.so', 'Java_com_example_leidian_MainActivity_add');
const addFunction = new NativeFunction(addAddr, 'int64', ['int64', 'int64', 'int', 'int']);
const result = addFunction(0, 0, 1, 2);
console.log("result: ", result);
}

hook任何函数

总之,hook可以hook任何函数,请不要局限于你的想象力。

注意事项

如果需要手动计算函数地址,请注意安卓32bit、64bit的计算区别:

安卓位数 指令 计算方式
32 位 thumb so 基址 + 函数在 so 中的偏移 + 1
64 位 arm so 基址 + 函数在 so 中的偏移

也可在IDA中通过显示汇编指令对应的硬编码来判断。因为 arm 指令为 4 个字节,如果函数中有些指令是两个字节,那么函数地址计算需要 + 1。(加不加1都可以试一下)

Interceptor.replace函数

Interceptor.replace(target, replacement[, data]):将target处的函数替换为replacement的实现(使用NativeCallback实现替换)。如果您想要完全或部分替换现有函数的实现,通常会使用此方法。

示例:

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修改参数值以及返回值
function replaceHook(addr) {
//参数个数和类型可通过IDA查看
//创建一个新的native函数,用于调用指定地址(第一个参数决定)的函数,
const addFunction = new NativeFunction(addr, 'int64', ['int64', 'int64', 'int', 'int']);
// 重实现目标函数地址的功能实现
Interceptor.replace(addr, new NativeCallback(
function(arg1, arg2, arg3, arg4){
console.log("come into add function");
console.log("arg1: ", arg1);
console.log("arg2: ", arg2);
console.log("arg3: ", arg3);
console.log("arg4: ", arg4);
//保持原函数功能不变
var result = addFunction(arg1, arg2, arg3, arg4);//主动调用
return result
}, 'int64', ['int64', 'int64', 'int', 'int'])
)

//调用hook后的函数查看效果
var result = addFunction(0, 0, 1, 2);
console.log("result: ", result);

}

function hookAdd(){
var addAddr = Module.findExportByName('libleidian.so', 'Java_com_example_leidian_MainActivity_add');
replaceHook(addAddr);
}

setImmediate(hookAdd)

请注意!!!需先启动应用,然后再attach,而非spawn,否者找不到函数地址。

Interceptor.attach函数

Interceptor.attach(target, callbacks[, data]):拦截target处的函数调用,callbacks包括onEnteronLeave的回调函数实现。

示例:

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修改参数值以及返回值
function replaceHook(addr) {

//拦截目标函数调用
Interceptor.attach(addr,
{
onEnter: function(args){//目标函数执行之前需要做的事
console.log("come into add function");
console.log("arg1: ", args[0]);
console.log("arg2: ", args[1]);
console.log("arg3: ", args[2]);
console.log("arg4: ", args[3]);
console.log('Context : ' + JSON.stringify(this.context));
//context包含各种寄存器的值
//Context : {"pc":"0x7fff588cf950","sp":"0x7fff6bbf9828","rax":"0x0","rcx":"0x2","rdx":"0x1","rbx":"0x0","rsp":"0x7fff6bbf9828","rbp":"0x7fff6bbf9830","rsi":"0x0","rdi":"0x0","r8":"0x18","r9":"0x7fff5a0f512c","r10":"0x0","r11":"0x7fff588cf950","r12":"0x7fff6bbf9990","r13":"0x1","r14":"0x4","r15":"0xfffffffffffffffc","rip":"0x7fff588cf950"}
},
onLeave: function(retval){//目标函数执行之后需要做的事
console.log("return value: ", retval);
//修改返回值
retval.replace(0x9)//修改成整数
//retval.replace(ptr(0x14792))//修改成指针
}
}
)

//调用hook后的函数查看效果
const addFunction = new NativeFunction(addr, 'int64', ['int64', 'int64', 'int', 'int']);
var result = addFunction(0, 0, 1, 2);
console.log("result: ", result);

}

同样需要先等待应用程序启动,再attach。

hook修改函数的参数和返回值

修改数值

1
2
3
4
5
6
7
8
9
Interceptor.attach(helloAddr, {
onEnter: function (args) {
args[2] = ptr(1000) // 指针类型参数 和 整数类型参数 修改
},
onLeave: function (retval) {
retval.replace(1337) // 整数类型返回值替换
retval.replace(ptr("0x1234"))//指针类型返回值替换 (指针其实仍然是数值)
},
})

修改字符串????问题很严重

利用so层原有字符串进行地址替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Interceptor.attach(funcAddr, {
onEnter: function (args) {
const strAddr = baseAddr.add(0x142D7) // 字符串的文件偏移量0x142D7
args[1] = strAddr // 替换字符串地址参数
const str = strAddr.readCString()
console.log("target string: ", str);

args[2] = ptr(str.length) // 替换字符串长度参数
console.log("target string length: ", args[2]);
},
onLeave: function (retval) {

},
})
创建新字符串并进行地址替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const newStr = "some strings";
const newStrAddr = Memory.allocUtf8String(newStr); // 使用Frida的Memory来申请内存区域 返回的是一个指针

Interceptor.attach(funcAddr, {
onEnter: function (args) {
// const newStrAddr = Memory.allocUtf8String(newStr); // 如果在这里申请的话,到onLeave将会回收 可以在全局定义或使用this.newStrAddr 附加到自身
args[1] = newStrAddr
console.log("target string: ", args[1].readCString())

args[2] = ptr(newStr.length)
console.log("target string length: ", args[2]);
},
onLeave: function (retval) {},
})
原地址上修改字符
1
2
3
4
5
6
7
8
9
10
11
12
Interceptor.attach(funcAddr, {
onEnter: function (args) {
let newStr = 'some strings'
// 需要写入字节数组的方式来写入字符串
args[1].writeByteArray(hexToBytes(stringToHex(newStr) + '00')) // c语言字符串结尾为0字节
console.log("target string: ", args[1].readCString())

args[2] = ptr(newStr.length)
console.log("target string length: ", args[2]);
},
onLeave: function (retval) {},
})

内存读写

https://frida.re/docs/javascript-api/#memory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 读取指定地址的字符串
const baseAddr = Module.findBaseAddress('libleidian.so')
console.log(baseAddr.add(0x1234).readCString())

// 2. dump指定地址的内存
console.log(hexdump(baseAddr.add(0x1234)))

// 3. 读指定地址的内存
console.log(baseAddr.add(0x1234).readByteArray(16))
console.log(Memory.readByteArray(baseAddr.add(0x2c00), 16)) //原先的API

// 4. 写指定地址的内存
baseAddr.add(0x1234).writeByteArray(stringToBytes('xiaojianbang'))

// 5. 申请新内存写入
Memory.alloc()
Memory.allocUtf8String()

// 6. 修改内存权限
Memory.protect(ptr(libso.base), libso.size, 'rwx')

修改native层代码

需注意架构区别!!!arm架构使用Arm64Writer修改,x64x86架构使用X86Writer修改。

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 hexToBytes(hex) {
let bytes = [];
for (let i = 0; i < hex.length; i += 2) {
bytes.push(parseInt(hex.substr(i, 2), 16));
}
return bytes;
}

function alterCode(){
const addFuncAddr = Module.findExportByName("libleidian.so", "Java_com_example_leidian_MainActivity_add")
const addInsAddr = addFuncAddr.add(0x15)
console.log("origin instruction => ", Instruction.parse(addInsAddr));//将对应地址的硬编码解析成汇编指令
// console.log(Process.arch);

// 修改内存保护为可写
// Memory.protect(addInsAddr, 0x1000, 'rwx');
//修改对应地址的硬编码
// addInsAddr.writeByteArray(hexToBytes(0x8B45EC))//无效

Memory.patchCode(addInsAddr, 1, function (code) {
// let Writer = new Arm64Writer(code, {pc: addInsAddr});
let Writer = new X86Writer(code, {pc: addInsAddr});
Writer.putBytes(hexToBytes("2b"));
Writer.flush();
});
console.log("current instruction => ", Instruction.parse(addInsAddr));//将对应地址的硬编码解析成汇编指令
}

函数堆栈打印

1
2
3
4
5
6
console.log(
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
//.map(symbol => `${symbol.name} (${symbol.address})`)
.join('\n')
)

寻找native函数所在的动态库

native函数根据注册时机可分为静态注册函数、动态注册函数,它们对应的hook点不同。

  • 动态注册函数:动态注册函数hook RegisterNatives函数。
  • 静态注册函数:可以hook dlsym函数(在动态加载的场景下)。

动态注册函数

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_RegisterNatives() {
let RegisterNatives_addr = null
let symbols = Process.findModuleByName('libart.so').enumerateSymbols()
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i].name
if (symbol.indexOf('CheckJNI') == -1 && symbol.indexOf('JNI') >= 0) {//适合所有的Android系统版本
if (symbol.indexOf('RegisterNatives') >= 0) {
RegisterNatives_addr = symbols[i].address
console.log('RegisterNatives_addr: ', RegisterNatives_addr)
}
}
}
Interceptor.attach(RegisterNatives_addr, {
onEnter: function (args) {
let env = args[0]
let jclass = args[1]
let class_name = Java.vm.tryGetEnv().getClassName(jclass)
let methods_ptr = ptr(args[2])
let method_count = args[3].toInt32()
console.log('RegisterNatives method counts: ', method_count)
/*
JNINativeMethod jmethods[] = {
{"DynamicRegistration", "()V", (void *)(DynamicRegistrationNative)}//DynamicRegistrationNative为native方法
};
*/
for (let i = 0; i < method_count; i++) {
let name = methods_ptr.add(i * Process.pointerSize * 3)
.readPointer()
.readCString()
let sig = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize)
.readPointer()
.readCString()
let fnPtr_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2)
.readPointer()
let find_module = Process.findModuleByAddress(fnPtr_ptr)
console.log(
'RegisterNatives java_class: ', class_name,
'name: ', name,
'sig: ', sig,
'fnPtr: ', fnPtr_ptr,
'module_name: ', find_module.name,
'module_base: ', find_module.base,
'offset: ', ptr(fnPtr_ptr).sub(find_module.base),
)
}
},
onLeave: function (retval) {},
})
}

静态注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_dlsym(dlsymFuncAddr){
Interceptor.attach(dlsymFuncAddr, {
onEnter: function (args) {
// do something
this.libAddr = ptr(args[0]) // args[0]是库基址
this.funcName = ptr(args[1]) // args[1]是函数名
},
onLeave: function (retval) {
// do something
let module = Process.findModuleByAddress(retval)//this.libAddr
console.log("function name: ", this.funcName.readCString(),
"lib name: ", module.name, //是否可以直接用libAddr.name?
"function's memory address: "retval,
"function offset: ", retval.sub(module.base)
)
},
})
}

let dlsymFuncAddr = Module.findExportByName('libdl.so', 'dlsym')
hook_dlsym(dlsymFuncAddr)

hook运行时动态加载的库与函数

  • dlopen 函数用于运行时动态加载一个共享库,并返回一个句柄。这个句柄可以用于后续的符号解析。
  • dlsym 函数用于获取运行时动态加载的库中符号的地址。该符号可以是函数、变量等。

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
29
30
31
32
33
34
35
36
//hook_dlopen
function hook_dlopen(dlopenFuncAdrr, libName, targetFuncOffset, callback) {
Interceptor.attach(addr, {
onEnter: function (args) {
let libPath = args[0].readCString()
// 是否是目标动态库
if (libPath.indexOf(libName) != -1) {
this.hook = true
}
},
onLeave: function (retval) {
//得对应的lib库加载完了才能hook目标函数
if (this.hook){
//dlopen返回值即lib库的指针
callback(retval.add(targetFuncOffset))
}
}
})
}

function hook_targetFunc(targetFuncAddr) {
Interceptor.attach(targetFuncAddr, {
onEnter: function (args) {
// do something
},
onLeave: function (retval) {
// do something
},
})
}

let dlopenFuncAdrr = Module.findExportByName('libdl.so', 'dlopen') // 低版本安卓系统
hook_dlopen(dlopenFuncAdrr, 'libxiaojianbang.so', hook_targetFunc)

//let dlopenFuncAdrr = Module.findExportByName('libdl.so', 'android_dlopen_ext') // 高版本安卓系统
//hook_dlopen(dlopenFuncAdrr, 'libxiaojianbang.so', hook_targetFunc)

hook dlsym函数

获取目标函数所在的so库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hook_dlsym(dlsymFuncAddr){
Interceptor.attach(dlsymFuncAddr, {
onEnter: function (args) {
// do something
this.libAddr = ptr(args[0]) // args[0]是库基址
this.funcName = ptr(args[1]) // args[1]是函数名
},
onLeave: function (retval) {
// do something
let module = Process.findModuleByAddress(retval)//this.libAddr
console.log("function name: ", this.funcName.readCString(),
"lib name: ", module.name, //是否可以直接用libAddr.name?
"function's memory address: "retval,
"function offset: ", retval.sub(module.base)
)
},
})
}

let dlsymFuncAddr = Module.findExportByName('libdl.so', 'dlsym')
hook_dlsym(dlsymFuncAddr)

inlineHook(针对寄存器的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//查看任意地址处的寄存器值
function inlineHook() {
var nativePointer = Module.findBaseAddress('libleidian.so')
var hookAddr = nativePointer.add(0x2080C)
Interceptor.attach(hookAddr, {
onEnter: function (args) {
console.log('onEnter: ', JSON.stringify(this.context))
// console.log('rax: ', this.context.rax)
},
onLeave: function (retval) {},
})

var funcAddr = Module.findExportByName('libleidian.so', 'Java_com_example_leidian_MainActivity_stringFromJNI');
var func = new NativeFunction(funcAddr, "int64", ["pointer"])
//主动调用该函数,以查看效果
func(Java.vm.tryGetEnv())

}

相关工具


参考:

https://frida.re/docs/javascript-api/(官方文档)

https://kuizuo.cn/docs/frida-so-hookhttps://blog.csdn.net/weixin_35016347/article/details/104002411)


Frida Native层Hook学习笔记
http://example.com/2024/07/29/Android安全/Frida Native层Hook学习笔记/
作者
gla2xy
发布于
2024年7月29日
许可协议