リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
独習逆アセンブラへの道 - その3 「逆アセンブラ作成 - ジャンプ命令を逆アセンブルできるまで」
前回はMOV命令を自作逆アセンブラを使ってだいたい逆アセンブルさせることができました。今回はMOV r rmとジャンプ命令について学びます。
[!] ここでの検証環境はUbuntu16.04を使用して行っております。バイナリファイルもELFフォーマットを使ってます。
逆アセンブラ - MOV r rm
前回MOV rm rを実装しました。今回はついでにそのオペランドが逆のMOV r rmを実装していきます。
オペランドが逆なだけなのですぐに出来そうですね。
テストプログラム
BITS 64
section .text
global _start
_start:
mov eax, DWORD [ebx]
逆アセンブラ
...
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
else:
return False
...
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} {} {}".format(reg.RIP-(reg.RIP-orig_rip), operand1_name, operand2_name)
...
レジスタに値をセットするときにリトルエンディアンになおしていたのですが、不必要だと思いコメントアウトしました。
...
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
...
OP_SIZE_PREFIX = 0x66
ADDR_SIZE_PREFIX = 0x67
REX_W_PREFIX = 0x48
# REX_PREFIX = 0x48
ROWLENGTH = 16
PARTLENGTH = 8
MOV rm rを逆にしただけですね。笑
逆アセンブラ - ジャンプ命令
ジャンプ命令を実装します。まずは無条件ジャンプを実装してみます。
ジャンプ命令にはショートジャンプ、ニアジャンプ、ファージャンプがあります。
今回実装する命令はJMP relです。これはニアジャンプです。ちなみにrelは相対アドレスです。
テストプログラム
BITS 64
section .text
global _start
_start:
jmp 0x10
instructions.pyは他の命令も少し修正しました。
...
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
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)
reg.RIP += 1*operand_size
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)
reg.RIP += 1*operand_size
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)
ジャンプ命令実装で詰まったことがあります。例えば、jmp 0x4と指定すると、バイナリにした時にrelの値が、0x0となります。つまり、e9 00...となります。なぜ、0x4と指定したのに4だけ減算されてるのかということです。これは色々考えた結果(恐らくですが)、relを取得した際オペランドサイズ分(この場合DWORD)RIPを進めます。その結果、この命令によってジャンプするときのRIPはオペランドサイズ分進んだ状態ということですね。それを考慮してオペランドサイズ分減算しているということです。試しにjmp short 0x1と指定すると、8bitオペランドサイズ(BYTE)になります。そのバイナリはeb 01...となっていたことが確認できました。つまり、RIPが次の命令を指した箇所を基準に考えるということですね。興味深いですね!
まとめ
- ジャンプ命令にはショートジャンプ、ニアジャンプ、ファージャンプがある
- relは相対アドレス
- ジャンプ命令のジャンプは次の命令を指し示したRIPを基準に考える