Unicornエミュレーションの基本とライブラリ関数呼び出しの実装方法とコード例

実行環境:python 基本インポート:from unicorn import *

概要

1. Unicornオブジェクトの初期化:
UC = Uc(unicorn_const.UC_ARCH_X86, unicorn_const.UC_MODE_16)

Ucが受け取る2つの値は、それぞれシミュレートするアーキテクチャとビット数を指定します。後続の操作で使用するレジスタ(rax、eax、axなど)は、プログラムのビット数に厳密に一致させる必要があります。

2. メモリマッピング
# ベースアドレス
BASE_ADDR = 0x0
# メモリサイズ
MEM_SIZE = 16 * 1024
UC.mem_map(BASE_ADDR, MEM_SIZE)

mem_mapはUCに「有効な」領域を割り当てます。この範囲外のアドレスへの操作は、Invalid memory operation (UC_ERR_READ_UNMAPPED)エラーが発生する可能性があります。

3. セグメント初期化/メモリ書き込み
BASE_ADDR = 0x0
MEM_SIZE = 16 * 1024
MEM = b'0' * MEM_SIZE
UC.mem_map(BASE_ADDR, MEM_SIZE)
UC.mem_write(BASE_ADDR, MEM)

これはメモリ割り当ての継続です。unicornはマッピング後にメモリ空間を初期化しません。そのため、ここでは割り当てたメモリ領全体を0で上書きし、潜在的な問題を回避します。 セグメントを割り当てる前に、対象セグメントの物理オフセットと仮想アドレスを取得する必要があります。PEツール(DIEなど)を使用して確認してください。 コード:

RDATA_OFFSET = 0x6000
RDATA_LEN = 0x800
FILE = open(f".\\test.bin", "rb")
FILE.seek(RDATA_OFFSET)
RDATA = FILE.read(RDATA_LEN)
UC.mem_write(BASE_ADDR + RDATA_OFFSET, RDATA)

4. スタック割り当て

一般的にはコードセグメントの後に追加するか、メモリ空間の末尾をスタック領域として使用します。ここで注意すべきは、割り当てたスタック空間は比較的自由ですが、spレジスタの値を適切に設定する必要があります。

    STACK = b'0' * 1024
    STACK_POINT = BASE_ADDR + CODE_LEN + 1024
    UC.reg_write(unicorn.x86_const.UC_X86_REG_SP, STACK_POINT)
    UC.mem_write(STACK_POINT, STACK)

同様に、ここでもスタック領域の初期化が行われています。

5. レジスタ割り当て
STACK_POINT = BASE_ADDR + CODE_LEN + 1024
UC.reg_read(unicorn.x86_const.UC_X86_REG_IP)
UC.reg_write(unicorn.x86_const.UC_X86_REG_SP, STACK_POINT)

unicornパックをインポートした後、レジスタ定数はunicorn.x86_const.で呼び出すか、直接:mips_const.UC_MIPS_REG_15のように使用できます。 注意点: pythonパッケージにはデフォルトでriscv定数が含まれていない場合があり、自分でインポートする必要があります

# Unicorn Python bindings, by Nguyen Anh Quynnh <aquynh@gmail.com>

from . import arm_const, arm64_const, mips_const, sparc_const, m68k_const, x86_const

from .unicorn_const import *

from .unicorn import Uc, uc_version, uc_arch_supported, version_bind, debug, UcError, __version__

6. フックとトレース
def trace(mu: Uc, address, size, data):
	md = cs.Cs(cs.CS_ARCH_X86, cs.CS_MODE_16)
    EIP = mu.reg_read(unicorn.x86_const.UC_X86_REG_EIP)
    CODE = [i for i in md.disasm(mu.mem_read(address, size), size)][0]
    print(">>> EIP : %x" % (EIP), CODE.mnemonic, CODE.op_str)

これは基本的なunicornトレースコールバック関数のテンプレートです。ここで、これは「trace」コールバック関数のテンプレートです。unicornは異なる状況のフックに対して、異なるパラメータのコールバック関数を必要とします。例:

    UC.hook_add(UC_HOOK_CODE, trace)  # 各命令をフックし、ステップバイステップでトレース
    UC.hook_add(UC_HOOK_MEM_FETCH_UNMAPPED, printf)
    
def trace(mu: Uc, address, size, data):
    EIP = mu.reg_read(unicorn.x86_const.UC_X86_REG_EIP)
    CODE = [i for i in md.disasm(mu.mem_read(address, size), size)][0]
    print(">>> EIP : %x" % (EIP), CODE.mnemonic, CODE.op_str)

def printf(mu: Uc, access, address, size, value, user_data):

    ESP = mu.reg_read(unicorn.x86_const.UC_X86_REG_ESP)
    # ここでの値は即値
    VALUE = bytes(mu.mem_read(ESP + 8, 4))
    VALUE = int.from_bytes(VALUE, 'little')
    # フォーマットはポインタで、アドレスはrdataセグメントに属する
    FORMAT = mu.mem_read(int.from_bytes(
        bytes(mu.mem_read(ESP + 4, 4)), 'little'), 4)
    # フォーマットの種類を確認
    if bytes(FORMAT)[0:2] == b'%d':
        print(">>> call print : %x" % (VALUE))
    # printfの呼び出し元から再実行
    OLD_IP = mu.mem_read(ESP, 4)  # 戻りアドレスを取得
    OLD_IP = int.from_bytes(bytes(OLD_IP), 'little')
    mu.emu_stop()
    try:
        mu.emu_start(OLD_IP, 2 * 1024 * 1024)
    except UcError as e:
        print("ERROR: %s" % e)

7. エミュレーションの開始
    START = ADDRESS + 0x41E

    try:

        UC.emu_start(START, 2 * 1024 * 1024)

    except UcError as e:

        print("ERROR ", e)

emu_startの2番目のパラメータは実行の終了アドレスと考えることができます。最終実行アドレスと一致させるか、単純にメモリの終わりなどのerrorに設定できます。

外部ライブラリ関数のUnicornでの実行

Unicornで外部関数を呼び出すプログラムをシミュレートすることはお勧めしません。 しかし、どうしても実行する必要がある場合は、いくつかの方法があります: ①pywinを使用してライブラリ関数を呼び出し、元の関数を置き換える。 ②ctypesを使用してライブラリ関数を呼び出し、元の関数を置き換える。 ③指定アドレスをフック|トレース時にチェックする。 ④アドレス異常をフックする。

実装例:

#include<stdio.h>

int main(){

    int a,b;

    a = 1;

    b = 2;

    a += b;

    printf("%d",a);

}

from unicorn import *

  
  

def test():

    # 定数定義、グローバルに移動可能
    BASE_ADDRESS = 0x400000
    CODE_LENGTH = 0x3200
    RDATA_LENGTH = 0x800
    RDATA_OFFSET = 0x6000
    MEMORY_SIZE = 4 * 1024 * 1024
    MEMORY = b'0' * MEMORY_SIZE

    # ターゲットプログラムの読み込み
    FILE = open(f".\\target_app.exe", "rb")

    # コードセグメントの設定
    UC = Uc(unicorn_const.UC_ARCH_X86, unicorn_const.UC_MODE_32)

    UC.mem_map(BASE_ADDRESS, MEMORY_SIZE)

  

    FILE.seek(0x400)

    CODE = FILE.read(CODE_LENGTH)

    UC.mem_write(BASE_ADDRESS, MEMORY)  # メモリ空間の初期化、厳密な空間管理の場合はここで初期化する必要はない

    UC.mem_write(BASE_ADDRESS, CODE)  # コードセグメントの書き込み

    # RDATAセグメントの設定
    FILE.seek(0x3800)

    RDATA = FILE.read(RDATA_LENGTH)

    UC.mem_write(BASE_ADDRESS + RDATA_OFFSET, RDATA)

    # スタック領域の設定
    STACK = b'0' * 1024

    STACK_POINTER = BASE_ADDRESS + CODE_LENGTH + 1024

    UC.reg_write(unicorn.x86_const.UC_X86_REG_ESP, STACK_POINTER)

    UC.mem_write(STACK_POINTER, STACK)

    # フックの設定
    UC.hook_add(UC_HOOK_CODE, trace)

    UC.hook_add(UC_HOOK_MEM_FETCH_UNMAPPED, printf)

    # エミュレーションの開始
    START_POINT = BASE_ADDRESS + 0x41E

    try:

        UC.emu_start(START_POINT, 2 * 1024 * 1024)

    except UcError as e:

        print("ERROR ", e)

  
  

def trace(mu: Uc, address, size, data):

  

    EIP = mu.reg_read(unicorn.x86_const.UC_X86_REG_EIP)

    print(">>> EIP : %x" % (EIP))

  
  

def printf(mu: Uc, access, address, size, value, user_data):

  

    ESP = mu.reg_read(unicorn.x86_const.UC_X86_REG_ESP)

    # ここでの値は即値
    VALUE = bytes(mu.mem_read(ESP + 8, 4))

    VALUE = int.from_bytes(VALUE, 'little')

    # フォーマットはポインタで、アドレスはrdataセグメントに属する
    FORMAT = mu.mem_read(int.from_bytes(

        bytes(mu.mem_read(ESP + 4, 4)), 'little'), 4)

  

    # フォーマットの種類を確認
    if bytes(FORMAT)[0:2] == b'%d':

        print(">>> call print : %x" % (VALUE))

  

    # printfの呼び出し元から再実行
    RETURN_IP = mu.mem_read(ESP, 4)  # 戻りアドレスを取得

    RETURN_IP = int.from_bytes(bytes(RETURN_IP), 'little')

  

    mu.emu_stop()

    try:

        mu.emu_start(RETURN_IP, 2 * 1024 * 1024)

    except UcError as e:

        print("ERROR: %s" % e)

  
  

if __name__ == "__main__":

    test()

  

# 実行されるコード:

'''

.text:0040141E C7 44 24 1C 01 00 00 00       mov     dword ptr [esp+1Ch], 1

.text:00401426 C7 44 24 18 02 00 00 00       mov     dword ptr [esp+18h], 2

.text:0040142E 8B 44 24 18                   mov     eax, [esp+18h]

.text:00401432 01 44 24 1C                   add     [esp+1Ch], eax

.text:00401436 8B 44 24 1C                   mov     eax, [esp+1Ch]

.text:0040143A 89 44 24 04                   mov     [esp+4], eax

.text:0040143E C7 04 24 44 60 40 00          mov     dword ptr [esp], offset Format  ; "%d"

.text:00401445 E8 7A 2A 00 00                call    _printf

.text:00401445

.text:0040144A B8 00 00 00 00                mov     eax, 0

.text:0040144F C9                            leave

.text:00401450 C3                            retn

  

'''

タグ: Unicorn エミュレーション x86 メモリ管理 リバースエンジニアリング

7月11日 21:06 投稿