1 背景 Frida是一款跨系统、跨平台的开源进程注入框架,官方网站是https://www.frida.re 。Frida支持Windows、GNU/Linux、macOS、IOS、Android等平台的进程注入,功能强大。使用Frida提供的Shell环境,可以查看目标进程内存中的任何内容、调用进程中加载的函数、注入一个外部的DEX文件或so动态库等。
2 Frida的安装与配置 2.1 计算机端安装 安装Frida分为计算机端安装和Android设备端安装两部分。计算机端基本配置信息为Windows 10、Python 3.8.7,用pip安装Frida的命令为pip install frida-tools ,如图1所示。
2.2 Android设备端安装 在Android设备端安装Frida的步骤也很简单,只需将frida-server复制到设备中。frida-server的可执行文件可以在https://github.com/frida/frida/releases 页面找到。
比如,针对Aarch64架构的frida-server最新版本的文件名为frida-server-15.2.2-android-arm64.xz ,要想知道Android设备CPU的架构类型,可以通过命令adb shell getprop ro.product.cpu.abi 查看。
如图2所示,我的Android Studio模拟器CPU架构类型是armeabi-v7a ,因此,我需要下载的frida-server文件名是frida-server-15.2.2-android-arm.xz ,解压后并push到/data/local/tmp目录下,然后赋予可执行权限,如图3所示。
【注】此处我使用AndroidStudio模拟器进行操作,因为可以创建CPU架构与真机一致的armeabi-v7a,其他模拟器都没有这种效果,虽然慢的要命┭┮﹏┭┮,但多等一会也能运行起来。
至此,Frida的安装过程就结束了,在计算机端执行命令frida-ls-devices ,可查看当前连接到计算机的设备,如图4所示。
在设备上启动frida-server ,然后另开一个终端执行命令frida-ps -U ,可查看设备上的进程信息,如图5&6所示。
3 Java层执行注入与Hook 下面主要从以下几个方面来介绍:
如何修改Java层的函数参数和返回值
如何拦截Native层的函数参数和返回值
3.1 Crackme0201 用Frida可向运行在Android设备上的进程注入一小段代码或一个程序。以Crackme0201为例,首先在设备上启动目标程序(如 Crackme0201)(注意:一定要先打开目标程序,再下一步操作),然后运行frida-server 。此时可用frida-ps -U 命令查看其进程信息,如图6所示。
执行命令frida -U –no-pause -f com.ihui.crackme0201 ,用Frida对com.ihui.crackme0201进程进行注入,如图7所示。
若注入成功,能看到图7箭头处显示的Shell环境。在其中可执行Frida API提供的方法,查看进程的详细信息。按Tab会以菜单的方式显示当前执行环境支持的方法和命令。图8是执行Process中的方法,看到的Process的一些信息。
执行Module中的一些方法,可查看模块信息。如libc.so的导入函数表和导出函数,及libc.so的内存区域中哪些内存区是可执行的:
Module.findBaseAddress(“libc.so”) Module.enumerateImportsSync(“libc.so”) Module.enumerateExportsSync(“libc.so”) Module.enumerateRangesSync(“libc.so”,”–x”)
现在的目标不仅是查看进程空间的内容,还要修改checkSN(),让其在传入任意参数时返回true,要执行此操作,需要使用与Java相关的方法。先执行命令Java.available ,判断当前进程是否支持执行Java方法,如图9所示。
输出为true,表示可执行Java.perform() 、Java.use() 等方法。为修改checkSN()的实现,要先用Java.use() 获取MainActivity类,再修改其checkSN的implementation 属性。整段JavaScript代码如下:
1 2 3 4 5 6 7 8 Java.perform(function() { var clz = Java.use("com.ihui.crackme0201.MainActivity"); clz.checkSN.implementation = function(s1, s2) { console.log("[*] checkSN() called"); return true; }; console.log("[*] checkSN handler modified"); })
【注意】在Frida的Shell环境里,无法一次性复制粘贴所有内容,所有代码要在一行中输入。故上述js代码要按图10进行输入。
但这样一行输入效率太低,Frida提供外部输入功能:先将要注入的代码写好,启动Frida程序时传入,重新整理上述代码
1 2 3 4 5 6 7 8 9 10 11 12 13 setImmediate(function() { console.log("[*] Starting script"); Java.perform(function() { var clz = Java.use("com.ihui.crackme0201.MainActivity"); clz.checkSN.implementation = function(s1, s2) { console.log("[*] checkSN() called"); console.log("[*] s1: " + s1); console.log("[*] s2: " + s2); return true; }; console.log("[*] checkSN handler modified"); }); });
【注意】变量clz一定要声明,原书未声明,导致报错,但奇怪的是前面的命令中clz未声明,却执行成功。
上述代码同样修改了checkSN()的返回值,还输出了两个参数,在Frida的Shell中按Ctrl+D 退出,同样提示注册成功,而Frida的Shell中输出了在目标程序中输入的两个参数,如图11所示。
除了JavaScript,Frida还支持多种语言的API绑定,如Python。可在Python的交互环境中执行import frida 命令,然后用Frida提供的所有Python API驱动Frida工作(注意:若出现frida.ServerNotRunningError: unable to connect to remote frida-server 错误,可尝试端口转发adb forward tcp:27042 tcp:27042 )。
如图12所示,可以看到,破解效果与前面介绍的相同。自动化脚本crackme0201hooker.py如下所示:
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 import frida import codecs import sys jscode= """ setImmediate(function() { console.log("[*] Starting script"); Java.perform(function() { var clz = Java.use("com.ihui.crackme0201.MainActivity"); clz.checkSN.implementation = function(s1, s2) { console.log("[*] checkSN() called"); console.log("[*] s1: " + s1); console.log("[*] s2: " + s2); return true; }; console.log("[*] onClick handler modified"); }); }); """ device = frida.get_usb_device() session = device.attach("Crackme0201") script = session.create_script(jscode) script.load() sys.stdin.read() session.detach()
【注意】这里需要对原书中的脚本稍加修改,不然会报错。
变量clz一定要声明,原书未声明,导致报错;
把含有print语句的一行代码删除;
session = device.attach(“com.droider.crackme0201”)修改为session = device.attach(“Crackme0201”)
3.2 FridaApp 使用Jadx打开FridaApp.apk文件后,进行常规分析,发现MainActivity中有5个按钮,点击后分别对应相应的事件响应。
我们先在AS模拟器上打开APP,不然会报错提示找不到这个程序进程。
3.2.1 Hook类的构造方法 这里Hook类Money的构造方法 ,在Hook之前点击HOOK构造方法 的按钮后,会显示RMB: 100 的字符串,Hook之后,会显示注入的美元: 10000 的字符串,如图13所示。
构造方法的固定写法是$init ,具体的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 # HookConstructionMethod.py import frida, sys jscode = """ Java.perform(function () { var money = Java.use('com.qianyu.fridaapp.Money'); money.$init.implementation = function (a, b) { console.log("Hook Start..."); send(arguments[0]); send(arguments[1]); send("Success!"); return this.$init(10000, "美元"); } }); """ def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_remote_device().attach('FridaApp') script= process.create_script(jscode) script.on("message", message) script.load() sys.stdin.read()
3.2.2 Hook类的普通方法 普通方法包括了静态方法、私有方法、公开方法等,这个操作和上面的构造方法其实很类似。这里Hook类Utils的普通方法 ,在Hook之前点击HOOK普通方法 的按钮后,会显示7000 的字符串,Hook之后,会显示注入的300 的字符串,如图14所示。
具体的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 # HookGeneralMethod.py import frida, sys jscode = """ Java.perform(function () { var utils = Java.use('com.qianyu.fridaapp.Utils'); utils.getCalc.implementation = function (a, b) { console.log("Hook Start..."); send(arguments[0]); send(arguments[1]); send("Success!"); console.log(this.getCalc(arguments[0], arguments[1])); return this.getCalc(100, 200); } }); """ def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_remote_device().attach('FridaApp') script= process.create_script(jscode) script.on("message", message) script.load() sys.stdin.read()
3.2.3 Hook类的重载方法 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。这里Hook类Utils的重载方法test(int) ,在Hook之前点击HOOK重载方法 的按钮后,会显示迁羽老师:31928 的字符串,Hook之后,会显示注入的qianyu 的字符串,如图15所示。
具体的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 # HookOverloadingMethod.py import frida, sys jscode = """ Java.perform(function () { var utils = Java.use('com.qianyu.fridaapp.Utils'); utils.test.overload("int").implementation = function (a) { console.log("Hook Start..."); send(arguments[0]); send("Success!"); return "qianyu"; } }); """ def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_remote_device().attach('FridaApp') script= process.create_script(jscode) script.on("message", message) script.load() sys.stdin.read()
3.2.4 Hook构造对象参数 这里Hook类Utils和Money的对象参数 ,在Hook之前点击HOOK构造对象参数 的按钮后,会显示迁羽老师QQ:3192850648 的字符串,Hook之后,会显示注入的迁羽老师: 800 的字符串,如图16所示。
具体的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 # HookConstructionObjectParameters.py import frida, sys jscode = """ Java.perform(function () { var utils = Java.use('com.qianyu.fridaapp.Utils'); var money = Java.use('com.qianyu.fridaapp.Money'); utils.test.overload().implementation = function () { send("Hook Start..."); var mon = money.$new(2000,'港币'); send(mon.getInfo()); return this.test(800); } }); """ def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_remote_device().attach('FridaApp') script= process.create_script(jscode) script.on("message", message) script.load() sys.stdin.read()
【注】构造一个对象其实很简单直接固定写法$new 即可,然后有了对象也可以直接调用其对应的方法即可。
3.2.5 Hook构造对象属性 这里Hook类Utils、Money和Class的对象属性 ,在Hook之前点击HOOK构造对象属性 的按钮后,会显示迁羽老师QQ:3192850648 的字符串,Hook之后,会显示注入的迁羽老师: 10010 的字符串,如图17所示。
修改对象的字段值需要用反射 去进行操作,具体的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 # HookConstructionObjectProperties.py import frida, sys jscode = """ Java.perform(function () { var utils = Java.use('com.qianyu.fridaapp.Utils'); var money = Java.use('com.qianyu.fridaapp.Money'); var clazz = Java.use('java.lang.Class'); utils.test.overload().implementation = function () { send("Hook Start..."); var mon = money.$new(200,'港币'); send(mon.getInfo()); var numid= Java.cast(mon.getClass(),clazz).getDeclaredField('num'); numid.setAccessible(true); send(numid.get(mon)); numid.setInt(mon, 1000); send(mon.getInfo()); return this.test(10010); } }); """ def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_remote_device().attach('FridaApp') script= process.create_script(jscode) script.on("message", message) script.load() sys.stdin.read()
到这里我们就把所有可能遇到的情形Java层hook操作都介绍完了,主要包括以下几种常见情形:
Hook类的构造方法和普通方法,注意构造方法是固定写法$init 即可,获取参数可以通过自定义参数名也可以直接用系统隐含的arguments变量获取即可。
修改方法的参数和返回值,直接调用原始方法传入需要修改的参数值和直接修改返回值即可。
构造对象使用固定写法$new 即可。
如果需要修改对象的字段值需要用反射 去进行操作。
堆栈信息打印直接调用Java的Exception类即可,通过adb logcat -s AndroidRuntime 来过滤日志信息查看崩溃堆栈信息。
3.3 CTF_100 安装并打开CTF_100.apk之后,发现题目为爬到指定的楼层然后才能显示FLAG,如图18所示。
使用Jadx反编译APK查看运行逻辑,MainActivity中的has_gone_int表示已经爬的楼数,to_reach_int表示需要爬的楼数,get_flag(int i)表示最终返回FLAG的方法在native层实现,因此我们就可以通过修改has_gone_int和to_reach_int的值满足条件,然后调用get_flag(int i)返回FLAG信息,具体的代码如下所示。
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 import frida, sys jscode = """ Java.perform(function () { var MainActivity = Java.use('com.ctf.test.ctf_100.MainActivity'); MainActivity.Btn_up_onclick.implementation = function (a) { send("Hook Start..."); this.has_gone_int = 1 this.to_reach_int = 1 var flag = this.get_flag(1) send(flag); send("Hook End..."); } }); """ def message(message, data): if message["type"] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_remote_device().attach('CTF_100') script= process.create_script(jscode) script.on("message", message) script.load() sys.stdin.read()
最后,frida注入成功,获得了flag,如图19所示。
4 Native层执行注入与Hook 4.1 系统电话 Frida除了基于Shell的注入方式实现Hook,还提供frida-trace 命令实现跟踪式Hook。如图20所示,执行命令frida-trace -U -i open com.android.phone ,Hook本地手机的系统电话的open() 函数,执行成功后,会在终端输出系统电话程序调用open()函数的日志信息。
可看到在handlers /libc.so 目录下生成了open.js 。按Ctrl+C 结束Frida运行,js文件如下面所示。
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 /* * Auto-generated by Frida. Please modify to match the signature of open. * 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 open. * * @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) { log('open()'); }, /** * Called synchronously when about to return from open. * * 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) { } }
将参数信息添加到输出的信息中,主要就是修改onEnter()函数,将open.js文件修改如下:
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 /* * Auto-generated by Frida. Please modify to match the signature of open. * 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 open. * * @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: function (log, args, state) { log("open(" + "pathname=" + Memory.readUtf8String(args[0])+ ", flags=" + args[1] + ")"); }, /** * Called synchronously when about to return from open. * * 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) { } }
再次执行命令frida-trace -U -i open com.android.phone ,即可看到参数信息,如图21所示。
4.2 jisuanqi1 首先,在AS创建的arm架构的模拟器中运行jisuanqi1.apk,如图22所示。
使用Jadx分析APK文件后,发现计算器实现的加减乘除都是在native层实现,进而使用IDA打开APK文件解压后的libjisuanqi.so文件,JNI_OnLoad中有Java层函数与C层函数的对应关系,另外在Exports中找到了加减乘除的相关实现,如图23所示。
由图23可知,addc()函数的地址为0x00000D0C,因此需要在对应的代码位置处修改,如下所示:
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 # Hooklibjisuanqi.py import frida, sys jscode = """ setImmediate(function () { send("start"); //遍历模块找基址 Process.enumerateModules({ onMatch: function (exp) { if (exp.name == "libjisuanqi.so") { send('enumerateModules find'); send(exp.name + "|" + exp.base + "|" + exp.size + "|" + exp.path); send(exp); return 'stop'; } }, onComplete: function () { send('enumerateModules stop'); } }); //hook导出函数 var exports = Module.enumerateExportsSync("libjisuanqi.so"); for(var i=0;i<exports.length;i++){ send("name:"+exports[i].name+" address:"+exports[i].address); } //通过模块名直接查找基址 var baseSOFile = Module.findBaseAddress("libjisuanqi.so"); Interceptor.attach(baseSOFile.add(0x00000D0C),{ onEnter: function(args) { console.log(args[2]); console.log(args[3]); }, onLeave: function(retval){ retval.replace(10) console.log(retval); } }); }); """ def on_message(message, data): if message['type'] == 'send': print("[*] {0}".format(message['payload'])) else: print(message) process = frida.get_usb_device().attach('jisuanqi1') script = process.create_script(jscode) script.on('message', on_message) script.load() sys.stdin.read()
最后,打印输入的两个数100和20的IEEE 754存储形式0x42c80000和0x41a00000,如图24所示。
Native层代码Hook操作总结如下:
5 总结 本文主要介绍了如何在Android系统中注入与Hook操作,并讲解了这些技术的原理,演示了如何通过Hook与注入JavaScript和Python代码的方法改变Crackme0201程序的行为,也演示了跟踪系统电话程序的Native方法来实现跟踪式Hook。
接下来,准备向第十一章《软件保护技术》进发。
6 参考文献 [1]丰生强. Android软件安全权威指南[M]. 电子工业出版社, 2019. [2]https://blog.csdn.net/freeking101/article/details/106965168
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 xingshuaikun@163.com。