diff --git a/examples/scripts/dllscollector.bat b/examples/scripts/dllscollector.bat
index 048d5f9c3..cd115093e 100644
--- a/examples/scripts/dllscollector.bat
+++ b/examples/scripts/dllscollector.bat
@@ -118,6 +118,8 @@ CALL :collect_dll64 shlwapi.dll
CALL :collect_dll64 user32.dll
CALL :collect_dll64 vcruntime140.dll
CALL :collect_dll64 vcruntime140d.dll
+CALL :collect_dll64 vcruntime140_1.dll
+CALL :collect_dll64 vcruntime140_1d.dll
CALL :collect_dll64 win32u.dll
CALL :collect_dll64 winhttp.dll
CALL :collect_dll64 wininet.dll
diff --git a/qiling/core.py b/qiling/core.py
index 2cb5aff75..6cc5c4706 100644
--- a/qiling/core.py
+++ b/qiling/core.py
@@ -32,10 +32,10 @@
class Qiling(QlCoreHooks, QlCoreStructs):
def __init__(
self,
- argv: Sequence[str] = None,
+ argv: Sequence[str] = [],
rootfs: str = r'.',
env: MutableMapping[AnyStr, AnyStr] = {},
- code: bytes = None,
+ code: Optional[bytes] = None,
ostype: Union[str, QL_OS] = None,
archtype: Union[str, QL_ARCH] = None,
verbose: QL_VERBOSE = QL_VERBOSE.DEFAULT,
@@ -90,18 +90,26 @@ def __init__(
##############
# argv setup #
##############
- if argv is None:
- argv = ['qilingcode']
+ if argv:
+ if code:
+ raise AttributeError('argv and code are mutually execlusive')
- elif not os.path.exists(argv[0]):
- raise QlErrorFileNotFound(f'Target binary not found: "{argv[0]}"')
+ target = argv[0]
+
+ if not os.path.isfile(target):
+ raise QlErrorFileNotFound(f'Target binary not found: "{target}"')
+ else:
+ # an empty argv list means we are going to execute a shellcode. to keep
+ # the 'path' api compatible, we insert a dummy placeholder
+
+ argv = ['']
self._argv = argv
################
# rootfs setup #
################
- if not os.path.exists(rootfs):
+ if not os.path.isdir(rootfs):
raise QlErrorFileNotFound(f'Target rootfs not found: "{rootfs}"')
self._rootfs = rootfs
@@ -697,11 +705,11 @@ def restore(self, saved_states: Mapping[str, Any] = {}, *, snapshot: Optional[st
# Map "ql_path" to any objects which implements QlFsMappedObject.
def add_fs_mapper(self, ql_path: Union["PathLike", str], real_dest):
- self.os.fs_mapper.add_fs_mapping(ql_path, real_dest)
+ self.os.fs_mapper.add_mapping(ql_path, real_dest)
# Remove "ql_path" mapping.
def remove_fs_mapper(self, ql_path: Union["PathLike", str]):
- self.os.fs_mapper.remove_fs_mapping(ql_path)
+ self.os.fs_mapper.remove_mapping(ql_path)
# push to stack bottom, and update stack register
def stack_push(self, data):
@@ -757,14 +765,16 @@ def emu_start(self, begin: int, end: int, timeout: int = 0, count: int = 0):
if getattr(self.arch, '_init_thumb', False):
begin |= 0b1
- self._state = QL_STATE.STARTED
-
# reset exception status before emulation starts
self._internal_exception = None
+ self._state = QL_STATE.STARTED
+
# effectively start the emulation. this returns only after uc.emu_stop is called
self.uc.emu_start(begin, end, timeout, count)
+ self._state = QL_STATE.STOPPED
+
# if an exception was raised during emulation, propagate it up
if self.internal_exception is not None:
raise self.internal_exception
diff --git a/qiling/core_hooks.py b/qiling/core_hooks.py
index f142684fd..58d4d736e 100644
--- a/qiling/core_hooks.py
+++ b/qiling/core_hooks.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
@@ -8,22 +8,53 @@
# handling hooks #
##############################################
-import functools
+from __future__ import annotations
+
+from functools import wraps
from typing import Any, Callable, MutableMapping, MutableSequence, Protocol
from typing import TYPE_CHECKING
-from unicorn import Uc
-from unicorn.unicorn_const import *
+from unicorn.unicorn_const import (
+ UC_HOOK_INTR,
+ UC_HOOK_INSN,
+ UC_HOOK_CODE,
+ UC_HOOK_BLOCK,
+
+ UC_HOOK_MEM_READ_UNMAPPED, # attempt to read from an unmapped memory location
+ UC_HOOK_MEM_WRITE_UNMAPPED, # attempt to write to an unmapped memory location
+ UC_HOOK_MEM_FETCH_UNMAPPED, # attempt to fetch from an unmapped memory location
+ UC_HOOK_MEM_UNMAPPED, # any of the 3 above
+
+ UC_HOOK_MEM_READ_PROT, # attempt to read from a non-readable memory location
+ UC_HOOK_MEM_WRITE_PROT, # attempt to write to a write-protected memory location
+ UC_HOOK_MEM_FETCH_PROT, # attempt to fetch from a non-executable memory location
+ UC_HOOK_MEM_PROT, # any of the 3 above
+
+ UC_HOOK_MEM_READ_INVALID, # UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_READ_PROT
+ UC_HOOK_MEM_WRITE_INVALID, # UC_HOOK_MEM_WRITE_UNMAPPED | UC_HOOK_MEM_WRITE_INVALID
+ UC_HOOK_MEM_FETCH_INVALID, # UC_HOOK_MEM_FETCH_UNMAPPED | UC_HOOK_MEM_FETCH_INVALID
+ UC_HOOK_MEM_INVALID, # any of the 3 above
+
+ UC_HOOK_MEM_READ, # valid memory read
+ UC_HOOK_MEM_WRITE, # valid memory write
+ UC_HOOK_MEM_FETCH, # valid instruction fetch
+ UC_HOOK_MEM_VALID, # any of the 3 above
+
+ UC_HOOK_MEM_READ_AFTER,
+ UC_HOOK_INSN_INVALID
+)
from .core_hooks_types import Hook, HookAddr, HookIntr, HookRet
from .const import QL_HOOK_BLOCK
from .exception import QlErrorCoreHook
if TYPE_CHECKING:
+ from unicorn import Uc
from qiling import Qiling
+
class MemHookCallback(Protocol):
- def __call__(self, __ql: 'Qiling', __access: int, __address: int, __size: int, __value: int, *__context: Any) -> Any:
+ def __call__(self, __ql: Qiling, __access: int, __address: int, __size: int, __value: int, *__context: Any) -> Any:
"""Memory access hook callback.
Args:
@@ -40,8 +71,9 @@ def __call__(self, __ql: 'Qiling', __access: int, __address: int, __size: int, _
"""
pass
+
class TraceHookCalback(Protocol):
- def __call__(self, __ql: 'Qiling', __address: int, __size: int, *__context: Any) -> Any:
+ def __call__(self, __ql: Qiling, __address: int, __size: int, *__context: Any) -> Any:
"""Execution hook callback.
Args:
@@ -56,8 +88,9 @@ def __call__(self, __ql: 'Qiling', __address: int, __size: int, *__context: Any)
"""
pass
+
class AddressHookCallback(Protocol):
- def __call__(self, __ql: 'Qiling', *__context: Any) -> Any:
+ def __call__(self, __ql: Qiling, *__context: Any) -> Any:
"""Address hook callback.
Args:
@@ -70,8 +103,9 @@ def __call__(self, __ql: 'Qiling', *__context: Any) -> Any:
"""
pass
+
class InterruptHookCallback(Protocol):
- def __call__(self, __ql: 'Qiling', intno: int, *__context: Any) -> Any:
+ def __call__(self, __ql: Qiling, intno: int, *__context: Any) -> Any:
"""Interrupt hook callback.
Args:
@@ -86,9 +120,8 @@ def __call__(self, __ql: 'Qiling', intno: int, *__context: Any) -> Any:
pass
-def hookcallback(ql: 'Qiling', callback: Callable):
-
- functools.wraps(callback)
+def hookcallback(ql: Qiling, callback: Callable):
+ @wraps(callback)
def wrapper(*args, **kwargs):
try:
return callback(*args, **kwargs)
@@ -113,10 +146,10 @@ def __init__(self, uc: Uc):
self._addr_hook: MutableMapping[int, MutableSequence[HookAddr]] = {}
self._addr_hook_fuc: MutableMapping[int, int] = {}
-
########################
# Callback definitions #
########################
+
def _hook_intr_cb(self, uc: Uc, intno: int, pack_data) -> None:
"""Interrupt hooks dispatcher.
"""
@@ -142,7 +175,6 @@ def _hook_intr_cb(self, uc: Uc, intno: int, pack_data) -> None:
if not handled:
raise QlErrorCoreHook("_hook_intr_cb : not handled")
-
def _hook_insn_cb(self, uc: Uc, *args):
"""Instruction hooks dispatcher.
"""
@@ -166,7 +198,6 @@ def _hook_insn_cb(self, uc: Uc, *args):
# use the last return value received
return retval
-
def _hook_trace_cb(self, uc: Uc, addr: int, size: int, pack_data) -> None:
"""Code and block hooks dispatcher.
"""
@@ -183,7 +214,6 @@ def _hook_trace_cb(self, uc: Uc, addr: int, size: int, pack_data) -> None:
if type(ret) is int and ret & QL_HOOK_BLOCK:
break
-
def _hook_mem_cb(self, uc: Uc, access: int, addr: int, size: int, value: int, pack_data):
"""Memory access hooks dispatcher.
"""
@@ -207,7 +237,6 @@ def _hook_mem_cb(self, uc: Uc, access: int, addr: int, size: int, value: int, pa
return True
-
def _hook_insn_invalid_cb(self, uc: Uc, pack_data) -> None:
"""Invalid instruction hooks dispatcher.
"""
@@ -228,7 +257,6 @@ def _hook_insn_invalid_cb(self, uc: Uc, pack_data) -> None:
if not handled:
raise QlErrorCoreHook("_hook_insn_invalid_cb : not handled")
-
def _hook_addr_cb(self, uc: Uc, addr: int, size: int, pack_data):
"""Address hooks dispatcher.
"""
@@ -247,18 +275,17 @@ def _hook_addr_cb(self, uc: Uc, addr: int, size: int, pack_data):
###############
# Class Hooks #
###############
+
def _ql_hook_internal(self, hook_type: int, callback: Callable, context: Any, *args) -> int:
_callback = hookcallback(self, callback)
return self._h_uc.hook_add(hook_type, _callback, (self, context), 1, 0, *args)
-
def _ql_hook_addr_internal(self, callback: Callable, address: int) -> int:
_callback = hookcallback(self, callback)
return self._h_uc.hook_add(UC_HOOK_CODE, _callback, self, address, address)
-
def _ql_hook(self, hook_type: int, h: Hook, *args) -> None:
def __handle_intr(t: int) -> None:
@@ -330,7 +357,6 @@ def __handle_invalid_insn(t: int) -> None:
if hook_type & t:
handler(t)
-
def ql_hook(self, hook_type: int, callback: Callable, user_data: Any = None, begin: int = 1, end: int = 0, *args) -> HookRet:
"""Intercept certain emulation events within a specified range.
@@ -355,7 +381,6 @@ def ql_hook(self, hook_type: int, callback: Callable, user_data: Any = None, beg
return HookRet(self, hook_type, hook)
-
def hook_code(self, callback: TraceHookCalback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept assembly instructions before they get executed.
@@ -374,12 +399,10 @@ def hook_code(self, callback: TraceHookCalback, user_data: Any = None, begin: in
return self.ql_hook(UC_HOOK_CODE, callback, user_data, begin, end)
-
# TODO: remove; this is a special case of hook_intno(-1)
def hook_intr(self, callback, user_data=None, begin=1, end=0):
return self.ql_hook(UC_HOOK_INTR, callback, user_data, begin, end)
-
def hook_block(self, callback: TraceHookCalback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept landings in new basic blocks in a specified range.
@@ -398,7 +421,6 @@ def hook_block(self, callback: TraceHookCalback, user_data: Any = None, begin: i
return self.ql_hook(UC_HOOK_BLOCK, callback, user_data, begin, end)
-
def hook_mem_unmapped(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept illegal accesses to unmapped memory in a specified range.
@@ -417,7 +439,6 @@ def hook_mem_unmapped(self, callback: MemHookCallback, user_data: Any = None, be
return self.ql_hook(UC_HOOK_MEM_UNMAPPED, callback, user_data, begin, end)
-
def hook_mem_read_invalid(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept illegal reading attempts from a specified range.
@@ -436,7 +457,6 @@ def hook_mem_read_invalid(self, callback: MemHookCallback, user_data: Any = None
return self.ql_hook(UC_HOOK_MEM_READ_INVALID, callback, user_data, begin, end)
-
def hook_mem_write_invalid(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept illegal writing attempts to a specified range.
@@ -455,7 +475,6 @@ def hook_mem_write_invalid(self, callback: MemHookCallback, user_data: Any = Non
return self.ql_hook(UC_HOOK_MEM_WRITE_INVALID, callback, user_data, begin, end)
-
def hook_mem_fetch_invalid(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept illegal code fetching attempts from a specified range.
@@ -474,7 +493,6 @@ def hook_mem_fetch_invalid(self, callback: MemHookCallback, user_data: Any = Non
return self.ql_hook(UC_HOOK_MEM_FETCH_INVALID, callback, user_data, begin, end)
-
def hook_mem_valid(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept benign memory accesses within a specified range.
This is equivalent to hooking memory reads, writes and fetches.
@@ -494,7 +512,6 @@ def hook_mem_valid(self, callback: MemHookCallback, user_data: Any = None, begin
return self.ql_hook(UC_HOOK_MEM_VALID, callback, user_data, begin, end)
-
def hook_mem_invalid(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept invalid memory accesses within a specified range.
This is equivalent to hooking invalid memory reads, writes and fetches.
@@ -514,7 +531,6 @@ def hook_mem_invalid(self, callback: MemHookCallback, user_data: Any = None, beg
return self.ql_hook(UC_HOOK_MEM_INVALID, callback, user_data, begin, end)
-
def hook_address(self, callback: AddressHookCallback, address: int, user_data: Any = None) -> HookRet:
"""Intercept execution from a certain memory address.
@@ -540,7 +556,6 @@ def hook_address(self, callback: AddressHookCallback, address: int, user_data: A
# note: assuming 0 is not a valid hook type
return HookRet(self, 0, hook)
-
def hook_intno(self, callback: InterruptHookCallback, intno: int, user_data: Any = None) -> HookRet:
"""Intercept interrupts.
@@ -558,7 +573,6 @@ def hook_intno(self, callback: InterruptHookCallback, intno: int, user_data: Any
return HookRet(self, UC_HOOK_INTR, hook)
-
def hook_mem_read(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept benign memory reads from a specified range.
@@ -577,7 +591,6 @@ def hook_mem_read(self, callback: MemHookCallback, user_data: Any = None, begin:
return self.ql_hook(UC_HOOK_MEM_READ, callback, user_data, begin, end)
-
def hook_mem_write(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept benign memory writes to a specified range.
@@ -596,7 +609,6 @@ def hook_mem_write(self, callback: MemHookCallback, user_data: Any = None, begin
return self.ql_hook(UC_HOOK_MEM_WRITE, callback, user_data, begin, end)
-
def hook_mem_fetch(self, callback: MemHookCallback, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept benign code fetches from a specified range.
@@ -615,7 +627,6 @@ def hook_mem_fetch(self, callback: MemHookCallback, user_data: Any = None, begin
return self.ql_hook(UC_HOOK_MEM_FETCH, callback, user_data, begin, end)
-
def hook_insn(self, callback, insn_type: int, user_data: Any = None, begin: int = 1, end: int = 0) -> HookRet:
"""Intercept execution of a certain instruction type within a specified range.
@@ -637,7 +648,6 @@ def hook_insn(self, callback, insn_type: int, user_data: Any = None, begin: int
return self.ql_hook(UC_HOOK_INSN, callback, user_data, begin, end, insn_type)
-
def hook_del(self, hret: HookRet) -> None:
"""Unregister an existing hook and release its resources.
@@ -692,7 +702,6 @@ def __remove(hooks_map, handles_map, key: int) -> None:
if hook_type & t:
handler(t)
-
def clear_hooks(self):
for ptr in self._hook_fuc.values():
self._h_uc.hook_del(ptr)
@@ -705,7 +714,6 @@ def clear_hooks(self):
self.clear_ql_hooks()
-
def clear_ql_hooks(self):
self._hook = {}
self._hook_fuc = {}
diff --git a/qiling/debugger/gdb/gdb.py b/qiling/debugger/gdb/gdb.py
index 6dd7b3614..e8c501181 100644
--- a/qiling/debugger/gdb/gdb.py
+++ b/qiling/debugger/gdb/gdb.py
@@ -78,36 +78,42 @@ def __init__(self, ql: Qiling, ip: str = '127.0.0.1', port: int = 9999):
self.ip = ip
self.port = port
- if ql.baremetal:
- load_address = ql.loader.load_address
- exit_point = load_address + os.path.getsize(ql.path)
- elif ql.code:
- load_address = ql.os.entry_point
- exit_point = load_address + len(ql.code)
- else:
- load_address = ql.loader.load_address
- exit_point = load_address + os.path.getsize(ql.path)
+ def __get_attach_addr() -> int:
+ if ql.baremetal:
+ entry_point = ql.loader.entry_point
- if ql.baremetal:
- entry_point = ql.loader.entry_point
- elif ql.os.type in (QL_OS.LINUX, QL_OS.FREEBSD) and not ql.code:
- entry_point = ql.os.elf_entry
- else:
- entry_point = ql.os.entry_point
+ elif ql.os.type in (QL_OS.LINUX, QL_OS.FREEBSD) and not ql.code:
+ entry_point = ql.os.elf_entry
+
+ else:
+ entry_point = ql.os.entry_point
+
+ # though linkers set the entry point LSB to indicate arm thumb mode, the
+ # effective entry point address is aligned. make sure we have it aligned
+ if hasattr(ql.arch, 'is_thumb'):
+ entry_point &= ~0b1
- # though linkers set the entry point LSB to indicate arm thumb mode, the
- # effective entry point address is aligned. make sure we have it aligned
- if hasattr(ql.arch, 'is_thumb'):
- entry_point &= ~0b1
+ return entry_point
+
+ def __get_detach_addr() -> int:
+ if ql.baremetal:
+ base = ql.loader.load_address
+ size = os.path.getsize(ql.path)
+
+ elif ql.code:
+ base = ql.os.entry_point
+ size = len(ql.code)
+
+ else:
+ base = ql.loader.load_address
+ size = os.path.getsize(ql.path)
- # Only part of the binary file will be debugged.
- if ql.entry_point is not None:
- entry_point = ql.entry_point
+ return base + size
- if ql.exit_point is not None:
- exit_point = ql.exit_point
+ attach_addr = __get_attach_addr() if ql.entry_point is None else ql.entry_point
+ detach_addr = __get_detach_addr() if ql.exit_point is None else ql.exit_point
- self.gdb = QlGdbUtils(ql, entry_point, exit_point)
+ self.gdb = QlGdbUtils(ql, attach_addr, detach_addr)
self.features = QlGdbFeatures(self.ql.arch.type, self.ql.os.type)
self.regsmap = self.features.regsmap
@@ -293,11 +299,11 @@ def handle_m(subcmd: str) -> Reply:
addr, size = (int(p, 16) for p in subcmd.split(','))
try:
- data = self.ql.mem.read(addr, size).hex()
- except UcError:
- return 'E14'
+ data = self.ql.mem.read(addr, size)
+ except UcError as ex:
+ return f'E{ex.errno:02d}'
else:
- return data
+ return data.hex()
def handle_M(subcmd: str) -> Reply:
"""Write target memory.
@@ -313,8 +319,8 @@ def handle_M(subcmd: str) -> Reply:
try:
self.ql.mem.write(addr, data)
- except UcError:
- return 'E01'
+ except UcError as ex:
+ return f'E{ex.errno:02d}'
else:
return REPLY_OK
@@ -693,8 +699,8 @@ def handle_X(subcmd: str) -> Reply:
try:
if data:
self.ql.mem.write(addr, data.encode(ENCODING))
- except UcError:
- return 'E01'
+ except UcError as ex:
+ return f'E{ex.errno:02d}'
else:
return REPLY_OK
@@ -837,7 +843,7 @@ def readpackets(self) -> Iterator[bytes]:
buffer += incoming
# discard incoming acks
- if buffer[0:1] == REPLY_ACK:
+ if buffer.startswith(REPLY_ACK):
del buffer[0]
packet = pattern.match(buffer)
diff --git a/qiling/extensions/idaplugin/qilingida.py b/qiling/extensions/idaplugin/qilingida.py
index 2d68a258a..f3524ec94 100644
--- a/qiling/extensions/idaplugin/qilingida.py
+++ b/qiling/extensions/idaplugin/qilingida.py
@@ -899,7 +899,7 @@ def start(self, *args, **kwargs):
elffile = ELFFile(f)
elf_header = elffile.header
if elf_header['e_type'] == 'ET_EXEC':
- self.baseaddr = self.ql.os.elf_mem_start
+ self.baseaddr = self.ql.loader.images[0].base
elif elf_header['e_type'] == 'ET_DYN':
if self.ql.arch.bits == 32:
self.baseaddr = int(self.ql.os.profile.get("OS32", "load_address"), 16)
diff --git a/qiling/loader/dos.py b/qiling/loader/dos.py
index 5e3db8bbd..847a51c49 100644
--- a/qiling/loader/dos.py
+++ b/qiling/loader/dos.py
@@ -84,7 +84,7 @@ def run(self):
# https://en.wikipedia.org/wiki/Master_boot_record#BIOS_to_MBR_interface
if not self.ql.os.fs_mapper.has_mapping(0x80):
- self.ql.os.fs_mapper.add_fs_mapping(0x80, QlDisk(path, 0x80))
+ self.ql.os.fs_mapper.add_mapping(0x80, QlDisk(path, 0x80))
# 0x80 -> first drive
self.ql.arch.regs.dx = 0x80
diff --git a/qiling/loader/elf.py b/qiling/loader/elf.py
index 5b44ef570..f3df83b99 100644
--- a/qiling/loader/elf.py
+++ b/qiling/loader/elf.py
@@ -230,10 +230,15 @@ def load_elf_segments(elffile: ELFFile, load_address: int, info: str):
# load the interpreter, if there is one
if interp_path:
- interp_local_path = os.path.normpath(self.ql.rootfs + interp_path)
- self.ql.log.debug(f'Interpreter path: {interp_local_path}')
+ interp_vpath = self.ql.os.path.virtual_abspath(interp_path)
+ interp_hpath = self.ql.os.path.virtual_to_host_path(interp_path)
- with open(interp_local_path, 'rb') as infile:
+ self.ql.log.debug(f'Interpreter path: {interp_vpath}')
+
+ if not self.ql.os.path.is_safe_host_path(interp_hpath):
+ raise PermissionError(f'unsafe path: {interp_hpath}')
+
+ with open(interp_hpath, 'rb') as infile:
interp = ELFFile(infile)
min_vaddr = min(seg['p_vaddr'] for seg in interp.iter_segments(type='PT_LOAD'))
@@ -244,10 +249,10 @@ def load_elf_segments(elffile: ELFFile, load_address: int, info: str):
self.ql.log.debug(f'Interpreter addr: {interp_address:#x}')
# load interpreter segments data to memory
- interp_start, interp_end = load_elf_segments(interp, interp_address, interp_local_path)
+ interp_start, interp_end = load_elf_segments(interp, interp_address, interp_vpath)
# add interpreter to the loaded images list
- self.images.append(Image(interp_start, interp_end, os.path.abspath(interp_local_path)))
+ self.images.append(Image(interp_start, interp_end, interp_hpath))
# determine entry point
entry_point = interp_address + interp['e_entry']
@@ -353,7 +358,6 @@ def __push_str(top: int, s: str) -> int:
self.init_sp = self.ql.arch.regs.arch_sp
self.ql.os.entry_point = self.entry_point = entry_point
- self.ql.os.elf_mem_start = mem_start
self.ql.os.elf_entry = self.elf_entry
self.ql.os.function_hook = FunctionHook(self.ql, elf_phdr, elf_phnum, elf_phent, load_address, mem_end)
diff --git a/qiling/loader/loader.py b/qiling/loader/loader.py
index 6be0ccf1d..32475f5fe 100644
--- a/qiling/loader/loader.py
+++ b/qiling/loader/loader.py
@@ -1,18 +1,23 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
+from __future__ import annotations
+
import os
-from typing import Any, Mapping, MutableSequence, NamedTuple, Optional
+from typing import TYPE_CHECKING, Any, Mapping, MutableSequence, NamedTuple, Optional
+
+if TYPE_CHECKING:
+ from qiling import Qiling
-from qiling import Qiling
class Image(NamedTuple):
base: int
end: int
path: str
+
class QlLoader:
def __init__(self, ql: Qiling):
self.ql = ql
diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py
index 439a6ae13..9234bbafa 100644
--- a/qiling/loader/pe.py
+++ b/qiling/loader/pe.py
@@ -3,13 +3,18 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
-import os, pefile, pickle, secrets, ntpath
-from typing import Any, Dict, MutableMapping, NamedTuple, Optional, Mapping, Sequence, Tuple, Union
+from __future__ import annotations
+
+import os
+import pefile
+import pickle
+import secrets
+import ntpath
+from typing import TYPE_CHECKING, Any, Dict, MutableMapping, NamedTuple, Optional, Mapping, Sequence, Tuple, Union
from unicorn import UcError
from unicorn.x86_const import UC_X86_REG_CR4, UC_X86_REG_CR8
-from qiling import Qiling
from qiling.arch.x86_const import FS_SEGMENT_ADDR, GS_SEGMENT_ADDR
from qiling.const import QL_ARCH, QL_STATE
from qiling.exception import QlErrorArch
@@ -20,6 +25,10 @@
from qiling.os.windows.structs import *
from .loader import QlLoader, Image
+if TYPE_CHECKING:
+ from qiling import Qiling
+
+
class QlPeCacheEntry(NamedTuple):
ba: int
data: bytearray
@@ -859,6 +868,7 @@ def load(self, pe: Optional[pefile.PE]):
self.ql.os.entry_point = self.entry_point
self.init_sp = self.ql.arch.regs.arch_sp
+
class ShowProgress:
"""Display a progress animation while performing a time
consuming task.
diff --git a/qiling/log.py b/qiling/log.py
index d085266ed..7303afeeb 100644
--- a/qiling/log.py
+++ b/qiling/log.py
@@ -3,32 +3,39 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
+from __future__ import annotations
+
import copy
import logging
import os
import re
import weakref
-from typing import Optional, TextIO
+from typing import TYPE_CHECKING, Optional, TextIO
+from logging import Filter, Formatter, LogRecord, Logger, NullHandler, StreamHandler, FileHandler
from qiling.const import QL_VERBOSE
+if TYPE_CHECKING:
+ from qiling import Qiling
+
+
QL_INSTANCE_ID = 114514
FMT_STR = '%(levelname)s\t%(message)s'
+
class COLOR:
- WHITE = '\033[37m'
CRIMSON = '\033[31m'
RED = '\033[91m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
MAGENTA = '\033[95m'
- CYAN = '\033[96m'
- ENDC = '\033[0m'
+ DEFAULT = '\033[39m'
-class QlBaseFormatter(logging.Formatter):
+
+class QlBaseFormatter(Formatter):
__level_tag = {
'WARNING' : '[!]',
'INFO' : '[=]',
@@ -37,9 +44,10 @@ class QlBaseFormatter(logging.Formatter):
'ERROR' : '[x]'
}
- def __init__(self, ql, *args, **kwargs):
+ def __init__(self, ql: Qiling, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.ql = weakref.proxy(ql)
+
+ self.ql: Qiling = weakref.proxy(ql)
def get_level_tag(self, level: str) -> str:
return self.__level_tag[level]
@@ -47,7 +55,7 @@ def get_level_tag(self, level: str) -> str:
def get_thread_tag(self, thread: str) -> str:
return thread
- def format(self, record: logging.LogRecord):
+ def format(self, record: LogRecord):
# In case we have multiple formatters, we have to keep a copy of the record.
record = copy.copy(record)
@@ -64,6 +72,7 @@ def format(self, record: logging.LogRecord):
return super().format(record)
+
class QlColoredFormatter(QlBaseFormatter):
__level_color = {
'WARNING' : COLOR.YELLOW,
@@ -76,22 +85,24 @@ class QlColoredFormatter(QlBaseFormatter):
def get_level_tag(self, level: str) -> str:
s = super().get_level_tag(level)
- return f'{self.__level_color[level]}{s}{COLOR.ENDC}'
+ return f'{self.__level_color[level]}{s}{COLOR.DEFAULT}'
def get_thread_tag(self, tid: str) -> str:
s = super().get_thread_tag(tid)
- return f'{COLOR.GREEN}{s}{COLOR.ENDC}'
+ return f'{COLOR.GREEN}{s}{COLOR.DEFAULT}'
+
-class RegexFilter(logging.Filter):
+class RegexFilter(Filter):
def update_filter(self, regexp: str):
self._filter = re.compile(regexp)
- def filter(self, record: logging.LogRecord):
+ def filter(self, record: LogRecord):
msg = record.getMessage()
return self._filter.match(msg) is not None
+
def resolve_logger_level(verbose: QL_VERBOSE) -> int:
return {
QL_VERBOSE.DISABLED : logging.CRITICAL,
@@ -102,6 +113,7 @@ def resolve_logger_level(verbose: QL_VERBOSE) -> int:
QL_VERBOSE.DUMP : logging.DEBUG
}[verbose]
+
def __is_color_terminal(stream: TextIO) -> bool:
"""Determine whether standard output is attached to a color terminal.
@@ -142,7 +154,8 @@ def __default(_: int) -> bool:
return handler(stream.fileno())
-def setup_logger(ql, log_file: Optional[str], console: bool, log_override: Optional[logging.Logger], log_plain: bool):
+
+def setup_logger(ql: Qiling, log_file: Optional[str], console: bool, log_override: Optional[Logger], log_plain: bool):
global QL_INSTANCE_ID
# If there is an override for our logger, then use it.
@@ -155,15 +168,19 @@ def setup_logger(ql, log_file: Optional[str], console: bool, log_override: Optio
# Disable propagation to avoid duplicate output.
log.propagate = False
+
# Clear all handlers and filters.
- log.handlers = []
- log.filters = []
+ log.handlers.clear()
+ log.filters.clear()
# Do we have console output?
if console:
- handler = logging.StreamHandler()
+ handler = StreamHandler()
- if log_plain or not __is_color_terminal(handler.stream):
+ # adhere to the NO_COLOR convention (see: https://no-color.org/)
+ no_color = os.getenv('NO_COLOR', False)
+
+ if no_color or log_plain or not __is_color_terminal(handler.stream):
formatter = QlBaseFormatter(ql, FMT_STR)
else:
formatter = QlColoredFormatter(ql, FMT_STR)
@@ -171,18 +188,25 @@ def setup_logger(ql, log_file: Optional[str], console: bool, log_override: Optio
handler.setFormatter(formatter)
log.addHandler(handler)
else:
- handler = logging.NullHandler()
+ handler = NullHandler()
log.addHandler(handler)
# Do we have to write log to a file?
if log_file is not None:
- handler = logging.FileHandler(log_file)
+ handler = FileHandler(log_file)
formatter = QlBaseFormatter(ql, FMT_STR)
handler.setFormatter(formatter)
log.addHandler(handler)
log.setLevel(logging.INFO)
+ # optimize logging speed by avoiding the collection of unnecesary logging properties
+ logging._srcfile = None
+ logging.logThreads = False
+ logging.logProcesses = False
+ logging.logMultiprocessing = False
+
return log
+
__all__ = ['RegexFilter', 'setup_logger', 'resolve_logger_level']
diff --git a/qiling/os/filestruct.py b/qiling/os/filestruct.py
index 050d749f7..c57bbc54e 100644
--- a/qiling/os/filestruct.py
+++ b/qiling/os/filestruct.py
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
import os
-from typing import AnyStr
+from typing import AnyStr, Optional
from qiling.exception import *
from qiling.os.posix.stat import *
@@ -14,25 +14,28 @@
except ImportError:
pass
+
class ql_file:
def __init__(self, path: AnyStr, fd: int):
self.__path = path
self.__fd = fd
+ self.__closed = False
+
# information for syscall mmap
self._is_map_shared = False
self._mapped_offset = -1
- self._close_on_exec = 0
+ self.close_on_exec = False
@classmethod
- def open(cls, open_path: AnyStr, open_flags: int, open_mode: int, dir_fd: int = None):
- open_mode &= 0x7fffffff
+ def open(cls, path: AnyStr, flags: int, mode: int, dir_fd: Optional[int] = None):
+ mode &= 0x7fffffff
try:
- fd = os.open(open_path, open_flags, open_mode, dir_fd=dir_fd)
+ fd = os.open(path, flags, mode, dir_fd=dir_fd)
except OSError as e:
raise QlSyscallError(e.errno, e.args[1] + ' : ' + e.filename)
- return cls(open_path, fd)
+ return cls(path, fd)
def read(self, read_len: int) -> bytes:
return os.read(self.__fd, read_len)
@@ -52,6 +55,8 @@ def lseek(self, lseek_offset: int, lseek_origin: int = os.SEEK_SET) -> int:
def close(self) -> None:
os.close(self.__fd)
+ self.__closed = True
+
def fstat(self):
return Fstat(self.__fd)
@@ -88,9 +93,21 @@ def name(self):
return self.__path
@property
- def close_on_exec(self) -> int:
- return self._close_on_exec
+ def closed(self) -> bool:
+ return self.__closed
+
+
+class PersistentQlFile(ql_file):
+ """A persistent variation of the ql_file class, which silently drops
+ attempts to close its udnerlying file. This is useful when using host
+ environment resources, which should not be closed when their wrapping
+ ql_file gets closed.
+
+ For example, stdout and stderr might be closed by the emulated program
+ by calling POSIX dup2 or dup3 system calls, and then replaced by another
+ file or socket. this class prevents the emulated program from closing
+ shared resources on the hosting system.
+ """
- @close_on_exec.setter
- def close_on_exec(self, value: int) -> None:
- self._close_on_exec = value
+ def close(self):
+ pass
diff --git a/qiling/os/freebsd/freebsd.py b/qiling/os/freebsd/freebsd.py
index e0218d4da..5bcb920d0 100644
--- a/qiling/os/freebsd/freebsd.py
+++ b/qiling/os/freebsd/freebsd.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
@@ -10,16 +10,15 @@
from qiling.const import QL_OS
from qiling.os.posix.posix import QlOsPosix
+
class QlOsFreebsd(QlOsPosix):
type = QL_OS.FREEBSD
def __init__(self, ql):
super(QlOsFreebsd, self).__init__(ql)
- self.elf_mem_start = 0x0
self.load()
-
def load(self):
gdtm = GDTManager(self.ql)
@@ -29,16 +28,14 @@ def load(self):
self.ql.hook_insn(self.hook_syscall, UC_X86_INS_SYSCALL)
-
def hook_syscall(self, ql):
return self.load_syscall()
-
def run(self):
if self.ql.exit_point is not None:
self.exit_point = self.ql.exit_point
- if self.ql.entry_point is not None:
+ if self.ql.entry_point is not None:
self.ql.loader.elf_entry = self.ql.entry_point
try:
diff --git a/qiling/os/linux/linux.py b/qiling/os/linux/linux.py
index 959cf5ab5..0313218d6 100644
--- a/qiling/os/linux/linux.py
+++ b/qiling/os/linux/linux.py
@@ -48,7 +48,6 @@ def __init__(self, ql: Qiling):
self.futexm = None
self.fh = None
self.function_after_load_list = []
- self.elf_mem_start = 0x0
self.load()
def load(self):
@@ -118,15 +117,22 @@ def load(self):
# on fork or execve, do not inherit opened files tagged as 'close on exec'
for i in range(len(self.fd)):
- if getattr(self.fd[i], 'close_on_exec', 0):
+ if getattr(self.fd[i], 'close_on_exec', False):
self.fd[i] = None
def setup_procfs(self):
- self.fs_mapper.add_fs_mapping(r'/proc/self/auxv', partial(QlProcFS.self_auxv, self))
- self.fs_mapper.add_fs_mapping(r'/proc/self/cmdline', partial(QlProcFS.self_cmdline, self))
- self.fs_mapper.add_fs_mapping(r'/proc/self/environ', partial(QlProcFS.self_environ, self))
- self.fs_mapper.add_fs_mapping(r'/proc/self/exe', partial(QlProcFS.self_exe, self))
- self.fs_mapper.add_fs_mapping(r'/proc/self/maps', partial(QlProcFS.self_map, self.ql.mem))
+ files = (
+ (r'/proc/self/auxv', lambda: partial(QlProcFS.self_auxv, self)),
+ (r'/proc/self/cmdline', lambda: partial(QlProcFS.self_cmdline, self)),
+ (r'/proc/self/environ', lambda: partial(QlProcFS.self_environ, self)),
+ (r'/proc/self/exe', lambda: partial(QlProcFS.self_exe, self)),
+ (r'/proc/self/maps', lambda: partial(QlProcFS.self_map, self.ql.mem))
+ )
+
+ for filename, wrapper in files:
+ # add mapping only if the user has not already mapped it
+ if not self.fs_mapper.has_mapping(filename):
+ self.fs_mapper.add_mapping(filename, wrapper())
def hook_syscall(self, ql, intno = None):
return self.load_syscall()
@@ -161,12 +167,14 @@ def run(self):
if self.ql.entry_point is not None:
self.ql.loader.elf_entry = self.ql.entry_point
+ # do we have an interp?
elif self.ql.loader.elf_entry != self.ql.loader.entry_point:
entry_address = self.ql.loader.elf_entry
if self.ql.arch.type == QL_ARCH.ARM:
entry_address &= ~1
+ # start running interp, but stop when elf entry point is reached
self.ql.emu_start(self.ql.loader.entry_point, entry_address, self.ql.timeout)
self.ql.do_lib_patch()
self.run_function_after_load()
diff --git a/qiling/os/linux/procfs.py b/qiling/os/linux/procfs.py
index 11bd623f1..dc51e0611 100644
--- a/qiling/os/linux/procfs.py
+++ b/qiling/os/linux/procfs.py
@@ -16,6 +16,10 @@ class FsMappedStream(io.BytesIO):
def __init__(self, fname: str, *args) -> None:
super().__init__(*args)
+ # note that the name property should reflect the actual file name
+ # on the host file system, and here we get a virtual file name
+ # instead. we should be fine, however, since there is no file
+ # backing this object anyway
self.name = fname
diff --git a/qiling/os/mapper.py b/qiling/os/mapper.py
index cc0d840c5..583081097 100644
--- a/qiling/os/mapper.py
+++ b/qiling/os/mapper.py
@@ -4,11 +4,15 @@
#
import os
+from os import PathLike
from typing import Any, Callable, MutableMapping, Union
from .path import QlOsPath
from .filestruct import ql_file
+QlPath = Union['PathLike[str]', str, 'PathLike[bytes]', bytes]
+
+
# All mapped objects should inherit this class.
# Note this object is compatible with ql_file.
# Q: Why not derive from ql_file directly?
@@ -63,115 +67,193 @@ def __init__(self, path: QlOsPath):
self._mapping: MutableMapping[str, Any] = {}
self.path = path
- def _open_mapping_ql_file(self, ql_path: str, openflags: int, openmode: int):
- real_dest = self._mapping[ql_path]
+ def __contains__(self, vpath: str) -> bool:
+ # canonicalize the path first
+ absvpath = self.path.virtual_abspath(vpath)
- if isinstance(real_dest, str):
- obj = ql_file.open(real_dest, openflags, openmode)
+ return absvpath in self._mapping
- elif callable(real_dest):
- obj = real_dest()
+ def has_mapping(self, vpath: str) -> bool:
+ """Check whether a specific virtrual path has a binding.
- else:
- obj = real_dest
+ Args:
+ vpath: virtual path name to check
- return obj
+ Returns: `True` if the specified virtual path has been bound, `False` otherwise.
+ """
- def _open_mapping(self, ql_path: str, openmode: str):
- real_dest = self._mapping[ql_path]
+ return vpath in self
- if isinstance(real_dest, str):
- obj = open(real_dest, openmode)
+ def __len__(self) -> int:
+ return len(self._mapping)
- elif callable(real_dest):
- obj = real_dest()
+ def mapping_count(self) -> int:
+ """Count of currently existing bindings.
+ """
+
+ return len(self)
+ def __open_mapped(self, absvpath: str, opener: Callable, *args) -> Any:
+ """Internal method user for opening an existing mapped object.
+
+ Args:
+ absvpath: absolute virtual path name
+ opener: a method to use to open the target host path
+ *args: arguments to the opener method
+ """
+
+ mapped = self._mapping[absvpath]
+
+ # mapped to a file name on the host file system
+ if isinstance(mapped, str):
+ obj = opener(mapped, *args)
+
+ # mapped to a class or a method
+ elif callable(mapped):
+ obj = mapped()
+
+ # mapped to another kind of object
else:
- obj = real_dest
+ obj = mapped
return obj
- def has_mapping(self, fm: str) -> bool:
- return fm in self._mapping
+ def __open_new(self, absvpath: str, opener: Callable, *args) -> Any:
+ hpath = self.path.virtual_to_host_path(absvpath)
- def mapping_count(self) -> int:
- return len(self._mapping)
+ if not self.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
- def open_ql_file(self, path: str, openflags: int, openmode: int):
- if self.has_mapping(path):
- return self._open_mapping_ql_file(path, openflags, openmode)
+ return opener(hpath, *args)
- host_path = self.path.virtual_to_host_path(path)
+ def open_ql_file(self, vpath: str, flags: int, mode: int):
+ absvpath = self.path.virtual_abspath(vpath)
+ opener = self.__open_mapped if self.has_mapping(absvpath) else self.__open_new
+
+ return opener(absvpath, ql_file.open, flags, mode)
+
+ def open(self, vpath: str, mode: str):
+ absvpath = self.path.virtual_abspath(vpath)
+ opener = self.__open_mapped if self.has_mapping(absvpath) else self.__open_new
+
+ return opener(absvpath, open, mode)
+
+ def file_exists(self, vpath: str) -> bool:
+ """Check whether a file exists on the virtual file system.
+
+ Args:
+ vpath: virtual path name to check
- if not self.path.is_safe_host_path(host_path):
- raise PermissionError(f'unsafe path: {host_path}')
+ Returns: `True` if the specified virtual path has an existing mapping or
+ resolves to an existing file on the virtual file system. `False` otherwise.
+ """
- return ql_file.open(host_path, openflags, openmode)
- def file_exists(self, path:str) -> bool:
- # check if file exists
- if self.has_mapping(path):
+ if self.has_mapping(vpath):
return True
- host_path = self.path.virtual_to_host_path(path)
- if not self.path.is_safe_host_path(host_path):
- raise PermissionError(f'unsafe path: {host_path}')
- return os.path.isfile(host_path)
-
- def create_empty_file(self, path:str)->bool:
- if not self.file_exists(path):
- try:
- f = self.open(path, "w+")
- f.close()
- return True
+ hpath = self.path.virtual_to_host_path(vpath)
+
+ if not self.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
- except Exception as e:
+ return os.path.isfile(hpath)
+
+ def create_empty_file(self, vpath: str) -> bool:
+ if not self.file_exists(vpath):
+ try:
+ f = self.open(vpath, "w+")
+ except OSError:
# for some reason, we could not create an empty file.
return False
+ else:
+ f.close()
+
return True
-
- def open(self, path: str, openmode: str):
- if self.has_mapping(path):
- return self._open_mapping(path, openmode)
- host_path = self.path.virtual_to_host_path(path)
+ def __fspath(self, path: QlPath) -> str:
+ """Similar to os.fspath, this method takes a path-like object and returns
+ its string representation.
+ """
+
+ if isinstance(path, PathLike):
+ path = path.__fspath__()
- if not self.path.is_safe_host_path(host_path):
- raise PermissionError(f'unsafe path: {host_path}')
+ if isinstance(path, str):
+ return path
- return open(host_path, openmode)
+ elif isinstance(path, bytes):
+ return path.decode('utf-8')
- def _parse_path(self, p: Union[os.PathLike, str]) -> str:
- fspath = getattr(p, '__fspath__', None)
+ raise TypeError(path)
- # p is an `os.PathLike` object
- if fspath is not None:
- p = fspath()
+ def add_mapping(self, vpath: QlPath, binding: Union[QlPath, QlFsMappedObject, Callable], *, force: bool = False) -> None:
+ """Create a new mapping in the virtual filesystem.
- if isinstance(p, bytes): # os.PathLike.__fspath__ may return bytes.
- p = p.decode("utf-8")
+ Args:
+ vpath: a virtual path to bind
- return p
+ binding: a target to use whenever the bound virtual path is referenced. such a target can be
+ either a path on the host filesystem, an object instance or a class. the behavior of the mapping
+ is determined by the bound object type:
+ [*] a string: bind a path on the host filesystem (e.g. "/dev/urandom"). use with caution!
+ [*] an object: bind an object instance which will be returned each time the virtual path is opened
+ [*] a class: bind a class that will be instantiated each time the virtual path is opened
- def add_fs_mapping(self, ql_path: Union[os.PathLike, str], real_dest: Union[str, QlFsMappedObject, Callable]) -> None:
- """Map an object to Qiling emulated file system.
+ force: when set to `True`, re-mapping an existing vpath becomes possible. In such case, the
+ old mapping will be discarded
- Args:
- ql_path: Emulated path which should be convertable to a string or a hashable object. e.g. pathlib.Path
- real_dest: Mapped object, can be a string, an object or a callable(class).
- string: mapped path in the host machine, e.g. '/dev/urandom' -> '/dev/urandom'
- object: mapped object, will be returned each time the emulated path has been opened
- class: mapped callable, will be used to create a new instance each time the emulated path has been opened
+ Raises:
+ `KeyError`: in case the specified vpath has already been mapped (default behavior).
"""
- ql_path = self._parse_path(ql_path)
- real_dest = self._parse_path(real_dest)
+ vpath = self.__fspath(vpath)
+ absvpath = self.path.virtual_abspath(vpath)
+
+ if self.has_mapping(absvpath) and not force:
+ raise KeyError(f'mapping already exists: "{absvpath}"')
- self._mapping[ql_path] = real_dest
+ if isinstance(binding, (str, bytes, PathLike)):
+ binding = self.__fspath(binding)
- def remove_fs_mapping(self, ql_path: Union[os.PathLike, str]):
+ self._mapping[absvpath] = binding
+
+ def remove_mapping(self, vpath: QlPath) -> None:
"""Remove a mapping from the fs mapper.
Args:
- ql_path (Union[os.PathLike, str]): The mapped path.
+ vpath: bound virtual path to remove
+
+ Raises:
+ `KeyError`: in case the specified vpath has no mapping
"""
- del self._mapping[self._parse_path(ql_path)]
+
+ vpath = self.__fspath(vpath)
+ absvpath = self.path.virtual_abspath(vpath)
+
+ if not self.has_mapping(absvpath):
+ raise KeyError(absvpath)
+
+ del self._mapping[absvpath]
+
+ def rename_mapping(self, old_vpath: str, new_vpath: str) -> None:
+ old_absvpath = self.path.virtual_abspath(old_vpath)
+
+ # vpath to rename does not exist
+ if not self.has_mapping(old_absvpath):
+ raise KeyError(old_vpath)
+
+ new_absvpath = self.path.virtual_abspath(new_vpath)
+
+ # new vpath already exists
+ if self.has_mapping(new_absvpath):
+ raise KeyError(new_vpath)
+
+ # avoid renaming to the same vapth
+ if old_absvpath == new_absvpath:
+ return
+
+ binding = self._mapping[old_absvpath]
+
+ # remove old mapping and add a new one instead
+ self._mapping[new_absvpath] = binding
+ del self._mapping[old_absvpath]
diff --git a/qiling/os/os.py b/qiling/os/os.py
index 006d094f9..7982fe946 100644
--- a/qiling/os/os.py
+++ b/qiling/os/os.py
@@ -4,7 +4,8 @@
#
import sys
-from typing import Any, Hashable, Iterable, Optional, Callable, Mapping, Sequence, TextIO, Tuple
+from io import UnsupportedOperation
+from typing import Any, Dict, Iterable, Optional, Callable, Mapping, Sequence, TextIO, Tuple, Union
from unicorn import UcError
@@ -13,7 +14,7 @@
from qiling.os.const import STRING, WSTRING, GUID
from qiling.os.fcall import QlFunctionCall, TypedArg
-from .filestruct import ql_file
+from .filestruct import PersistentQlFile
from .mapper import QlFsMapper
from .stats import QlOsStats
from .utils import QlOsUtils
@@ -47,23 +48,31 @@ def __init__(self, ql: Qiling, resolvers: Mapping[Any, Resolver] = {}):
self.path = QlOsPath(ql.rootfs, cwd, self.type)
self.fs_mapper = QlFsMapper(self.path)
- self.user_defined_api = {
- QL_INTERCEPT.CALL : {},
+ self.user_defined_api: Dict[QL_INTERCEPT, Dict[Union[int, str], Callable]] = {
+ QL_INTERCEPT.CALL: {},
QL_INTERCEPT.ENTER: {},
- QL_INTERCEPT.EXIT : {}
+ QL_INTERCEPT.EXIT: {}
}
- # IDAPython has some hack on standard io streams and thus they don't have corresponding fds.
try:
- import ida_idaapi
- except ImportError:
- self._stdin = ql_file('stdin', sys.stdin.fileno())
- self._stdout = ql_file('stdout', sys.stdout.fileno())
- self._stderr = ql_file('stderr', sys.stderr.fileno())
- else:
+ # Qiling may be used on interactive shells (ex: IDLE) or embedded python
+ # interpreters (ex: IDA Python). such environments use their own version
+ # for the standard streams which usually do not support certain operations,
+ # such as fileno(). here we use this to determine how we are going to use
+ # the environment standard streams
+ sys.stdin.fileno()
+ except UnsupportedOperation:
+ # Qiling is used on an interactive shell or embedded python interpreter.
+ # if the internal stream buffer is accessible, we should use it
self._stdin = getattr(sys.stdin, 'buffer', sys.stdin)
self._stdout = getattr(sys.stdout, 'buffer', sys.stdout)
self._stderr = getattr(sys.stderr, 'buffer', sys.stderr)
+ else:
+ # Qiling is used in a script, or on an environment that supports ordinary
+ # stanard streams
+ self._stdin = PersistentQlFile('stdin', sys.stdin.fileno())
+ self._stdout = PersistentQlFile('stdout', sys.stdout.fileno())
+ self._stderr = PersistentQlFile('stderr', sys.stderr.fileno())
# defult exit point
self.exit_point = {
@@ -207,7 +216,7 @@ def call(self, pc: int, func: Callable, proto: Mapping[str, Any], onenter: Optio
return retval
- def set_api(self, target: Hashable, handler: Callable, intercept: QL_INTERCEPT = QL_INTERCEPT.CALL):
+ def set_api(self, target: Union[int, str], handler: Callable, intercept: QL_INTERCEPT = QL_INTERCEPT.CALL):
"""Either hook or replace an OS API with a custom one.
Args:
diff --git a/qiling/os/path.py b/qiling/os/path.py
index 61c5b8db1..61b3c8cd5 100644
--- a/qiling/os/path.py
+++ b/qiling/os/path.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
@@ -10,6 +10,7 @@
AnyPurePath = Union[PurePosixPath, PureWindowsPath]
+
class QlOsPath:
"""Virtual to host path manipulations helper.
"""
@@ -60,6 +61,10 @@ def __strip_parent_refs(path: AnyPurePath) -> AnyPurePath:
return path
+ @property
+ def root(self) -> str:
+ return str(self._cwd_anchor)
+
@property
def cwd(self) -> str:
return str(self._cwd_anchor / self._cwd_vpath)
@@ -274,6 +279,18 @@ def host_to_virtual_path(self, hostpath: str) -> str:
return str(virtpath)
+ def is_virtual_abspath(self, virtpath: str) -> bool:
+ """Determine whether a given virtual path is absolute.
+
+ Args:
+ virtpath : virtual path to query
+
+ Returns: `True` if `virtpath` is an absolute path, `False` if relative
+ """
+
+ vpath = self.PureVirtualPath(virtpath)
+
+ return vpath.is_absolute()
def virtual_abspath(self, virtpath: str) -> str:
"""Convert a relative virtual path to an absolute virtual path based
@@ -346,4 +363,3 @@ def host_casefold_path(self, hostpath: str) -> Optional[str]:
return QlOsPath.__host_casefold_path(hostpath)
return hostpath
-
diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py
index 632a95ffa..103559f02 100644
--- a/qiling/os/posix/const.py
+++ b/qiling/os/posix/const.py
@@ -1003,11 +1003,38 @@ class qnx_mmap_flags(Flag):
}
# shm syscall
-IPC_CREAT = 8**3
-IPC_EXCL = 2*(8**3)
-IPC_NOWAIT = 4*(8**3)
-
-SHM_RDONLY = 8**4
-SHM_RND = 2*(8**4)
-SHM_REMAP= 4*(8**4)
-SHM_EXEC = 1*(8**5)
+IPC_PRIVATE = 0
+
+# see: https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/ipc.h
+IPC_CREAT = 0o0001000 # create if key is nonexistent
+IPC_EXCL = 0o0002000 # fail if key exists
+IPC_NOWAIT = 0o0004000 # return error on wait
+
+# see: https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/linux/shm.h
+SHM_W = 0o000200
+SHM_R = 0o000400
+SHM_HUGETLB = 0o004000 # segment will use huge TLB pages
+SHM_RDONLY = 0o010000 # read-only access
+SHM_RND = 0o020000 # round attach address to SHMLBA boundary
+SHM_REMAP = 0o040000 # take-over region on attach
+SHM_EXEC = 0o100000 # execution access
+
+SHMMNI = 4096 # max num of segs system wide
+
+# see: https://elixir.bootlin.com/linux/v5.19.17/source/include/uapi/asm-generic/hugetlb_encode.h
+HUGETLB_FLAG_ENCODE_SHIFT = 26
+HUGETLB_FLAG_ENCODE_MASK = 0x3f
+
+# ipc syscall
+SEMOP = 1
+SEMGET = 2
+SEMCTL = 3
+SEMTIMEDOP = 4
+MSGSND = 11
+MSGRCV = 12
+MSGGET = 13
+MSGCTL = 14
+SHMAT = 21
+SHMDT = 22
+SHMGET = 23
+SHMCTL = 24
diff --git a/qiling/os/posix/const_mapping.py b/qiling/os/posix/const_mapping.py
index 2fcd2d3c2..9b32b8c4a 100644
--- a/qiling/os/posix/const_mapping.py
+++ b/qiling/os/posix/const_mapping.py
@@ -156,12 +156,13 @@ def socket_domain_mapping(p: int, archtype: QL_ARCH, ostype: QL_OS) -> str:
def socket_tcp_option_mapping(t: int, archtype: QL_ARCH) -> str:
socket_option_map = {
- QL_ARCH.X86: linux_socket_tcp_options,
+ QL_ARCH.X86: linux_socket_tcp_options,
QL_ARCH.X8664: linux_socket_tcp_options,
- QL_ARCH.ARM: linux_socket_tcp_options,
+ QL_ARCH.ARM: linux_socket_tcp_options,
QL_ARCH.ARM64: linux_socket_tcp_options,
- QL_ARCH.MIPS: linux_socket_tcp_options,
+ QL_ARCH.MIPS: linux_socket_tcp_options,
}[archtype]
+
return _constant_mapping(t, socket_option_map)
diff --git a/qiling/os/posix/posix.py b/qiling/os/posix/posix.py
index a4f88f052..e7e609b98 100644
--- a/qiling/os/posix/posix.py
+++ b/qiling/os/posix/posix.py
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
from inspect import signature, Parameter
-from typing import TextIO, Union, Callable, IO, List, Optional
+from typing import Dict, TextIO, Tuple, Union, Callable, IO, List, Optional
from unicorn.arm64_const import UC_ARM64_REG_X8, UC_ARM64_REG_X16
from unicorn.arm_const import (
@@ -72,7 +72,7 @@ def __init__(self):
def __len__(self):
return len(self.__fds)
- def __getitem__(self, idx: Union[slice, int]):
+ def __getitem__(self, idx: int):
return self.__fds[idx]
def __setitem__(self, idx: int, val: Optional[IO]):
@@ -91,6 +91,51 @@ def restore(self, fds):
self.__fds = fds
+# vaguely reflects a shmid64_ds structure
+class QlShmId:
+
+ def __init__(self, key: int, uid: int, gid: int, mode: int, segsz: int) -> None:
+ # ipc64_perm
+ self.key = key
+ self.uid = uid
+ self.gid = gid
+ self.mode = mode
+
+ self.segsz = segsz
+
+ # track the memory locations this segment is currently attached to
+ self.attach: List[int] = []
+
+
+class QlShm:
+ def __init__(self) -> None:
+ self.__shm: Dict[int, QlShmId] = {}
+ self.__id: int = 0x0F000000
+
+ def __len__(self) -> int:
+ return len(self.__shm)
+
+ def add(self, shm: QlShmId) -> int:
+ shmid = self.__id
+ self.__shm[shmid] = shm
+
+ self.__id += 0x1000
+
+ return shmid
+
+ def remove(self, shmid: int) -> None:
+ del self.__shm[shmid]
+
+ def get_by_key(self, key: int) -> Tuple[int, Optional[QlShmId]]:
+ return next(((shmid, shmobj) for shmid, shmobj in self.__shm.items() if shmobj.key == key), (-1, None))
+
+ def get_by_id(self, shmid: int) -> Optional[QlShmId]:
+ return self.__shm.get(shmid, None)
+
+ def get_by_attaddr(self, shmaddr: int) -> Optional[QlShmId]:
+ return next((shmobj for shmobj in self.__shm.values() if shmobj.attach.count(shmaddr) > 0), None)
+
+
class QlOsPosix(QlOs):
def __init__(self, ql: Qiling):
@@ -110,9 +155,9 @@ def __init__(self, ql: Qiling):
self.ifrname_ovr = conf.get('ifrname_override')
self.posix_syscall_hooks = {
- QL_INTERCEPT.CALL : {},
+ QL_INTERCEPT.CALL: {},
QL_INTERCEPT.ENTER: {},
- QL_INTERCEPT.EXIT : {}
+ QL_INTERCEPT.EXIT: {}
}
self.__syscall_id_reg = {
@@ -157,7 +202,7 @@ def __init__(self, ql: Qiling):
self.stdout = self._stdout
self.stderr = self._stderr
- self._shms = {}
+ self._shm = QlShm()
def __get_syscall_mapper(self, archtype: QL_ARCH):
qlos_path = f'.os.{self.type.name.lower()}.map_syscall'
@@ -191,7 +236,7 @@ def root(self, enabled: bool) -> None:
self.euid = 0 if enabled else self.uid
self.egid = 0 if enabled else self.gid
- def set_syscall(self, target: Union[int, str], handler: Callable, intercept: QL_INTERCEPT=QL_INTERCEPT.CALL):
+ def set_syscall(self, target: Union[int, str], handler: Callable, intercept: QL_INTERCEPT = QL_INTERCEPT.CALL):
"""Either hook or replace a system call with a custom one.
Args:
@@ -298,7 +343,7 @@ def __get_os_module(osname: str):
raise e
# print out log entry
- syscall_basename = syscall_name[len(SYSCALL_PREF) if syscall_name.startswith(SYSCALL_PREF) else 0:]
+ syscall_basename = syscall_name[len(SYSCALL_PREF) if syscall_name.startswith(SYSCALL_PREF) else 0:]
args = []
@@ -349,3 +394,7 @@ def set_syscall_return(self, retval: int):
@property
def fd(self):
return self._fd
+
+ @property
+ def shm(self):
+ return self._shm
\ No newline at end of file
diff --git a/qiling/os/posix/syscall/__init__.py b/qiling/os/posix/syscall/__init__.py
index 4e87d9b65..a8c25d18f 100644
--- a/qiling/os/posix/syscall/__init__.py
+++ b/qiling/os/posix/syscall/__init__.py
@@ -16,10 +16,12 @@
from .sched import *
from .select import *
from .sendfile import *
+from .shm import *
from .signal import *
from .socket import *
from .stat import *
from .sysctl import *
+from .syscall import *
from .sysinfo import *
from .time import *
from .types import *
diff --git a/qiling/os/posix/syscall/fcntl.py b/qiling/os/posix/syscall/fcntl.py
index bca208863..5b1f2a9c3 100644
--- a/qiling/os/posix/syscall/fcntl.py
+++ b/qiling/os/posix/syscall/fcntl.py
@@ -4,7 +4,6 @@
#
import os
-from pathlib import Path
from qiling import Qiling
from qiling.const import QL_OS, QL_ARCH
@@ -13,119 +12,96 @@
from qiling.os.posix.const_mapping import ql_open_flag_mapping
from qiling.os.posix.filestruct import ql_socket
-def ql_syscall_open(ql: Qiling, filename: int, flags: int, mode: int):
- path = ql.os.utils.read_cstring(filename)
- real_path = ql.os.path.transform_to_real_path(path)
- relative_path = ql.os.path.transform_to_relative_path(path)
+from .unistd import virtual_abspath_at, get_opened_fd
+
+def __do_open(ql: Qiling, absvpath: str, flags: int, mode: int) -> int:
flags &= 0xffffffff
mode &= 0xffffffff
+ # look for the next available fd slot
idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] is None), -1)
if idx == -1:
- regreturn = -EMFILE
- else:
- try:
- if ql.arch.type == QL_ARCH.ARM and ql.os.type != QL_OS.QNX:
- mode = 0
+ return -EMFILE
- flags = ql_open_flag_mapping(ql, flags)
- ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(path, flags, mode)
- regreturn = idx
- except QlSyscallError as e:
- regreturn = - e.errno
+ if ql.arch.type is QL_ARCH.ARM and ql.os.type is not QL_OS.QNX:
+ mode = 0
+ # translate emulated os open flags into host os open flags
+ flags = ql_open_flag_mapping(ql, flags)
- ql.log.debug("open(%s, 0o%o) = %d" % (relative_path, mode, regreturn))
+ try:
+ ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(absvpath, flags, mode)
+ except QlSyscallError:
+ return -1
- if regreturn >= 0 and regreturn != 2:
- ql.log.debug(f'File found: {real_path:s}')
- else:
- ql.log.debug(f'File not found {real_path:s}')
+ return idx
- return regreturn
-def ql_syscall_creat(ql: Qiling, filename: int, mode: int):
- flags = posix_open_flags["O_WRONLY"] | posix_open_flags["O_CREAT"] | posix_open_flags["O_TRUNC"]
+def ql_syscall_open(ql: Qiling, filename: int, flags: int, mode: int):
+ vpath = ql.os.utils.read_cstring(filename)
+ absvpath = ql.os.path.virtual_abspath(vpath)
- path = ql.os.utils.read_cstring(filename)
- real_path = ql.os.path.transform_to_real_path(path)
- relative_path = ql.os.path.transform_to_relative_path(path)
+ regreturn = __do_open(ql, absvpath, flags, mode)
- flags &= 0xffffffff
- mode &= 0xffffffff
+ ql.log.debug(f'open("{absvpath}", {flags:#x}, 0{mode:o}) = {regreturn}')
- idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] is None), -1)
+ return regreturn
- if idx == -1:
- regreturn = -ENOMEM
- else:
- try:
- if ql.arch.type == QL_ARCH.ARM:
- mode = 0
- flags = ql_open_flag_mapping(ql, flags)
- ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(path, flags, mode)
- regreturn = idx
- except QlSyscallError as e:
- regreturn = -e.errno
+def ql_syscall_openat(ql: Qiling, fd: int, path: int, flags: int, mode: int):
+ vpath = ql.os.utils.read_cstring(path)
+ absvpath = virtual_abspath_at(ql, vpath, fd)
- ql.log.debug("creat(%s, 0o%o) = %d" % (relative_path, mode, regreturn))
+ regreturn = -1 if absvpath is None else __do_open(ql, absvpath, flags, mode)
- if regreturn >= 0 and regreturn != 2:
- ql.log.debug(f'File found: {real_path:s}')
- else:
- ql.log.debug(f'File not found {real_path:s}')
+ ql.log.debug(f'openat({fd:d}, "{vpath}", {flags:#x}, 0{mode:o}) = {regreturn:d}')
return regreturn
-def ql_syscall_openat(ql: Qiling, fd: int, path: int, flags: int, mode: int):
- file_path = ql.os.utils.read_cstring(path)
- # real_path = ql.os.path.transform_to_real_path(path)
- # relative_path = ql.os.path.transform_to_relative_path(path)
- flags &= 0xffffffff
+def ql_syscall_creat(ql: Qiling, filename: int, mode: int):
+ vpath = ql.os.utils.read_cstring(filename)
+
+ # FIXME: this is broken
+ flags = posix_open_flags["O_WRONLY"] | posix_open_flags["O_CREAT"] | posix_open_flags["O_TRUNC"]
mode &= 0xffffffff
idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] is None), -1)
if idx == -1:
- regreturn = -EMFILE
+ regreturn = -ENOMEM
else:
- try:
- if ql.arch.type == QL_ARCH.ARM:
- mode = 0
+ if ql.arch.type == QL_ARCH.ARM:
+ mode = 0
+ try:
flags = ql_open_flag_mapping(ql, flags)
- fd = ql.unpacks(ql.pack(fd))
+ ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(vpath, flags, mode)
+ except QlSyscallError as e:
+ regreturn = -e.errno
+ else:
+ regreturn = idx
- if 0 <= fd < NR_OPEN:
- fobj = ql.os.fd[fd]
- # ql_file object or QlFsMappedObject
- if hasattr(fobj, "fileno") and hasattr(fobj, "name"):
- if not Path.is_absolute(Path(file_path)):
- file_path = Path(fobj.name) / Path(file_path)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
+ absvpath = ql.os.path.virtual_abspath(vpath)
- ql.os.fd[idx] = ql.os.fs_mapper.open_ql_file(file_path, flags, mode)
+ ql.log.debug(f'creat("{absvpath}", {mode:#o}) = {regreturn}')
- regreturn = idx
- except QlSyscallError as e:
- regreturn = -e.errno
-
- ql.log.debug(f'openat(fd = {fd:d}, path = {file_path}, mode = {mode:#o}) = {regreturn:d}')
+ if regreturn >= 0 and regreturn != 2:
+ ql.log.debug(f'File found: {hpath:s}')
+ else:
+ ql.log.debug(f'File not found {hpath:s}')
return regreturn
def ql_syscall_fcntl(ql: Qiling, fd: int, cmd: int, arg: int):
- if fd not in range(NR_OPEN):
- return -EBADF
-
- f = ql.os.fd[fd]
+ f = get_opened_fd(ql.os, fd)
if f is None:
- return -EBADF
+ return -1
if cmd == F_DUPFD:
if arg not in range(NR_OPEN):
@@ -140,10 +116,10 @@ def ql_syscall_fcntl(ql: Qiling, fd: int, cmd: int, arg: int):
regreturn = -EMFILE
elif cmd == F_GETFD:
- regreturn = getattr(f, "close_on_exec", 0)
+ regreturn = int(getattr(f, "close_on_exec", False))
elif cmd == F_SETFD:
- f.close_on_exec = 1 if arg & FD_CLOEXEC else 0
+ f.close_on_exec = bool(arg & FD_CLOEXEC)
regreturn = 0
elif cmd == F_GETFL:
@@ -209,28 +185,36 @@ def ql_syscall_flock(ql: Qiling, fd: int, operation: int):
def ql_syscall_rename(ql: Qiling, oldname_buf: int, newname_buf: int):
- """
- rename(const char *oldpath, const char *newpath)
- description: change the name or location of a file
- ret value: On success, zero is returned. On error, -1 is returned
- """
- regreturn = 0 # default value is success
- oldpath = ql.os.utils.read_cstring(oldname_buf)
- newpath = ql.os.utils.read_cstring(newname_buf)
+ old_vpath = ql.os.utils.read_cstring(oldname_buf)
+ new_vpath = ql.os.utils.read_cstring(newname_buf)
- ql.log.debug(f"rename() path: {oldpath} -> {newpath}")
+ old_absvpath = ql.os.path.virtual_abspath(old_vpath)
- old_realpath = ql.os.path.transform_to_real_path(oldpath)
- new_realpath = ql.os.path.transform_to_real_path(newpath)
+ # if has a mapping, rename the mapped vpath
+ if ql.os.fs_mapper.has_mapping(old_absvpath):
+ try:
+ ql.os.fs_mapper.rename_mapping(old_vpath, new_vpath)
+ except KeyError:
+ regreturn = -1
+ else:
+ regreturn = 0
- if old_realpath == new_realpath:
- # do nothing, just return success
- return regreturn
+ # otherwise, rename the actual files
+ else:
+ old_hpath = ql.os.path.virtual_to_host_path(old_vpath)
+ new_hpath = ql.os.path.virtual_to_host_path(new_vpath)
- try:
- os.rename(old_realpath, new_realpath)
- except OSError:
- ql.log.exception(f"rename(): {newpath} exists!")
- regreturn = -1
+ # if source and target paths are identical, do nothing
+ if old_hpath == new_hpath:
+ return 0
- return regreturn
\ No newline at end of file
+ try:
+ os.rename(old_hpath, new_hpath)
+ except OSError:
+ regreturn = -1
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'rename("{old_vpath}", "{new_vpath}") = {regreturn}')
+
+ return regreturn
diff --git a/qiling/os/posix/syscall/mman.py b/qiling/os/posix/syscall/mman.py
index 6d80a74e1..4990b3c51 100755
--- a/qiling/os/posix/syscall/mman.py
+++ b/qiling/os/posix/syscall/mman.py
@@ -225,33 +225,3 @@ def ql_syscall_mmap2(ql: Qiling, addr: int, length: int, prot: int, flags: int,
pgoffset *= ql.mem.pagesize
return syscall_mmap_impl(ql, addr, length, prot, flags, fd, pgoffset, 2)
-
-
-def ql_syscall_shmget(ql: Qiling, key: int, size: int, shmflg: int):
- if shmflg & IPC_CREAT:
- if shmflg & IPC_EXCL:
- if key in ql.os._shms:
- return EEXIST
- else:
- #addr = ql.mem.map_anywhere(size)
- ql.os._shms[key] = (key, size)
- return key
- else:
- if key not in ql.os._shms:
- return ENOENT
-
-
-def ql_syscall_shmat(ql: Qiling, shmid: int, shmaddr: int, shmflg: int):
- # shmid == key
- # dummy implementation
- if shmid not in ql.os._shms:
- return EINVAL
-
- key, size = ql.os._shms[shmid]
-
- if shmaddr == 0:
- addr = ql.mem.map_anywhere(size)
- else:
- addr = ql.mem.map(shmaddr, size, info="[shm]")
-
- return addr
diff --git a/qiling/os/posix/syscall/shm.py b/qiling/os/posix/syscall/shm.py
new file mode 100644
index 000000000..7bd8228ac
--- /dev/null
+++ b/qiling/os/posix/syscall/shm.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+#
+# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
+#
+
+from unicorn.unicorn_const import UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC
+
+from qiling import Qiling
+from qiling.const import QL_ARCH
+from qiling.exception import QlMemoryMappedError
+from qiling.os.posix.const import *
+from qiling.os.posix.posix import QlShmId
+
+
+def ql_syscall_shmget(ql: Qiling, key: int, size: int, shmflg: int):
+
+ def __create_shm(key: int, size: int, flags: int) -> int:
+ """Create a new shared memory segment for the specified key.
+
+ Returns: shmid of the newly created segment, -1 if an error has occured
+ """
+
+ if len(ql.os.shm) >= SHMMNI:
+ return -1 # ENOSPC
+
+ mode = flags & ((1 << 9) - 1)
+
+ # determine size alignment: either normal or huge page
+ if flags & SHM_HUGETLB:
+ shiftsize = (flags >> HUGETLB_FLAG_ENCODE_SHIFT) & HUGETLB_FLAG_ENCODE_MASK
+ alignment = (1 << shiftsize)
+ else:
+ alignment = ql.mem.pagesize
+
+ shm_size = ql.mem.align_up(size, alignment)
+
+ shmid = ql.os.shm.add(QlShmId(key, ql.os.uid, ql.os.gid, mode, shm_size))
+
+ ql.log.debug(f'created a new shm: key = {key:#x}, mode = 0{mode:o}, size = {shm_size:#x}. assigned id: {shmid:#x}')
+
+ return shmid
+
+ # create new shared memory segment
+ if key == IPC_PRIVATE:
+ shmid = __create_shm(key, size, shmflg)
+
+ else:
+ shmid, shm = ql.os.shm.get_by_key(key)
+
+ # a shm with the specified key does not exist
+ if shm is None:
+ # the user asked to create a new one?
+ if shmflg & IPC_CREAT:
+ shmid = __create_shm(key, size, shmflg)
+
+ else:
+ return -1 # ENOENT
+
+ # a shm with the specified key exists
+ else:
+ # the user asked to create a new one?
+ if shmflg & (IPC_CREAT | IPC_EXCL):
+ return -1 # EEXIST
+
+ # check whether the user has permissions to access this shm
+ # FIXME: should probably use ql.os.cuid instead, but we don't support it yet
+ if (ql.os.uid == shm.uid) and (shm.mode & (SHM_W | SHM_R)):
+ return shmid
+
+ else:
+ return -1 # EACCES
+
+ return shmid
+
+
+def ql_syscall_shmat(ql: Qiling, shmid: int, shmaddr: int, shmflg: int):
+ shm = ql.os.shm.get_by_id(shmid)
+
+ # a shm with the specified key does not exist
+ if shm is None:
+ return -1 # EINVAL
+
+ if shmaddr == 0:
+ # system may choose any suitable page-aligned address
+ attaddr = ql.mem.find_free_space(shm.segsz, ql.loader.mmap_address)
+
+ elif shmflg & SHM_RND:
+ # select the appropriate SHMLBA value, based on the platform
+ shmlba = {
+ QL_ARCH.MIPS: 0x40000,
+ QL_ARCH.ARM: ql.mem.pagesize * 4,
+ QL_ARCH.ARM64: ql.mem.pagesize * 4,
+ QL_ARCH.X86: ql.mem.pagesize,
+ QL_ARCH.X8664: ql.mem.pagesize
+ }
+
+ # align the address specified by shmaddr to platform SHMLBA
+ attaddr = ql.mem.align(shmaddr, shmlba[ql.arch.type])
+
+ else:
+ # shmaddr is expected to be aligned
+ if shmaddr & (ql.mem.pagesize - 1):
+ return -1 # EINVAL
+
+ attaddr = shmaddr
+
+ perms = UC_PROT_READ
+
+ if shmflg & SHM_RDONLY == 0:
+ perms |= UC_PROT_WRITE
+
+ if shmflg & SHM_EXEC:
+ perms |= UC_PROT_EXEC
+
+ # user asked to attached the seg as readable; is it allowed?
+ if (perms & UC_PROT_READ) and not (shm.mode & SHM_R):
+ return -1 # EACCES
+
+ # user asked to attached the seg as writable; is it allowed?
+ if (perms & UC_PROT_WRITE) and not (shm.mode & SHM_W):
+ return -1 # EACCES
+
+ # TODO: if segment is already attached, there is no need to map another memory for it.
+ # if we do, data changes will not be reflected between the segment attachments. we could
+ # use a mmio map for additional attachments, and have writes and reads directed to the
+ # first attachment mapping
+
+ try:
+ # attach the segment at shmaddr
+ ql.mem.map(attaddr, shm.segsz, perms, '[shm]')
+ except QlMemoryMappedError:
+ return -1 # EINVAL
+
+ # track attachment
+ shm.attach.append(attaddr)
+
+ ql.log.debug(f'shm {shmid:#x} attached at {attaddr:#010x}')
+
+ return attaddr
+
+
+def ql_syscall_shmdt(ql: Qiling, shmaddr: int):
+ shm = ql.os.shm.get_by_attaddr(shmaddr)
+
+ if shm is None:
+ return -1 # EINVAL
+
+ shm.attach.remove(shmaddr)
+
+ return 0
+
+
+__all__ = [
+ 'ql_syscall_shmget',
+ 'ql_syscall_shmdt',
+ 'ql_syscall_shmat'
+]
diff --git a/qiling/os/posix/syscall/socket.py b/qiling/os/posix/syscall/socket.py
index 4fd56dafb..d02b8cfec 100644
--- a/qiling/os/posix/syscall/socket.py
+++ b/qiling/os/posix/syscall/socket.py
@@ -27,10 +27,10 @@ def inet_aton(ipaddr: str) -> int:
return int.from_bytes(ipdata, byteorder='big')
-def inet6_aton(ipaddr: str) -> int:
- ipdata = ipaddress.IPv6Address(ipaddr).packed
+def inet6_aton(ipaddr: str) -> Tuple[int, ...]:
+ abytes = ipaddress.IPv6Address(ipaddr).packed
- return int.from_bytes(ipdata, byteorder='big')
+ return tuple(abytes)
def inet_htoa(ql: Qiling, addr: int) -> str:
@@ -52,6 +52,10 @@ def inet6_htoa(ql: Qiling, addr: bytes) -> str:
def inet6_ntoa(addr: bytes) -> str:
+ # if addr arg is not strictly a bytes object, convert it to bytes
+ if not isinstance(addr, bytes):
+ addr = bytes(addr)
+
return ipaddress.IPv6Address(addr).compressed
@@ -260,13 +264,13 @@ def ql_syscall_connect(ql: Qiling, sockfd: int, addr: int, addrlen: int):
dest = (host, port)
elif sock.family == AF_INET6 and ql.os.ipv6:
- sockaddr_in6 = make_sockaddr_in(abits, endian)
+ sockaddr_in6 = make_sockaddr_in6(abits, endian)
sockaddr_obj = sockaddr_in6.from_buffer(data)
- port = ntohs(ql, sockaddr_obj.sin_port)
+ port = ntohs(ql, sockaddr_obj.sin6_port)
host = inet6_htoa(ql, sockaddr_obj.sin6_addr.s6_addr)
- ql.log.debug(f'Conecting to {host}:{port}')
+ ql.log.debug(f'Connecting to {host}:{port}')
dest = (host, port)
if dest is not None:
@@ -409,10 +413,10 @@ def ql_syscall_bind(ql: Qiling, sockfd: int, addr: int, addrlen: int):
dest = (host, port)
elif sa_family == AF_INET6 and ql.os.ipv6:
- sockaddr_in6 = make_sockaddr_in(abits, endian)
+ sockaddr_in6 = make_sockaddr_in6(abits, endian)
sockaddr_obj = sockaddr_in6.from_buffer(data)
- port = ntohs(ql, sockaddr_obj.sin_port)
+ port = ntohs(ql, sockaddr_obj.sin6_port)
host = inet6_ntoa(sockaddr_obj.sin6_addr.s6_addr)
if ql.os.bindtolocalhost:
@@ -879,10 +883,10 @@ def ql_syscall_sendto(ql: Qiling, sockfd: int, buf: int, length: int, flags: int
dest = (host, port)
elif sa_family == AF_INET6 and ql.os.ipv6:
- sockaddr_in6 = make_sockaddr_in(abits, endian)
+ sockaddr_in6 = make_sockaddr_in6(abits, endian)
sockaddr_obj = sockaddr_in6.from_buffer(data)
- port = ntohs(ql, sockaddr_obj.sin_port)
+ port = ntohs(ql, sockaddr_obj.sin6_port)
host = inet6_ntoa(sockaddr_obj.sin6_addr.s6_addr)
ql.log.debug(f'Sending to {host}:{port}')
diff --git a/qiling/os/posix/syscall/stat.py b/qiling/os/posix/syscall/stat.py
index 0a6da9e4f..d4e1be375 100644
--- a/qiling/os/posix/syscall/stat.py
+++ b/qiling/os/posix/syscall/stat.py
@@ -1225,27 +1225,45 @@ def transform_path(ql: Qiling, dirfd: int, path: int, flags: int = 0):
def ql_syscall_chmod(ql: Qiling, filename: int, mode: int):
- file_path = ql.os.utils.read_cstring(filename)
- real_path = ql.os.path.transform_to_real_path(file_path)
+ vpath = ql.os.utils.read_cstring(filename)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
+
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
+
try:
- os.chmod(real_path, mode)
- regreturn = 0
- except:
+ os.chmod(hpath, mode)
+ except OSError:
regreturn = -1
- ql.log.debug(f'chmod("{ql.os.utils.read_cstring(filename)}", {mode:d}) = 0')
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'chmod("{vpath}", 0{mode:o}) = {regreturn}')
+
return regreturn
+
def ql_syscall_fchmod(ql: Qiling, fd: int, mode: int):
- if fd not in range(NR_OPEN) or ql.os.fd[fd] is None:
- return -EBADF
+ if fd not in range(NR_OPEN):
+ return -1
+
+ f = ql.os.fd[fd]
+
+ if f is None:
+ return -1
+
try:
- os.fchmod(ql.os.fd[fd].fileno(), mode)
- regreturn = 0
- except:
+ os.fchmod(f.fileno(), mode)
+ except OSError:
regreturn = -1
- ql.log.debug("fchmod(%d, %d) = %d" % (fd, mode, regreturn))
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'fchmod({fd}, 0{mode:o}) = {regreturn}')
+
return regreturn
+
def ql_syscall_fstatat64(ql: Qiling, dirfd: int, path: int, buf_ptr: int, flags: int):
dirfd, real_path = transform_path(ql, dirfd, path, flags)
@@ -1476,32 +1494,45 @@ def ql_syscall_mknodat(ql: Qiling, dirfd: int, path: int, mode: int, dev: int):
def ql_syscall_mkdir(ql: Qiling, pathname: int, mode: int):
- file_path = ql.os.utils.read_cstring(pathname)
- real_path = ql.os.path.transform_to_real_path(file_path)
- regreturn = 0
+ vpath = ql.os.utils.read_cstring(pathname)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
+
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
try:
- if not os.path.exists(real_path):
- os.mkdir(real_path, mode)
- except:
+ if not os.path.exists(hpath):
+ os.mkdir(hpath, mode)
+ except OSError:
regreturn = -1
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'mkdir("{vpath}", 0{mode:o}) = {regreturn}')
- ql.log.debug("mkdir(%s, 0%o) = %d" % (real_path, mode, regreturn))
return regreturn
+
def ql_syscall_rmdir(ql: Qiling, pathname: int):
- file_path = ql.os.utils.read_cstring(pathname)
- real_path = ql.os.path.transform_to_real_path(file_path)
- regreturn = 0
+ vpath = ql.os.utils.read_cstring(pathname)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
+
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
try:
- if os.path.exists(real_path):
- os.rmdir(real_path)
- except:
+ if os.path.exists(hpath):
+ os.rmdir(hpath)
+ except OSError:
regreturn = -1
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'rmdir("{vpath}") = {regreturn}')
return regreturn
+
def ql_syscall_fstatfs(ql: Qiling, fd: int, buf: int):
data = b"0" * (12 * 8) # for now, just return 0s
regreturn = 0
diff --git a/qiling/os/posix/syscall/syscall.py b/qiling/os/posix/syscall/syscall.py
new file mode 100644
index 000000000..3aba66931
--- /dev/null
+++ b/qiling/os/posix/syscall/syscall.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+#
+# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
+#
+
+from qiling import Qiling
+from qiling.os.posix.const import *
+
+from .shm import *
+
+
+def ql_syscall_ipc(ql: Qiling, call: int, first: int, second: int, third: int, ptr: int, fifth: int):
+ version = call >> 16 # hi word
+ call &= 0xffff # lo word
+
+ # FIXME: this is an incomplete implementation.
+ # see: https://elixir.bootlin.com/linux/v5.19.17/source/ipc/syscall.c
+
+ def __call_shmat(*args: int) -> int:
+ if version == 1:
+ return -1 # EINVAL
+
+ return ql_syscall_shmat(ql, args[0], args[3], args[1])
+
+ def __call_shmdt(*args: int) -> int:
+ return ql_syscall_shmdt(ql, args[3])
+
+ def __call_shmget(*args: int) -> int:
+ return ql_syscall_shmget(ql, args[0], args[1], args[2])
+
+ ipc_call = {
+ SHMAT: __call_shmat,
+ SHMDT: __call_shmdt,
+ SHMGET: __call_shmget
+ }
+
+ if call not in ipc_call:
+ return -1 # ENOSYS
+
+ return ipc_call[call](first, second, third, ptr, fifth)
+
+
+__all__ = [
+ 'ql_syscall_ipc'
+]
diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py
index b52f5e4b7..a70e83a28 100644
--- a/qiling/os/posix/syscall/unistd.py
+++ b/qiling/os/posix/syscall/unistd.py
@@ -3,23 +3,26 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
+from __future__ import annotations
+
import os
-import stat
import itertools
import pathlib
-from typing import Iterator
-from multiprocessing import Process
+from typing import TYPE_CHECKING, Iterator, Optional
from qiling import Qiling
from qiling.const import QL_ARCH, QL_OS
from qiling.os.posix.filestruct import ql_pipe
from qiling.os.posix.const import *
-from qiling.os.posix.stat import Stat
from qiling.core_hooks import QlCoreHooks
+if TYPE_CHECKING:
+ from qiling.os.posix.posix import QlOsPosix
+
+
def ql_syscall_exit(ql: Qiling, code: int):
- if ql.os.child_processes == True:
+ if ql.os.child_processes:
os._exit(0)
if ql.multithread:
@@ -37,7 +40,7 @@ def _sched_cb_exit(cur_thread):
def ql_syscall_exit_group(ql: Qiling, code: int):
- if ql.os.child_processes == True:
+ if ql.os.child_processes:
os._exit(0)
if ql.multithread:
@@ -135,99 +138,163 @@ def ql_syscall_setgroups(ql: Qiling, gidsetsize: int, grouplist: int):
def ql_syscall_setresuid(ql: Qiling):
return 0
+
def ql_syscall_setresgid(ql: Qiling):
return 0
+
def ql_syscall_capget(ql: Qiling, hdrp: int, datap: int):
return 0
+
def ql_syscall_capset(ql: Qiling, hdrp: int, datap: int):
return 0
+
def ql_syscall_kill(ql: Qiling, pid: int, sig: int):
return 0
+def get_opened_fd(os: QlOsPosix, fd: int):
+ if fd not in range(NR_OPEN):
+ # TODO: set errno to EBADF
+ return None
+
+ f = os.fd[fd]
+
+ if f is None:
+ # TODO: set errno to EBADF
+ return None
+
+ return f
+
+
def ql_syscall_fsync(ql: Qiling, fd: int):
- try:
- os.fsync(ql.os.fd[fd].fileno())
- regreturn = 0
- except:
+ f = get_opened_fd(ql.os, fd)
+
+ if f is None:
regreturn = -1
- ql.log.debug("fsync(%d) = %d" % (fd, regreturn))
+
+ else:
+ try:
+ os.fsync(f.fileno())
+ except OSError:
+ regreturn = -1
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'fsync({fd:d}) = {regreturn}')
+
return regreturn
def ql_syscall_fdatasync(ql: Qiling, fd: int):
try:
os.fdatasync(ql.os.fd[fd].fileno())
- regreturn = 0
- except:
+ except OSError:
regreturn = -1
- ql.log.debug("fdatasync(%d) = %d" % (fd, regreturn))
+ else:
+ regreturn = 0
+
+ ql.log.debug(f'fdatasync({fd:d}) = {regreturn}')
+
return regreturn
-def ql_syscall_faccessat(ql: Qiling, dfd: int, filename: int, mode: int):
- access_path = ql.os.utils.read_cstring(filename)
- real_path = ql.os.path.transform_to_real_path(access_path)
+def virtual_abspath_at(ql: Qiling, vpath: str, dirfd: int) -> Optional[str]:
+ if ql.os.path.is_virtual_abspath(vpath):
+ return vpath
- if not os.path.exists(real_path):
- regreturn = -1
- else:
- regreturn = 0
+ #
+ def __as_signed(value: int, nbits: int) -> int:
+ msb = (1 << (nbits - 1))
+
+ return -(((value & msb) << 1) - value)
+
+ # syscall params are read as unsigned int by default. until we fix that
+ # broadly, this is a workaround to turn fd into a signed value
+ dirfd = __as_signed(dirfd, ql.arch.bits)
+ #
+
+ if dirfd == AT_FDCWD:
+ basedir = ql.os.path.cwd
- if regreturn == -1:
- ql.log.debug(f'File not found or skipped: {access_path}')
else:
- ql.log.debug(f'File found: {access_path}')
+ f = get_opened_fd(ql.os, dirfd)
- return regreturn
+ if f is None or not hasattr(f, 'name'):
+ # EBADF
+ return None
+ hpath = f.name
-def ql_syscall_lseek(ql: Qiling, fd: int, offset: int, origin: int):
- if fd not in range(NR_OPEN):
- return -EBADF
+ if not os.path.isdir(hpath):
+ # ENOTDIR
+ return None
- f = ql.os.fd[fd]
+ basedir = ql.os.path.host_to_virtual_path(hpath)
- if f is None:
- return -EBADF
+ return str(ql.os.path.PureVirtualPath(basedir, vpath))
- offset = ql.unpacks(ql.pack(offset))
- try:
- regreturn = f.seek(offset, origin)
- except OSError:
+def ql_syscall_faccessat(ql: Qiling, dirfd: int, filename: int, mode: int):
+ vpath = ql.os.utils.read_cstring(filename)
+ vpath_at = virtual_abspath_at(ql, vpath, dirfd)
+
+ if vpath_at is None:
regreturn = -1
- # ql.log.debug("lseek(fd = %d, ofset = 0x%x, origin = 0x%x) = %d" % (fd, offset, origin, regreturn))
+ else:
+ hpath = ql.os.path.virtual_to_host_path(vpath_at)
+
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
+
+ regreturn = 0 if os.path.exists(hpath) else -1
+
+ ql.log.debug(f'faccessat({dirfd:d}, "{vpath}", {mode:d}) = {regreturn}')
return regreturn
-def ql_syscall__llseek(ql: Qiling, fd: int, offset_high: int, offset_low: int, result: int, whence: int):
- if fd not in range(NR_OPEN):
- return -EBADF
+def ql_syscall_lseek(ql: Qiling, fd: int, offset: int, origin: int):
+ offset = ql.unpacks(ql.pack(offset))
- f = ql.os.fd[fd]
+ f = get_opened_fd(ql.os, fd)
if f is None:
- return -EBADF
+ regreturn = -1
+
+ else:
+ try:
+ regreturn = f.seek(offset, origin)
+ except OSError:
+ regreturn = -1
+ ql.log.debug(f'lseek({fd:d}, {offset:#x}, {origin}) = {regreturn}')
+
+ return regreturn
+
+
+def ql_syscall__llseek(ql: Qiling, fd: int, offset_high: int, offset_low: int, result: int, whence: int):
# treat offset as a signed value
offset = ql.unpack64s(ql.pack64((offset_high << 32) | offset_low))
- origin = whence
- try:
- ret = f.seek(offset, origin)
- except OSError:
+ f = get_opened_fd(ql.os, fd)
+
+ if f is None:
regreturn = -1
+
else:
- ql.mem.write_ptr(result, ret, 8)
- regreturn = 0
+ try:
+ ret = f.seek(offset, whence)
+ except OSError:
+ regreturn = -1
+ else:
+ ql.mem.write_ptr(result, ret, 8)
+ regreturn = 0
- # ql.log.debug("_llseek(%d, 0x%x, 0x%x, 0x%x) = %d" % (fd, offset_high, offset_low, origin, regreturn))
+ ql.log.debug(f'_llseek({fd:d}, {offset_high:#x}, {offset_low:#x}, {result:#x}, {whence}) = {regreturn}')
return regreturn
@@ -251,75 +318,68 @@ def ql_syscall_brk(ql: Qiling, inp: int):
def ql_syscall_access(ql: Qiling, path: int, mode: int):
- file_path = ql.os.utils.read_cstring(path)
- real_path = ql.os.path.transform_to_real_path(file_path)
- relative_path = ql.os.path.transform_to_relative_path(file_path)
+ vpath = ql.os.utils.read_cstring(path)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
- regreturn = 0 if os.path.exists(real_path) else -1
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
- # ql.log.debug("access(%s, 0x%x) = %d " % (relative_path, access_mode, regreturn))
+ regreturn = 0 if os.path.exists(hpath) else -1
- if regreturn == 0:
- ql.log.debug(f'File found: {relative_path}')
- else:
- ql.log.debug(f'No such file or directory: {relative_path}')
+ ql.log.debug(f'access("{vpath}", 0{mode:o}) = {regreturn}')
return regreturn
def ql_syscall_close(ql: Qiling, fd: int):
- if fd not in range(NR_OPEN):
- return -1
-
- f = ql.os.fd[fd]
+ f = get_opened_fd(ql.os, fd)
if f is None:
- return -1
+ regreturn = -1
- f.close()
- ql.os.fd[fd] = None
+ else:
+ f.close()
+ ql.os.fd[fd] = None
+ regreturn = 0
- return 0
+ ql.log.debug(f'close({fd:d}) = {regreturn}')
+ return regreturn
-def ql_syscall_pread64(ql: Qiling, fd: int, buf: int, length: int, offt: int):
- if fd not in range(NR_OPEN):
- return -1
- f = ql.os.fd[fd]
+def ql_syscall_pread64(ql: Qiling, fd: int, buf: int, length: int, offt: int):
+ f = get_opened_fd(ql.os, fd)
if f is None:
- return -1
+ regreturn = -1
- # https://chromium.googlesource.com/linux-syscall-support/+/2c73abf02fd8af961e38024882b9ce0df6b4d19b
- # https://chromiumcodereview.appspot.com/10910222
- if ql.arch.type == QL_ARCH.MIPS:
- offt = ql.mem.read_ptr(ql.arch.regs.arch_sp + 0x10, 8)
+ else:
+ # https://chromium.googlesource.com/linux-syscall-support/+/2c73abf02fd8af961e38024882b9ce0df6b4d19b
+ # https://chromiumcodereview.appspot.com/10910222
+ if ql.arch.type == QL_ARCH.MIPS:
+ offt = ql.mem.read_ptr(ql.arch.regs.arch_sp + 0x10, 8)
- try:
- pos = f.tell()
- f.seek(offt)
+ try:
+ pos = f.tell()
+ f.seek(offt)
- data = f.read(length)
- f.seek(pos)
+ data = f.read(length)
+ f.seek(pos)
+ except OSError:
+ regreturn = -1
+ else:
+ ql.mem.write(buf, data)
- ql.mem.write(buf, data)
- except:
- regreturn = -1
- else:
- regreturn = len(data)
+ regreturn = len(data)
return regreturn
def ql_syscall_read(ql: Qiling, fd, buf: int, length: int):
- if fd not in range(NR_OPEN):
- return -EBADF
-
- f = ql.os.fd[fd]
+ f = get_opened_fd(ql.os, fd)
if f is None:
- return -EBADF
+ return -1
try:
data = f.read(length)
@@ -334,13 +394,10 @@ def ql_syscall_read(ql: Qiling, fd, buf: int, length: int):
def ql_syscall_write(ql: Qiling, fd: int, buf: int, count: int):
- if fd not in range(NR_OPEN):
- return -EBADF
-
- f = ql.os.fd[fd]
+ f = get_opened_fd(ql.os, fd)
if f is None:
- return -EBADF
+ return -1
try:
data = ql.mem.read(buf, count)
@@ -357,90 +414,95 @@ def ql_syscall_write(ql: Qiling, fd: int, buf: int, count: int):
ql.log.warning(f'write failed since fd {fd:d} does not have a write method')
regreturn = -1
-
return regreturn
-def ql_syscall_readlink(ql: Qiling, path_name: int, path_buff: int, path_buffsize: int):
- pathname = ql.os.utils.read_cstring(path_name)
- # pathname = str(pathname, 'utf-8', errors="ignore")
- host_path = ql.os.path.virtual_to_host_path(pathname)
- virt_path = ql.os.path.virtual_abspath(pathname)
+def __do_readlink(ql: Qiling, absvpath: str, outbuf: int) -> int:
+ target = None
- # cover procfs psaudo files first
- # TODO: /proc/self/root, /proc/self/cwd
- if virt_path == r'/proc/self/exe':
- p = ql.os.path.host_to_virtual_path(ql.path)
- p = p.encode('utf-8')
+ # cover a few procfs pseudo files first
+ if absvpath == r'/proc/self/exe':
+ # note this would raise an exception if the binary is not under rootfs
+ target = ql.os.path.host_to_virtual_path(ql.path)
- ql.mem.write(path_buff, p + b'\x00')
- regreturn = len(p)
+ elif absvpath == r'/proc/self/cwd':
+ target = ql.os.path.cwd
- elif os.path.exists(host_path):
- regreturn = 0
+ elif absvpath == r'/proc/self/root':
+ target = ql.os.path.root
else:
- regreturn = -1
+ hpath = ql.os.path.virtual_to_host_path(absvpath)
- ql.log.debug('readlink("%s", 0x%x, 0x%x) = %d' % (virt_path, path_buff, path_buffsize, regreturn))
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
- return regreturn
+ # FIXME: we do not really know how to emulated links, so we do not read them
+ if os.path.exists(hpath):
+ target = ''
+
+ if target is None:
+ return -1
+ cstr = target.encode('utf-8')
+
+ if cstr:
+ ql.mem.write(outbuf, cstr + b'\x00')
+
+ return len(cstr)
-def ql_syscall_getcwd(ql: Qiling, path_buff: int, path_buffsize: int):
- localpath = ql.os.path.transform_to_relative_path('./')
- localpath = bytes(localpath, 'utf-8') + b'\x00'
- ql.mem.write(path_buff, localpath)
- regreturn = len(localpath)
+def ql_syscall_readlink(ql: Qiling, pathname: int, buf: int, bufsize: int):
+ vpath = ql.os.utils.read_cstring(pathname)
+ absvpath = ql.os.path.virtual_abspath(vpath)
- pathname = ql.os.utils.read_cstring(path_buff)
- # pathname = str(pathname, 'utf-8', errors="ignore")
+ regreturn = __do_readlink(ql, absvpath, buf)
- ql.log.debug("getcwd(%s, 0x%x) = %d" % (pathname, path_buffsize, regreturn))
+ ql.log.debug(f'readlink("{vpath}", {buf:#x}, {bufsize:#x}) = {regreturn}')
return regreturn
-def ql_syscall_chdir(ql: Qiling, path_name: int):
- pathname = ql.os.utils.read_cstring(path_name)
- host_path = ql.os.path.virtual_to_host_path(pathname)
- virt_path = ql.os.path.virtual_abspath(pathname)
+def ql_syscall_readlinkat(ql: Qiling, dirfd: int, pathname: int, buf: int, bufsize: int):
+ vpath = ql.os.utils.read_cstring(pathname)
+ absvpath = virtual_abspath_at(ql, vpath, dirfd)
- if os.path.exists(host_path) and os.path.isdir(host_path):
- ql.os.path.cwd = virt_path
+ regreturn = -1 if absvpath is None else __do_readlink(ql, absvpath, buf)
- regreturn = 0
- ql.log.debug("chdir(%s) = %d"% (virt_path, regreturn))
- else:
- regreturn = -1
- ql.log.warning("chdir(%s) = %d : not found" % (virt_path, regreturn))
+ ql.log.debug(f'readlinkat({dirfd:d}, "{vpath}", {buf:#x}, {bufsize:#x}) = {regreturn}')
return regreturn
-def ql_syscall_readlinkat(ql: Qiling, dfd: int, path: int, buf: int, bufsize: int):
- pathname = ql.os.utils.read_cstring(path)
- # pathname = str(pathname, 'utf-8', errors="ignore")
- host_path = ql.os.path.virtual_to_host_path(pathname)
- virt_path = ql.os.path.virtual_abspath(pathname)
+def ql_syscall_getcwd(ql: Qiling, path_buff: int, path_buffsize: int):
+ cwd = ql.os.path.cwd
- # cover procfs psaudo files first
- # TODO: /proc/self/root, /proc/self/cwd
- if virt_path == r'/proc/self/exe':
- p = ql.os.path.host_to_virtual_path(ql.path)
- p = p.encode('utf-8')
+ cwd_bytes = cwd.encode('utf-8') + b'\x00'
+ ql.mem.write(path_buff, cwd_bytes)
+ regreturn = len(cwd_bytes)
- ql.mem.write(buf, p + b'\x00')
- regreturn = len(p)
+ ql.log.debug(f'getcwd("{cwd}", {path_buffsize}) = {regreturn}')
- elif os.path.exists(host_path):
- regreturn = 0
+ return regreturn
+
+
+def ql_syscall_chdir(ql: Qiling, path_name: int):
+ vpath = ql.os.utils.read_cstring(path_name)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
+
+ absvpath = ql.os.path.virtual_abspath(vpath)
+
+ if os.path.isdir(hpath):
+ ql.os.path.cwd = absvpath
+
+ regreturn = 0
else:
regreturn = -1
- ql.log.debug('readlinkat(%d, "%s", 0x%x, 0x%x) = %d' % (dfd, virt_path, buf, bufsize, regreturn))
+ ql.log.debug(f'chdir("{absvpath}") = {regreturn}')
return regreturn
@@ -455,6 +517,8 @@ def ql_syscall_getppid(ql: Qiling):
def ql_syscall_vfork(ql: Qiling):
if ql.host.os == QL_OS.WINDOWS:
+ from multiprocessing import Process
+
try:
pid = Process()
pid = 0
@@ -479,15 +543,24 @@ def ql_syscall_vfork(ql: Qiling):
def ql_syscall_fork(ql: Qiling):
return ql_syscall_vfork(ql)
+
def ql_syscall_setsid(ql: Qiling):
return os.getpid()
def ql_syscall_execve(ql: Qiling, pathname: int, argv: int, envp: int):
- file_path = ql.os.utils.read_cstring(pathname)
- real_path = ql.os.path.transform_to_real_path(file_path)
+ vpath = ql.os.utils.read_cstring(pathname)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
- def __read_str_array(addr: int) -> Iterator[str]:
+ # is it safe to run?
+ if not ql.os.path.is_safe_host_path(hpath):
+ return -1 # EACCES
+
+ # is it a file? does it exist?
+ if not os.path.isfile(hpath):
+ return -1 # EACCES
+
+ def __read_ptr_array(addr: int) -> Iterator[int]:
if addr:
while True:
elem = ql.mem.read_ptr(addr)
@@ -495,25 +568,28 @@ def __read_str_array(addr: int) -> Iterator[str]:
if elem == 0:
break
- yield ql.os.utils.read_cstring(elem)
+ yield elem
addr += ql.arch.pointersize
- args = [s for s in __read_str_array(argv)]
+ def __read_str_array(addr: int) -> Iterator[str]:
+ yield from (ql.os.utils.read_cstring(ptr) for ptr in __read_ptr_array(addr))
+
+ args = list(__read_str_array(argv))
env = {}
for s in __read_str_array(envp):
k, _, v = s.partition('=')
env[k] = v
- ql.emu_stop()
+ ql.stop()
+ ql.clear_ql_hooks()
+ ql.mem.unmap_all()
- ql.log.debug(f'execve({file_path}, [{", ".join(args)}], [{", ".join(f"{k}={v}" for k, v in env.items())}])')
+ ql.log.debug(f'execve("{vpath}", [{", ".join(args)}], [{", ".join(f"{k}={v}" for k, v in env.items())}])')
ql.loader.argv = args
ql.loader.env = env
- ql._argv = [real_path] + args
- ql.mem.map_info = []
- ql.clear_ql_hooks()
+ ql._argv = [hpath] + args
# Clean debugger to prevent port conflicts
# ql.debugger = None
@@ -534,56 +610,86 @@ def __read_str_array(addr: int) -> Iterator[str]:
QlCoreHooks.__init__(ql, uc)
ql.os.load()
+
+ # close all open fd marked with 'close_on_exec'
+ for i in range(NR_OPEN):
+ f = ql.os.fd[i]
+
+ if f and getattr(f, 'close_on_exec', False) and not f.closed:
+ f.close()
+ ql.os.fd[i] = None
+
ql.loader.run()
ql.run()
def ql_syscall_dup(ql: Qiling, oldfd: int):
- if oldfd not in range(NR_OPEN):
- return -EBADF
-
- f = ql.os.fd[oldfd]
+ f = get_opened_fd(ql.os, oldfd)
if f is None:
- return -EBADF
+ return -1
- idx = next((i for i in range(NR_OPEN) if ql.os.fd[i] is None), -1)
+ newfd = next((i for i in range(NR_OPEN) if ql.os.fd[i] is None), -1)
- if idx == -1:
+ if newfd == -1:
return -EMFILE
- ql.os.fd[idx] = f.dup()
+ ql.os.fd[newfd] = f.dup()
- return idx
+ ql.log.debug(f'dup({oldfd:d}) = {newfd:d}')
+ return newfd
-def ql_syscall_dup2(ql: Qiling, fd: int, newfd: int):
- if fd not in range(NR_OPEN) or newfd not in range(NR_OPEN):
- return -EBADF
- f = ql.os.fd[fd]
+def ql_syscall_dup2(ql: Qiling, oldfd: int, newfd: int):
+ f = get_opened_fd(ql.os, oldfd)
if f is None:
- return -EBADF
+ return -1
+
+ if newfd not in range(NR_OPEN):
+ return -1
+
+ newslot = ql.os.fd[newfd]
+
+ if newslot is not None:
+ newslot.close()
ql.os.fd[newfd] = f.dup()
+ ql.log.debug(f'dup2({oldfd:d}, {newfd:d}) = {newfd:d}')
+
return newfd
-def ql_syscall_dup3(ql: Qiling, fd: int, newfd: int, flags: int):
- if fd not in range(NR_OPEN) or newfd not in range(NR_OPEN):
- return -1
+def ql_syscall_dup3(ql: Qiling, oldfd: int, newfd: int, flags: int):
+ O_CLOEXEC = 0o2000000
- f = ql.os.fd[fd]
+ f = get_opened_fd(ql.os, oldfd)
if f is None:
return -1
- ql.os.fd[newfd] = f.dup()
+ if newfd not in range(NR_OPEN):
+ return -1
+
+ newslot = ql.os.fd[newfd]
+
+ if newslot is not None:
+ newslot.close()
+
+ newf = f.dup()
+
+ if flags & O_CLOEXEC:
+ newf.close_on_exec = True
+
+ ql.os.fd[newfd] = newf
+
+ ql.log.debug(f'dup3({oldfd:d}, {newfd:d}, 0{flags:o}) = {newfd:d}')
return newfd
+
def ql_syscall_set_tid_address(ql: Qiling, tidptr: int):
if ql.os.thread_management:
regreturn = ql.os.thread_management.cur_thread.id
@@ -621,102 +727,107 @@ def ql_syscall_nice(ql: Qiling, inc: int):
return 0
-def ql_syscall_truncate(ql: Qiling, path: int, length: int):
- file_path = ql.os.utils.read_cstring(path)
- real_path = ql.os.path.transform_to_real_path(file_path)
- st_size = Stat(real_path).st_size
+def __do_truncate(ql: Qiling, hpath: str, length: int) -> int:
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
try:
- if st_size >= length:
- os.truncate(real_path, length)
+ st_size = os.path.getsize(hpath)
- else:
+ if st_size > length:
+ os.truncate(hpath, length)
+
+ elif st_size < length:
padding = length - st_size
- with open(real_path, 'a+b') as ofile:
+ with open(hpath, 'a+b') as ofile:
ofile.write(b'\x00' * padding)
- except:
- regreturn = -1
+ except OSError:
+ return -1
else:
- regreturn = 0
+ return 0
+
+
+def ql_syscall_truncate(ql: Qiling, path: int, length: int):
+ vpath = ql.os.utils.read_cstring(path)
+ hpath = ql.os.path.virtual_to_host_path(vpath)
- ql.log.debug('truncate(%s, 0x%x) = %d' % (file_path, length, regreturn))
+ regreturn = __do_truncate(ql, hpath, length)
+
+ ql.log.debug(f'truncate("{vpath}", {length:#x}) = {regreturn}')
return regreturn
def ql_syscall_ftruncate(ql: Qiling, fd: int, length: int):
- real_path = ql.os.fd[fd].name
- st_size = Stat(real_path).st_size
+ f = get_opened_fd(ql.os, fd)
- try:
- if st_size >= length:
- os.truncate(real_path, length)
+ regreturn = -1 if f is None else __do_truncate(ql, f.name, length)
- else:
- padding = length - st_size
+ ql.log.debug(f'ftruncate({fd}, {length:#x}) = {regreturn}')
- with open(real_path, 'a+b') as ofile:
- ofile.write(b'\x00' * padding)
- except:
- regreturn = -1
- else:
- regreturn = 0
+ return regreturn
- ql.log.debug("ftruncate(%d, 0x%x) = %d" % (fd, length, regreturn))
- return regreturn
+def __do_unlink(ql: Qiling, absvpath: str) -> int:
+ def __has_opened_fd(hpath: str) -> bool:
+ opened_fds = (ql.os.fd[i] for i in range(NR_OPEN) if ql.os.fd[i] is not None)
+ f = next((fd for fd in opened_fds if getattr(fd, 'name', '') == hpath), None)
-def ql_syscall_unlink(ql: Qiling, pathname: int):
- file_path = ql.os.utils.read_cstring(pathname)
- real_path = ql.os.path.transform_to_real_path(file_path)
+ return f is not None and f.closed
- opened_fds = [getattr(ql.os.fd[i], 'name', None) for i in range(NR_OPEN) if ql.os.fd[i] is not None]
- path = pathlib.Path(real_path)
+ hpath = ql.os.path.virtual_to_host_path(absvpath)
- if any((real_path not in opened_fds, path.is_block_device(), path.is_fifo(), path.is_socket(), path.is_symlink())):
- try:
- os.unlink(real_path)
- except FileNotFoundError:
- ql.log.debug('No such file or directory')
- regreturn = -1
- except:
- regreturn = -1
- else:
- regreturn = 0
+ if ql.os.fs_mapper.has_mapping(absvpath):
+ if __has_opened_fd(hpath):
+ return -1
+
+ ql.os.fs_mapper.remove_mapping(absvpath)
else:
- regreturn = -1
+ if not ql.os.path.is_safe_host_path(hpath):
+ raise PermissionError(f'unsafe path: {hpath}')
- ql.log.debug("unlink(%s) = %d" % (file_path, regreturn))
+ # NOTE: no idea why these are always ok to remove
+ def __ok_to_remove(hpath: str) -> bool:
+ path = pathlib.Path(hpath)
- return regreturn
+ return any((path.is_block_device(), path.is_fifo(), path.is_socket(), path.is_symlink()))
+ if __has_opened_fd(hpath) and not __ok_to_remove(hpath):
+ return -1
-def ql_syscall_unlinkat(ql: Qiling, fd: int, pathname: int):
- file_path = ql.os.utils.read_cstring(pathname)
- real_path = ql.os.path.transform_to_real_path(file_path)
+ try:
+ os.unlink(hpath)
+ except OSError:
+ return -1
- try:
- dir_fd = ql.os.fd[fd].fileno()
- except:
- dir_fd = None
+ return 0
- try:
- if dir_fd is None:
- os.unlink(real_path)
- else:
- os.unlink(file_path, dir_fd=dir_fd)
- except OSError as e:
- regreturn = -e.errno
- else:
- regreturn = 0
- ql.log.debug("unlinkat(fd = %d, path = '%s') = %d" % (fd, file_path, regreturn))
+def ql_syscall_unlink(ql: Qiling, pathname: int):
+ vpath = ql.os.utils.read_cstring(pathname)
+ absvpath = ql.os.path.virtual_abspath(vpath)
+
+ regreturn = __do_unlink(ql, absvpath)
+
+ ql.log.debug(f'unlink("{vpath}") = {regreturn}')
return regreturn
+
+def ql_syscall_unlinkat(ql: Qiling, dirfd: int, pathname: int, flags: int):
+ vpath = ql.os.utils.read_cstring(pathname)
+ absvpath = virtual_abspath_at(ql, vpath, dirfd)
+
+ regreturn = -1 if absvpath is None else __do_unlink(ql, absvpath)
+
+ ql.log.debug(f'unlinkat({dirfd}, "{vpath}") = {regreturn}')
+
+ return regreturn
+
+
# https://man7.org/linux/man-pages/man2/getdents.2.html
# struct linux_dirent {
# unsigned long d_ino; /* Inode number */
@@ -827,5 +938,6 @@ def _type_mapping(ent):
def ql_syscall_getdents(ql: Qiling, fd: int, dirp: int, count: int):
return __getdents_common(ql, fd, dirp, count, is_64=False)
+
def ql_syscall_getdents64(ql: Qiling, fd: int, dirp: int, count: int):
return __getdents_common(ql, fd, dirp, count, is_64=True)
diff --git a/qiling/os/qnx/qnx.py b/qiling/os/qnx/qnx.py
index 9f79f1d95..7187aefcf 100644
--- a/qiling/os/qnx/qnx.py
+++ b/qiling/os/qnx/qnx.py
@@ -1,10 +1,8 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
-import os
-
from unicorn import UcError
from qiling import Qiling
@@ -20,6 +18,7 @@
from qiling.os.const import *
from qiling.os.posix.posix import QlOsPosix
+
class QlOsQnx(QlOsPosix):
type = QL_OS.QNX
@@ -45,9 +44,8 @@ def __init__(self, ql: Qiling):
self.futexm = None
self.fh = None
self.function_after_load_list = []
- self.elf_mem_start = 0x0
self.load()
-
+
# use counters to get free Ids
self.channel_id = 1
# TODO: replace 0x400 with NR_OPEN from Qiling 1.25
@@ -75,37 +73,35 @@ def load(self):
'get_tls': 0xffff0fe0
})
-
def hook_syscall(self, ql, intno):
return self.load_syscall()
-
def register_function_after_load(self, function):
if function not in self.function_after_load_list:
self.function_after_load_list.append(function)
-
def run_function_after_load(self):
for f in self.function_after_load_list:
f()
-
def run(self):
if self.ql.exit_point is not None:
self.exit_point = self.ql.exit_point
- if self.ql.entry_point is not None:
+ if self.ql.entry_point is not None:
self.ql.loader.elf_entry = self.ql.entry_point
- self.cpupage_addr = int(self.ql.os.profile.get("OS32", "cpupage_address"), 16)
- self.cpupage_tls_addr = int(self.ql.os.profile.get("OS32", "cpupage_tls_address"), 16)
- self.tls_data_addr = int(self.ql.os.profile.get("OS32", "tls_data_address"), 16)
- self.syspage_addr = int(self.ql.os.profile.get("OS32", "syspage_address"), 16)
- syspage_path = os.path.join(self.ql.rootfs, "syspage.bin")
+ profile = self.ql.os.profile['OS32']
+
+ self.cpupage_addr = profile.getint('cpupage_address')
+ self.cpupage_tls_addr = profile.getint('cpupage_tls_address')
+ self.tls_data_addr = profile.getint('tls_data_address')
+ self.syspage_addr = profile.getint('syspage_address')
self.ql.mem.map(self.syspage_addr, 0x4000, info="[syspage_mem]")
-
- with open(syspage_path, "rb") as sp:
+ syspage_hpath = self.ql.os.path.virtual_to_host_path("/syspage.bin")
+
+ with open(syspage_hpath, "rb") as sp:
self.ql.mem.write(self.syspage_addr, sp.read())
# Address of struct _thread_local_storage for our thread
diff --git a/qiling/os/windows/dlls/kernel32/fileapi.py b/qiling/os/windows/dlls/kernel32/fileapi.py
index 1176462f6..60465ff87 100644
--- a/qiling/os/windows/dlls/kernel32/fileapi.py
+++ b/qiling/os/windows/dlls/kernel32/fileapi.py
@@ -52,21 +52,13 @@ def hook_FindFirstFileA(ql: Qiling, address: int, params):
if len(filename) >= MAX_PATH:
return ERROR_INVALID_PARAMETER
- host_path = ql.os.path.virtual_to_host_path(filename)
-
- # Verify the directory is in ql.rootfs to ensure no path traversal has taken place
- if not ql.os.path.is_safe_host_path(host_path):
- ql.os.last_error = ERROR_FILE_NOT_FOUND
-
- return INVALID_HANDLE_VALUE
-
# Check if path exists
filesize = 0
try:
- f = ql.os.fs_mapper.open(host_path, "r")
+ f = ql.os.fs_mapper.open(filename, "r")
- filesize = os.path.getsize(host_path)
+ filesize = os.path.getsize(f.name)
except FileNotFoundError:
ql.os.last_error = ERROR_FILE_NOT_FOUND
diff --git a/qiling/utils.py b/qiling/utils.py
index 14528b689..05cb0a43e 100644
--- a/qiling/utils.py
+++ b/qiling/utils.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
@@ -8,11 +8,13 @@
thoughout the qiling framework
"""
-from functools import partial
-from pathlib import Path
-import importlib, inspect, os
+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
@@ -38,27 +40,32 @@ def __name_to_enum(name: str, mapping: Mapping[str, T], aliases: Mapping[str, st
return mapping.get(aliases.get(key) or key)
+
def os_convert(os: str) -> Optional[QL_OS]:
alias_map = {
- 'darwin' : 'macos'
+ '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'
+ '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')
@@ -67,6 +74,7 @@ def ql_get_module(module_name: str) -> ModuleType:
return module
+
def ql_get_module_function(module_name: str, member_name: str):
module = ql_get_module(module_name)
@@ -77,6 +85,7 @@ def ql_get_module_function(module_name: str, member_name: str):
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
@@ -89,6 +98,7 @@ def __emu_env_from_pathname(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_O
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.
@@ -99,7 +109,7 @@ def __emu_env_from_elf(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], O
ELFCLASS32 = 1 # 32-bit
ELFCLASS64 = 2 # 64-bit
- #ei_data
+ # ei_data
ELFDATA2LSB = 1 # little-endian
ELFDATA2MSB = 2 # big-endian
@@ -121,8 +131,8 @@ def __emu_env_from_elf(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], O
EM_PPC = 20
endianess = {
- ELFDATA2LSB : (QL_ENDIAN.EL, 'little'),
- ELFDATA2MSB : (QL_ENDIAN.EB, 'big')
+ ELFDATA2LSB: (QL_ENDIAN.EL, 'little'),
+ ELFDATA2MSB: (QL_ENDIAN.EB, 'big')
}
machines32 = {
@@ -140,8 +150,8 @@ def __emu_env_from_elf(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], O
}
classes = {
- ELFCLASS32 : machines32,
- ELFCLASS64 : machines64
+ ELFCLASS32: machines32,
+ ELFCLASS64: machines64
}
abis = {
@@ -192,6 +202,7 @@ def __emu_env_from_elf(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], O
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'
@@ -220,6 +231,7 @@ def __emu_env_from_macho(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS],
return arch, ostype, endian
+
def __emu_env_from_pe(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Optional[QL_ENDIAN]]:
import pefile
@@ -260,6 +272,7 @@ def __emu_env_from_pe(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Op
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,
@@ -278,9 +291,10 @@ def ql_guess_emu_env(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], Opt
return arch, ostype, endian
+
def select_loader(ostype: QL_OS, libcache: bool) -> QlClassInit['QlLoader']:
if ostype == QL_OS.WINDOWS:
- kwargs = {'libcache' : libcache}
+ kwargs = {'libcache': libcache}
else:
kwargs = {}
@@ -305,6 +319,7 @@ def select_loader(ostype: QL_OS, libcache: bool) -> QlClassInit['QlLoader']:
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'
@@ -313,6 +328,7 @@ def select_component(component_type: str, component_name: str, **kwargs) -> QlCl
return partial(obj, **kwargs)
+
def select_debugger(options: Union[str, bool]) -> Optional[QlClassInit['QlDebugger']]:
if options is True:
options = 'gdb'
@@ -353,14 +369,15 @@ def __int_nothrow(v: str, /) -> Optional[int]:
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 == QL_ARCH.ARM:
- kwargs = {'endian' : endian, 'thumb' : thumb}
+ if archtype is QL_ARCH.ARM:
+ kwargs = {'endian': endian, 'thumb': thumb}
# set endianess for mips arch
- elif archtype == QL_ARCH.MIPS:
- kwargs = {'endian' : endian}
+ elif archtype is QL_ARCH.MIPS:
+ kwargs = {'endian': endian}
else:
kwargs = {}
@@ -386,6 +403,7 @@ def select_arch(archtype: QL_ARCH, endian: QL_ENDIAN, thumb: bool) -> QlClassIni
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()}'
@@ -395,14 +413,15 @@ def select_os(ostype: QL_OS) -> QlClassInit['QlOs']:
return partial(obj)
+
def profile_setup(ostype: QL_OS, user_config: Optional[Union[str, dict]]):
# mcu uses a yaml-based config
- if ostype == QL_OS.MCU:
+ if ostype is QL_OS.MCU:
import yaml
if user_config:
with open(user_config) as f:
- config = yaml.load(f, Loader=yaml.Loader)
+ config = yaml.load(f, Loader=yaml.SafeLoader)
else:
config = {}
@@ -426,6 +445,7 @@ def profile_setup(ostype: QL_OS, user_config: Optional[Union[str, dict]]):
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
@@ -463,6 +483,7 @@ def verify_ret(ql: 'Qiling', err):
else:
raise
+
__all__ = [
'os_convert',
'arch_convert',
diff --git a/tests/test_elf_multithread.py b/tests/test_elf_multithread.py
index 2121deb46..c6a7fd4ef 100644
--- a/tests/test_elf_multithread.py
+++ b/tests/test_elf_multithread.py
@@ -3,353 +3,550 @@
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
-import http.client, platform, socket, sys, os, threading, time, unittest
+import http.client
+import platform
+import re
+import sys
+import os
+import threading
+import time
+import unittest
+
+from typing import List
sys.path.append("..")
from qiling import Qiling
from qiling.const import *
from qiling.exception import *
from qiling.os.filestruct import ql_file
+from qiling.os.stats import QlOsNullStats
+
+
+BASE_ROOTFS = r'../examples/rootfs'
+X86_LINUX_ROOTFS = fr'{BASE_ROOTFS}/x86_linux'
+X64_LINUX_ROOTFS = fr'{BASE_ROOTFS}/x8664_linux'
+ARM_LINUX_ROOTFS = fr'{BASE_ROOTFS}/arm_linux'
+ARMEB_LINUX_ROOTFS = fr'{BASE_ROOTFS}/armeb_linux'
+ARM64_LINUX_ROOTFS = fr'{BASE_ROOTFS}/arm64_linux'
+MIPSEB_LINUX_ROOTFS = fr'{BASE_ROOTFS}/mips32_linux'
+MIPSEL_LINUX_ROOTFS = fr'{BASE_ROOTFS}/mips32el_linux'
+
class ELFTest(unittest.TestCase):
@unittest.skipIf(platform.system() == "Darwin" and platform.machine() == "arm64", 'darwin host')
def test_elf_linux_execve_x8664(self):
- ql = Qiling(["../examples/rootfs/x8664_linux/bin/posix_syscall_execve"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.DEBUG)
+ ql = Qiling([fr'{X64_LINUX_ROOTFS}/bin/posix_syscall_execve'], X64_LINUX_ROOTFS, verbose=QL_VERBOSE.DEBUG)
ql.run()
- for key, value in ql.loader.env.items():
- QL_TEST=value
+ env = ql.loader.env
- self.assertEqual("TEST_QUERY", QL_TEST)
- self.assertEqual("child", ql.loader.argv[0])
-
- del QL_TEST
- del ql
+ self.assertIn('QL_TEST', env)
+ self.assertEqual('TEST_QUERY', env['QL_TEST'])
+ self.assertEqual('child', ql.loader.argv[0])
def test_elf_linux_cloexec_x8664(self):
- ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_cloexec_test"], "../examples/rootfs/x8664_linux", verbose=QL_VERBOSE.DEBUG, multithread=True)
+ ql = Qiling([fr'{X64_LINUX_ROOTFS}/bin/x8664_cloexec_test'], X64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
- filename = 'output.txt'
- err = ql_file.open(filename, os.O_RDWR | os.O_CREAT, 0o777)
+ filename = 'stderr.txt'
+ err = ql_file.open(filename, os.O_RDWR | os.O_CREAT, 0o644)
+ ql.os.stats = QlOsNullStats()
ql.os.stderr = err
+
ql.run()
err.close()
- with open(filename, 'rb') as f:
- content = f.read()
+ with open(filename, 'r') as f:
+ contents = f.readlines()
# cleanup
os.remove(filename)
- self.assertIn(b'fail', content)
-
- del ql
+ self.assertGreaterEqual(len(contents), 4)
+ self.assertIn('Operation not permitted', contents[-2])
+ self.assertIn('Operation not permitted', contents[-1])
def test_multithread_elf_linux_x86(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- nonlocal buf_out
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- buf_out = buf
- except:
- pass
- buf_out = None
- ql = Qiling(["../examples/rootfs/x86_linux/bin/x86_multithreading"], "../examples/rootfs/x86_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{X86_LINUX_ROOTFS}/bin/x86_multithreading'], X86_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertTrue("thread 2 ret val is" in buf_out)
-
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_multithread_elf_linux_arm64(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- nonlocal buf_out
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- buf_out = buf
- except:
- pass
- buf_out = None
- ql = Qiling(["../examples/rootfs/arm64_linux/bin/arm64_multithreading"], "../examples/rootfs/arm64_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARM64_LINUX_ROOTFS}/bin/arm64_multithreading'], ARM64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertTrue("thread 2 ret val is" in buf_out)
-
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_multithread_elf_linux_x8664(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- nonlocal buf_out
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- buf_out = buf
- except:
- pass
- buf_out = None
- ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_multithreading"], "../examples/rootfs/x8664_linux", multithread=True)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{X64_LINUX_ROOTFS}/bin/x8664_multithreading'], X64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertTrue("thread 2 ret val is" in buf_out)
-
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_multithread_elf_linux_mips32eb(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- nonlocal buf_out
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- buf_out = buf
- except:
- pass
- buf_out = None
- ql = Qiling(["../examples/rootfs/mips32_linux/bin/mips32_multithreading"], "../examples/rootfs/mips32_linux", verbose=QL_VERBOSE.DEBUG, multithread=True)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{MIPSEB_LINUX_ROOTFS}/bin/mips32_multithreading'], MIPSEB_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertTrue("thread 2 ret val is" in buf_out)
-
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_multithread_elf_linux_mips32el(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- nonlocal buf_out
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- buf_out = buf
- except:
- pass
- buf_out = None
- ql = Qiling(["../examples/rootfs/mips32el_linux/bin/mips32el_multithreading"], "../examples/rootfs/mips32el_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{MIPSEL_LINUX_ROOTFS}/bin/mips32el_multithreading'], MIPSEL_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertTrue("thread 2 ret val is" in buf_out)
-
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_multithread_elf_linux_arm(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- nonlocal buf_out
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- buf_out = buf
- except:
- pass
- buf_out = None
- ql = Qiling(["../examples/rootfs/arm_linux/bin/arm_multithreading"], "../examples/rootfs/arm_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARM_LINUX_ROOTFS}/bin/arm_multithreading'], ARM_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertTrue("thread 2 ret val is" in buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
- del ql
+ @unittest.skip('broken: unicorn.unicorn.UcError: Invalid instruction (UC_ERR_INSN_INVALID)')
+ def test_multithread_elf_linux_armeb(self):
+ logged: List[str] = []
- # unicorn.unicorn.UcError: Invalid instruction (UC_ERR_INSN_INVALID)
- # def test_multithread_elf_linux_armeb(self):
- # def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- # nonlocal buf_out
- # try:
- # buf = ql.mem.read(write_buf, write_count)
- # buf = buf.decode()
- # buf_out = buf
- # except:
- # pass
- # buf_out = None
- # ql = Qiling(["../examples/rootfs/armeb_linux/bin/armeb_multithreading"], "../examples/rootfs/armeb_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
- # ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
- # ql.run()
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 1:
+ content = ql.mem.read(write_buf, count)
- # self.assertTrue("thread 2 ret val is" in buf_out)
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARMEB_LINUX_ROOTFS}/bin/armeb_multithreading'], ARMEB_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
+ ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
+ ql.run()
- # del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('thread 1 ret val is'))
+ self.assertTrue(logged[-1].startswith('thread 2 ret val is'))
def test_tcp_elf_linux_x86(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server send()"):
- ql.buf_out = buf
- except:
- pass
- ql = Qiling(["../examples/rootfs/x86_linux/bin/x86_tcp_test", "20001"], "../examples/rootfs/x86_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{X86_LINUX_ROOTFS}/bin/x86_tcp_test', '20000'], X86_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server send() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recv()'))
+ self.assertTrue(logged[-1].startswith('server send()'))
- del ql
+ # the server is expected to send the value it received, for example:
+ # 'server recv() return 14.\n'
+ # 'server send() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_tcp_elf_linux_x8664(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server send()"):
- ql.buf_out = buf
- except:
- pass
- ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_tcp_test", "20002"], "../examples/rootfs/x8664_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{X64_LINUX_ROOTFS}/bin/x8664_tcp_test', '20001'], X64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server send() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recv()'))
+ self.assertTrue(logged[-1].startswith('server send()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server recv() return 14.\n'
+ # 'server send() 14 return 14.\n'
- del ql
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_tcp_elf_linux_arm(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server write()"):
- ql.buf_out = buf
- except:
- pass
- ql = Qiling(["../examples/rootfs/arm_linux/bin/arm_tcp_test", "20003"], "../examples/rootfs/arm_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARM_LINUX_ROOTFS}/bin/arm_tcp_test', '20002'], ARM_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server write() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server read()'))
+ self.assertTrue(logged[-1].startswith('server write()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server read() return 14.\n'
+ # 'server write() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
- del ql
+ num = m.group('num')
+ msg = logged[-1].strip()
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_tcp_elf_linux_arm64(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server send()"):
- ql.buf_out = buf
- except:
- pass
- ql = Qiling(["../examples/rootfs/arm64_linux/bin/arm64_tcp_test", "20004"], "../examples/rootfs/arm64_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARM64_LINUX_ROOTFS}/bin/arm64_tcp_test', '20003'], ARM64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server send() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recv()'))
+ self.assertTrue(logged[-1].startswith('server send()'))
- del ql
+ # the server is expected to send the value it received, for example:
+ # 'server recv() return 14.\n'
+ # 'server send() 14 return 14.\n'
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_tcp_elf_linux_armeb(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server send()"):
- ql.buf_out = buf
- except:
- pass
- ql = Qiling(["../examples/rootfs/armeb_linux/bin/armeb_tcp_test", "20003"], "../examples/rootfs/armeb_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARMEB_LINUX_ROOTFS}/bin/armeb_tcp_test', '20004'], ARMEB_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server send() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recv()'))
+ self.assertTrue(logged[-1].startswith('server send()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server recv() return 14.\n'
+ # 'server send() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
- del ql
+ num = m.group('num')
+ msg = logged[-1].strip()
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_tcp_elf_linux_mips32eb(self):
- ql = Qiling(["../examples/rootfs/mips32_linux/bin/mips32_tcp_test", "20005"], "../examples/rootfs/mips32_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{MIPSEB_LINUX_ROOTFS}/bin/mips32_tcp_test', '20005'], MIPSEB_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
+ ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recv()'))
+ self.assertTrue(logged[-1].startswith('server send()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server recv() return 14.\n'
+ # 'server send() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_tcp_elf_linux_mips32el(self):
- ql = Qiling(["../examples/rootfs/mips32el_linux/bin/mips32el_tcp_test", "20005"], "../examples/rootfs/mips32el_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{MIPSEL_LINUX_ROOTFS}/bin/mips32el_tcp_test', '20006'], MIPSEL_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
+ ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- del ql
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server read()'))
+ self.assertTrue(logged[-1].startswith('server write()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server read() return 14.\n'
+ # 'server write() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_udp_elf_linux_x86(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server sendto()"):
- ql.buf_out = buf
- except:
- pass
-
- ql = Qiling(["../examples/rootfs/x86_linux/bin/x86_udp_test", "20007"], "../examples/rootfs/x86_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{X86_LINUX_ROOTFS}/bin/x86_udp_test', '20010'], X86_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server sendto() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recvfrom()'))
+ self.assertTrue(logged[-1].startswith('server sendto()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server recvfrom() return 14.\n'
+ # 'server sendto() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
- del ql
+ num = m.group('num')
+ msg = logged[-1].strip()
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_udp_elf_linux_x8664(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server sendto()"):
- ql.buf_out = buf
- except:
- pass
-
- ql = Qiling(["../examples/rootfs/x8664_linux/bin/x8664_udp_test", "20008"], "../examples/rootfs/x8664_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{X64_LINUX_ROOTFS}/bin/x8664_udp_test', '20011'], X64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server sendto() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recvfrom()'))
+ self.assertTrue(logged[-1].startswith('server sendto()'))
- del ql
+ # the server is expected to send the value it received, for example:
+ # 'server recvfrom() return 14.\n'
+ # 'server sendto() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_udp_elf_linux_arm64(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server sendto()"):
- ql.buf_out = buf
- except:
- pass
-
- ql = Qiling(["../examples/rootfs/arm64_linux/bin/arm64_udp_test", "20009"], "../examples/rootfs/arm64_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARM64_LINUX_ROOTFS}/bin/arm64_udp_test', '20013'], ARM64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server sendto() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recvfrom()'))
+ self.assertTrue(logged[-1].startswith('server sendto()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server recvfrom() return 14.\n'
+ # 'server sendto() 14 return 14.\n'
- del ql
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
+
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_udp_elf_linux_armeb(self):
- def check_write(ql, write_fd, write_buf, write_count, *args, **kw):
- try:
- buf = ql.mem.read(write_buf, write_count)
- buf = buf.decode()
- if buf.startswith("server sendto()"):
- ql.buf_out = buf
- except:
- pass
-
- ql = Qiling(["../examples/rootfs/armeb_linux/bin/armeb_udp_test", "20010"], "../examples/rootfs/armeb_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ logged: List[str] = []
+
+ def check_write(ql: Qiling, fd: int, write_buf, count: int):
+ if fd == 2:
+ content = ql.mem.read(write_buf, count)
+
+ logged.append(content.decode())
+
+ ql = Qiling([fr'{ARMEB_LINUX_ROOTFS}/bin/armeb_udp_test', '20014'], ARMEB_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
+
+ ql.os.stats = QlOsNullStats()
ql.os.set_syscall("write", check_write, QL_INTERCEPT.ENTER)
ql.run()
- self.assertEqual("server sendto() 14 return 14.\n", ql.buf_out)
+ self.assertGreaterEqual(len(logged), 2)
+ self.assertTrue(logged[-2].startswith('server recvfrom()'))
+ self.assertTrue(logged[-1].startswith('server sendto()'))
+
+ # the server is expected to send the value it received, for example:
+ # 'server recvfrom() return 14.\n'
+ # 'server sendto() 14 return 14.\n'
+
+ m = re.search(r'(?P\d+)\.\s+\Z', logged[-2])
+ self.assertIsNotNone(m, 'could not extract numeric value from log message')
- del ql
+ num = m.group('num')
+ msg = logged[-1].strip()
+
+ self.assertTrue(msg.endswith(f'{num} return {num}.'))
def test_http_elf_linux_x8664(self):
+ PORT = 20020
+
def picohttpd():
- ql = Qiling(["../examples/rootfs/x8664_linux/bin/picohttpd", "12911"], "../examples/rootfs/x8664_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ ql = Qiling([fr'{X64_LINUX_ROOTFS}/bin/picohttpd', f'{PORT:d}'], X64_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
ql.run()
picohttpd_therad = threading.Thread(target=picohttpd, daemon=True)
@@ -357,14 +554,18 @@ def picohttpd():
time.sleep(1)
- f = http.client.HTTPConnection('localhost', 12911, timeout=10)
- f.request("GET", "/")
- response = f.getresponse()
- self.assertEqual("httpd_test_successful", response.read().decode())
+ conn = http.client.HTTPConnection('localhost', PORT, timeout=10)
+ conn.request('GET', '/')
+
+ response = conn.getresponse()
+ feedback = response.read()
+ self.assertEqual('httpd_test_successful', feedback.decode())
def test_http_elf_linux_arm(self):
+ PORT = 20021
+
def picohttpd():
- ql = Qiling(["../examples/rootfs/arm_linux/bin/picohttpd", "12912"], "../examples/rootfs/arm_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ ql = Qiling([fr'{ARM_LINUX_ROOTFS}/bin/picohttpd', f'{PORT:d}'], ARM_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
ql.run()
picohttpd_therad = threading.Thread(target=picohttpd, daemon=True)
@@ -372,14 +573,18 @@ def picohttpd():
time.sleep(1)
- f = http.client.HTTPConnection('localhost', 12912, timeout=10)
- f.request("GET", "/")
- response = f.getresponse()
- self.assertEqual("httpd_test_successful", response.read().decode())
+ conn = http.client.HTTPConnection('localhost', PORT, timeout=10)
+ conn.request('GET', '/')
+
+ response = conn.getresponse()
+ feedback = response.read()
+ self.assertEqual('httpd_test_successful', feedback.decode())
def test_http_elf_linux_armeb(self):
+ PORT = 20022
+
def picohttpd():
- ql = Qiling(["../examples/rootfs/armeb_linux/bin/picohttpd", "12913"], "../examples/rootfs/armeb_linux", multithread=True, verbose=QL_VERBOSE.DEBUG)
+ ql = Qiling([fr'{ARMEB_LINUX_ROOTFS}/bin/picohttpd', f'{PORT:d}'], ARMEB_LINUX_ROOTFS, multithread=True, verbose=QL_VERBOSE.DEBUG)
ql.run()
picohttpd_thread = threading.Thread(target=picohttpd, daemon=True)
@@ -387,19 +592,23 @@ def picohttpd():
time.sleep(1)
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
- s.connect(("localhost", 12913))
- s.sendall(b"GET / HTTP/1.1\r\nHost: 127.0.0.1:12913\r\nUser-Agent: curl/7.74.0\r\nAccept: */*\r\n\r\n")
- data = s.recv(1024)
+ # armeb libc uses statx to query stdout stats, but fails because 'stdout' is not a valid
+ # path on the hosting paltform. it prints out the "Server started" message, but stdout is
+ # not found and the message is kept buffered in.
+ #
+ # later on, picohttpd dups the client socket into stdout fd and uses ordinary printf to
+ # send data out. however, when the "successful" message is sent, it is sent along with
+ # the buffered message, which arrives first and raises a http.client.BadStatusLine exception
+ # as it reads as a malformed http response.
+ #
+ # here we use a raw 'recv' method instead of 'getresponse' to work around that.
- res = data.decode("UTF-8",'replace')
- self.assertIn("httpd_test_successful", res)
+ conn = http.client.HTTPConnection('localhost', PORT, timeout=10)
+ conn.request('GET', '/')
+
+ feedback = conn.sock.recv(96).decode()
+ self.assertTrue(feedback.endswith('httpd_test_successful'))
if __name__ == "__main__":
unittest.main()
-
-
-
-
-
diff --git a/tests/test_shellcode.py b/tests/test_shellcode.py
index 8d01e4e59..3aa0adb9c 100644
--- a/tests/test_shellcode.py
+++ b/tests/test_shellcode.py
@@ -1,97 +1,162 @@
#!/usr/bin/env python3
-#
+#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#
-import sys, unittest
-from binascii import unhexlify
+import sys
+import unittest
sys.path.append("..")
-from qiling import *
-from qiling.exception import *
-from qiling.const import QL_VERBOSE
-
-test = unhexlify('cccc')
-X86_LIN = unhexlify('31c050682f2f7368682f62696e89e3505389e1b00bcd80')
-X8664_LIN = unhexlify('31c048bbd19d9691d08c97ff48f7db53545f995257545eb03b0f05')
-MIPS32EL_LIN = unhexlify('ffff0628ffffd004ffff05280110e4270ff08424ab0f02240c0101012f62696e2f7368')
-X86_WIN = unhexlify('fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b592001d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ffe05f5f5a8b12eb8d5d6a01eb2668318b6f87ffd5bbf0b5a25668a695bd9dffd53c067c0a80fbe07505bb4713726f6a0053ffd5e8d5ffffff63616c6300')
-X8664_WIN = unhexlify('fc4881e4f0ffffffe8d0000000415141505251564831d265488b52603e488b52183e488b52203e488b72503e480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed5241513e488b52203e8b423c4801d03e8b80880000004885c0746f4801d0503e8b48183e448b40204901d0e35c48ffc93e418b34884801d64d31c94831c0ac41c1c90d4101c138e075f13e4c034c24084539d175d6583e448b40244901d0663e418b0c483e448b401c4901d03e418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a3e488b12e949ffffff5d49c7c1000000003e488d95fe0000003e4c8d850f0100004831c941ba45835607ffd54831c941baf0b5a256ffd548656c6c6f2c2066726f6d204d534621004d657373616765426f7800')
-ARM_LIN = unhexlify('01308fe213ff2fe178460e300190491a921a0827c251033701df2f62696e2f2f7368')
-ARM_THUMB = unhexlify('401c01464fea011200bf')
-ARM64_LIN = unhexlify('420002ca210080d2400080d2c81880d2010000d4e60300aa01020010020280d2681980d2010000d4410080d2420002cae00306aa080380d2010000d4210400f165ffff54e0000010420002ca210001caa81b80d2010000d4020004d27f0000012f62696e2f736800')
-X8664_FBSD = unhexlify('6a61586a025f6a015e990f054897baff02aaaa80f2ff524889e699046680c2100f05046a0f05041e4831f6990f0548976a035852488d7424f080c2100f0548b8523243427730637257488d3e48af74084831c048ffc00f055f4889d04889fe48ffceb05a0f0575f799043b48bb2f62696e2f2f73685253545f5257545e0f05')
-X8664_macos = unhexlify('4831f65648bf2f2f62696e2f7368574889e74831d24831c0b00248c1c828b03b0f05')
+from qiling import Qiling
+from qiling.const import QL_INTERCEPT, QL_VERBOSE
+
+
+# test = bytes.fromhex('cccc')
+
+X86_LIN = bytes.fromhex('31c050682f2f7368682f62696e89e3505389e1b00bcd80')
+X8664_LIN = bytes.fromhex('31c048bbd19d9691d08c97ff48f7db53545f995257545eb03b0f05')
+
+MIPS32EL_LIN = bytes.fromhex('''
+ ffff0628ffffd004ffff05280110e4270ff08424ab0f02240c0101012f62696e
+ 2f7368
+''')
+
+X86_WIN = bytes.fromhex('''
+ fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c
+ 617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b5920
+ 01d38b4918e33a498b348b01d631ffacc1cf0d01c738e075f6037df83b7d2475
+ e4588b582401d3668b0c4b8b581c01d38b048b01d0894424245b5b61595a51ff
+ e05f5f5a8b12eb8d5d6a01eb2668318b6f87ffd5bbf0b5a25668a695bd9dffd5
+ 3c067c0a80fbe07505bb4713726f6a0053ffd5e8d5ffffff63616c6300
+''')
+
+X8664_WIN = bytes.fromhex('''
+ fc4881e4f0ffffffe8d0000000415141505251564831d265488b52603e488b52
+ 183e488b52203e488b72503e480fb74a4a4d31c94831c0ac3c617c022c2041c1
+ c90d4101c1e2ed5241513e488b52203e8b423c4801d03e8b80880000004885c0
+ 746f4801d0503e8b48183e448b40204901d0e35c48ffc93e418b34884801d64d
+ 31c94831c0ac41c1c90d4101c138e075f13e4c034c24084539d175d6583e448b
+ 40244901d0663e418b0c483e448b401c4901d03e418b04884801d0415841585e
+ 595a41584159415a4883ec204152ffe05841595a3e488b12e949ffffff5d49c7
+ c1000000003e488d95fe0000003e4c8d850f0100004831c941ba45835607ffd5
+ 4831c941baf0b5a256ffd548656c6c6f2c2066726f6d204d534621004d657373
+ 616765426f7800
+''')
+
+ARM_LIN = bytes.fromhex('''
+ 01308fe213ff2fe178460e300190491a921a0827c251033701df2f62696e2f2f
+ 7368
+''')
+
+ARM_THUMB = bytes.fromhex('401c01464fea011200bf')
+
+ARM64_LIN = bytes.fromhex('''
+ 420002ca210080d2400080d2c81880d2010000d4e60300aa01020010020280d2
+ 681980d2010000d4410080d2420002cae00306aa080380d2010000d4210400f1
+ 65ffff54e0000010420002ca210001caa81b80d2010000d4020004d27f000001
+ 2f62696e2f736800
+''')
+
+X8664_FBSD = bytes.fromhex('''
+ 6a61586a025f6a015e990f054897baff02aaaa80f2ff524889e699046680c210
+ 0f05046a0f05041e4831f6990f0548976a035852488d7424f080c2100f0548b8
+ 523243427730637257488d3e48af74084831c048ffc00f055f4889d04889fe48
+ ffceb05a0f0575f799043b48bb2f62696e2f2f73685253545f5257545e0f05
+''')
+
+X8664_MACOS = bytes.fromhex('''
+ 4831f65648bf2f2f62696e2f7368574889e74831d24831c0b00248c1c828b03b
+ 0f05
+''')
+
+
+# some shellcodes call execve, which under normal circumstences, does not return.
+# however, those shellcodes attempt to run a non-exsting '/bin/sh' binary and do
+# not bother to handle failures gracefully.
+#
+# the execution then continues to the next bytes, which are usually the '/bin/sh'
+# string and not valid code. that causes Qiling to raise an exception, and this is
+# why we need a way to thwart those execve failures and end the emulation gracefully
+def graceful_execve(ql: Qiling, pathname: int, argv: int, envp: int, retval: int):
+ assert retval != 0, f'execve is not expected to return on success'
+
+ vpath = ql.os.utils.read_cstring(pathname)
+
+ ql.log.debug(f'failed to call execve("{vpath}"), ending emulation gracefully')
+ ql.stop()
+
class TestShellcode(unittest.TestCase):
def test_linux_x86(self):
print("Linux X86 32bit Shellcode")
- ql = Qiling(code = X86_LIN, archtype = "x86", ostype = "linux", verbose=QL_VERBOSE.OFF)
+ ql = Qiling(code=X86_LIN, archtype="x86", ostype="linux", verbose=QL_VERBOSE.OFF)
ql.run()
def test_linux_x64(self):
print("Linux X86 64bit Shellcode")
- ql = Qiling(code = X8664_LIN, archtype = "x8664", ostype = "linux", verbose=QL_VERBOSE.OFF)
+ ql = Qiling(code=X8664_LIN, archtype="x8664", ostype="linux", verbose=QL_VERBOSE.OFF)
ql.run()
def test_linux_mips32(self):
print("Linux MIPS 32bit EL Shellcode")
- ql = Qiling(code = MIPS32EL_LIN, archtype = "mips", ostype = "linux", verbose=QL_VERBOSE.OFF)
+ ql = Qiling(code=MIPS32EL_LIN, archtype="mips", ostype="linux", verbose=QL_VERBOSE.OFF)
+
+ ql.os.set_syscall('execve', graceful_execve, QL_INTERCEPT.EXIT)
ql.run()
- #This shellcode needs to be changed to something non-blocking
+ # This shellcode needs to be changed to something non-blocking
def test_linux_arm(self):
- print("Linux ARM 32bit Shellcode")
- ql = Qiling(code = ARM_LIN, archtype = "arm", ostype = "linux", verbose=QL_VERBOSE.OFF)
- ql.run()
-
+ print("Linux ARM 32bit Shellcode")
+ ql = Qiling(code=ARM_LIN, archtype="arm", ostype="linux", verbose=QL_VERBOSE.OFF)
+ ql.run()
def test_linux_arm_thumb(self):
print("Linux ARM Thumb Shllcode")
- ql = Qiling(code = ARM_THUMB, archtype = "arm", ostype = "linux", verbose=QL_VERBOSE.OFF, thumb = True)
+ ql = Qiling(code=ARM_THUMB, archtype="arm", ostype="linux", verbose=QL_VERBOSE.OFF, thumb=True)
ql.run()
-
def test_linux_arm64(self):
print("Linux ARM 64bit Shellcode")
- ql = Qiling(code = ARM64_LIN, archtype = "arm64", ostype = "linux", verbose=QL_VERBOSE.OFF)
+ ql = Qiling(code=ARM64_LIN, archtype="arm64", ostype="linux", verbose=QL_VERBOSE.OFF)
+
+ ql.os.set_syscall('execve', graceful_execve, QL_INTERCEPT.EXIT)
ql.run()
# #This shellcode needs to be changed to something simpler not requiring rootfs
# def test_windows_x86(self):
# print("Windows X86 32bit Shellcode")
- # ql = Qiling(code = X86_WIN, archtype = "x86", ostype = "windows", rootfs="../examples/rootfs/x86_reactos", verbose=QL_VERBOSE.OFF)
+ # ql = Qiling(code=X86_WIN, archtype="x86", ostype="windows", rootfs="../examples/rootfs/x86_reactos", verbose=QL_VERBOSE.OFF)
# ql.run()
# #This shellcode needs to be changed to something simpler not requiring rootfs
# def test_windows_x64(self):
# print("\nWindows X8664 64bit Shellcode")
- # ql = Qiling(code = X8664_WIN, archtype = "x8664", ostype = "windows", rootfs="../examples/rootfs/x86_reactos", verbose=QL_VERBOSE.OFF)
+ # ql = Qiling(code=X8664_WIN, archtype="x8664", ostype="windows", rootfs="../examples/rootfs/x86_reactos", verbose=QL_VERBOSE.OFF)
# ql.run()
- #This shellcode needs to be changed to something simpler, listen is blocking
- #def test_freebsd_x64(self):
+ # #This shellcode needs to be changed to something simpler, listen is blocking
+ # def test_freebsd_x64(self):
# print("FreeBSD X86 64bit Shellcode")
- # ql = Qiling(code = X8664_FBSD, archtype = "x8664", ostype = "freebsd", verbose=QL_VERBOSE.OFF)
+ # ql = Qiling(code=X8664_FBSD, archtype="x8664", ostype="freebsd", verbose=QL_VERBOSE.OFF)
# ql.run()
# def test_macos_x64(self):
# print("macos X86 64bit Shellcode")
- # ql = Qiling(code = X8664_macos, archtype = "x8664", ostype = "macos", verbose=QL_VERBOSE.OFF)
+ # ql = Qiling(code=X8664_macos, archtype="x8664", ostype="macos", verbose=QL_VERBOSE.OFF)
# ql.run()
# def test_invalid_os(self):
# print("Testing Unknown OS")
- # self.assertRaises(QlErrorOsType, Qiling, code = test, archtype = "arm64", ostype = "qilingos", verbose=QL_VERBOSE.DEFAULT )
+ # self.assertRaises(QlErrorOsType, Qiling, code=test, archtype="arm64", ostype="qilingos", verbose=QL_VERBOSE.DEFAULT )
# def test_invalid_arch(self):
# print("Testing Unknown Arch")
- # self.assertRaises(QlErrorArch, Qiling, code = test, archtype = "qilingarch", ostype = "linux", verbose=QL_VERBOSE.DEFAULT )
+ # self.assertRaises(QlErrorArch, Qiling, code=test, archtype="qilingarch", ostype="linux", verbose=QL_VERBOSE.DEFAULT )
# def test_invalid_output(self):
# print("Testing Invalid output")
- # self.assertRaises(QlErrorOutput, Qiling, code = test, archtype = "arm64", ostype = "linux", verbose=QL_VERBOSE.DEFAULT )
-
+ # self.assertRaises(QlErrorOutput, Qiling, code=test, archtype="arm64", ostype="linux", verbose=QL_VERBOSE.DEFAULT )
+
if __name__ == "__main__":
unittest.main()