Merge branch 'qilingframework:dev' into dev

This commit is contained in:
xwings
2026-09-13 20:07:33 +08:00
committed by GitHub
5 changed files with 121 additions and 7 deletions
+9
View File
@@ -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,
+26 -3
View File
@@ -12,6 +12,8 @@ from qiling import Qiling
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.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
@@ -56,7 +58,8 @@ class QlOsLinux(QlOsPosix):
# 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,
@@ -66,13 +69,15 @@ class QlOsLinux(QlOsPosix):
# 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
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 +142,24 @@ class QlOsLinux(QlOsPosix):
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', # ARM / ARM64 undefined instruction
MIPS_EXCP.RI: 'SIGILL', # MIPS reserved (illegal) instruction
}.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)
+32 -4
View File
@@ -1413,17 +1413,45 @@ class Statx64(ctypes.Structure):
_pack_ = 4
# Big-endian counterparts of the statx structs. The kernel statx layout is the
# same on every architecture, so the big-endian variants reuse the little-endian
# field lists verbatim and only change the ctypes base class (and the nested
# timestamp type, which must itself be big-endian). Without these, statx() byte-
# swaps every field on a big-endian guest (e.g. MIPS/MIPS64 EB), so stx_mode
# loses its S_IFDIR bit and tools like `ls` treat directories as plain files.
class StatxTimestamp32EB(ctypes.BigEndianStructure):
_fields_ = StatxTimestamp32._fields_
class StatxTimestamp64EB(ctypes.BigEndianStructure):
_fields_ = StatxTimestamp64._fields_
def _statx_fields_eb(fields):
swap = {StatxTimestamp32: StatxTimestamp32EB, StatxTimestamp64: StatxTimestamp64EB}
return [(name, swap.get(ftype, ftype)) for (name, ftype) in fields]
class Statx32EB(ctypes.BigEndianStructure):
_fields_ = _statx_fields_eb(Statx32._fields_)
_pack_ = 8
class Statx64EB(ctypes.BigEndianStructure):
_fields_ = _statx_fields_eb(Statx64._fields_)
_pack_ = 4
# int statx(int dirfd, const char *restrict pathname, int flags,
# unsigned int mask, struct statx *restrict statxbuf);
def ql_syscall_statx(ql: Qiling, dirfd: int, path: int, flags: int, mask: int, buf_ptr: int):
is_eb = ql.arch.endian == QL_ENDIAN.EB
def statx_convert_timestamp(tv_sec, tv_nsec):
tv_sec = struct.unpack('i', struct.pack('f', tv_sec))[0]
tv_nsec = struct.unpack('i', struct.pack('f', tv_nsec))[0]
if ql.arch.bits == 32:
return StatxTimestamp32(tv_sec=tv_sec, tv_nsec=tv_nsec)
Timestamp = StatxTimestamp32EB if is_eb else StatxTimestamp32
else:
return StatxTimestamp64(tv_sec=tv_sec, tv_nsec=tv_nsec)
Timestamp = StatxTimestamp64EB if is_eb else StatxTimestamp64
return Timestamp(tv_sec=tv_sec, tv_nsec=tv_nsec)
def major(dev):
@@ -1438,9 +1466,9 @@ def ql_syscall_statx(ql: Qiling, dirfd: int, path: int, flags: int, mask: int, b
st = Stat(real_path, fd)
if ql.arch.bits == 32:
Statx = Statx32
Statx = Statx32EB if is_eb else Statx32
else:
Statx = Statx64
Statx = Statx64EB if is_eb else Statx64
stx = Statx(
stx_mask = 0x07ff, # STATX_BASIC_STATS
+31
View File
@@ -836,6 +836,37 @@ class ELFTest(unittest.TestCase):
del ql
# Regression for statx() byte order on big-endian guests: the statx struct
# must be serialized in the guest's endianness. Otherwise stx_mode is byte-
# swapped and a directory loses its S_IFDIR bit (so e.g. `ls` treats a
# directory as a regular file). Exercised on a big-endian MIPS context.
def test_linux_statx_bigendian(self):
import stat as _stat
from qiling.const import QL_ENDIAN
from qiling.os.posix.syscall.stat import ql_syscall_statx, Statx32
ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS, ostype=QL_OS.LINUX,
endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips32_linux",
verbose=QL_VERBOSE.OFF)
base = 0x100000
ql.mem.map(base, 0x1000)
path_ptr, buf_ptr = base, base + 0x200
ql.mem.write(path_ptr, b"/\x00")
AT_FDCWD = -100 & 0xffffffff
STATX_BASIC_STATS = 0x07ff
ret = ql_syscall_statx(ql, AT_FDCWD, path_ptr, 0, STATX_BASIC_STATS, buf_ptr)
self.assertEqual(ret, 0)
# the guest is big-endian, so it reads stx_mode in big-endian byte order;
# it must come back as a directory (S_IFDIR). Before the fix the struct
# was emitted little-endian and the type bits were lost.
mode = int.from_bytes(ql.mem.read(buf_ptr + Statx32.stx_mode.offset, 2), 'big')
self.assertTrue(_stat.S_ISDIR(mode))
del ql
def test_memory_search(self):
ql = Qiling(code=b"\xCC", archtype=QL_ARCH.X8664, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.DEBUG)
+23
View File
@@ -123,6 +123,29 @@ class TestShellcode(unittest.TestCase):
ql.os.set_syscall('execve', graceful_execve, QL_INTERCEPT.EXIT)
ql.run()
# the tests above end the emulation from the execve hook, so they never reach
# the shellcode's trailing '/bin/sh' string. the two below deliberately let
# execve return and fall through into it: those bytes are not valid code, so
# the cpu raises an undefined/reserved instruction exception. the kernel would
# deliver a fatal SIGILL and terminate the process, and qiling must do the
# same. before this fix nothing hooked those exceptions, so the unhandled
# interrupt was turned into QlErrorCoreHook and ql.run() blew up.
def _run_past_execve(self, code: bytes, archtype: QL_ARCH):
def returning_execve(ql: Qiling, pathname: int, argv: int, envp: int, retval: int):
assert retval != 0, 'execve is not expected to return on success'
ql = Qiling(code=code, archtype=archtype, ostype=QL_OS.LINUX, verbose=QL_VERBOSE.OFF)
ql.os.set_syscall('execve', returning_execve, QL_INTERCEPT.EXIT)
ql.run()
def test_linux_arm64_illegal_instruction(self):
print("Linux ARM 64bit illegal instruction")
self._run_past_execve(ARM64_LIN, QL_ARCH.ARM64)
def test_linux_mips32_illegal_instruction(self):
print("Linux MIPS 32bit EL illegal instruction")
self._run_past_execve(MIPS32EL_LIN, QL_ARCH.MIPS)
# #This shellcode needs to be changed to something simpler not requiring rootfs
# def test_windows_x86(self):
# print("Windows X86 32bit Shellcode")