Frida Android hook
From:https://eternalsakura13.com/2020/07/04/frida/
目錄
1、r0ysue 大佬
2、Frida 環境
2.1 pyenv
2.2 frida 安裝
2.3 安裝 objection
2.4 frida 使用
frida 幫助 和 frida-server 幫助
2.5 frida 開發環境搭建
3、FRIDA 基礎
3.1 frida 查看當前存在的進程
3.2 frida 打印參數和修改返回值
3.3 frida 尋找 instance,主動調用。
3.4 frida rpc
3.5 frida 動態修改
3.6 API List
4、Frida 動靜態結合分析
4.1 Objection
objection 之 啟動并注入內存
objection 之 memory 命令
memory list modules ( 查看加載的 so 庫?)
memory list exports ( 查看 so 庫的導出函數表?)
memory dump(dump 內存空間)? ? ? ?
memory search(搜索 內存空間)
objection 之 android 命令
android heap search instances 類名
android heap execute 實例ID 實例方法
android hooking list activities/services
android intent launch_activity/launch_service activity/服務
android hooking list classes
android hooking search classes display
android hooking list class_methods 類名
android hooking search methods display
hook 類的方法(hook 類里的所有方法 / 具體某個方法)
grep trick 和 文件保存( objection log )
4.2 案例學習
案例學習 1:《仿VX數據庫原型取證逆向分析》
案例學習 2:主動調用爆破密碼
5、Frida hook 基礎(一)
5.1 Frida hook : 打印參數、返回值 / 設置返回值 / 主動調用
5.2 Frida hook : 主動調用靜態/非靜態函數 以及 設置靜態/非靜態成員變量的值
5.3 Frida hook : 內部類,枚舉類的方法?并 hook,trace原型1
5.4 Frida hook : hook 動態加載的 dex,與查找 interface,
5.5 Frida hook : 枚舉 class,trace原型2
frida rpc 枚舉 實現 接口的類
5.6 Frida hook : 搜索 interface 的具體實現類
6、Frida hook 基礎(二)
6.1 spawn / attach
6.2 Frida hook : hook構造函數/打印棧回溯
6.3 Frida hook : 打印棧回溯
6.4 Frida hook : 手動加載 dex 并調用
7、Frida 打印 與 參數構造
gson 打印 Java 對象的內容
char[] / [Object Object]
byte[]
java array 構造
類的多態:轉型 / Java.cast
interface / Java.registerClass
成員內部類 / 匿名內部類
hook enum
打印 hash map
打印 non-ascii
8、Frida native hook : NDK 開發入門
9、Frida native hook : JNIEnv 和 反射
9.1 以 jni字符串 來掌握基本的 JNIEnv用法
9.2 Java 反射
10、Frida 反調試 與 反反調試
11、Frida native hook : 符號 hook JNI、art&libc
11.1 Native函數的Java Hook及主動調用
11.2 jni.h 頭文件導入
11.3 JNI 函數符號 hook
11.4 JNI 函數參數、返回值打印和替換
12、Frida native hook : JNI_Onload / 動態注冊 / inline_hook / native層調用棧打印
12.1 JNI_Onload / 動態注冊原理
12.2 Frida hook RegisterNative
12.3 native 層調用棧打印
12.4 主動調用去進行方法參數替換
12.5 inline hook ( so庫里面的函數?)
13、Frida native hook : Frida hook native app 實戰
14、Frida trace 四件套
14.1 jni trace : trace jni
14.2 strace : trace syscall
14.3 frida-trace : trace libc(or more)
art trace
14.4 hook_artmethod : trace java 函數調用
14.5 修改AOSP源碼打印
15、Frida native hook : init_array 開發和自動化逆向
15.1 init_array原理 (?so 加載、啟動、執行 )
15.2 IDA靜態分析 init_array
15.3 IDA 動態調試 so
15.4 init_array && JNI_Onload "自吐"
JNI_Onload
init_array
15.5 native層未導出函數主動調用(任意符號和地址)
16、C/C++ hook
16.1 Native/JNI層參數打印和主動調用參數構造
16.2 C/C++編成 so 并引入 Frida 調用其中的函數
1、r0ysue 大佬
這篇文章完全來源于 r0ysue 的知識星球,推薦下大佬的星球
2、Frida 環境
github 地址:https://github.com/frida/frida
2.1 pyenv
python 全版本隨機切換,這里提供?macOS上的配置方法
brew update brew install pyenv echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bash_profile下載一個3.8.2,下載真的很慢,要慢慢等 pyenv install 3.8.2pyenv versions sakura@sakuradeMacBook-Pro:~$ pyenv versionssystem * 3.8.2 (set by /Users/sakura/.python-version) 切換到我們裝的 pyenv local 3.8.2 python -V pip -V 原本系統自帶的 python local system python -V另外當你需要臨時禁用 pyenv 的時候
把這個注釋了然后另開終端就好了。關于卸載某個 python 版本
Uninstalling Python Versions As time goes on, you will accumulate Python versions in your $(pyenv root)/versions directory.To remove old Python versions, pyenv uninstall command to automate the removal process.Alternatively, simply rm -rf the directory of the version you want to remove. You can find the directory of a particular Python version with the pyenv prefix command, e.g. pyenv prefix 2.6.8.2.2 frida 安裝
如果直接按下述安裝則會直接安裝 frida 和 frida-tools 的最新版本。
pip install frida pip install frida-tools frida --version frida-ps --version也可以通過 版本號 安裝舊版本的 frida,例如 12.8.0
pyenv install 3.7.7 pyenv local 3.7.7 pip install frida==12.8.0 pip install frida-tools==5.3.0老版本 frida 和 對應關系,對應關系很好找:
2.3 安裝 objection
安裝命令:
pyenv local 3.8.2 pip install objection objection -hpyenv local 3.7.7 pip install objection==1.8.4 objection -h2.4 frida 使用
下載 frida-server 并解壓,在這里下載 frida-server :https://github.com/frida/frida/releases
先 adb shell,然后切換到 root 權限,把之前 push 進來的 frida server 改個名字叫 fs,然后運行 frida
adb push /tmp/frida-server-12.8.0-android-arm64 /data/local/tmp mv frida-server-12.8.0-android-arm64 fs chmod 777 fs ./fs如果要監聽端口,就
./fs -l 0.0.0.0:8888frida 幫助 和 frida-server 幫助
frida --help
frida-server --help
2.5 frida 開發環境搭建
? ? ? ? git clone https://github.com/oleavr/frida-agent-example.git
? ? ? ? cd frida-agent-example/
? ? ? ? npm install
下面是測試腳本
s1.js
function main() {Java.perform(function x() {console.log("sakura")}) } setImmediate(main)loader.py
import time import fridadevice8 = frida.get_device_manager().add_remote_device("192.168.0.9:8888") pid = device8.spawn("com.android.settings") device8.resume(pid) time.sleep(1) session = device8.attach(pid) with open("si.js") as f:script = session.create_script(f.read()) script.load() input() #等待輸入解釋一下:這個腳本就是先通過 frida.get_device_manager().add_remote_device 來找到 device,然后以 spawn 方式啟動 settings,然后 attach 到上面,并執行 frida 腳本。
當代碼里面沒有指定端口時,需要手動轉發端口:adb forward tcp:27042 tcp:27042
import sys import time import fridajs_code = ''' function main() {Java.perform(function x() {console.log("sakura")}) } setImmediate(main); '''def on_message(msg, data):if msg['type'] == 'send':print(f'[*] {msg["payload"]}')else:print(msg)if __name__ == '__main__':select = 1if 1 == select:# ########################### 會自動重啟 app ############################ 會自動重啟 appdevice = frida.get_remote_device()pid = device.spawn(["com.android.settings"])device.resume(pid)time.sleep(1)process = device.attach("com.android.settings")script = process.create_script(js_code)script.load()input('按任意鍵繼續') # 等待輸入elif 2 == select:# ############ 需要先手動啟動 app , 然后才能執行腳本進行 hook ############## get_remote_device 獲取遠程設備 (get_usb_device) attach 附加進程process = frida.get_remote_device().attach('com.android.settings')script = process.create_script(js_code)script.on('message', on_message) # 綁定 js 回調script.load()sys.stdin.read()pass運行結果:
3、FRIDA 基礎
3.1 frida 查看當前存在的進程
frida-ps 命令:
frida-ps -U?查看通過 usb 連接的 android 手機上的進程。可以通過 grep 過濾就可以找到我們想要的包名。
sakura@sakuradeMacBook-Pro:~$ frida-ps -UPID Name ----- ---------------------------------------------------3640 ATFWD-daemon707 adbd728 adsprpcd 26041 android.hardware.audio@2.0-service741 android.hardware.biometrics.fingerprint@3.2 frida 打印參數和修改返回值
package myapplication.example.com.frida_demo;import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.util.Log;public class MainActivity extends AppCompatActivity {private String total = "@@@###@@@";@Overrideprotected 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("sakura.string" , fun("LoWeRcAsE Me!!!!!!!!!"));}}void fun(int x , int y ){Log.d("sakura.Sum" , String.valueOf(x+y));}String fun(String x){total +=x;return x.toLowerCase();}String secret(){return total;} }注入的 js 代碼:
function main() {console.log("Enter the Script!");Java.perform(function x() {console.log("Inside Java perform");var MainActivity = Java.use("myapplication.example.com.frida_demo.MainActivity");// 重載找到指定的函數MainActivity.fun.overload('java.lang.String').implementation = function (str) {//打印參數console.log("original call : str:" + str);//修改結果var ret_value = "sakura";return ret_value;};}) } setImmediate(main);查看設備。(?-f 是通過 spawn,也就是重啟 apk 注入 js )
sakura@sakura:~$ frida-ps -U | grep frida 8738 frida-helper-32 8897 myapplication.example.com.frida_demo// -f 是通過 spawn,也就是重啟 apk 注入 js sakura@sakura:~$ frida -U -f myapplication.example.com.frida_demo -l frida_demo.js ... original call : str:LoWeRcAsE Me!!!!!!!!! 12-21 04:46:49.875 9594-9594/myapplication.example.com.frida_demo D/sakura.string: sakura3.3 frida 尋找 instance,主動調用。
function main() {console.log("Enter the Script!");Java.perform(function x() {console.log("Inside Java perform");var MainActivity = Java.use("myapplication.example.com.frida_demo.MainActivity");// overload 選擇被重載的對象MainActivity.fun.overload('java.lang.String').implementation = function (str) {//打印參數console.log("original call : str:" + str);//修改結果var ret_value = "sakura";return ret_value;};// 尋找類型為 classname 的實例Java.choose("myapplication.example.com.frida_demo.MainActivity", {onMatch: function (x) {console.log("find instance :" + x);console.log("result of secret func:" + x.secret());},onComplete: function () {console.log("end");}});}); } setImmediate(main);3.4 frida rpc
function callFun() {Java.perform(function fn() {console.log("begin");Java.choose("myapplication.example.com.frida_demo.MainActivity", {onMatch: function (x) {console.log("find instance :" + x);console.log("result of fun(string) func:" + x.fun(Java.use("java.lang.String").$new("sakura")));},onComplete: function () {console.log("end");}})}) } rpc.exports = {callfun: callFun };Python 調用:
import time import fridadevice = frida.get_usb_device() pid = device.spawn(["myapplication.example.com.frida_demo"]) device.resume(pid) time.sleep(1) session = device.attach(pid) with open("frida_demo_rpc_call.js") 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()執行:
sakura@sakura:~/frida-agent-example/agent$ python frida_demo_rpc_loader.py begin find instance :myapplication.example.com.frida_demo.MainActivity@1d4b09d result of fun(string):sakura end3.5 frida 動態修改
即將手機上的 app 的內容發送到 PC 上的 frida python 程序,然后處理后返回給 app,然后 app 再做后續的流程,核心是理解?send/recv?函數。
<TextViewandroid:id="@+id/textView"android:layout_width="wrap_content"android:layout_height="wrap_content"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" /><EditTextandroid: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" /><EditTextandroid: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" /><Buttonandroid:id="@+id/button"android:layout_width="100dp"android:layout_height="35dp"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" /> public class MainActivity extends AppCompatActivity {EditText username_et;EditText password_et;TextView message_tv;@Overrideprotected 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() {@Overridepublic void onClick(View v) {if (username_et.getText().toString().compareTo("admin") == 0) {message_tv.setText("You cannot login as admin");return;}//hook targetmessage_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));}});} }先分析問題,我的最終目標是讓 message_tv.setText 可以”發送”username為admin的base64字符串。
那肯定是 hook TextView.setText 這個函數。
Python 腳本:
import time import frida import 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 = data.decode("base64")#data = datadata = str(base64.b64decode(data))print( 'data:',data)user, pw = data.split(":")print( 'pw:',pw)#data = ("admin" + ":" + pw).encode("base64")data = str(base64.b64encode(("admin" + ":" + pw).encode()))print( "encoded data:", data)script.post({"my_data": data}) # send JSON objectprint( "Modified data sent")device = frida.get_usb_device() pid = device.spawn(["myapplication.example.com.frida_demo"]) device.resume(pid) time.sleep(1) session = device.attach(pid) with open("frida_demo2.js") as f:script = session.create_script(f.read()) script.on("message", my_message_handler) script.load() input()執行和輸出:
sakura@sakuradeMacBook-Pro:~/gitsource/frida-agent-example/agent$ python frida_demo_rpc_loader2.py Script loaded successfully {'type': 'send', 'payload': 'Sending to the server :c2FrdXJhOjEyMzQ1Ng==\n'} None Sending to the server :c2FrdXJhOjEyMzQ1Ng==message: {'type': 'send', 'payload': 'Sending to the server :c2FrdXJhOjEyMzQ1Ng==\n'} data: b'sakura:123456' pw: 123456' encoded data: b'YWRtaW46MTIzNDU2Jw==' Modified data sent string_to_recv: b'YWRtaW46MTIzNDU2Jw=='參考鏈接:https://github.com/Mind0xP/Frida-Python-Binding
3.6 API List
-
Java.choose(className: string, callbacks: Java.ChooseCallbacks): void
通過掃描 Java VM 的堆來枚舉 className類 的 live instance。 -
Java.use(className: string): Java.Wrapper<{}>
動態為 className 生成 JavaScript Wrappe r,可以通過調用$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已經執行完畢并返回
4、Frida 動靜態結合分析
4.1 Objection
- 參考這篇文章:實用FRIDA進階:內存漫游、hook anywhere、抓包:https://www.anquanke.com/post/id/197657
- objection:https://pypi.org/project/objection/
objection 之 啟動并注入內存
?命令:objection -d -g package_name exploresakura@sakura:~$ objection -d -g com.android.settings explore [debug] Agent path is: /Users/sakura/.pyenv/versions/3.7.7/lib/python3.7/site-packages/objection/agent.js [debug] Injecting agent... Using USB device `Google Pixel` [debug] Attempting to attach to process: `com.android.settings` [debug] Process attached! Agent injected and responds ok!_ _ _ ____| |_|_|___ ___| |_|_|___ ___ | . | . | | -_| _| _| | . | | |___|___| |___|___|_| |_|___|_|_||___|(object)inject(ion) v1.8.4Runtime Mobile Explorationby: @leonjza from @sensepost[tab] for command suggestions com.android.settings on (google: 8.1.0) [usb] #objection 之 memory 命令
memory list modules ( 查看加載的 so 庫?)
modules 就是指的 so 庫
查看內存中加載的 module,命令:memory list modulescom.android.settings on (google: 8.1.0) [usb] # memory list modules Save the output by adding `--json modules.json` to this command Name Base Size Path ----------------------- ------------ -------------------- ------------------------------------ app_process64 0x64ce143000 32768 (32.0 KiB) /system/bin/app_process64 libandroid_runtime.so 0x7a90bc3000 1990656 (1.9 MiB) /system/lib64/libandroid_runtime.so libbinder.so 0x7a9379f000 557056 (544.0 KiB) /system/lib64/libbinder.somemory list exports ( 查看 so 庫的導出函數表?)
查看庫的導出函數:memory list exports libssl.socom.android.settings on (google: 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 0x7c8ff006f8 function SSL_CTX_set_dos_protection_cb 0x7c8ff077b8 function SSL_SESSION_set_ex_data 0x7c8ff098f4 function SSL_CTX_set_session_psk_dhe_timeout 0x7c8ff0a754 function SSL_CTX_sess_accept 0x7c8ff063b8 function SSL_select_next_proto 0x7c8ff06a74memory dump(dump 內存空間)? ? ? ?
memory dump all 文件名 memory dump from_base 起始地址 字節數 文件memory search(搜索 內存空間)
用法:memory search "<pattern eg: 41 41 41 ?? 41>" (--string) (--offsets-only)objection 之 android 命令
android heap search instances 類名
在內存堆上搜索類的實例 ,命令:android heap search instances 類名
sakura@sakura:~$ objection -g myapplication.example.com.frida_demo explore Using USB device `Google Pixel` Agent injected and responds ok![usb] # android heap search instances myapplication.example.com.frida_demo .MainActivity Class instance enumeration complete for myapplication.example.com.frida_demo.MainActivity Handle Class toString() -------- ------------------------------------------------- --------------------------------------------------------- 0x2102 myapplication.example.com.frida_demo.MainActivity myapplication.example.com.frida_demo.MainActivity@5b1b0afandroid heap execute 實例ID 實例方法
調用實例的方法, 命令:android heap execute 實例ID 實例方法android hooking list activities/services
查看當前可用的 activity 或者 service 命令:android hooking list activities/servicescom.android.settings on (google: 8.1.0) [usb] # android hooking list services com.android.settings.SettingsDumpService com.android.settings.TetherService com.android.settings.bluetooth.BluetoothPairingServiceandroid intent launch_activity/launch_service activity/服務
直接啟動 activity 或者服務 命令:android intent launch_activity/launch_service activity/服務示例:這個命令比較有趣的是用在如果有些設計的不好,可能就直接繞過了密碼鎖屏等直接進去。 android intent launch_activity com.android.settings.DisplaySettingsandroid hooking list classes
列出內存中所有的類 :android hooking list classesandroid hooking search classes display
在內存中所有已加載的類中搜索包含特定關鍵詞的類 命令:android hooking search classes display //搜索類中包含 display 的類com.android.settings on (google: 8.1.0) [usb] # android hooking search classes display [Landroid.icu.text.DisplayContext$Type; [Landroid.icu.text.DisplayContext; [Landroid.view.Display$Mode; android.hardware.display.DisplayManager android.hardware.display.DisplayManager$DisplayListener android.hardware.display.DisplayManagerGlobalandroid hooking list class_methods 類名
內存中搜索指定類的所有方法 命令:android hooking list class_methods 類名com.android.settings on (google: 8.1.0) [usb] # android hooking list class_methods java.nio.charset.Charset private static java.nio.charset.Charset java.nio.charset.Charset.lookup(java.lang.String) private static java.nio.charset.Charset java.nio.charset.Charset.lookup2(java.lang.String) private static java.nio.charset.Charset java.nio.charset.Charset.lookupViaProviders(java.lang.String)android hooking search methods display
在內存中所有已加載的類的方法中搜索包含特定關鍵詞的方法
命令:?android hooking search methods display
com.android.settings on (google: 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 5529 classes, searching methods (this may take some time)... android.app.ActionBar.getDisplayOptions android.app.ActionBar.setDefaultDisplayHomeAsUpEnabled android.app.ActionBar.setDisplayHomeAsUpEnabledhook 類的方法(hook 類里的所有方法 / 具體某個方法)
- android hooking watch class 類名
? ? 這樣就可以 hook 這個類里面的所有方法,每次調用都會被 log 出來。 - android hooking watch class 類名 --dump-args --dump-backtrace --dump-return
? ? 在上面的基礎上,額外 dump 參數,棧回溯,返回值
? ? 示例:android hooking watch class xxx.MainActivity --dump-args --dump-backtrace --dump-return - android hooking watch class_method 方法名
? ? ? ? // 可以直接 hook 到所有重載
? ? ? ? android hooking watch class_method xxx.MainActivity.fun --dump-args --dump-backtrace --dump-return
grep trick 和 文件保存( objection log )
objection log 默認是不能用 grep 過濾的,但是可以通過 objection run xxx | grep yyy 的方式,從終端通過管道來過濾。用法如下
sakura@sakura:~$ objection -g com.android.settings run memory list modules | grep libc Warning: Output is not to a terminal (fd=1). libcutils.so 0x7a94a1c000 81920 (80.0 KiB) /system/lib64/libcutils.so libc++.so 0x7a9114e000 983040 (960.0 KiB) /system/lib64/libc++.so libc.so 0x7a9249d000 892928 (872.0 KiB) /system/lib64/libc.so libcrypto.so 0x7a92283000 1155072 (1.1 MiB) /system/lib64/libcrypto.so有的命令后面可以通過?--json logfile?來直接保存結果到文件里。
有的可以通過查看 .objection (位置:C:\Users\用戶名\.objection )文件里的輸出 log 來查看結果。
sakura@sakurade:~/.objection$ cat *log | grep -i display android.hardware.display.DisplayManager android.hardware.display.DisplayManager$DisplayListener android.hardware.display.DisplayManagerGlobal4.2 案例學習
案例學習 1:《仿VX數據庫原型取證逆向分析》
案例學習case1:《仿VX數據庫原型取證逆向分析》
附件鏈接?:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1082706
android-backup-extractor工具鏈接:https://github.com/nelenkov/android-backup-extractor
sakura@sakurade:~/Desktop/frida_learn$ java -version java version "1.8.0_141"sakura@sakuradeMacBook-Pro:~/Desktop/frida_learn$ java -jar abe-all.jar unpack 1.ab 1.tar 0% 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% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99% 100% 9097216 bytes written to 1.tar.... sakura@sakurade:~/Desktop/frida_learn/apps/com.example.yaphetshan.tencentwelcome$ ls Encryto.db _manifest a db裝個夜神模擬器玩
sakura@sakura:/Applications/NoxAppPlayer.app/Contents/MacOS$ ./adb connect 127.0.0.1:62001 * daemon not running. starting it now on port 5037 * adb E 5139 141210 usb_osx.cpp:138] Unable to create an interface plug-in (e00002be) * daemon started successfully * connected to 127.0.0.1:62001 sakura@sakuradeMacBook-Pro:/Applications/NoxAppPlayer.app/Contents/MacOS$ ./adb shell dream2qltechn:/ # whoami root dream2qltechn:/ # uname -a Linux localhost 4.0.9+ #222 SMP PREEMPT Sat Mar 14 18:24:36 HKT 2020 i686肯定還是先定位目標字符串?Wait a Minute,What was happend?
jadx 搜索字符串
重點在 a() 代碼里,其實是根據明文的 name 和 password,然后?aVar.a(a2 + aVar.b(a2, contentValues.getAsString("password"))).substring(0, 7)?再做一遍復雜的計算并截取7位當做密碼,傳入 getWritableDatabase 去解密 demo.db 數據庫。
所以我們 hook一下 getWritableDatabase 即可。
// 首先查看要注入的進程 frida-ps -U ... 5662 com.example.yaphetshan.tencentwelcome ...// 使用 objection 注入 objection -d -g com.example.yaphetshan.tencentwelcome explore看一下源碼
package net.sqlcipher.database; ... public abstract class SQLiteOpenHelper {...public synchronized SQLiteDatabase getWritableDatabase(char[] cArr) {也可以 objection search 一下這個 method
(samsung: 7.1.2) [usb] # android hooking search methods getWritableDatabase Warning, searching all classes may take some time and in some cases, crash the target application. Continue? [y/N]: y Found 4650 classes, searching methods (this may take some time)...android.database.sqlite.SQLiteOpenHelper.getWritableDatabase ... net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabasehook 一下這個 method
[usb] # android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return - [incoming message] ------------------ {"payload": "Attempting to watch class \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m and method \u001b[32mgetWritableDatabase\u001b[39m.","type": "send" } - [./incoming message] ---------------- (agent) Attempting to watch class net.sqlcipher.database.SQLiteOpenHelper and method getWritableDatabase. - [incoming message] ------------------ {"payload": "Hooking \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m.\u001b[92mgetWritableDatabase\u001b[39m(\u001b[31mjava.lang.String\u001b[39m)","type": "send" } - [./incoming message] ---------------- (agent) Hooking net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(java.lang.String) - [incoming message] ------------------ {"payload": "Hooking \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m.\u001b[92mgetWritableDatabase\u001b[39m(\u001b[31m[C\u001b[39m)","type": "send" } - [./incoming message] ---------------- (agent) Hooking net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase([C) - [incoming message] ------------------ {"payload": "Registering job \u001b[94mjytq1qeyllq\u001b[39m. Type: \u001b[92mwatch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase\u001b[39m","type": "send" } - [./incoming message] ---------------- (agent) Registering job jytq1qeyllq. Type: watch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase ...mple.yaphetshan.tencentwelcome on (samsung: 7.1.2) [usb] #hook 好之后再打開這個 apk
(agent) [1v488x28gcs] Called net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(java.lang.String) ... (agent) [1v488x28gcs] Backtrace:net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(Native Method)com.example.yaphetshan.tencentwelcome.MainActivity.a(MainActivity.java:55)com.example.yaphetshan.tencentwelcome.MainActivity.onCreate(MainActivity.java:42)android.app.Activity.performCreate(Activity.java:6692) ... (agent) [1v488x28gcs] Arguments net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(ae56f99)... ...mple.yaphetshan.tencentwelcome on (samsung: 7.1.2) [usb] # jobs list Job ID Hooks Type ----------- ------- ----------------------------------------------------------------------------- 1v488x28gcs 2 watch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase找到參數?ae56f99
剩下的就是用這個密碼去打開加密的 db。
然后base64解密一下就好了。
還有一種策略是主動調用,即自己去調用 a 函數以觸發 getWritableDatabase 的數據庫解密。先尋找 a 所在類的實例,然后 hook getWritableDatabase,最終主動調用 a。這里幸運的是 a 沒有什么奇奇怪怪的參數需要我們傳入。
[usb] # android heap search instances com.example.yaphetshan.tencentwelcome.MainActivity Class instance enumeration complete for com.example.yaphetshan.tencentwelcome.MainActivity Handle Class toString() -------- -------------------------------------------------- ---------------------------------------------------------- 0x20078a com.example.yaphetshan.tencentwelcome.MainActivity com.example.yaphetshan.tencentwelcome.MainActivity@1528f80[usb] # android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return [usb] # android heap execute 0x20078a a (agent) [taupgwkum4h] Arguments net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(ae56f99)案例學習 2:主動調用爆破密碼
附件鏈接:https://bbs.pediy.com/thread-257745.htm
因為直接找?Unfortunately,note the right PIN :(找不到,可能是把字符串藏在什么資源文件里了。
review 代碼之后找到校驗的核心函數,邏輯就是將 input 編碼一下之后和密碼比較,這肯定是什么不可逆的加密。
這里就爆破一下密碼。
// 查看 app 進程 frida-ps -U | grep qualification 7660 org.teamsik.ahe17.qualification.easy// frida 命令執行 js 進行 hook frida -U -f org.teamsik.ahe17.qualification.easy -l force.js# 上面命令執行成功后,會進入 frida,再輸入 %resume 然后回車,即可讓程序繼續執行或者使用 -F 大寫F 參數,frida -U -F?org.teamsik.ahe17.qualification.easy -l force.js
js 腳本:
function main() {Java.perform(function x() {console.log("In Java perform")var verify = Java.use("org.teamsik.ahe17.qualification.Verifier")var stringClass = Java.use("java.lang.String")var p = stringClass.$new("09042ec2c2c08c4cbece042681caf1d13984f24a")var pSign = p.getBytes()// var pStr = stringClass.$new(pSign)// console.log(parseInt(pStr))for (var i = 999; i < 10000; i++){var v = stringClass.$new(String(i))var vSign = verify.encodePassword(v)if (parseInt(stringClass.$new(pSign)) == parseInt(stringClass.$new(vSign))) {console.log("yes: " + v)break}console.log("not :" + v)}}) } setImmediate(main)?
?...
not :9080
not :9081
not :9082
yes: 9083
這里注意 parseInt
5、Frida hook 基礎(一)
- 調用 靜態函數 和 非靜態函數
- 設置 (同名)成員變量
- 內部類,枚舉類的函數并hook,trace原型1
- 查找接口,hook動態加載dex
- 枚舉class,trace原型2
- objection 不能切換 classloader
5.1 Frida hook : 打印參數、返回值 / 設置返回值 / 主動調用
demo 就不貼了,還是先定位登錄失敗點,然后搜索字符串。
public class LoginActivity extends AppCompatActivity {/* access modifiers changed from: private */public Context mContext;public void onCreate(Bundle bundle) {super.onCreate(bundle);this.mContext = this;setContentView((int) R.layout.activity_login);final EditText editText = (EditText) findViewById(R.id.username);final EditText editText2 = (EditText) findViewById(R.id.password);((Button) findViewById(R.id.login)).setOnClickListener(new View.OnClickListener() {public void onClick(View view) {String obj = editText.getText().toString();String obj2 = editText2.getText().toString();if (TextUtils.isEmpty(obj) || TextUtils.isEmpty(obj2)) {Toast.makeText(LoginActivity.this.mContext, "username or password is empty.", 1).show();} else if (LoginActivity.a(obj, obj).equals(obj2)) {LoginActivity.this.startActivity(new Intent(LoginActivity.this.mContext, FridaActivity1.class));LoginActivity.this.finishActivity(0);} else {Toast.makeText(LoginActivity.this.mContext, "Login failed.", 1).show();}}});}LoginActivity.a(obj, obj).equals(obj2)?分析之后可得 obj2 來自 password,由從 username 得來的 obj,經過 a 函數運算之后得到一個值,這兩個值相等則登錄成功。所以這里關鍵是 hook a 函數的參數,最簡腳本如下。
//打印參數、返回值 function Login(){Java.perform(function(){Java.use("com.example.androiddemo.Activity.LoginActivity").a.overload('java.lang.String', 'java.lang.String').implementation = function (str, str2){var result = this.a(str, str2);console.log("args0:" + str + " args1:" + str2 + " result:" + result);return result;}}) } setImmediate(Login)觀察輸入和輸出,這里也可以直接主動調用。
function login() {Java.perform(function () {console.log("start")var login = Java.use("com.example.androiddemo.Activity.LoginActivity")var result = login.a("1234","1234")console.log(result)}) } setImmediate(login)輸出:
... start 4e4feaea959d426155a480dc07ef92f4754ee93edbe56d993d74f131497e66fb 然后 adb shell input text "4e4feaea959d426155a480dc07ef92f4754ee93edbe56d993d74f131497e66fb"接下來是第一關
public abstract class BaseFridaActivity extends AppCompatActivity implements View.OnClickListener {public Button mNextCheck;public void CheckSuccess() {}public abstract String getNextCheckTitle();public abstract void onCheck();/* access modifiers changed from: protected */public void onCreate(Bundle bundle) {super.onCreate(bundle);setContentView((int) R.layout.activity_frida);this.mNextCheck = (Button) findViewById(R.id.next_check);this.mNextCheck.setOnClickListener(this);Button button = this.mNextCheck;button.setText(getNextCheckTitle() + ",點擊進入下一關");}public void onClick(View view) {onCheck();}public void CheckFailed() {Toast.makeText(this, "Check Failed!", 1).show();} } ...public class FridaActivity1 extends BaseFridaActivity {private static final char[] table = {'L', 'K', 'N', 'M', 'O', 'Q', 'P', 'R', 'S', 'A', 'T', 'B', 'C', 'E', 'D', 'F', 'G', 'H', 'I', 'J', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'o', 'd', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'e', 'f', 'g', 'h', 'j', 'i', 'k', 'l', 'm', 'n', 'y', 'z', '0', '1', '2', '3', '4', '6', '5', '7', '8', '9', '+', '/'};public String getNextCheckTitle() {return "當前第1關";}public void onCheck() {try {if (a(b("請輸入密碼:")).equals("R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=")) {CheckSuccess();startActivity(new Intent(this, FridaActivity2.class));finishActivity(0);return;}super.CheckFailed();} catch (Exception e) {e.printStackTrace();}}public static String a(byte[] bArr) throws Exception {StringBuilder sb = new StringBuilder();for (int i = 0; i <= bArr.length - 1; i += 3) {byte[] bArr2 = new byte[4];byte b = 0;for (int i2 = 0; i2 <= 2; i2++) {int i3 = i + i2;if (i3 <= bArr.length - 1) {bArr2[i2] = (byte) (b | ((bArr[i3] & 255) >>> ((i2 * 2) + 2)));b = (byte) ((((bArr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255) >>> 2);} else {bArr2[i2] = b;b = 64;}}bArr2[3] = b;for (int i4 = 0; i4 <= 3; i4++) {if (bArr2[i4] <= 63) {sb.append(table[bArr2[i4]]);} else {sb.append('=');}}}return sb.toString();}public static byte[] b(String str) {try {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();GZIPOutputStream gZIPOutputStream = new GZIPOutputStream(byteArrayOutputStream);gZIPOutputStream.write(str.getBytes());gZIPOutputStream.finish();gZIPOutputStream.close();byte[] byteArray = byteArrayOutputStream.toByteArray();try {byteArrayOutputStream.close();return byteArray;} catch (Exception e) {e.printStackTrace();return byteArray;}} catch (Exception unused) {return null;}} }關鍵函數在??a(b("請輸入密碼:")).equals("R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=")
這里應該直接 hook a,讓其返回值為?R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=就可以進入下一關了。
function ch1() {Java.perform(function () {console.log("start")Java.use("com.example.androiddemo.Activity.FridaActivity1").a.implementation = function (x) {return "R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL="}}) }上面的代碼 hook 了 類中的靜態函數。在寫 hook 的 js 代碼時,靜態 和 非靜態 區別:
- hook 靜態函數時候,不需要 overload
- hook 類的成員函數時,需要 overload
上面代碼是 hook 并 實現函數,然后調用,
下面代碼是?hook 但 不實現函數,只調用
5.2 Frida hook : 主動調用靜態/非靜態函數 以及 設置靜態/非靜態成員變量的值
總結:
- 靜態函數直接 use class 然后調用方法,非靜態函數需要先 choose 實例然后調用
- 設置成員變量的值,寫法是?xx.value = yy,其他方面和函數一樣。
- 如果有一個成員變量和成員函數的名字相同,則在其前面加一個_,如?_xx.value = yy
然后是第二關
public class FridaActivity2 extends BaseFridaActivity {private static boolean static_bool_var = false;private boolean bool_var = false;public String getNextCheckTitle() {return "當前第2關";}private static void setStatic_bool_var() {static_bool_var = true;}private void setBool_var() {this.bool_var = true;}public void onCheck() {if (!static_bool_var || !this.bool_var) {super.CheckFailed();return;}CheckSuccess();startActivity(new Intent(this, FridaActivity3.class));finishActivity(0);} }這一關的關鍵在于下面的 if 判斷要為 false,則?static_bool_var?和?this.bool_var?都要為 true。
if (!static_bool_var || !this.bool_var) {super.CheckFailed();return; }這樣就要調用?setBool_var?和?setStatic_bool_var?兩個函數了。
function ch2() {Java.perform(function () {console.log("start")var FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2")// hook 靜態函數 直接調用FridaActivity2.setStatic_bool_var()// hook 動態函數,找到 instance 實例,從 實例中 調用函數方法Java.choose("com.example.androiddemo.Activity.FridaActivity2", {onMatch: function (instance) {instance.setBool_var()},onComplete: function () {console.log("end")}})}) } setImmediate(ch2)接下來是第三關
public class FridaActivity3 extends BaseFridaActivity {private static boolean static_bool_var = false;private boolean bool_var = false;private boolean same_name_bool_var = false;public String getNextCheckTitle() {return "當前第3關";}private void same_name_bool_var() {Log.d("Frida", static_bool_var + " " + this.bool_var + " " + this.same_name_bool_var);}public void onCheck() {if (!static_bool_var || !this.bool_var || !this.same_name_bool_var) {super.CheckFailed();return;}CheckSuccess();startActivity(new Intent(this, FridaActivity4.class));finishActivity(0);} }關鍵是讓?if (!static_bool_var || !this.bool_var || !this.same_name_bool_var)為 false,則三個變量都要為 true
function ch3() {Java.perform(function () {console.log("start")var FridaActivity3 = Java.use("com.example.androiddemo.Activity.FridaActivity3")FridaActivity3.static_bool_var.value = trueJava.choose("com.example.androiddemo.Activity.FridaActivity3", {onMatch: function (instance) {instance.bool_var.value = trueinstance._same_name_bool_var.value = true},onComplete: function () {console.log("end")}})}) }注意:類里有一個 成員函數 和 成員變量 都叫做??same_name_bool_var?,這種時候在成員變量前加一個?_,修改值的形式為??xx.value = yy
5.3 Frida hook : 內部類,枚舉類的方法?并 hook,trace原型1
總結:
- 對于內部類,通過?類名$內部類名?去 use 或者 choose
- 對 use 得到的 clazz 應用反射,如?clazz.class.getDeclaredMethods()?可以得到 類里面聲明的所有方法,即 可以枚舉類里面的所有函數。
接下來是第四關
public class FridaActivity4 extends BaseFridaActivity {public String getNextCheckTitle() {return "當前第4關";}private static class InnerClasses {public static boolean check1() { return false;}public static boolean check2() { return false;}public static boolean check3() { return false;}public static boolean check4() { return false;}public static boolean check5() { return false;}public static boolean check6() { return false;}private InnerClasses() {}}public void onCheck() {if (!InnerClasses.check1() || !InnerClasses.check2() || !InnerClasses.check3() || !InnerClasses.check4() || !InnerClasses.check5() || !InnerClasses.check6()){super.CheckFailed();return;}CheckSuccess();startActivity(new Intent(this, FridaActivity5.class));finishActivity(0);} }這一關的關鍵是讓?if (!InnerClasses.check1() || !InnerClasses.check2() || !InnerClasses.check3() || !InnerClasses.check4() || !InnerClasses.check5() || !InnerClasses.check6())?中的所有 check 全部返回 true。
其實這里唯一的問題就是尋找內部類?InnerClasses,對于內部類的 hook,通過 類名$內部類名 去 use。
function ch4() {Java.perform(function () {var InnerClasses = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses")console.log("start")InnerClasses.check1.implementation = function () { return true }InnerClasses.check2.implementation = function () { return true }InnerClasses.check3.implementation = function () { return true }InnerClasses.check4.implementation = function () { return true }InnerClasses.check5.implementation = function () { return true }InnerClasses.check6.implementation = function () { return true }}) }利用反射,獲取類中的所有 method 聲明,然后字符串拼接去獲取到方法名,例如下面的 check1,然后就可以批量 hook,而不用像我上面那樣一個一個寫。
var inner_classes = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses") var all_methods = inner_classes.class.getDeclaredMethods();... public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check1(), public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check2(), public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check3(), public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check4(), public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check5(), public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check6()hook 類方法 的 所有 重載
方法 1:
//目標類 var hook = Java.use(targetClass); //重載次數 var overloadCount = hook[targetMethod].overloads.length; //打印日志:追蹤的方法有多少個重載 console.log("Tracing " + targetClassMethod + " [" + overloadCount + " overload(s)]"); //每個重載都進入一次 for (var i = 0; i < overloadCount; i++) { //hook每一個重載hook[targetMethod].overloads[i].implementation = function() {console.warn("\n*** entered " + targetClassMethod);//可以打印每個重載的調用棧,對調試有巨大的幫助,當然,信息也很多,盡量不要打印,除非分析陷入僵局Java.perform(function() {var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());console.log("\nBacktrace:\n" + bt);}); // 打印參數if (arguments.length) console.log();for (var j = 0; j < arguments.length; j++) {console.log("arg[" + j + "]: " + arguments[j]);}//打印返回值var retval = this[targetMethod].apply(this, arguments); // rare crash (Frida bug?)console.log("\nretval: " + retval);console.warn("\n*** exiting " + targetClassMethod);return retval;} }方法 2:
function hookOneClassAllMethod(clsName) {console.log("開始 hook 一個類的所有方法");// var NetContent = Java.use("com.xbiao.utils.net.NetContent");var clazz = Java.use(clsName); // 得到 classvar all_method = clazz.class.getDeclaredMethods(); // 得到類的所有方法all_method.forEach(function (mth) { // 遍歷類的所有方法var mthName = mth.getName(); // 得到 方法名var all_overload = clazz[mthName].overloads;all_overload.forEach(function (olad) {// hook 重載 argumentTypesolad.implementation = function () {console.log("\r")// printStack()for (var i = 0; i < arguments.length; i++) {var arg_type = olad.argumentTypes[i].className;var arg_val = arguments[i];var arg_fmt = JSON.stringify(arg_val);var msg = mthName + " ---> " + "arg[" + i + ":"+ arg_type +"]:" + arg_fmt;console.log(msg);}var retVal = this[mthName].apply(this, arguments);console.log(mthName + " ---> 返回值:" + retVal);return retVal;}})}) } function main() {Java.perform(() => {Java.enumeratteClassLoaders({onMatch: function(loader){console.log("ClassLoader start");try {if(loader.findClass("com.xbiao.utils.AESdedUtil")){console.log("Successfully found loader")console.log(loader);Java.classFactory.loader = loader ;hookOneClassAllMethod("com.xbiao.utils.net.NetContent")}}catch(error){console.log("find error:" + error)}},onComplete: function (){console.log("ClassLoader end");}});}); } setImmediate(main)執行結果:?
5.4 Frida hook : hook 動態加載的 dex,與查找 interface,
總結:
- 通過?enumerateClassLoaders?來枚舉加載進內存的 classloader,
- 再?loader.findClass(xxx)?尋找是否包括我們想要的 interface 的實現類,
- 最后通過?Java.classFactory.loader = loader?來切換 classloader,從而加載該實現類。
第五關比較有趣,它的 check 函數是動態加載進來的。
java 里有 interface 的概念,是指一系列抽象的接口,需要類來實現。
package com.example.androiddemo.Dynamic;public interface CheckInterface { boolean check(); } ...public class DynamicCheck implements CheckInterface {public boolean check() { return false; } } ... public class FridaActivity5 extends BaseFridaActivity {private CheckInterface DynamicDexCheck = null;...public CheckInterface getDynamicDexCheck() {if (this.DynamicDexCheck == null) {loaddex();}return this.DynamicDexCheck;}/* access modifiers changed from: protected */public void onCreate(Bundle bundle) {super.onCreate(bundle);loaddex();// this.DynamicDexCheck = (CheckInterface) new DexClassLoader(// str, filesDir.getAbsolutePath(), (String) null, getClassLoader()// ).loadClass("com.example.androiddemo.Dynamic.DynamicCheck").newInstance();}public void onCheck() {if (getDynamicDexCheck() == null) {Toast.makeText(this, "onClick loaddex Failed!", 1).show();} else if (getDynamicDexCheck().check()) {CheckSuccess();startActivity(new Intent(this, FridaActivity6.class));finishActivity(0);} else {super.CheckFailed();}} }這里有個 loaddex 其實就是先從資源文件加載 classloader 到內存里,再 loadClass DynamicCheck,創建出一個實例,最終調用這個實例的 check。
所以現在我們就要先枚舉 class loader,找到能實例化我們要的 class 的那個 class loader,然后把它設置成 Java 的默認 class factory 的 loader。
現在就可以用這個 class loader 來使用 .use 去 import 一個給定的類。
function ch5() {Java.perform(function () {// Java.choose("com.example.androiddemo.Activity.FridaActivity5",{// onMatch:function(x){// console.log(x.getDynamicDexCheck().$className)// },onComplete:function(){}// })console.log("start")Java.enumerateClassLoaders({onMatch: function (loader) {try {if(loader.findClass("com.example.androiddemo.Dynamic.DynamicCheck")){console.log("Successfully found loader")console.log(loader);Java.classFactory.loader = loader ;}}catch(error){console.log("find error:" + error)}},onComplete: function () {console.log("end1")}})Java.use("com.example.androiddemo.Dynamic.DynamicCheck").check.implementation = function () {return true}console.log("end2")}) } setImmediate(ch5)todo有一個疑問:https://github.com/frida/frida/issues/1049
5.5 Frida hook : 枚舉 class,trace原型2
總結: 通過?Java.enumerateLoadedClasses?來 枚舉類,然后?name.indexOf(str)?過濾一下并 hook。
接下來是第六關
import com.example.androiddemo.Activity.Frida6.Frida6Class0; import com.example.androiddemo.Activity.Frida6.Frida6Class1; import com.example.androiddemo.Activity.Frida6.Frida6Class2;public class FridaActivity6 extends BaseFridaActivity {public String getNextCheckTitle() {return "當前第6關";}public void onCheck() {if (!Frida6Class0.check() || !Frida6Class1.check() || !Frida6Class2.check()) {super.CheckFailed();return;}CheckSuccess();startActivity(new Intent(this, FridaActivity7.class));finishActivity(0);} }這關是 import 了一些類,然后 調用類里的靜態方法,所以我們枚舉所有的類,然后過濾一下,并把過濾出來的結果 hook 上,改掉其返回值。
function ch6() {Java.perform(function () {Java.enumerateLoadedClasses({onMatch: function (class_name, handle){if (class_name.indexOf("com.example.androiddemo.Activity.Frida6") != -1) {console.log("class_name:" + class_name + " handle:" + handle)Java.use(class_name).check.implementation = function () {return true}}},onComplete: function () {console.log("end")}})}) }frida rpc 枚舉 實現 接口的類
import sys import frida''' frida rpc 枚舉類 '''def on_message(message, data):if message['type'] == 'send':print("[*] {message['payload']}")else:print(message)hook = """ Java.perform(function(){Java.enumerateLoadedClasses({"onMatch" : function(classname){if(classname.indexOf("com.csair.mbp") < 0){return;}// 實現類 implementstry{var hookCls = Java.use(classname)var interFaces = hookCls.class.getInterfaces();if (interFaces.length > 0) {console.log(classname)for (var i in interFaces) {// 接口類 interFacesconsole.log("\t", interFaces[i].toString())}} }catch(e){console.log(e)}},"onComplete" : function(){}}) }) """process = frida.get_usb_device().attach('com.csair.mbp') script = process.create_script(hook) script.on('message', on_message) print('[*] Running CTF') script.load() sys.stdin.read()5.6 Frida hook : 搜索 interface 的具體實現類
利用反射得到類里面實現的 interface 數組,并打印出來。
function more() {Java.perform(function () {Java.enumerateLoadedClasses({onMatch: function (class_name){if (class_name.indexOf("com.example.androiddemo") < 0) {return}else {var hook_cls = Java.use(class_name)var interfaces = hook_cls.class.getInterfaces()if (interfaces.length > 0) {console.log(class_name + ": ")for (var i in interfaces) {console.log("\t", interfaces[i].toString())}}}},onComplete: function () {console.log("end")}})}) }6、Frida hook 基礎(二)
- spawn / attach
- 各種主動調用
- hook函數 和 hook構造函數
- 調用棧? /簡單腳本
- 動態加載自己的 dex
題目下載地址:https://github.com/tlamb96/kgb_messenger
6.1 spawn / attach
firda 的 -f 參數代表 span 啟動:frida -U -f com.tlamb96.spetsnazmessenger -l frida_russian.js --no-pause
上面命令執行完后,會進入 frida ,然后在輸入 %resume 恢復程序運行
/* access modifiers changed from: protected */public void onCreate(Bundle bundle) {super.onCreate(bundle);setContentView((int) R.layout.activity_main);String property = System.getProperty("user.home");String str = System.getenv("USER");if (property == null || property.isEmpty() || !property.equals("Russia")) {a("Integrity Error", "This app can only run on Russian devices.");} else if (str == null || str.isEmpty() || !str.equals(getResources().getString(R.string.User))) {a("Integrity Error", "Must be on the user whitelist.");} else {a.a(this);startActivity(new Intent(this, LoginActivity.class));}} }這個題目比較簡單,但是因為這個 check 是在?onCreate?里,所以 app 剛啟動就自動檢查,所以這里需要用 spawn 的方式去啟動 frida 腳本 hook,而不是 attach。
這里有兩個檢查,一個是檢查 property 的值,一個是檢查 str 的值。分別從?System.getProperty?和?System.getenv?里獲取,hook 住這兩個函數就行。
這里要注意從資源文件里找到 User 的值。
frida_russian.js?
function main() {Java.perform(function () {Java.use("java.lang.System").getProperty.overload('java.lang.String').implementation = function (str) {return "Russia";}Java.use("java.lang.System").getenv.overload('java.lang.String').implementation = function(str){return "RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==";}}) } setImmediate(main)運行結果截圖:
接下來進入到 login 功能
public void onLogin(View view) {EditText editText = (EditText) findViewById(R.id.login_username);EditText editText2 = (EditText) findViewById(R.id.login_password);this.n = editText.getText().toString();this.o = editText2.getText().toString();if (this.n != null && this.o != null && !this.n.isEmpty() && !this.o.isEmpty()) {if (!this.n.equals(getResources().getString(R.string.username))) {Toast.makeText(this, "User not recognized.", 0).show();editText.setText("");editText2.setText("");} else if (!j()) {Toast.makeText(this, "Incorrect password.", 0).show();editText.setText("");editText2.setText("");} else {i();startActivity(new Intent(this, MessengerActivity.class));}}} ...private boolean j() {String str = "";for (byte b : this.m.digest(this.o.getBytes())) {str = str + String.format("%x", new Object[]{Byte.valueOf(b)});}return str.equals(getResources().getString(R.string.password));} ...private void i() {char[] cArr = {'(', 'W', 'D', ')', 'T', 'P', ':', '#', '?', 'T'};cArr[0] = (char) (cArr[0] ^ this.n.charAt(1));cArr[1] = (char) (cArr[1] ^ this.o.charAt(0));cArr[2] = (char) (cArr[2] ^ this.o.charAt(4));cArr[3] = (char) (cArr[3] ^ this.n.charAt(4));cArr[4] = (char) (cArr[4] ^ this.n.charAt(7));cArr[5] = (char) (cArr[5] ^ this.n.charAt(0));cArr[6] = (char) (cArr[6] ^ this.o.charAt(2));cArr[7] = (char) (cArr[7] ^ this.o.charAt(3));cArr[8] = (char) (cArr[8] ^ this.n.charAt(6));cArr[9] = (char) (cArr[9] ^ this.n.charAt(8));Toast.makeText(this, "FLAG{" + new String(cArr) + "}", 1).show();}從資源文件里找到 username,密碼則是要算一個 j() 函數,要讓它返回 true,順便打印一下 i 函數 toast 到界面的 flag。
( github 代碼中 不是 j() 函數,而是?checkPassword() 函數?)
var clazz = Java.use("com.tlamb96.kgbmessenger.LoginActivity") clazz.j.implementation = function (){return true} ...var clazz = Java.use("android.widget.Toast") clazz.makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function (x, y, z) {var flag = Java.use("java.lang.String").$new(y)console.log(flag) } ... [Google Pixel::com.tlamb96.spetsnazmessenger]-> FLAG{G&qG13 R0}代碼:
Java.perform(function () {Java.use("com.tlamb96.kgbmessenger.LoginActivity").checkPassword.implementation = function () {return true};Java.use("android.widget.Toast").makeText.overload('android.content.Context', 'java.lang.CharSequence', 'int').implementation = function (x, y, z) {var flag = Java.use("java.lang.String").$new(y);console.log(flag);return this.makeText(x, y, z);} })執行結果:
6.2 Frida hook : hook構造函數/打印棧回溯
總結:hook 構造函數實現 是 通過use取得類,然后clazz.$init.implementation = callback?hook 構造函數。
我們先學習一下怎么 hook 構造函數。
add(new com.tlamb96.kgbmessenger.b.a(R.string.katya, "Archer, you up?", "2:20 am", true)); ... package com.tlamb96.kgbmessenger.b; public class a { ...public a(int i, String str, String str2, boolean z) {this.f448a = i;this.b = str;this.c = str2;this.d = z;} ... }用?$init?來 hook 構造函數
function printstack() {console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); }Java.use("com.tlamb96.kgbmessenger.b.a").$init.implementation = function (i, str1, str2, z) {this.$init(i, str1, str2, z)console.log(i, str1, str2, z)printStack("com.tlamb96.kgbmessenger.b.a") }打印堆棧:
function printstack() {console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); }6.3 Frida hook : 打印棧回溯
打印棧回溯
function printStack(name) {Java.perform(function () {var Exception = Java.use("java.lang.Exception");var instanceTemp = Exception.$new("Exception");var straces = instanceTemp.getStackTrace();if (straces != undefined && straces != null) {var strace = straces.toString();var replaceStr = strace.replace(/,/g, "\\n");console.log("=============================" + name + " Stack strat=======================");console.log(replaceStr);console.log("=============================" + name + " Stack end=======================\r\n");Exception.$dispose();}}); }輸出就是這樣
[Google Pixel::com.tlamb96.spetsnazmessenger]-> 2131558449 111 02:27 下午 false =============================com.tlamb96.kgbmessenger.b.a Stack strat======================= com.tlamb96.kgbmessenger.b.a.<init>(Native Method) com.tlamb96.kgbmessenger.MessengerActivity.onSendMessage(Unknown Source:40) java.lang.reflect.Method.invoke(Native Method) android.support.v7.app.m$a.onClick(Unknown Source:25) android.view.View.performClick(View.java:6294) android.view.View$PerformClick.run(View.java:24770) android.os.Handler.handleCallback(Handler.java:790) android.os.Handler.dispatchMessage(Handler.java:99) android.os.Looper.loop(Looper.java:164) android.app.ActivityThread.main(ActivityThread.java:6494) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) =============================com.tlamb96.kgbmessenger.b.a Stack end=======================6.4 Frida hook : 手動加載 dex 并調用
總結:編譯出 dex 后,通過 Java.openClassFile("xxx.dex").load() 加載,就可以正常通過 Java.use 調用里面的方法了。
現在我們來繼續解決這個問題。
public void onSendMessage(View view) {EditText editText = (EditText) findViewById(R.id.edittext_chatbox);String obj = editText.getText().toString();if (!TextUtils.isEmpty(obj)) {this.o.add(new com.tlamb96.kgbmessenger.b.a(R.string.user, obj, j(), false));this.n.c();if (a(obj.toString()).equals(this.p)) {Log.d("MessengerActivity", "Successfully asked Boris for the password.");this.q = obj.toString();this.o.add(new com.tlamb96.kgbmessenger.b.a(R.string.boris, "Only if you ask nicely", j(), true));this.n.c();}if (b(obj.toString()).equals(this.r)) {Log.d("MessengerActivity", "Successfully asked Boris nicely for the password.");this.s = obj.toString();this.o.add(new com.tlamb96.kgbmessenger.b.a(R.string.boris, "Wow, no one has ever been so nice to me! Here you go friend: FLAG{" + i() + "}", j(), true));this.n.c();}this.m.b(this.m.getAdapter().a() - 1);editText.setText("");} }新的一關是一個聊天框。分析一下代碼可知,obj 是我們輸入的內容,輸入完了之后,加到一個 this.o 的 ArrayList 里。關鍵的 if 判斷就是 if (a(obj.toString()).equals(this.p)) 和 if (b(obj.toString()).equals(this.r)),所以 hook 住 a函數 和 b函數,讓它們的返回值等于下面的字符串即可。
private String p = "V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003"; private String q; private String r = "\u0000dslp}oQ\u0000 dks$|M\u0000h +AYQg\u0000P*!M$gQ\u0000"; private String s;但實際上這題比我想象中的還要麻煩,這題的邏輯上是如果通過了 a 和 b 這兩個函數的計算,等于對應的值之后,會把用來計算的 obj 的值賦值給 q 和 s,然后根據這個 q 和 s 來計算出最終的 flag。
所以如果不逆向算法,通過 hook 的方式通過了 a和b 的計算,obj 的值還是錯誤的,也計算不出正確的 flag。
這樣就逆向一下算法好了,先自己寫一個 apk,用 java 去實現注冊機。
可以直接把 class 文件轉成 dex,不復述,我比較懶,所以我直接解壓 apk 找到?classes.dex,并 push 到手機上。
然后用 frida 加載這個 dex,并調用里面的方法。
7、Frida 打印 與 參數構造
- 數組 / (字符串)對象數組 / gson / Java.array
- 對象 / 多態、強轉Java.cast / 接口Java.register
- 泛型、List、Map、Set、迭代打印
打印 [object object]
- 方法 1:先確認 object 是什么類型,比如要打印 p,先 console.log(p.$className) 查看 p 是什么數據類型,然后用 Java.cast 把 p 強制轉為對應類型,強制轉換之后,在調用轉換后類型的輸出方法,通常為 toString()
- 方法 2:使用 js 里面的 json 類,嘗試 console.log(JSON.stringify(p)),可能打印不出來字符串,一般能打印出 p 的字節數組
- 方法 3:使用 objection 插件 wallbreak
$className 參看官方文檔說明:
gson 打印 Java 對象的內容
Gson 是谷歌官方推出的支持?JSON 和 Java Object?相互轉換的 Java?序列化/反序列化?庫。
gson?基本用法:https://blog.csdn.net/chenrenxiang/article/details/80291224? ??https://www.cnblogs.com/baiqiantao/p/7512336.html
Android Gson使用詳解:https://www.jianshu.com/p/0444693c2639
Frida?打印 [object] 解決 'gson'?包重名的問題:https://www.52pojie.cn/thread-1167397-1-1.html
使用 Frida 時,想要打印 Java 對象的內容,可以使用谷歌的 gson包,可以非常優秀的將 Java 對象的內容,以 json 的格式打印出來。
Java.openClassFile("/data/local/tmp/r0gson.dex").load(); const gson = Java.use('com.r0ysue.gson.Gson'); console.log(gson.$new().toJson(xxx));char[] / [Object Object]
Log.d("SimpleArray", "onCreate: SImpleArray"); char arr[][] = new char[4][]; // 創建一個4行的二維數組 arr[0] = new char[] { '春', '眠', '不', '覺', '曉' }; // 為每一行賦值 arr[1] = new char[] { '處', '處', '聞', '啼', '鳥' }; arr[2] = new char[] { '夜', '來', '風', '雨', '聲' }; arr[3] = new char[] { '花', '落', '知', '多', '少' }; Log.d("SimpleArray", "-----橫版-----"); for (int i = 0; i < 4; i++) { // 循環4行Log.d("SimpleArraysToString", Arrays.toString(arr[i]));Log.d("SimpleStringBytes", Arrays.toString(Arrays.toString(arr[i]).getBytes()));for (int j = 0; j < 5; j++) { // 循環5列Log.d("SimpleArray", Character.toString(arr[i][j])); // 輸出數組中的元素}if (i % 2 == 0) {Log.d("SimpleArray", ",");// 如果是一、三句,輸出逗號} else {Log.d("SimpleArray", "。");// 如果是二、四句,輸出句號} }新建一個 Android 項目,把上面代碼放到 onCreate 函數中,再導入缺失的包
import android.util.Log; import java.util.Arrays;?點擊菜單欄上的 build ---> 生成 apk,成功生成 apk 后,手機安裝 apk,在啟動 frida-server 和端口轉發
?執行命令:frida-ps -Ua 查看 apk 的包名
main.js?
function main() {Java.perform(function x() {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;}}) }setImmediate(main)?在執行命令:frida -U -f com.example.myapplication -l .\main.js --no-pause?
這里的?[C?是 JNI 函數簽名
byte[]
function main() {Java.perform(function x() {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;}}) }setImmediate(main)?// byte[] bArray = "123abc".getBytes() // java 代碼
var strTemp = Java.use("java.lang.String").$new(bArray);
console.log(strTemp)
java array 構造
如果不只是想打印出結果,而是要替換原本的參數,就要先自己構造出一個charArray,使用?Java.array 這個API
/*** Creates a Java array with elements of the specified `type`, from a* JavaScript array `elements`. The resulting Java array behaves like* a JS array, but can be passed by reference to Java APIs in order to* allow them to modify its contents.** @param type Type name of elements.* @param elements Array of JavaScript values to use for constructing the* Java array.*/ function array(type: string, elements: any[]): any[]; 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?來強制轉換對象的類型。
/*** Creates a JavaScript wrapper given the existing instance at `handle` of* given class `klass` as returned from `Java.use()`.** @param handle An existing wrapper or a JNI handle.* @param klass Class wrapper for type to cast to.*/ function cast(handle: Wrapper | NativePointerValue, klass: Wrapper): Wrapper;java 示例代碼( 定義一個 water 類 和 一個 Juice 類,同時 Juice 繼承 water ):
public class Water { // 水 類public static String flow(Water W) { // 水 的方法// SomeSentenceLog.d("2Object", "water flow: I`m flowing");return "water flow: I`m flowing";}public String still(Water W) { // 水 的方法// SomeSentenceLog.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!";}js 示例代碼:
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));示例:
function printHashMap(param_hm){
? ? var HashMap = Java.use('java.util.HashMap');
? ? var args_map = Java.cast(param_hm, HashMap)
? ? send('args_map:' + args_map.toString());
}
interface / Java.registerClass
public interface liquid {public String flow(); }frida 提供能力去創建一個新的 java class
/*** Creates a new Java class.** @param spec Object describing the class to be created.*/ function registerClass(spec: ClassSpec): Wrapper;首先獲取要實現的 interface,然后調用 registerClass 來實現 interface。
function main() {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()) }) } setImmediate(main)成員內部類 / 匿名內部類
看 smali 或者 枚舉出來的類。
hook enum
關于 java 枚舉,從這篇文章了解:https://www.cnblogs.com/jingmoxukong/p/6098351.html
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;}} } 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
【深入Java基礎】HashMap 的基本用法:https://blog.csdn.net/wxgxgp/article/details/79194360
Java集合之HashMap的用法:https://blog.csdn.net/weixin_43263961/article/details/86427533
frida 打印 map
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!")}})})frida 復雜類型參數打印、參數轉換、調用棧打印:https://blog.csdn.net/weixin_35762183/article/details/106802647
示例:
# -*- coding: UTF-8 -*- import frida, sysjsCode = """ Java.perform(function () {/*var clazz = Java.use("xxx");clazz.b.overload('java.util.Map').implementation = function (args1) {var result = "";var keyset = args1.keySet();var it = keyset.iterator();while (it.hasNext()) {var keystr = it.next().toString();var valuestr = args1.get(keystr).toString();console.log(keystr)console.log(valuestr)result += valuestr;}var args = this.b(args1)console.log("出參--", args)return args}*/var HashMap = Java.use('java.util.HashMap');var ShufferMap = Java.use('com.xiaojianbang.app.ShufferMap');ShufferMap.show.implementation = function (map) {var hm = HashMap.$new();hm.put("user","dajianbang");hm.put("pass","87654321");hm.put("code","123456");return this.show(hm);} }); """;def message(message, data):if message["type"] == 'send':print(u"[*] {0}".format(message['payload']))else:print(message)process = frida.get_remote_device().attach("com.xiaojianbang.app") script= process.create_script(jsCode) script.on("message", message) script.load() sys.stdin.read()js_code:
js_code = ''' Java.perform(function() {var clazz = Java.use('com.xxx.xxx');clazz.signUrl.overload('java.lang.String', 'java.util.Map').implementation = function(arg_str, arg_map) { console.log('arg_str:', arg_str);console.log('arg_map:', arg_map); var result = "";var key_set = arg_map.keySet();var key_set_it = key_set.iterator();while(key_set_it.hasNext()){var key_str = key_set_it.next().toString();var value_str = arg_map.get(key_str).toString();console.log(key_str) console.log(value_str) } return this.signUrl(arg_str, arg_map)} }); '''js_code?( hashmap 有個 toString 函數,可以直接打印?)
function printHashMap(param_hm){var HashMap = Java.use('java.util.HashMap');var args_map = Java.cast(param_hm, HashMap)send('args_map:' + args_map.toString()); }打印 non-ascii
https://api-caller.com/2019/03/30/frida-note/#non-ascii
類名非 ASCII 字符串時,先編碼打印出來,?再用編碼后的字符串去 hook。
//場景 hook cls.forName尋找目標類的 classloader。 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);//通過日志確認base64后的非ascii字符串,下面對比并打印classloader//clsName為特殊字符o.?é?if ('o.%CE%99%C9%AB' == base64Name) {//打印classloaderconsole.log(arg3);}return clsName; }8、Frida native hook : NDK 開發入門
https://www.jianshu.com/p/87ce6f565d37
- Android JNI(一)——NDK與JNI基礎
- Android JNI學習(二)——實戰JNI之“hello world”
- Android JNI學習(三)——Java與Native相互調用
- Android JNI學習(四)——JNI的常用方法的中文API
- Android JNI學習(五)——Demo演示
extern "C"?與 名稱修飾 (name mangling)
- 通過 C++ filt 工具可以直接還原得到原來的函數名
- https://zh.wikipedia.org/zh-hans/名字修飾
- 通過 extern "C" 導出的 JNI 函數不會被 name mangling
- JNI參數 與 基本類型
- 第一個 NDK程序
-
JNI log
9、Frida native hook : JNIEnv 和 反射
9.1 以 jni字符串 來掌握基本的 JNIEnv用法
public native String stringWithJNI(String context); ...extern "C" JNIEXPORT jstring JNICALL Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI(JNIEnv *env, jobject instance, jstring context_) {const char *context = env->GetStringUTFChars(context_, 0);int context_size = env->GetStringUTFLength(context_);if (context_size > 0) {LOGD("%s\n", context);}env->ReleaseStringUTFChars(context_, context);return env->NewStringUTF("sakura1328"); }12-26 22:30:00.548 15764-15764/myapplication.example.com.ndk_demo D/sakura1328: sakura9.2 Java 反射
總結:多去讀一下 Java 的反射 API。
?Java高級特性 -----?反射?:https://www.jianshu.com/p/9be58ee20dee
- 查找調用各種 API 接口、JNI、frida/xposed原理的一部分
- 反射基本 API
- 反射修改訪問控制、修改屬性值
- JNI so調用反射進入java世界
- xposed/Frida hook原理
這里其實有一個伏筆,就是為什么我們要 trace artmethod,hook artmethod ?????
是因為有些 so 混淆得非常厲害,然后也就很難靜態分析看出 so 里面調用了哪些 java 函數,也不是通過類似 JNI 的 GetMethodID 這樣來調用的。
而是通過類似 findclass 這種方法先得到類,然后再反射調用 app 里面的某個 java 函數。
所以去 hook 它執行的位置,每一個 java 函數對于 Android 源碼而言都是一個 artmethod 結構體,然后 hook 拿到 artmethod 實例后,再去調用類函數,打印這個函數的名稱。
public class MainActivity extends AppCompatActivity {private static final String TAG = "sakura";// Used to load the 'native-lib' library on application startup.static {System.loadLibrary("native-lib");}@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);// Example of a call to a native methodTextView tv = (TextView) findViewById(R.id.sample_text);tv.setText(stringWithJNI("sakura")); // Log.d(TAG, stringFromJNI()); // Log.d(TAG, stringWithJNI("sakura"));try {testClass();} catch (ClassNotFoundException e) {e.printStackTrace();} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (NoSuchMethodException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}public void testClass() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {Test sakuraTest = new Test();// 獲得Class的方法(三種)Class testClazz = MainActivity.class.getClassLoader().loadClass("myapplication.example.com.ndk_demo.Test");Class testClazz2 = Class.forName("myapplication.example.com.ndk_demo.Test");Class testClazz3 = Test.class;Log.i(TAG, "Classloader.loadClass->" + testClazz);Log.i(TAG, "Classloader.loadClass->" + testClazz2);Log.i(TAG, "Classloader.loadClass->" + testClazz3.getName());// 獲得類中屬性相關的方法Field publicStaticField = testClazz3.getDeclaredField("publicStaticField");Log.i(TAG, "testClazz3.getDeclaredField->" + publicStaticField);Field publicField = testClazz3.getDeclaredField("publicField");Log.i(TAG, "testClazz3.getDeclaredField->" + publicField);//對于Field的get方法,如果是static,則傳入null即可;如果不是,則需要傳入一個類的實例String valueStaticPublic = (String) publicStaticField.get(null);Log.i(TAG, "publicStaticField.get->" + valueStaticPublic);String valuePublic = (String) publicField.get(sakuraTest);Log.i(TAG, "publicField.get->" + valuePublic);//對于private屬性,需要設置AccessibleField privateStaticField = testClazz3.getDeclaredField("privateStaticField");privateStaticField.setAccessible(true);String valuePrivte = (String) privateStaticField.get(null);Log.i(TAG, "modified before privateStaticField.get->" + valuePrivte);privateStaticField.set(null, "modified");valuePrivte = (String) privateStaticField.get(null);Log.i(TAG, "modified after privateStaticField.get->" + valuePrivte);Field[] fields = testClazz3.getDeclaredFields();for (Field i : fields) {Log.i(TAG, "testClazz3.getDeclaredFields->" + i);}// 獲得類中method相關的方法Method publicStaticMethod = testClazz3.getDeclaredMethod("publicStaticFunc");Log.i(TAG, "testClazz3.getDeclaredMethod->" + publicStaticMethod);publicStaticMethod.invoke(null);Method publicMethod = testClazz3.getDeclaredMethod("publicFunc", java.lang.String.class);Log.i(TAG, "testClazz3.getDeclaredMethod->" + publicMethod);publicMethod.invoke(sakuraTest, " sakura");}/*** A native method that is implemented by the 'native-lib' native library,* which is packaged with this application.*/public native String stringFromJNI();public native String stringWithJNI(String context); } ... public class Test {private static final String TAG = "sakura_test";public static String publicStaticField = "i am a publicStaticField";public String publicField = "i am a publicField";private static String privateStaticField = "i am a privateStaticField";private String privateField = "i am a privateField";public static void publicStaticFunc() {Log.d(TAG, "I`m from publicStaticFunc");}public void publicFunc(String str) {Log.d(TAG, "I`m from publicFunc" + str);}private static void privateStaticFunc() {Log.i(TAG, "I`m from privateFunc");}private void privateFunc() {Log.i(TAG, "I`m from privateFunc");} } ... ... 12-26 23:57:11.784 17682-17682/myapplication.example.com.ndk_demo I/sakura: Classloader.loadClass->class myapplication.example.com.ndk_demo.Test 12-26 23:57:11.784 17682-17682/myapplication.example.com.ndk_demo I/sakura: Classloader.loadClass->class myapplication.example.com.ndk_demo.Test 12-26 23:57:11.784 17682-17682/myapplication.example.com.ndk_demo I/sakura: Classloader.loadClass->myapplication.example.com.ndk_demo.Test 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredField->public static java.lang.String myapplication.example.com.ndk_demo.Test.publicStaticField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredField->public java.lang.String myapplication.example.com.ndk_demo.Test.publicField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: publicStaticField.get->i am a publicStaticField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: publicField.get->i am a publicField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: modified before privateStaticField.get->i am a privateStaticField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: modified after privateStaticField.get->modified 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->private java.lang.String myapplication.example.com.ndk_demo.Test.privateField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->public java.lang.String myapplication.example.com.ndk_demo.Test.publicField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->private static final java.lang.String myapplication.example.com.ndk_demo.Test.TAG 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->private static java.lang.String myapplication.example.com.ndk_demo.Test.privateStaticField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredFields->public static java.lang.String myapplication.example.com.ndk_demo.Test.publicStaticField 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredMethod->public static void myapplication.example.com.ndk_demo.Test.publicStaticFunc() 12-26 23:57:11.785 17682-17682/myapplication.example.com.ndk_demo D/sakura_test: I`m from publicStaticFunc 12-26 23:57:11.786 17682-17682/myapplication.example.com.ndk_demo I/sakura: testClazz3.getDeclaredMethod->public void myapplication.example.com.ndk_demo.Test.publicFunc(java.lang.String) 12-26 23:57:11.786 17682-17682/myapplication.example.com.ndk_demo D/sakura_test: I`m from publicFunc sakuramemory list modules
流出加載的 so 庫
10、Frida 反調試 與 反反調試
這一節的主要內容就是關于反調試的原理和如何破解反調試,重要內容還是看文章理解即可。
因為我并不需要做反調試相關的工作,所以部分內容略過。
Frida 反調試 與 反反調試 基本思路
(Java層API、Native層API、Syscall)
- AntiFrida:https://github.com/qtfreet00/AntiFrida
- frida-detection-demo:https://github.com/b-mueller/frida-detection-demo
- 多種特征檢測Frida:https://bbs.pediy.com/thread-217482.htm
- 來自高維的對抗 - 逆向TinyTool自制:https://yq.aliyun.com/articles/71120
- Unicorn 在 Android 的應用:https://bbs.pediy.com/thread-253868.htm
11、Frida native hook : 符號 hook JNI、art&libc
11.1 Native函數的Java Hook及主動調用
對 native 函數的 java 層 hook 和主動調用和普通 java 函數完全一致,略過。
11.2 jni.h 頭文件導入
導入 jni.h,先 search 一下這個文件在哪。
Error /Users/sakura/Library/Android/sdk/ndk-bundle/sysroot/usr/include/jni.h,27: Can't open include file 'stdarg.h' Total 1 errors Caching 'Exports'... ok報錯,所以拷貝一份 jni.h 出來,將這兩個頭文件導入刪掉
導入成功
現在就能識別_JNIEnv了,如圖
11.3 JNI 函數符號 hook
先查看一下導出了哪些函數。
extern "C" JNIEXPORT jstring JNICALL Java_myapplication_example_com_ndk_1demo_MainActivity_stringFromJNI(JNIEnv *env,jobject /* this */) {std::string hello = "Hello from C++";LOGD("sakura1328");return env->NewStringUTF(hello.c_str()); } extern "C" JNIEXPORT jstring JNICALL Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI(JNIEnv *env, jobject instance,jstring context_) {const char *context = env->GetStringUTFChars(context_, 0);int context_size = env->GetStringUTFLength(context_);if (context_size > 0) {LOGD("%s\n", context);}env->ReleaseStringUTFChars(context_, context);return env->NewStringUTF("sakura1328"); }這里有幾個需要的 API。
- 首先是找到是否 so 被加載,通過?Process.enumerateModules(),這個API可以枚舉被加載到內存的 modules。
- 然后通過Module.findBaseAddress(module name)來查找要hook的函數所在的so的基地址,如果找不到就返回null。
- 然后可以通過findExportByName(moduleName: string, exportName: string): NativePointer來查找導出函數的絕對地址。如果不知道moduleName是什么,可以傳入一個null進入,但是會花費一些時間遍歷所有的module。如果找不到就返回null。
- 找到地址之后,就可以攔截function/instruction的執行。通過Interceptor.attach。使用方法見下代碼。
- 另外為了將jstring的值打印出來,可以使用jenv的函數getStringUtfChars,就像正常的寫native程序一樣。
Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()
這里是循環調用的 string_with_jni,如果不循環調用,就要主動調用一下這個函數,或者 hook dlopen。
hook dlopen 的方法(?https://github.com/lasting-yang/frida_dump/blob/master/dump_dex.js )可以參考。
function hook_native() {// console.log(JSON.stringify(Process.enumerateModules()));var libnative_addr = Module.findBaseAddress("libnative-lib.so")console.log("libnative_addr is: " + libnative_addr)if (libnative_addr) {var string_with_jni_addr = Module.findExportByName("libnative-lib.so", "Java_myapplication_example_com_ndk_1demo_MainActivity_stringWithJNI")console.log("string_with_jni_addr is: " + string_with_jni_addr)}Interceptor.attach(string_with_jni_addr, {onEnter: function (args) {console.log("string_with_jni args: " + args[0], args[1], args[2])console.log(Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())},onLeave: function (retval) {console.log("retval:", retval)console.log(Java.vm.getEnv().getStringUtfChars(retval, null).readCString())var newRetval = Java.vm.getEnv().newStringUtf("new retval from hook_native");retval.replace(ptr(newRetval));}}) } libnative_addr is: 0x7a0842f000 string_with_jni_addr is: 0x7a08436194 [Google Pixel::myapplication.example.com.ndk_demo]-> string_with_jni args: 0x7a106cc1c0 0x7ff0b71da4 0x7ff0b71da8 sakura retval: 0x75 sakura1328這里還寫了一個 hook env 里的 GetStringUTFChars 的代碼,和上面一樣,不贅述了。
function hook_art(){var addr_GetStringUTFChars = null;//console.log( JSON.stringify(Process.enumerateModules()));var symbols = Process.findModuleByName("libart.so").enumerateSymbols();for(var i = 0;i<symbols.length;i++){var symbol = symbols[i].name;if((symbol.indexOf("CheckJNI")==-1)&&(symbol.indexOf("JNI")>=0)){if(symbol.indexOf("GetStringUTFChars")>=0){console.log(symbols[i].name);console.log(symbols[i].address);addr_GetStringUTFChars = symbols[i].address;}}}console.log("addr_GetStringUTFChars:", addr_GetStringUTFChars);Java.perform(function (){Interceptor.attach(addr_GetStringUTFChars, {onEnter: function (args) {console.log("addr_GetStringUTFChars OnEnter args[0],args[1]",args[0],args[1]);//console.log(hexdump(args[0].readPointer()));//console.log(Java.vm.tryGetEnv().getStringUtfChars(args[0]).readCString()); }, onLeave: function (retval) {console.log("addr_GetStringUTFChars OnLeave",ptr(retval).readCString());}})}) }11.4 JNI 函數參數、返回值打印和替換
- libc 函數符號 hook
- libc 函數參數、返回值打印和替換
hook libc 的也和上面的完全一樣,也不贅述了。
所以看到這里,究其本質就是找到導出符號和它所在的so基地址了。
12、Frida native hook : JNI_Onload / 動態注冊 / inline_hook / native層調用棧打印
https://github.com/android/ndk-samples
12.1 JNI_Onload / 動態注冊原理
JNI_Onload / 動態注冊 / Frida hook RegisterNative
- JNI與動態注冊:https://zhuanlan.kanxue.com/article-4482.htm
- native 方法的動態注冊:https://eternalsakura13.com/2018/02/08/jni2/
- Frida hook art:https://github.com/lasting-yang/frida_hook_libart
詳細的內容參見我寫的文章,這里只給出例子。
Log.d(TAG,stringFromJNI2()); public native String stringFromJNI2(); JNIEXPORT jstring JNICALL stringFromJNI2(JNIEnv *env,jclass clazz) {jclass testClass = env->FindClass("myapplication/example/com/ndk_demo/Test");jfieldID publicStaticField = env->GetStaticFieldID(testClass, "publicStaticField","Ljava/lang/String;");jstring publicStaticFieldValue = (jstring) env->GetStaticObjectField(testClass,publicStaticField);const char *value_ptr = env->GetStringUTFChars(publicStaticFieldValue, NULL);LOGD("now content is %s", value_ptr);std::string hello = "Hello from C++ stringFromJNI2";return env->NewStringUTF(hello.c_str()); } ... JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env;vm->GetEnv((void **) &env, JNI_VERSION_1_6);JNINativeMethod methods[] = {{"stringFromJNI2", "()Ljava/lang/String;", (void *) stringFromJNI2},};env->RegisterNatives(env->FindClass("myapplication/example/com/ndk_demo/MainActivity"), methods,1);return JNI_VERSION_1_6; }12.2 Frida hook RegisterNative
使用下面這個腳本來打印出 RegisterNatives 的參數,這里需要注意的是使用了enumerateSymbolsSync,它是 enumerateSymbols 的同步版本。
另外和我們之前通過?Java.vm.tryGetEnv().getStringUtfChars來調用env里的方法不同。
這里則是通過將之前找到的getStringUtfChars函數地址和參數信息封裝起來,直接調用,具體的原理我沒有深入分析,先記住用法。
原理其實是一樣的,都是?根據符號找到地址,然后hook符號地址,然后打印參數。
結果很明顯的打印了出來,包括動態注冊的函數的名字,函數簽名,加載地址和在so里的偏移量,
[RegisterNatives] java_class: myapplication.example.com.ndk_demo.MainActivity name: stringFromJNI2 sig: ()Ljava/lang/String; fnPtr: 0x79f8698484 module_name: libnative-lib.so module_base: 0x79f8691000 offset: 0x7484最后測試一下 yang 開源的一個hook art的腳本,很有意思,trace 出了非常多的需要的信息。
frida -U --no-pause -f package_name -l hook_art.js ... [FindClass] name:myapplication/example/com/ndk_demo/Test [GetStaticFieldID] name:publicStaticField, sig:Ljava/lang/String; [GetStringUTFChars] result:i am a publicStaticField [NewStringUTF] bytes:Hello from C++ stringFromJNI2 [GetStringUTFChars] result:sakura12.3 native 層調用棧打印
直接使用 frida 提供的接口打印棧回溯。
Interceptor.attach(f, {onEnter: function (args) {console.log('RegisterNatives called from:\n' +Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');} });效果如下,我加到了 hook registerNative 的地方。
[Google Pixel::myapplication.example.com.ndk_demo]-> RegisterNatives called from: 0x7a100be03c libart.so!0xe103c 0x7a100be038 libart.so!0xe1038 0x79f85699a0 libnative-lib.so!_ZN7_JNIEnv15RegisterNativesEP7_jclassPK15JNINativeMethodi+0x44 0x79f85698e0 libnative-lib.so!JNI_OnLoad+0x90 0x7a102b9fd4 libart.so!_ZN3art9JavaVMExt17LoadNativeLibraryEP7_JNIEnvRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEP8_jobjectP8_jstringPS9_+0x638 0x7a08e3820c libopenjdkjvm.so!JVM_NativeLoad+0x110 0x70b921c4 boot.oat!oatexec+0xa81c412.4 主動調用去進行方法參數替換
使用?Interceptor.replace,不贅述。主要目的還是為了改掉函數原本的執行行為,而不是僅僅打印一些信息。
12.5 inline hook ( so庫里面的函數?)
inline hook 簡單理解就是:不是 hook 函數開始執行的地方,而是 hook 函數中間執行的指令
整體來說沒什么區別,就是把找函數符號地址改成從so里找到偏移,然后加到so基地址上就行,注意一下它的 attach 的 callback。
我的 so 是自己編譯的,具體的匯編代碼如下,總之這里很明顯在 775C 時,x0 里保存的是一個指向 "sakura"?這個字符串的指針。(其實我也不是很看得懂 arm64 了已經,就隨便 hook 了一下)
所以 hook 這個指令,然后?Memory.readCString(this.context.x0);打印出來,結果如下
到這里已經可以總結一下我目前的學習了,需要補充一些 frida api 的學習,比如 NativePointr 里居然有個 readCString,這些 API 是需要再看看的。
13、Frida native hook : Frida hook native app 實戰
- 破解 Frida 全端口檢測的 native層反調試
- hook libc 的 pthread_create 函數
- 破解 TracePid 的 native 反調試
- target:Android反調試技術整理與實踐:https://gtoad.github.io/2017/06/25/Android-Anti-Debug/
- solve : hook libc的fgets函數
- native 層修改參數、返回值
- 靜態分析 JNI_Onload
- 動態 trace 主動注冊 & IDA溯源
- 動態trace JNI、libc函數 & IDA溯源
- native層主動調用、打調用棧
- 主動調用 libc 讀寫文件
首先看下 logcat
n/u0a128 for activity com.gdufs.xman/.MainActivity 12-28 05:53:26.898 26615 26615 V com.gdufs.xman: JNI_OnLoad() 12-28 05:53:26.898 26615 26615 V com.gdufs.xman: RegisterNatives() --> nativeMethod() ok 12-28 05:53:26.898 26615 26615 D com.gdufs.xman m=: 0 12-28 05:53:26.980 26615 26615 D com.gdufs.xman m=: Xman sakura@sakuradeMacBook-Pro:~/gitsource/frida-agent-example/agent$ frida -U --no-pause -f com.gdufs.xman -l hook_reg.js ... [Google Pixel::com.gdufs.xman]-> [RegisterNatives] method_count: 0x3 [RegisterNatives] java_class: com.gdufs.xman.MyApp name: initSN sig: ()V fnPtr: 0xd4ddf3b1 module_name: libmyjni.so module_base: 0xd4dde000 offset: 0x13b1 [RegisterNatives] java_class: com.gdufs.xman.MyApp name: saveSN sig: (Ljava/lang/String;)V fnPtr: 0xd4ddf1f9 module_name: libmyjni.so module_base: 0xd4dde000 offset: 0x11f9 [RegisterNatives] java_class: com.gdufs.xman.MyApp name: work sig: ()V fnPtr: 0xd4ddf4cd module_name: libmyjni.so module_base: 0xd4dde000 offset: 0x14cdinitSN
感覺意思應該是從?/sdcard/reg.dat?里讀一個值,然后和?EoPAoY62@ElRD?進行比較。
最后setValue,從導出函數看一下,最后推測第一個參數應該是JNIEnv *env,然后就看到了給字段m賦值。
aveSN
這個看上去就是根據str的值,去變換”W3_arE_whO_we_ARE”字符串,然后寫入到/sdcard/reg.dat里
結合一下看,只要initSN檢查到/sdcard/reg.dat里是EoPAoY62@ElRD,應該就會給m設置成1。
只要m的值是1,就能走到work()函數的邏輯。
參考:?frida 的 file api:https://frida.re/docs/javascript-api/#file
function main() {var file = new File("/sdcard/reg.dat",'w')file.write("EoPAoY62@ElRD")file.flush()file.close() } setImmediate(main())這樣我們繼續看 work 的邏輯
v2 是從 getValue 得到的,看上去就是 m字段的值,此時應該是1,一會 hook 一下看看。
[NewStringUTF] bytes:輸入即是flag,格式為xman{……}!callWork 里又調用了 work 函數,死循環了。
那看來看去最后還是回到了initSN,那其實我們看的順序似乎錯了。
理一下邏輯,n2執行完保存到文件,然后n1 check一下,所以最后還是要逆n2的算法,pass。
14、Frida trace 四件套
14.1 jni trace : trace jni
https://github.com/chame1eon/jnitrace
pip install jnitraceRequirement already satisfied: frida>=12.5.0 in /Users/sakura/.pyenv/versions/3.7.7/lib/python3.7/site-packages (from jnitrace) (12.8.0) Requirement already satisfied: colorama in /Users/sakura/.pyenv/versions/3.7.7/lib/python3.7/site-packages (from jnitrace) (0.4.3) Collecting hexdump (from jnitrace)Downloading https://files.pythonhosted.org/packages/55/b3/279b1d57fa3681725d0db8820405cdcb4e62a9239c205e4ceac4391c78e4/hexdump-3.3.zip Installing collected packages: hexdump, jnitraceRunning setup.py install for hexdump ... doneRunning setup.py install for jnitrace ... done Successfully installed hexdump-3.3 jnitrace-3.0.8usage:?jnitrace [options] -l libname target
默認應該是 spawn 運行的,
- -m?來指定是?spawn?還是?attach
- -b?指定是?fuzzy?還是?accurate
- -i <regex>?指定一個正則表達式來過濾出方法名,例如?-i Get -i RegisterNatives?就會只打印出名字里包含 Get 或者 RegisterNatives 的 JNI methods。
- -e <regex>?和?-i?相反,同樣通過正則表達式來過濾,但這次會將指定的內容忽略掉。
- -I <string>?trace 導出的方法,jnitrace 認為導出的函數應該是從 Java 端能夠直接調用的函數,所以可以包括使用 RegisterNatives 來注冊的函數,例如?-I stringFromJNI -I nativeMethod([B)V,就包括導出名里有 stringFromJNI,以及使用 RegisterNames 來注冊,并帶有 nativeMethod([B)V 簽名的函數。
- -o path/output.json,導出輸出到文件里。
- -p path/to/script.js,用于在加載 jnitrace 腳本之前將指定路徑的 Frida 腳本加載到目標進程中,這可以用于在 jnitrace 啟動之前對抗反調試。
- -a path/to/script.js,用于在加載 jnitrace 腳本之后將指定路徑的 Frida 腳本加載到目標進程中
- --ignore-env,不打印所有的 JNIEnv 函數
- --ignore-vm,不打印所有的 JavaVM 函數
14.2 strace : trace syscall
strace 跟蹤進程中的系統調用:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/strace.html
14.3 frida-trace : trace libc(or more)
? ? ? ? frida-trace:https://frida.re/docs/frida-trace/
? ? ? ? Usage:frida-trace [options] target
frida-trace -U -i "strcmp" -f com.gdufs.xman ...5634 ms strcmp(s1="fi", s2="es-US")5635 ms strcmp(s1="da", s2="es-US")5635 ms strcmp(s1="es", s2="es-US")5635 ms strcmp(s1="eu-ES", s2="es-US")5635 ms strcmp(s1="et-EE", s2="es-US")5635 ms strcmp(s1="et-EE", s2="es-US")art trace
hook artmethod:https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_artmethod.js
14.4 hook_artmethod : trace java 函數調用
https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_artmethod.js
14.5 修改AOSP源碼打印
改 aosp 源碼 trace 信息:https://bbs.pediy.com/thread-255653-1.htm
15、Frida native hook : init_array 開發和自動化逆向
15.1 init_array原理 (?so 加載、啟動、執行 )
常見的保護都會在 init_array 里面做,關于其原理,主要閱讀以下文章即可。
- IDA 調試 android so 的 .init_array 數組:https://www.cnblogs.com/bingghost/p/6297325.html
- Android NDK中.init段和.init_array段函數的定義方式:https://www.dllhook.com/post/213.html
- Linker 學習筆記( so 加載、啟動、執行):https://wooyun.js.org/drops/Android Linker學習筆記.html
15.2 IDA靜態分析 init_array
// 編譯生成后在.init段 [名字不可更改] extern "C" void _init(void) {LOGD("Enter init......"); }// 編譯生成后在.init_array段 [名字可以更改] __attribute__((__constructor__)) static void sakura_init() {LOGD("Enter sakura_init......"); } ... ... 2016-12-29 16:51:23.017 5160-5160/com.example.ndk_demo D/sakura1328: Enter init...... 2016-12-29 16:51:23.017 5160-5160/com.example.ndk_demo D/sakura1328: Enter sakura_init......IDA快捷鍵shift+F7找到segment,然后就可以找到.init_array段,然后就可以找到里面保存的函數地址。
15.3 IDA 動態調試 so
-
打開要調試的 apk,找到入口
- 啟動 apk,并讓設備將處于一個Waiting For Debugger的狀態
? ? ? ? adb shell am start -D -n com.example.ndk_demo/.MainActivity - 執行 android_server64
- 新開一個窗口使用forward程序進行端口轉發:adb forward tcp:23946 tcp:23946
adb forward tcp:<本地機器的網絡端口號> tcp:<模擬器或是真機的網絡端口號>
例:adb [-d|-e|-s?] forward tcp:6100 tcp:7100 表示把本機的6100端口號與模擬器的7100端口建立起相關,當模擬器或真機向自己的7100端口發送了數據,那們我們可以在本機的6100端口讀取其發送的內容,這是一個很關鍵的命令,以后我們使用jdb調試apk之前,就要用它先把目標進程和本地端口建立起關聯
-
打開IDA,選擇菜單Debugger -> Attach -> Remote ARM Linux/Android debugger
-
打開IDA,選擇菜單Debugger -> Process options, 填好,然后選擇進程去attach。
- 查看待調試的進程?adb jdwp
-
轉發端口adb forward tcp:8700 jdwp:10436,將該進程的調試端口和本機的8700綁定。
-
jdb連接調試端口,從而讓程序繼續運行?jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
-
找到斷點并斷下。
打開 module
找到 linker64
找到 call array 函數
下斷,并按 F9 斷下
最終我確實可以調試到?.init_array?的初始化,具體的代碼分析見?Linker學習筆記?這里。
15.4 init_array && JNI_Onload "自吐"
JNI_Onload
目標是找到動態注冊的函數的地址,因為這種函數沒有導出。
JNINativeMethod methods[] = {{"stringFromJNI2", "()Ljava/lang/String;", (void *) stringFromJNI2}, }; env->RegisterNatives(env->FindClass("com/example/ndk_demo/MainActivity"), methods, 1);首先:jnitrace -m spawn -i "RegisterNatives" -l libnative-lib.so com.example.ndk_demo
525 ms [+] JNIEnv->RegisterNatives 525 ms |- JNIEnv* : 0x7a106cc1c0 525 ms |- jclass : 0x89 { com/example/ndk_demo/MainActivity } 525 ms |- JNINativeMethod* : 0x7ff0b71120 525 ms |: 0x79f00d36b0 - stringFromJNI2()Ljava/lang/String;然后:objection -d -g com.example.ndk_demo run memory list modules explore | grep demo
sakura@sakuradeMacBook-Pro:~$ objection -d -g com.example.ndk_demo run memory list modules explore | grep demo [debug] Attempting to attach to process: `com.example.ndk_demo` Warning: Output is not to a terminal (fd=1). base.odex 0x79f0249000 106496 (104.0 KiB) /data/app/com.example.ndk_demo-HGAFhnKyKCSIpzn227pwXw==/oat/arm64/base.odex libnative-lib.so 0x79f00c4000 221184 (216.0 KiB) /data/app/com.example.ndk_demo-HGAFhnKyKCSIpzn227pwXw==/lib/arm64/libnative...offset = 0x79f00d36b0 - 0x79f00c4000 = 0xf6b0
這樣就找到了
init_array
沒有支持 arm64,可以在安裝 app 的時候?adb install --abi armeabi-v7a?強制讓 app 運行在32位模式
這個腳本整體來說就是 hook callfunction,然后打印出 init_array 里面的函數地址和參數等。
從源碼看,關鍵就是 call_array 這里調用的 call_function,第一個參數代表這是注冊的 init_array 里面的 function,第二個參數則是 init_array 里存儲的函數的地址。
template <typename F> static void call_array(const char* array_name __unused,F* functions,size_t count,bool reverse,const char* realpath) {if (functions == nullptr) {return;}TRACE("[ Calling %s (size %zd) @ %p for '%s' ]", array_name, count, functions, realpath);int begin = reverse ? (count - 1) : 0;int end = reverse ? -1 : count;int step = reverse ? -1 : 1;for (int i = begin; i != end; i += step) {TRACE("[ %s[%d] == %p ]", array_name, i, functions[i]);call_function("function", functions[i], realpath);}TRACE("[ Done calling %s for '%s' ]", array_name, realpath); } function LogPrint(log) {var theDate = new Date();var hour = theDate.getHours();var minute = theDate.getMinutes();var second = theDate.getSeconds();var mSecond = theDate.getMilliseconds()hour < 10 ? hour = "0" + hour : hour;minute < 10 ? minute = "0" + minute : minute;second < 10 ? second = "0" + second : second;mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;var time = hour + ":" + minute + ":" + second + ":" + mSecond;var threadid = Process.getCurrentThreadId();console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);}function hooklinker() {var linkername = "linker";var call_function_addr = null;var arch = Process.arch;LogPrint("Process run in:" + arch);if (arch.endsWith("arm")) {linkername = "linker";} else {linkername = "linker64";LogPrint("arm64 is not supported yet!");}var symbols = Module.enumerateSymbolsSync(linkername);for (var i = 0; i < symbols.length; i++) {var symbol = symbols[i];//LogPrint(linkername + "->" + symbol.name + "---" + symbol.address);if (symbol.name.indexOf("__dl__ZL13call_functionPKcPFviPPcS2_ES0_") != -1) {call_function_addr = symbol.address;LogPrint("linker->" + symbol.name + "---" + symbol.address)}}if (call_function_addr != null) {var func_call_function = new NativeFunction(call_function_addr, 'void', ['pointer', 'pointer', 'pointer']);Interceptor.replace(new NativeFunction(call_function_addr,'void', ['pointer', 'pointer', 'pointer']), new NativeCallback(function (arg0, arg1, arg2) {var functiontype = null;var functionaddr = null;var sopath = null;if (arg0 != null) {functiontype = Memory.readCString(arg0);}if (arg1 != null) {functionaddr = arg1;}if (arg2 != null) {sopath = Memory.readCString(arg2);}var modulebaseaddr = Module.findBaseAddress(sopath);LogPrint("after load:" + sopath + "--start call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);if (sopath.indexOf('libnative-lib.so') >= 0 && functiontype == "DT_INIT") {LogPrint("after load:" + sopath + "--ignore call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);} else {func_call_function(arg0, arg1, arg2);LogPrint("after load:" + sopath + "--end call_function,type:" + functiontype + "--addr:" + functionaddr + "---baseaddr:" + modulebaseaddr);}}, 'void', ['pointer', 'pointer', 'pointer']));} }setImmediate(hooklinker)我調試了一下 linker64,因為沒有導出 call_function 的地址,所以不能直接 hook 符號名,而是要根據偏移去 hook,以后再說。
其實要看?init_array,直接 shift+F7 去 segment 里面找?.init_array?段就可以了,這里主要是為了反反調試,因為可能反調試會加在 init_array 里,hook call_function 就可以讓它不加載反調試程序。
15.5 native層未導出函數主動調用(任意符號和地址)
現在我想要主動調用 sakura_add 來打印值,可以 ida 打開找符號,或者根據偏移,總之最終用這個 NativePointer 指針來初始化一個 NativeFunction 來調用。
extern "C" JNIEXPORT jint JNICALL Java_com_example_ndk_1demo_MainActivity_sakuraWithInt(JNIEnv *env, jobject thiz, jint a, jint b) {// TODO: implement sakuraWithInt()return sakura_add(a,b); } ... int sakura_add(int a, int b){int sum = a+b;LOGD("sakura add a+b:",sum);return sum; } function main() {var libnative_lib_addr = Module.findBaseAddress("libnative-lib.so");console.log("libnative_lib_addr is :", libnative_lib_addr);if (libnative_lib_addr) {var sakura_add_addr1 = Module.findExportByName("libnative-lib.so", "_Z10sakura_addii");var sakura_add_addr2 = libnative_lib_addr.add(0x0F56C) ;console.log("sakura_add_addr1 ", sakura_add_addr1);console.log("sakura_add_addr2 ", sakura_add_addr2)}var sakura_add1 = new NativeFunction(sakura_add_addr1, "int", ["int", "int"]);var sakura_add2 = new NativeFunction(sakura_add_addr2, "int", ["int", "int"]);console.log("sakura_add1 result is :", sakura_add1(200, 33));console.log("sakura_add2 result is :", sakura_add2(100, 133)); } setImmediate(main()) ... ... libnative_lib_addr is : 0x79fa1c5000 sakura_add_addr1 0x79fa1d456c sakura_add_addr2 0x79fa1d456c sakura_add1 result is : 233 sakura_add2 result is : 23316、C/C++ hook
//todo
16.1 Native/JNI層參數打印和主動調用參數構造
jni的基本類型要通過調用jni相關的api轉化成c++對象,才能打印和調用。
jni主動調用的時候,參數構造有兩種方式,一種是Java.vm.getenv,另一種是hook獲取env之后來調用jni相關的api構造參數。
16.2 C/C++編成 so 并引入 Frida 調用其中的函數
總結
以上是生活随笔為你收集整理的Frida Android hook的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CompletableFuture详解~
- 下一篇: 从输入URL到页面显示的过程