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. 獲取 SO 基地址#

Ⅰfrida 獲取基地址#

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

Ⅱ Unidbg 獲取基地址#

// 加載so到虛擬內存
DalvikModule dm = vm.loadLibrary("libnative-lib.so", true);
// 加載好的so對應為一個模塊
module = dm.getModule();
// 打印libnative-lib.so在Unidbg虛擬內存中的基地址
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 這樣的 Inline 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 引擎可以在樹莓派上模擬執行;

Dynarmic 引擎是為了更快的模擬執行;

Unicorn 是最強大最完善的模擬執行引擎,但它相比 Dynarmic 太慢了,同場景下,Dynarmic 比 Unicorn 模擬執行快數倍甚至十數倍。如果使用 Unidbg 是為了實現生產環境下的模擬執行,速度最重要,那麼 Dynarmic + unidbg-boot-server 這個高並發 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 沒有局限,其他方案局限性較大。比如 Inline Hook 方案不能 Hook 短函數,或者兩個相鄰的地址;PLT Hook 不能 Hook Sub_xxx 子函數。
  • 第三方 inline Hook 框架和原生 Hook 方案同時使用時會摩擦出 BUG 的火花,事實上,單使用 Unicorn 的某些 Hook 功能都有 BUG。所以說,統一用原生 Hook 會少一些 BUG,少一些麻煩。

總結如下

Ⅰ 以模擬執行為目的#

使用第三方 Hook 方案,arm32 下 HookZz 的支持較好,arm64 下 Dobby 的支持較好,HookZz/Dobby Hook 不成功時,如果函數是導出函數,使用 xHook,否則使用 Whale。

Ⅱ 以算法還原為目的#

使用 Console Debugger 和 Unicorn Hook,並建議不優先使用第三方 Hook 方案。

4. 本篇的基礎代碼#

即模擬執行 demo 的代碼

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:一塊 buffer 的首地址,用來存放轉碼後的內容。

char *in:原字符串的首地址,指向原字符串內容。

int inlen:原字符串長度。

返回值:正常情況下返回轉換後字符串的實際長度。

本節的任務就是打印 base64 前的內容,以及編碼後的內容。

1.Frida#

// Frida Version
function main(){
    // get base address of target 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 快速打擊、快速驗證 的交互調試器

// debug
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 使程序繼續運行,到返回值處斷下
  • 查看此時的 buffer

需要注意的是,在 onLeave 中 mr2 是胡鬧。R2 只在程序入口處表示參數 3,在函數運算的過程中,R2 作為通用寄存器被用於存儲、運算,它已經不是指向 buffer 的地址了。在 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 返回 ture 時,斷點觸發時不會進入交互界面;為 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 框架,優點是挺穩定好用,缺點是不能 Hook Sub_xxx 子函數。

Ⅱ HookZz#

public void HookByHookZz(){
    IHookZz hookZz = HookZz.getInstance(emulator); // 加載HookZz,支持inline 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 是 inline hook 方案,因此可以 Hook Sub_xxx,缺點是短函數可能出 bug,受限於 inline 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 框架,在 Andorid Native Hook 上也是 inline Hook 方案,具體情況我了解的不多。

4.Unicorn Hook#

如果想對某個函數進行集中的、高強度的、同時又靈活的調試,Unicorn CodeHook 是一個好選擇。比如我想查看目標函數第一條指令的 r1,第二條指令的 r2,第三條指令的 r3,類似於這種需求。

hook_add_new 第一个參數是 Hook 回調,我們這裡選擇 CodeHook,它是逐條指令 Hook,參數 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);
}

三、Replace 參數和返回值#

1. 替換參數#

需求:如果入參為 lilac,改為 hello world,對應的入參長度也要改。正確結果是 aGVsbG8gd29ybGQ=

ⅠFrida#

// Frida Version
function main(){
    // get base address of target 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 如何實現這一目標?

①下斷點,運行代碼後進入 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,支持inline 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();
}

因為可以用 HookZzArm32RegisterContext,相對來說代碼簡單一些。

2. 修改返回值#

修改返回值的邏輯和替換參數並沒什麼區別,但它可以引出第四節,所以還是仔細講一下。

在 demo 中,有一個 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 Version
function main(){
    // get base address of target 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 函數里的 log 還是打印出來了。在一些情況中,我們想改掉函數原本的執行行為,而不是僅僅打印一些信息或者替換入參和返回值。即需要徹底的函數替換 —— 替換原有函數,使用自己的函數。

四、替換函數#

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 的版本很清晰易懂,我們做了兩件事

  • R0 賦值為 0
  • LR 賦值給 PC,這意味著函數一行不執行就返回了,又因為 R0 賦值 0 所以返回值為 0。

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

非常清晰易懂。

五、Call 函數#

分析具體算法時,常需要對其進行主動調用,進行更靈活和細致的分析

// TODO

1.Frida#

2.Unidbg#

六、Patch 與內存檢索#

1.Patch#

Patch 就是直接對二進制文件進行修改,Patch 本質上只有兩種形式

  • patch 二進制文件
  • 在內存裡 patch

Patch 的應用場景很多,在一些場景比 Hook 更好用,這就是需要介紹它的原因。Patch 二進制文件的形式是大多數人所熟悉的,在 IDA 中使用 KeyPatch 打補丁的體驗很友好。但這裡我們關注的是內存 Patch。

image

0x8CA 處調用了一個簽名校驗函數,第三四節中通過 Replace 返回值或函數的方式來處理它,但實際上,修改 0x8CA 處這條四字節指令也是好辦法。

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

但這並不是最佳實踐,因為相較於 Unidbg,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);

// safely modify bytes at address
Memory.patchCode(n_addr_assemble, 4, function () {
    // 以 thumb的方式獲取一個patch對象
    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 的多個版本,需要 Patch 簽名校驗或者某處匯編,地址在不同版本不固定,但函數特徵固定,內存檢索 + 動態 Patch 就是一個好辦法,可以很好適應不同版本、碎片化。

搜索特徵片段依據需求,可能是搜索函數開頭十字節,也可能是搜索目標地址上下字節或者其他。

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);
        // safely modify bytes at address
        Memory.patchCode(n_addr_assemble, 4, function () {
            // 以 thumb的方式獲取一個patch對象
            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 Patch 二進制文件實現。

七、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 中做文章,通常做法是 Hook Linker 中的 call_function 或 call_constructor 函數。而在 Unidbg 中,有以下一些辦法。

以我們的 demo 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,然後 hook strcmp 函數,修改其返回值為 - 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 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,支持inline 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. 限定於某 SO#

Ⅰ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。一個辦法是 hook strcmp 函數,只在 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 中系統調用的實現。

有兩個問題需要解釋

  • 為什麼要修改系統調用?

    Unidbg 中部分系統調用沒實現或者沒實現好,以及有時候想要固定其輸出,比如獲取時間的系統調用,這些需求需要我們修復或修改 Unidbg 中系統調用的實現。

  • 為什麼不直接修改 Unidbg 源碼

    1 是靈活性較差,2 是我們的實現或修改並不是完美的,直接改 Unidbg 源碼是對運行環境的污染,影響其他項目。

在分析算法時,輸入不變的前提下,如果輸出在不停變化,會干擾算法分析,這種情況的一大來源是時間戳參與了運算。在 Frida 中,為了控制這種干擾因素,常常會 Hook libc 的 gettimeodfay 這個時間獲取函數。

1.Frida#

hook time

var time = Module.findExportByName(null, "time");
if (time != null) {
    Interceptor.attach(time, {
        onEnter: function (args) {

        },
        onLeave: function (retval) {
            // time返回秒級時間戳,修改返回值為100
            retval.replace(100);
        }
    })
}

hook gettimeofday

function hook_gettimeofday() {
    var addr_gettimeofday = Module.findExportByName(null, "gettimeofday");
    var gettimeofday = new NativeFunction(addr_gettimeofday, "int", ["pointer", "pointer"]);
    
    Interceptor.replace(addr_gettimeofday, new NativeCallback(function (ptr_tz, ptr_tzp) {

        var result = gettimeofday(ptr_tz, ptr_tzp);
        if (result == 0) {
            console.log("hook gettimeofday:", ptr_tz, ptr_tzp, result);
            var t = new Int32Array(ArrayBuffer.wrap(ptr_tz, 8));
            t[0] = 0xAAAA;
            t[1] = 0xBBBB;
            console.log(hexdump(ptr_tz));
        }
        return result;
    }, "int", ["pointer", "pointer"]));
}

但 Frida 做這件事並不容易做圓滿,單是 libc.so,就有 time、gettimeodfay、clock_gettime、clock 這四個庫函數可以獲取時間戳,而且樣本可以通過內聯匯編使用系統調用,獲取時間戳。

2.Unidbg#

Unidbg 中可以更方便、更大範圍的固定時間,不必像 Frida 那般。time 和 gettimeodfay 庫函數基於 gettimeodfay 這個系統調用,clock_gettime 和 clock 基於 clock_gettime 系統調用。所以只要在 Unidbg 中固定 gettimeodfay 和 clock_gettime 這兩個系統調用獲取的時間戳,就可以一勞永逸。

首先實現時間相關的系統調用處理器,其中的 * System.currentTimeMillis ()System.nanoTime ()* 改成定數。

package com.tutorial;

import com.github.unidbg.Emulator;
import com.github.unidbg.linux.ARM32SyscallHandler;
import com.github.unidbg.memory.SvcMemory;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.unix.struct.TimeVal32;
import com.github.unidbg.unix.struct.TimeZone;
import com.sun.jna.Pointer;
import unicorn.ArmConst;

import java.util.Calendar;

public class TimeSyscallHandler extends ARM32SyscallHandler {
    public TimeSyscallHandler(SvcMemory svcMemory) {
        super(svcMemory);
    }

    @Override
    protected boolean handleUnknownSyscall(Emulator emulator, int NR) {
        switch (NR) {
            case 78:
                // gettimeofday
                mygettimeofday(emulator);
                return true;
            case 263:
                // clock_gettime
                myclock_gettime(emulator);
                return true;

        }

        return super.handleUnknownSyscall(emulator, NR);
    }


    private void mygettimeofday(Emulator<?> emulator) {
        Pointer tv = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R0);
        Pointer tz = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R1);
        emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, mygettimeofday(tv, tz));
    };

    private int mygettimeofday(Pointer tv, Pointer tz) {
        long currentTimeMillis = System.currentTimeMillis();

        long tv_sec = currentTimeMillis / 1000;
        long tv_usec = (currentTimeMillis % 1000) * 1000;

        TimeVal32 timeVal = new TimeVal32(tv);
        timeVal.tv_sec = (int) tv_sec;
        timeVal.tv_usec = (int) tv_usec;
        timeVal.pack();

        if (tz != null) {
            Calendar calendar = Calendar.getInstance();
            int tz_minuteswest = -(calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000);
            TimeZone timeZone = new TimeZone(tz);
            timeZone.tz_minuteswest = tz_minuteswest;
            timeZone.tz_dsttime = 0;
            timeZone.pack();
        }
        return 0;
    }

    private static final int CLOCK_REALTIME = 0;
    private static final int CLOCK_MONOTONIC = 1;
    private static final int CLOCK_THREAD_CPUTIME_ID = 3;
    private static final int CLOCK_MONOTONIC_RAW = 4;
    private static final int CLOCK_MONOTONIC_COARSE = 6;
    private static final int CLOCK_BOOTTIME = 7;
    private final long nanoTime = System.nanoTime();

    private int myclock_gettime(Emulator<?> emulator) {
        int clk_id = emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0).intValue();
        Pointer tp = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R1);
        long offset = clk_id == CLOCK_REALTIME ? System.currentTimeMillis() * 1000000L : System.nanoTime() - nanoTime;
        long tv_sec = offset / 1000000000L;
        long tv_nsec = offset % 1000000000L;

        switch (clk_id) {
            case CLOCK_REALTIME:
            case CLOCK_MONOTONIC:
            case CLOCK_MONOTONIC_RAW:
            case CLOCK_MONOTONIC_COARSE:
            case CLOCK_BOOTTIME:
                tp.setInt(0, (int) tv_sec);
                tp.setInt(4, (int) tv_nsec);
                return 0;
            case CLOCK_THREAD_CPUTIME_ID:
                tp.setInt(0, 0);
                tp.setInt(4, 1);
                return 0;
        }
        throw new UnsupportedOperationException("clk_id=" + clk_id);
    }
}

在自己的模擬器上使用它,

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。