From:Hook 神器家族的 Frida 工具使用详解:https://blog.csdn.net/FlyPigYe/article/details/90258758
详解 Hook 框架 frida ( 信抢红包 ):https://www.freebuf.com/company-information/180480.html
APP逆向神器之Frida【Android初级篇】:https://www.jianshu.com/p/2d755beb1c54
frida 官网文档:https://frida.re/docs/home/
《FRIDA操作手册》:https://python.ctolib.com/hookmaster-frida-all-in-one.html
FridaApp_Python的Hook脚本.zip:链接: https://pan.baidu.com/s/196-f9xggQ6QNNavmwiP8PA 提取码: vzuh
利用Frida绕过Android App(途牛apk)的SSL Pinning:https://blog.csdn.net/weixin_44677409/article/details/106650473
Frida从入门到入门—安卓逆向菜鸟的 frida 使用说明:https://bbs.pediy.com/thread-226846.htm
哔哩哔哩视频教程:
frida java 层 hook:https://www.bilibili.com/video/BV1UE411A7rW?p=78
frida native 层 hook:https://www.bilibili.com/video/BV1UE411A7rW?p=79
关键字:frida hook 或 fridaapp.apk 或 frida工具使用详解
Android 之 Frida 框架:https://mp.weixin.qq.com/s?__biz=MzU5Mjg5NjMyNA==&mid=2247484311&idx=1&sn=f18d19fd7cbf72c66beba9455caf24cc
雷电3版本模拟器上 frida-server 跑不起来。雷电4模拟器可以。https://blog.csdn.net/ugooo/article/details/112045478
但是 雷电4模拟器没法设置代理进行抓包。 https://www.ldmnq.com/forum/thread-67291-1-1.html
所以抓包可以用雷电3,运行frida-server 可以用 雷电4
一、前言
说到逆向APP,很多人首先想到的都是反编译,但是单看反编译出来的代码很难得知某个函数在被调用时所传入的参数和它返回的值,极大地增加了逆向时的复杂度,有没有什么办法可以方便地知道被传入的参数和返回值呢?答案是有的,这个方法就是Hook,Hook的原理简单地说就是用一个新的函数替代掉原来的函数,在这个新的函数中你想做什么都可以,为所欲为。
在逆向过程中有一个 Hook 神器是必不可少的工具,之前已经介绍了 Xposed 和 Substrate 了,不了解的可以看这两篇文章:Android中Hook神器Xposed工具介绍 和 Android中Hook神器SubstrateCydia工具介绍 这两篇文章非常重要,一个是 Hook Java层的时候最常用的 Xposed , 一个是Hook Native层常用的SubstrateCydia,可以看之前的文章比如写微信插件等都采用了Xposed工具,因为个人觉得Xposed用起来比较爽,写代码比较方便。而对于SubstrateCydia工具可以 Hook Native 层。
那么有了这两个神器为啥还要介绍 Frida工具呢?而且这个工具网上已经有介绍了,为什么还有介绍了,因为这个Frida工具对于逆向者操作破解来说非常方便,所谓方便是他的安装环境和配置要求都非常简单兼容性也非常好,因为最近在弄一个协议解密,无奈手机上安装 Cydia 之后不兼容导致死机所以就转向用了这个工具实现了 hook,所以觉得这个工具非常好用就单独介绍一下。
Frida 也是一个很常用的 Hook 工具,只需要编写一段 Javascript 代码就能轻松地对指定的函数进行 Hook,而且它基本上可以算是全平台的(主流平台全覆盖),除了 Android 以外,iOS 和 PC 端的APP也可以用它来进行Hook,非常方便。
1、Hook是个什么鬼?
Hook 翻译过来就是 "钩子" 的意思,钩子实际上是一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,亦即钩子函数先得到控制权。这时钩子函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。Hook 技术无论对安全软件还是恶意软件都是十分关键的一项技术,其本质就是 劫持函数调用 。
2、Frida框架的那些事
frida 是一款基于 python 和 java 的 hook 框架,是一种动态插桩工具,可以插入代码到原生App的内存空间中,动态的监视和修改其行行为,可运行在Android、iOS、Linux和windows等多个平台。
插桩技术 是指将额外的代码注入程序中以收集运行时的信息,可分为两种:
- 1. 源代码插桩【Source Code Instrumentation(SCI)】:额外代码注入到程序源代码中。
- 2. 二进制插桩【Binary Instrumentation】:额外代码注入到二进制可执行文件中,其又可分为两种:
● 静态二进制插桩 【 Static Binary Instrumentation(SBI) 】:
在程序执行前插入额外的代码和数据,生成一个永久改变的可执行文件。
● 动态二进制插桩 【 Dynamic Binary Instrumentation(DBI) 】:
在程序运行时实时地插入额外代码和数据,对可执行文件没有任何永久改变。
使用 Frida 框架到底能做什么呢 ?
- (1)访问进程的内存,提取我们感兴趣的信息或敏感信息
- (2)在应用程序运行时覆盖一些功能,改变其程序运行逻辑
- (3)从导入的类中调用函数
- (4)在堆上查找对象实例并使用这些对象实例
- (5)Hook,动态跟踪、拦截变量和函数 等等。
二、环境安装配置
如何让 Frida 奔跑起来 ?
用到的 frida 框架分为两部分:
- 一部分是 "客户端", 即:用于连接远程设备,提交要注入的 JS 代码到服务端,并接受服务端发来的消息;
- 另一部分是 "服务器端", 即:注入JS代码到目标进程,操作内存数据,并将相关信息发送至给客户端。
官网安装说明
frida
环境要求
系统环境 – Windows、macOS、Linux
Python – 最新的3.x版本,
Adb环境 – 请自行下载adb工具或安装 android studio 工具
环境准备好了,让我们来安装 frida CLI 吧!
安装 frida CLI 有很多种方法,这里只介绍两种,即:pip 和 npm
pip 安装 frida
执行 pip install frida
执行 pip install frida-tools
执行 frida -version
npm 安装 frida,首先安装 NodeJS
执行 npm install frida
执行 npm install frida-tools
执行 frida --version
执行一波 pip install frida-tools
,安装完毕以后,测试是否安装正确,因为官网安装文档的下半部分是用于测试刚装好的库是否可用,但是比较麻烦,这里可以直接使用 frida-ps
命令来测试(显示本机的进程)
如果加上参数 U 表示的是 usb 连接的设备。( 模拟器也是 -U 参数)
frida-ps -aU
看起来是没问题了,然后我们怎么Hook Android手机上的 APP 呢?别急,还需要在手机上做一些操作你才能这么做。
我们需要有一台已经Root了的Android手机,因为不同型号的手机Root方法存在差异,本文中就不指导如何对手机进行Root操作了,请自行通过搜索引擎查找方法。实在没有可以Root的Android手机的话可以选择使用模拟器,推荐使用Genymotion之类系统较为原生的模拟器,并将Android版本选择在6.0以上,否则可能会出现一些奇奇怪怪的问题。
手机准备好了之后,找到 Frida 文档中 Tutorials 栏里的Android页,开始进行Frida的手机端准备工作。
文档中能看到,Frida官方最近的大部分测试都是在运行着Android 9的Pixel 3上进行的,所以理论上来讲如果你在使用中遇到任何奇怪的问题,都可能是你手机的系统导致,所以这里再次建议,使用较为原生和偏高版本的系统(建议至少6.0)进行操作。
Frida 环境
1. 手机运行服务端
2. 电脑端运行客户端,电脑端再进行端口转发adb forward tcp:27042 tcp:27042adb forward tcp:27043 tcp:27043
frida-server
环境要求
手机一部,本文主要讲述 Android系统。手机必须被 Root,具体 Root 方法请自行查询
下载 frida-server ,地址:https://github.com/frida/frida/releases
安装方法( 先要看构架:adb shell getprop ro.product.cpu.abi )
什么是 Android ABI ?
ABI 就是 Application binary interface 的意思,即应用程序二进制接口。他定义了一套规则,允许编译好的二进制目标代码能在所有兼容该ABI的操作系统中无需改动就能运行。不同的Android手机使用不同的CPU,因此需要提供对应的二进制接口交互规则(即对应的ABI文件)才能进行交互。目前,有部分CPU是能支持多种交互规则,但这是在牺牲性能的前提下所做的兼容。
API架构 | 说明 |
---|---|
armeabiv-v7a | 第7代及以上的 ARM 处理器。目前,大部分手机的CPU都支持此ABI交互规则 |
arm64-v8a | 第8代64位ARM处理器,支持此ABI的手机还比较少 |
armeabi | 第5代、第6代的ARM处理器,早期手机用得较多 |
x86 | 平板、模拟器用得比较多 |
x86_64 | 64位的平板 |
在 Android 手机上安装一个应用时,只有手机CPU架构支持的ABI架构对应的.so文件会被安装。如果支持多个ABI架构,会按照优先级进行安装。
CPU架构 | 支持的ABI架构对应的.so文件安装优先级 |
---|---|
ARMv5 | armeabi |
ARMv7 | armeabi,armeabi-v7a |
ARMv8 | armeabi,armeabi-v7a,arm64-v8a |
MIPS | mips |
MIPS64 | mips,mips64 |
x86 | x86,armeabi,armeabi-v7a |
x86_64 | :armeabi,x86,x86_64 |
CPU大都是向前兼容的,但是会按优先级来选择ABI。如x86架构的CPU会优先选择x86目录下的.so包,当不存在时,才会选择armeabi-v7a目录下的.so包,如果仍然不存在,最后才会选择armeabi目录下的.so文件。
注:x86设备运行armeabi下的so库时会损失性能。
打开 GitHub 之后你会发现,这里有很多个不同的版本,应该下载哪一个呢?
可以看到末尾处都有个 系统 和 CPU架构 的标识,我们直接看 Android 的。
这里标了Android的一共有四个,怎么判断自己的手机/模拟器属于哪一种CPU架构的呢?
查CPU架构的方法很多,这里介绍一个比较方便快捷的——使用一个名叫 Device Info HW 的 APP。
Device Info HW 下载地址:https://www.cr173.com/soft/845060.html
下载的文件名的格式是: frida-server-(version)-(platform)-(cpu).xz
例如:手机是 nexus6p, cpu 为 arm64
需要下载的是: frida-server-11.0.13-android-x86_64.xz;
注意, frida-server 的版本一定要跟 frida CLI 的版本一致。
执行以下命令
adb devices
adb push path/to/frida-server /data/local/tmp
adb shell
su
cd /data/local/tmp
chmod a+x ./frida-server 或者 chmod 777 ./frida-server
./frida-server 或者 ./frida-server &
frida tools
Frida 提供的工具,frida-trace,frida-ps,frida,frida-discover,这些工具都位于 python 的 Scripts 路径下,
frida 的工具共有 6 个:
(1)frida 命令是一个交互式解释器(REPL),用于实现 Hook 代码的动态注入,他的交互形式跟 IPython 很类似。
命令帮助:
示例:
将一个脚本注入到 Android 目标进程:frida -U -l myhook.js com.xxx.xxxx
参数解释:
- -U 指定对 USB 设备操作
- -l 指定加载一个 Javascript 脚本
- 最后指定一个进程名,如果想指定进程 pid,用
-p
选项。正在运行的进程可以用frida-ps -U
命令查看
frida 运行过程中,执行 %resume
重新注入,执行 %reload
来重新加载脚本;执行 exit
结束脚本注入
(2) frida-ps: 用于列出进程的一个命令行工具,当我们需要跟远程系统进行交互的时候,这个是非常有用的。
(备注:这里的 com.android.chrome 就是通过 frida-ps 命令查询到的远程客户端进程。)
另外还有四个分别是: frida-trace,frida-discover, frida-ls-devices, frida-kill
由于不是经常用到,这边就不一一详细介绍了, 感兴趣的可以去 frida 的官网查看他们的详细介绍和用法。
常用 Java API
官网也有文档说明:https://www.frida.re/docs/javascript-api,
由于 frida 的注入脚本是 Java,因此在开始 frida 初探之前,有必要对 Java 注入相关的 API 做一个简单介绍, 后面我们都将通过 js 脚本来操作设备上的 Java 代码。
当我们使用 Java.use() 获取 Java类之后,我们就可以通过 class.method.implementations = function() {} 的形式 hook 某类的某方法,不管是实例方法还是静态方法都可以。
另外,由于 js 代码注入时可能会出现超时的错误, 为了防止这个问题,我们通常还需要在最外面包装一层setImmediate(function(){}) 的代码,如下所示:
初探 Frida:Hello World 程序
apk 下载地址:https://github.com/OWASP/owasp-mstg/tree/master/Crackmes/Android
github 下载网速有点感人。链接: https://pan.baidu.com/s/1zYd4PmMQNlGqhzEUtXee6w 提取码: 6y8e
CTF之安卓逆向 uncrackme level1:https://blog.csdn.net/nini_boom/article/details/104565943/
Frida Hook app简单教程:https://blog.csdn.net/PLA12147111/article/details/100525588
- 注入模式
- 嵌入模式
- 预加载模式
注入模式
- 大部分情况下,我们都是附加到一个已经运行到进程,或者是在程序启动到时候进行劫持,然后再在目标进程中运行我们的代码逻辑。这种方式就是Frida最常用的使用方式,因此我们的文档也把大部分的篇幅集中在这种方式上。
- 注入模式的大致实现思路是这样的,带有GumJS的Frida核心引擎被打包成一个动态连接库,然后把这个动态连接库注入到目标进程中,同时提供了一个双向通信通道,这样你的控制端就可以和注入的模块进行通信了,在不需要的时候,还可以在目标进程中把这个注入的模块给卸载掉。
- 除了上述功能,Frida还提供了枚举已经安装的App列表,运行的进程列表已经已经连接的设备列表,这里所说的设备列表通常就是frida-server 所在的设备。frida-server 是一个守护进程,通过TCP和Frida核心引擎通信,默认的监听端口是27042。
嵌入模式
- 在实际使用的过程中,你会发现在没有 root 过的iOS Android设备上你是没有办法对进程进行注入的,也就是说第一种注入模式失效了,这个时候嵌入模式就派上用场了。Frida提供了一个动态连接库组件 frida-gadget, 你可以把这个动态库集成到你程序里面来使用Frida的动态执行功能。一旦你集成了gadget,你就可以和你的程序使用Frida进行交互,并且使用 frida-trace 这样的功能,同时也支持从文件自动加载Js文件执行JS逻辑。
- 关于 Gadget 更多功能,请参考原文链接(https://www.frida.re/docs/modes)
预加载模式
- 大家应该熟悉 LD_PRELOAD,或者 DYLDINSERTLIBRARIES吧, 如果有 JS_PRELOAD 这种东西是不是更爽了呢!事实上对于我们上面讨论的 Gadget 是支持这种模式的, 这才是Gadget 的强大之处,通过这种模式你就可以达到自动加载Js文件并执行的目的了。
- 译者注:这种模式我没测试,我猜测主要目的是说可以执行那些在程序入口点执行之前就需要执行的JS逻辑,而注入模式是在入口点跑过之后才能执行JS逻辑的,这个是最大的区别吧,大家最好再看下原文,避免被我误导)
通过上面的介绍,现在终于可以来真格的了。而这里所说的 Hello World 程序,和我们日常看编程书籍所说的有所不同,我们上网找了一个 OWASP Uncrackable Crackme Level 1 的 APP,并对其 "防Root检测" 功能进行了Hook。
将 UnCrackable-Level1.apk 安装到安卓设备( 可以直接拖进模拟器安装 ):adb install UnCrackable-Level1.apk
打开运行,存在 root 检测,由于我的模拟器是已经 root,所以点击 OK,程序它就自己退出了。
*********** 注意:这里使用的是 "雷电模拟器4",3版本运行 frida-server-12.11.17-android-x86 报错 ***********
方法 1( 使用 jadx-gui 或者 jeb 打开并自动反编译 apk ):
使用 jadx-gui 或者 jeb 打开 apk ,自动反编译,然后查看 java 源码。这里使用 jadx-gui 打开 apk 并自动反编译。。。
关键函数:
在 MainActivity 类中可以看到,函数 a 打开了一个对话框,并设置了一个 onClickListener监听,当按 ok 按钮时,onClick 事件函数就会被触发。onClick事件函数只是简单的使用 system.exit ( 0 ) 来退出 app 。
因此只需要不调用 system.exit ( 0 ) 阻止 App退出即可。所以需要 hook 住 onClick 方法。。。。。
frida_hook_java.py 代码:
hook 匿名内部类:
https://stackoverflow.com/questions/60236107/how-to-hook-into-onclick-method-inside-a-method-using-frida
import sys
import fridajs_code = '''
Java.perform(function(){console.log("[*] START...")var mClass = Java.use("sg.vantagepoint.uncrackable1.MainActivity$1")mClass.onClick.implementation=function() {console.log("[*] Clicked ")}
});
'''def on_message(msg, data): # js中执行 send 函数后要回调的函数if msg['type'] == 'send':print(f'[*] {msg["payload"]}')else:print(msg)if __name__ == '__main__':# get_remote_device 获取远程设备 (get_usb_device) attach 附加进程process = frida.get_remote_device().attach('owasp.mstg.uncrackable1')script = process.create_script(js_code)script.on('message', on_message) # 绑定一个事件script.load()sys.stdin.read()pass
说明:on_message 的用途是接收 Javascript 代码中调用 send 函数传出的日志,可以不用管它,或者可以使用 console.log 打日志,效果也是差不多的。然后就是 jscode 这个变量,这个变量其实建议使用一个单独的 .js 文件代替,因为这样的话可以使用各种编辑器、IDE的JavaScript代码格式化、智能提示等功能,写起来会舒服很多。如果你要替换掉的话,改成读 JS 代码文件之后 read 出内容字符串赋值给 jscode 就行了。这里直接使用一个变量保存 js ,没有使用 单独的 js 文件保存 js 代码。。。
import sys
import fridadef read_file_all(file_path):with open(file_path, 'r', encoding='utf-8') as f:text = f.read()return textdef on_message(message, data):if message['type'] == 'error':print("[!] " + message['stack'])elif message['type'] == 'send':print(message['payload'])if data is not None:print("[data] " + format_bytes(data))else:print(message)def format_bytes(byte_array):string = '['for b in byte_array:string = string + str(b) + ','return string[:len(string) - 1] + "]"if __name__ == '__main__': device = frida.get_usb_device()process = device.attach("com.xxxx.xxxx")js_code = read_file_all("hookMD5.js")script = process.create_script(js_code)script.on('message', on_message)script.load()sys.stdin.read()
打开两个 cmd 窗口,一个执行 frida-server 对应的版本,一个执行 端口转发,如图:
然后在模拟器中打开 apk,再执行上面的 python 代码,可以看到,点击 OK 后 apk 不会退出,hook 成功
方法 2( dex2jar classes.dex -o classes.jar ):
将 APK 文件扩展名修改为.zip,然后找到 classes.dex 文件将其编译为 classes.jar 文件。
dex2jar classes.dex -o classes.jar
编译完后,使用 JD-GUI 打开 classes.jar 文件,查找关键字,这里的关键字是 “Root detected”。
a 方法中关键代码,可以看到 new 了一个 b ,然后把 b 类的实例转换成 OnClickListener。
js 代码
现在让我们运行此 App 程序,它依然弹出了 Root检测的对话框。
现在让我们来运行 frida 注入程序,然后点击 OK 按钮,神奇的事情发生了,App 并没有退出,我们正常的进入了App界面。
frida注入界面:
frida 注入后的 App 界面
至此,我们以一个实例展示了frida框架的强大和神奇之处,这个Hook功能虽然简单,但是同样体现了整个的Hook过程,所谓麻雀虽小,但五脏俱全。
示例 2:crackme 自毁程序密码
Frida Hook和Xposed Hook 再搞Crackme :https://www.52pojie.cn/thread-1315865-1-1.html
阿里 crackme 的新解法 --- frida:https://www.52pojie.cn/thread-608505-1-1.html
一个简单 CrackMe 的逆向总结:https://blog.csdn.net/u012551350/article/details/98871295
crackme.apk:链接: https://pan.baidu.com/s/15Fwl0_MeZAr9ejGA9vh9sw 提取码: 6666
安装完 crackme.apk 之后,打开 app ,随意输入,然后点击 按钮
使用 jadx-gui 打开 apk,查看 AndroidManifest.xml
找到 com.yaotong.crackme.MainActivity,然后进入 MainActivity 查看源码
只要 securityCheck 函数 返回 true 即可。。。
使用 frida 开始 hook
import frida # 导入frida模块
import sys # 导入sys模块# 从此处开始定义用来Hook的javascript代码
js_code = """ Java.perform(function() {// 获取当前安卓设备的安卓版本var v = Java.androidVersion;send('version:' + v);//获取该应用加载的类var class_names = Java.enumerateLoadedClassesSync(); for (var i = 0; i < class_names.length; i++){send('class name:' + class_names[i])}var MainActivity = Java.use('com.yaotong.crackme.MainActivity'); //获得MainActivity类var java_string = Java.use('java.lang.String');MainActivity.securityCheck.implementation = function(java_string){ send('I am here'); // 发送信息,用于回调python中的函数return true; //劫持返回值,修改为我们想要返回的字符串}});
"""def on_message_1(msg, data):if msg['type'] == 'send':print(f'[*] {msg["payload"]}')else:print(msg)def on_message(msg, data): # js中执行send函数后要回调的函数print(msg)# 得到设备并劫持进程 com.example.testfrida
# (刚开始用get_usb_device函数用来获取设备,但是一直报错找不到设备,改用get_remote_device函数即可解决这个问题)
process = frida.get_remote_device().attach('com.yaotong.crackme')
script = process.create_script(js_code) # 创建js脚本
script.on('message', on_message) # 加载回调函数,也就是js中执行send函数规定要执行的python函数
script.load() # 加载脚本
sys.stdin.read()
上传 frida-server ,添加权限 chmod 777 frida-server-xxxx-android-x86,最后运行 frida-server。
在打开一个 cmd 窗口,执行端口转发: adb forward tcp:27042 tcp:27042
效果:任意输入,点击按钮都可以通过。
frida 远程连接手机 --- frida局域网连接 远程注入
frida 远程连接手机 frida局域网连接 远程注入:https://www.jianshu.com/p/ed5ff5e9b952
大家使用frida 进行hook的时候大部分时候是用usb数据线去连接。
可是总是有一些比较坑爹的时候 比如设备根本不能连接数据线 比如一些iot设备 或者 数据线连接时断时续比较感人,这个时候就可以用到frida 远程连接了
大家好,我是王铁头 一个乙方安全公司搬砖的菜鸡,持续更新移动安全,iot安全,编译原理相关原创视频文章
视频演示:https://space.bilibili.com/430241559
主要使用场景:
- usb无法使用
- usb连接不稳定
- usb同时hook多个设备
使用依赖
- pc和远程设备要安装frida环境
- 处在同一局域网环境下 (非局域网我自己没测试 测试了我再来改)
- frida_server要以root权限执行
frida远程连接的两种方式
1. 命令行远程连接:
1) 远程设备端启动 frida_server。这里端口可以加参数去修改 如果不加参数默认是 27042
//这里的 6666端口可以自定义 不加的话是 27042
frida:/data/local/tmp # ./fs_1413_a64 -l 0.0.0.0:6666
2) pc 端执行 frida -H 主机IP:端口
这里演示一下 查看进程 和 注入脚本的命令。查看进程
//查看进程的命令
frida-ps -H 192.168.2.102:6666
frida 远程连接手机
注入脚本
//注入脚本的命令
frida -H 192.168.2.102:6666 -f com.wangtietou.test_activity -l C:\\Users\\wangtietou\\Desktop\\hook_activity.js --no-pause
这里的脚本很简单 就是在主界面加载的时候 打印一行日志
====> on create
脚本代码:
var str_name_class = "com.wangtietou.test_activity.MainActivity";Java.perform(function()
{var obj = Java.use(str_name_class);obj.onCreate.implementation = function (arg){console.log('====> on create');return this.onCreate(arg);}
});
执行结果:
2. python 脚本远程连接
这里除了命令行直接执行js脚本 大家还经常使用 Python脚本使用frida
这里的脚本是hook Test类的 t1方法 当调用t1方法的时候 会打印一行日志:
====> t1
1) usb 连接时候的 python 脚本的注入代码
import frida
import sysrdev = frida.get_usb_device()
session = rdev.attach("com.wangtietou.test_activity") #要hook的程序包名scr = """
var str_name_class = "com.wangtietou.test_activity.Test";Java.perform(function()
{var obj = Java.use(str_name_class);obj.t1.implementation = function (){console.log('====> t1');return this.t1();}
});
"""script = session.create_script(scr)
def on_message(message ,data):print (message)
script.on("message" , on_message)
script.load()
sys.stdin.read()
2) 无线连接时候的 python 脚本的注入代码
import frida
import sysstr_host = '192.168.2.102:6666'
manager = frida.get_device_manager()
remote_device = manager.add_remote_device(str_host)
session = remote_device.attach("com.wangtietou.test_activity")scr = """
var str_name_class = "com.wangtietou.test_activity.Test";Java.perform(function()
{var obj = Java.use(str_name_class);obj.t1.implementation = function (){console.log('====> t1');return this.t1();}
});
"""script = session.create_script(scr)
def on_message(message ,data):print (message)
script.on("message" , on_message)
script.load()
sys.stdin.read()
这两个脚本大部分代码都是相同的 不同点在于连接设备的代码部分
连接 usb 的设备代码
rdev = frida.get_usb_device()
session = rdev.attach("com.wangtietou.test_activity")
连接远程设备的代码
str_host = '192.168.2.102:6666'
manager = frida.get_device_manager()
remote_device = manager.add_remote_device(str_host)
session = remote_device.attach("com.wangtietou.test_activity")
这里执行注入的 python 脚本,先启动设备端的 frida_server
执行远程注入脚本:
成功执行。持续更新移动安全,iot安全,编译原理相关原创视频文章。。。视频演示:[https://space.bilibili.com/430241559]
三、Java 层 Hook 操作案例分析
下面主要从以下几个方面来介绍:
- 第一、如何修改Java层的函数参数和返回值
- 第二、如何打印Java层的方法堆栈信息
- 第三、如何拦截native层的函数参数和返回值
对于 Java 层会注重介绍,因为我们用过 Xposed 工具之后都知道,比如参数是自定义类型怎么Hook等。
因为 Frida 大致原理是手机端安装一个 server 程序,然后把手机端的端口转到 PC端,PC端写 python 脚本进行通信,而 python 脚本中需要 hook 的代码采用 javascript 语言。
所以这么看来我们首先需要安装 PC端的 python 环境,安装 frida 直接运行命令:pip install frida
安装完成之后,我们再去官网下载对应版本的手机端程序 frida-server:https://github.com/frida/frida/releases
注意:frida-server 版本 和 PC端安装的 frida 版本必须一致,不然运行报错的。其实这里看到真的实现hook功能的是手机端的frida-server,这个也是开源的大家可以研究他的原理。我们也看到这个工具和 IDA 是不是很类似,也是把手机端的端口转发到PC端进行通信而已。
有了 frida-server 之后就好办了,直接 push 到手机目录下,然后修改一下文件的属性即可:
adb push C:\frida-server /data/local/tmpchmod 777 /data/local/tmp/frida-server# 然后直接运行这个程序:
/data/local/tmp# ./frida-server# 然后 端口转发
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043
接下来就开始 在PC端开始编写 hook 程序进行操作了:
这里代码也非常简单,因为安装好了 frida 模块,直接导入模块,然后调用 api 获取设备的 session 然后 hook 程序包名,接着就可以执行 js 脚本代码进行 hook 操作,然后打印消息:
这里用了 python 的 print 函数打印,其实如果想要打印可以在上面的 js 脚本中使用 console.log 也是可以的,看自己的习惯了。
可以看到脚本的大致流程就是:最外面用 python 引用 frida 库进行和设备通信,然后编写 js 脚本执行 hook 操作。
所以这里最主要的还是 js 脚本也就是需要理解 js 语法了。
下面就开始分部拆解操作,看看如何涵盖我们平常使用的 hook 案例。
Utils 类:
CoinMoney 类:
第一个案例:hook类的构造方法
我们有时候想 hook 一个类的构造方法,在 Xposed 中直接用 findConstructor 方法就可以了,因为构造方法可能有多种重载形式,所以需要用参数作为区分。这里 hook 案例的 CoinMoney 类 的 构造方法:
首先脚本中使用 Java.use 方法通过类名获取类类型,构造方法是固定写法:$init;这个要记住,然后因为需要重载所以用overload(......)形式即可,参数和参数之间用逗号隔开即可。后面就是拦截之后的操作了,这里方法参数可以自定义变量名,因为js是弱语言,不对类型做强检查,当然这里还有其他获取参数的方法后面会介绍。
这里使用 send 来发送打印消息即可,当然也可以用 console.log 形式打印日志,代码编写完了,下面就开始运行看效果,运行也很简单,直接 python frida.py:
在这之前一定要先打开 hook 的应用,不然会报错提示找不到这个程序进程:
这时候在运行看到了就成功了,我们把构造方法的参数打印出来了,那么这里 hook 就成功了。所以可以看到这个操作是不是比Xposed 工具更方便呢。但是他也有弊端后面会总结的。
第二、hook类的普通方法( 静态、私有、公开 )
这里的普通方法包括了 静态方法,私有方法 、公开方法 等,这个操作和上面的构造方法其实很类似
代码如下:
这个就是把构造方法的固定写法 $init 改成了需要 hook 的方法名即可。如果方法有重载形式还是用 overload 进行区分即可,比如这里我们 hook 了 Uitls.getPwd(String pwd) 方法:
然后这里我们看到可以用一个隐含的变量 arguments 获取参数,这个是保存了方法的参数信息是系统自带的。所以我们有两种方式获取方法的参数信息。运行看一下效果:
看到打印消息,hook成功了。所以这里就把 hook 方法获取参数的案例都介绍完了。
总结一下很简单:
- 构造方法使用固定写法 $init。
- 其他方法全部用 方法名 即可。
- 如果方法有重载形式需要用 overload 形式操作参数用逗号分隔。
- 获取参数可以自定义参数名或者用系统隐含的 arguments 变量获取。
- 当然,上面的所有操作之前,都需要用 Java.use 通过类名获取类型。
第三、修改方法的参数和返回值
我们在使用 Xposed 进行 hook 的时候最常用的可能就是修改参数和返回值来实现插件和外挂功能了,在Frida中其实也可以做到但是和 Xposed不一样,我们从上面的代码可以看到,没有像 Xposed 的 before 方法和 after 方法,而 Frida 直接是你可以在function 中调用原来的方法这样来进行参数修改,比如这里我要修改上面的方法参数和返回值:
因为 Frida 中没有 before 和 after 方法,但是可以直接调用原来的方法其实 Xposed 中也可以可以直接调用原来的方法的,但是不怎么常用,只要可以调用原来的方法,那么参数和返回值就可以随意修改了,这里我们把参数改成 jiangwei212,返回值后面追加 yyyy ,看打印的日志:
其实这么做比 before 和 after 形式更为方便,而且可以在原始方法调用前做一些事情和后面做一些事情。
第四、构造和修改自定义类型对象和属性
我们在 Xposed 写外挂的时候也会遇到这种比较常见的问题,就是方法的参数不是基本类型是自定义类型,然后也想修改他的属性值或者调用他的一个方法我们会使用反射来进行操作,而在返回值的时候,想构造一个自定义类型的对象也是直接用反射实例化一个对象进行操作的。其实在这里因为js中也是支持反射操作的,所以就很简单了:
这里构造一个对象其实很简单直接固定写法 $new 即可,然后有了对象也可以直接调用其对应的方法即可,然后就是如何修改一个对象类型的字段值呢?这个就要用反射了:
这里我们拦截了 getCoinMoney 方法,参数是 CoinMoney 类型,我们想修改他的money字段值,这时候我们直接调用他的方法没什么问题,但是如果直接调用字段值或者修改就会出现失败了,所以只能通过反射去修改字段值,不过要先获取这个对象对应的class类型,用Java.cast接口就可以,然后获取反射字段直接修改即可,这里要注意不管字段是private还是public的写法都是一样的,都是这段代码大家要注意把这段代码记住即可。我们看看hook之后的结果:
如果没有用反射去操作直接获取字段值打印就是object了。
第五、打印方法的堆栈信息
我们在破解过程中有时候通过抛出异常来打印堆栈信息跟踪代码效率会更高,Xposed 中操作很方便直接 Java 代码用 Log.xxx 方法打印堆栈信息即可,但是在 Frida 中有点麻烦了,因为他是js代码不好操作,第一次想到的办法就是自己写一个打印堆栈信息的类然后弄成一个dex之后,把这个dex注入到程序中,因为Frida支持把一个dex文件注入到原始程序中运行的,注入之后在需要打印堆栈信息的方法中调用这个dex中的那个方法就可以了。具体怎么注入本文不多介绍了。当时觉得这种方案太麻烦了,那么还有其他方案吗?其实还是有的,因为我们既然可以构造一个对象那么为什么不直接构造一个 Exception 对象呢?其实操作很简单,首先我们用 Java.use 方法获取类型变量:var Exception = Java.use("java.lang.Exception"); 然后是js中支持 throw 语法的,直接在需要打印堆栈信息的方法中调用即可:
不过这个是真得抛出异常了,没有捕获住,所以程序崩溃,我们在开发 Android 应用的时候如果程序崩溃了最快的查看异常信息的方法就是用日志过滤方式:adb logcat -s AndroidRuntime
这样我们就把堆栈信息打印出来了,其实这里可以看到这个是真的一个崩溃异常了,因为没有 catch 所以直接用系统崩溃日志就可以查看了。这种方式最简单粗暴了。对于跟踪代码非常有用的。
到这里我们就把所有可能遇到的情形Java层hook操作都介绍完了,主要包括以下几种常见情形:
- 第一、Hook类的构造方法和普通方法,注意构造方法是固定写法$init即可,获取参数可以通过自定义参数名也可以直接用系统隐含的arguments变量获取即可。
- 第二、修改方法的参数和返回值,直接调用原始方法传入需要修改的参数值和直接修改返回值即可。
- 第三、构造对象使用固定写法$new即可。
- 第四、如果需要修改对象的字段值需要用反射去进行操作。
- 第五、堆栈信息打印直接调用Java的Exception类即可,通过adb logcat -s AndroidRuntime来过滤日志信息查看崩溃堆栈信息。
总结:记得用 Java.use 方法获取类的类型,如果遇到重载的方法用 overload 实现即可。
示例代码:
# -*- coding: UTF-8 -*-import sys
import fridajs_code = """
if (Java.available) {Java.perform(function () {var MainActivity = Java.use("com.lanshifu.demo_module.ui.activity.DemoMainActivity");MainActivity.testCrash.overload("int").implementation = function (chinese, math) {console.log("[javascript] testCrash method be called.");send("hook testCrash method success>>>");return this.testCrash(12345678);}//声明一个Java类var MyClass = Java.use("com.lanshifu.demo_module.ui.activity.DemoHookTestActivity$MyClass");//hook 无参构造MyClass.$init.overload().implementation = function () {//调用原来构造this.$init();send("hook 无参构造 ");}//hook 有参构造,入参是 int[]MyClass.$init.overload('[I').implementation = function (param) {send("hook 有参构造 init(int[] i) method ");}//hook 多个参数构造,入参是 int,intMyClass.$init.overload("int", "int").implementation = function (param1, param2) {send("hook success ");send(param1);send(param2);//修改返回值return 100;}//void函数MyClass.method1.overload().implementation = function () {send("hook method1 ");this.method1();}//有入参的函数MyClass.method2.overload("int", "int").implementation = function (param1, param2) {send("hook method2 ");//实例化一个类并调用它的method3方法var ClassName = Java.use("com.lanshifu.demo_module.ui.activity.DemoHookTestActivity$MyClass");var instance = ClassName.$new();instance.method3(1, 2, 3);this.method2(100, 100)}//MyClass.method3.overload("int", "int", "int").implementation = function (param1, param2, param3) {send("hook method3 ");this.method3(100, 100, 100)}});
}
"""def on_message(message, data):if message['type'] == 'send':print("[*] {0}".format(message['payload']))else:print(message)pass# 查找 USB设备并附加到目标进程
session = frida.get_usb_device().attach('com.lanshifu.demo_module')
script = session.create_script(js_code) # 在目标进程里创建脚本
script.on('message', on_message) # 注册消息回调
print('[*] Start attach')
script.load() # 加载创建好的javascript脚本
sys.stdin.read() # 读取系统输入
四. Native层 Hook 操作案例分析
下面继续来看 Frida 更强大的地方就是 hook native 代码,说的强大不是因为功能,而是便捷程度,我们之前 hook native 可能用Cydia 比较多,但是都知道 Cydia 和 Xposed 一样都有兼容问题,环境安装配置太麻烦了,而 Frida 还是只需要几行 js 代码即可搞定,
这里 hook native 还是用两个案例介绍:一个是hook导出的函数,一个是hook未导出的函数,通过获取参数和修改返回值来演示,这里我们不自己写 native 代码了,直接用之前破解快手的数据请求的 so文件,他有一个函数在底层获取字符串信息,还有一个是最近正在研究的资讯类app的加密算法so,我们修改他的函数返回值。
第一、hook未导出函数功能
未导出的函数我们需要手动的计算出函数地址,然后将其转化成一个 NativePointer 的对象然后进行 hook 操作,那么如何计算一个函数地址呢?这个很简单只要得到 so 的内存基地址加上函数的相对地址就可以了。基地址获取直接查看程序对应的 maps 文件即可:
相对地址直接用 IDA 打开 so 文件就可以查看,比如这里我们通过静态分析之后想 hook 这个 sub_5070 函数:
然后我们 F5 查看函数对应的 C 语言代码查看参数信息:
这里看到是三个参数,那么计算了后的实际地址就是 0x7816A000+5070=0x7816F070,不过这个地址不是最后的地址,因为thumb 和 arm 指令的区分,地址最后一位的奇偶性来进行标志,所以这里还需加1也就是最终的0x7816F071,这一点很重要不管使用 Cydia 还是 Frida 都要注意最后计算的绝对地址要 +1,不然会报错的:
这里 hook 之后有两个回调方法一个是进入函数之前,一个是执行完之后,这个和 Xposed 非常类似了,我们打印参数,不过这个和之前Hook Java层就不一样了,因为在C中大部分都是和地址指针相关,特别是常见的字符串信息,我们如果要正确的打印字符串值就需要借助 Memory系统类来通过指针获取字符串信息了,这个类非常重要,在后面修改返回值也是用它写内存值的。我们先看看这个函数原始返回值是什么:
这个是加密之后的值了,然后我们获取到参数了,而通过 IDA 分析之后发现这个函数最终的结果不是通过 return 来返回的,而是通过第三个指针参数返回的,因为 C中有一个参数传值功能,就是直接操作指针就可以传回结果,这个在C中经常用到,因为一个函数返回值只有一处要是一个函数有多个返回值就没办法了,所以可以通过参数指针来传递。所以如果我们想修改函数的最终结果,需要修改参数指针的内存段数据,我们先把那个内存段数据获取到打印出来,这里因为通过静态分析知道最终的结果是16个字节数据,所以这里不能在用读取内存字符串方法了,而是读取纯的字节数据:
然后在把返回值修改了,返回值修改也很简单,直接重写那段内存值就可以了,比如这里修改成1111:
所以看到了C语言中很多地方都在直接操作内存也就是地址,特别需要借助 Memory类,他有很多方法,包括内存拷贝等。具体用到的可以去官网查询:https://www.frida.re/docs/javascript-api/#memory;然后我们看hook结果:
我们hook到了他的参数信息,第一个参数是需要加密的字符串信息我们是通过Memory方法获取字符串的,因为本身这个参数是一个字符串指针,第二个参数应该是字符串长度,第三个参数是操作结果值的指针,然后看到我们获取到的结果值就是原始加密的信息。说明我们获取成功了,然后再看看我们修改之后的1111值,通过日志查看:
看到了在 Java 成通过 native 访问得到的签名信息已经被修改成了1111了,说明我们成功了。到这里我们就成功的,在hook native的时候一定要注意函数的绝对地址要计算对,最后一定要记住+1,函数的返回值有可能不是通过return而是参数指针传递的,操作内存的时候用 Memory类即可。
在雷电模拟器上 hook native层
apk 和 源码地址:链接: https://pan.baidu.com/s/1pcd-sHa9tgGqwCxjpVMrAQ 提取码: 6ztm
模拟器运行 apk 截图:
注意:雷电模拟器是 x86 架构,所以需要使用 IDA Pro 打开 x86 的 so 文件,找到 函数偏移地址,然后再通过 " 基址 + 函数偏移地址 " 得到函数加载到内存后的真实地址,hook 这个函数的真实地址。。。
函数偏移地址:
frida_hook_native.py 代码:
import sys
import fridajs_code = '''
setImmediate(function(){send("start");// 遍历模块,找基址Process.enumerateModules({onMatch:function(exp){if(exp.name == "libnative-lib.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("libnative-lib.so");for (var i = 0; i < exports.length; i++){send("name:" + exports[i].name + " address:" + exports[i].address); }// 通过模块名直接查找基址var baseAddress = Module.findBaseAddress("libnative-lib.so");Interceptor.attach(baseAddress.add(0x000091D0), {onEnter:function(args){send(args[2]);send(args[3]);send(args[4]);//send(Memory.readCString(args[2]));//send(Memory.readCString(args[3]));//send(Memory.readCString(args[4]));},onLeave:function(ret_val){console.log(ret_val);send(ret_val)}});
})
'''def message(msg, data):if msg['type'] == 'send':print(f'[*] {msg["payload"]}')else:print(msg)if __name__ == '__main__':# get_remote_device 获取远程设备 (get_usb_device) attach 附加进程process = frida.get_remote_device().attach('com.example.frida_native_demo')script = process.create_script(js_code)script.on('message', message) # 绑定一个事件script.load()sys.stdin.read()pass
打开两个 cmd 窗口,一个执行 frida-server 对应的版本,一个执行 端口转发,如图:
模拟器中打开 apk,再执行上面的 python 代码:
第二、hook导出函数功能
这部分内容很简单了,比上面的简单是因为不需要手动的计算函数地址,因为是导出的,所以直接可以得到导出的函数名即可,因为C语言中没有重载的形式,而C++中有,所以有时候发现导出的函数名和正常的函数名前面加上了一串数据作为区分那应该是 C++ 代码写的。有了so文件和导出的函数名就不需要构造 NativePoniter 了:
这个看到比上面自己手动找函数地址方便多了吧,打印参数都一样的代码了。这里通过函数名可以知道就是一个native函数了,那么他第一个参数肯定是JNIEnv指针,第二个参数是jclass类型,这个是标准的如果是静态方法第二个参数没啥用,后面的参数就是真的传递到native层的值了,比如这里Java层的方法:
那么按照上面的说明native层的函数就是4个参数了:
的确是这样的,后面两个参数才是我们想要的值,我们通过IDA查看这个函数:
然后我们用F5查看伪代码他的返回值:
用 env 指针调用了 NewStringUTF 返回一个 jstring 对象了,好了到这里我们先不说返回值修改的问题,先看看hook参数信息:
但是我们看到我们打印的返回值是个空也就是空指针,而如果这里我们想hook他的返回值怎么办呢?如果是一个正常的返回字符串信息,我们可以直接用Memory的方法构造出来Memory.allocUtf8String("XXXXX")一个内存字符串信息,然后直接返回一个指针地址即可,但是现在这里是返回一个jstring对象,其实这个我们通过查看jni.h文件可以知道jstring是C++中定义的对象:
而基本类型就是基本数据类型:
这个修改没有任何问题的,那么现在问题是修改非基本类型,比如这里的如何返回jstring对象呢?这里我能想到的一个办法就是通过获取NewStringUTF函数指针,通过NativeFunction方法获取函数,然后调用
这里看到代码逻辑没什么问题,现在缺的就是 NewStringUTF 的函数地址了,这个因为在 so 中没法查看,所以怎么办呢?不着急我们在看看 JNIEnv 的定义:
他是一个结构体,再看看那个函数地址:
我们已经有了 JNIEnv 结构体指针了,每个函数指针都是 int 类型也就是 四个字节,所以从 JNIEnv 指针开始依次计算就可以得到NewStringUTF 函数对应的地址了。不过都说了找不到方法的时候就去官网找,JNIEnv 变量其实有对应的方法,这里构造 jstring 方法其实很简单:
这个比找 函数指针 方便多了,其实 env 有很多方法在这里都有对应的 api。
所以到这里我们发现了 Frida 在 Hook 底层函数返回 jni 中的类型的时候有点麻烦了,但是 Cydia 就不会了,因为他是 Android 工程,可以引用 jni.h 头文件的,比如我们用 Cydia 来修改这个函数的返回值:
看到了吧,这样就很方便了因为是 Android 工程,所以可以直接应用 jni.h 头文件,然后直接调用 NewStringUTF 方法返回了,看看 hook 的结果:
也修改成功了。所以这里看到Frida也不是万能的,要看什么问题怎么去分析了。
五、技术总结
到这里我们就把Frida常用的功能和hook常见的用法都说明完了,下面就来总结一下:
第一、Java层代码Hook操作
1、hook方法包括构造方法和对象方法,构造方法固定写法是$init,普通方法直接是方法名,参数可以自己定义也可以使用系统隐含的变量arguments获取。
2、修改方法的参数和返回值,直接调用原始方法通过传入想要修改的参数来做到修改参数的目的,以及修改返回值即可。
3、构造对象和修改对象的属性值,直接用反射进行操作,构造对象用固定写法的$new即可。
4、直接用Java的Exception对象打印堆栈信息,然后通过adb logcat -s AndroidRuntime来查看异常信息跟踪代码。
总结:获取对象的类类型是Java.use方法,方法有重载的话用overload(.......)解决。
第二、Native层代码Hook操作
1、hook导出的函数直接用so文件名和函数名即可。
2、hook未导出的函数需要计算出函数在内存中的绝对地址,通过查看maps文件获取so的基地址+函数的相对地址即可,最后不要忘了+1操作。
总结:Native中最常用的就是内存地址指针了,所以如果要正确的获取值一定要用Memory类作为辅助,特别是字符串信息。
frida hook jni_onload
************************ 打印 jni_onload 里面的注册函数 ************************
:https://www.bilibili.com/video/BV1UE411A7rW?p=79 28' 40''
以调试模式
hook.js 代码:
打印结果:
frida 简单使用
From:https://www.cnblogs.com/tjp40922/p/11357660.html
示例代码:
# get_front_app.py.py
# 得到android手机当前最前端Activity所在的进程import frida
rdev = frida.get_remote_device()
front_app = rdev.get_frontmost_application()
print (front_app)# enum_process.py内容如下:
# 枚举android手机所有的进程import frida
rdev = frida.get_remote_device()
processes = rdev.enumerate_processes()
for process in processes:print (process)# 枚举某个进程加载的所有模块以及模块中的导出函数
import frida
rdev = frida.get_remote_device()
session = rdev.attach("com.tencent.mm") #如果存在两个一样的进程名可以采用rdev.attach(pid)的方式
modules = session.enumerate_modules()
for module in modules:print (module)export_funcs = module.enumerate_exports()print ("\tfunc_name\tRVA")for export_func in export_funcs:print ("\t%s\t%s"%(export_func.name,hex(export_func.relative_address)))# 枚举某个进程加载的所有模块以及模块中的导出函数
from __future__ import print_function
import frida, sysdef on_message(message, data):print(message)jscode = """Process.enumerateModules({onMatch:function(exp){send(exp.name);},onComplete:function(){send("stop");}
})
"""process = frida.get_usb_device().attach('com.yinghuan.aiyou')
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
sys.stdin.read()# hook android的native函数
import frida
import sys
rdev = frida.get_remote_device()
session = rdev.attach("com.tencent.mm")
scr = """
Interceptor.attach(Module.findExportByName("libc.so" , "open"), {onEnter: function(args) {send("open("+Memory.readCString(args[0])+","+args[1]+")");},onLeave:function(retval){}
});
"""
script = session.create_script(scr)
def on_message(message ,data):print (message)
script.on("message" , on_message)
script.load()
sys.stdin.read()# hook android的java层函数
# 如下代码为hook微信(测试版本为6.3.13,不同版本由于混淆名字
# 的随机生成的原因或者代码改动导致类名不一样)com.tencent.mm.sdk.platformtools.ay类
# 的随机数生成函数,让微信猜拳随机(type=2),而摇色子总是为6点(type=5)import frida
import sys
rdev = frida.get_remote_device()
session = rdev.attach("com.tencent.mm")scr = """
Java.perform(function () {
var ay = Java.use("com.tencent.mm.sdk.platformtools.ay");
ay.pu.implementation = function(){var type = arguments[0];send("type="+type);if (type == 2){return this.pu(type);}else{return 5;}
};});
"""script = session.create_script(scr)
def on_message(message ,data):print message
script.on("message" , on_message)
script.load()
sys.stdin.read()# 通过frida向android进程注入 dex
import frida, sys, optparse, re
def on_message(message, data):if message['type'] == 'send':print("[*] {0}".format(message['payload']))else:print(message)jscode = """
Java.perform(function () {var currentApplication = Java.use("android.app.ActivityThread").currentApplication();var context = currentApplication.getApplicationContext();var pkgName = context.getPackageName();var dexPath = "%s";var entryClass = "%s";Java.openClassFile(dexPath).load();console.log("inject " + dexPath +" to " + pkgName + " successfully!")Java.use(entryClass).%s("%s");console.log("call entry successfully!")
});
"""def checkRequiredArguments(opts, parser):missing_options = []for option in parser.option_list:if re.match(r'^\[REQUIRED\]', option.help) and eval('opts.' + option.dest) == None:missing_options.extend(option._long_opts)if len(missing_options) > 0:parser.error('Missing REQUIRED parameters: ' + str(missing_options))if __name__ == "__main__":usage = "usage: python %prog [options] arg\n\n" \"example: python %prog -p com.android.launcher " \"-f /data/local/tmp/test.apk " \"-e com.parker.test.DexMain/main " \"\"hello fridex!\""parser = optparse.OptionParser(usage)parser.add_option("-p", "--package", dest="pkg", type="string",help="[REQUIRED]package name of the app to be injected.")parser.add_option("-f", "--file", dest="dexPath", type="string",help="[REQUIRED]path of the dex")parser.add_option("-e", "--entry", dest="entry", type="string",help="[REQUIRED]the entry function Name.")(options, args) = parser.parse_args()checkRequiredArguments(options, parser)if len(args) == 0:arg = ""else:arg = args[0]pkgName = options.pkgdexPath = options.dexPathentry = options.entry.split("/")if len(entry) > 1:entryClass = entry[0]entryFunction = entry[1]else:entryClass = entry[0]entryFunction = "main"process = frida.get_usb_device(1).attach(pkgName)jscode = jscode%(dexPath, entryClass, entryFunction, arg)script = process.create_script(jscode)script.on('message', on_message)print('[*] Running fridex')script.load()sys.stdin.read()# 通过注入抛出异常代码实现跟踪程序调用栈在<<Android 软件安全与逆向分析>>这本书中第八章有介绍通过重打包写入异常代码进行栈跟踪,但是这样比较麻烦,使用frida注入更方便。
六、Hook 家族神器的对比
下面继续来看看 Frida,Xposed,SubstrateCydia 这三个 Hook 神器的区别和优缺点:
第一、Xposed的优缺点
优点:在编写Java层hook插件的时候非常好用,这一点完全优越于Frida和SubstrateCydia,因为他也是Android项目,可以直接编写Java代码调用各类api进行操作。而且可以安装到手机上直接使用。
缺点:配置安装环境繁琐,兼容性差,在Hook底层的时候就很无助了。
第二、Frida的优缺点
优点:在上面我们可以看到他的优点在于配置环境很简单,操作也很便捷,对于破解者开发阶段非常好用。支持Java层和Native层hook操作,在Native层hook如果是非基本类型的话操作有点麻烦。
缺点:因为他只适用于破解者在开发阶段,也就是他没法像Xposed用于实践生产中,比如我写一个微信外挂用Frida写肯定不行的,因为他无法在手机端运行。也就是破解者用的比较多。
第三、SubstrateCydia的优缺点
优点:可以运行在手机端,和Xposed类似可以用于实践生产中。支持Java层和Native层的hook操作,但是Java层hook不怎么常用,用的比较多的是 Native 层 hook 操作,因为他也是Android工程可以引用系统api,操作更为方便。
缺点:和Xposed一样安装配置环境繁琐,兼容性差。
以上这三个工具可以说是现在用的最多的 hook工具了,总结一句话就是写Java层Hook还是Xposed方便,写 Native 层 Hook 还是Cydia 了,而对于破解者开发那还是 Frida 最靠谱了。但是不管怎么样,写外挂最难的也是最重要的不是写代码而是寻找 hook点,也就是逆向分析 app 找到那个地方,然后写 hook 代码实现插件功能。
《Android应用安全防护和逆向分析》