banner
Silas

REAO

Be a better man
github

[龍哥投稿] Unidbg Hook 大全

本文は Unidbg Hook と Call の知識をまとめたもので、部分的に Hook コードは Frida と Unidbg を対照的に使用しており、Frida に慣れているが Unidbg に不慣れな読者が迅速に入門できるようにしています。サンプルは百度云からダウンロードできます。

リンク:https://pan.baidu.com/s/1ZRPtQrx4QAPEQhrpq6gbgg
提取码:6666

Unidbg の使用やアルゴリズム復元に関するさらなるチュートリアルは、星球でご覧いただけます。

image

一、基礎知識#

1.S0 基アドレスの取得#

Ⅰfrida による基アドレスの取得#

var baseAddr = Module.findBaseAddress("libnative-lib.so");

Ⅱ Unidbg による基アドレスの取得#

// soを仮想メモリにロード
DalvikModule dm = vm.loadLibrary("libnative-lib.so", true);
// ロードされたsoはモジュールに対応
module = dm.getModule();
// Unidbg仮想メモリ内のlibnative-lib.soの基アドレスを出力
System.out.println("baseAddr:"+module.base);

複数の SO がロードされた場合

// 特定のSOのハンドルを取得
Module yourModule = emulator.getMemory().findModule("yourModuleName");
// 基アドレスを出力
System.out.println("baseAddr:"+yourModule.base);

もし一つの SO のみを積極的にロードした場合、その基アドレスは常に 0x40000000 です。これは Unidbg を検出するポイントであり、com/github/unidbg/memory/Memory.java で修正できます。

public interface Memory extends IO, Loader, StackMemory {

    long STACK_BASE = 0xc0000000L;
    int STACK_SIZE_OF_PAGE = 256; // 1024k

	// メモリマッピングの開始アドレスを変更
    long MMAP_BASE = 0x40000000L;

    UnidbgPointer allocateStack(int size);
    UnidbgPointer pointer(long address);
    void setStackPoint(long sp);

2. 関数アドレスの取得#

Ⅰ Frida によるエクスポート関数アドレスの取得#

Module.findExportByName("libc.so", "strcmp")

Ⅱ Unidbg によるエクスポート関数アドレスの取得#

// soを仮想メモリにロード
DalvikModule dm = vm.loadLibrary("libnative-lib.so", true);
// ロードされたlibscmain.soはモジュールに対応
module = dm.getModule();
int address = (int) module.findSymbolByName("funcNmae").getAddress();

Ⅲ Frida による非エクスポート関数アドレスの取得#

var soAddr = Module.findBaseAddress("libnative-lib.so");
var FuncAddr = soAddr.add(0x1768 + 1);

Ⅳ Unidbg による非エクスポート関数アドレスの取得#

// soを仮想メモリにロード
DalvikModule dm = vm.loadLibrary("libnative-lib.so", true);
// ロードされたsoはモジュールに対応
module = dm.getModule();
// offsetはIDAで確認
int offset = 0x1768;
// 実際のアドレス = baseAddr + offset
int address = (int) (module.base + offset);

3.Unidbg Hook の大盤点#

Unidbg は Android 上でサポートされる Hook を二大類に分けることができます。

  • Unidbg に内蔵されたサードパーティ Hook フレームワーク、xHook/Whale/HookZz を含む
  • Unicorn Hook および Unidbg がそれに基づいてラップした Console Debugger

第一類は Unidbg がサポートし内蔵されたサードパーティ Hook フレームワークで、Dobby(前身 HookZz)/Whale のようなインライン Hook フレームワークや、xHook のような PLT Hook フレームワークがあります。Unidbg が Frida をサポートできるかどうかに困惑する方もいるかもしれませんが、私の個人的な見解では現段階では現実的ではありません。Frida は Dobby や xHook よりもはるかに複雑で、Unidbg は現在それを実行できません。それに加えて、Dobby + Whale + xHook も十分に使えますので、Frida が必要というわけではありません。

第二類は Unidbg の基盤エンジンが Unicorn に選択されている場合(デフォルトエンジン)、Unicorn が自ら持つ Hook 機能です。Unicorn は様々なレベルと粒度の Hook を提供しており、メモリ Hook / 命令 / 基本ブロック Hook / 例外 Hook などがあり、非常に強力で使いやすいです。また、Unidbg はそれに基づいて使いやすい Console Debugger をラップしています。

Hook の選択方法は?これは Unidbg の使用目的によります。プロジェクトがシミュレーション実行に使用される場合、Console Debugger を使用して迅速に分析し、エラーを排除し、コードを通した後にサードパーティ Hook フレームワークを使用して永続化することをお勧めします。なぜなら、これは Unidbg がサポートするアセンブリ実行エンジンに由来します。Unidbg は多くの基盤エンジンをサポートしており、最初にデフォルトのエンジンは Unicorn であり、その名前からもわかるように、Unidbg と Unicorn には大きな関係があります。しかし、その後 Unidbg は数個のエンジンをサポートするようになり、プログラムの複雑さを高める行動は、必ず何らかの痛点を解決するためのものです。

hypervisor エンジンはApple Siliconチップを搭載したデバイスでシミュレーション実行を行うことができます;

KVM エンジンは Raspberry Pi でシミュレーション実行を行うことができます;

Dynarmic エンジンはより高速なシミュレーション実行のために設計されています;

Unicorn は最も強力で最も完全なシミュレーション実行エンジンですが、Dynarmic に比べて遅すぎます。同じシーンで、Dynarmic は Unicorn よりも数倍、時には十数倍速くシミュレーション実行を行います。Unidbg を使用して生産環境でのシミュレーション実行を実現するために、速度が最も重要であれば、Dynarmic + **unidbg-boot-server** という高並行性のサーバーが完璧な選択です。一般的な実践では、最初に Unicorn エンジンを使用してシミュレーション実行コードを通し、Dynarmic に切り替えた後、直接生産環境に移行します。

Dynarmic エンジンの使用

private static AndroidEmulator createARMEmulator() {
    return AndroidEmulatorBuilder.for32Bit()
            // Dynarmicエンジンに切り替え
            .addBackendFactory(new DynarmicFactory(true))
            .build();
}

Unicorn デフォルトエンジン

private static AndroidEmulator createARMEmulator() {
    return AndroidEmulatorBuilder.for32Bit()
            .build();
}

Unidbg の第二の使用シーンはアルゴリズム復元の補助です。つまり、シミュレーション実行はアルゴリズム復元の前奏に過ぎず、シミュレーション実行が問題ない場合に Unidbg を使用してアルゴリズム復元を補助します。この場合、自然に Unicorn エンジンを使用し、二大類の Hook 方案を使用できますが、どちらを選ぶか?私は始終第二類方案、つまり Unicorn Hook に基づく方案を使用することを好みます。

私の個人的な見解では、三つの利点があります。

  • HookZz や xHook などの方案は、その Hook 実装原理に基づいて検出できますが、Unicorn のネイティブ Hook は検出されにくいです。
  • Unicorn Hook には制限がなく、他の方案には大きな制限があります。例えば、インライン Hook 方案は短い関数や隣接する二つのアドレスを Hook できません;PLT Hook は Sub_xxx サブ関数を Hook できません。
  • サードパーティのインライン Hook フレームワークとネイティブ Hook 方案を同時に使用すると、バグの火花が摩擦します。実際、Unicorn の一部の Hook 機能を単独で使用してもバグがあります。つまり、ネイティブ Hook を統一して使用することで、バグやトラブルを減らすことができます。

以下のようにまとめます。

Ⅰ シミュレーション実行を目的とする場合#

サードパーティ Hook 方案を使用し、arm32 では HookZz のサポートが良好で、arm64 では Dobby のサポートが良好です。HookZz/Dobby Hook が成功しない場合、関数がエクスポート関数であれば xHook を使用し、そうでなければ Whale を使用します。

Ⅱ アルゴリズム復元を目的とする場合#

Console Debugger と Unicorn Hook を使用し、サードパーティ Hook 方案を優先的に使用しないことをお勧めします。

4. 本篇の基礎コード#

シミュレーション実行のデモコード

package com.tutorial;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.Backend;
import com.github.unidbg.arm.backend.CodeHook;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.hook.whale.IWhale;
import com.github.unidbg.hook.whale.Whale;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import unicorn.ArmConst;
import unicorn.Unicorn;

import java.io.File;

public class hookInUnidbg {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;

    hookInUnidbg() {

        // シミュレータインスタンスを作成
        emulator = AndroidEmulatorBuilder.for32Bit().build();

        // シミュレータのメモリ操作インターフェース
        final Memory memory = emulator.getMemory();
        // システムライブラリの解析を設定
        memory.setLibraryResolver(new AndroidResolver(23));
        // Android仮想マシンを作成
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/tutorial/hookinunidbg.apk"));

//        emulator.attach().addBreakPoint(0x40000000+0xa80);


        // soを仮想メモリにロード
        DalvikModule dm = vm.loadLibrary("hookinunidbg", true);
        // ロードされたlibhookinunidbg.soはモジュールに対応
        module = dm.getModule();

        // JNIOnLoadを実行(もしあれば)
        dm.callJNI_OnLoad(emulator);
    }

    public void call(){
        DvmClass dvmClass = vm.resolveClass("com/example/hookinunidbg/MainActivity");
        String methodSign = "call()V";
        DvmObject<?> dvmObject = dvmClass.newObject(null);

        dvmObject.callJniMethodObject(emulator, methodSign);

    }


    public static void main(String[] args) {
        hookInUnidbg mydemo = new hookInUnidbg();
        mydemo.call();
    }


}

実行時にいくつかのログ出力があり、正常なロジックです。

二、Hook 関数#

demo hookInunidbg では数個の関数が実行され、その中で base64_encode 関数の実行に注目します。

unsigned int
base64_encode(const unsigned char *in, unsigned int inlen, char *out);

パラメータの説明は以下の通りです。

char *out:エンコードされた内容を格納するためのバッファの先頭アドレス。
char *in:元の文字列の先頭アドレス、元の文字列の内容を指します。
int inlen:元の文字列の長さ。
戻り値:正常な場合、変換後の文字列の実際の長さを返します。

このセクションのタスクは、base64 前の内容とエンコード後の内容を印刷することです。

1.Frida#

// Fridaバージョン
function main(){
    // 対象soの基アドレスを取得
    var base_addr = Module.findBaseAddress("libhookinunidbg.so");

    if (base_addr){
        var func_addr = Module.findExportByName("libhookinunidbg.so", "base64_encode");
        console.log("hook base64_encode function")
        Interceptor.attach(func_addr,{
            // 引数を印刷
            onEnter: function (args) {
                console.log("\n input:")
                this.buffer = args[2];
                var length = args[1];
                console.log(hexdump(args[0],{length: length.toUInt32()}))
                console.log("\n")
            },
            // 戻り値を印刷
            onLeave: function () {
                console.log(" output:")
                console.log(this.buffer.readCString());
            }
        })
    }


}

setImmediate(main);

2.Console Debugger#

Console Debugger は迅速な打撃、迅速な検証のインタラクティブデバッガです。

// デバッグ
emulator.attach().addBreakPoint(module.findSymbolByName("base64_encode").getAddress());

いくつかの概念を再確認し、強調する必要があります。

  • 対応するアドレスに到達したときにブレークポイントがトリガーされ、GDB デバッグや IDA デバッグに似ており、タイミングはターゲット命令実行前です。
  • ブレークポイントは関数のさまざまな概念を持たず、ARM アセンブリ命令の観点から関数を理解する必要があります。
  • Console Debugger はアルゴリズム分析を補助するために使用され、特定の関数の機能を迅速に分析、確認するために使用されます。Unicorn エンジンの下でのみ使用できます。

第二条について補足します。

ARM ATPCS 呼び出し規約に従い、パラメータの数が 4 個以下の場合、サブプログラム間で R0〜R3 を介してパラメータを渡します(つまり、R0-R3 はパラメータ 1 - パラメータ 4 を表します)。パラメータの数が 4 個を超える場合、残りのパラメータは sp が指すデータスタックを介して渡されます。関数の戻り値は常に R0 を介して返されます。

ターゲット関数の例では、関数呼び出し前に、呼び出し元は三つのパラメータを R0-R2 に順次配置します。

2.png

即値は直接確認できます。例えば、ここでのパラメータ 2 は 5 です。ポインタであると疑う場合、パラメータ 1 とパラメータ 3 をインタラクティブデバッグ中に mxx を入力して確認します。mrx は Frida の hexdump (xxx) に相当します。ここでの r0 の例では、mr0 または m0x400022e0 でその指向するメモリを確認できます。

Unidbg はデータ表示において、Frida の Hexdump とはいくつかの違いがあります。二つの側面に現れます。

  • Frida の hexdump では、左側の基準アドレスは現在のアドレスから始まりますが、Unidbg は 0 から始まります。
  • Unidbg は印刷されたデータブロックの md5 値を提供し、二つのデータブロックの内容が一致するかどうかを比較しやすく、Unidbg はデータの Hex String を表示し、大量のログの中で検索しやすくします。

Console Debugger は多くのデバッグ、分析コマンドをサポートしており、すべてを以下に示します。

c: continue
n: step over
bt: back trace

st hex: search stack
shw hex: search writable heap
shr hex: search readable heap
shx hex: search executable heap

nb: break at next block
s|si: step into
s[decimal]: execute specified amount instruction
s(blx): execute util BLX mnemonic, low performance

m(op) [size]: show memory, default size is 0x70, size may hex or decimal
mr0-mr7, mfp, mip, msp [size]: show memory of specified register
m(address) [size]: show memory of specified address, address must start with 0x

wr0-wr7, wfp, wip, wsp <value>: write specified register
wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x
wx(address) <hex>: write bytes to memory at specified address, address must start with 0x

b(address): add temporarily breakpoint, address must start with 0x, can be module offset
b: add breakpoint of register PC
r: remove breakpoint of register PC
blr: add temporarily breakpoint of register LR

p (assembly): patch assembly at PC address
where: show java stack trace

trace [begin end]: Set trace instructions
traceRead [begin end]: Set trace memory read
traceWrite [begin end]: Set trace memory write
vm: view loaded modules
vbs: view breakpoints
d|dis: show disassemble
d(0x): show disassemble at specify address
stop: stop emulation
run [arg]: run test
cc size: convert asm from 0x400008a0 - 0x400008a0 + size bytes to c function

Frida コードでは、console.log(hexdump(args[0],{length: args[1].toUInt32()}))を使用してパラメータ 1 が指すメモリブロックをパラメータ 2 の長さで印刷するという効果を表現していますが、Unidbg でも同様に長さを処理できます。

mr0 5

>-----------------------------------------------------------------------------<
[23:41:37 891]r0=RX@0x400022e0[libhookinunidbg.so]0x22e0, md5=f5704182e75d12316f5b729e89a499df, hex=6c696c6163
size: 5
0000: 6C 69 6C 61 63                                     lilac
^-----------------------------------------------------------------------------^

現在、Console Debugger はmr0 r1のような構文をまだサポートしていません。

これで Frida の OnEnter 機能を実現しました。次に、OnLeave のタイミングを取得する必要があります。つまり、関数が実行されたタイミングです。ARM アセンブリでは、LR レジスタにプログラムの戻りアドレスが格納されており、関数が LR が指すアドレスに到達したとき、関数は終了しています。また、ブレークポイントはターゲットアドレスの実行前にトリガーされるため、LR の位置でブレークポイントを設定すると、ターゲット関数が実行を終えた直後にトリガーされます。これが Frida の OnLeave タイミングポイントの原理です。Console Debugger のインタラクティブデバッグ中に、blr コマンドを使用して lr の位置に一時的なブレークポイントを設定できます。これは一度だけトリガーされます。

全体のロジックは以下の通りです。

  • ターゲット関数のアドレスでブレークポイントを設定
  • ブレークポイントに到達したら、Console Debugger のインタラクティブデバッグに入る
  • mxx シリーズでパラメータを確認
  • blr で関数の戻り位置にブレークポイントを設定
  • c でプログラムを続行し、戻り値の位置でブレークします
  • この時のバッファを確認します

注意が必要なのは、onLeave の中で mr2 は無意味です。R2 はプログラムのエントリポイントでパラメータ 3 を表しますが、関数の計算過程では、R2 は汎用レジスタとしてストレージや計算に使用され、バッファのアドレスを指しているわけではありません。Frida では、OnEnter で args [2]、つまり R2 の値を this.buffer に保存し、OnLeave で再度取り出して印刷します。しかし、Console Debugger のインタラクティブデバッグでは、より簡単な方法があります。マウスを上に引っ張って、元の r2 の値が何であるかを確認し、0x401d2000 で m0x401d2000 を確認します。

これで Frida の同等機能を実現しました。少し面倒に聞こえますが、習熟すれば私の意見に同意するでしょう。Console Debugger は最も良く、最も速く、最も安定したデバッグツールです。さらに、Console Debugger は永続的な Hook も行うことができます。コードは以下の通りです。

public void HookByConsoleDebugger(){
    emulator.attach().addBreakPoint(module.findSymbolByName("base64_encode").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            Pointer input = context.getPointerArg(0);
            int length = context.getIntArg(1);
            Pointer buffer = context.getPointerArg(2);

            Inspector.inspect(input.getByteArray(0, length), "base64 input");
            // OnLeave
            emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    String result = buffer.getString(0);
                    System.out.println("base64 result:"+result);
                    return true;
                }
            });
            return true;
        }
    });
}

onHit が true を返すと、ブレークポイントがトリガーされたときにインタラクティブインターフェースに入らず、false の場合は入ります。関数が数百回呼び出された場合、何度も停止しては「c」を押して続行することは望ましくありません。

3. サードパーティ Hook フレームワーク#

以下のターゲット関数はすべて JNIOnLoad の前に呼び出されます。

ⅠxHook#

public void HookByXhook(){
    IxHook xHook = XHookImpl.getInstance(emulator);
    xHook.register("libhookinunidbg.so", "base64_encode", new ReplaceCallback() {
        @Override
        public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
            Pointer input = context.getPointerArg(0);
            int length = context.getIntArg(1);
            Pointer buffer = context.getPointerArg(2);
            Inspector.inspect(input.getByteArray(0, length), "base64 input");
            context.push(buffer);
            return HookStatus.RET(emulator, originFunction);
        }
        @Override
        public void postCall(Emulator<?> emulator, HookContext context) {
            Pointer buffer = context.pop();
            System.out.println("base64 result:"+buffer.getString(0));
        }
    }, true);
    // 効力を持たせる
    xHook.refresh();
}

xHook は愛奇芸がオープンソースした Android PLT Hook フレームワークで、安定して使いやすいのが利点ですが、Sub_xxx サブ関数を Hook できないのが欠点です。

Ⅱ HookZz#

public void HookByHookZz(){
    IHookZz hookZz = HookZz.getInstance(emulator); // HookZzをロードし、インラインHookをサポート
    hookZz.enable_arm_arm64_b_branch(); // テストenable_arm_arm64_b_branch、あってもなくても良い
    hookZz.wrap(module.findSymbolByName("base64_encode"), new WrapCallback<HookZzArm32RegisterContext>() {
        @Override
        public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext context, HookEntryInfo info) {
            Pointer input = context.getPointerArg(0);
            int length = context.getIntArg(1);
            Pointer buffer = context.getPointerArg(2);
            Inspector.inspect(input.getByteArray(0, length), "base64 input");
            context.push(buffer);
        }
        @Override
        public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext context, HookEntryInfo info) {
            Pointer buffer = context.pop();
            System.out.println("base64 result:"+buffer.getString(0));
        }
    });
    hookZz.disable_arm_arm64_b_branch();
}

HookZz も単一行のブレークポイントのような Hook を実現できますが、Unidbg の Hook の大環境下ではあまり役に立たないと感じますので、使用をお勧めしません。

IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.instrument(module.base + 0x978 + 1, new InstrumentCallback<RegisterContext>() {
    @Override
    public void dbiCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
        System.out.println(ctx.getIntArg(0));
    }
});

HookZz は現在 Dobby と呼ばれ、Unidbg では HookZz と Dobby は二つの独立した Hook ライブラリであり、作者は HookZz が arm32 で良好にサポートされ、Dobby が arm64 で良好にサポートされると考えています。HookZz はインライン Hook 方案であるため、Sub_xxx を Hook できますが、短い関数はバグが発生する可能性があり、インライン Hook の原理に制限されます。

Ⅲ Whale#

public void HookByWhale(){
    IWhale whale = Whale.getInstance(emulator);
    whale.inlineHookFunction(module.findSymbolByName("base64_encode"), new ReplaceCallback() {
        Pointer buffer;
        @Override
        public HookStatus onCall(Emulator<?> emulator, long originFunction) {
            RegisterContext context = emulator.getContext();
            Pointer input = context.getPointerArg(0);
            int length = context.getIntArg(1);
            buffer = context.getPointerArg(2);
            Inspector.inspect(input.getByteArray(0, length), "base64 input");
            return HookStatus.RET(emulator, originFunction);
        }

        @Override
        public void postCall(Emulator<?> emulator, HookContext context) {
            System.out.println("base64 result:"+buffer.getString(0));
        }
    }, true);
}

Whale はクロスプラットフォームの Hook フレームワークで、Android Native Hook でもインライン Hook 方案です。具体的な状況についてはあまり詳しくありません。

4.Unicorn Hook#

特定の関数を集中して、高強度で、同時に柔軟にデバッグしたい場合、Unicorn CodeHook は良い選択です。例えば、ターゲット関数の最初の命令の r1、二番目の命令の r2、三番目の命令の r3 を確認したい場合、類似のニーズです。

hook_add_new の最初のパラメータは Hook コールバックで、ここでは CodeHook を選択します。パラメータ 2 は開始アドレス、パラメータ 3 は終了アドレス、パラメータ 4 は一般的に null を設定します。これは、開始アドレスから終了アドレスまでの実行範囲内の各命令を実行前に処理できることを意味します。

ターゲット関数のコード範囲を見つけます。

image

public void HookByUnicorn(){
    long start = module.base+0x97C;
    long end = module.base+0x97C+0x17A;
    emulator.getBackend().hook_add_new(new CodeHook() {
        @Override
        public void hook(Backend backend, long address, int size, Object user) {
            RegisterContext registerContext = emulator.getContext();
            if(address == module.base + 0x97C){
                int r0 = registerContext.getIntByReg(ArmConst.UC_ARM_REG_R0);
                System.out.println("0x97C 处 r0:"+Integer.toHexString(r0));
            }
            if(address == module.base + 0x97C + 2){
                int r2 = registerContext.getIntByReg(ArmConst.UC_ARM_REG_R2);
                System.out.println("0x97C +2 处 r2:"+Integer.toHexString(r2));
            }
            if(address == module.base + 0x97C + 4){

                int r4 = registerContext.getIntByReg(ArmConst.UC_ARM_REG_R4);
                System.out.println("0x97C +4 处 r4:"+Integer.toHexString(r4));
            }
        }

        @Override
        public void onAttach(Unicorn.UnHook unHook) {

        }

        @Override
        public void detach() {

        }
    }, start, end, null);
}

三、パラメータと戻り値の置換#

1. パラメータの置換#

要求:もし入力パラメータが lilac であれば、hello world に変更し、対応する入力パラメータの長さも変更します。正しい結果は **aGVsbG8gd29ybGQ=** です。

ⅠFrida#

// Fridaバージョン
function main(){
    // 対象soの基アドレスを取得
    var base_addr = Module.findBaseAddress("libhookinunidbg.so");

    if (base_addr){
        var func_addr = Module.findExportByName("libhookinunidbg.so", "base64_encode");
        console.log("hook base64_encode function")
        var fakeinput = "hello world"
        var fakeinputPtr = Memory.allocUtf8String(fakeinput);
        Interceptor.attach(func_addr,{
            onEnter: function (args) {
                args[0] = fakeinputPtr;
                args[1] = ptr(fakeinput.length);
                this.buffer = args[2];
            },
            // 戻り値を印刷
            onLeave: function () {
                console.log(" output:")
                console.log(this.buffer.readCString());
            }
        })
    }


}

setImmediate(main);

Ⅱ Console Debugger#

迅速な打撃、迅速な検証の Console Debugger でこの目標を実現するには?

①ブレークポイントを設定し、コードを実行した後にデバッガに入ります。

emulator.attach().addBreakPoint(module.findSymbolByName("base64_encode").getAddress());

②コマンドを介して引数 1 と 2 を変更します。

wx0x40002403 68656c6c6f20776f726c64

>-----------------------------------------------------------------------------<
[14:06:46 165]RX@0x40002403[libhookinunidbg.so]0x2403, md5=5eb63bbbe01eeed093cb22bb8f5acdc3, hex=68656c6c6f20776f726c64
size: 11
0000: 68 65 6C 6C 6F 20 77 6F 72 6C 64                   hello world
^-----------------------------------------------------------------------------^
wr1 11
>>> r1=0xb

Console Debugger は以下の書き込み操作をサポートしています。

wr0-wr7, wfp, wip, wsp <value>: write specified register
wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x
wx(address) <hex>: write bytes to memory at specified address, address must start with 0x

しかし、これは実際には便利ではなく、永続化を行う方が快適です。

public void ReplaceArgByConsoleDebugger(){
    emulator.attach().addBreakPoint(module.findSymbolByName("base64_encode").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            String fakeInput = "hello world";
            int length = fakeInput.length();
            // r1の値を新しい長さに変更
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R1, length);
            MemoryBlock fakeInputBlock = emulator.getMemory().malloc(length, true);
            fakeInputBlock.getPointer().write(fakeInput.getBytes(StandardCharsets.UTF_8));
            // r0を新しい文字列の新しいポインタを指すように変更
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer);

            Pointer buffer = context.getPointerArg(2);
            // OnLeave
            emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    String result = buffer.getString(0);
                    System.out.println("base64 result:"+result);
                    return true;
                }
            });
            return true;
        }
    });
}

Ⅲ 第三者 Hook フレームワーク#

外殻だけが変わります。

① xHook

public void ReplaceArgByXhook(){
    IxHook xHook = XHookImpl.getInstance(emulator);
    xHook.register("libhookinunidbg.so", "base64_encode", new ReplaceCallback() {
        @Override
        public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
            String fakeInput = "hello world";
            int length = fakeInput.length();
            // r1の値を新しい長さに変更
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R1, length);
            MemoryBlock fakeInputBlock = emulator.getMemory().malloc(length, true);
            fakeInputBlock.getPointer().write(fakeInput.getBytes(StandardCharsets.UTF_8));
            // r0を新しい文字列の新しいポインタを指すように変更
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer);

            Pointer buffer = context.getPointerArg(2);
            context.push(buffer);
            return HookStatus.RET(emulator, originFunction);
        }
        @Override
        public void postCall(Emulator<?> emulator, HookContext context) {
            Pointer buffer = context.pop();
            System.out.println("base64 result:"+buffer.getString(0));
        }
    }, true);
    // 効力を持たせる
    xHook.refresh();
}

② HookZz

public void ReplaceArgByHookZz(){
    IHookZz hookZz = HookZz.getInstance(emulator); // HookZzをロードし、インラインHookをサポート
    hookZz.enable_arm_arm64_b_branch(); // テストenable_arm_arm64_b_branch、あってもなくても良い
    hookZz.wrap(module.findSymbolByName("base64_encode"), new WrapCallback<HookZzArm32RegisterContext>() {
        @Override
        public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext context, HookEntryInfo info) {
            Pointer input = context.getPointerArg(0);
            String fakeInput = "hello world";
            input.setString(0, fakeInput);
            context.setR1(fakeInput.length());

            Pointer buffer = context.getPointerArg(2);
            context.push(buffer);
        }
        @Override
        public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext context, HookEntryInfo info) {
            Pointer buffer = context.pop();
            System.out.println("base64 result:"+buffer.getString(0));
        }
    });
    hookZz.disable_arm_arm64_b_branch();
}

HookZz は HookZzArm32RegisterContext を使用できるため、相対的にコードが簡潔になります。

2. 戻り値の変更#

戻り値を変更するロジックはパラメータの置換と同じですが、第四節を引き出すことができるため、詳しく説明します。

デモ中に verifyApkSign 関数があり、常に 1 を返し、APK の検証に失敗します。したがって、目標は 0 を返すようにすることです。

extern "C"
JNIEXPORT void JNICALL
Java_com_example_hookinunidbg_MainActivity_call(JNIEnv *env, jobject thiz) {
    int verifyret = verifyApkSign();
    if(verifyret == 1){
        LOGE("APK sign verify failed!");
    } else{
        LOGE("APK sign verify success!");
    }
    testBase64();
}

extern "C" int verifyApkSign(){
    LOGE("verify apk sign");
    return 1;
};

ⅠFrida#

// Fridaバージョン
function main(){
    // 対象soの基アドレスを取得
    var base_addr = Module.findBaseAddress("libhookinunidbg.so");

    if (base_addr){
        var func_addr = Module.findExportByName("libhookinunidbg.so", "verifyApkSign");
        console.log("hook verifyApkSign function")
        Interceptor.attach(func_addr,{
            onEnter: function (args) {

            },
            // 戻り値を印刷
            onLeave: function (retval) {
                retval.replace(0);
            }
        })
    }


}

setImmediate(main);

Ⅱ Console Debugger#

public void ReplaceRetByConsoleDebugger(){
    emulator.attach().addBreakPoint(module.findSymbolByName("verifyApkSign").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            // OnLeave
            emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0);
                    return true;
                }
            });
            return true;
        }
    });
}

私たちの Hook は効果を発揮しましたが、verifyApkSign 関数内のログはまだ出力されます。特定の状況では、関数の元の実行動作を変更したい場合があり、単にいくつかの情報を印刷したり、引数や戻り値を置換したりするだけではありません。つまり、関数を完全に置換し、元の関数を置き換えて自分の関数を使用する必要があります。

四、関数の置換#

1.Frida#

const verifyApkSignPtr = Module.findExportByName("libhookinunidbg.so", "verifyApkSign");
Interceptor.replace(verifyApkSignPtr, new NativeCallback(() => {
    console.log("replace verifyApkSign Function")
    return 0;
}, 'void', []));

2. サードパーティ Hook フレームワーク#

ここでは xHook のバージョンを示します。

public void ReplaceFuncByHookZz(){
    HookZz hook = HookZz.getInstance(emulator);
    hook.replace(module.findSymbolByName("verifyApkSign").getAddress(), new ReplaceCallback() {
        @Override
        public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
            emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R0,0);
            return HookStatus.RET(emulator,context.getLR());
        }
    });
}

xHook のバージョンは非常に明確で理解しやすいです。

3.Console Debugger#

public void ReplaceFuncByConsoleDebugger(){
    emulator.attach().addBreakPoint(module.findSymbolByName("verifyApkSign").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            System.out.println("関数verifyApkSignを置き換えます");
            RegisterContext registerContext = emulator.getContext();
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, registerContext.getLRPointer().peer);
            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, 0);
            return true;
        }
    });
}

非常に明確で理解しやすいです。

五、関数の呼び出し#

具体的なアルゴリズムを分析する際、柔軟で詳細な分析を行うために、積極的に呼び出す必要があります。

// TODO

1.Frida#

2.Unidbg#

六、パッチとメモリ検索#

1. パッチ#

パッチはバイナリファイルを直接変更することを意味し、パッチの本質は二つの形式です。

  • バイナリファイルのパッチ
  • メモリ内でのパッチ

パッチの適用シーンは多く、いくつかのシーンでは Hook よりも便利です。バイナリファイルの形式はほとんどの人が馴染みがあり、IDA で KeyPatch を使用してパッチを当てる体験は非常に快適です。しかし、ここではメモリパッチに注目します。

image

0x8CA で署名検証関数が呼び出され、第三四節で戻り値や関数の置換方法を使用して処理しますが、実際には 0x8CA でこの 4 バイト命令を変更するのも良い方法です。

image

注意が必要なのは、この記事では arm32 のみを議論し、命令セットは最も一般的な thumb2 を考慮し、arm および arm64 は自己テストできます。

ⅠFrida#

①方法一

var str_name_so = "libhookinunidbg.so";    // Hookするso名
var n_addr_func_offset = 0x8CA;         // Hookする関数のオフセット、thumbは+1

var n_addr_so = Module.findBaseAddress(str_name_so);
var n_addr_assemble = n_addr_so.add(n_addr_func_offset);

Memory.protect(n_addr_assemble, 4, 'rwx'); // メモリ属性を変更し、プログラムセクションを可書きにする
n_addr_assemble.writeByteArray([0x00, 0x20, 0x00, 0xBF]);

しかし、これは最良の実践ではありません。Frida の操作は実際の Android システム上で行われるため、二つの問題があります。

  • 複数のスレッドがターゲットアドレスのメモリを操作する可能性があるか?衝突はないか
  • arm のキャッシュフラッシュメカニズム

そのため、Frida はメモリ内のバイトを変更するためのより安全で信頼性の高い一連の API を提供します。

②方法二

var str_name_so = "libhookinunidbg.so";    // Hookするso名
var n_addr_func_offset = 0x8CA;         // Hookする関数のオフセット、thumbは+1

var n_addr_so = Module.findBaseAddress(str_name_so);
var n_addr_assemble = n_addr_so.add(n_addr_func_offset);

// 安全にアドレスのバイトを変更
Memory.patchCode(n_addr_assemble, 4, function () {
    // thumbの方式でパッチオブジェクトを取得
    var cw = new ThumbWriter(n_addr_assemble);
    // 小端順
    // 00 20
    cw.putInstruction(0x2000)
    // 00 BF
    cw.putInstruction(0xBF00);
    cw.flush(); // メモリフラッシュ
    console.log(hexdump(n_addr_assemble))
});

Ⅱ Unidbg#

Unidbg ではメモリを変更する際に、機械コードを渡すことも、アセンブリ命令を渡すこともできます。

①方法一

public void Patch1(){
    // 00 20 00 bf
    int patchCode = 0xBF002000; // movs r0,0
    emulator.getMemory().pointer(module.base + 0x8CA).setInt(0,patchCode);
}

②方法二

public void Patch2(){
    byte[] patchCode = {0x00, 0x20, 0x00, (byte) 0xBF};
    emulator.getBackend().mem_write(module.base + 0x8CA, patchCode);
}

③方法三

public void Patch3(){
    try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) {
        KeystoneEncoded encoded = keystone.assemble("movs r0,0;nop");
        byte[] patchCode = encoded.getMachineCode();
        emulator.getMemory().pointer(module.base + 0x8CA).write(0, patchCode, 0, patchCode.length);
    }
}

2. メモリ検索#

SO が断片化していると仮定します。例えば、特定の SO の複数のバージョンを分析する必要があり、署名検証や特定のアセンブリをパッチする必要がありますが、アドレスは異なるバージョンで固定されていません。しかし、関数の特徴は固定されています。メモリ検索 + 動的パッチは良い方法であり、異なるバージョンや断片化に適応できます。

特徴的なスニペットを検索する基準は、関数の先頭の十バイトを検索することもあれば、ターゲットアドレスの上下のバイトや他のものを検索することもあります。

image

ⅠFrida#

function searchAndPatch() {
    var module = Process.findModuleByName("libhookinunidbg.so");
    var pattern = "80 b5 6f 46 84 b0 03 90 02 91"
    var matches = Memory.scanSync(module.base, module.size, pattern);
    console.log(matches.length)
    if (matches.length !== 0)
    {
        var n_addr_assemble = matches[0].address.add(10);
        // 安全にアドレスのバイトを変更
        Memory.patchCode(n_addr_assemble, 4, function () {
            // thumbの方式でパッチオブジェクトを取得
            var cw = new ThumbWriter(n_addr_assemble);
            // 小端順
            // 00 20
            cw.putInstruction(0x2000)
            // 00 BF
            cw.putInstruction(0xBF00);
            cw.flush(); // メモリフラッシュ
            console.log(hexdump(n_addr_assemble))
        });
    }
}

setImmediate(searchAndPatch);

Ⅱ Unidbg#

public void SearchAndPatch(){
    byte[] patterns = {(byte) 0x80, (byte) 0xb5,0x6f,0x46, (byte) 0x84, (byte) 0xb0,0x03, (byte) 0x90,0x02, (byte) 0x91};
    Collection<Pointer> pointers = searchMemory(module.base, module.base+module.size, patterns);
    if(pointers.size() > 0){
        try (Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb)) {
            KeystoneEncoded encoded = keystone.assemble("movs r0,0;nop");
            byte[] patchCode = encoded.getMachineCode();
            ((ArrayList<Pointer>) pointers).get(0).write(10, patchCode, 0, patchCode.length);
        }
    }

}

private Collection<Pointer> searchMemory(long start, long end, byte[] data) {
    List<Pointer> pointers = new ArrayList<>();
    for (long i = start, m = end - data.length; i < m; i++) {
        byte[] oneByte = emulator.getBackend().mem_read(i, 1);
        if (data[0] != oneByte[0]) {
            continue;
        }

        if (Arrays.equals(data, emulator.getBackend().mem_read(i, data.length))) {
            pointers.add(UnidbgPointer.pointer(emulator, i));
            i += (data.length - 1);
        }
    }
    return pointers;
}

このセクションの内容は、LIEFを使用してバイナリファイルにパッチを当てることでも実現できます。

七、Hook タイミングが遅すぎる問題#

前述の通り、Hook コードはSO がロードされた後、JNI_OnLoad の前に配置されており、以下の Frida コードと同等のタイミングです。

var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if (android_dlopen_ext != null) {
    Interceptor.attach(android_dlopen_ext, {
        onEnter: function (args) {
            this.hook = false;
            var soName = args[0].readCString();
            if (soName.indexOf("libhookinunidbg.so") !== -1) {
                this.hook = true;
            }
        },
        onLeave: function (retval) {
            if (this.hook) {
                this.hook = false;
                // your code
            }
        }
    });
}

しかし、.init および.init_array セクションにコードロジックが存在する場合(init→init_array→JNIOnLoad)、Hook タイミングが遅すぎることがあります。この場合、Hook タイミングを init の実行前に早める必要があります。

Frida では、これを実現するために linker で作業を行う必要があります。通常の方法は、Linker 内の call_function または call_constructor 関数を Hook することです。一方、Unidbg では、以下のいくつかの方法があります。

私たちのデモ hookInUnidbg を例にとると、init セクションには以下のようなロジックがあります。二つの文字列の大小を比較します。

// コンパイル生成後に.initセクションに配置 [名前は変更できません]
extern "C" void _init(void) {
    char str1[15];
    char str2[15];
    int ret;

    strcpy(str1, "abcdef");
    strcpy(str2, "ABCDEF");

    ret = strcmp(str1, str2);

    if(ret < 0)
    {
        LOGI("str1 小于 str2");
    }
    else if(ret > 0)
    {
        LOGI("str1 大于 str2");
    }
    else
    {
        LOGI("str1 等于 str2");
    }

}

現在、str1 大于 str2と表示されており、私たちの Hook の目標はstr1 小于 str2と表示させることです。

1.libc を事前にロードする#

libc を事前にロードし、strcmp 関数を Hook して戻り値を - 1 に変更するのは一つの方法です。以下は完全なコードで、Console Debugger と HookZz の二つのバージョンを提供します。

package com.tutorial;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import com.sun.jna.Pointer;
import unicorn.ArmConst;
import java.io.File;

public class hookInUnidbg {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final Module moduleLibc;

    hookInUnidbg() {

        // シミュレータインスタンスを作成
        emulator = AndroidEmulatorBuilder.for32Bit().build();

        // シミュレータのメモリ操作インターフェース
        final Memory memory = emulator.getMemory();
        // システムライブラリの解析を設定
        memory.setLibraryResolver(new AndroidResolver(23));
        // Android仮想マシンを作成
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/tutorial/hookinunidbg.apk"));

        // 先にlibc.soをロード
        DalvikModule dmLibc = vm.loadLibrary(new File("unidbg-android/src/main/resources/android/sdk23/lib/libc.so"), true);
        moduleLibc = dmLibc.getModule();

        // hook
        hookStrcmpByUnicorn();
        // または
        // hookStrcmpByHookZz();

        // soを仮想メモリにロード
        DalvikModule dm = vm.loadLibrary("hookinunidbg", true);
        // ロードされたlibhookinunidbg.soはモジュールに対応
        module = dm.getModule();

        // JNIOnLoadを実行(もしあれば)
        dm.callJNI_OnLoad(emulator);
    }

    public void call(){
        DvmClass dvmClass = vm.resolveClass("com/example/hookinunidbg/MainActivity");
        String methodSign = "call()V";
        DvmObject<?> dvmObject = dvmClass.newObject(null);
        dvmObject.callJniMethodObject(emulator, methodSign);

    }


    public static void main(String[] args) {
        hookInUnidbg mydemo = new hookInUnidbg();
        mydemo.call();
    }

    public void hookStrcmpByUnicorn(){
        emulator.attach().addBreakPoint(moduleLibc.findSymbolByName("strcmp").getAddress(), new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                RegisterContext registerContext = emulator.getContext();
                String arg1 = registerContext.getPointerArg(0).getString(0);

                emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
                    @Override
                    public boolean onHit(Emulator<?> emulator, long address) {
                        if(arg1.equals("abcdef")){
                            emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, -1);
                        }
                        return true;
                    }
                });
                return true;
            }
        });
    }

    public void hookStrcmpByHookZz(){
        IHookZz hookZz = HookZz.getInstance(emulator); // HookZzをロードし、インラインHookをサポート
        hookZz.enable_arm_arm64_b_branch(); // テストenable_arm_arm64_b_branch、あってもなくても良い
        hookZz.wrap(moduleLibc.findSymbolByName("strcmp"), new WrapCallback<HookZzArm32RegisterContext>() {
            String arg1;
            @Override
            public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                arg1 = ctx.getPointerArg(0).getString(0);
            }
            @Override
            public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
                if(arg1.equals("abcdef")){
                    ctx.setR0(-1);
                }
            }
        });
        hookZz.disable_arm_arm64_b_branch();
    }
}

しかし、もし Hook したいターゲット関数が libc にない場合、効果がありません。例えば、0x978 でブレークポイントを設定したい場合。

image

2. 固定アドレスでブレークポイントを設定#

これは最も一般的で便利な方法ですが、Unicorn エンジンでのみ使用できます。

vm.loadLibrary でロードされた最初のユーザー SO の基アドレスは 0x40000000 です。したがって、IDA で関数オフセットを確認し、絶対アドレス Console Debugger Hook を設定します。

package com.tutorial;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;

public class hookInUnidbg {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private Module moduleLibc;

    hookInUnidbg() {

        // シミュレータインスタンスを作成
        emulator = AndroidEmulatorBuilder.for32Bit().build();

        // シミュレータのメモリ操作インターフェース
        final Memory memory = emulator.getMemory();
        // システムライブラリの解析を設定
        memory.setLibraryResolver(new AndroidResolver(23));
        // Android仮想マシンを作成
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/tutorial/hookinunidbg.apk"));

        emulator.attach().addBreakPoint(0x40000000 + 0x978);

        // soを仮想メモリにロード
        DalvikModule dm = vm.loadLibrary("hookinunidbg", true);
        // ロードされたlibhookinunidbg.soはモジュールに対応
        module = dm.getModule();

        // JNIOnLoadを実行(もしあれば)
        dm.callJNI_OnLoad(emulator);
    }

    public void call(){
        DvmClass dvmClass = vm.resolveClass("com/example/hookinunidbg/MainActivity");
        String methodSign = "call()V";
        DvmObject<?> dvmObject = dvmClass.newObject(null);
        dvmObject.callJniMethodObject(emulator, methodSign);

    }


    public static void main(String[] args) {
        hookInUnidbg mydemo = new hookInUnidbg();
        mydemo.call();
    }
    
}

image

もし複数のユーザー SO がロードされた場合、まずコードを一度実行してターゲット SO の基アドレスを確認します(Unidbg ではアドレスのランダム化は存在せず、ターゲット関数のアドレスは毎回固定です)。その後、loadLibrary の前にそのアドレスを Hook すれば、Hook が漏れることはありません。

3.Unidbg が提供するモジュールリスナーを使用#

独自のモジュールリスナーを実装します。

package com.tutorial;

import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.ModuleListener;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.InstrumentCallback;

public class MyModuleListener implements ModuleListener {
    private HookZz hook;

    @Override
    public void onLoaded(Emulator<?> emulator, Module module) {
        // Hookフレームワークを事前にロード
        if(module.name.equals("libc.so")){
             hook = HookZz.getInstance(emulator);
        }

        // ターゲット関数でHook
        if(module.name.equals("libhookinunidbg.so")){
            hook.instrument(module.base + 0x978 + 1, new InstrumentCallback<RegisterContext>() {
                @Override
                public void dbiCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
                    System.out.println(ctx.getIntArg(0));
                }
            });
        }
    }
}

memory.addModuleListenerでバインドします。

package com.tutorial;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import java.io.File;

public class hookInUnidbg{
    private final AndroidEmulator emulator;
    private final VM vm;

    hookInUnidbg() {

        // シミュレータインスタンスを作成
        emulator = AndroidEmulatorBuilder.for32Bit().build();

        // シミュレータのメモリ操作インターフェース
        final Memory memory = emulator.getMemory();

        // モジュールロードリスナーを追加
        memory.addModuleListener(new MyModuleListener());

        // システムライブラリの解析を設定
        memory.setLibraryResolver(new AndroidResolver(23));
        // Android仮想マシンを作成
        vm = emulator.createDalvikVM(new File("unidbg-android/src/test/resources/tutorial/hookinunidbg.apk"));

        // soを仮想メモリにロード
        DalvikModule dm = vm.loadLibrary("hookinunidbg", true);
        // ロードされたlibhookinunidbg.soはモジュールに対応
        Module module = dm.getModule();

        // JNIOnLoadを実行(もしあれば)
        dm.callJNI_OnLoad(emulator);
    }

    public void call(){
        DvmClass dvmClass = vm.resolveClass("com/example/hookinunidbg/MainActivity");
        String methodSign = "call()V";
        DvmObject<?> dvmObject = dvmClass.newObject(null);
        dvmObject.callJniMethodObject(emulator, methodSign);

    }


    public static void main(String[] args) {
        hookInUnidbg mydemo = new hookInUnidbg();
        mydemo.call();
    }

}

各方法には対応する使用シーンがあり、必要に応じて使用します。さらに、Unidbg のソースコードを修正して callInitFunction 関数の前に独自のロジックを追加することもできます。

八、条件ブレークポイント#

アルゴリズム分析時に条件ブレークポイントは干渉情報を減らすことができます。strcmp を例にとると、全プロセスのすべてのモジュールが strcmp 関数を呼び出す可能性があります。

1.S0 に限定#

ⅠFrida#

Interceptor.attach(
    Module.findExportByName("libc.so", "strcmp"), {
        onEnter: function(args) {
            var moduleName = Process.getModuleByAddress(this.returnAddress).name;
            console.log("strcmp arg1:"+args[0].readCString())
            // moduleNameでフィルタリングして印刷できます
            console.log("call from :"+moduleName)
        },
        onLeave: function(ret) {
        }
    }
);

Ⅱ Unidbg#

public void hookstrcmp(){
    long address = module.findSymbolByName("strcmp").getAddress();
    emulator.attach().addBreakPoint(address, new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();
            String arg1 = registerContext.getPointerArg(0).getString(0);
            String moduleName = emulator.getMemory().findModuleByAddress(registerContext.getLRPointer().peer).name;
            if(moduleName.equals("libhookinunidbg.so")){
                System.out.println("strcmp arg1:"+arg1);
            }
            return true;
        }
    });
}

2. 特定の関数に限定#

例えば、特定の関数が SO 内で多く使用されている場合、今はこの関数が関数 a 内で使用されているのを分析したいだけです。

ⅠFrida#

var show = false;
Interceptor.attach(
    Module.findExportByName("libc.so", "strcmp"), {
        onEnter: function(args) {
            if(show){
                console.log("strcmp arg1:"+args[0].readCString())
            }
        },
        onLeave: function(ret) {

        }
    }
);

Interceptor.attach(
    Module.findExportByName("libhookinunidbg.so", "targetfunction"),{
        onEnter: function(args) {
            show = this;
        },
        onLeave: function(ret) {
            show = false;
        }
    }
)

Ⅱ Unidbg#

// 早くからグローバル変数を宣言 public boolean show = false;

public void hookstrcmp(){
    emulator.attach().addBreakPoint(module.findSymbolByName("targetfunction").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();

            show = true;
            emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {
                @Override
                public boolean onHit(Emulator<?> emulator, long address) {
                    show = false;
                    return true;
                }
            });
            return true;
        }
    });

    emulator.attach().addBreakPoint(module.findSymbolByName("strcmp").getAddress(), new BreakPointCallback() {
        @Override
        public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext registerContext = emulator.getContext();
            String arg1 = registerContext.getPointerArg(0).getString(0);
            if(show){
                System.out.println("strcmp arg1:"+arg1);
            }
            return true;
        }
    });
}

3. 特定の場所に限定#

image

例えば、上の図のように、0xA00 で発生する strcmp だけに注目したい場合。一つの方法は strcmp 関数を Hook し、lr レジスタが module.base + 0xA00 + 4 + 1 のときに出力を印刷することです。

もう一つの方法は Console Debugger で、非常に便利です。

emulator.attach().addBreakPoint(module, 0xA00);
emulator.attach().addBreakPoint(module, 0xA04);

これらの知識を習得し、柔軟に応用できるようにしてください。実戦では、「A hook が有効になった後に B 関数の出力を印刷する」というのは非常に一般的です。そうでなければ、各関数が数百行印刷され、見る人が混乱してしまいます。

九、システムコールのインターセプト —— 時間の例#

ここで言うシステムコールのインターセプトは、システムコールを Hook することではありません。例えば、frida - syscall - intercceptorのように、すべてのシステムコールは Unidbg 自身が実装しており、ログを開くだけで確認でき、明らかに Hook する必要はありません。Unidbg のシステムコールのインターセプトは、システムコールを置換し、Unidbg 内のシステムコールの実装を変更するためのものです。

二つの問題を説明する必要があります。

  • なぜシステムコールを変更するのか?

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。