From 03aa598514bebe887aca1455a3f8c934ea37a321 Mon Sep 17 00:00:00 2001 From: Clement Rouault Date: Tue, 15 Sep 2015 09:17:25 +0200 Subject: [PATCH] Rewrite x64 SIB/Immediat logic to match x86 + fixes + test_x64 --- native_exec/simple_x64.py | 420 ++++++++++++++++++--------------- native_exec/simple_x86.py | 1 - native_exec/test_simple_x64.py | 98 ++++++++ native_exec/test_simple_x86.py | 5 +- 4 files changed, 333 insertions(+), 191 deletions(-) create mode 100644 native_exec/test_simple_x64.py diff --git a/native_exec/simple_x64.py b/native_exec/simple_x64.py index 0c3ae18..c1e5df2 100644 --- a/native_exec/simple_x64.py +++ b/native_exec/simple_x64.py @@ -57,13 +57,13 @@ class BitArray(object): return int("".join([str(i) for i in self.array]), 2) @classmethod - def from_string(cls): + def from_string(cls, str_base): l = [] for c in bytearray(reversed(str_base)): for i in range(8): l.append(c & 1) c = c >> 1 - self.array = l + return cls(len(str_base) * 8, list(reversed(l))) @classmethod def from_int(cls, size, x): @@ -71,19 +71,26 @@ class BitArray(object): x = x & ((2 ** size) - 1) return cls(size, bin(x)[2:]) + def copy(self): + return type(self)(self.size, self.array) + # Rules: bytes only !!!! +mem_access = collections.namedtuple('mem_access', ['base', 'index', 'scale', 'disp']) + reg_order = ['RAX', 'RCX', 'RDX', 'RBX', 'RSP', 'RBP', 'RSI', 'RDI'] new_reg_order = ['R8', 'R9', 'R10', 'R11', 'R12', 'R13', 'R14', 'R15'] - - x64_regs = reg_order + new_reg_order -mem_access = collections.namedtuple('mem_access', ['base', 'index', 'scale', 'disp']) + def create_displacement(base=None, index=None, scale=None, disp=0): if index is not None and scale is None: scale = 1 + if scale and index is None: + raise ValueError("Cannot create displacement with scale and no index") + if scale and index.upper() == "RSP": + raise ValueError("Cannot create displacement with index == RSP") return mem_access(base, index, scale, disp) def mem(data): @@ -162,59 +169,106 @@ class X64RegisterSelector(object): class RawBits(BitArray): def accept_arg(self, previous, args): - return (0, self, None) + return (0, self.copy(), None) +class ImmediatOverflow(ValueError): + pass -class Immediat(object): - def __init__(self, add=0): - self.add = add +def accept_as_8immediat(x): + try: + return struct.pack("".format(mem_access.scale)) - scale_bits = BitArray.from_int(2, scale[mem_access.scale]) - base_bits = X64RegisterSelector.get_reg_bits(mem_access.base) - if X64.is_new_reg(mem_access.base): - self.rex[7] = 1 - index_bits = X64RegisterSelector.get_reg_bits(mem_access.index) - if X64.is_new_reg(mem_access.index): - self.rex[6] = 1 - return scale_bits + index_bits + base_bits - -class ModRM_REG__DEREF_SIB(SubModRM): - """Only handle [BASE + INDEX]""" - @classmethod - def match(cls, arg1, arg2): - return X64.is_reg(arg1) and X64.is_mem_acces(arg2)# and X64.mem_access_has_only(arg2, ["base", "index", "scale", ']) - - def __init__(self, arg1, arg2, reversed): - super(ModRM_REG__DEREF_SIB, self).__init__() - self.mod = BitArray(2, "10") - #import pdb;pdb.set_trace() - self.rm = BitArray(3, "100") - self.is_rex_needed = True - self.rex[4] = 1 - self.setup_reg_as_register(arg1) - self.after = self.create_sib(arg2) + BitArray.from_int(32, X64.to_little_endian(arg2.disp, size=32)) - self.direction = not reversed - - def create_sib(self, mem_access): - scale = {None:0, 1: 0, 2 : 1, 4: 2, 8 : 3} - #if mem_access.disp: - # raise NotImplementedError("SIB WITH DISPLACEMENT") - if mem_access.scale not in scale: - raise ValueError("Invalid scale for mem access <{0}>".format(mem_access.scale)) - scale_bits = BitArray.from_int(2, scale[mem_access.scale]) - base_bits = X64RegisterSelector.get_reg_bits(mem_access.base) - if X64.is_new_reg(mem_access.base): - self.rex[7] = 1 - if mem_access.index is None: - index_bits = BitArray(3, "100") + if not FIRE_UP_SIB: + self.is_rex_needed = True + self.rex[4] = 1 + self.setup_reg_as_register(arg1) + self.setup_rm_as_register(arg2.base) + self.compute_displacement(arg2.disp) + self.direction = not reversed + return + # FIRE UP THE SIB + # Handle no base and base == EBP special case + if not arg2.base: + force_displacement = 4 + elif arg2.base.upper() == "RBP": + force_displacement = 1 else: - index_bits = X64RegisterSelector.get_reg_bits(mem_access.index) - if X64.is_new_reg(mem_access.index): - self.rex[6] = 1 - return scale_bits + index_bits + base_bits + force_displacement = 0 + + self.setup_reg_as_register(arg1) + self.rm = BitArray(3, "100") + self.compute_displacement(arg2.disp, force_displacement) + self.after = self.compute_sib(arg2) + self.after + if not arg2.base: + self.mod = BitArray(2, "00") + self.direction = not reversed + + def compute_displacement(self, displacement, force_displacement=0): + if not displacement and not force_displacement: + self.mod = BitArray(2, "00") + self.after = BitArray(0, "") + return + # Pack in a byte + try: + v = accept_as_8immediat(displacement) + except ImmediatOverflow: + v = None + if v is not None and force_displacement <= 1: + self.mod = BitArray(2, "01") + self.after = BitArray.from_string(v) + return + # Pack in a dword + try: + v = accept_as_32immediat(displacement) + except ImmediatOverflow: + v = None + if v is not None and force_displacement <= 4: + self.mod = BitArray(2, "10") + self.after = BitArray.from_string(v) + return + raise ValueError("Displacement {0} is too big".format(hex(displacement))) + + def compute_sib(self, mem_access): + scale = {1: 0, 2 : 1, 4: 2, 8 : 3} + if mem_access.index is None and mem_access.base is None: + return BitArray(2, "00") + BitArray(3, "100") + BitArray(3, "101") + if mem_access.index is None: + return BitArray(2, "00") + BitArray(3, "100") + self.setup_sib_base_rex(mem_access.base) + if mem_access.scale not in scale: + raise ValueError("Invalid scale for mem access <{0}>".format(mem_access.scale)) + if mem_access.base is None: + return BitArray.from_int(2, scale[mem_access.scale]) + self.setup_sib_index_rex(mem_access.index) + BitArray(3, "101") + return BitArray.from_int(2, scale[mem_access.scale]) + self.setup_sib_index_rex(mem_access.index) + self.setup_sib_base_rex(mem_access.base) class Slash(object): "No idea for the name: represent the modRM for single args + encoding in reg (/7 in cmp in man intel)" - def __init__(self, reg): + def __init__(self, reg_num): "reg = 7 for /7" - self.mod = None - self.reg = BitArray.from_int(3, reg) - - self.rm = None + self.reg = reg_order[reg_num] def accept_arg(self, previous, args): - x = args[0] - ok, bits, rex = X64RegisterSelector().accept_arg(None, [x]) - if ok is not None: - self.mod = BitArray(2, "11") - self.rm = bits - return 1, self.mod + self.reg + self.rm, rex - # TODO: register ! - if X64.mem_access_has_only(x, ["base"]) and x.base not in ['ESP', 'EBP']: - self.mod = BitArray(2, "00") - ok, bits = X86RegisterSelector().accept_arg(None, [x.base]) - self.rm = bits - return 1, self.mod + self.reg + self.rm, rex - # TODO: Other - if X64.mem_access_has_only(x, ["base", "disp"]): - self.mod = BitArray(2, "10") - ok, bits, rex = X64RegisterSelector().accept_arg(None, [x.base]) - self.rm = bits - return 1, self.mod + self.reg + self.rm + BitArray.from_int(32, X64.to_little_endian(x.disp, size=32)), rex - return None, None + if len(args) < 1: + raise ValueError("Missing arg for Slash") + # Reuse all the MODRm logique with the reg as our self.reg + # The sens of param is strange I need to fix the `reversed` logique + arg_consum, value, rex = ModRM([ModRM_REG64__REG64, ModRM_REG64__MEM], has_direction_bit=False).accept_arg(previous, args[:1] + [self.reg] + args[1:]) + if value is None: + return arg_consum, value, rex + return arg_consum-1, value, rex class Instruction(object): encoding = [] + default_rex = BitArray(8, "") def __init__(self, *initial_args): for type_encoding in self.encoding: args = list(initial_args) res = [] - full_rex = BitArray(8, "") + full_rex = self.default_rex if hasattr(self, "default_32_bits") and self.default_32_bits: full_rex = BitArray.from_int(8, 0x48) for element in type_encoding: @@ -525,8 +564,6 @@ class JmpType(Instruction): return DelayedJump(cls, arg) return super(JmpType, cls).__new__(cls, *initial_args) -# -# class Push(Instruction): encoding = [(RawBits.from_int(5, 0x50 >> 3), X64RegisterSelector()), (RawBits.from_int(8, 0x68), Imm32())] @@ -535,7 +572,7 @@ class Pop(Instruction): encoding = [(RawBits.from_int(5, 0x58 >> 3), X64RegisterSelector())] class Call(Instruction): - encoding = [(RawBits.from_int(13, 0xffd0 >> 3), X64RegisterSelector())] + encoding = [(RawBits.from_int(8, 0xff), Slash(2))] class Ret(Instruction): encoding = [(RawBits.from_int(8, 0xc3),)] @@ -555,7 +592,9 @@ class Add(Instruction): default_32_bits = True encoding = [(RawBits.from_int(8, 0x05), RegisterRax(), Imm32()), (RawBits.from_int(8, 0x81), Slash(0), Imm32()), - (RawBits.from_int(8, 0x01), ModRM(ModRM_REG64__REG64, ModRM_REG__DEREF_REG, ModRM_REG__DEREF_REG_IMM, ModRM_REG__DEREF_BASE_INDEX, )),] + #(RawBits.from_int(8, 0x01), ModRM(ModRM_REG64__REG64, ModRM_REG__DEREF_REG, ModRM_REG__DEREF_REG_IMM, ModRM_REG__DEREF_BASE_INDEX, )),] + (RawBits.from_int(8, 0x01), ModRM([ModRM_REG64__REG64, ModRM_REG64__MEM])),] + class Sub(Instruction): default_32_bits = True @@ -572,31 +611,28 @@ class In(Instruction): (RawBits.from_int(16, 0x66ed), FixedRegister('AX'), FixedRegister('DX')), # Fuck-it hardcoded prefix for now (RawBits.from_int(8, 0xed), FixedRegister('EAX'), FixedRegister('DX'))] -class JmpImm8(Immediat): +class JmpImm(object): + accept_as_Ximmediat = None def __init__(self, sub): self.sub = sub - def accept_arg(self, previous, args): - try: - x = int(args[0]) - except (ValueError, TypeError): - return (None, None, None) - if not (-128 + self.sub) <= x <= 127: - return (None, None, None) - x -= self.sub - return (1, BitArray.from_int(8, X64.to_little_endian(x, size=8)), None) -class JmpImm32(Immediat): - def __init__(self, sub): - self.sub = sub def accept_arg(self, previous, args): try: - x = int(args[0]) + jump_size = int(args[0]) except (ValueError, TypeError): - return (None, None, None) - #if not (-128 + self.ADD) <= x <= 127: - # return (None, None) - x -= self.sub - return (1, BitArray.from_int(32, X64.to_little_endian(x, size=32)), None) + return (None, None) + jump_size -= self.sub + try: + jmp_imm = self.accept_as_Ximmediat(jump_size) + except ImmediatOverflow: + return (None, None) + return (1, BitArray.from_string(jmp_imm), None) + +class JmpImm8(JmpImm): + accept_as_Ximmediat = accept_as_8immediat + +class JmpImm32(JmpImm): + accept_as_Ximmediat = accept_as_32immediat class Jmp(JmpType): encoding = [(RawBits.from_int(8, 0xeb), JmpImm8(2)), @@ -623,21 +659,27 @@ class Jnb(JmpType): encoding = [(RawBits.from_int(8, 0x73), JmpImm8(2)), (RawBits.from_int(16, 0x0f83), JmpImm32(6))] + +class Lea(Instruction): + #default_rex = BitArray(8, "01001000") + refuse_reverse = True + #default_32_bits = False + encoding = [(RawBits.from_int(8, 0x8d), ModRM([ModRM_REG64__MEM], accept_reverse=False, has_direction_bit=False))] + class Mov(Instruction): default_32_bits = True - encoding = [(RawBits.from_int(8, 0x89), ModRM(ModRM_REG64__REG64, ModRM_REG__DEREF_REG, ModRM_REG__DEREF_REG_IMM, ModRM_REG__DEREF_BASE_INDEX, ModRM_REG__DEREF_SIB)), - (RawBits.from_int(5, 0xb8 >> 3), X64RegisterSelector(), Imm64()), - (Mov_RAX_OFF64(),), (Mov_OFF64_RAX(),)] + encoding = [(Mov_RAX_OFF64(),), (Mov_OFF64_RAX(),), (RawBits.from_int(8, 0x89), ModRM([ModRM_REG64__REG64, ModRM_REG64__MEM])), + (RawBits.from_int(5, 0xb8 >> 3), X64RegisterSelector(), Imm64())] class Cmp(Instruction): default_32_bits = True encoding = [(RawBits.from_int(8, 0x3d), RegisterRax(), Imm32()), (RawBits.from_int(8, 0x81), Slash(7), Imm32()), - (RawBits.from_int(8, 0x3b), ModRM(ModRM_REG64__REG64, ModRM_REG__DEREF_REG)),] + (RawBits.from_int(8, 0x3b), ModRM([ModRM_REG64__REG64, ModRM_REG64__MEM])),] class Xor(Instruction): default_32_bits = True - encoding = [(RawBits.from_int(8, 0x31), ModRM(ModRM_REG64__REG64))] + encoding = [(RawBits.from_int(8, 0x31), ModRM([ModRM_REG64__REG64, ModRM_REG64__MEM]))] class Nop(Instruction): encoding = [(RawBits.from_int(8, 0x90),)] @@ -678,7 +720,7 @@ class MultipleInstr(object): def get_code(self): if self.expected_labels: raise ValueError("Unresolved labels: {self.expected_labels}".format(self=self)) - return "".join([str(x[1].get_code()) for x in sorted(self.instrs.items())]) + return b"".join([bytes(x[1].get_code()) for x in sorted(self.instrs.items())]) def add_instruction(self, instruction): if isinstance(instruction, Label): @@ -746,7 +788,7 @@ class MultipleInstr(object): to_remove = [offset for offset,instr in self.instrs.items() if type(instr) == _NopArtifact] def _remove_nop_artifact(self, offset): - # Remove a NOP from the shellcode + """Remove a NOP from the shellcode, adjust jump and labels""" for src, dst in self.computed_jump: # Reduce size of Jump over the nop (both sens) if src < offset < dst or dst < offset < src: diff --git a/native_exec/simple_x86.py b/native_exec/simple_x86.py index 1c1da4d..e6b8b69 100644 --- a/native_exec/simple_x86.py +++ b/native_exec/simple_x86.py @@ -310,7 +310,6 @@ class ModRM_REG__MEM(object): # Those registers cannot be addressed without SIB # No index -> no scale -> no SIB FIRE_UP_SIB = (arg2.base and arg2.base.upper() in ["ESP", "EBP"]) or arg2.index - if not FIRE_UP_SIB: self.reg = X86RegisterSelector.get_reg_bits(arg1) self.rm = X86RegisterSelector.get_reg_bits(arg2.base) diff --git a/native_exec/test_simple_x64.py b/native_exec/test_simple_x64.py new file mode 100644 index 0000000..8e9f046 --- /dev/null +++ b/native_exec/test_simple_x64.py @@ -0,0 +1,98 @@ +import capstone +from simple_x64 import * + +disassembleur = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) +disassembleur.detail = True + +def disas(x): + return list(disassembleur.disasm(x, 0)) + +mnemonic_name_exception = {'movabs' : 'mov'} + +class TestInstr(object): + def __init__(self, instr_to_test, immediat_accepted=None): + self.instr_to_test = instr_to_test + self.immediat_accepted = immediat_accepted + + def __call__(self, *args): + res = bytes(self.instr_to_test(*args).get_code()) + capres_list = disas(res) + if len(capres_list) != 1: + raise AssertionError("Trying to disas an instruction resulted in multiple disassembled instrs") + capres = capres_list[0] + print("{0} {1}".format(capres.mnemonic, capres.op_str)) + if len(res) != len(capres.bytes): + raise AssertionError("Not all bytes have been used by the disassembler") + self.compare_mnemo(capres) + self.compare_args(args, capres) + + def compare_mnemo(self, capres): + expected = self.instr_to_test.__name__.lower() + cap_mnemo = mnemonic_name_exception.get(str(capres.mnemonic), str(capres.mnemonic)) + if expected != cap_mnemo: + raise AssertionError("Expected menmo {0} got {1}".format(expected, str(capres.mnemonic))) + return True + + def compare_args(self, args, capres): + capres_op = list(capres.operands) + if len(args) != len(capres_op): + raise AssertionError("Expected {0} operands got {1}".format(len(args), len(capres_op))) + for op_args, cap_op in zip(args, capres_op): + if isinstance(op_args, str): # Register + if cap_op.type != capstone.x86.X86_OP_REG: + raise AssertionError("Expected args {0} operands got {1}".format(op_args, capres_op)) + if op_args.lower() != capres.reg_name(cap_op.reg).lower(): + raise AssertionError("Expected register <{0}> got {1}".format(op_args.lower(), capres.reg_name(cap_op.reg).lower())) + elif isinstance(op_args, (int, long)): + if (op_args != cap_op.imm) and not (self.immediat_accepted and self.immediat_accepted == cap_op.imm): + raise AssertionError("Expected Immediat <{0}> got {1}".format(op_args, cap_op.imm)) + elif isinstance(op_args, mem_access): + self.compare_mem_access(op_args, capres, cap_op) + else: + raise ValueError("Unknow argument {0} of type {1}".format(op_args, type(op_args))) + + def compare_mem_access(self, memaccess, capres, cap_op): + if cap_op.type != capstone.x86.X86_OP_MEM: + raise AssertionError("Expected Memaccess <{0}> got {1}".format(memaccess, cap_op)) + cap_mem = cap_op.mem + if memaccess.base is None and cap_mem.base != capstone.x86.X86_REG_INVALID: + raise AssertionError("Unexpected memaccess base <{0}>".format(capres.reg_name(cap_mem.base))) + if memaccess.base is not None and capres.reg_name(cap_mem.base) != memaccess.base.lower(): + raise AssertionError("Expected mem.base {0} got {1}".format(memaccess.base.lower(), capres.reg_name(cap_mem.base))) + if memaccess.index is None and cap_mem.index != capstone.x86.X86_REG_INVALID: + raise AssertionError("Unexpected memaccess index <{0}>".format(capres.reg_name(cap_mem.base))) + if memaccess.index is not None and capres.reg_name(cap_mem.index) != memaccess.index.lower(): + raise AssertionError("Expected mem.index {0} got {1}".format(memaccess.index.lower(), capres.reg_name(cap_mem.index))) + if memaccess.scale != cap_mem.scale and not (memaccess.scale is None and cap_mem.scale == 1): + raise AssertionError("Expected mem.scale {0} got {1}".format(memaccess.scale, cap_mem.scale)) + if memaccess.disp != cap_mem.disp: + raise AssertionError("Expected mem.disp {0} got {1}".format(memaccess.disp, cap_mem.disp)) + + +TestInstr(Add)('RAX', 'RSP') +TestInstr(Add)('RAX', mem('[RCX]')) +TestInstr(Add)('RAX', mem('[RDI + 0x10]')) +TestInstr(Add)('RAX', mem('[RSI + 0x7fffffff]')) +TestInstr(Add)('RAX', mem('[RSI + -0x1]')) +TestInstr(Add)('RAX', mem('[0x10]')) +TestInstr(Add)('RAX', mem('[RSI + RDI * 2]')) +TestInstr(Add)('RAX', mem('[RSI + RDI * 2 + 0x10]')) +TestInstr(Add)('RAX', mem('[R15 * 8 + 0x10]')) +TestInstr(Add)('RAX', mem('[R9 + R8 * 2 + 0x7fffffff]')) +TestInstr(Add)('RAX', mem('[R9 + R8 * 2 + -0x80000000]')) +TestInstr(Add)('RAX', mem('[-1]')) +TestInstr(Add)('RAX', mem('[0x7fffffff]')) +TestInstr(Xor)('R15', mem('[RAX + R8 * 2 + 0x11223344]')) +TestInstr(Xor)('RAX', 'RAX') +TestInstr(Cmp)('RAX', -1) +TestInstr(Lea)('RAX', mem('[RAX + 1]')) +TestInstr(Mov)('RAX', mem('[0x1122334455667788]')) +TestInstr(Mov)('RCX', 0x1122334455667788) +TestInstr(Mov)('RCX', -1) +TestInstr(Mov, immediat_accepted=-1)('RCX', 0xffffffffffffffff) +TestInstr(Mov)(mem('[0x1122334455667788]'), 'RAX') +TestInstr(Push)('R15') +TestInstr(Push)(0x42) +TestInstr(Push)(-1) +TestInstr(Call)('RAX') +TestInstr(Call)(mem('[RAX + RCX * 8]')) \ No newline at end of file diff --git a/native_exec/test_simple_x86.py b/native_exec/test_simple_x86.py index 4677b55..4a9d5d7 100644 --- a/native_exec/test_simple_x86.py +++ b/native_exec/test_simple_x86.py @@ -72,8 +72,11 @@ TestInstr(Mov)('EDX', mem('[ECX + 0x10]')) TestInstr(Mov)('EDX', mem('[EDI * 8 + 0xffff]')) TestInstr(Mov)('EDX', mem('[0x11223344]')) TestInstr(Mov)('EDX', mem('[ESP + EBP * 2 + 0x223344]')) -TestInstr(Mov)('ESP', mem('[EBP + EBP * 2 + 0x223344]')) +TestInstr(Mov)(mem('[EBP + EBP * 2 + 0x223344]'), 'ESP') TestInstr(Mov)('ESI', mem('[ESI + EDI * 1]')) TestInstr(Add)('EAX', 8) TestInstr(Add)('EAX', 0xffffffff) +TestInstr(Lea)('EAX', mem('[EAX + 1]')) +TestInstr(Lea)('ECX', mem('[EDI + -0xff]')) +