0x00 前言
一直都知道Frida是一个非常强大的Hook框架,而且其Hook对象不仅限于安卓,连Windows应用的Hook他都可以搞定
然而受限于对JavaScript的不熟悉,一直拖着没有系统学习
借着《吾爱破解安卓逆向入门教程》提供的模板和介绍、对JS语言的相关语法的查阅以及利用其它框架的经验
还是系统地学学Frida比较好
参考:
《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上)
《安卓逆向这档事》十四、是时候学习一下Frida一把梭了(中)
《安卓逆向这档事》十五、是时候学习一下Frida一把梭了(下)
《安卓逆向这档事》十六、是时候学习一下Frida一把梭了(终)
0x01 Frida基础知识
Frida是一款开源的动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,支持Windows、Mac、Linux、Android或者iOS,从安卓层面来讲,可以实现Java
层和Native
层Hook
操作。
Frida的安装非常简单,不指定版本的话用pip就行(我的版本为16.4.3)
之后再把Frida-server(需要与Frida版本对应)给push手机上(别用模拟器)
1
| adb push frida-s /data/local/tmp/
|
之后启动手机上的Frida-server就好,这边提供两个脚本
一个快捷打开frida-sever
bat文件
1 2 3
| @REM runfs.bat文件内容 adb shell < runfs.sh pause
|
对应的sh文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| su cd /data/local/tmp
pid=`ps -A | grep "frida-s" | grep -v grep | awk '{print $2}'`
if [ -z "$pid" ] then echo "not running, starting" ./frida-s & ps -A | grep "frida-s" | grep -v grep echo "started!" else echo "already runing!" ps -A | grep "frida-s" | grep -v grep fi
|
一个快捷杀死Frida-Server进程
1 2 3
| @REM stopfs.bat文件内容 adb shell < stopfs.sh pause
|
对应的sh文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| su cd /data/local/tmp
pid=`ps -A | grep "frida-s" | grep -v grep | awk '{print $2}'`
if [ -z "$pid" ] then echo "not running, no need to do anything" else kill -9 $pid echo "kill success!" fi
|
可以通过
测试是否打开是否成功
之后就可以愉快使用Frida了
0x02 Frida操作模式
操作模式 |
描述 |
优点 |
主要用途 |
CLI(命令行)模式 |
通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 |
便于直接注入和操作 |
在较小规模的操作或者需求比较简单的场景中使用 |
RPC模式 |
使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 |
在对复杂数据的处理上可以通过RPC传输给Python脚本来进行,有利于减少被注入进程的性能损耗 |
在大规模调用中更加普遍,特别是对于复杂数据处理的需求 |
0x03 Frida注入模式与启动命令
注入模式 |
描述 |
命令或参数 |
优点 |
主要用途 |
Spawn模式 |
将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App |
在CLI模式中,Frida通过加上 -f 参数指定包名以spawn模式操作App |
适合于需要在App启动时即进行注入的场景,可以在App启动时即捕获其行为 |
当需要监控App从启动开始的所有行为时使用 |
Attach模式 |
在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作 |
在CLI模式中,如果不添加 -f 参数,则默认会通过attach模式注入App |
适合于已经运行的App,不会重新启动App,对用户体验影响较小 |
在App已经启动,或者我们只关心特定时刻或特定功能的行为时使用 |
Spawn模式
1
| frida -U -f 进程名 -l hook.js
|
Attach模式
0x04 Frida基础语法
API名称 |
描述 |
Java.use(className) |
获取指定的Java类并使其在JavaScript代码中可用。 |
Java.perform(callback) |
确保回调函数在Java的主线程上执行。 |
Java.choose(className, callbacks) |
枚举指定类的所有实例。 |
Java.cast(obj, cls) |
将一个Java对象转换成另一个Java类的实例。 |
Java.enumerateLoadedClasses(callbacks) |
枚举进程中已经加载的所有Java类。 |
Java.enumerateClassLoaders(callbacks) |
枚举进程中存在的所有Java类加载器。 |
Java.enumerateMethods(targetClassMethod) |
枚举指定类的所有方法。 |
0x05 日志输出方法
日志方法 |
描述 |
区别 |
console.log() |
使用JavaScript直接进行日志打印 |
多用于在CLI模式中,console.log() 直接输出到命令行界面,使用户可以实时查看。在RPC模式中,console.log() 同样输出在命令行,但可能被Python脚本的输出内容掩盖。 |
send() |
Frida的专有方法,用于发送数据或日志到外部Python脚本 |
多用于RPC模式中,它允许JavaScript脚本发送数据到Python脚本,Python脚本可以进一步处理或记录这些数据。 |
0x06 Hook模板
1 2 3 4 5 6
| function main(){ Java.perform(function(){ hook(); }); } setImmediate(main);
|
0x07 Frida在Java层
1.Hook普通方法、打印参数和修改返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function hook(){ var utils = Java.use("包名.类名"); utils.method.implementation = function(a, b){ a = 123; b = 456; var retval = this.method(a, b); console.log(a, b, retval); return retval; } }
|
2.Hook重载函数
1 2 3 4 5 6 7 8 9 10 11 12 13
|
function hook(){ var utils = Java.use("包名.类名"); utils.Inner.overload('参数1','参数2').implementation = function(a,b){ b = "aaaaaaaaaa"; this.Inner(a,b); console.log(b); } }
|
3.Hook构造函数
1 2 3 4 5 6 7 8 9 10
| function hook(){ var utils = Java.use("包名.类名"); utils.$init.overload('参数').implementation = function(str){ console.log(str); str = "52"; this.$init(str); } }
|
4.Hook字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function hook(){ Java.perform(function(){ var utils = Java.use("包名.类名"); utils.staticField.value = "我是被修改的静态变量"; console.log(utils.staticField.value); Java.choose("包名.类名", { onMatch: function(obj){ obj._privateInt.value = 9999; obj.privateInt.value = 9999; }, onComplete: function(){
} }); }); }
|
5.Hook内部类
1 2 3 4 5 6 7 8 9 10
| function hook(){ Java.perform(function(){ var innerClass = Java.use("包名.类名$内部类名"); console.log(innerClass); innerClass.$init.implementation = function(){ console.log("eeeeeeee"); } }); }
|
6.枚举所有的类与类的所有方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function hook(){ Java.perform(function(){ Java.enumerateLoadedClasses({ onMatch: function(name,handle){ if(name.indexOf("包名.类名") !=-1){ console.log(name); var clazz =Java.use(name); console.log(clazz); var methods = clazz.class.getDeclaredMethods(); console.log(methods); } }, onComplete: function(){} }) }) }
|
7.枚举所有方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function hook(){ Java.perform(function(){ var Demo = Java.use("包名.类名"); var methods =Demo.class.getDeclaredMethods(); for(var j=0; j < methods.length; j++){ var methodName = methods[j].getName(); console.log(methodName); for(var k=0; k<Demo[methodName].overloads.length;k++){ Demo[methodName].overloads[k].implementation = function(){ for(var i=0;i<arguments.length;i++){ console.log(arguments[i]); } return this[methodName].apply(this,arguments); } } } }) }
|
8.主动调用
静态方法
1 2 3 4 5 6
| function hook(){ Java.perform(function(){ var ClassName = Java.use("包名.类名"); var ret = ClassName.方法名("参数"); }) }
|
非静态方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| function hook(){ Java.perform(function () { var ret = null; Java.choose("包名.类名",{ onMatch:function(instance){ ret=instance.方法名("参数"); }, onComplete:function(){ console.log("result: " + ret); } }); }) }
|
0x08 Frida在Native层
在Native层
对于静态注册的函数,可以通过在导出表中搜索关键字java
来找到(完整的是Java_包名_类名_方法名)
而动态注册的,则需要结合jadx找到函数名然后直接搜索函数名
需要注意的是,静态注册的函数既可以通过Java
层hook也可以通过Native
层hook,但是动态注册的只能在Native
层hook
在IDA分析时,由于Native层的特性,函数的第一个参数一般为JNIEnv *env
,分析时改改类型可能可以直接识别出一些函数。
其他类型参考jadx传值和下表对照进行修改
C++ 数据类型 |
Java 数据类型 |
JNI 数据类型签名 |
jint |
int |
“I” |
jboolean |
boolean |
“Z” |
jbyte |
byte |
“B” |
jchar |
char |
“C” |
jshort |
short |
“S” |
jlong |
long |
“J” |
jfloat |
float |
“F” |
jdouble |
double |
“D” |
jobject |
Object |
“Ljava/lang/Object;” |
jstring |
String |
“Ljava/lang/String;” |
jarray |
Array |
“[elementType” |
jobjectArray |
Object[] |
“[Ljava/lang/Object;” |
jbooleanArray |
boolean[] |
“[Z” |
jbyteArray |
byte[] |
“[B” |
jcharArray |
char[] |
“[C” |
jshortArray |
short[] |
“[S” |
jintArray |
int[] |
“[I” |
jlongArray |
long[] |
“[J” |
jfloatArray |
float[] |
“[F” |
jdoubleArray |
double[] |
“[D” |
1.Process、Module和Memory
Process
对象代表当前被Hook的进程,能获取进程的信息,枚举模块,枚举范围等
API |
含义 |
Process.id |
返回附加目标进程的 PID |
Process.isDebuggerAttached() |
检测当前是否对目标程序已经附加 |
Process.enumerateModules() |
枚举当前加载的模块,返回模块对象的数组 |
Process.enumerateThreads() |
枚举当前所有的线程,返回包含 id , state , context 等属性的对象数组 |
Module
对象代表一个加载到进程的模块(例如,在 Windows 上的 DLL,或在 Linux/Android 上的 .so 文件),能查询模块的信息,如模块的基址、名称、导入/导出的函数等
API |
含义 |
Module.load() |
加载指定so文件,返回一个Module对象 |
enumerateImports() |
枚举所有Import库函数,返回Module数组对象 |
enumerateExports() |
枚举所有Export库函数,返回Module数组对象 |
enumerateSymbols() |
枚举所有Symbol库函数,返回Module数组对象 |
Module.findExportByName(exportName)、Module.getExportByName(exportName) |
寻找指定so中export库中的函数地址 |
Module.findBaseAddress(name)、Module.getBaseAddress(name) |
返回so的基地址 |
Memory
是一个工具对象,提供直接读取和修改进程内存的功能,能够读取特定地址的值、写入数据、分配内存等
方法 |
功能 |
Memory.copy() |
复制内存 |
Memory.scan() |
搜索内存中特定模式的数据 |
Memory.scanSync() |
同上,但返回多个匹配的数据 |
Memory.alloc() |
在目标进程的堆上申请指定大小的内存,返回一个NativePointer |
Memory.writeByteArray() |
将字节数组写入一个指定内存 |
Memory.readByteArray |
读取内存 |
2.so基址的获取方式
1 2 3 4 5 6 7 8 9 10
| function hook(){ Java.perform(function(){ var moduleAddr1 = Process.findModuleByName("so库名").base; var moduleAddr2 = Process.getModuleByName("so库名").base; var moduleAddr3 = Module.findBaseAddress("so库名"); console.log(moduleAddr1) console.log(moduleAddr2) console.log(moduleAddr3) }) }
|
3.枚举导入导出表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function hook(){ Java.perform(function(){ var imports = Module.enumerateImports("so库名"); for(var i =0; i < imports.length;i++){ if(imports[i].name == "导入函数名"){ console.log(JSON.stringify(imports[i])); console.log(imports[i].address); } } var exports = Module.enumerateExports("so库名"); for(var i =0; i < exports.length;i++){ console.log(JSON.stringify(exports[i])); }
}) }
|
4.Native函数的基础Hook打印
(1)整数型、布尔值类型、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
| function hook(){ Java.perform(function(){ var helloAddr = Module.findExportByName("so库名","函数名(IDA中显示的函数)"); console.log(helloAddr); if(helloAddr != null){ Interceptor.attach(helloAddr,{ onEnter: function(args){ console.log(args[0]); console.log(this.context.x1); console.log(args[1].toInt32()); console.log(args[2].readCString()); console.log(hexdump(args[2])); }, onLeave: function(retval){ console.log(retval); console.log("retval",retval.toInt32()); retval.replace(1); } }) } }) }
|
(2)字符串类型
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
| function hook(){ Java.perform(function(){ var helloAddr = Module.findExportByName("so库名","函数名"); if(helloAddr != null){ Interceptor.attach(helloAddr,{ onEnter: function(args){ var jString = Java.cast(args[2], Java.use('java.lang.String')); console.log("参数:", jString.toString()); var JNIEnv = Java.vm.getEnv(); var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString(); console.log("参数:", originalStrPtr); }, onLeave: function(retval){ var returnedJString = Java.cast(retval, Java.use('java.lang.String')); console.log("返回值:", returnedJString.toString()); } }) } }) }
|
5.Native函数的基础Hook修改
(1)整数型修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function hook(){ Java.perform(function(){ var helloAddr = Module.findExportByName("so库名","函数名"); console.log(helloAddr); if(helloAddr != null){ Interceptor.attach(helloAddr,{ onEnter: function(args){ args[0] = ptr(1000); console.log(args[0]); }, onLeave: function(retval){ retval.replace(20000); console.log("retval",retval.toInt32()); } }) } }) }
|
(2)字符串类型修改
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(){ Java.perform(function(){ var helloAddr = Module.findExportByName("so库名","函数名"); if(helloAddr != null){ Interceptor.attach(helloAddr,{ onEnter: function(args){ var JNIEnv = Java.vm.getEnv(); var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString(); console.log("参数:", originalStrPtr); var modifiedContent = "至尊"; var newJString = JNIEnv.newStringUtf(modifiedContent); args[2] = newJString; }, onLeave: function(retval){ var returnedJString = Java.cast(retval, Java.use('java.lang.String')); console.log("返回值:", returnedJString.toString()); var JNIEnv = Java.vm.getEnv(); var modifiedContent = "无敌"; var newJString = JNIEnv.newStringUtf(modifiedContent); retval.replace(newJString); } }) } }) }
|
6.Hook未导出函数与函数地址计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function hook(){ Java.perform(function(){ var soAddr = Module.findBaseAddress("so库名"); console.log(soAddr); var funcaddr = soAddr.add(相对地址); console.log(funcaddr); if(funcaddr != null){ Interceptor.attach(funcaddr,{ onEnter: function(args){
}, onLeave: function(retval){ console.log(retval.toInt32()); } }) } }) }
|
函数地址计算
- 安卓里一般32 位的 so 中都是
thumb
指令,64 位的 so 中都是arm
指令
- 通过IDA里的opcode bytes来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)
- thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1
arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移
7.Hook_dlopen
so库的加载如下(图片借用自《安卓逆向这档事》十二、大佬帮我分析一下):
对应的函数如下
函数名 |
描述 |
android_dlopen_ext() 、dlopen() 、do_dlopen() |
这三个函数主要用于加载库文件。android_dlopen_ext 是系统的一个函数,用于在运行时动态加载共享库。与标准的 dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。 |
find_library() |
find_library() 函数用于查找库,基本的用途是给定一个库的名字,然后查找并返回这个库的路径。 |
call_constructors() |
call_constructors() 是用于调用动态加载库中的构造函数的函数。 |
init |
库的构造函数,用于初始化库中的静态变量或执行其他需要在库被加载时完成的任务。如果没有定义init 函数,系统将不会执行任何动作。需要注意的是,init 函数不应该有任何参数,并且也没有返回值。 |
init_array |
init_array 是ELF(Executable and Linkable Format,可执行和可链接格式)二进制格式中的一个特殊段(section),这个段包含了一些函数的指针,这些函数将在main() 函数执行前被调用,用于初始化静态局部变量和全局变量。 |
jni_onload |
这是Android JNI(Java Native Interface)中的一个函数。当一个native库被系统加载时,该函数会被自动调用。JNI_OnLoad 可以做一些初始化工作,例如注册你的native方法或者初始化一些数据结构。如果你的native库没有定义这个函数,那么JNI会使用默认的行为。JNI_OnLoad 的返回值应该是需要的JNI版本,一般返回JNI_VERSION_1_6 。 |
dlopen
和android_dlopen_ext
就是用于加载so库的,而且加载时机很早,方便进行其他操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function hook_dlopen() { var dlopen = Module.findExportByName(null, "dlopen"); Interceptor.attach(dlopen, { onEnter: function (args) { var so_name = args[0].readCString(); if (so_name.indexOf("so库名") >= 0) this.call_hook = true; }, onLeave: function (retval) { if (this.call_hook) hook(); } }); var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext"); Interceptor.attach(android_dlopen_ext, { onEnter: function (args) { var so_name = args[0].readCString(); if (so_name.indexOf("so库名") >= 0) this.call_hook = true; }, onLeave: function (retval) { if (this.call_hook) hook(); } }); }
|
注意一般配合frida的-f
参数使用,因为要在非常早的时候hook,因此交由frida来启动会比较准确
0x09 Frida写数据
1 2 3 4 5 6 7 8
| var file_path = "私有目录地址"; var file_handle = new File(file_path, "wb"); if (file_handle && file_handle != null) { file_handle.write(data); file_handle.flush(); file_handle.close(); }
|
0x0A Frida_inlineHook与读写汇编
Inline hook(内联钩子)是一种在程序运行时修改函数执行流程的技术。
它通过修改函数的原始代码,将目标函数的执行路径重定向到自定义的代码段,从而实现对目标函数的拦截和修改。
简单来说就是可以对任意地址的指令进行hook读写操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| function inline_hook() { var soAddr = Module.findBaseAddress("so库名"); if (soAddr) { var func_addr = soAddr.add(0x10428); Java.perform(function () { Interceptor.attach(func_addr, { onEnter: function (args) { console.log(this.context.x22); this.context.x22 = ptr(1); }, onLeave: function (retval) { } } ) }) } }
|
(1)地址解析成汇编
1 2 3
| var soAddr = Module.findBaseAddress("so库名"); var codeAddr = Instruction.parse(soAddr.add(相对地址)); console.log(codeAddr.toString());
|
(2)FridaAPI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| var soAddr = Module.findBaseAddress("so库名"); var codeAddr = soAddr.add(相对地址); Memory.patchCode(codeAddr, 4, function(code) { const writer = new Arm64Writer(code, { pc: codeAddr }); writer.putBytes(hexToBytes("20008052")); writer.flush(); }); function hexToBytes(str) { var pos = 0; var len = str.length; if (len % 2 != 0) { return null; } len /= 2; var hexA = new Array(); for (var i = 0; i < len; i++) { var s = str.substr(pos, 2); var v = parseInt(s, 16); hexA.push(v); pos += 2; } return hexA; }
|
0x0B 普通函数与jni函数的主动调用
官方文档规定了支持的类型以及调用约定
1 2 3 4 5 6 7 8 9
| var funcAddr = Module.findBaseAddress("so库名").add(相对地址); var funcAddr = Module.findExportByName("so库名", "函数名");
var aesAddr = new NativeFunction(funcAddr , 'pointer', ['pointer', 'pointer', 'pointer']); var encry_text = Memory.allocUtf8String("OOmGYpk6s0qPSXEPp4X31g=="); var key = Memory.allocUtf8String('wuaipojie0123456'); console.log(aesAddr(Java.vm.getEnv(), encry_text ,key).readCString());
|