From e892d0207a664383a4f4d878d5c2d5fa345cc0d3 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Sun, 21 Jun 2026 19:45:36 +1000 Subject: [PATCH 1/4] Handle ARM/ARM64 Linux undefined-instruction exception ARM and ARM64 Linux only hooked the svc syscall trap (exception 2), so any other CPU exception fell through to the unhandled-interrupt dispatcher and raised QlErrorCoreHook, crashing the framework. This surfaces in examples/shellcode_run.py: since execve was patched to return -1 for paths outside the rootfs, rootfs-less shellcode no longer stops at execve and instead runs into its trailing data. On ARM64 that data decodes as an undefined instruction (exception 1), which a real kernel would deliver as SIGILL and terminate the process. Hook EXCP.UDEF and emulate that termination by stopping cleanly, and replace the magic syscall-trap number with the named EXCP.SWI constant. Co-Authored-By: Claude Opus 4.8 (1M context) --- qiling/os/linux/linux.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/qiling/os/linux/linux.py b/qiling/os/linux/linux.py index 0313218d6..8e53868a3 100644 --- a/qiling/os/linux/linux.py +++ b/qiling/os/linux/linux.py @@ -12,6 +12,7 @@ from qiling.arch.x86_const import GS_SEGMENT_ADDR, GS_SEGMENT_SIZE from qiling.arch.x86_utils import GDTManager, SegmentManager86, SegmentManager64 from qiling.arch import arm_utils +from qiling.arch.cortex_m_const import EXCP from qiling.cc import QlCC, intel, arm, mips, riscv, ppc from qiling.const import QL_ARCH, QL_OS from qiling.os.fcall import QlFunctionCall @@ -56,7 +57,8 @@ def load(self): # ARM if self.ql.arch.type == QL_ARCH.ARM: self.ql.arch.enable_vfp() - self.ql.hook_intno(self.hook_syscall, 2) + self.ql.hook_intno(self.hook_syscall, EXCP.SWI) + self.ql.hook_intno(self.hook_cpu_exception, EXCP.UDEF) self.thread_class = thread.QlLinuxARMThread arm_utils.init_linux_traps(self.ql, { 'memory_barrier': 0xffff0fa0, @@ -72,7 +74,8 @@ def load(self): # ARM64 elif self.ql.arch.type == QL_ARCH.ARM64: self.ql.arch.enable_vfp() - self.ql.hook_intno(self.hook_syscall, 2) + self.ql.hook_intno(self.hook_syscall, EXCP.SWI) + self.ql.hook_intno(self.hook_cpu_exception, EXCP.UDEF) self.thread_class = thread.QlLinuxARM64Thread # X86 @@ -137,6 +140,23 @@ def setup_procfs(self): def hook_syscall(self, ql, intno = None): return self.load_syscall() + def hook_cpu_exception(self, ql, intno = None): + # A cpu exception the kernel would turn into a fatal signal that + # terminates the process (e.g. SIGILL on an undefined instruction). + # Emulate that termination by stopping cleanly instead of letting the + # unhandled-interrupt dispatcher raise QlErrorCoreHook. This commonly + # happens with shellcode that falls through into trailing data once a + # terminal syscall (e.g. a denied execve) returns instead of replacing + # the image. + signame = { + EXCP.UDEF: 'SIGILL', + }.get(intno, f'exception {intno:#x}') + + pc = ql.arch.regs.arch_pc + + ql.log.debug(f'CPU raised {signame} at {pc:#x}; terminating emulated process') + ql.stop() + def register_function_after_load(self, function): if function not in self.function_after_load_list: self.function_after_load_list.append(function) From d0c7b6605661b550ea90d49e041dd445526fe32d Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Sun, 21 Jun 2026 19:48:37 +1000 Subject: [PATCH 2/4] Handle MIPS Linux reserved-instruction exception Same class of crash as the ARM/ARM64 fix: MIPS Linux only hooked the syscall exception, so any other CPU exception reached the unhandled-interrupt dispatcher and raised QlErrorCoreHook. In examples/shellcode_run.py the rootfs-less MIPS shellcode runs past its denied execve into the trailing "/bin/sh" string, which decodes as a reserved instruction (exception 20) -- delivered as SIGILL on a real kernel. Hook the reserved-instruction exception and route it through the existing hook_cpu_exception handler. Add a small EXCP enum to mips_const so the syscall and reserved-instruction codes are named rather than magic numbers. Co-Authored-By: Claude Opus 4.8 (1M context) --- qiling/arch/mips_const.py | 9 +++++++++ qiling/os/linux/linux.py | 7 +++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/qiling/arch/mips_const.py b/qiling/arch/mips_const.py index c7f1a5722..a56c7e0b4 100644 --- a/qiling/arch/mips_const.py +++ b/qiling/arch/mips_const.py @@ -3,8 +3,17 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +from enum import IntEnum + from unicorn.mips_const import * + +class EXCP(IntEnum): + # subset of QEMU's MIPS exception codes, as reported to unicorn interrupt hooks + SYSCALL = 17 # system call + BREAK = 18 # breakpoint + RI = 20 # reserved (illegal) instruction + reg_map = { "r0": UC_MIPS_REG_0, "r1": UC_MIPS_REG_1, diff --git a/qiling/os/linux/linux.py b/qiling/os/linux/linux.py index 8e53868a3..cbba81fb1 100644 --- a/qiling/os/linux/linux.py +++ b/qiling/os/linux/linux.py @@ -13,6 +13,7 @@ from qiling.arch.x86_utils import GDTManager, SegmentManager86, SegmentManager64 from qiling.arch import arm_utils from qiling.arch.cortex_m_const import EXCP +from qiling.arch.mips_const import EXCP as MIPS_EXCP from qiling.cc import QlCC, intel, arm, mips, riscv, ppc from qiling.const import QL_ARCH, QL_OS from qiling.os.fcall import QlFunctionCall @@ -68,7 +69,8 @@ def load(self): # MIPS32 elif self.ql.arch.type == QL_ARCH.MIPS: - self.ql.hook_intno(self.hook_syscall, 17) + self.ql.hook_intno(self.hook_syscall, MIPS_EXCP.SYSCALL) + self.ql.hook_intno(self.hook_cpu_exception, MIPS_EXCP.RI) self.thread_class = thread.QlLinuxMIPS32Thread # ARM64 @@ -149,7 +151,8 @@ def hook_cpu_exception(self, ql, intno = None): # terminal syscall (e.g. a denied execve) returns instead of replacing # the image. signame = { - EXCP.UDEF: 'SIGILL', + EXCP.UDEF: 'SIGILL', # ARM / ARM64 undefined instruction + MIPS_EXCP.RI: 'SIGILL', # MIPS reserved (illegal) instruction }.get(intno, f'exception {intno:#x}') pc = ql.arch.regs.arch_pc From 46f2d4798d0eb667255d74cc81f7e32cd3811d95 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Sun, 21 Jun 2026 19:55:24 +1000 Subject: [PATCH 3/4] Skip shellcode_run.py Windows stages when system DLLs are absent The Windows shellcode stages need genuine Windows system DLLs, which are not redistributable and so are not shipped with Qiling; they must be collected from a licensed Windows host via examples/scripts/dllscollector.bat. Without them the loader failed to map ntdll.dll and the example crashed with an unhandled UC_ERR_READ_UNMAPPED traceback. Probe for ntdll.dll under each Windows rootfs and skip those stages with a message pointing at the collector script, so the example runs to completion on a stock checkout. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/shellcode_run.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/examples/shellcode_run.py b/examples/shellcode_run.py index c7e189696..1507fdab6 100644 --- a/examples/shellcode_run.py +++ b/examples/shellcode_run.py @@ -3,12 +3,23 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +import os import sys sys.path.append("..") from qiling import Qiling from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE + +def windows_rootfs_ready(rootfs: str) -> bool: + # The Windows examples need genuine Windows system DLLs, which cannot be + # redistributed and are therefore not shipped with Qiling. They must be + # collected from a licensed Windows host using the helper script at + # examples/scripts/dllscollector.bat. Probe for ntdll.dll so we can skip + # these stages with a helpful message instead of crashing on an unmapped + # read when the DLLs are absent. + return os.path.isfile(os.path.join(rootfs, 'Windows', 'System32', 'ntdll.dll')) + X86_LIN = bytes.fromhex('31c050682f2f7368682f62696e89e3505389e1b00bcd80') X8664_LIN = bytes.fromhex('31c048bbd19d9691d08c97ff48f7db53545f995257545eb03b0f05') @@ -87,12 +98,20 @@ ql.run() print("\nWindows x86 Shellcode") - ql = Qiling(code=X86_WIN, archtype=QL_ARCH.X86, ostype=QL_OS.WINDOWS, rootfs=r'rootfs/x86_windows') - ql.run() + if windows_rootfs_ready(r'rootfs/x86_windows'): + ql = Qiling(code=X86_WIN, archtype=QL_ARCH.X86, ostype=QL_OS.WINDOWS, rootfs=r'rootfs/x86_windows') + ql.run() + else: + print(" [skipped] Windows system DLLs not found under rootfs/x86_windows/Windows/System32.") + print(" Collect them from a licensed Windows host with examples/scripts/dllscollector.bat.") print("\nWindows x86-64 Shellcode") - ql = Qiling(code=X8664_WIN, archtype=QL_ARCH.X8664, ostype=QL_OS.WINDOWS, rootfs=r'rootfs/x8664_windows') - ql.run() + if windows_rootfs_ready(r'rootfs/x8664_windows'): + ql = Qiling(code=X8664_WIN, archtype=QL_ARCH.X8664, ostype=QL_OS.WINDOWS, rootfs=r'rootfs/x8664_windows') + ql.run() + else: + print(" [skipped] Windows system DLLs not found under rootfs/x8664_windows/Windows/System32.") + print(" Collect them from a licensed Windows host with examples/scripts/dllscollector.bat.") # FIXME: freebsd sockets are currently broken. # From 47f13767e375a4b6033c0365dc8aba4b4073c7e2 Mon Sep 17 00:00:00 2001 From: retrocpugeek Date: Sun, 21 Jun 2026 20:02:16 +1000 Subject: [PATCH 4/4] Add MIPS32 big-endian shellcode test test_shellcode.py only exercised little-endian MIPS. Add a big-endian counterpart (MIPS32EB_LIN) so MIPS BE emulation is covered too. The shellcode is the byte-swapped form of MIPS32EL_LIN (instruction words reversed, '/bin/sh' string left intact) and is run with endian=QL_ENDIAN.EB, reusing the existing graceful_execve EXIT hook to end emulation cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/test_shellcode.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/test_shellcode.py b/tests/test_shellcode.py index 9b2b4e054..34428f1cf 100644 --- a/tests/test_shellcode.py +++ b/tests/test_shellcode.py @@ -9,7 +9,7 @@ sys.path.append("..") from qiling import Qiling -from qiling.const import QL_ARCH, QL_OS, QL_INTERCEPT, QL_VERBOSE +from qiling.const import QL_ARCH, QL_OS, QL_ENDIAN, QL_INTERCEPT, QL_VERBOSE # test = bytes.fromhex('cccc') @@ -22,6 +22,13 @@ 2f7368 ''') +# big-endian counterpart of MIPS32EL_LIN: the instruction words are byte-swapped +# while the trailing '/bin/sh' string is left as-is +MIPS32EB_LIN = bytes.fromhex(''' + 2806ffff04d0ffff2805ffff27e410012484f00f24020fab0101010c2f62696e + 2f7368 +''') + X86_WIN = bytes.fromhex(''' fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c 617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b5920 @@ -105,6 +112,13 @@ def test_linux_mips32(self): ql.os.set_syscall('execve', graceful_execve, QL_INTERCEPT.EXIT) ql.run() + def test_linux_mips32eb(self): + print("Linux MIPS 32bit EB Shellcode") + ql = Qiling(code=MIPS32EB_LIN, archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX, endian=QL_ENDIAN.EB, 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 def test_linux_arm(self): print("Linux ARM 32bit Shellcode")