リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
独習逆アセンブラへの道 - その4 「逆アセンブラ作成 - ジャンプ命令を逆アセンブルできるまで(続き)」
前回はJMP命令の相対アドレス指定バージョンを自作逆アセンブラを使って逆アセンブルさせることができました。今回はまずもうひとつのJMP命令について学びます。その次は、インクリメント・デクリメント命令について学びます。
[!] ここでの検証環境はUbuntu16.04を使用して行っております。バイナリファイルもELFフォーマットを使ってます。
JMP r/m
今回はJMP命令の絶対アドレス指定バージョンを自作逆アセンブラに実装してみます。
この命令はModr/mのデコードが必要になるようです。
命令のオペコードはffです。
テストプログラム
BITS 64
section .text
global _start
_start:
jmp QWORD [rax]
逆アセンブラ
64bitモードではこの命令は、64bitのアドレス指定しかサポートされていないようなので、REXプレフィクスが付きませんでした。なので正しいのか分かりませんがmemory.pyとinstruction.pyでは、ELFファイルの5Byte目のEL_CLASSを見て、32bitなのか64bitなのか判断しています。
...
def run_mode(self):
return self.dump[4] # el_class: 0->Invalid, 1->32bit, 2->64bit
...
def read_modrm(self, reg, rex_pref=False, opsize_pref=False, run_mode=False):
...
if rex_pref==REX_W_PREFIX or run_mode==RUN_MODE_64:
SIZE = QWORD
...
if run_mode==RUN_MODE_64:
operand1_name = operand1_name.replace("e", "r")
operand2_name = operand2_name.replace("e", "r")
return operand1_name, operand2_name, operand1, operand2
...
def get_instructions(opcode):
if 0x88<=opcode<=0x89:
return mov_rm_r
elif 0x8a<=opcode<=0x8b:
return mov_r_rm
elif 0xb8<=opcode<=0xb8+0x8:
return mov_r_imm
elif 0xe8<=opcode<=0xe9:
return jmp_rel
elif 0xff:
return jmp_rm
else:
return False
...
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
reg.RIP += 1*operand_size
return "{:08x} jmp {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name)
INC rm & DEC rm
これらの命令のオペコードがffでした。JMP命令のオペコードもffでした。
「どういうこっちゃ?」と思ったわけです。笑
IDMをよく見ると、オペコードの横に「/digit」が付いていることに気づきました。これは「/r」ならレジスタ、「/digit」なら命令を拡張するという意味になるようです。/digitはREG領域を見れば判別できるみたいです。
ここで、もう一度各命令について確かめてみると、INCが/0、DECが/1、JMPが/4でした。これを元に実装してみます。
テストプログラム
BITS 64
section .text
global _start
_start:
inc eax
BITS 64
section .text
global _start
_start:
dec eax
逆アセンブラ
...
# execute instruction
instruction_dic = instructions.get_instructions(memory.dump[reg.RIP])
if instruction_dic:
if len(instruction_dic)==1:
instruction = instruction_dic[0]
result.append(instruction(memory, reg, rex_pref=rex_prefix, opsize_pref=opsize_prefix))
else:
instruction = instructions.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))
# show result
...
...
def get_instructions(opcode):
if 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 select_instruction(memory, reg, instruction_dic, rex_pref=False, opsize_pref=False):
reg.RIP += 1
memory.read_modrm(reg, rex_pref=rex_pref, opsize_pref=opsize_pref)
reg.RIP -= 1
return instruction_dic[memory.REG>>3]
...
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
reg.RIP += 1*operand_size
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
reg.RIP += 1*operand_size
return "{:08x} dec {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name)
どんくさいコードで申し訳ないです。
上記の通りREG領域で判別するためにselect_instruction関数を作成しました。あとは、いつもどおりですね。
まとめ
- JMPは64bitモードでは64bitのアドレス指定のみをサポート
- ELFフォーマットの5bit目はそのファイルのbitモードを表す
- 「/digit」は命令拡張の意味。REGフィールドで判別