From e7679ecfcff5b0ca384daa290f391b2d9c6bb6ee Mon Sep 17 00:00:00 2001 From: hakril Date: Thu, 23 Jan 2025 09:17:23 +0100 Subject: [PATCH] [WIP] simple_arm64 + testsuite --- tests/test_simple_arm64.py | 108 +++++++++++++++ windows/native_exec/simple_arm64.py | 203 ++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 tests/test_simple_arm64.py create mode 100644 windows/native_exec/simple_arm64.py diff --git a/tests/test_simple_arm64.py b/tests/test_simple_arm64.py new file mode 100644 index 0000000..af3e659 --- /dev/null +++ b/tests/test_simple_arm64.py @@ -0,0 +1,108 @@ +try: + import capstone +except ImportError as e: + capstone = None +import pytest + +import windows.native_exec.simple_arm64 as arm64 +from windows.native_exec.simple_arm64 import * + +from windows.pycompat import int_types + +if capstone: + disassembleur = capstone.Cs(capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM) + disassembleur.detail = True + +@pytest.fixture +def need_capstone(): + if capstone is None: + raise pytest.skip("Capstone is not installed") + return True + +pytestmark = pytest.mark.usefixtures("need_capstone") + + +def disas(x): + return list(disassembleur.disasm(x, 0)) + +mnemonic_name_exception = {'movabs': 'mov'} + + +class CheckInstr(object): + def __init__(self, instr_to_test, expected_result=None, immediat_accepted=None, must_fail=None, debug=False): + self.instr_to_test = instr_to_test + self.immediat_accepted = immediat_accepted + self.expected_result = expected_result + self.must_fail = must_fail + self.debug = debug + + def __call__(self, *args): + try: + if self.debug: + import pdb;pdb.set_trace() + pdb.DONE = True + arm64.DEBUG = self.debug + res = bytes(self.instr_to_test(*args).get_code()) + if self.debug: + print(repr(res)) + except ValueError as e: + if self.must_fail == True: + return True + else: + raise + else: + if self.must_fail: + raise ValueError("Instruction did not failed as expected") + 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 self.expected_result is not None: + if "{0} {1}".format(capres.mnemonic, capres.op_str) == self.expected_result: + return True + else: + raise AssertionError("Expected result <{0}> got <{1}>".format(self.expected_result, "{0} {1}".format(capres.mnemonic, capres.op_str))) + if len(res) != len(capres.bytes): + print("<{0}> vs <{1}>".format(repr(res), repr(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.arm64.ARM64_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_types): + 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)) + else: + raise ValueError("Unknow argument {0} of type {1}".format(op_args, type(op_args))) + +def test_assembler(): + CheckInstr(Add)('W0', 'W0', 0) + CheckInstr(Add)('W1', 'W0', 0) + CheckInstr(Add)('W30', 'W12', 0) + CheckInstr(Add)('W0', 'W0', 1) + + CheckInstr(Add)('X0', 'X0', 0) + CheckInstr(Add)('X30', 'X12', 0) + CheckInstr(Add)('X0', 'X0', 1) + CheckInstr(Add)('X11', 'X12', 0x123) + + # Error test todo + # CheckInstr(Add)('X11', 'W12', 0x123) + CheckInstr(Add)('X11', 'X12', 0x12345678) \ No newline at end of file diff --git a/windows/native_exec/simple_arm64.py b/windows/native_exec/simple_arm64.py new file mode 100644 index 0000000..5e0287f --- /dev/null +++ b/windows/native_exec/simple_arm64.py @@ -0,0 +1,203 @@ +import sys +import collections +import struct +import binascii +import operator + + +# py3 +is_py3 = (sys.version_info.major >= 3) +if is_py3: + basestring = str + int_types = int +else: + int_types = (int, long) + +# https://documentation-service.arm.com/static/67581b3355451e3c38d97c22 +# Chapter C4: A64 Instruction Set Encoding: : + +## C2.1.3 +# 32-bit variant (sf = 0). +# 64-bit variant (sf = 1). + +## C2.1.5 +# The following symbol conventions are used: +# The 64-bit name of a general-purpose register (X0-X30) or the zero register (XZR). +# The 32-bit name of a general-purpose register (W0-W30) or the zero register (WZR). +# The 64-bit name of a general-purpose register (X0-X30) or the current stack pointer (SP). +# The 32-bit name of a general-purpose register (W0-W30) or the current stack pointer (WSP). +# , , , , +# The 8, 16, 32, 64, or 128-bit name of a SIMD and floating-point register in a scalar context, as +# described in Register names. +# The name of a SIMD and floating-point register in a vector context, as described in Register names. +# The name of an SVE scalable vector register, as described in Treatment of SVE scalable vector +# registers. +# The name of an SVE scalable predicate register, as described in Vector predication + + +# Make a special memoryview that match what is show in the ARM Chapter C4 ? + +XREGISTER = {'X0', 'X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', 'X11', 'X12', 'X13', 'X14', 'X15', 'X16', 'X17', 'X18', 'X19', 'X20', 'X21', 'X22', 'X23', 'X24', 'X25', 'X26', 'X27', 'X28', 'X29', 'X30'} +WREGISTER = {'W0', 'W1', 'W2', 'W3', 'W4', 'W5', 'W6', 'W7', 'W8', 'W9', 'W10', 'W11', 'W12', 'W13', 'W14', 'W15', 'W16', 'W17', 'W18', 'W19', 'W20', 'W21', 'W22', 'W23', 'W24', 'W25', 'W26', 'W27', 'W28', 'W29', 'W30'} +ALL_REGISTER = XREGISTER | WREGISTER +SP = "SP" +WSP = "WSP" + + +class InstructionEncoding(object): + def __init__(self): + super(InstructionEncoding, self).__init__() + # Bits are in + # 0 1 2 3 4 ... 31 + # Translation to real little-endian is done last + self.bytearray = bytearray(32) + self.bits = memoryview(self.bytearray) + + # Disable with SF = FALSE ? + self.bitness = None + + @classmethod + def is_register(self, arg, accept_sp): + arg = arg.upper() + return (accept_sp and (arg in [SP, WSP])) or arg in ALL_REGISTER + + @classmethod + def is_imm12(self, arg): + try: + value = int(arg) + except (ValueError, TypeError): + return False + return True # Check size max ? + + @classmethod + def is_shift(self, arg): + return True + + @classmethod + def gen(cls, **encoding_array): + class GeneratedEncoding(cls): + ENCODING_VALUES = encoding_array + return GeneratedEncoding + + # Instruction filing at instanciation + + def binencode_imm(self, immediat, outsize): + binstr = "{:0{outsize}b}".format(immediat, outsize=outsize) + assert len(binstr) == outsize, "Could not encode immediat {0} in {1} bits. Value take {2} bits".format(immediat, outsize, len(binstr)) + binlist = [int(c) for c in reversed(binstr)] + return bytearray(binlist) + + def setup_bitness(self, bitness): + assert bitness in (32, 64) + if self.bitness is None: + self.bitness = bitness + if bitness == 32: + self.sf[:] = b"\x00" + else: # bitness == 64: + self.sf[:] = b"\x01" + assert self.bitness == bitness, "bitness mismatch in instruction" + + def encode_register(self, register, outsize=5): + register = register.upper() + assert register in ALL_REGISTER + if register in XREGISTER: + self.setup_bitness(64) + else: + self.setup_bitness(32) + return self.binencode_imm(int(register[1:]), outsize) + + def setup_register(self, regfield, register): + encoded = self.encode_register(register) + regfield[:] = encoded + + # Instruction filing at instanciation + def setup_immediat(self, immfield, value): + immsize = len(immfield) + immfield[:] = self.binencode_imm(value, immsize) + return True + + + +# C4.1.93 Data Processing - Immediate + +class DataProcessingImmediate(InstructionEncoding): + def __init__(self): + super(DataProcessingImmediate, self).__init__() + self.bits[26:29] = bytearray((0,0,1)) + self.op0 = self.bits[29:31] + self.op1 = self.bits[22:26] + +class AddSubtractImmediate(DataProcessingImmediate): + SF = True + RD = True + RN = True + IMM12 = True + SH = True + + def __init__(self, argsdict): + super(AddSubtractImmediate, self).__init__() + self.sf = self.bits[31:32] # Keep it a memoryview + self.op = self.bits[30:31] # Keep it a memoryview + self.S = self.bits[29:30] # Keep it a memoryview + self.bits[23:29] = bytearray((0, 1, 0, 0, 0, 1)) + self.sh = self.bits[22:23] + self.imm12 = self.bits[10:22] + self.rn = self.bits[5:10] + self.rd = self.bits[0:5] + + for name, value in self.ENCODING_VALUES.items(): + print("{0} setting {1} to {2}".format(type(self).__name__, name, value)) + if isinstance(value, int): + value = bytearray((value,)) + # self.x[:] = value + getattr(self, name)[:] = value + + # Change instruction based of parameter + self.setup_register(self.rd, argsdict[0]) + self.setup_register(self.rn, argsdict[1]) + self.setup_immediat(self.imm12, argsdict[2]) + + + + + + @classmethod + def accept_arg(cls, argsdict): + return (cls.is_register(argsdict[0], accept_sp=True) and + cls.is_register(argsdict[1], accept_sp=True) and + cls.is_imm12(argsdict[2]) and + cls.is_shift(argsdict.get(3))) + +class Instruction(object): + encoding = [] + + def __init__(self, *args): + argsdict = dict(enumerate(args)) # Like a list but allow arg.get(4) + for encodcls in self.encoding: + if encodcls.accept_arg(argsdict): + self.encoded = encodcls(argsdict) + return + raise ValueError("Cannot encode <{0} {1}>:(".format(type(self).__name__, args)) + + def get_code(self): + intlist = list(self.encoded.bits) + if not is_py3: + intlist = [ord(x) for x in intlist] + # Our encoding to real little-endian + encoding_getter = operator.itemgetter(7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12, 11, 10, 9, 8, 23, 22, 21, 20, 19, 18, 17, 16, 31, 30, 29, 28, 27, 26, 25, 24) + dword = 0 + for bit in encoding_getter(intlist): + assert bit in (0, 1), "Unexpected bite value in encoding of {0} : {1} in {2}".format(type(self).__name__, bit, intlist) + dword = (dword << 1) | bit + return struct.pack(">I", dword) # We already have handled endianess + + + + # Fix endianned + + +class Add(Instruction): + encoding = [AddSubtractImmediate.gen(op=0, S=0)] + +class Subs(Instruction): + encoding = [AddSubtractImmediate.gen(op=1, S=1)]