503 lines
14 KiB
Python
503 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
|
|
#
|
|
|
|
"""
|
|
This module is intended for general purpose functions that can be used
|
|
thoughout the qiling framework
|
|
"""
|
|
|
|
import importlib
|
|
import inspect
|
|
import os
|
|
|
|
from functools import partial
|
|
from configparser import ConfigParser
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
from typing import TYPE_CHECKING, Any, Callable, Mapping, Optional, Tuple, TypeVar, Union
|
|
|
|
from unicorn import UC_ERR_READ_UNMAPPED, UC_ERR_FETCH_UNMAPPED
|
|
|
|
from qiling.exception import *
|
|
from qiling.const import QL_ARCH, QL_ENDIAN, QL_OS, QL_DEBUGGER
|
|
from qiling.const import debugger_map, arch_map, os_map, arch_os_map
|
|
|
|
if TYPE_CHECKING:
|
|
from qiling import Qiling
|
|
from qiling.arch.arch import QlArch
|
|
from qiling.debugger.debugger import QlDebugger
|
|
from qiling.loader.loader import QlLoader
|
|
from qiling.os.os import QlOs
|
|
|
|
T = TypeVar('T')
|
|
QlClassInit = Callable[['Qiling'], T]
|
|
|
|
|
|
def __name_to_enum(name: str, mapping: Mapping[str, T], aliases: Mapping[str, str] = {}) -> Optional[T]:
|
|
key = name.casefold()
|
|
|
|
return mapping.get(aliases.get(key) or key)
|
|
|
|
|
|
def os_convert(os: str) -> Optional[QL_OS]:
|
|
alias_map = {
|
|
'darwin': 'macos'
|
|
}
|
|
|
|
return __name_to_enum(os, os_map, alias_map)
|
|
|
|
|
|
def arch_convert(arch: str) -> Optional[QL_ARCH]:
|
|
alias_map = {
|
|
'x86_64': 'x8664',
|
|
'riscv32': 'riscv'
|
|
}
|
|
|
|
return __name_to_enum(arch, arch_map, alias_map)
|
|
|
|
|
|
def debugger_convert(debugger: str) -> Optional[QL_DEBUGGER]:
|
|
return __name_to_enum(debugger, debugger_map)
|
|
|
|
|
|
def arch_os_convert(arch: QL_ARCH) -> Optional[QL_OS]:
|
|
return arch_os_map.get(arch)
|
|
|
|
|
|
def ql_get_module(module_name: str) -> ModuleType:
|
|
try:
|
|
module = importlib.import_module(module_name, 'qiling')
|
|
except (ModuleNotFoundError, KeyError):
|
|
raise QlErrorModuleNotFound(f'Unable to import module {module_name}')
|
|
|
|
return module
|
|
|
|
|
|
def ql_get_module_function(module_name: str, member_name: str):
|
|
module = ql_get_module(module_name)
|
|
|
|
try:
|
|
member = getattr(module, member_name)
|
|
except AttributeError:
|
|
raise QlErrorModuleFunctionNotFound(f'Unable to import {member_name} from {module_name}')
|
|
|
|
return member
|
|
|
|
|
|
def __emu_env_from_pathname(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Optional[QL_ENDIAN]]:
|
|
if os.path.isdir(path) and path.endswith('.kext'):
|
|
return QL_ARCH.X8664, QL_OS.MACOS, QL_ENDIAN.EL
|
|
|
|
if os.path.isfile(path):
|
|
_, ext = os.path.splitext(path)
|
|
|
|
if ext in ('.DOS_COM', '.DOS_MBR', '.DOS_EXE'):
|
|
return QL_ARCH.A8086, QL_OS.DOS, QL_ENDIAN.EL
|
|
|
|
return None, None, None
|
|
|
|
|
|
def __emu_env_from_elf(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Optional[QL_ENDIAN]]:
|
|
# instead of using full-blown elffile parsing, we perform a simple parsing to avoid
|
|
# external dependencies for target systems that do not need them.
|
|
#
|
|
# see: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
|
|
|
|
# ei_class
|
|
ELFCLASS32 = 1 # 32-bit
|
|
ELFCLASS64 = 2 # 64-bit
|
|
|
|
# ei_data
|
|
ELFDATA2LSB = 1 # little-endian
|
|
ELFDATA2MSB = 2 # big-endian
|
|
|
|
# ei_osabi
|
|
ELFOSABI_SYSV = 0
|
|
ELFOSABI_LINUX = 3
|
|
ELFOSABI_FREEBSD = 9
|
|
ELFOSABI_ARM_AEABI = 64
|
|
ELFOSABI_ARM = 97
|
|
ELFOSABI_STANDALONE = 255
|
|
|
|
# e_machine
|
|
EM_386 = 3
|
|
EM_MIPS = 8
|
|
EM_ARM = 40
|
|
EM_X86_64 = 62
|
|
EM_AARCH64 = 183
|
|
EM_RISCV = 243
|
|
EM_PPC = 20
|
|
|
|
endianess = {
|
|
ELFDATA2LSB: (QL_ENDIAN.EL, 'little'),
|
|
ELFDATA2MSB: (QL_ENDIAN.EB, 'big')
|
|
}
|
|
|
|
machines32 = {
|
|
EM_386 : QL_ARCH.X86,
|
|
EM_MIPS : QL_ARCH.MIPS,
|
|
EM_ARM : QL_ARCH.ARM,
|
|
EM_RISCV : QL_ARCH.RISCV,
|
|
EM_PPC : QL_ARCH.PPC
|
|
}
|
|
|
|
machines64 = {
|
|
EM_X86_64 : QL_ARCH.X8664,
|
|
EM_AARCH64 : QL_ARCH.ARM64,
|
|
EM_RISCV : QL_ARCH.RISCV64
|
|
}
|
|
|
|
classes = {
|
|
ELFCLASS32: machines32,
|
|
ELFCLASS64: machines64
|
|
}
|
|
|
|
abis = {
|
|
ELFOSABI_SYSV : QL_OS.LINUX,
|
|
ELFOSABI_LINUX : QL_OS.LINUX,
|
|
ELFOSABI_FREEBSD : QL_OS.FREEBSD,
|
|
ELFOSABI_ARM_AEABI : QL_OS.LINUX,
|
|
ELFOSABI_ARM : QL_OS.LINUX,
|
|
ELFOSABI_STANDALONE : QL_OS.BLOB
|
|
}
|
|
|
|
archtype = None
|
|
ostype = None
|
|
archendian = None
|
|
|
|
with open(path, 'rb') as binfile:
|
|
e_ident = binfile.read(16)
|
|
e_type = binfile.read(2)
|
|
e_machine = binfile.read(2)
|
|
|
|
# qnx may be detected by the interpreter name: 'ldqnx.so'.
|
|
# instead of properly parsing the file to locate the pt_interp
|
|
# segment, we detect qnx fuzzily by looking for that string in
|
|
# the first portion of the file.
|
|
blob = binfile.read(0x200 - 20)
|
|
|
|
if e_ident[:4] == b'\x7fELF':
|
|
ei_class = e_ident[4] # arch bits
|
|
ei_data = e_ident[5] # arch endianess
|
|
ei_osabi = e_ident[7]
|
|
|
|
if ei_class in classes:
|
|
machines = classes[ei_class]
|
|
|
|
if ei_data in endianess:
|
|
archendian, endian = endianess[ei_data]
|
|
|
|
machine = int.from_bytes(e_machine, endian)
|
|
|
|
if machine in machines:
|
|
archtype = machines[machine]
|
|
|
|
if ei_osabi in abis:
|
|
ostype = abis[ei_osabi]
|
|
|
|
if blob and b'ldqnx.so' in blob:
|
|
ostype = QL_OS.QNX
|
|
|
|
return archtype, ostype, archendian
|
|
|
|
|
|
def __emu_env_from_macho(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Optional[QL_ENDIAN]]:
|
|
macho_macos_sig64 = b'\xcf\xfa\xed\xfe'
|
|
macho_macos_sig32 = b'\xce\xfa\xed\xfe'
|
|
macho_macos_fat = b'\xca\xfe\xba\xbe' # should be header for FAT
|
|
|
|
arch = None
|
|
ostype = None
|
|
endian = None
|
|
|
|
with open(path, 'rb') as f:
|
|
ident = f.read(32)
|
|
|
|
if ident[:4] in (macho_macos_sig32, macho_macos_sig64, macho_macos_fat):
|
|
ostype = QL_OS.MACOS
|
|
|
|
# if ident[7] == 0: # 32 bit
|
|
# arch = QL_ARCH.X86
|
|
|
|
if ident[4] == 0x07 and ident[7] == 0x01: # X86 64 bit
|
|
endian = QL_ENDIAN.EL
|
|
arch = QL_ARCH.X8664
|
|
|
|
elif ident[4] == 0x0c and ident[7] == 0x01: # ARM64
|
|
endian = QL_ENDIAN.EL
|
|
arch = QL_ARCH.ARM64
|
|
|
|
return arch, ostype, endian
|
|
|
|
|
|
def __emu_env_from_pe(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Optional[QL_ENDIAN]]:
|
|
import pefile
|
|
|
|
try:
|
|
pe = pefile.PE(path, fast_load=True)
|
|
except:
|
|
return None, None, None
|
|
|
|
arch = None
|
|
ostype = None
|
|
archendian = None
|
|
|
|
machine_map = {
|
|
pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_I386'] : QL_ARCH.X86,
|
|
pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_AMD64'] : QL_ARCH.X8664,
|
|
pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_ARM'] : QL_ARCH.ARM,
|
|
pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_THUMB'] : QL_ARCH.ARM,
|
|
pefile.MACHINE_TYPE['IMAGE_FILE_MACHINE_ARM64'] : QL_ARCH.ARM64
|
|
}
|
|
|
|
# get arch
|
|
arch = machine_map.get(pe.FILE_HEADER.Machine)
|
|
|
|
if arch:
|
|
subsystem_uefi = (
|
|
pefile.SUBSYSTEM_TYPE['IMAGE_SUBSYSTEM_EFI_APPLICATION'],
|
|
pefile.SUBSYSTEM_TYPE['IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER'],
|
|
pefile.SUBSYSTEM_TYPE['IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER'],
|
|
pefile.SUBSYSTEM_TYPE['IMAGE_SUBSYSTEM_EFI_ROM']
|
|
)
|
|
|
|
if pe.OPTIONAL_HEADER.Subsystem in subsystem_uefi:
|
|
ostype = QL_OS.UEFI
|
|
else:
|
|
ostype = QL_OS.WINDOWS
|
|
|
|
archendian = QL_ENDIAN.EL
|
|
|
|
return arch, ostype, archendian
|
|
|
|
|
|
def ql_guess_emu_env(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Optional[QL_ENDIAN]]:
|
|
guessing_methods = (
|
|
__emu_env_from_pathname,
|
|
__emu_env_from_elf,
|
|
__emu_env_from_macho,
|
|
__emu_env_from_pe
|
|
)
|
|
|
|
for gm in guessing_methods:
|
|
arch, ostype, endian = gm(path)
|
|
|
|
if None not in (arch, ostype, endian):
|
|
break
|
|
else:
|
|
arch, ostype, endian = (None, ) * 3
|
|
|
|
return arch, ostype, endian
|
|
|
|
|
|
def select_loader(ostype: QL_OS, libcache: bool) -> QlClassInit['QlLoader']:
|
|
if ostype is QL_OS.WINDOWS:
|
|
kwargs = {'libcache': libcache}
|
|
|
|
else:
|
|
kwargs = {}
|
|
|
|
module = {
|
|
QL_OS.LINUX : r'elf',
|
|
QL_OS.FREEBSD : r'elf',
|
|
QL_OS.QNX : r'elf',
|
|
QL_OS.MACOS : r'macho',
|
|
QL_OS.WINDOWS : r'pe',
|
|
QL_OS.UEFI : r'pe_uefi',
|
|
QL_OS.DOS : r'dos',
|
|
QL_OS.EVM : r'evm',
|
|
QL_OS.MCU : r'mcu',
|
|
QL_OS.BLOB : r'blob'
|
|
}[ostype]
|
|
|
|
qlloader_path = f'.loader.{module}'
|
|
qlloader_class = f'QlLoader{module.upper()}'
|
|
|
|
obj = ql_get_module_function(qlloader_path, qlloader_class)
|
|
|
|
return partial(obj, **kwargs)
|
|
|
|
|
|
def select_component(component_type: str, component_name: str, **kwargs) -> QlClassInit[Any]:
|
|
component_path = f'.{component_type}.{component_name}'
|
|
component_class = f'Ql{component_name.capitalize()}Manager'
|
|
|
|
obj = ql_get_module_function(component_path, component_class)
|
|
|
|
return partial(obj, **kwargs)
|
|
|
|
|
|
def select_debugger(options: Union[str, bool]) -> Optional[QlClassInit['QlDebugger']]:
|
|
if options is True:
|
|
options = 'gdb'
|
|
|
|
if type(options) is str:
|
|
objname, *args = options.split(':')
|
|
dbgtype = debugger_convert(objname)
|
|
|
|
if dbgtype == QL_DEBUGGER.GDB:
|
|
kwargs = dict(zip(('ip', 'port'), args))
|
|
|
|
elif dbgtype == QL_DEBUGGER.QDB:
|
|
kwargs = {}
|
|
|
|
def __int_nothrow(v: str, /) -> Optional[int]:
|
|
try:
|
|
return int(v, 0)
|
|
except ValueError:
|
|
return None
|
|
|
|
# qdb init args are independent and may include any combination of: rr enable, init hook and script
|
|
for a in args:
|
|
if a == 'rr':
|
|
kwargs['rr'] = True
|
|
|
|
elif __int_nothrow(a) is not None:
|
|
kwargs['init_hook'] = a
|
|
|
|
else:
|
|
kwargs['script'] = a
|
|
|
|
else:
|
|
raise QlErrorOutput('Debugger not supported')
|
|
|
|
obj = ql_get_module_function(f'.debugger.{objname}.{objname}', f'Ql{str.capitalize(objname)}')
|
|
|
|
return partial(obj, **kwargs)
|
|
|
|
return None
|
|
|
|
|
|
def select_arch(archtype: QL_ARCH, endian: QL_ENDIAN, thumb: bool) -> QlClassInit['QlArch']:
|
|
# set endianess and thumb mode for arm-based archs
|
|
if archtype is QL_ARCH.ARM:
|
|
kwargs = {'endian': endian, 'thumb': thumb}
|
|
|
|
# set endianess for mips arch
|
|
elif archtype is QL_ARCH.MIPS:
|
|
kwargs = {'endian': endian}
|
|
|
|
else:
|
|
kwargs = {}
|
|
|
|
module = {
|
|
QL_ARCH.A8086 : r'x86',
|
|
QL_ARCH.X86 : r'x86',
|
|
QL_ARCH.X8664 : r'x86',
|
|
QL_ARCH.ARM : r'arm',
|
|
QL_ARCH.ARM64 : r'arm64',
|
|
QL_ARCH.MIPS : r'mips',
|
|
QL_ARCH.EVM : r'evm.evm',
|
|
QL_ARCH.CORTEX_M : r'cortex_m',
|
|
QL_ARCH.RISCV : r'riscv',
|
|
QL_ARCH.RISCV64 : r'riscv64',
|
|
QL_ARCH.PPC : r'ppc'
|
|
}[archtype]
|
|
|
|
qlarch_path = f'.arch.{module}'
|
|
qlarch_class = f'QlArch{archtype.name.upper()}'
|
|
|
|
obj = ql_get_module_function(qlarch_path, qlarch_class)
|
|
|
|
return partial(obj, **kwargs)
|
|
|
|
|
|
def select_os(ostype: QL_OS) -> QlClassInit['QlOs']:
|
|
qlos_name = ostype.name
|
|
qlos_path = f'.os.{qlos_name.lower()}.{qlos_name.lower()}'
|
|
qlos_class = f'QlOs{qlos_name.capitalize()}'
|
|
|
|
obj = ql_get_module_function(qlos_path, qlos_class)
|
|
|
|
return partial(obj)
|
|
|
|
|
|
def profile_setup(ostype: QL_OS, user_config: Optional[Union[str, dict]]):
|
|
# mcu uses a yaml-based config
|
|
if ostype is QL_OS.MCU:
|
|
import yaml
|
|
|
|
if user_config:
|
|
with open(user_config) as f:
|
|
config = yaml.load(f, Loader=yaml.SafeLoader)
|
|
else:
|
|
config = {}
|
|
|
|
else:
|
|
# patch 'getint' to convert integers of all bases
|
|
int_converter = partial(int, base=0)
|
|
config = ConfigParser(converters={'int': int_converter})
|
|
|
|
qiling_home = Path(inspect.getfile(inspect.currentframe())).parent
|
|
os_profile = qiling_home / 'profiles' / f'{ostype.name.lower()}.ql'
|
|
|
|
# read default profile first
|
|
config.read(os_profile)
|
|
|
|
# user-specified profile adds or overrides existing setting
|
|
if isinstance(user_config, dict):
|
|
config.read_dict(user_config)
|
|
|
|
elif user_config:
|
|
config.read(user_config)
|
|
|
|
return config
|
|
|
|
|
|
# verify if emulator returns properly
|
|
def verify_ret(ql: 'Qiling', err):
|
|
# init_sp location is not consistent; this is here to work around that
|
|
if not hasattr(ql.os, 'init_sp'):
|
|
ql.os.init_sp = ql.loader.init_sp
|
|
|
|
ql.log.debug("Got exception %u: init SP = %x, current SP = %x, PC = %x" %(err.errno, ql.os.init_sp, ql.arch.regs.arch_sp, ql.arch.regs.arch_pc))
|
|
|
|
if hasattr(ql.os, 'RUN'):
|
|
ql.os.RUN = False
|
|
|
|
# timeout is acceptable in this case
|
|
if err.errno in (UC_ERR_READ_UNMAPPED, UC_ERR_FETCH_UNMAPPED):
|
|
if ql.os.type == QL_OS.MACOS:
|
|
if ql.loader.kext_name:
|
|
# FIXME: Should I push saved RIP before every method callings of IOKit object?
|
|
if ql.os.init_sp == ql.arch.regs.arch_sp - 8:
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
if ql.arch.type == QL_ARCH.X8664: # Win64
|
|
if ql.os.init_sp == ql.arch.regs.arch_sp or ql.os.init_sp + 8 == ql.arch.regs.arch_sp or ql.os.init_sp + 0x10 == ql.arch.regs.arch_sp: # FIXME
|
|
# 0x11626 c3 ret
|
|
# print("OK, stack balanced!")
|
|
pass
|
|
else:
|
|
raise
|
|
else: # Win32
|
|
if ql.os.init_sp + 12 == ql.arch.regs.arch_sp: # 12 = 8 + 4
|
|
# 0x114dd c2 08 00 ret 8
|
|
pass
|
|
else:
|
|
raise
|
|
else:
|
|
raise
|
|
|
|
|
|
__all__ = [
|
|
'os_convert',
|
|
'arch_convert',
|
|
'debugger_convert',
|
|
'arch_os_convert',
|
|
'ql_get_module',
|
|
'ql_get_module_function',
|
|
'ql_guess_emu_env',
|
|
'select_os',
|
|
'select_arch',
|
|
'select_loader',
|
|
'select_debugger',
|
|
'select_component',
|
|
'profile_setup',
|
|
'verify_ret'
|
|
]
|