這恐怕是學習Frida最詳細的筆記了


轉載自Sakura的博客:https://eternalsakura13.com/2020/07/04/frida

title: Frida Android hook
categories:

  • Android逆向

致謝

本篇文章學到的內容來自且完全來自r0ysue的知識星球,推薦一下(這個男人啥都會,還能陪你在線撩騷)。

Frida環境

https://github.com/frida/frida

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.2

pyenv versions
sakura@sakuradeMacBook-Pro:~$ pyenv versions
  system
* 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.

frida安裝

如果直接按下述安裝則會直接安裝frida和frida-tools的最新版本。

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和對應關係
對應關係很好找

安裝objection

pyenv local 3.8.2
pip install objection
objection -h
pyenv local 3.7.7
pip install objection==1.8.4
objection -h

frida使用

下載frida-server並解壓,在這裏下載[frida-server-12.8.0] (可以使用最新版)
(https://github.com/frida/frida/releases/download/12.8.0/frida-server-12.8.0-android-arm64.xz)

先adb shell,然後切換到root權限,把之前push進來的frida server改個名字叫fs
然後運行frida

adb push /Users/sakura/Desktop/lab/alpha/tools/android/frida-server-12.8.0-android-arm64 /data/local/tmp
chmod +x fs
./fs

如果要監聽端口,就

./fs -l 0.0.0.0:8888

frida開發環境搭建

  1. 安裝
git clone https://github.com/oleavr/frida-agent-example.git
cd frida-agent-example/
npm install
  1. 使用vscode打開此工程,在agent文件夾下編寫js,會有智能提示。
  2. npm run watch會監控代碼修改自動編譯生成js文件
  3. python腳本或者cli加載_agent.js
    frida -U -f com.example.android --no-pause -l _agent.js

下面是測試腳本

s1.js

function main() {
    Java.perform(function x() {
        console.log("sakura")
    })
}
setImmediate(main)

loader.py

import time
import frida

device8 = 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腳本。

FRIDA基礎

frida查看當前存在的進程

frida-ps -U查看通過usb連接的android手機上的進程。

sakura@sakuradeMacBook-Pro:~$ frida-ps --help
Usage: frida-ps [options]

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -D ID, --device=ID    connect to device with the given ID
  -U, --usb             connect to USB device
  -R, --remote          connect to remote frida-server
  -H HOST, --host=HOST  connect to remote frida-server on HOST
  -a, --applications    list only applications
  -i, --installed       include all installed applications
sakura@sakuradeMacBook-Pro:~$ frida-ps -U
  PID  Name
-----  ---------------------------------------------------
 3640  ATFWD-daemon
  707  adbd
  728  adsprpcd
26041  [email protected]
  741  android.hardware.biometrics.fingerprint@

通過grep過濾就可以找到我們想要的包名。

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 = "@@@###@@@";

    @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("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;
    }
}
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);
sakura@sakuradeMacBook-Pro:~$ frida-ps -U | grep frida
8738  frida-helper-32
8897  myapplication.example.com.frida_demo

// -f是通過spawn,也就是重啓apk注入js
sakura@sakuradeMacBook-Pro:~$ 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: sakura

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);

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
};
import time
import frida

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_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@sakuradeMacBook-Pro:~/gitsource/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
end

frida動態修改

即將手機上的app的內容發送到PC上的frida python程序,然後處理後返回給app,然後app再做後續的流程,核心是理解send/recv函數

<TextView
        android: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" />


    <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" />

    <Button
        android: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;

    @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;
                }
                //hook target
                message_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這個函數。

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); // send data to python code
        recv(function (received_json_object) {
            string_to_recv = received_json_object.my_data
            console.log("string_to_recv: " + string_to_recv);
        }).wait(); //block execution till the message is received
        var my_string = Java.use("java.lang.String").$new(string_to_recv);
        this.setText(my_string);
    }
});
import time
import frida
import base64

def 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 = data
        data = 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 object
        print( "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

API List

  • 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動靜態結合分析

Objection

objection啓動並注入內存

objection -d -g package_name explore

sakura@sakuradeMacBook-Pro:~$ 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.4

     Runtime Mobile Exploration
        by: @leonjza from @sensepost

[tab] for command suggestions
com.android.settings on (google: 8.1.0) [usb] #

objection memory

查看內存中加載的module memory list modules
com.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.so
查看庫的導出函數 memory list exports libssl.so
com.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                                  0x7c8ff06a74
dump內存空間
  • memory dump all 文件名
  • memory dump from_base 起始地址 字節數 文件名
搜索內存空間

Usage: memory search "<pattern eg: 41 41 41 ?? 41>" (--string) (--offsets-only)

objection android

內存堆搜索實例 android heap search instances 類名

在堆上搜索類的實例

sakura@sakuradeMacBook-Pro:~$ 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@5b1b0af
調用實例的方法 android heap execute 實例ID 實例方法
查看當前可用的activity或者service android hooking list activities/services
直接啓動activity或者服務 android intent launch_activity/launch_service activity/服務

android intent launch_activity com.android.settings.DisplaySettings
這個命令比較有趣的是用在如果有些設計的不好,可能就直接繞過了密碼鎖屏等直接進去。

com.android.settings on (google: 8.1.0) [usb] # android hooking list services
com.android.settings.SettingsDumpService
com.android.settings.TetherService
com.android.settings.bluetooth.BluetoothPairingService
列出內存中所有的類 android hooking list classes
在內存中所有已加載的類中搜索包含特定關鍵詞的類。 android hooking search classes 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.DisplayManagerGlobal
內存中搜索指定類的所有方法 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

知道名字開始在內存裏搜就很有用

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.setDisplayHomeAsUpEnabled
hook類的方法(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默認是不能用grep過濾的,但是可以通過objection run xxx | grep yyy的方式,從終端通過管道來過濾。
用法如下

sakura@sakuradeMacBook-Pro:~$ 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文件裏的輸出log來查看結果。

sakura@sakuradeMacBook-Pro:~/.objection$ cat *log | grep -i display
android.hardware.display.DisplayManager
android.hardware.display.DisplayManager$DisplayListener
android.hardware.display.DisplayManagerGlobal

案例學習

案例學習case1:《仿VX數據庫原型取證逆向分析》

附件鏈接
android-backup-extractor工具鏈接

sakura@sakuradeMacBook-Pro:~/Desktop/lab/alpha/tools/android/frida_learn$ java -version
java version "1.8.0_141"

sakura@sakuradeMacBook-Pro:~/Desktop/lab/alpha/tools/android/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@sakuradeMacBook-Pro:~/Desktop/lab/alpha/tools/android/frida_learn/apps/com.example.yaphetshan.tencentwelcome$ ls
Encryto.db _manifest  a          db

裝個夜神模擬器玩

sakura@sakuradeMacBook-Pro:/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 -d -g com.example.yaphetshan.tencentwelcome explore

看一下源碼

package net.sqlcipher.database;
...
public abstract class SQLiteOpenHelper {
    ...
    public synchronized SQLiteDatabase getWritableDatabase(char[] cArr) {

也可以objection search一下這個method

...mple.yaphetshan.tencentwelcome on (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.getWritableDatabase

hook一下這個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沒有什麼奇奇怪怪的參數需要我們傳入,主動調用這種策略在循環註冊等地方可能就會有需求8.

 [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)

案例學習case2:主動調用爆破密碼

附件鏈接

因爲直接找Unfortunately,note the right PIN :(找不到,可能是把字符串藏在什麼資源文件裏了。
review代碼之後找到校驗的核心函數,邏輯就是將input編碼一下之後和密碼比較,這肯定是什麼不可逆的加密。

    public static boolean verifyPassword(Context context, String input) {
        if (input.length() != 4) {
            return false;
        }
        byte[] v = encodePassword(input);
        byte[] p = "09042ec2c2c08c4cbece042681caf1d13984f24a".getBytes();
        if (v.length != p.length) {
            return false;
        }
        for (int i = 0; i < v.length; i++) {
            if (v[i] != p[i]) {
                return false;
            }
        }
        return true;
    }

這裏就爆破一下密碼。

frida-ps -U | grep qualification
7660  org.teamsik.ahe17.qualification.easy

frida -U org.teamsik.ahe17.qualification.easy -l force.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

Frida hook基礎(一)

  • 調用靜態函數和調用非靜態函數
  • 設置(同名)成員變量
  • 內部類,枚舉類的函數並hook,trace原型1
  • 查找接口,hook動態加載dex
  • 枚舉class,trace原型2
  • objection不能切換classloader

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="
        }
    })
}

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_varthis.bool_var都要爲true。

if (!static_bool_var || !this.bool_var) {
            super.CheckFailed();
            return;
        }

這樣就要調用setBool_varsetStatic_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 = true
        
        Java.choose("com.example.androiddemo.Activity.FridaActivity3", {
            onMatch: function (instance) {
                instance.bool_var.value = true
                instance._same_name_bool_var.value = true
            },
            onComplete: function () {
                console.log("end")
            }
        })
    })
}

這裏要注意類裏有一個成員函數和成員變量都叫做same_name_bool_var,這種時候在成員變量前加一個_,修改值的形式爲xx.value = yy

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()

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

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 (name, handle){
                if (name.indexOf("com.example.androiddemo.Activity.Frida6") != -1) {
                    console.log("name:" + name + " handle:" + handle)
                    Java.use(name).check.implementation = function () {
                        return true
                    }
                }
            },
            onComplete: function () {
                console.log("end")
            }
        })
    })
}

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")
            }
        })
    })
}

Frida hook基礎(二)

  • spawn/attach
  • 各種主動調用
  • hook函數和hook構造函數
  • 調用棧/簡單腳本
  • 動態加載自己的dex

題目下載地址:
https://github.com/tlamb96/kgb_messenger

spawn/attach

firda的-f參數代表span啓動
frida -U -f com.tlamb96.spetsnazmessenger -l frida_russian.js --no-pause

    /* 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.getPropertySystem.getenv裏獲取,hook住這兩個函數就行。

這裏要注意從資源文件裏找到User的值。

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。

Java.use("com.tlamb96.kgbmessenger.LoginActivity").j.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)
}
...
[Google Pixel::com.tlamb96.spetsnazmessenger]-> FLAG{G&qG13     R0}

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構造函數

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")
        }

Frida hook : 打印棧回溯

打印棧回溯

function printStack(name) {
    Java.perform(function () {
        var Exception = Java.use("java.lang.Exception");
        var ins = Exception.$new("Exception");
        var straces = ins.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=======================

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,並調用裏面的方法。

var dex = Java.openClassFile("/data/local/tmp/classes.dex").load();
        console.log("decode_P:"+Java.use("myapplication.example.com.reversea.reverseA").decode_P());
        console.log("r_to_hex:"+Java.use("myapplication.example.com.reversea.reverseA").r_to_hex());
...
...
decode_P:Boris, give me the password
r_to_hex:0064736c707d6f510020646b73247c4d0068202b4159516700502a214d24675100

Frida打印與參數構造

  • 數組/(字符串)對象數組/gson/Java.array
  • 對象/多態、強轉Java.cast/接口Java.register
  • 泛型、List、Map、Set、迭代打印
  • non-ascii 、 child-gating、rpc 上傳到PC上打印

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", "。");// 如果是二、四句,輸出句號
    }
}
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;
}

這裏的[C是JNI函數簽名

byte[]

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;
}

java array構造

如果不只是想打印出結果,而是要替換原本的參數,就要先自己構造出一個charArray,使用Java.arrayAPI

    /**
     * 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;
public class Water { // 水 類
    public static String flow(Water W) { // 水 的方法
        // SomeSentence
        Log.d("2Object", "water flow: I`m flowing");
        return "water flow: I`m flowing";
    }

    public String still(Water W) { // 水 的方法
        // SomeSentence
        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!";
    }

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

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。

    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())      
    })
}

成員內部類/匿名內部類

看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.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!")
            }
        })
    })

打印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) {
            //打印classloader
            console.log(arg3);
        }
        return clsName;
    }

Frida native hook : NDK開發入門

https://www.jianshu.com/p/87ce6f565d37

  • extern "C"與名稱修飾
    • 通過c++filt工具可以直接還原得到原來的函數名
    • https://zh.wikipedia.org/zh-hans/%E5%90%8D%E5%AD%97%E4%BF%AE%E9%A5%B0
    • 通過extern "C"導出的JNI函數不會被name mangling
  • JNI參數與基本類型
  • 第一個NDK程序

  • JNI log
#define TAG "sakura1328"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__)

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());
}
...
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");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
        Log.d(TAG, stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}

Frida native hook : JNIEnv和反射

以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: sakura

Java反射

總結: 多去讀一下java的反射API。

Java高級特性——反射

  • 查找調用各種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");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView 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屬性,需要設置Accessible
        Field 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 sakura

memory list modules

Frida反調試

這一節的主要內容就是關於反調試的原理和如何反反調試,重要內容還是看文章理解即可。
因爲我並不需要做反調試相關的工作,所以部分內容略過。

Frida native hook : 符號hook JNI、art&libc

Native函數的Java Hook及主動調用

對native函數的java層hook和主動調用和普通java函數完全一致,略過。

jni.h頭文件導入

導入jni.h,先search一下這個文件在哪。

sakura@sakuradeMacBook-Pro:~/Library/Android/sdk$ find ./ -name "jni.h"
.//ndk-bundle/sysroot/usr/include/jni.h

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了,如圖

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的方法在這個代碼可以參考。

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());
            }
        })
    })
}

JNI函數參數、返回值打印和替換

  • libc函數符號hook
  • libc函數參數、返回值打印和替換
    hook libc的也和上面的完全一樣,也不贅述了。
    所以看到這裏,究其本質就是找到導出符號和它所在的so基地址了。
function hook_libc(){
    var pthread_create_addr = null;
    var symbols = Process.findModuleByName("libc.so").enumerateSymbols();
    for(var i = 0;i<symbols.length;i++){
        var symbol = symbols[i].name;
    
        if(symbol.indexOf("pthread_create")>=0){
            //console.log(symbols[i].name);
            //console.log(symbols[i].address);
            pthread_create_addr = symbols[i].address;
        }
    
    }
    console.log("pthread_create_addr,",pthread_create_addr);
    Interceptor.attach(pthread_create_addr,{
        onEnter:function(args){
            console.log("pthread_create_addr args[0],args[1],args[2],args[3]:",args[0],args[1],args[2],args[3]);

        },onLeave:function(retval){
            console.log("retval is:",retval)
        }
    })
}

Frida native hook : JNI_Onload/動態註冊/inline_hook/native層調用棧打印

https://github.com/android/ndk-samples

JNI_Onload/動態註冊原理

詳細的內容參見我寫的文章,這裏只給出栗子。

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;
}

Frida hook RegisterNative

使用下面這個腳本來打印出RegisterNatives的參數,這裏需要注意的是使用了enumerateSymbolsSync,它是enumerateSymbols的同步版本。
另外和我們之前通過Java.vm.tryGetEnv().getStringUtfChars來調用env裏的方法不同。
這裏則是通過將之前找到的getStringUtfChars函數地址和參數信息封裝起來,直接調用,具體的原理我沒有深入分析,先記住用法。
原理其實是一樣的,都是根據符號找到地址,然後hook符號地址,然後打印參數

declare const NativeFunction: NativeFunctionConstructor;

interface NativeFunctionConstructor {
    new(address: NativePointerValue, retType: NativeType, argTypes: NativeType[], abiOrOptions?: NativeABI | NativeFunctionOptions): NativeFunction;
    readonly prototype: NativeFunction;
}
...
var funcGetStringUTFChars = new NativeFunction(addrGetStringUTFChars, "pointer", ["pointer", "pointer", "pointer"]);
var ishook_libart = false;

function hook_libart() {
    if (ishook_libart === true) {
        return;
    }
    var symbols = Module.enumerateSymbolsSync("libart.so");
    var addrGetStringUTFChars = null;
    var addrNewStringUTF = null;
    var addrFindClass = null;
    var addrGetMethodID = null;
    var addrGetStaticMethodID = null;
    var addrGetFieldID = null;
    var addrGetStaticFieldID = null;
    var addrRegisterNatives = null;
    var addrAllocObject = null;
    var addrCallObjectMethod = null;
    var addrGetObjectClass = null;
    var addrReleaseStringUTFChars = null;
    for (var i = 0; i < symbols.length; i++) {
        var symbol = symbols[i];
        if (symbol.name == "_ZN3art3JNI17GetStringUTFCharsEP7_JNIEnvP8_jstringPh") {
            addrGetStringUTFChars = symbol.address;
            console.log("GetStringUTFChars is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI12NewStringUTFEP7_JNIEnvPKc") {
            addrNewStringUTF = symbol.address;
            console.log("NewStringUTF is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI9FindClassEP7_JNIEnvPKc") {
            addrFindClass = symbol.address;
            console.log("FindClass is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI11GetMethodIDEP7_JNIEnvP7_jclassPKcS6_") {
            addrGetMethodID = symbol.address;
            console.log("GetMethodID is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI17GetStaticMethodIDEP7_JNIEnvP7_jclassPKcS6_") {
            addrGetStaticMethodID = symbol.address;
            console.log("GetStaticMethodID is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI10GetFieldIDEP7_JNIEnvP7_jclassPKcS6_") {
            addrGetFieldID = symbol.address;
            console.log("GetFieldID is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI16GetStaticFieldIDEP7_JNIEnvP7_jclassPKcS6_") {
            addrGetStaticFieldID = symbol.address;
            console.log("GetStaticFieldID is at ", symbol.address, symbol.name);
        } else if (symbol.name == "_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi") {
            addrRegisterNatives = symbol.address;
            console.log("RegisterNatives is at ", symbol.address, symbol.name);
        } else if (symbol.name.indexOf("_ZN3art3JNI11AllocObjectEP7_JNIEnvP7_jclass") >= 0) {
            addrAllocObject = symbol.address;
            console.log("AllocObject is at ", symbol.address, symbol.name);
        }  else if (symbol.name.indexOf("_ZN3art3JNI16CallObjectMethodEP7_JNIEnvP8_jobjectP10_jmethodIDz") >= 0) {
            addrCallObjectMethod = symbol.address;
            console.log("CallObjectMethod is at ", symbol.address, symbol.name);
        } else if (symbol.name.indexOf("_ZN3art3JNI14GetObjectClassEP7_JNIEnvP8_jobject") >= 0) {
            addrGetObjectClass = symbol.address;
            console.log("GetObjectClass is at ", symbol.address, symbol.name);
        } else if (symbol.name.indexOf("_ZN3art3JNI21ReleaseStringUTFCharsEP7_JNIEnvP8_jstringPKc") >= 0) {
            addrReleaseStringUTFChars = symbol.address;
            console.log("ReleaseStringUTFChars is at ", symbol.address, symbol.name);
        }
    }

    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                var env = args[0];
                var java_class = args[1];
                
                var funcAllocObject = new NativeFunction(addrAllocObject, "pointer", ["pointer", "pointer"]);
                var funcGetMethodID = new NativeFunction(addrGetMethodID, "pointer", ["pointer", "pointer", "pointer", "pointer"]);
                var funcCallObjectMethod = new NativeFunction(addrCallObjectMethod, "pointer", ["pointer", "pointer", "pointer"]);
                var funcGetObjectClass = new NativeFunction(addrGetObjectClass, "pointer", ["pointer", "pointer"]);
                var funcGetStringUTFChars = new NativeFunction(addrGetStringUTFChars, "pointer", ["pointer", "pointer", "pointer"]);
                var funcReleaseStringUTFChars = new NativeFunction(addrReleaseStringUTFChars, "void", ["pointer", "pointer", "pointer"]);

                var clz_obj = funcAllocObject(env, java_class);
                var mid_getClass = funcGetMethodID(env, java_class, Memory.allocUtf8String("getClass"), Memory.allocUtf8String("()Ljava/lang/Class;"));
                var clz_obj2 = funcCallObjectMethod(env, clz_obj, mid_getClass);
                var cls = funcGetObjectClass(env, clz_obj2);
                var mid_getName = funcGetMethodID(env, cls, Memory.allocUtf8String("getName"), Memory.allocUtf8String("()Ljava/lang/String;"));
                var name_jstring = funcCallObjectMethod(env, clz_obj2, mid_getName);
                var name_pchar = funcGetStringUTFChars(env, name_jstring, ptr(0));
                var class_name = ptr(name_pchar).readCString();
                funcReleaseStringUTFChars(env, name_jstring, name_pchar);

                //console.log(class_name);

                var methods_ptr = ptr(args[2]);

                var method_count = parseInt(args[3]);
                for (var i = 0; i < method_count; i++) {
                    var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                    var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                    var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

                    var name = Memory.readCString(name_ptr);
                    var sig = Memory.readCString(sig_ptr);
                    var find_module = Process.findModuleByAddress(fnPtr_ptr);
                    console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "module_name:", find_module.name, "module_base:", find_module.base, "offset:", ptr(fnPtr_ptr).sub(find_module.base));

                }
            },
            onLeave: function (retval) { }
        });
    }

    ishook_libart = true;
}

hook_libart();

結果很明顯的打印了出來,包括動態註冊的函數的名字,函數簽名,加載地址和在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:sakura

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+0xa81c4           

主動調用去進行方法參數替換

使用Interceptor.replace,不贅述。主要目的還是爲了改掉函數原本的執行行爲,而不是僅僅打印一些信息。

inline hook

inline hook簡單理解就是不是hook函數開始執行的地方,而是hook函數中間執行的指令
整體來說沒什麼區別,就是把找函數符號地址改成從so裏找到偏移,然後加到so基地址上就行,注意一下它的attach的callback。

/**
 * Callback to invoke when an instruction is about to be executed.
 */
type InstructionProbeCallback = (this: InvocationContext, args: InvocationArguments) => void;
type InvocationContext = PortableInvocationContext | WindowsInvocationContext | UnixInvocationContext;

interface PortableInvocationContext {
    /**
     * Return address.
     */
    returnAddress: NativePointer;

    /**
     * CPU registers. You may also update register values by assigning to these keys.
     */
    context: CpuContext;

    /**
     * OS thread ID.
     */
    threadId: ThreadId;

    /**
     * Call depth of relative to other invocations.
     */
    depth: number;

    /**
     * User-defined invocation data. Useful if you want to read an argument in `onEnter` and act on it in `onLeave`.
     */
    [x: string]: any;
}
...
...
interface Arm64CpuContext extends PortableCpuContext {
    x0: NativePointer;
    x1: NativePointer;
    x2: NativePointer;
    x3: NativePointer;
    x4: NativePointer;
    x5: NativePointer;
    x6: NativePointer;
    x7: NativePointer;
    x8: NativePointer;
    x9: NativePointer;
    x10: NativePointer;
    x11: NativePointer;
    x12: NativePointer;
    x13: NativePointer;
    x14: NativePointer;
    x15: NativePointer;
    x16: NativePointer;
    x17: NativePointer;
    x18: NativePointer;
    x19: NativePointer;
    x20: NativePointer;
    x21: NativePointer;
    x22: NativePointer;
    x23: NativePointer;
    x24: NativePointer;
    x25: NativePointer;
    x26: NativePointer;
    x27: NativePointer;
    x28: NativePointer;

    fp: NativePointer;
    lr: NativePointer;
}

我的so是自己編譯的,具體的彙編代碼如下,總之這裏很明顯在775C時,x0裏保存的是一個指向"sakura"這個字符串的指針。(其實我也不是很看得懂arm64了已經,就隨便hook了一下)
所以hook這個指令,然後Memory.readCString(this.context.x0);打印出來,結果如下

.text:000000000000772C ; __unwind {
.text:000000000000772C                 SUB             SP, SP, #0x40
.text:0000000000007730                 STP             X29, X30, [SP,#0x30+var_s0]
.text:0000000000007734                 ADD             X29, SP, #0x30
.text:0000000000007738 ; 6:   v6 = a1;
.text:0000000000007738                 MOV             X8, XZR
.text:000000000000773C                 STUR            X0, [X29,#var_8]
.text:0000000000007740 ; 7:   v5 = a3;
.text:0000000000007740                 STUR            X1, [X29,#var_10]
.text:0000000000007744                 STR             X2, [SP,#0x30+var_18]
.text:0000000000007748 ; 8:   v4 = (const char *)_JNIEnv::GetStringUTFChars(a1, a3, 0LL);
.text:0000000000007748                 LDUR            X0, [X29,#var_8]
.text:000000000000774C                 LDR             X1, [SP,#0x30+var_18]
.text:0000000000007750                 MOV             X2, X8
.text:0000000000007754                 BL              ._ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh ; _JNIEnv::GetStringUTFChars(_jstring *,uchar *)
.text:0000000000007758                 STR             X0, [SP,#0x30+var_20]
.text:000000000000775C ; 9:   if ( (signed int)_JNIEnv::GetStringUTFLength(v6, v5) > 0 )
.text:000000000000775C                 LDUR            X0, [X29,#var_8]
.text:0000000000007760                 LDR             X1, [SP,#0x30+var_18]
function inline_hook() {
    var libnative_lib_addr = Module.findBaseAddress("libnative-lib.so");
    if (libnative_lib_addr) {
        console.log("libnative_lib_addr:", libnative_lib_addr);
        var addr_775C = libnative_lib_addr.add(0x775C);
        console.log("addr_775C:", addr_775C);

        Java.perform(function () {
            Interceptor.attach(addr_775C, {
                onEnter: function (args) {
                    var name = this.context.x0.readCString()
                    console.log("addr_775C OnEnter :", this.returnAddress, name);
                },
                onLeave: function (retval) {
                     console.log("retval is :", retval) 
                }
            })
        })
    }
}
setImmediate(inline_hook())
Attaching...                                                            
libnative_lib_addr: 0x79fabe0000
addr_775C: 0x79fabe775c
TypeError: cannot read property 'apply' of undefined
    at [anon] (../../../frida-gum/bindings/gumjs/duktape.c:56618)
    at frida/runtime/core.js:55
[Google Pixel::myapplication.example.com.ndk_demo]-> addr_775C OnEnter : 0x79fabe7758 sakura
addr_775C OnEnter : 0x79fabe7758 sakura

到這裏已經可以總結一下我目前的學習了,需要補充一些frida api的學習,比如NativePointr里居然有個readCString,這些API是需要再看看的。

Frida native hook : Frida hook native app實戰

  • 反解Frida全端口檢測的native層反調試
    • hook libc的pthread_create函數
  • 反TracePid的native反調試
    • target: 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: 0x14cd
  • initSN
    感覺意思應該是從/sdcard/reg.dat裏讀一個值,然後和EoPAoY62@ElRD進行比較。
    最後setValue,從導出函數看一下,最後推測第一個參數應該是JNIEnv *env,然後就看到了給字段m賦值。


  • saveSN
    這個看上去就是根據str的值,去變換"W3_arE_whO_we_ARE"字符串,然後寫入到/sdcard/reg.dat

結合一下看,只要initSN檢查到/sdcard/reg.dat裏是EoPAoY62@ElRD,應該就會給m設置成1。
只要m的值是1,就能走到work()函數的邏輯。

參考frida的file api

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。

Frida trace四件套

jni trace : trace jni

https://github.com/chame1eon/jnitrace

pip install jnitrace

Requirement 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, jnitrace
  Running setup.py install for hexdump ... done
  Running setup.py install for jnitrace ... done
Successfully installed hexdump-3.3 jnitrace-3.0.8

usage: 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函數
sakura@sakuradeMacBook-Pro:~/Desktop/lab/alpha/tools/android/frida_learn/0620/0620/xman/resources/lib/armeabi-v7a$ jnitrace -l libmyjni.so com.gdufs.xman
Tracing. Press any key to quit...
Traced library "libmyjni.so" loaded from path "/data/app/com.gdufs.xman-X0HkzLhbptSc0tjGZ3yQ2g==/lib/arm".

           /* TID 28890 */
    355 ms [+] JavaVM->GetEnv
    355 ms |- JavaVM*          : 0xefe99140
    355 ms |- void**           : 0xda13e028
    355 ms |:     0xeff312a0
    355 ms |- jint             : 65542
    355 ms |= jint             : 0

    355 ms ------------------------Backtrace------------------------
    355 ms |-> 0xda13a51b: JNI_OnLoad+0x12 (libmyjni.so:0xda139000)


           /* TID 28890 */
    529 ms [+] JNIEnv->FindClass
    529 ms |- JNIEnv*          : 0xeff312a0
    529 ms |- char*            : 0xda13bdef
    529 ms |:     com/gdufs/xman/MyApp
    529 ms |= jclass           : 0x81    { com/gdufs/xman/MyApp }

    529 ms ------------------------Backtrace------------------------
    529 ms |-> 0xda13a539: JNI_OnLoad+0x30 (libmyjni.so:0xda139000)


           /* TID 28890 */
    584 ms [+] JNIEnv->RegisterNatives
    584 ms |- JNIEnv*          : 0xeff312a0
    584 ms |- jclass           : 0x81    { com/gdufs/xman/MyApp }
    584 ms |- JNINativeMethod* : 0xda13e004
    584 ms |:     0xda13a3b1 - initSN()V
    584 ms |:     0xda13a1f9 - saveSN(Ljava/lang/String;)V
    584 ms |:     0xda13a4cd - work()V
    584 ms |- jint             : 3
    584 ms |= jint             : 0

    584 ms ------------------------Backtrace------------------------
    584 ms |-> 0xda13a553: JNI_OnLoad+0x4a (libmyjni.so:0xda139000)


           /* TID 28890 */
    638 ms [+] JNIEnv->FindClass
    638 ms |- JNIEnv*          : 0xeff312a0
    638 ms |- char*            : 0xda13bdef
    638 ms |:     com/gdufs/xman/MyApp
    638 ms |= jclass           : 0x71    { com/gdufs/xman/MyApp }

    638 ms -----------------------Backtrace-----------------------
    638 ms |-> 0xda13a377: setValue+0x12 (libmyjni.so:0xda139000)


           /* TID 28890 */
    688 ms [+] JNIEnv->GetStaticFieldID
    688 ms |- JNIEnv*          : 0xeff312a0
    688 ms |- jclass           : 0x71    { com/gdufs/xman/MyApp }
    688 ms |- char*            : 0xda13be04
    688 ms |:     m
    688 ms |- char*            : 0xda13be06
    688 ms |:     I
    688 ms |= jfieldID         : 0xf1165004    { m:I }

    688 ms -----------------------Backtrace-----------------------
    688 ms |-> 0xda13a38d: setValue+0x28 (libmyjni.so:0xda139000)

strace : trace syscall

https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/strace.html

frida-trace : trace libc(or more)

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")

hook_artmethod : trace java函數調用

https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_artmethod.js

修改AOSP源碼打印

改aosp源碼trace信息

Frida native hook : init_array開發和自動化逆向

init_array原理

常見的保護都會在init_array裏面做,關於其原理,主要閱讀以下文章即可。

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段,然後就可以找到裏面保存的函數地址。


IDA動態調試so

  • 打開要調試的apk,找到入口
sakura@sakuradeMacBook-Pro:~/.gradle/caches$ adb shell dumpsys activity top | grep TASK
TASK com.android.systemui id=29 userId=0
TASK null id=26 userId=0
TASK com.example.ndk_demo id=161 userId=0
  • 啓動apk,並讓設備將處於一個Waiting For Debugger的狀態
    adb shell am start -D -n com.example.ndk_demo/.MainActivity

  • 執行android_server64

sailfish:/data/local/tmp # ./android_server64
IDA Android 64-bit remote debug server(ST) v1.22. Hex-Rays (c) 2004-2017
Listening on 0.0.0.0:23946...
  • 新開一個窗口使用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

sakura@sakuradeMacBook-Pro:~$ adb jdwp
10436
  • 轉發端口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學習筆記這裏。

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就可以讓它不加載反調試程序。

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 : 233

C/C++ hook

//todo

Native/JNI層參數打印和主動調用參數構造

jni的基本類型要通過調用jni相關的api轉化成c++對象,才能打印和調用。
jni主動調用的時候,參數構造有兩種方式,一種是Java.vm.getenv,另一種是hook獲取env之後來調用jni相關的api構造參數。

C/C++編成so並引入Frida調用其中的函數

關注【小白技術社】 完成爬蟲工程師的逆向之路

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章