Python x86エミュレータの作成(call/ret命令)
本記事は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が格納できたことが確認できました。