リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5
私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。
独習逆アセンブラへの道 - その5 「逆アセンブラ作成 - ADD命令が逆アセンブルできるまで」
前回はJMP命令とINC/DEC命令を実装しました。今回はADD命令(の一つ)を実装した後、複数命令を実行できるようにします。
[!] ここでの検証環境はUbuntu16.04を使用して行っております。バイナリファイルもELFフォーマットを使ってます。
ADD r/m imm8
この命令のオペコードは83/0です。
本当は、RFLAGSをADDやINC、DEC後に更新する機能を実装したかったのですが、逆アセンブラ自体には必要ないので実装しませんでした。
テストプログラム
BITS 64
section .text
global _start
_start:
add eax, 0x1
逆アセンブラ
...
def get_instructions(opcode):
if 0x83==opcode:
return {0:add_rm_imm8}
elif 0x88<=opcode<=0x89:
return {0:mov_rm_r}
elif 0x8a<=opcode<=0x8b:
return {0:mov_r_rm}
elif 0xb8<=opcode<=0xb8+0x8:
return {0:mov_r_imm}
elif 0xe8<=opcode<=0xe9:
return {0:jmp_rel}
elif 0xfe<=opcode<=0xff:
return {0:inc_rm, 1:dec_rm, 4:jmp_rm}
else:
return False
...
def add_rm_imm8(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand1_name, _, operand1, _ = memory.read_modrm(reg, rex_pref, opsize_pref)
imm = memory.read(reg.RIP, BYTE)
value = int(imm.to_bytes(BYTE, byteorder="little").hex(), 16)
reg.RIP += 1*BYTE
if not reg.set_register(memory.RM, BYTE, value):
return False
return "{:08x} add {} 0x{:02x}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name, imm)
...
...
def read_modrm(self, reg, rex_pref=False, opsize_pref=False, run_mode=False):
...
reg.RIP += 1
return operand1_name, operand2_name, operand1, operand2
...
複数命令実行
そうなんです。今まで1つの命令しか実行できませんでした。笑
そもそも実行ファイルをどこまで読みこめばよいのでしょうか。とりあえず、Cで作成したソースファイルをgccでコンパイルし逆アセンブルしてみるとHLT命令で止まっているみたいです。なので、同じようにHLT命令で停止するようにします。でも、恐らく私の理解は間違っています。HLTで命令ではCPUのフェッチは終了しないと思っています。
テストプログラム
BITS 64
section .text
global _start
_start:
add eax, 0x1
inc eax
mov ebx, eax
hlt
逆アセンブラ
...
while True:
# check REX.W(48H) prefix
rex_prefix = memory.check_rexprefix(reg)
if rex_prefix:
reg.RIP += 1
# check 66H prefix
opsize_prefix = memory.check_66prefix(reg)
if opsize_prefix:
reg.RIP += 1
# check 67H prefix
addrsize_prefix = memory.check_67prefix(reg)
if addrsize_prefix:
reg.RIP += 1
# execute instruction
instruction_dic = instructions.get_instructions(memory.dump[reg.RIP])
if instruction_dic:
if not instruction_dic.get(8):
instruction = instruction_dic[0]
result.append(instruction(memory, reg, rex_pref=rex_prefix, opsize_pref=opsize_prefix))
else:
select_instruction = instruction_dic[8]
instruction = select_instruction(memory, reg, instruction_dic, rex_pref=rex_prefix, opsize_pref=opsize_prefix)
result.append(instruction(memory, reg, rex_pref=rex_prefix, opsize_pref=opsize_prefix))
if instruction==instructions.hlt:
break
...
...
def get_instructions(opcode):
if 0x83==opcode:
return {0:add_rm_imm8}
elif 0x88<=opcode<=0x89:
return {0:mov_rm_r}
elif 0x8a<=opcode<=0x8b:
return {0:mov_r_rm}
elif 0xb8<=opcode<=0xb8+0x8:
return {0:mov_r_imm}
elif 0xe8<=opcode<=0xe9:
return {0:jmp_rel}
elif 0xf4==opcode:
return {0:hlt}
elif 0xfe<=opcode<=0xff:
return {0:inc_rm, 1:dec_rm, 4:jmp_rm, 8:select_instruction}
else:
return False
...
def add_rm_imm8(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand1_name, _, operand1, _ = memory.read_modrm(reg, rex_pref, opsize_pref)
imm = memory.read(reg.RIP, BYTE)
value = int(imm.to_bytes(BYTE, byteorder="little").hex(), 16)
reg.RIP += 1*BYTE
if not reg.set_register(memory.RM, BYTE, value):
return False
return "{:08x} add {} 0x{:02x}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name, imm)
def mov_rm_r(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand1_name, operand2_name, operand1, operand2 = memory.read_modrm(reg, rex_pref, opsize_pref)
if operand1_name.startswith("["):
memory.write(operand1, operand_size, operand2)
else:
reg.set_register(memory.RM, operand_size, operand2)
return "{:08x} mov {} {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name, operand2_name)
def mov_r_rm(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand2_name, operand1_name, operand2, operand1 = memory.read_modrm(reg, rex_pref, opsize_pref)
if operand1_name.startswith("["):
memory.write(operand1, operand_size, operand2)
else:
reg.set_register(memory.REG, operand_size, operand2)
return "{:08x} mov {} {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name, operand2_name)
def mov_r_imm(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg_n = opcode-0xb8
reg.RIP += 1
imm = memory.read(reg.RIP, operand_size)
value = int(imm.to_bytes(operand_size, byteorder="little").hex(), 16)
reg.RIP += 1*operand_size
if not reg.set_register(reg_n, operand_size, value):
return False
return "{:08x} mov {} {}".format(reg.RIP-(reg.RIP-orig_rip), reg.get_register_name(reg_n, rex_pref=rex_pref, opsize_pref=opsize_pref), imm)
def jmp_rel(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
rel = memory.read(reg.RIP, operand_size)+operand_size
# reg.RIP += rel
reg.RIP = 1*operand_size
return "{:08x} jmp 0x{:08x}".format(reg.RIP-(reg.RIP-orig_rip), rel)
def jmp_rm(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
run_mode = memory.run_mode()
if run_mode==RUN_MODE_64:
operand_size = QWORD
elif rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand1_name, _, operand1, _ = memory.read_modrm(reg, rex_pref, opsize_pref, run_mode)
# reg.RIP = operand1
return "{:08x} jmp {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name)
def hlt(memory, reg, rex_pref=False, opsize_pref=False):
return "{:08x} hlt".format(reg.RIP)
def inc_rm(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
run_mode = memory.run_mode()
if run_mode==RUN_MODE_64:
operand_size = QWORD
elif rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand1_name, _, operand1, _ = memory.read_modrm(reg, rex_pref, opsize_pref)
operand1 += 0x1
if operand1_name.startswith("["):
memory.write(operand1, operand_size, operand1)
else:
reg.set_register(memory.RM, operand_size, operand1)
# reg.RIP = operand1
return "{:08x} inc {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name)
def dec_rm(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
run_mode = memory.run_mode()
if run_mode==RUN_MODE_64:
operand_size = QWORD
elif rex_pref==REX_W_PREFIX:
operand_size = QWORD
orig_rip -= 1
elif opsize_pref:
operand_size = WORD
orig_rip -= 1
opcode = memory.dump[reg.RIP]
reg.RIP += 1
operand1_name, _, operand1, _ = memory.read_modrm(reg, rex_pref, opsize_pref)
operand1 -= 0x1
if operand1_name.startswith("["):
memory.write(operand1, operand_size, operand1)
else:
reg.set_register(memory.RM, operand_size, operand1)
# reg.RIP = operand1
return "{:08x} dec {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name)
...
def read_modrm(self, reg, rex_pref=False, opsize_pref=False, run_mode=False, read_only=False):
...
orig_rip = reg.RIP
...
if read_only:
reg.RIP -= reg.RIP-orig_rip
return operand1_name, operand2_name, operand1, operand2
...
案の定色々バグが残ってました(主にRIPの更新)。それらを修正してみるとちゃんとテストプログラムを逆アセンブルすることが出来ました。
まとめ
- 算術演算後はRFLAGS(32bitではEFLAGS)を更新
- HLT命令でプログラム中のCPUのフェッチは終了??
- (やっと)複数命令を実行できるようになった
よし!自作逆アセンブラはここまでにします!笑
次回は自作メモリエディタについて学ぶ予定です。