环境与系统
Windows
python,目前没有研究过哪个版本安装这个比较好,但是笔者通过测试的版本是 python3.7.9 。
adb,需要有 adb 的支持。
一个已经 root 或者具有 root 权限的 Android 手机。
安装 frida pip 安装 frida 库 这里笔者按照教程安装的是 12.8.0 版本。
1 2 pip install frida==12.8.0 pip install frida-tools==5.3.0
安装 frida server 下载 frida server 进入 Frida Release 页面。选择和前面安装 frida 一致的版本的 server(笔者这里是12.8.0 版本的)。同时注意自己手机的架构,这里选择的是 arm64。笔者下载的版本链接: frida-server-12.8.0-android-arm64.xz 。
安装&运行 frida server
push 文件到手机
进入 Android Linux 控制台,,添加可执行权限。
首先确保手机正确连接到电脑,并且开启了 USB 调试
模式。(可以在电脑终端通过 adb devices
查看自己的设备是否正确连接。
运行命令 adb shell
,即可进入 手机的 Linux 命令行中。
进入目录给目标添加权限。(确保自己的 root 权限)
1 2 cd /data/local/tmp chmod 777 frida-server-12.8.0-android-arm64
在手机中运行服务端
frida server 运行测试 在开启端口转发的终端中运行下面的命令查看 Android 中正在运行的进程列表
如果有下面的这种效果的输出,那就没啥问题了
1 2 3 4 5 6 7 8 9 10 11 12 PID Name ----- ---------------------------------------------------------- 9113 .dataservices 1160 ATFWD-daemon 18557 adbd 1118 adsprpcd 8401 android.hardware.audio@2.0-service 693 android.hardware.bluetooth@1.0-service-qti 694 android.hardware.camera.provider@2.4-service 695 android.hardware.cas@1.0-service 696 android.hardware.configstore@1.0-service 。。。。。。。。。
frida 调试测试 在安装好上述的环境配置等之后,可以编写脚本进行测试。 读者可以参考下方是实例 GitHub demo,但是笔者更推荐手动编写。
GitHub demo GitHub 上有 demo 可以安装教程进行简单的使用测试。自然此玩意要求有 node.js 的环境。
大致 demo 用法如下:
1 2 3 4 $ git clone git://github.com/oleavr/frida-agent-example.git $ cd frida-agent-example/$ npm install $ frida -U -f com.example.android --no-pause -l _agent.js
手动编写 demo 手动编写 demo 笔者认为理论上这样就可以不用安装多余的 node.js 。只是安装了再配上一个好的编辑器他会有自动补全等功能。
下面先编写测试脚本:
si.js 1 2 3 4 5 6 function main ( ) { Java .perform (function x ( ) { console .log ("hello world" ) }) } setImmediate (main)
loader.py 1 2 3 4 5 6 7 8 9 10 11 12 import timeimport fridadevice8 = frida.get_device_manager().add_remote_device("127.0.0.1:27042" ) pid = device8.spawn("com.android.settings" ) device8.resume(pid) time.sleep(1 ) session = device8.attach(pid) with open ("si.js" , "r" , encoding="utf-8" ) as f: script = session.create_script(f.read()) script.load() input ()
解释一下,这个脚本就是先通过frida.get_device_manager().add_remote_device
来找到device,然后spawn方式启动settings,然后attach到上面,并执行frida脚本。
注意,这里第4行的端口一定要和前面运行时进行端口转发的端口一致才能进行转发。
然后将上面的两个文件放在同一个文件夹下面,并在此文件夹中启动一个终端,运行 python 脚本
1 2 3 PS D:\Users\Desktop\frida> python .\loader.py hello world This_Is_What_I_Input_To_Continue
在一切都顺利的情况下,连接的手机设备会自动弹出设置页面,然后运行 python 的终端就会打印一个 “hello world” 。
frida 打印和修改参数 下面将进行模拟对目标程序进行 js 注入,然后打印和修改其返回值。
frida_demo.apk 首先编写一个 demo 的简单的 apk,用于进行测试。
MainActivity.java
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 package com.example.frida_demo;import androidx.appcompat.app.AppCompatActivity;import android.util.Log;import android.os.Bundle;public class MainActivity extends AppCompatActivity { private String total = "@@@###@@@" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } fun(50 ,30 ); Log.d("Ron_Log_TAG_string" , fun("LoWeRcAsE ThIs!!!!!!!!!" )); } } void fun (int x , int y ) { Log.d("Ron_Log_TAG_sum" , String.valueOf(x+y)); } String fun (String x) { total +=x; return x.toLowerCase(); } String secret () { return total; } }
然后 Android Studio 会自动生成 xml 等文件,保持其不变,然后编译并安装此 apk 到调试机上,点击其中的 logcat
查看系统的日志,可以看到如下所示的日志记录:
2021-03-22 14:12:44.365 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:12:44.366 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!! 2021-03-22 14:12:45.366 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:12:45.367 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!! 2021-03-22 14:12:46.373 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:12:46.375 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!! 2021-03-22 14:12:47.377 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:12:47.379 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!! 2021-03-22 14:12:48.380 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:12:48.382 2570-2570/com.example.frida_demo D/Ron_Log_TAG_string: lowercase this!!!!!!!!! 2021-03-22 14:12:49.383 2570-2570/com.example.frida_demo D/Ron_Log_TAG_sum: 80
由于写了一个死循环,因此此日志会应该程序一直运行一直打印日志。
frida_demo.js 下面和前面手写的 demo 一样,编写一个 js 用于注入进行执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function main ( ) { console .log ("Enter the Script!" ); Java .perform (function x ( ) { console .log ("Inside Java perform by frida_demo.js" ); var MainActivity = Java .use ("com.example.frida_demo.MainActivity" ); MainActivity .fun .overload ('java.lang.String' ).implementation = function (str ) { console .log ("original call : str:" + str); var ret_value = "Ron_js" ; return ret_value; }; }) } setImmediate (main);
运行测试 首先检测机器中是否在运行测试的 frida_demo.apk
。 通过 findstr
实现结果的过滤。
1 2 3 PS D:\Users\Desktop\frida\frida_demo> frida-ps -U | findstr "frida" 3071 com.example.frida_demo 21252 frida-helper-32
然后通过 -f
即通过 spawn,重启 apk 注入 js 代码。
利用 Android Studio 启动应用,查看 logcat 依旧可以看到如前面的日志一样的 Ron_Log_TAG_string: lowercase this!!!!!!!!!
。
然后在保存 js 文件的文件夹中打开终端, 输入下面的命令后,测试机上的应用会被重启,可以看到 Android Studio 中的日志也在刷新重启。然后,按照终端中的提示,下面第 14 行 [OPPO PAFM00::com.example.frida_demo]->
后面输入 %resume
以恢复应用,不然应用会出现 Process terminated
而无法看到结果。
然后可以看到终端中不断打印出应用中函数 fun(string)
的输入变量的值。同时查看 Android Studio 中的 logcat 可以看到日志已经被修改了 /Ron_Log_TAG_string: Ron_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 PS D:\Users\D1esktop\frida\frida_demo> frida -U -f com.example.frida_demo -l frida_demo.js ____ / _ | Frida 12.8.0 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ Spawning `com.example.frida_demo`... Enter the Script! Spawned `com.example.frida_demo`. Use %resume to let the main thread start executing! [OPPO PAFM00::com.example.frida_demo]-> %resume [OPPO PAFM00::com.example.frida_demo]-> Inside Java perform by frida_demo.js original call : str:LoWeRcAsE ThIs!!!!!!!!! original call : str:LoWeRcAsE ThIs!!!!!!!!! original call : str:LoWeRcAsE ThIs!!!!!!!!! ......(省略一万行) original call : str:LoWeRcAsE ThIs!!!!!!!!! Process terminated [OPPO PAFM00::com.example.frida_demo]-> Thank you for using Frida!
2021-03-22 14:42:24.575 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:42:24.580 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js 2021-03-22 14:42:25.582 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:42:25.590 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js 2021-03-22 14:42:26.591 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:42:26.597 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js 2021-03-22 14:42:27.599 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:42:27.606 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js 2021-03-22 14:42:28.607 5732-5732/com.example.frida_demo D/Ron_Log_TAG_sum: 80 2021-03-22 14:42:28.614 5732-5732/com.example.frida_demo D/Ron_Log_TAG_string: Ron_js
frida 选择 instance,并进行修改 继续利用上面的 frida_demo.apk
进行测试 。
frida_demo_rpc.js 新建一个 js 用于查找目标程序的 instance 当找到的时候,这里调用了其中的 fun(String)
函数,传入参数为 “Ron_rpc_js”, 并获取其返回值进行打印。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function callFun ( ) { Java .perform (function fn ( ) { console .log ("begin" ); Java .choose ("com.example.frida_demo.MainActivity" , { onMatch : function (x ) { console .log ("find instance by frida_demo_rpc.js :" + x); console .log ("result of fun(string):" + x.fun (Java .use ("java.lang.String" ).$new("Ron_rpc_js" ))); }, onComplete : function ( ) { console .log ("end" ); } }) }) } rpc.exports = { callfun : callFun };
loader.py 和最前面进行打开菜单的测试一样,这里通过在手机中开启 frida 的服务端并开启端口转发,然后 python 连接设备调用前面的 frida_demo_rpc.js
进行执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import timeimport fridadevice = frida.get_device_manager().add_remote_device("127.0.0.1:27042" ) pid = device.spawn(["com.example.frida_demo" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) with open ("frida_demo_rpc.js" , "r" , encoding="utf-8" ) as f: script = session.create_script(f.read()) def my_message_handler (message, payload ): print (message) print (payload) script.on("message" , my_message_handler) script.load() script.exports.callfun()
运行测试 新建一个终端,然后运行上面的 loader.py
,如果一切正常的话,可以看到手机中的应用被启动,然后终端打印调用其中函数后输出的结果。
1 2 3 4 5 PS D:\Users\Desktop\frida\frida_demo> python .\loader.py begin find instance by frida_demo_rpc.js :com.example.frida_demo.MainActivity@9f74e23 result of fun(string):ron_rpc_js end
如果运行报错,尝试运行命令安装包后再进行测试
npm install rpc
frida 动态修改 动态修改,可以实现将手机上 app 中的内容发送到电脑端,通过 python 将数据处理之后,再转发给 app 进行后续的处理。这里的关键,参考的 blog 将是 send
和 recv
函数。
如下我们将构建一个测试用的 app,它会发送一个 base64 编码后的用户名和觅马,而我们的目标是:
让 message_tv.setText 可以 ”发送” username 为 admin 的 base64 字符串。
因此,我们需要 hook 的函数是 TextView.setText
。
frida_demo.apk 首先还是构建测试 apk 进行测试。
MainActivity.java
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 package com.example.frida_demo;import androidx.appcompat.app.AppCompatActivity;import android.util.Base64;import android.util.Log;import android.os.Bundle;import android.view.View;import android.widget.EditText;import android.widget.TextView;public class MainActivity extends AppCompatActivity { EditText username_et; EditText password_et; TextView message_tv; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); password_et = (EditText) this .findViewById(R.id.editText2); username_et = (EditText) this .findViewById(R.id.editText); message_tv = ((TextView) findViewById(R.id.textView)); this .findViewById(R.id.button).setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { if (username_et.getText().toString().compareTo("admin" ) == 0 ) { message_tv.setText("You cannot login as admin" ); return ; } message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT)); } }); } }
activity_main.xml
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 55 56 <?xml version="1.0" encoding="utf-8" ?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:app ="http://schemas.android.com/apk/res-auto" xmlns:tools ="http://schemas.android.com/tools" android:layout_width ="match_parent" android:layout_height ="match_parent" tools:context =".MainActivity" > <TextView android:id ="@+id/textView" android:layout_width ="239dp" android:layout_height ="82dp" android:text ="please input username and password" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintLeft_toLeftOf ="parent" app:layout_constraintRight_toRightOf ="parent" app:layout_constraintTop_toTopOf ="parent" /> <EditText android:id ="@+id/editText" android:layout_width ="fill_parent" android:layout_height ="40dp" android:hint ="username" android:maxLength ="20" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintEnd_toEndOf ="parent" app:layout_constraintHorizontal_bias ="1.0" app:layout_constraintStart_toStartOf ="parent" app:layout_constraintTop_toTopOf ="parent" app:layout_constraintVertical_bias ="0.095" /> <EditText android:id ="@+id/editText2" android:layout_width ="fill_parent" android:layout_height ="40dp" android:hint ="password" android:maxLength ="20" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintTop_toTopOf ="parent" app:layout_constraintVertical_bias ="0.239" tools:ignore ="MissingConstraints" /> <Button android:id ="@+id/button" android:layout_width ="170dp" android:layout_height ="59dp" android:layout_gravity ="right|center_horizontal" android:text ="提交" android:visibility ="visible" app:layout_constraintBottom_toBottomOf ="parent" app:layout_constraintEnd_toEndOf ="parent" app:layout_constraintStart_toStartOf ="parent" app:layout_constraintTop_toTopOf ="parent" app:layout_constraintVertical_bias ="0.745" /> </androidx.constraintlayout.widget.ConstraintLayout >
app 编写完毕后,记得编译并在测试机中进行安装。
此时在手机端输入用户名和觅马后点击提交,app 中会打印 base 之后的结果,这里输入
username:Ron
password:12345678
提交就打印的结果为:
b’Um9uOjEyMzQ1Njc4’
frida_demo3.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 console .log ("Script loaded successfully " );Java .perform (function ( ) { var tv_class = Java .use ("android.widget.TextView" ); tv_class.setText .overload ("java.lang.CharSequence" ).implementation = function (x ) { var string_to_send = x.toString (); var string_to_recv; send (string_to_send); recv (function (received_json_object ) { string_to_recv = received_json_object.my_data console .log ("string_to_recv: " + string_to_recv); }).wait (); var my_string = Java .use ("java.lang.String" ).$new(string_to_recv); this .setText (my_string); } });
loader.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 27 28 29 30 31 32 33 import timeimport fridaimport base64def my_message_handler (message, payload ): print (message) print (payload) if message["type" ] == "send" : print (message["payload" ]) data = message["payload" ].split(":" )[1 ].strip() print ( 'message:' , message) data = str (base64.b64decode(data)) print ( 'data:' ,data) user, pw = data.split(":" ) print ( 'pw:' ,pw) data = str (base64.b64encode(("admin" + ":" + pw).encode())) print ( "encoded data:" , data) script.post({"my_data" : data}) print ( "Modified data sent" ) device = frida.get_device_manager().add_remote_device("127.0.0.1:27042" ) pid = device.spawn(["com.example.frida_demo" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid) with open ("frida_demo3.js" , "r" , encoding="utf-8" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load() input ()
运行测试 上述完毕之后,运行 loader.py
可以看到手机中的测试 demo 被启动起来,同时 python 的终端打印 Script loaded successfully
并开始等待。
此时在手机端输入和之前同样的用户名和觅马后点击提交,可以看到 python 终端已经拦截到了数据,并进行了修改和替换,然后手机端显示的结果也是 python 替换之后的。
1 2 3 4 5 6 7 8 9 10 11 12 13 PS D:\Users\Desktop\frida\frida_demo2> python .\loader.py Script loaded successfully {'type': 'send', 'payload': 'Sending to the server :Um9uOjEyMzQ1Njc4\n'} None Sending to the server :Um9uOjEyMzQ1Njc4 message: {'type': 'send', 'payload': 'Sending to the server :Um9uOjEyMzQ1Njc4\n'} data: b'Ron:12345678' pw: 12345678' encoded data: b'YWRtaW46MTIzNDU2Nzgn' Modified data sent string_to_recv: b'YWRtaW46MTIzNDU2Nzgn' This_Is_What_I_Input_To_Continue
打开 python 可以轻易进行验证。
1 2 3 4 5 6 7 In [1 ]: from base64 import b64decode, b64encode In [2 ]: b64decode("YWRtaW46MTIzNDU2Nzgn" ) Out[2 ]: b"admin:12345678'" In [3 ]: b64encode(b"Ron:12345678" ) Out[3 ]: b'Um9uOjEyMzQ1Njc4'
API List 本部分简要列举了一些常用的(js 的) API 。(其中有些前面已经用到了)
Java.choose(className: string, callbacks: Java.ChooseCallbacks): void
通过扫描 Java VM 的堆来枚举 className 类的 live instance。
Java.use(className: string): Java.Wrapper<{}>
动态为 className 生成 JavaScript Wrapper ,可以通过调用 $new()
来调用构造函数来实例化对象。 在实例上调用 $dispose()
以对其进行显式清理,或者等待 JavaScript 对象被 gc。
Java.perform(fn: () => void): void
Function to run while attached to the VM. Ensures that the current thread is attached to the VM and calls fn. (This isn’t necessary in callbacks from Java.) Will defer calling fn if the app’s class loader is not available yet. Use Java.performNow() if access to the app’s classes is not needed.
send(message: any, data?: ArrayBuffer | number[]): void
任何 JSON 可序列化的值。 将 JSON 序列化后的 message 发送到您的基于 Frida 的应用程序,并包含(可选)一些原始二进制数据。 The latter is useful if you e.g. dumped some memory using NativePointer#readByteArray().
recv(callback: MessageCallback): MessageRecvOperation
Requests callback to be called on the next message received from your Frida-based application. This will only give you one message, so you need to call recv() again to receive the next one.
wait(): void
堵塞,直到 message 已经 receive 并且 callback 已经执行完毕并返回
frida 动静结合分析 frida 查询顶部 activity
有时候很不方便看到目标 app 的包名,比如其加了壳的时候,使用 frida 可以查看当前状态下顶部的 activity 是谁
1 2 3 4 5 # windows adb shell dumpsys activity top | findstr ACTIVITY # linux adb shell dumpsys activity top | grep ACTIVITY
objection
虽然 frida 提供了各种且丰富的 API 供调用,但是一些具体功能的实现需要手动编写并利用各种 API 来组合。这样显然比较难受,于是有大佬将各种常见的常用的功能整合进了一个工具,以供在命令行中快捷使用。这个工具就是 objection
。
objection
功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法 hook
打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。
pip 安装 objection 这里同样不是安装的最新版,而是安装的 1.8.4 版本。运行完毕后,就可以在命令行中直接执行 objection
命令,键入 --help
参数可以查看帮助信息。
1 pip install objection==1.8.4
内存漫游 获取基本信息 基本操作方法
键入命令回车执行
help 命令:在任意命令前键入 help 命令,执行会打印对当前命令的解释信息。如 help env
空格:显示提示信息,上下键移动选择,再空格确认选择。
jobs:作业系统,可以同时运行多项 hook 作业。
实际演示 在手机上运行 frida-server
(与前面一致),并在手机上打开设置,然后通过终端查看 ”设置“ 应用的包名。这里是 com.android.settings
。
1 2 3 4 5 PS D:\Users\Desktop\frida> frida-ps -U | findstr -i setting 19321 com.android.settings 19363 com.android.settings:index 15869 com.coloros.simsettings 19408 com.coloros.wirelesssettings
再使用 objection
注入 ”设置“ 应用。键入命令 objection -g com.android.settings explore
,如下可以看到注入成功,终端在等待命令执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 PS D:\Users\Desktop\frida\frida_demo2> objection -g com.android.settings explore Using USB device `OPPO PAFM00` Agent injected and responds ok! _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) v1.8.4 Runtime Mobile Exploration by: @leonjza from @sensepost [tab] for command suggestions com.android.settings on (OPPO: 8.1.0) [usb] #
启动 objection
之后,会出现提示它的 logo
,这时候不知道输入啥命令的话,可以按下空格,有提示的命令及其功能出来;再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令,如下图执行应用环境信息命令 env
和 frida-server
版本信息命令。
提取内存信息 查看内存中加载的库 通过 object
还可以查看内存中加载的库。 运行命令 memory list modules
可以查看到内存中加载的库。
1 2 3 4 5 6 7 8 9 com.android.settings on (OPPO: 8.1.0) [usb] # memory list modules Save the output by adding `--json modules.json` to this command Name Base Size Path ----------------------- ------------ -------------------- ----------------------------------- app_process64 0x64c770a000 32768 (32.0 KiB) /system/bin/app_process64 libandroid_runtime.so 0x7ae7b57000 2154496 (2.1 MiB) /system/lib64/libandroid_runtime.so libbase.so 0x7ae7641000 77824 (76.0 KiB) /system/lib64/libbase.so libbinder.so 0x7ae79e5000 589824 (576.0 KiB) /system/lib64/libbinder.so ......
查看库的导出函数 运行命令 memory list exports libssl.so
,可以查看 libssl.so
库的导出函数。
1 2 3 4 5 6 7 8 9 10 com.android.settings on (OPPO: 8.1.0) [usb] # memory list exports libssl.so Save the output by adding `--json exports.json` to this command Type Name Address -------- ----------------------------------------------------- ------------ function SSL_use_certificate_ASN1 0x7a662ab200 function SSL_CTX_set_dos_protection_cb 0x7a662b3fdc function SSL_SESSION_set_ex_data 0x7a662b628c function SSL_CTX_set_session_psk_dhe_timeout 0x7a662b7144 function SSL_CTX_sess_accept 0x7a662b2394 ......
将结果保存到 json
中 如果信息太多可能导致终端无法显示,可以将结果导出至文件中,再利用其他软件进行查看。
运行命令 memory list exports libart.so --json ./libart.json
将 libssl.so
库的导出函数保存到当前文件夹下的 libart.json
。
1 2 3 com.android.settings on (OPPO: 8.1.0) [usb] # memory list exports libart.so --json ./libart.json Writing exports as json to ./libart.json... Wrote exports to: ./libart.json
然后就可以在保存位置查看保存到文件的数据了。
提取整个(或部分)内存 通过命令 memory dump all from_base
可以实现。直接 dump 全部有 1.7 G,会炸。在后文脱壳中再用。
1 2 3 4 com.android.settings on (OPPO: 8.1.0) [usb] # memory dump all from_base Will dump 886 rw- images, totalling 1.7 GiB (frida:11832): GLib-GIO-WARNING **: 16:54:46.966: _g_dbus_worker_do_read_cb: error determining bytes needed: Blob indicates that message exceeds maximum message length (128MiB)
搜索整个内存 通过命令 memory search --string --offsets-only
可以实现,在后文脱壳中再用。
内存堆搜索与执行 在堆上搜索实例 这里还是通过运行 ”设置“ 来进行测试,为了进行演示测试,需要去官方(AOSP源码设置 )查找一些 ”设置“ 中存在的类,发现存在 DisplaySettings
类。然后可以在堆上搜索是否存在着该类的实例。
依旧同样在手机中打开 ”设置“,然后在终端运行命令 android heap search instances com.android.settings.DisplaySettings
,查看相应的实例地址。按照参考博客,理论上应该出现下面的结果。
1 2 3 4 5 com.android.settings on (OPPO: 8.1.0) [usb] # android heap search instances com.android.settings.DisplaySettings Using exsiting matches for com.android.settings.DisplaySettings. Use --fresh flag for new instances. Handle Class toString() -------- ------------------------------------ ----------------------------------------- 0x252a com.android.settings.DisplaySettings DisplaySettings{69d91ee #0 id=0x7f0a0231}
但是笔者实际上的结果是这样,至于发生了什么,我暂且梦在蒲里。
1 2 com.android.settings on (OPPO: 8.1.0) [usb] # android heap search instances com.android.settings.DisplaySettings Class instance enumeration complete for com.android.settings.DisplaySettings
于是,笔者故技重施,反手一个 exit
退出了当前会话,然后手机打开刚才的 frida_demo
的 app ,然后终端去 hook
这个应用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PS D:\Users\Desktop\frida> objection -g com.example.frida_demo explore Using USB device `OPPO PAFM00` Agent injected and responds ok! _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) v1.8.4 Runtime Mobile Exploration by: @leonjza from @sensepost [tab] for command suggestions com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap search instances com.example.frida_demo.MainActivity --fresh flag Class instance enumeration complete for com.example.frida_demo.MainActivity Handle Class toString() -------- ----------------------------------- ------------------------------------------- 0x203a com.example.frida_demo.MainActivity com.example.frida_demo.MainActivity@9f74e23
出现了想要的结果,查到了目标中存在的实例。至于前面设置为啥没成功,也许是不是原生安卓的原因?我暂且蒙在蒲里。
调用实例的方法 既然上面的查询设置中的实例都失败了,再调用其中的方法显然是不现实的,于是干脆就用 frida_demo
的 app 进行接下来的测试了。
为了方便起见,我们在之前的 frida_demo.java
(第二个版本)中添加一个方法,放置在 onCreate()
方法之后。该方法会将文本框中的内容进行覆盖,并返回自己的函数名字。修改完毕记得进行重新编译。
1 2 3 4 protected String JustPrintTest () { message_tv.setText("I'm func JustPrintTest()" ); return "come from JustPrintTest()" ; }
然后在终端中通过命令 objection -g com.example.frida_demo explore
连接到这个 session。然后通过命令 android heap execute 0x203a JustPrintTest
执行其中新添加的方法。此处注意 0X203a
地址是上一条命令返回的 com.example.frida_demo.MainActivity
的地址。
运行之后,终端显示执行成功,我们可以在测试手机中查看,文本框中的内容的确被覆盖了。
1 2 3 4 5 6 7 8 9 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap search instances com.example.frida_demo.MainActivity --fresh flag Class instance enumeration complete for com.example.frida_demo.MainActivity Handle Class toString() -------- ----------------------------------- ------------------------------------------- 0x203a com.example.frida_demo.MainActivity com.example.frida_demo.MainActivity@9f74e23 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap execute 0x203a JustPrintTest Handle 0x203a is to class com.example.frida_demo.MainActivity Executing method: JustPrintTest() come from JustPrintTest()
在实例上执行 js 代码 也可以在找到的实例上直接执行 js
脚本。 输入 android heap evaluate [address]
后会进入一个迷你编辑器,输入 console.log("evaluate result:"+clazz.JustPrintTest())
这串脚本,然后按 ESC
退出编辑器,然后回车就会执行这个脚本。
迷你编辑器展示如下
1 2 3 4 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap evaluate 0x203a (The handle at `0x203a` will be available as the `clazz` variable.) JavaScript edit mode. [ESC] and then [ENTER] to accept. [CTRL] + C to cancel.
输入脚本
1 console .log ("evaluate result:" +clazz.JustPrintTest ());
键入 ESC
然后回车,可以看到下面的结果,打印了 evaluate result:come from JustPrintTest()
。
1 2 3 4 5 6 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android heap evaluate 0x203a (The handle at `0x203a` will be available as the `clazz` variable.) console.log("evaluate result:"+clazz.JustPrintTest()); JavaScript capture complete. Evaluating... Handle 0x203a is to class com.example.frida_demo.MainActivity evaluate result:come from JustPrintTest()
这个功能其实非常实用,可以即时编写、出结果、即时调试自己的代码,不用再编写→注入→操作→看结果→再调整,而是直接出结果。
启动 activity 或者 service 直接启动 activity 直接上代码,想要进入显示设置,可以在 任意界面 直接运行以下代码进入显示设置:
理论上会显示下面的界面,但是笔者又失败了。
1 2 3 # android intent launch_activity com.android.settings.DisplaySettings (agent) Starting activity com.android.settings.DisplaySettings... (agent) Activity successfully asked to start
根据下面的一个子目录的提示,我觉得参考博客讲错了,也就是上面使用了删除线的 ”任意界面“ ,或者说这样说有歧义。正确的说法,应该是在当前的 app 的范围内,可以启动而不会报错,而启动别的就会报错。
果然,重新新建一个在设置中的监听之后,再执行显示,就可以正确跳转到显示设置页面中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 PS D:\Users\S9039124> objection -g com.android.settings explore Using USB device `OPPO PAFM00` Agent injected and responds ok! _ _ _ _ ___| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_| |___|(object)inject(ion) v1.8.4 Runtime Mobile Exploration by: @leonjza from @sensepost [tab] for command suggestions com.android.settings on (OPPO: 8.1.0) [usb] # android intent launch_activity com.android.settings.DisplaySettings (agent) Starting activity com.android.settings.DisplaySettings... (agent) Activity successfully asked to start.
查看当前可用的 activity
可以使用 android hooking list activities
来查看当前可用的 activities
。然后再使用上面的方法进行启动。
1 2 3 4 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking list activities com.example.frida_demo.MainActivity Found 1 classes
直接启动 service
也可以先使用 android hooking list services
查看可供开启的服务。
然后使用 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService
命令来开启服务。
frida hook anywhere 我们新手在学习 Frida
的时候,遇到的第一个问题就是,无法找到正确的类及子类,无法定位到实现功能的准确的方法,无法正确的构造参数、继而进入正确的重载,这时候可以使用 Frida
进行动态调试,来确定以上具体的名称和写法,最后写出正确的 hook
代码。
objection(内存漫游) 列出内存中所有的类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking list classes [B [C [D [F [I [J [Landroid.animation.Animator; [Landroid.animation.Keyframe$FloatKeyframe; ...... sun.util.logging.PlatformLogger sun.util.logging.PlatformLogger$1 sun.util.logging.PlatformLogger$Level void Found 5711 classes
内存中搜索所有的类 很显然上面的方法搜索的结果太多,我们需要过滤一下最自己有用的部分。
可以使用命令 android hooking search classes [keyword]
来进行筛选。如下,可以看到已经少了很多。
1 2 3 4 5 6 7 8 9 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking search classes display [Landroid.icu.text.DisplayContext$Type; [Landroid.icu.text.DisplayContext; [Landroid.view.Display$Mode; ...... javax.microedition.khronos.egl.EGLDisplay oppo.util.OppoDisplayUtils Found 52 classes
内存中搜索关键的方法 在内存中搜索所有的方法也是同理。但是前面内存中已加载的类就已经高达 5711
个了,那么他们的方法一定是类的个数的数倍,一一列举的话,整个过程会相当庞大和耗时,因此没有必要再一一列举,这里就直接筛选服务关键字的命令。 android hooking search methods display
。
可以看到,即使我们进行了筛选,终端还是进行了警告。这里我们选择 y
。整个过程明显变慢了很多,因为要一一去遍历所有的方法进行对比。
1 2 3 4 5 6 7 8 9 10 11 com.example.frida_demo on (OPPO: 8.1.0) [usb] # android hooking search methods display Warning, searching all classes may take some time and in some cases, crash the target application. Continue? [y/N]: y Found 5711 classes, searching methods (this may take some time)... android.app.ActionBar.getDisplayOptions android.app.ActionBar.setDefaultDisplayHomeAsUpEnabled ...... androidx.constraintlayout.solver.widgets.analyzer.DependencyGraph.generateDisplayNode An unexpected internal exception has occurred. If this looks like a code related error, please file a bug report!(session detach message) process-terminated script is destroyed
果然还是崩溃了。
列举出某个类的所有方法 当搜索到了比较关心的类之后,就可以直接查看它有哪些方法,比如我们想要查看 com.android.settings.DisplaySettings
类有哪些方法。如下所示。
1 2 3 4 5 6 7 8 9 10 11 com.android.settings on (OPPO: 8.1.0) [usb] # android hooking list class_methods com.android.settings.DisplaySettings private static java.util.List<com.android.settings.core.PreferenceController> com.android.settings.DisplaySettings.buildPreferenceControllers(android.content.Context,com.android.settings.core.lifecycle.Lifecycle) protected int com.android.settings.DisplaySettings.getHelpResource() protected int com.android.settings.DisplaySettings.getPreferenceScreenResId() protected java.lang.String com.android.settings.DisplaySettings.getLogTag() protected java.util.List<com.android.settings.core.PreferenceController> com.android.settings.DisplaySettings.getPreferenceControllers(android.content.Context) public int com.android.settings.DisplaySettings.getMetricsCategory() public void com.android.settings.DisplaySettings.onAttach(android.content.Context) static java.util.List com.android.settings.DisplaySettings.access$000(android.content.Context,com.android.settings.core.lifecycle.Lifecycle) Found 8 method(s)
参考博客还与源码 进行了比对,发现是一模一样的。有兴趣的读者也可以自己比对一下。
直接生成 hook
代码 上文中在列出类的方法时,还直接把参数也提供了,也就是说我们可以直接动手写 hook
了,既然上述写 hook
的要素已经全部都有了,objection
这个“自动化”工具,当然可以直接生成代码。使用命令 android hooking generate simple com.android.settings.DisplaySettings
可以完成。
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 55 56 57 58 com.android.settings on (OPPO: 8.1 .0 ) [usb] # android hooking generate simple com.android.settings.DisplaySettings Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.getMetricsCategory.implementation = function() { return clazz.getMetricsCategory.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.onAttach.implementation = function() { return clazz.onAttach.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.getPreferenceControllers.implementation = function() { return clazz.getPreferenceControllers.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.getHelpResource.implementation = function() { return clazz.getHelpResource.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.buildPreferenceControllers.implementation = function() { return clazz.buildPreferenceControllers.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.getPreferenceScreenResId.implementation = function() { return clazz.getPreferenceScreenResId.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.getLogTag.implementation = function() { return clazz.getLogTag.apply(this , arguments); } }); Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings' ); clazz.access$000. implementation = function() { return clazz.access$000. apply(this , arguments); } });
生成的代码大部分要素都有了,只是参数貌似没有填上,还是需要我们后续补充一些,看来还是无法做到完美。如何使用后面讲到。
objection (hook) 上述操作均是基于在内存中直接枚举搜索,已经可以获取到大量有用的静态信息,我们再来介绍几个方法,可以获取到执行时动态的信息,当然、同样地,不用写一行代码。
hook
某个类的所有方法我们以手机连接蓝牙耳机播放音乐为例为例,看看手机蓝牙接口的动态信息。首先我们将手机连接上我的蓝牙耳机 —— AirPods 2
,并可以正常播放音乐;然后我们按照上文的方法,搜索一下与蓝牙相关的类,搜到一个高度可疑的类:android.bluetooth.BluetoothDevice
。
1 2 3 4 5 6 7 8 9 10 com.android.settings on (OPPO: 8.1.0) [usb] # android hooking search classes bluetooth android.bluetooth.BluetoothA2dp android.bluetooth.BluetoothA2dp$1 android.bluetooth.BluetoothA2dp$2 android.bluetooth.BluetoothAdapter ...... com.oppo.settings.widget.preference.OppoBluetoothEntryPreference com.oppo.settings.widget.preference.OppoBluetoothEntryPreference$1 Found 36 classes
运行以下命令,hook
这个类:
1 2 3 4 5 6 7 8 com.android.settings on (OPPO: 8.1.0) [usb] # android hooking watch class android.bluetooth.BluetoothDevice (agent) Hooking android.bluetooth.BluetoothDevice.-get0() (agent) Hooking android.bluetooth.BluetoothDevice.-set0(android.bluetooth.IBluetooth) ...... (agent) Hooking android.bluetooth.BluetoothDevice.setSimAccessPermission(int) (agent) Hooking android.bluetooth.BluetoothDevice.toString() (agent) Hooking android.bluetooth.BluetoothDevice.writeToParcel(android.os.Parcel, int) (agent) Registering job yi6z8sxiz2. Type: watch-class for: android.bluetooth.BluetoothDevice
使用 jobs list
命令可以看到 objection
为我们创建的 Hooks
数为 55
,也就是将 android.bluetooth.BluetoothDevice
类下的所有方法都 hook
了。
1 2 3 4 com.android.settings on (OPPO: 8.1.0) [usb] # jobs list Job ID Hooks Type ---------- ------- -------------------------------------------------- yi6z8sxiz2 55 watch-class for: android.bluetooth.BluetoothDevice
这时候我们在 设置→声音→媒体播放到
上进行操作,在蓝牙耳机与“此设备”之间切换时,会命中这些 hook
之后,此时 objection
就会将方法打印出来,会将类似这样的信息“吐”出来:
1 2 3 4 5 6 com.android.settings on (google: 9) [usb] # (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.isConnected() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() ...... (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService()
理论上应该吐出来吧?但是笔者的手机没有这个选项,无法切换输出源。
hook
方法的参数、返回值和调用栈在这些方法中,我们对哪些方法感兴趣,就可以查看哪些个方法的参数、返回值和调用栈,比如想看getName()
方法,则运行以下命令: android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace
1 2 3 4 com.android.settings on (OPPO: 8.1.0) [usb] # android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace (agent) Attempting to watch class android.bluetooth.BluetoothDevice and method getName. (agent) Hooking android.bluetooth.BluetoothDevice.getName() (agent) Registering job cwojtdpd3w. Type: watch-method for: android.bluetooth.BluetoothDevice.getName
但是还是很遗憾,并没有打印出,我想看到的信息,理论上应该是有很多 hook
之后的输出的。
注意最后加上的三个选项 --dump-args --dump-return --dump-backtrace
,为我们成功打印出来了我们想要看的信息,其实返回值 Return Value
就是getName()
方法的返回值,我的蓝牙耳机的型号名字 OnePlus Bullets Wireless 2
;从调用栈可以反查如何一步一步调用到 getName()
这个方法的;虽然这个方法没有参数,大家可以再找个有参数的试一下。
hook
方法的所有重载objection
的 help
中指出,在 hook
给出的单个方法的时候,会 hook
它的所有重载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 com.android.settings on (OPPO: 8.1.0) [usb] # help android hooking watch class_method Command: android hooking watch class_method Usage: android hooking watch class_method <fully qualified class method> <optional overload> (optional: --dump-args) (optional: --dump-backtrace) (optional: --dump-return) Hooks a specified class method and reports on invocations, together with the number of arguments that method was called with. This command will also hook all of the methods available overloads unless a specific overload is specified. If the --include-backtrace flag is provided, a full stack trace that lead to the methods invocation will also be dumped. This would aid in discovering who called the original method. Examples: android hooking watch class_method com.example.test.login android hooking watch class_method com.example.test.helper.executeQuery android hooking watch class_method com.example.test.helper.executeQuery "java.lang.String,java.lang.String" android hooking watch class_method com.example.test.helper.executeQuery --dump-backtrace android hooking watch class_method com.example.test.login --dump-args --dump-return
那我们可以用 File
类的构造器来试一下效果。运行命令 android hooking watch class_method java.io.File.$init --dump-args
。
1 2 3 4 5 6 7 8 9 com.android.settings on (OPPO: 8.1.0) [usb] # android hooking watch class_method java.io.File.$init --dump-args (agent) Attempting to watch class java.io.File and method $init. (agent) Hooking java.io.File.$init(java.io.File, java.lang.String) (agent) Hooking java.io.File.$init(java.lang.String) (agent) Hooking java.io.File.$init(java.lang.String, int) (agent) Hooking java.io.File.$init(java.lang.String, java.io.File) (agent) Hooking java.io.File.$init(java.lang.String, java.lang.String) (agent) Hooking java.io.File.$init(java.net.URI) (agent) Registering job lxv3z0u0ep8. Type: watch-method for: java.io.File.$init
可以看到 objection
为我们 hook
了 File
构造器的所有重载,一共是 6 个。在设置界面随意进出几个子设置界面,可以看到命中很多次该方法的不同重载,每次参数的值也都不同。
1 2 3 4 5 6 7 8 com.android.settings on (OPPO: 8.1.0) [usb] # (agent) [lxv3z0u0ep8] Called java.io.File.File(java.lang.String) (agent) [lxv3z0u0ep8] Arguments java.io.File.File(/system/media/theme/default/com.oppo.launcher) (agent) [lxv3z0u0ep8] Called java.io.File.File(java.io.File, java.lang.String) (agent) [lxv3z0u0ep8] Arguments java.io.File.File(/data/user_de/0/com.android.settings, shared_prefs) (agent) [lxv3z0u0ep8] Called java.io.File.File(java.io.File, java.lang.String) (agent) [lxv3z0u0ep8] Arguments java.io.File.File(/data/user_de/0/com.android.settings/shared_prefs, development.xml) (agent) [lxv3z0u0ep8] Called java.io.File.File(java.io.File, java.lang.String) ......
ZenTracer(hook) 更大范围的 hook 工具 ZenTracer 请查看参考博客。地址 。
frida 参数打印与构造 首先解释一下下,这个是啥意思。
gson Google 的库用于辅助构造后续的部分类型。(如 char[]
)。
配置步骤
下载 gson
压缩包,地址 。
将文件 利用 adb push
到手机中,解压到与 frida-server
放在一起。为了避免与引用了此包的 app 混淆,建议重命名一下。
使用方法 在注入的 js
代码中直接引用,如下所示:
1 2 3 4 Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load ();const gson = Java .use ('com.r0ysue.gson.Gson' );console .log ("charArray Object Object:" ,gson.$new().toJson (charArray));
这里看不懂没关系,下文中会在 char[]
, byte[]
等的构造中,广泛用到。
参数打印 bytes2hex
传入的参数为 byte[],
example:[12, 0, 156, -127]
return: [0c, 00, 9c, fe]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function bytes2hex (arrBytes ){ var str = "" ; for (var i = 0 ; i < arrBytes.length ; i++) { var tmp; var num = arrBytes[i]; if (num < 0 ) { tmp = (255 + num + 1 ).toString (16 ); } else { tmp = num.toString (16 ); } if (tmp.length == 1 ) { tmp = "0" + tmp; } if (i>0 ){ str += " " +tmp; }else { str += tmp; } } return str; }
string2bytes 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 string2bytes (str ) { var bytes = new Array (); var len, c; len = str.length ; for (var i = 0 ; i < len; i++) { c = str.charCodeAt (i); if (c >= 0x010000 && c <= 0x10FFFF ) { bytes.push (((c >> 18 ) & 0x07 ) | 0xF0 ); bytes.push (((c >> 12 ) & 0x3F ) | 0x80 ); bytes.push (((c >> 6 ) & 0x3F ) | 0x80 ); bytes.push ((c & 0x3F ) | 0x80 ); } else if (c >= 0x000800 && c <= 0x00FFFF ) { bytes.push (((c >> 12 ) & 0x0F ) | 0xE0 ); bytes.push (((c >> 6 ) & 0x3F ) | 0x80 ); bytes.push ((c & 0x3F ) | 0x80 ); } else if (c >= 0x000080 && c <= 0x0007FF ) { bytes.push (((c >> 6 ) & 0x1F ) | 0xC0 ); bytes.push ((c & 0x3F ) | 0x80 ); } else { bytes.push (c & 0xFF ); } } return bytes; }
bytes2string 据说这个转化在 Chrome 的 console 里面是没问题的,但是放入 frida 的脚本中,如果遇到非 unicode 解码范围的值就会报错。
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 function bytes2string (arr ) { if (typeof arr === 'string' ) { return arr; } var str = '' , _arr = arr; for (var i = 0 ; i < _arr.length ; i++) { var one = _arr[i].toString (2 ), v = one.match (/^1+?(?=0)/ ); if (v && one.length == 8 ) { var bytesLength = v[0 ].length ; var store = _arr[i].toString (2 ).slice (7 - bytesLength); for (var st = 1 ; st < bytesLength; st++) { store += _arr[st + i].toString (2 ).slice (2 ); } try { str += String .fromCharCode (parseInt (store, 2 )); } catch (error) { str += parseInt (store, 2 ).toString (); console .log (error); } i += bytesLength - 1 ; } else { try { str += String .fromCharCode (_arr[i]); } catch (error) { str += parseInt (store, 2 ).toString (); console .log (error); } } } return str; }
也可以通过使用 Java
的函数来实现。
1 2 3 4 5 function bytesToString (value ) { var buffer = Java .array ('byte' , value); var StringClass = Java .use ('java.lang.String' ); return StringClass .$new(buffer); }
下面的就需要使用 Google 的 gson
库来进行辅助构造。同时还需要参阅 JNI
签名。
char[] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load ();const gson = Java .use ('com.r0ysue.gson.Gson' );Java .use ("java.lang.Character" ).toString .overload ('char' ).implementation = function (char ){ var result = this .toString (char); console .log ("char,result" ,char,result); return result; } Java .use ("java.util.Arrays" ).toString .overload ('[C' ).implementation = function (charArray ){ var result = this .toString (charArray); console .log ("charArray,result:" ,charArray,result) console .log ("charArray Object Object:" ,gson.$new().toJson (charArray)); return result; }
byte[] 1 2 3 4 5 6 7 8 9 Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load ();const gson = Java .use ('com.r0ysue.gson.Gson' );Java .use ("java.util.Arrays" ).toString .overload ('[B' ).implementation = function (byteArray ){ var result = this .toString (byteArray); console .log ("byteArray,result):" ,byteArray,result) console .log ("byteArray Object Object:" ,gson.$new().toJson (byteArray)); return result; }
byteBuffer 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 Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load ();const gson = Java .use ('com.r0ysue.gson.Gson' );my_class.b .overload ('java.nio.ByteBuffer' , 'java.nio.ByteBuffer' ).implementation = function (x0,x1 ) { var result = this .b (x0,x1); var bytesArray = bytesBuffer2bytesArray (x0, 'hb' ); var tmp = gson.$new().toJson (x0); tmp = JSON .parse (tmp); console .log ("tmp:" +typeof (tmp)); console .log ("decrypt --> message: " ,tmp["backingArray" ]); return result; } function bytesBuffer2bytesArray (bytesBuffer, key ) { Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load (); const gson = Java .use ('com.r0ysue.gson.Gson' ); var tmp = gson.$new().toJson (bytesBuffer); var dataKey = key tmp = JSON .parse (tmp); tmp = new Array (tmp[dataKey]); tmp = tmp.toString (); var tmp_array = tmp.split ("," ); var tmp_int_array=[]; tmp_int_array=tmp_array.map (function (data ){ return +data; }); return tmp_int_array }
打印memorybuffer 1 2 3 4 var view = new DataView (this .context .r0 .readByteArray (12 ));var value = '0x' + view.getUint8 (11 ).toString (16 ) + view.getUint8 (10 ).toString (16 ) + view.getUint8 (9 ).toString (16 ) + view.getUint8 (8 ).toString (16 );
打印non-ascii https://api-caller.com/2019/03/30/frida-note/#non-ascii 类名非 ASCII 字符串时,先编码打印出来, 再用编码后的字符串去 hook.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 cls.forName .overload ('java.lang.String' , 'boolean' , 'java.lang.ClassLoader' ).implementation = function (arg1, arg2, arg3 ) { var clsName = cls.forName (arg1, arg2, arg3); console .log ('oriClassName:' + arg1) var base64Name = encodeURIComponent (arg1) console .log ('encodeName:' + base64Name); if ('o.%CE%99%C9%AB' == base64Name) { console .log (arg3); } return clsName; }
hook enum Java
代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 enum Signal { GREEN, YELLOW, RED } public class TrafficLight { public static Signal color = Signal.RED; public static void main () { Log.d("4enum" , "enum " + color.getClass().getName().toString()); switch (color) { case RED: color = Signal.GREEN; break ; case YELLOW: color = Signal.RED; break ; case GREEN: color = Signal.YELLOW; break ; } } }
JavaScript 代码如下
1 2 3 4 5 6 7 8 9 10 Java .perform (function ( ){ Java .choose ("com.r0ysue.a0526printout.Signal" ,{ onMatch :function (instance ){ console .log ("instance.name:" ,instance.name ()); console .log ("instance.getDeclaringClass:" ,instance.getDeclaringClass ()); },onComplete :function ( ){ console .log ("search completed!" ) } }) })
打印 hash map 1 2 3 4 5 6 7 8 9 10 11 Java .perform (function ( ){ Java .choose ("java.util.HashMap" ,{ onMatch :function (instance ){ if (instance.toString ().indexOf ("ISBN" )!= -1 ){ console .log ("instance.toString:" ,instance.toString ()); } },onComplete :function ( ){ console .log ("search complete!" ) } }) })
参数构造 如果不仅仅是想打印参数,同时还想替换掉原来的参数,则需要自行先构造参数。
Java array 构造 使用 Java.array
API 构造 charArray 。
对 Java.array
的解释文档。
1 2 3 4 5 6 7 8 9 10 11 function array (type: string, elements: any[] ): any[];
构建代码如下
1 2 3 4 5 6 7 8 Java .use ("java.util.Arrays" ).toString .overload ('[C' ).implementation = function (charArray ){ var newCharArray = Java .array ('char' , [ '一' ,'去' ,'二' ,'三' ,'里' ]); var result = this .toString (newCharArray); console .log ("newCharArray,result:" ,newCharArray,result) console .log ("newCharArray Object Object:" ,gson.$new().toJson (newCharArray)); var newResult = Java .use ('java.lang.String' ).$new(Java .array ('char' , [ '烟' ,'村' ,'四' ,'五' ,'家' ])) return newResult; }
类的多态与转型(Java.cast) 可以通过 getClass().getName().toString()
来查看当前实例的类型。
找到一个 instance
,通过 Java.cast
来强制转换对象的类型。
对 Java.cast
的解释文档
1 2 3 4 5 6 7 8 function cast (handle: Wrapper | NativePointerValue, klass: Wrapper ): Wrapper ;
Java
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Water { public static String flow (Water W) { Log.d("2Object" , "water flow: I`m flowing" ); return "water flow: I`m flowing" ; } public String still (Water W) { Log.d("2Object" , "water still: still water runs deep!" ); return "water still: still water runs deep!" ; } } ... public class Juice extends Water { public String fillEnergy () { Log.d("2Object" , "Juice: i`m fillingEnergy!" ); return "Juice: i`m fillingEnergy!" ; }
查询处理代码
1 2 3 4 5 6 7 8 9 10 11 12 13 var JuiceHandle = null ;Java .choose ("com.r0ysue.a0526printout.Juice" ,{ onMatch :function (instance ){ console .log ("found juice instance" ,instance); console .log ("juice instance call fill" ,instance.fillEnergy ()); JuiceHandle = instance; },onComplete :function ( ){ console .log ("juice handle search completed!" ) } }) console .log ("Saved juice handle :" ,JuiceHandle );var WaterHandle = Java .cast (JuiceHandle ,Java .use ("com.r0ysue.a0526printout.Water" ))console .log ("call Waterhandle still method:" ,WaterHandle .still (WaterHandle ));
Interface/Java.registerClass 1 2 3 public interface liquid { public String flow () ; }
frida 可以构建一个新的 class。 registerClass
的说明。
1 2 3 4 5 6 function registerClass (spec: ClassSpec ): Wrapper ;
首先获取要实现的 interface ,然后调用 registerClass
来实现 interface。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Java .perform (function ( ){ var liquid = Java .use ("com.r0ysue.a0526printout.liquid" ); var beer = Java .registerClass ({ name : 'com.r0ysue.a0526printout.beer' , implements : [liquid], methods : { flow : function ( ) { console .log ("look, beer is flowing!" ) return "look, beer is flowing!" ; } } }); console .log ("beer.bubble:" ,beer.$new().flow ()) })
参考链接 参考连接1:安装搭建 frida https://cloud.tencent.com/developer/article/1610276
参考连接2:安装和用法 https://eternalsakura13.com/2020/07/04/frida/
参考连接3:objection https://www.anquanke.com/post/id/197657
参考链接4:frida 打印与参数构造 https://www.wangt.cc/2020/11/frida%E6%89%93%E5%8D%B0%E4%B8%8E%E5%8F%82%E6%95%B0%E6%9E%84%E9%80%A0/
参考链接5: frida hook 常用函数分享 https://www.52pojie.cn/thread-1196917-1-1.html