
本記事はPythonで簡単なx86エミュレータを作成します。
前回ではModR/Mを実装し、オペランドを柔軟に指定する方法について学びました。
今回はサブルーチンを呼び出す命令であるcall/retについて学んできます。
call/retとは
前回では、ModR/Mを実装し、オペランドを柔軟に指定する方法について学びました。
今回はサブルーチンを呼び出す命令であるcallと、呼び出し元へ戻るretを実装していきます。
まず、以下のC言語プログラムについて見ていきます。
// add1.c
int add1(int x){
return x + 1;
}
int main(){
return add1(1);
}
上記はadd1()で与えられた引数に1を加算して返す単純なプログラムです。
これを逆アセンブルしたものを以下に記します。
00000000 E80E000000 call 0x13 00000005 E9F683FFFF jmp 0xffff8400 0000000A 55 push ebp 0000000B 89E5 mov ebp,esp 0000000D 8B4508 mov eax,[ebp+0x8] 00000010 40 inc eax 00000011 5D pop ebp 00000012 C3 ret 00000013 55 push ebp 00000014 89E5 mov ebp,esp 00000016 6A01 push byte +0x1 00000018 E8EDFFFFFF call 0xa 0000001D 83C404 add esp,byte +0x4 00000020 C9 leave 00000021 C3 ret
上記の0xAから0x12までがadd1()になり、main()ではadd1()を呼び出す前にpush ebp、mov ebp, espを実行していますが、これは呼び出し元へ確実に戻るために必要な作業で、これによりサブルーチンから新たなサブルーチンを呼び出したりするような連鎖が起きても、問題なく呼び出し元へ戻ることができます。
そしてpush byte +0x1をスタックに積んでadd1()を呼び出します。
add1()内では、mov eax,[ebp+0x8]でスタックに積んだ引数(1)をeaxにコピーし、最終的にオペコード0x40のinc命令でeaxに1を加算します。
なお、C言語では戻り値はeaxに格納されることになっており、戻り値を2つ以上に指定することはできないため、その際はポインタを使用します。
Pythonによるスクリプトの作成
それでは、Pythonでcall/retを実装していきます。
# emulator.py
class ModRM:
def __init__(self):
self.modrm = {
"mod" :0x00,
"opecode" :0x00,
"reg_index" :0x00,
"rm" :0x00,
"sib" :0x00,
"disp8" :0x00,
"disp32" :0x00
}
class Emulator:
def __init__(self):
self.register_name = ["EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI"]
self.registers = {
"EAX": 0x00,
"ECX": 0x00,
"EDX": 0x00,
"EBX": 0x00,
"ESP": 0x00,
"EBP": 0x00,
"ESI": 0x00,
"EDI": 0x00
}
self.eflags = None
self.memory = None
self.eip = None
self.instructions = [None for i in range(256)]
def init_instructions(self):
self.instructions[0x01] = self.add_rm32_r32
self.instructions[0x40] = self.inc_eax
for i in range(8):
self.instructions[0x50+i] = self.push_r32
for i in range(8):
self.instructions[0x58+i] = self.pop_r32
self.instructions[0x68] = self.push_imm32
self.instructions[0x6a] = self.push_imm8
self.instructions[0x83] = self.code_83
self.instructions[0x89] = self.mov_rm32_r32
self.instructions[0x8b] = self.mov_r32_rm32
for i in range(8):
self.instructions[0xb8 + i] = self.mov_r32_imm32
self.instructions[0xc3] = self.ret
self.instructions[0xc7] = self.mov_rm32_imm32
self.instructions[0xc9] = self.leave
self.instructions[0xe8] = self.call_rel32
self.instructions[0xe9] = self.near_jump
self.instructions[0xeb] = self.short_jump
self.instructions[0xff] = self.code_ff
def create_emu(self, size, eip, esp):
self.eip = eip
self.registers["ESP"] = esp
self.memory = [0x00 for _ in range(size)]
def dump_registers(self):
for i in range(len(self.registers)):
name = self.register_name[i]
print("{} = 0x{:08x}".format(name, self.registers[name]))
print("EIP = 0x{:08x}".format(self.eip))
def mov_r32_imm32(self):
reg = self.get_code8(0) - 0xb8
value = self.get_code32(1)
reg_name = self.register_name[reg]
self.registers[reg_name] = value
self.eip += 5
if self.eip >= 0x100000000:
self.eip ^= 0x100000000
def short_jump(self):
diff = self.get_sign_code8(1)
if diff & 0x80:
diff -= 0x100
self.eip += (diff + 2)
def get_code8(self, index):
code = self.memory[self.eip + index]
if not type(code) == int:
code = int.from_bytes(code, 'little')
return code
def get_sign_code8(self, index):
code = self.memory[self.eip + index]
code = int.from_bytes(code, 'little')
return code & 0xff
def get_code32(self, index):
ret = 0x00
for i in range(4):
ret |= self.get_code8(index + i) << (i * 8)
return ret
def get_sign_code32(self, index):
return self.get_code32(index)
def near_jump(self):
diff = self.get_sign_code32(1)
if diff & 0x80000000:
diff -= 0x100000000
self.eip += (diff + 5)
def parse_modrm(self):
m = ModRM()
code = self.get_code8(0)
m.modrm["mod"] = ((code & 0xc0) >> 6)
m.modrm["opecode"] = m.modrm["reg_index"] = ((code & 0x38) >> 3)
m.modrm["rm"] = code & 0x07
self.eip += 1
if (m.modrm["mod"] != 3 and m.modrm["rm"] == 4):
m.modrm["sib"] = self.get_code8(0)
eip += 1
if (m.modrm["mod"] == 0 and m.modrm["rm"] == 5) or m.modrm["mod"] == 2:
m.modrm["disp32"] = self.get_sign_code32(0)
m.modrm["disp8"] = m.modrm["disp32"] & 0xff
eip += 4
elif m.modrm["mod"] == 1:
m.modrm["disp8"] = m.modrm["disp32"] = self.get_sign_code8(0)
self.eip += 1
return m
def mov_rm32_imm32(self):
self.eip += 1
m = self.parse_modrm()
value = self.get_code32(0)
self.eip += 4
self.set_rm32(m, value)
def set_rm32(self, m, value):
if m.modrm["mod"] == 3:
self.set_register32(m.modrm["rm"], value)
else:
address = self.calc_memory_address(m)
self.set_memory32(address, value)
def set_memory8(self, address, value):
self.memory[address] = value & 0xff
def set_memory32(self, address, value):
for i in range(4):
self.set_memory8(address+i, value >> (i*8))
def calc_memory_address(self, m):
if m.modrm["mod"] == 0:
if m.modrm["rm"] == 4:
print("not implemented ModRM mod = 0, rm = 4")
sys.exit(0)
elif m.modrm["rm"] == 5:
return m.modrm["disp32"]
else:
return self.get_register32(m.modrm["rm"])
elif m.modrm["mod"] == 1:
if m.modrm["rm"] == 4:
print("not implemented ModRM mod = 1, rm = 4")
sys.exit(0)
else:
return self.get_register32(m.modrm["rm"]) + m.modrm["disp8"]
elif m.modrm["mod"] == 2:
if m.modrm["rm"] == 4:
print("not implemented ModRM mod = 2, rm = 4")
sys.exit(0)
else:
return self.get_register32(m.modrm["rm"]) + m.modrm["disp32"]
else:
print("not implemented ModRM mod = 3")
sys.exit(0)
def mov_rm32_r32(self):
self.eip += 1
m = self.parse_modrm()
r32 = self.get_r32(m)
self.set_rm32(m, r32)
def mov_r32_rm32(self):
self.eip += 1
m = self.parse_modrm()
rm32 = self.get_rm32(m)
self.set_r32(m, rm32)
def get_rm32(self, m):
if m.modrm["mod"] == 3:
return self.get_register32(m.modrm["rm"])
else:
address = self.calc_memory_address(m)
return self.get_memory32(address)
def get_memory8(self, address):
return self.memory[address]
def get_memory32(self, address):
ret = 0
for i in range(4):
mem = self.get_memory8(address + i)
if not type(mem) == int:
mem = ord(mem)
ret |= mem << (8*i)
return ret
def set_r32(self, m, value):
self.set_register32(m.modrm["reg_index"], value)
def get_r32(self, m):
return self.get_register32(m.modrm["reg_index"])
def add_rm32_r32(self):
self.eip += 1
m = self.parse_modrm()
r32 = self.get_r32(m)
rm32 = self.get_rm32(m)
self.set_rm32(m, rm32 + r32)
def sub_rm32_imm8(self, m):
rm32 = self.get_rm32(m)
imm8 = self.get_sign_code8(0)
self.eip += 1
self.set_rm32(m, rm32 - imm8)
def code_83(self):
self.eip += 1
m = self.parse_modrm()
if m.modrm["opecode"] == 0:
self.add_rm32_imm8(m)
elif m.modrm["opecode"] == 5:
self.sub_rm32_imm8(m)
else:
print("not implemented: 83 /{}".format(m.modrm["opecode"]))
sys.exit(1)
def inc_rm32(self, m):
value = self.get_rm32(m)
self.set_rm32(m, value + 1)
def inc_eax(self):
self.registers["EAX"] += 1
self.eip += 1
def code_ff(self):
self.eip += 1
m = self.parse_modrm()
if m.modrm["opecode"] == 0:
self.inc_rm32(m)
else:
print("not implemented: FF /{}".format(m.modrm["opecode"]))
sys.exit(1)
def get_register32(self, index):
reg = self.register_name[index]
return self.registers[reg]
def set_register32(self, index, value):
reg = self.register_name[index]
self.registers[reg] = value
def push_r32(self):
reg = self.get_code8(0) - 0x50
self.push32(self.get_register32(reg))
self.eip += 1
def pop_r32(self):
reg = self.get_code8(0) - 0x58
self.set_register32(reg, self.pop32())
self.eip += 1
def push32(self, value):
esp = self.register_name.index("ESP")
address = self.get_register32(esp) - 4
self.set_register32(esp, address)
self.set_memory32(address, value)
def pop32(self):
esp = self.register_name.index("ESP")
address = self.get_register32(esp)
ret = self.get_memory32(address)
self.set_register32(esp, address + 4)
return ret
def call_rel32(self):
diff = self.get_sign_code32(1)
if diff & 0x80000000:
diff -= 0x100000000
self.push32(self.eip + 5)
self.eip += (diff + 5)
def ret(self):
self.eip = self.pop32()
def leave(self):
ebp = self.get_register32(self.register_name.index("EBP"))
self.set_register32(self.register_name.index("ESP"), ebp)
self.set_register32(self.register_name.index("EBP"), self.pop32())
self.eip += 1
def push_imm8(self):
value = self.get_code8(1)
self.push32(value)
self.eip += 2
def push_imm32(self):
value = self.get_code32(1)
self.push32(value)
self.eip += 5
def add_rm32_imm8(self, m):
rm32 = self.get_rm32(m)
imm8 = self.get_sign_code8(0)
self.eip += 1
self.set_rm32(m, rm32+imm8)
mem_size = 1024 * 1024
emu = Emulator()
emu.create_emu(mem_size, 0x7c00, 0x7c00)
binary = open('add1.bin', 'rb')
offset = 0x7c00
while True:
b = binary.read(1)
if b == b'':
break
emu.memory[offset] = b
offset += 1
binary.close()
emu.init_instructions()
while emu.eip < mem_size:
code = emu.get_code8(0)
print("EIP = 0x{:02x}, Code = 0x{:02x}".format(emu.eip, code))
if emu.instructions[code] == None:
print("\n\nNot Implemented: 0x{:02x}".format(code))
break
emu.instructions[code]()
if emu.eip == 0x00:
print("\n\nend of program.\n\n")
break
emu.dump_registers()
動作確認
それでは、上記で作成したスクリプトを実行してみます。
なお、事前にアセンブリ言語のプログラムはbinファイルとしてビルドしておきます。
> python emulator.py EIP = 0x7c00, Code = 0xe8 EIP = 0x7c13, Code = 0x55 EIP = 0x7c14, Code = 0x89 EIP = 0x7c16, Code = 0x6a EIP = 0x7c18, Code = 0xe8 EIP = 0x7c0a, Code = 0x55 EIP = 0x7c0b, Code = 0x89 EIP = 0x7c0d, Code = 0x8b EIP = 0x7c10, Code = 0x40 EIP = 0x7c11, Code = 0x5d EIP = 0x7c12, Code = 0xc3 EIP = 0x7c1d, Code = 0x83 EIP = 0x7c20, Code = 0xc9 EIP = 0x7c21, Code = 0xc3 EIP = 0x7c05, Code = 0xe9 end of program. EAX = 0x00000002 ECX = 0x00000000 EDX = 0x00000000 EBX = 0x00000000 ESP = 0x00007c00 EBP = 0x00000000 ESI = 0x00000000 EDI = 0x00000000 EIP = 0x00000000
問題なくサブルーチンを呼び出し、eaxに計算結果である2が格納できたことが確認できました。