実行環境: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
'''