Frida so层中的hook
前言
so 中会接触到的东西:系统库函数、加密算法、jni 调用、系统调用、自定义算法
如何 hook
so hook 只需要得到一个地址,有函数地址就能 hook 与主动调用,与 java 层的 hook 一致。
得到函数地址的方式
- 通过 frida 提供的 api 来得到,该函数必须有符号的才可以
- 通过计算得到地址:so 基址+函数在 so 中的偏移[+1]
演示代码如下
const moduleName = 'libnative-lib.so'
let baseAddr = Module.findBaseAddress(moduleName)
let sub_99C0 = baseAddr.add(0x99c0 + 1)
Interceptor.attach(funcPtr, {
onEnter: function (args) {
// ...
},
onLeave: function (retval) {
// ...
},
})
API
枚举导入表
const improts = Module.enumerateImports('libencryptlib.so')
for (const iterator of improts) {
console.log(JSON.stringify(iterator))
// {"type":"function","name":"__cxa_atexit","module":"/apex/com.android.runtime/lib64/bionic/libc.so","address":"0x778957bd34"}
}
枚举导出表
const exports = Module.enumerateExports('libencryptlib.so')
for (const iterator of exports) {
console.log(JSON.stringify(iterator))
// {"type":"letiable","name":"_ZTSx","address":"0x74d594b1c0"}
}
枚举符号表
const symbols = Module.enumerateSymbols('libencryptlib.so')
for (const iterator of symbols) {
console.log(JSON.stringify(iterator))
// {"isGlobal":true,"type":"function","name":"pthread_getspecific","address":"0x0","size":0
}
枚举进程中已加载的模块
const modules = Process.enumerateModules()
console.log(JSON.stringify(modules[0].enumerateExports()[0]))
findExportByName
注: 函数名以汇编中出现的为准
const funcAddr = Module.findExportByName(
'libencryptlib.so',
'_ZN7MD5_CTX11MakePassMD5EPhjS0_',
)
// 返回的是函数地址 第二个参数根据汇编中为准
console.log(funcAddr)
// 通过Interceptor.attach来对函数进行hook
Interceptor.attach(funcAddr, {
onEnter: function (args) {
console.log('args[1]: ', hexdump(args[1])) // 打印参数的地址 通过hexdump打印16进制
console.log(this.context.x1) // 打印寄存器内容
console.log('args[2]: ', args[2].toInt32()) // 默认显示16进制,这里转为10进制
this.args3 = args[3] // 将args[3]值保存到this上
},
onLeave: function (retval) {
console.log('args[3]: ', hexdump(this.args3))
},
})
模块基址获取方式
如果在导入表、导出表、符号表里找不到的函数,那么函数地址需要自己计算
计算公式:so 基址+函数在 so 中的偏移[+1]
安卓位数 | 指令 | 计算方式 |
---|---|---|
32 位 | thumb | so 基址 + 函数在 so 中的偏移 + 1 |
64 位 | arm | so 基址 + 函数在 so 中的偏移 |
也可通过显示汇编指令对应的 opcode bytes,来判断
IDA -> Options -> General -> Number of opcode bytes (non-graph) 改为 4
arm 指令为 4 个字节,如果函数中有些指令是两个字节,那么函数地址计算需要 + 1
不清楚的话,+1 和不+1 都试一遍即可
所以获取基址就显得尤为重要
Process.findModuleByName
通过模块名找到模块
const module = Process.findModuleByName('libencryptlib.so')
console.log(JSON.stringify(module))
// {"name":"libencryptlib.so","base":"0x74d5934000","size":303104,"path":"/data/app/~~Nzn4SQ_RZn1-PYH7TbX7Ig==/com.pocket.snh48.activity-Muxx7c_dtplxjFPY2SGF0A==/lib/arm64/libencryptlib.so"}
// base为基址
Process.getModuleByName
同 findModuleByName
Module.findBaseAddress()(常用)
直接获得模块基址
const baseAddr = Module.findBaseAddress('libencryptlib.so')
console.log(baseAddr)
// 0x74d5934000
Process.findModuleByAddress(address)
通过基址来找到模块
Process.getModuleByAddress(address)
同 findModuleByAddress
测试 hook 任意函数
const baseAddr = Module.findBaseAddress('libencryptlib.so')
// const so = 0x77ab999000;
// console.log(ptr(so).add(0x1FA38)); // ptr 是 new NativePointer()的简写
const funcAddr = baseAddr.add(0x1fa38) // 0x1FA38 是IDA中函数定义的地址
Interceptor.attach(funcAddr, {})
打印参数
function print_arg(addr) {
const module = Process.findRangeByAddress(addr)
// 判断传入的参数是否为地址
if (module !== null) return hexdump(addr) + '\n'
return ptr(addr) + '\n'
}
参数的方法
// args[0] 是一个内存地址
hexdump(args[0]) // 打印参数的所在内存区域的字节数据
args[0].readCString() // 读取参数所对应的C字符串 (前提: 参数是一个可见字符串)
args[0].readPointer() // 用读地址方式去读取参数所对应的值 (如果参数是一个指针的话可能就需要使用)
修改函数数值参数和返回值
修改数值
Interceptor.attach(helloAddr, {
onEnter: function (args) {
args[2] = ptr(1000) // 直接将第三个参数修改为1000
console.log(args[2].toInt32())
},
onLeave: function (retval) {
retval.replace(20000) // 通过replace 修改成20000
console.log('retval', retval.toInt32())
},
})
修改字符串
hex 与 string 转化封装函数(中文无法转化)
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 (let bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16))
return bytes
}
function hexToString(hexStr) {
let hex = hexStr.toString()
let str = ''
for (let i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16))
return str
}
将指向的字符串修改成新的字符串(新字符串不宜超过原有字符串长度)
Interceptor.attach(funcAddr, {
onEnter: function (args) {
let newStr = 'some strings'
// 需要写入字节数组的方式来写入字符串
args[1].writeByteArray(hexToBytes(stringToHex(newStr) + '00')) // c语言字符串结尾为0字节
console.log(hexdump(args[1]))
args[2] = ptr(newStr.length)
console.log(args[2].toInt32())
},
onLeave: function (retval) {},
})
有缺陷,如果字符串长度大于原字符串长度,有可能导致内存中其他区域被修改,导致不可预知的 BUG
将 so 层中已有的字符串传给函数(字符串地址替换)
Interceptor.attach(funcAddr, {
onEnter: function (args) {
args[1] = baseAddr.add(0x38a1) // 0x38a1 为IDA中所对应的字符串地址
console.log(hexdump(args[1]))
args[2] = ptr(baseAddr.add(0x38a1).readCString().length) // 读取字符串长度
console.log(args[2].toInt32())
},
onLeave: function (retval) {},
})
替换函数(建议使用)
cosnt newStr = "some strings";
cosnt newStrAddr = Memory.allocUtf8String(newStr); // 使用Frida的Memory来申请内存区域 返回的是一个指针
Interceptor.attach(funcAddr, {
onEnter: function (args) {
// cosnt newStrAddr = Memory.allocUtf8String(newStr); // 如果在这里申请的话,到onLeave将会回收 可以在全局定义或使用this.newStrAddr 附加到自身
args[1] = newStrAddr
console.log(hexdump(args[1]))
args[2] = ptr(newStr.length)
console.log(args[2].toInt32())
},
onLeave: function (retval) {},
})
内存读写
// 1. 读取指定地址的字符串
let baseAddr = Module.findBaseAddress('libxiaojianbang.so')
console.log(baseAddr.add(0x2c00).readCString())
// 2. dump指定地址的内存
console.log(hexdump(baseAddr.add(0x2c00)))
// 3. 读指定地址的内存
console.log(baseAddr.add(0x2c00).readByteArray(16))
console.log(Memory.readByteArray(baseAddr.add(0x2c00), 16)) //原先的API
// 4. 写指定地址的内存
baseAddr.add(0x2c00).writeByteArray(stringToBytes('xiaojianbang'))
console.log(hexdump(baseAddr.add(0x2c00)))
// 5. 申请新内存写入
Memory.alloc()
Memory.allocUtf8String()
// 6. 修改内存权限
Memory.protect(ptr(libso.base), libso.size, 'rwx')
修改 so 函数代码(需了解 ARM 汇编相关知识)
// 1. 修改地址对应的指令
let baseAddr = Module.findBaseAddress("libxiaojianbang.so");
baseAddr.add(0x1684).writeByteArray(hexToBytes("0001094B"));
ARM与Hex在线转换 https://armconverter.com/
// 2. 将对应地址的指令解析成汇编
let ins = Instruction.parse(baseAddr.add(0x1684));
console.log(ins.toString());
// 3. 利用frida提供的api来写汇编代码
new Arm64Writer(baseAddr.add(0x167C)).putNop();
console.log(Instruction.parse(baseAddr.add(0x167C)).toString());
// 4. 利用frida提供的api来写汇编代码
let codeAddr = baseAddr.add(0x167C);
Memory.patchCode(codeAddr, 8, function (code) {
let Writer = new Arm64Writer(code, {pc: codeAddr});
Writer.putBytes(hexToBytes("0001094B"));
Writer.putBytes(hexToBytes("FF830091"));
Writer.putRet();
Writer.flush();
});
主动调用任意函数
-
声明函数指针
文档:https://frida.re/docs/javascript-api/#NativeFunction 语法:
new NativeFunction(address, returnType, argTypes[, abi])
-
支持的 returnType 和 argTypes
void、pointer、int、uint、long、ulong、char、uchar、float、double int8、uint8、int16、uint16、int32、uint32、int64、uint64、bool size_t、ssize_t
-
代码示例
Java.perform(function () {
//拿到函数地址
let funcAddr = Module.findBaseAddress('libxiaojianbang.so').add(0x23f4)
//声明函数指针
let func = new NativeFunction(funcAddr, 'pointer', ['pointer', 'pointer'])
let env = Java.vm.tryGetEnv() // 获取JNIEnv
console.log('env: ', JSON.stringify(env))
// {"handle":"0xb400007911df2c10","vm":{"handle":"0xb400007921d5f710"}}
if (env != null) {
// 创建java字符串 (jstr是一个地址)
let jstr = env.newStringUtf('some strings')
let cstr = func(env, jstr)
console.log(cstr.readCString())
console.log(hexdump(cstr))
}
})
hook libc.so 读写文件
// 找到C中操作文件的api
let fopenAddr = Module.findExportByName('libc.so', 'fopen')
let fputsAddr = Module.findExportByName('libc.so', 'fputs')
let fcloseAddr = Module.findExportByName('libc.so', 'fclose')
console.log(fopenAddr, fputsAddr, fcloseAddr)
let fopen = new NativeFunction(fopenAddr, 'pointer', ['pointer', 'pointer'])
let fputs = new NativeFunction(fputsAddr, 'int', ['pointer', 'pointer'])
let fclose = new NativeFunction(fcloseAddr, 'int', ['pointer'])
// 需要申请内存地址 (由于需要传入指针)
let fileName = Memory.allocUtf8String(
'/data/data/com.xiaojianbang.app/xiaojianbang.txt',
)
let openMode = Memory.allocUtf8String('w')
let data = Memory.allocUtf8String('QQ24358757\n')
let file = fopen(fileName, openMode)
console.log(file)
fputs(data, file)
fclose(file)
hook jni 函数
libart.so 存放 jni 函数
jni 文档可在 jni.h 头文件中查看
安卓 10 以下 /system/lib
或 /system/lib64
安卓 10 以后 /system/apex/com.android.runtime.release/lib64/libart.so
例如 hook env->NewStringUTF()方法
// 找到 env->NewStringUTF(a1, str) 函数
function findNewStringUtfAddr() {
let artSym = Module.enumerateSymbols('libart.so')
for (const sym of artSym) {
if (!sym.name.includes('CheckJNI') && sym.name.includes('NewStringUTF')) {
// console.log(JSON.stringify(sym));
return sym.address
}
}
return null
}
function hookNewStringUTF() {
const NewStringUTFAddr = findNewStringUtfAddr()
// console.log('NewStringUTFAddr', NewStringUTFAddr);
if (NewStringUTFAddr !== null) {
Interceptor.attach(NewStringUTFAddr, {
onEnter: function (args) {
console.log(args[1].readCString())
},
onLeave: function (retval) {},
})
}
}
hookNewStringUTF()
计算地址方 式(了解)
Java.perform(function () {
console.log('Process.arch: ', Process.arch)
let envAddr = ptr(Java.vm.tryGetEnv().handle).readPointer()
// 获取到的是JNINativeInterface 结构体
// 0x538 是结构体偏移的指针 需要计算
let newStringUtfAddr = envAddr.add(0x538).readPointer()
console.log('newStringUtfAddr', newStringUtfAddr)
if (newStringUtfAddr != null) {
Interceptor.attach(newStringUtfAddr, {
onEnter: function (args) {
console.log(args[1].readCString())
},
onLeave: function (retval) {},
})
}
})
主动调用 JNI 函数
使用 frida 封装的函数来调用 jni
let funcAddr = Module.findExportByName('libxiaojianbang.so', 'helloFromC')
console.log(funcAddr)
if (funcAddr != null) {
Interceptor.attach(funcAddr, {
onEnter: function (args) {},
onLeave: function (retval) {
let env = Java.vm.tryGetEnv()
let jstr = env.newStringUtf('bbs.125.la') //主动调用jni函数 cstr转jstr
retval.replace(jstr)
let cstr = env.getStringUtfChars(jstr) //主动调用 jstr转cstr
console.log(cstr.readCString())
console.log(hexdump(cstr))
},
})
}
NativeFunction 方式主动调用
let symbols = Process.getModuleByName('libart.so').enumerateSymbols()
let newStringUtf = null
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i]
if (
symbol.name.indexOf('CheckJNI') == -1 &&
symbol.name.indexOf('NewStringUTF') != -1
) {
console.log(symbol.name, symbol.address)
newStringUtf = symbol.address
}
}
let newStringUtf_func = new NativeFunction(newStringUtf, 'pointer', [
'pointer',
'pointer',
])
let jstring = newStringUtf_func(
Java.vm.tryGetEnv().handle,
Memory.allocUtf8String('xiaojianbang'),
)
console.log(jstring)
let envAddr = Java.vm.tryGetEnv().handle.readPointer()
let GetStringUTFChars = envAddr.add(0x548).readPointer()
let GetStringUTFChars_func = new NativeFunction(GetStringUTFChars, 'pointer', [
'pointer',
'pointer',
'pointer',
])
let cstr = GetStringUTFChars_func(Java.vm.tryGetEnv().handle, jstring, ptr(0))
console.log(cstr.readCString())