リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
独習逆アセンブラへの道 - その2 「逆アセンブラ作成 - MOV命令を逆アセンブルできるまで(続き)」
前回は逆アセンブラとは何なのかや、作成にあたって必要そうな情報を集めました。そして勢い余って少しだけ、逆アセンブラの作成を行いました。やっぱり、情報をただ眺めるだけでは、ちゃんと頭に入らない感じでしたが、実際書いてみると色んな疑問にぶつかります。そして、それを調べているうちにだんだん理解していってるような気がしました。やはり、作ってみるという実践は大事ですね!あと実践のほうが楽しいですしね!
[!] ここでの検証環境はUbuntu16.04を使用して行っております。バイナリファイルもELFフォーマットを使ってます。
逆アセンブラ作成 - MOV命令を逆アセンブルできるまで(続き)
今回は前回の逆アセンブラ作成の続きをします。
前回レジスタをどのように見分けるのか問題(前回の追記参照)がありました。結論はプレフィクスで見分けるようです。今回まずは、このプレフィクスを見分けられるようにしようと思います。前回REXプレフィクスはおいておくと言いましたが、ついでなのでとりあえず66HとREX.Wプレフィクスだけ実装しちゃいます。
テストプログラム
BITS 64
section .text
global _start
_start:
mov ax, 1000
逆アセンブラ
以下チェック処理を追加したバージョンです。
...
# check REX.W(48H) prefix
rex_prefix = memory.check_rexWprefix(reg)
if rex_prefix:
reg.RIP += 1
# check 66H prefix
opsize_prefix = memory.check_66prefix(reg)
if opsize_prefix:
reg.RIP += 1
# execute instruction
instruction = instructions.get_instructions(memory.dump[reg.RIP])
if instruction:
result.append(instruction(memory, reg, rex_pref=rex_prefix, opsize_pref=opsize_prefix))
...
...
def read(self, reg, size):
value = 0
for i, v in enumerate(self.dump[reg.RIP:reg.RIP+size]):
value |= v<<i*8
return value
def check_66prefix(self, reg):
if self.dump[reg.RIP]==OP_SIZE_PREFIX:
return True
else:
return False
def check_rexWprefix(self, reg):
if self.dump[reg.RIP]==REX_W_PREFIX:
return True
else:
return False
...
...
def get_register_name(self, n, rex_pref=False, opsize_pref=False):
if n==0:
if opsize_pref:
return "ax"
elif rex_pref:
return "rax"
else:
return "eax"
elif n==1:
if opsize_pref:
return "cx"
elif rex_pref:
return "rcx"
else:
return "ecx"
elif n==2:
if opsize_pref:
return "dx"
elif rex_pref:
return "rdx"
else:
return "edx"
elif n==3:
if opsize_pref:
return "bx"
elif rex_pref:
return "rbx"
else:
return "ebx"
elif n==4:
if opsize_pref:
return "sp"
elif rex_pref:
return "rsp"
else:
return "esp"
elif n==5:
if opsize_pref:
return "bp"
elif rex_pref:
return "rbp"
else:
return "ebp"
elif n==6:
if opsize_pref:
return "si"
elif rex_pref:
return "rsi"
else:
return "esi"
elif n==7:
if opsize_pref:
return "di"
elif rex_pref:
return "rdi"
else:
return "edi"
else:
return False
...
...
def mov_r_imm(memory, reg, rex_pref=False, opsize_pref=False):
operand_size = DWORD
orig_rip = reg.RIP
if rex_pref:
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, operand_size)
reg.RIP += 1
if not reg.set_register(reg_n, imm):
return False
return "{:08x} {} {}".format(reg.RIP-(reg.RIP-orig_rip), reg.get_register_name(reg_n, rex_pref=rex_pref, opsize_pref=opsize_pref), imm)
...
これで、66HとREX.Wプレフィクスには対応できたと思います。
あと、memory.pyのread関数をこっそり修正しました。
次は、ModR/Mの実装をしてみます。
逆アセンブラ作成 - ModR/M
とりあえずオペコード89HのMOV r/m16(または r/m32, r/m64) r16(または r32, r64)が、ModR/Mを使うのでこの命令を実装します。88Hは8bitを指定するのですが、これはひとまず保留です。
テストプログラム
テストプログラムは2つ用意しました。
BITS 64
section .text
global _start
_start:
mov eax, ebx
BITS 64
section .text
global _start
_start:
mov [eax], ebx
逆アセンブラ
表には規則性があり、それぞれMod、Reg、R/M、SIBの領域に分かれてますね。まずModR/M領域をこの各フィールドに分解します。次にModで大きくジャンルが分かれています。さらに、R/Mで各レジスタまたはSIBに分かれています。これらの表現を実装します。
以下はModR/Mを使うMOV命令を実装したバージョンです。
...
# check 67H prefix
addrsize_prefix = memory.check_67prefix(reg)
if addrsize_prefix:
reg.RIP += 1
...
memory.show()
...
def read(self, addr, size):
value = 0
for i, v in enumerate(self.dump[addr:addr+size]):
value |= v<<i*8
return value
def write(self, addr, operand_size, value):
value = value.to_bytes(operand_size, byteorder="little")
for i in range(addr, operand_size):
v = value[i].to_bytes(1, byteorder="little")
if i==0:
self.dump = v+self.dump[i+1:]
else:
self.dump = self.dump[:i]+v+self.dump[i+1:]
return self
def read_modrm(self, reg, rex_pref=False, opsize_pref=False):
operand1_name = None
operand2_name = None
operand1 = None
operand2 = None
self.MODRM = self.dump[reg.RIP]
self.MOD = self.MODRM&0xc0
self.REG = self.MODRM&0x38
self.RM = self.MODRM&0x7
self.SIB = None
self.DISP = None
# reading size
SIZE = DWORD
if rex_pref==REX_W_PREFIX:
SIZE = QWORD
elif opsize_pref:
SIZE = WORD
# check SIB and DISPLACEMENT
if self.MOD==0x0 and self.RM==0x4:
reg.RIP += 1
self.SIB = self.dump[reg.RIP]
elif (self.MOD==0x40 or self.MOD==0x80) and self.RM==0x4:
reg.RIP += 1
self.SIB = self.dump[reg.RIP]
reg.RIP += 1
self.DISP = self.read(reg.RIP, DWORD)
# check REGISTER and self.DISPLACEMENT
elif self.MOD==0x0 and self.RM==0x5:
reg.RIP += 1
self.DISP = self.dump[reg.RIP]
elif self.MOD==0x0:
operand1_name = "["+reg.get_register_name(self.RM, rex_pref, opsize_pref)+"]"
operand1 = reg.get_register(self.RM)
operand2_name = reg.get_register_name(self.REG>>3)
operand2 = reg.get_register(self.REG>>3)
elif self.MOD==0x40:
reg.RIP += 1
self.DISP = self.read(reg.RIP, BYTE)
operand1_name = "["+reg.get_register_name(self.RM, rex_pref, opsize_pref)+"+"+str(self.DISP)+"]"
operand1 = self.read(reg.get_register(self.RM)+self.DISP, SIZE)
operand2_name = reg.get_register_name(self.REG>>3)
operand2 = reg.get_register(self.REG>>3)
elif self.MOD==0x80:
reg.RIP += 1
self.DISP = self.read(reg.RIP, DWORD)
operand1_name = "["+reg.get_register_name(self.RM, rex_pref, opsize_pref)+"+"+str(self.DISP)+"]"
operand1 = self.read(reg.get_register(self.RM)+self.DISP, SIZE)
operand2_name = reg.get_register_name(self.REG>>3)
operand2 = reg.get_register(self.REG>>3)
elif self.MOD==0xc0:
operand1_name = reg.get_register_name(self.RM, rex_pref, opsize_pref)
operand1 = reg.get_register(self.RM)
operand2_name = reg.get_register_name(self.REG>>3)
operand2 = reg.get_register(self.REG>>3)
return operand1_name, operand2_name, operand1, operand2
...
def check_67prefix(self, reg):
if self.dump[reg.RIP]==ADDR_SIZE_PREFIX:
return True
else:
return False
...
def show(self):
n = 0x0
for start_row in range(0, len(self.dump), ROWLENGTH):
row = self.dump[start_row:start_row+ROWLENGTH].hex()
print("|{:08x}|".format(n), end=" ")
for start_part in range(0, len(row), PARTLENGTH):
print(row[start_part:start_part+PARTLENGTH], end=" ")
print()
n += 0x10
...
def set_register(self, n, operand_size, value):
ret = True
value = int(value.to_bytes(operand_size, byteorder="little").hex(), 16)
if n==0:
self.RAX = value
elif n==1:
self.RCX = value
elif n==2:
self.RDX = value
elif n==3:
self.RBX = value
elif n==4:
self.RSP = value
elif n==5:
self.RBP = value
elif n==6:
self.RSI = value
elif n==7:
self.RDI = value
else:
ret = False
return ret
...
def get_instructions(opcode):
if 0x88<=opcode<=0x89:
return mov_rm_r
elif 0xb8<=opcode<=0xb8+0x8:
return mov_r_imm
else:
return False
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} {} {}".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)
reg.RIP += 1
if not reg.set_register(reg_n, operand_size, imm):
return False
return "{:08x} {} {}".format(reg.RIP-(reg.RIP-orig_rip), reg.get_register_name(reg_n, rex_pref=rex_pref, opsize_pref=opsize_pref), imm)
read_modrm関数が非常に長くなってしまいました。笑
SIBは実装していません。
あと、わかりやすいように最後にメモリマップも表示するようにしてみました。sample_ch16_2ではレジスタ値が、sample_ch16_3ではメモリマップが逆アセンブラすると変化しているのが確認できます。
今回はmov_rm_rを実装しました。逆にmov_r_rmもあります。
めちゃくちゃ楽しくなってきましたね。
今回はここまでにします。
次回はまず、mov_r_rmをついでに実装し、その次は、ジャンプ命令を実装してみる予定です。
まとめ
- レジスタサイズを見分けるためにプレフィクスをチェック
- ModR/Mの表を見ると規則性がある
- 命令によってレジスタとレジスタ/メモリが逆転することがある