diff --git a/examples/hello_mips64_linux_customapi.py b/examples/hello_mips64_linux_customapi.py new file mode 100644 index 000000000..68f1a7d93 --- /dev/null +++ b/examples/hello_mips64_linux_customapi.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +# NOTE: this runs a dynamically-linked glibc MIPS64 (n64) binary, which links +# above the 2GB useg and uses ll/sc locks. It requires the MIPS64 virtual-TLB +# fixes in unicorn (https://github.com/unicorn-engine/unicorn issues #2272 / +# the ll-AdEL fix); on an unfixed unicorn it raises a spurious RI/AdEL. + +import sys +sys.path.append("..") + +from qiling import Qiling +from qiling.os.const import STRING +from qiling.const import QL_VERBOSE + +def my_puts(ql: Qiling): + params = ql.os.resolve_fcall_params({'s': STRING}) + + print(f'puts("{params["s"]}")') + +if __name__ == "__main__": + ql = Qiling(["rootfs/mips64_linux/bin/mips64_hello"], "rootfs/mips64_linux", verbose=QL_VERBOSE.DEBUG) + ql.os.set_api("puts", my_puts) + ql.run() diff --git a/examples/hello_mips64el_linux_customapi.py b/examples/hello_mips64el_linux_customapi.py new file mode 100644 index 000000000..7eb01ff72 --- /dev/null +++ b/examples/hello_mips64el_linux_customapi.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +# NOTE: this runs a dynamically-linked glibc MIPS64 (n64, little-endian) binary, +# which links above the 2GB useg and uses ll/sc locks. It requires the MIPS64 +# virtual-TLB fixes in unicorn (https://github.com/unicorn-engine/unicorn issues +# #2272 / the ll-AdEL fix); on an unfixed unicorn it raises a spurious RI/AdEL. + +import sys +sys.path.append("..") + +from qiling import Qiling +from qiling.os.const import STRING +from qiling.const import QL_VERBOSE + +def my_puts(ql: Qiling): + params = ql.os.resolve_fcall_params({'s': STRING}) + + print(f'puts("{params["s"]}")') + +if __name__ == "__main__": + ql = Qiling(["rootfs/mips64el_linux/bin/mips64el_hello"], "rootfs/mips64el_linux", verbose=QL_VERBOSE.DEBUG) + ql.os.set_api("puts", my_puts) + ql.run() diff --git a/examples/multithreading_mips64_linux.py b/examples/multithreading_mips64_linux.py new file mode 100644 index 000000000..ad7510717 --- /dev/null +++ b/examples/multithreading_mips64_linux.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +import sys +sys.path.append("..") + +from qiling import Qiling +from qiling.const import QL_VERBOSE + +def my_sandbox(path, rootfs): + ql = Qiling(path, rootfs, verbose=QL_VERBOSE.DEBUG, multithread=True) + ql.run() + +if __name__ == "__main__": + my_sandbox(["rootfs/mips64_linux/bin/mips64_multithreading"], "rootfs/mips64_linux") diff --git a/examples/rootfs b/examples/rootfs index df3fa4dfc..cdc219d15 160000 --- a/examples/rootfs +++ b/examples/rootfs @@ -1 +1 @@ -Subproject commit df3fa4dfc0b9d4164f8678699d8923df847eb3d2 +Subproject commit cdc219d156608fa52be58d54b79396cb018f40d8 diff --git a/examples/shellcode_mips64_run.py b/examples/shellcode_mips64_run.py new file mode 100644 index 000000000..5435545d1 --- /dev/null +++ b/examples/shellcode_mips64_run.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +import sys + +sys.path.append("..") +from qiling import Qiling +from qiling.const import QL_ARCH, QL_OS, QL_ENDIAN, QL_VERBOSE + +# MIPS64 n64 shellcode: write(1, "MIPS64 hello\n", 13) then exit_group(0). +# +# it uses the n64 syscall numbers (write=5001, exit_group=5205) and computes the +# address of the message with a bal/daddiu pc-relative trick. assembled with +# binutils mips64-linux-gnuabi64-as: +# +# .set noreorder +# __start: +# li $v0, 5001 # __NR_write +# li $a0, 1 # fd = stdout +# bal load +# nop +# load: +# daddiu $a1, $ra, (msg - load) # a1 = &msg +# li $a2, 13 # len +# syscall +# li $v0, 5205 # __NR_exit_group +# li $a0, 0 +# syscall +# msg: +# .ascii "MIPS64 hello\n" +MIPS64EB_LIN = bytes.fromhex(''' + 2402138924040001041100010000000067e500182406000d0000000c24021455 + 240400000000000c4d49505336342068656c6c6f0a +''') + +# little-endian counterpart of MIPS64EB_LIN: the instruction words are +# byte-swapped while the trailing string is left as-is +MIPS64EL_LIN = bytes.fromhex(''' + 891302240100042401001104000000001800e5670d0006240c00000055140224 + 000004240c0000004d49505336342068656c6c6f0a +''') + + +if __name__ == "__main__": + print("\nLinux MIPS 64bit EB (big-endian) Shellcode") + ql = Qiling(code=MIPS64EB_LIN, archtype=QL_ARCH.MIPS64, ostype=QL_OS.LINUX, endian=QL_ENDIAN.EB, verbose=QL_VERBOSE.DEFAULT) + ql.run() + + print("\nLinux MIPS 64bit EL (little-endian) Shellcode") + ql = Qiling(code=MIPS64EL_LIN, archtype=QL_ARCH.MIPS64, ostype=QL_OS.LINUX, endian=QL_ENDIAN.EL, verbose=QL_VERBOSE.DEFAULT) + ql.run() diff --git a/examples/src/linux/multithreading.c b/examples/src/linux/multithreading.c index c38af5dc8..4ceff48a2 100644 --- a/examples/src/linux/multithreading.c +++ b/examples/src/linux/multithreading.c @@ -28,6 +28,9 @@ void thread_2(void) { int main(void) { pthread_t id_1, id_2; int ret; + /* pthread_join writes a void* (8 bytes on 64-bit); using an int* here makes + the store unaligned on strict-alignment 64-bit targets (e.g. MIPS64) */ + void *retval; /*Create pthread 1*/ ret=pthread_create(&id_1, NULL, (void *) thread_1, NULL); @@ -44,9 +47,9 @@ int main(void) { } /*wait thread ending*/ - pthread_join(id_1, &ret); - printf("thread 1 ret val is : %d\n", ret); - pthread_join(id_2, &ret); - printf("thread 2 ret val is : %d\n", ret); + pthread_join(id_1, &retval); + printf("thread 1 ret val is : %d\n", (int)(long)retval); + pthread_join(id_2, &retval); + printf("thread 2 ret val is : %d\n", (int)(long)retval); return 0; } \ No newline at end of file diff --git a/examples/src/linux/statx.c b/examples/src/linux/statx.c new file mode 100644 index 000000000..e72144071 --- /dev/null +++ b/examples/src/linux/statx.c @@ -0,0 +1,36 @@ +// Regression test for statx struct serialization (esp. byte order on +// big-endian guests). statx() fills a `struct statx` whose stx_mode must carry +// the S_IFDIR bit for a directory; if the struct is emitted with the wrong +// endianness the type bits are lost and the directory looks like a plain file. +// +// Build (big-endian ARM, static so it needs no rootfs loader): +// armeb-linux-gnueabi-gcc -O2 -static -o armeb_statx statx.c +// +// Prints "DIR" when statx correctly reports the path as a directory. +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + const char *path = (argc > 1) ? argv[1] : "/"; + struct statx stx; + + memset(&stx, 0, sizeof(stx)); + + if (syscall(SYS_statx, AT_FDCWD, path, 0, STATX_BASIC_STATS, &stx) != 0) { + perror("statx"); + return 1; + } + + if (S_ISDIR(stx.stx_mode)) + printf("DIR\n"); + else + printf("NOTDIR mode=%o\n", (unsigned) stx.stx_mode); + + return 0; +} diff --git a/qiling/arch/mips64.py b/qiling/arch/mips64.py new file mode 100644 index 000000000..1ed716838 --- /dev/null +++ b/qiling/arch/mips64.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +from functools import cached_property +from typing import Optional + +from unicorn import ( + Uc, UC_ARCH_MIPS, UC_MODE_MIPS64, UC_MODE_BIG_ENDIAN, UC_MODE_LITTLE_ENDIAN, + UC_HOOK_TLB_FILL, UC_TLB_VIRTUAL, UC_PROT_ALL +) +from capstone import Cs, CS_ARCH_MIPS, CS_MODE_MIPS64, CS_MODE_BIG_ENDIAN, CS_MODE_LITTLE_ENDIAN +from keystone import Ks, KS_ARCH_MIPS, KS_MODE_MIPS64, KS_MODE_BIG_ENDIAN, KS_MODE_LITTLE_ENDIAN + +from qiling import Qiling +from qiling.arch.arch import QlArch +from qiling.arch import mips_const +from qiling.arch.models import MIPS64_CPU_MODEL +from qiling.arch.register import QlRegisterManager +from qiling.const import QL_ARCH, QL_ENDIAN + + +class QlArchMIPS64(QlArch): + type = QL_ARCH.MIPS64 + bits = 64 + + def __init__(self, ql: Qiling, *, cputype: Optional[MIPS64_CPU_MODEL], endian: QL_ENDIAN): + # unicorn's default MIPS64 core is an old MIPS III (R4000-class) that lacks + # MIPS IV / MIPS64 instructions such as movn/movz, which glibc emits freely; + # executing them traps as a reserved instruction. default to a generic + # MIPS64 Release 2 core (what mips64/mips64r2 toolchains target) so the full + # ISA is available. + if cputype is None: + cputype = MIPS64_CPU_MODEL.MIPS64_MIPS64R2_GENERIC + + super().__init__(ql, cputype=cputype) + + self._init_endian = endian + + @cached_property + def uc(self) -> Uc: + endian = { + QL_ENDIAN.EB: UC_MODE_BIG_ENDIAN, + QL_ENDIAN.EL: UC_MODE_LITTLE_ENDIAN + }[self.endian] + + uc = Uc(UC_ARCH_MIPS, UC_MODE_MIPS64 + endian) + + if self.cpu is not None: + uc.ctl_set_cpu_model(self.cpu.value) + + # unicorn's MIPS64 CPU-MMU only executes within the low 2GB (useg); any + # access at or above 0x80000000 raises an address exception. MIPS64 ELFs + # link at 0x120000000 and Qiling lays the stack/mmap out well above 4GB, + # so we switch to unicorn's virtual-TLB mode and supply an identity + # mapping (paddr == vaddr). that bypasses the CPU-MMU segment checks while + # keeping virtual == physical, so the rest of Qiling's flat memory model + # (mem.map/read/write, loader, syscall pointer reads) keeps working as-is. + uc.ctl_set_tlb_mode(UC_TLB_VIRTUAL) + uc.hook_add(UC_HOOK_TLB_FILL, self.__tlb_fill_identity) + + return uc + + @staticmethod + def __tlb_fill_identity(uc: Uc, vaddr: int, access: int, entry, user_data) -> bool: + # identity-map every page to itself (paddr == vaddr); accesses to pages + # that are not actually backed still fault, since there is no physical + # memory behind them + entry.paddr = vaddr & ~0xfff + entry.perms = UC_PROT_ALL + + return True + + @cached_property + def regs(self) -> QlRegisterManager: + # the register names are shared with MIPS32; unicorn widens them to + # 64 bits under UC_MODE_MIPS64 + regs_map = dict( + **mips_const.reg_map + ) + + pc_reg = 'pc' + sp_reg = 'sp' + + return QlRegisterManager(self.uc, regs_map, pc_reg, sp_reg) + + @cached_property + def disassembler(self) -> Cs: + endian = { + QL_ENDIAN.EL: CS_MODE_LITTLE_ENDIAN, + QL_ENDIAN.EB: CS_MODE_BIG_ENDIAN + }[self.endian] + + return Cs(CS_ARCH_MIPS, CS_MODE_MIPS64 + endian) + + @cached_property + def assembler(self) -> Ks: + endian = { + QL_ENDIAN.EL: KS_MODE_LITTLE_ENDIAN, + QL_ENDIAN.EB: KS_MODE_BIG_ENDIAN + }[self.endian] + + return Ks(KS_ARCH_MIPS, KS_MODE_MIPS64 + endian) + + @property + def endian(self) -> QL_ENDIAN: + return self._init_endian diff --git a/qiling/arch/models.py b/qiling/arch/models.py index 0f8c084b5..103ffa12e 100644 --- a/qiling/arch/models.py +++ b/qiling/arch/models.py @@ -117,6 +117,22 @@ class MIPS_CPU_MODEL(Enum): MIPS32_I7200 = 15 +class MIPS64_CPU_MODEL(Enum): + MIPS64_R4000 = 0 + MIPS64_VR5432 = 1 + MIPS64_5KC = 2 + MIPS64_5KF = 3 + MIPS64_20KC = 4 + MIPS64_MIPS64R2_GENERIC = 5 + MIPS64_5KEC = 6 + MIPS64_5KEF = 7 + MIPS64_I6400 = 8 + MIPS64_I6500 = 9 + MIPS64_LOONGSON_2E = 10 + MIPS64_LOONGSON_2F = 11 + MIPS64_MIPS64DSPR2 = 12 + + class PPC_CPU_MODEL(Enum): PPC32_401 = 0 PPC32_401A1 = 1 @@ -429,6 +445,7 @@ class RISCV64_CPU_MODEL(Enum): ARM_CPU_MODEL, ARM64_CPU_MODEL, MIPS_CPU_MODEL, + MIPS64_CPU_MODEL, PPC_CPU_MODEL, RISCV_CPU_MODEL, RISCV64_CPU_MODEL @@ -440,6 +457,7 @@ class RISCV64_CPU_MODEL(Enum): 'ARM_CPU_MODEL', 'ARM64_CPU_MODEL', 'MIPS_CPU_MODEL', + 'MIPS64_CPU_MODEL', 'PPC_CPU_MODEL', 'RISCV_CPU_MODEL', 'RISCV64_CPU_MODEL', diff --git a/qiling/arch/utils.py b/qiling/arch/utils.py index 4d44b545f..4a6945615 100644 --- a/qiling/arch/utils.py +++ b/qiling/arch/utils.py @@ -12,7 +12,7 @@ from functools import lru_cache from keystone import (Ks, KS_ARCH_ARM, KS_ARCH_ARM64, KS_ARCH_MIPS, KS_ARCH_X86, KS_ARCH_PPC, - KS_MODE_ARM, KS_MODE_THUMB, KS_MODE_MIPS32, KS_MODE_PPC32, KS_MODE_16, KS_MODE_32, KS_MODE_64, + KS_MODE_ARM, KS_MODE_THUMB, KS_MODE_MIPS32, KS_MODE_MIPS64, KS_MODE_PPC32, KS_MODE_16, KS_MODE_32, KS_MODE_64, KS_MODE_LITTLE_ENDIAN, KS_MODE_BIG_ENDIAN) from qiling import Qiling @@ -126,6 +126,7 @@ def assembler(arch: QL_ARCH, endianness: QL_ENDIAN, is_thumb: bool) -> Ks: QL_ARCH.ARM: (KS_ARCH_ARM, KS_MODE_ARM + endian + thumb), QL_ARCH.ARM64: (KS_ARCH_ARM64, KS_MODE_ARM), QL_ARCH.MIPS: (KS_ARCH_MIPS, KS_MODE_MIPS32 + endian), + QL_ARCH.MIPS64: (KS_ARCH_MIPS, KS_MODE_MIPS64 + endian), QL_ARCH.A8086: (KS_ARCH_X86, KS_MODE_16), QL_ARCH.X86: (KS_ARCH_X86, KS_MODE_32), QL_ARCH.X8664: (KS_ARCH_X86, KS_MODE_64), diff --git a/qiling/cc/mips.py b/qiling/cc/mips.py index 472b2a3ec..0532504e2 100644 --- a/qiling/cc/mips.py +++ b/qiling/cc/mips.py @@ -2,7 +2,10 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework -from unicorn.mips_const import UC_MIPS_REG_V0, UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_A2, UC_MIPS_REG_A3 +from unicorn.mips_const import ( + UC_MIPS_REG_V0, UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_A2, UC_MIPS_REG_A3, + UC_MIPS_REG_T0, UC_MIPS_REG_T1, UC_MIPS_REG_T2, UC_MIPS_REG_T3 +) from qiling.cc import QlCommonBaseCC, make_arg_list @@ -25,3 +28,14 @@ def getNumSlots(argbits: int): def unwind(self, nslots: int) -> int: # TODO: stack frame unwiding? return self.arch.regs.ra + + +class mips64n64(mipso32): + # n64 passes the first 8 arguments in registers: a0-a3 followed by a4-a7, + # which are the physical registers $8-$11 (unicorn names them t0-t3). unlike + # o32, n64 reserves no shadow space on the stack. + _argregs = make_arg_list( + UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_A2, UC_MIPS_REG_A3, + UC_MIPS_REG_T0, UC_MIPS_REG_T1, UC_MIPS_REG_T2, UC_MIPS_REG_T3 + ) + _shadow = 0 diff --git a/qiling/const.py b/qiling/const.py index b54359693..f7ca85214 100644 --- a/qiling/const.py +++ b/qiling/const.py @@ -23,6 +23,7 @@ class QL_ARCH(IntEnum): RISCV = 110 RISCV64 = 111 PPC = 112 + MIPS64 = 113 class QL_OS(IntEnum): diff --git a/qiling/debugger/gdb/gdb.py b/qiling/debugger/gdb/gdb.py index f6d6498d8..df3fc648e 100644 --- a/qiling/debugger/gdb/gdb.py +++ b/qiling/debugger/gdb/gdb.py @@ -15,6 +15,7 @@ import os import socket +import select import re import tempfile from functools import partial @@ -124,6 +125,10 @@ def run(self): server = GdbSerialConn(self.ip, self.port, self.ql.log) killed = False + # let the run hook break into a free-running guest when the client sends + # an async interrupt (ctrl-c / \x03); see QlGdbUtils.dbg_hook. + self.gdb.check_interrupt = server.poll_interrupt + def __hexstr(value: int, nibbles: int = 0) -> str: """Encode a value into a hex string. """ @@ -147,7 +152,13 @@ def __get_reg_value(reg: Optional[int], pos: int, nibbles: int) -> str: hexstr = __hexstr(value, nibbles) else: - hexstr = __unkown_reg_value(nibbles) + # registers the target description declares but the backend cannot + # supply (e.g. MIPS cp0 badvaddr/cause or the fpu regs, which unicorn + # does not expose) are reported as zero rather than '': + # plain gdb tolerates unavailable regs, but stricter clients such as + # Ghidra's ghidragdb abort when a register in the 'general' group is + # unavailable. + hexstr = '0' * nibbles return hexstr @@ -191,6 +202,7 @@ def handle_qmark(subcmd: str) -> Reply: QL_ARCH.ARM : UC_ARM_REG_R11, QL_ARCH.ARM64 : UC_ARM64_REG_X29, QL_ARCH.MIPS : UC_MIPS_REG_INVALID, # skipped + QL_ARCH.MIPS64 : UC_MIPS_REG_INVALID, # skipped QL_ARCH.A8086 : UC_X86_REG_EBP, QL_ARCH.CORTEX_M : UC_ARM_REG_R11, QL_ARCH.PPC : UC_PPC_REG_31 @@ -214,7 +226,7 @@ def __get_reg_info(ucreg: int) -> str: return f'{regnum:02x}:{hexval};' # mips targets skip this reg info pair - bp_info = '' if self.ql.arch.type == QL_ARCH.MIPS else __get_reg_info(arch_uc_bp) + bp_info = '' if self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64) else __get_reg_info(arch_uc_bp) # FIXME: a8086 should use 'esp' and 'eip' here instead of 'sp' and 'ip' set by its arch instance sp_info = __get_reg_info(self.ql.arch.regs.uc_sp) @@ -247,7 +259,10 @@ def handle_c(subcmd: str) -> Reply: reply = f'S{SIGINT:02x}' else: - if getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp: + if self.gdb.interrupted: + # emulation was stopped by an async interrupt from the client + reply = f'S{SIGINT:02x}' + elif getattr(self.ql.arch, 'effective_pc', self.ql.arch.regs.arch_pc) == self.gdb.last_bp: # emulation stopped because it hit a breakpoint reply = f'S{SIGTRAP:02x}' else: @@ -501,7 +516,16 @@ def handle_q(subcmd: str) -> Reply: return b'l' + auxv_data elif feature == 'exec-file' and op == 'read': - return f'l{self.ql.os.path.host_to_virtual_path(self.ql.path)}' + # map the host path of the executable to its guest path; if the + # binary was loaded from outside the rootfs (common when running + # a target straight from its build tree) there is no such mapping, + # so fall back to reporting the host path as-is. + try: + execpath = self.ql.os.path.host_to_virtual_path(self.ql.path) + except ValueError: + execpath = self.ql.path + + return f'l{execpath}' elif feature == 'libraries-svr4' and op == 'read': # TODO: this one requires information of loaded libraries which currently not provided @@ -823,6 +847,27 @@ def close(self): self.client.close() self.sock.close() + def poll_interrupt(self) -> bool: + """Non-blocking check for an async interrupt from the client. + + While the target is running the only thing gdb sends is a bare ``\\x03`` + break byte (it waits for a stop reply before sending anything else), so + any readable bytes here are that interrupt or a stray protocol ack. + Returns True if a break was seen. Called from the run hook, so it must + never block. + """ + + readable, _, _ = select.select([self.client], [], [], 0) + if not readable: + return False + + try: + incoming = self.client.recv(self.BUFSIZE) + except (ConnectionError, OSError): + return False + + return b'\x03' in incoming + def readpackets(self) -> Iterator[bytes]: """Iterate through incoming packets in an active connection until it is terminated. diff --git a/qiling/debugger/gdb/utils.py b/qiling/debugger/gdb/utils.py index 5e3b208bf..6413d1eed 100644 --- a/qiling/debugger/gdb/utils.py +++ b/qiling/debugger/gdb/utils.py @@ -14,6 +14,11 @@ class QlGdbUtils: + # how often (in guest instructions) the run hook polls the client socket for + # an async interrupt. small enough to feel instant, large enough that the + # extra non-blocking socket check does not dominate the per-instruction hook. + INTR_POLL_INTERVAL = 200 + def __init__(self, ql: Qiling, entry_point: int, exit_point: int): self.ql = ql @@ -21,6 +26,14 @@ def __init__(self, ql: Qiling, entry_point: int, exit_point: int): self.swbp = set() self.last_bp = None + # async-interrupt support: `check_interrupt` is a callable installed by the + # gdb stub that returns True when the client sent a break (ctrl-c / \x03) + # while the target was running. `interrupted` records that the last resume + # stopped for that reason (rather than a breakpoint or normal exit). + self.check_interrupt = None + self.interrupted = False + self._poll_counter = 0 + def __entry_point_hook(ql: Qiling): ql.hook_del(ep_hret) ql.hook_code(self.dbg_hook) @@ -36,6 +49,24 @@ def dbg_hook(self, ql: Qiling, address: int, size: int): if getattr(ql.arch, 'is_thumb', False): address |= 1 + # poll for an async interrupt from the client (gdb sends a bare \x03 while + # the target is running). throttled so the socket check stays off the hot + # path. this is the only way to break into a free-running guest, since the + # stub's packet loop is blocked inside emu_start until the target stops. + if self.check_interrupt is not None: + self._poll_counter += 1 + + if self._poll_counter >= self.INTR_POLL_INTERVAL: + self._poll_counter = 0 + + if self.check_interrupt(): + self.interrupted = True + self.last_bp = None + + ql.log.info(f'{PROMPT} interrupted by client, stopped at {address:#x}') + ql.stop() + return + # resuming emulation after hitting a breakpoint will re-enter this hook. # avoid an endless hooking loop by detecting and skipping this case if address == self.last_bp: @@ -83,4 +114,8 @@ def resume_emu(self, address: Optional[int] = None, steps: int = 0): op = f'stepping {steps} instructions' if steps else 'resuming' self.ql.log.info(f'{PROMPT} {op} from {address:#x}') + # clear any pending interrupt state from a previous resume + self.interrupted = False + self._poll_counter = 0 + self.ql.emu_start(address, self.exit_point, count=steps) diff --git a/qiling/debugger/gdb/xml/mips64/mips64-cp0.xml b/qiling/debugger/gdb/xml/mips64/mips64-cp0.xml new file mode 100644 index 000000000..397d2197e --- /dev/null +++ b/qiling/debugger/gdb/xml/mips64/mips64-cp0.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/qiling/debugger/gdb/xml/mips64/mips64-cpu.xml b/qiling/debugger/gdb/xml/mips64/mips64-cpu.xml new file mode 100644 index 000000000..b01bd572f --- /dev/null +++ b/qiling/debugger/gdb/xml/mips64/mips64-cpu.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qiling/debugger/gdb/xml/mips64/mips64-fpu.xml b/qiling/debugger/gdb/xml/mips64/mips64-fpu.xml new file mode 100644 index 000000000..6f2ad3937 --- /dev/null +++ b/qiling/debugger/gdb/xml/mips64/mips64-fpu.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/qiling/debugger/gdb/xml/mips64/target.xml b/qiling/debugger/gdb/xml/mips64/target.xml new file mode 100644 index 000000000..1f4cf09d7 --- /dev/null +++ b/qiling/debugger/gdb/xml/mips64/target.xml @@ -0,0 +1,20 @@ + + + + + + mips:isa64r2 + GNU/Linux + + + + + + + + + diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 6bc2371f4..a7de3ce88 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -147,6 +147,7 @@ def __load_regsmap(archtype: QL_ARCH, xmltree: ElementTree.ElementTree) -> Seque QL_ARCH.CORTEX_M: dict(**cortex_m_regs), QL_ARCH.ARM64: dict(**arm64_regs, **arm64_regs_v, **arm64_reg_map_fp), QL_ARCH.MIPS: dict(**mips_regs_gpr), + QL_ARCH.MIPS64: dict(**mips_regs_gpr), QL_ARCH.PPC: dict(**ppc_regs) }[archtype] diff --git a/qiling/loader/elf.py b/qiling/loader/elf.py index a5a8ac6fe..abd8278c1 100644 --- a/qiling/loader/elf.py +++ b/qiling/loader/elf.py @@ -374,7 +374,7 @@ def __push_str(top: int, s: str) -> int: sp_align = self.ql.arch.pointersize # mips requires doubleword alignment - if self.ql.arch.type is QL_ARCH.MIPS: + if self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): sp_align *= 2 new_stack = self.ql.mem.align(new_stack - len(elf_table), sp_align) diff --git a/qiling/os/blob/blob.py b/qiling/os/blob/blob.py index af52fa74a..e8d4bff1d 100644 --- a/qiling/os/blob/blob.py +++ b/qiling/os/blob/blob.py @@ -33,6 +33,7 @@ def __init__(self, ql: Qiling): QL_ARCH.ARM : arm.aarch32, QL_ARCH.ARM64 : arm.aarch64, QL_ARCH.MIPS : mips.mipso32, + QL_ARCH.MIPS64 : mips.mips64n64, QL_ARCH.RISCV : riscv.riscv, QL_ARCH.RISCV64 : riscv.riscv, QL_ARCH.PPC : ppc.ppc, diff --git a/qiling/os/linux/function_hook.py b/qiling/os/linux/function_hook.py index d8bbd9415..4406ace68 100644 --- a/qiling/os/linux/function_hook.py +++ b/qiling/os/linux/function_hook.py @@ -71,8 +71,8 @@ def get_ret_pc(self): if self.ql.arch.type == QL_ARCH.ARM: return self.ql.arch.regs.lr - # MIPS32 - elif self.ql.arch.type == QL_ARCH.MIPS: + # MIPS32 / MIPS64 (return address is in $ra for both o32 and n64) + elif self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): return self.ql.arch.regs.ra # ARM64 @@ -98,8 +98,8 @@ def context_fixup(self): if self.ql.arch.type == QL_ARCH.ARM: pass - # MIPS32 - elif self.ql.arch.type == QL_ARCH.MIPS: + # MIPS32 / MIPS64 + elif self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): pass # PPC @@ -125,8 +125,8 @@ def set_ret(self, addr): if self.ql.arch.type == QL_ARCH.ARM: self.ql.arch.regs.lr = addr - # MIPS32 - elif self.ql.arch.type == QL_ARCH.MIPS: + # MIPS32 / MIPS64 + elif self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): self.ql.arch.regs.ra = addr # PPC @@ -555,8 +555,8 @@ def __init__(self, ql, phoff, phnum, phentsize, load_base, hook_mem): ins = b'\x01\x10\x81\xe1' self.add_function_hook = self.add_function_hook_relocation - # MIPS32 - elif self.ql.arch.type == QL_ARCH.MIPS: + # MIPS32 / MIPS64 (shared R_MIPS_* relocation type numbers) + elif self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): # ref: https://sites.uclouvain.be/SystInfo/usr/include/elf.h.html self.GLOB_DAT = 51 self.JMP_SLOT = 127 @@ -624,7 +624,7 @@ def __init__(self, ql, phoff, phnum, phentsize, load_base, hook_mem): self.rel_list += self.plt_rel self.show_relocation(self.plt_rel) - if self.ql.arch.type == QL_ARCH.MIPS and self.plt_got != None and self.mips_gotsym != None and self.mips_local_gotno != None and self.mips_symtabno != None: + if self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64) and self.plt_got != None and self.mips_gotsym != None and self.mips_local_gotno != None and self.mips_symtabno != None: self.show_dynsym_name(self.mips_gotsym, self.mips_symtabno) self.ql.mem.map(hook_mem, 0x2000, perms=7, info="[hook_mem]") diff --git a/qiling/os/linux/linux.py b/qiling/os/linux/linux.py index 0313218d6..56b8d39cb 100644 --- a/qiling/os/linux/linux.py +++ b/qiling/os/linux/linux.py @@ -37,6 +37,7 @@ def __init__(self, ql: Qiling): QL_ARCH.ARM : arm.aarch32, QL_ARCH.ARM64 : arm.aarch64, QL_ARCH.MIPS : mips.mipso32, + QL_ARCH.MIPS64 : mips.mips64n64, QL_ARCH.RISCV : riscv.riscv, QL_ARCH.RISCV64 : riscv.riscv, QL_ARCH.PPC : ppc.ppc, @@ -64,8 +65,8 @@ def load(self): 'get_tls': 0xffff0fe0 }) - # MIPS32 - elif self.ql.arch.type == QL_ARCH.MIPS: + # MIPS32 / MIPS64 (same syscall exception number) + elif self.ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): self.ql.hook_intno(self.hook_syscall, 17) self.thread_class = thread.QlLinuxMIPS32Thread diff --git a/qiling/os/linux/map_syscall.py b/qiling/os/linux/map_syscall.py index c3a1289e7..957efacc1 100644 --- a/qiling/os/linux/map_syscall.py +++ b/qiling/os/linux/map_syscall.py @@ -18,6 +18,7 @@ def get_syscall_mapper(archtype: QL_ARCH): QL_ARCH.X8664 : x8664_syscall_table, QL_ARCH.X86 : x86_syscall_table, QL_ARCH.MIPS : mips_syscall_table, + QL_ARCH.MIPS64 : mips64_n64_syscall_table, QL_ARCH.RISCV : riscv32_syscall_table, QL_ARCH.RISCV64 : riscv64_syscall_table, QL_ARCH.PPC : ppc_syscall_table @@ -1935,6 +1936,376 @@ def __mapper(syscall_num: int) -> str: 4448: "process_mrelease", } +# MIPS64 n64 ABI (syscall base 5000), generated from the Linux kernel uapi +# header asm/unistd_n64.h +mips64_n64_syscall_table = { + 5000: "read", + 5001: "write", + 5002: "open", + 5003: "close", + 5004: "stat", + 5005: "fstat", + 5006: "lstat", + 5007: "poll", + 5008: "lseek", + 5009: "mmap", + 5010: "mprotect", + 5011: "munmap", + 5012: "brk", + 5013: "rt_sigaction", + 5014: "rt_sigprocmask", + 5015: "ioctl", + 5016: "pread64", + 5017: "pwrite64", + 5018: "readv", + 5019: "writev", + 5020: "access", + 5021: "pipe", + 5022: "_newselect", + 5023: "sched_yield", + 5024: "mremap", + 5025: "msync", + 5026: "mincore", + 5027: "madvise", + 5028: "shmget", + 5029: "shmat", + 5030: "shmctl", + 5031: "dup", + 5032: "dup2", + 5033: "pause", + 5034: "nanosleep", + 5035: "getitimer", + 5036: "setitimer", + 5037: "alarm", + 5038: "getpid", + 5039: "sendfile", + 5040: "socket", + 5041: "connect", + 5042: "accept", + 5043: "sendto", + 5044: "recvfrom", + 5045: "sendmsg", + 5046: "recvmsg", + 5047: "shutdown", + 5048: "bind", + 5049: "listen", + 5050: "getsockname", + 5051: "getpeername", + 5052: "socketpair", + 5053: "setsockopt", + 5054: "getsockopt", + 5055: "clone", + 5056: "fork", + 5057: "execve", + 5058: "exit", + 5059: "wait4", + 5060: "kill", + 5061: "uname", + 5062: "semget", + 5063: "semop", + 5064: "semctl", + 5065: "shmdt", + 5066: "msgget", + 5067: "msgsnd", + 5068: "msgrcv", + 5069: "msgctl", + 5070: "fcntl", + 5071: "flock", + 5072: "fsync", + 5073: "fdatasync", + 5074: "truncate", + 5075: "ftruncate", + 5076: "getdents", + 5077: "getcwd", + 5078: "chdir", + 5079: "fchdir", + 5080: "rename", + 5081: "mkdir", + 5082: "rmdir", + 5083: "creat", + 5084: "link", + 5085: "unlink", + 5086: "symlink", + 5087: "readlink", + 5088: "chmod", + 5089: "fchmod", + 5090: "chown", + 5091: "fchown", + 5092: "lchown", + 5093: "umask", + 5094: "gettimeofday", + 5095: "getrlimit", + 5096: "getrusage", + 5097: "sysinfo", + 5098: "times", + 5099: "ptrace", + 5100: "getuid", + 5101: "syslog", + 5102: "getgid", + 5103: "setuid", + 5104: "setgid", + 5105: "geteuid", + 5106: "getegid", + 5107: "setpgid", + 5108: "getppid", + 5109: "getpgrp", + 5110: "setsid", + 5111: "setreuid", + 5112: "setregid", + 5113: "getgroups", + 5114: "setgroups", + 5115: "setresuid", + 5116: "getresuid", + 5117: "setresgid", + 5118: "getresgid", + 5119: "getpgid", + 5120: "setfsuid", + 5121: "setfsgid", + 5122: "getsid", + 5123: "capget", + 5124: "capset", + 5125: "rt_sigpending", + 5126: "rt_sigtimedwait", + 5127: "rt_sigqueueinfo", + 5128: "rt_sigsuspend", + 5129: "sigaltstack", + 5130: "utime", + 5131: "mknod", + 5132: "personality", + 5133: "ustat", + 5134: "statfs", + 5135: "fstatfs", + 5136: "sysfs", + 5137: "getpriority", + 5138: "setpriority", + 5139: "sched_setparam", + 5140: "sched_getparam", + 5141: "sched_setscheduler", + 5142: "sched_getscheduler", + 5143: "sched_get_priority_max", + 5144: "sched_get_priority_min", + 5145: "sched_rr_get_interval", + 5146: "mlock", + 5147: "munlock", + 5148: "mlockall", + 5149: "munlockall", + 5150: "vhangup", + 5151: "pivot_root", + 5152: "_sysctl", + 5153: "prctl", + 5154: "adjtimex", + 5155: "setrlimit", + 5156: "chroot", + 5157: "sync", + 5158: "acct", + 5159: "settimeofday", + 5160: "mount", + 5161: "umount2", + 5162: "swapon", + 5163: "swapoff", + 5164: "reboot", + 5165: "sethostname", + 5166: "setdomainname", + 5167: "create_module", + 5168: "init_module", + 5169: "delete_module", + 5170: "get_kernel_syms", + 5171: "query_module", + 5172: "quotactl", + 5173: "nfsservctl", + 5174: "getpmsg", + 5175: "putpmsg", + 5176: "afs_syscall", + 5177: "reserved177", + 5178: "gettid", + 5179: "readahead", + 5180: "setxattr", + 5181: "lsetxattr", + 5182: "fsetxattr", + 5183: "getxattr", + 5184: "lgetxattr", + 5185: "fgetxattr", + 5186: "listxattr", + 5187: "llistxattr", + 5188: "flistxattr", + 5189: "removexattr", + 5190: "lremovexattr", + 5191: "fremovexattr", + 5192: "tkill", + 5193: "reserved193", + 5194: "futex", + 5195: "sched_setaffinity", + 5196: "sched_getaffinity", + 5197: "cacheflush", + 5198: "cachectl", + 5199: "sysmips", + 5200: "io_setup", + 5201: "io_destroy", + 5202: "io_getevents", + 5203: "io_submit", + 5204: "io_cancel", + 5205: "exit_group", + 5206: "lookup_dcookie", + 5207: "epoll_create", + 5208: "epoll_ctl", + 5209: "epoll_wait", + 5210: "remap_file_pages", + 5211: "rt_sigreturn", + 5212: "set_tid_address", + 5213: "restart_syscall", + 5214: "semtimedop", + 5215: "fadvise64", + 5216: "timer_create", + 5217: "timer_settime", + 5218: "timer_gettime", + 5219: "timer_getoverrun", + 5220: "timer_delete", + 5221: "clock_settime", + 5222: "clock_gettime", + 5223: "clock_getres", + 5224: "clock_nanosleep", + 5225: "tgkill", + 5226: "utimes", + 5227: "mbind", + 5228: "get_mempolicy", + 5229: "set_mempolicy", + 5230: "mq_open", + 5231: "mq_unlink", + 5232: "mq_timedsend", + 5233: "mq_timedreceive", + 5234: "mq_notify", + 5235: "mq_getsetattr", + 5236: "vserver", + 5237: "waitid", + 5239: "add_key", + 5240: "request_key", + 5241: "keyctl", + 5242: "set_thread_area", + 5243: "inotify_init", + 5244: "inotify_add_watch", + 5245: "inotify_rm_watch", + 5246: "migrate_pages", + 5247: "openat", + 5248: "mkdirat", + 5249: "mknodat", + 5250: "fchownat", + 5251: "futimesat", + 5252: "newfstatat", + 5253: "unlinkat", + 5254: "renameat", + 5255: "linkat", + 5256: "symlinkat", + 5257: "readlinkat", + 5258: "fchmodat", + 5259: "faccessat", + 5260: "pselect6", + 5261: "ppoll", + 5262: "unshare", + 5263: "splice", + 5264: "sync_file_range", + 5265: "tee", + 5266: "vmsplice", + 5267: "move_pages", + 5268: "set_robust_list", + 5269: "get_robust_list", + 5270: "kexec_load", + 5271: "getcpu", + 5272: "epoll_pwait", + 5273: "ioprio_set", + 5274: "ioprio_get", + 5275: "utimensat", + 5276: "signalfd", + 5277: "timerfd", + 5278: "eventfd", + 5279: "fallocate", + 5280: "timerfd_create", + 5281: "timerfd_gettime", + 5282: "timerfd_settime", + 5283: "signalfd4", + 5284: "eventfd2", + 5285: "epoll_create1", + 5286: "dup3", + 5287: "pipe2", + 5288: "inotify_init1", + 5289: "preadv", + 5290: "pwritev", + 5291: "rt_tgsigqueueinfo", + 5292: "perf_event_open", + 5293: "accept4", + 5294: "recvmmsg", + 5295: "fanotify_init", + 5296: "fanotify_mark", + 5297: "prlimit64", + 5298: "name_to_handle_at", + 5299: "open_by_handle_at", + 5300: "clock_adjtime", + 5301: "syncfs", + 5302: "sendmmsg", + 5303: "setns", + 5304: "process_vm_readv", + 5305: "process_vm_writev", + 5306: "kcmp", + 5307: "finit_module", + 5308: "getdents64", + 5309: "sched_setattr", + 5310: "sched_getattr", + 5311: "renameat2", + 5312: "seccomp", + 5313: "getrandom", + 5314: "memfd_create", + 5315: "bpf", + 5316: "execveat", + 5317: "userfaultfd", + 5318: "membarrier", + 5319: "mlock2", + 5320: "copy_file_range", + 5321: "preadv2", + 5322: "pwritev2", + 5323: "pkey_mprotect", + 5324: "pkey_alloc", + 5325: "pkey_free", + 5326: "statx", + 5327: "rseq", + 5328: "io_pgetevents", + 5424: "pidfd_send_signal", + 5425: "io_uring_setup", + 5426: "io_uring_enter", + 5427: "io_uring_register", + 5428: "open_tree", + 5429: "move_mount", + 5430: "fsopen", + 5431: "fsconfig", + 5432: "fsmount", + 5433: "fspick", + 5434: "pidfd_open", + 5435: "clone3", + 5436: "close_range", + 5437: "openat2", + 5438: "pidfd_getfd", + 5439: "faccessat2", + 5440: "process_madvise", + 5441: "epoll_pwait2", + 5442: "mount_setattr", + 5443: "quotactl_fd", + 5444: "landlock_create_ruleset", + 5445: "landlock_add_rule", + 5446: "landlock_restrict_self", + 5448: "process_mrelease", + 5449: "futex_waitv", + 5450: "set_mempolicy_home_node", + 5451: "cachestat", + 5452: "fchmodat2", + 5453: "map_shadow_stack", + 5454: "futex_wake", + 5455: "futex_wait", + 5456: "futex_requeue", + 5457: "statmount", + 5458: "listmount", + 5459: "lsm_get_self_attr", + 5460: "lsm_set_self_attr", + 5461: "lsm_list_modules", +} + riscv32_syscall_table = { 0: "io_setup", 1: "io_destroy", diff --git a/qiling/os/linux/syscall.py b/qiling/os/linux/syscall.py index d97de91bf..0e8125454 100644 --- a/qiling/os/linux/syscall.py +++ b/qiling/os/linux/syscall.py @@ -56,7 +56,7 @@ def ql_syscall_set_thread_area(ql: Qiling, u_info_addr: int): ql.log.warning(f"Wrong index {index} from address {hex(u_info_addr)}") return -1 - elif ql.arch.type == QL_ARCH.MIPS: + elif ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): CONFIG3_ULR = (1 << 13) ql.arch.regs.cp0_config3 = CONFIG3_ULR ql.arch.regs.cp0_userlocal = u_info_addr diff --git a/qiling/os/posix/const.py b/qiling/os/posix/const.py index df4c5b587..535cf6d79 100644 --- a/qiling/os/posix/const.py +++ b/qiling/os/posix/const.py @@ -380,24 +380,32 @@ class linux_mips_socket_level(Enum): # https://docs.huihoo.com/doxygen/linux/kernel/3.7/arch_2mips_2include_2uapi_2asm_2socket_8h_source.html # https://github.com/torvalds/linux/blob/master/arch/mips/include/uapi/asm/socket.h class linux_mips_socket_options(Enum): - SO_DEBUG = 0x01 - SO_REUSEADDR = 0x04 - SO_KEEPALIVE = 0x08 - SO_DONTROUTE = 0x10 - SO_BINDTODEVICE = 0x19 - SO_BROADCAST = 0x20 - SO_LINGER = 0x80 - SO_OOBINLINE = 0x00 - SO_REUSEPORT = 0x00 - SO_SNDBUF = 0x01 - SO_RCVBUF = 0x02 - SO_SNDLOWAT = 0x03 - SO_RCVLOWAT = 0x04 - SO_SNDTIMEO_OLD = 0x05 - SO_RCVTIMEO_OLD = 0x06 - SO_TIMESTAMP_OLD = 0x1d - # SO_TIMESTAMPNS_OLD = 0x23 - # SO_TIMESTAMPING_OLD = 0x25 + # MIPS uses its own SO_* numbering (arch/mips/include/uapi/asm/socket.h); + # the buffer/timeout/type options live in the 0x1000 range, unlike the + # generic asm-generic values. + SO_DEBUG = 0x0001 + SO_REUSEADDR = 0x0004 + SO_KEEPALIVE = 0x0008 + SO_DONTROUTE = 0x0010 + SO_BINDTODEVICE = 0x0019 + SO_BROADCAST = 0x0020 + SO_LINGER = 0x0080 + SO_OOBINLINE = 0x0100 + SO_REUSEPORT = 0x0200 + SO_TYPE = 0x1008 + SO_ERROR = 0x1007 + SO_SNDBUF = 0x1001 + SO_RCVBUF = 0x1002 + SO_SNDLOWAT = 0x1003 + SO_RCVLOWAT = 0x1004 + SO_SNDTIMEO_OLD = 0x1005 + SO_RCVTIMEO_OLD = 0x1006 + SO_ACCEPTCONN = 0x1009 + SO_PROTOCOL = 0x1028 + SO_DOMAIN = 0x1029 + SO_TIMESTAMP_OLD = 0x001d + # SO_TIMESTAMPNS_OLD = 0x0023 + # SO_TIMESTAMPING_OLD = 0x0025 SO_TIMESTAMP_NEW = 0x3f SO_TIMESTAMPNS_NEW = 0x40 SO_TIMESTAMPING_NEW = 0x41 diff --git a/qiling/os/posix/const_mapping.py b/qiling/os/posix/const_mapping.py index dd95f717e..fde1cc73b 100644 --- a/qiling/os/posix/const_mapping.py +++ b/qiling/os/posix/const_mapping.py @@ -45,6 +45,7 @@ def get_open_flags_class(archtype: QL_ARCH, ostype: QL_OS) -> Union[Type[Flag], QL_ARCH.ARM: linux_arm_open_flags, QL_ARCH.ARM64: linux_arm_open_flags, QL_ARCH.MIPS: linux_mips_open_flags, + QL_ARCH.MIPS64: linux_mips_open_flags, QL_ARCH.RISCV: linux_riscv_open_flags, QL_ARCH.RISCV64: linux_riscv_open_flags, QL_ARCH.PPC: linux_ppc_open_flags @@ -135,7 +136,8 @@ def socket_type_mapping(value: int, archtype: QL_ARCH) -> str: QL_ARCH.X8664: linux_x86_socket_types, QL_ARCH.ARM: linux_arm_socket_types, QL_ARCH.ARM64: linux_arm_socket_types, - QL_ARCH.MIPS: linux_mips_socket_types + QL_ARCH.MIPS: linux_mips_socket_types, + QL_ARCH.MIPS64: linux_mips_socket_types }[archtype] # https://code.woboq.org/linux/linux/net/socket.c.html#1363 @@ -148,7 +150,8 @@ def socket_domain_mapping(value: int, archtype: QL_ARCH, ostype: QL_OS) -> str: QL_ARCH.X8664: macos_x86_socket_domain if ostype is QL_OS.MACOS else linux_x86_socket_domain, QL_ARCH.ARM: linux_arm_socket_domain, QL_ARCH.ARM64: linux_arm_socket_domain, - QL_ARCH.MIPS: linux_mips_socket_domain + QL_ARCH.MIPS: linux_mips_socket_domain, + QL_ARCH.MIPS64: linux_mips_socket_domain }[archtype] return socket_domain(value).name @@ -161,6 +164,7 @@ def socket_tcp_option_mapping(value: int, archtype: QL_ARCH) -> str: QL_ARCH.ARM: linux_socket_tcp_options, QL_ARCH.ARM64: linux_socket_tcp_options, QL_ARCH.MIPS: linux_socket_tcp_options, + QL_ARCH.MIPS64: linux_socket_tcp_options, }[archtype] return socket_option(value).name @@ -172,7 +176,8 @@ def socket_level_mapping(value: int, archtype: QL_ARCH) -> str: QL_ARCH.X8664: linux_x86_socket_level, QL_ARCH.ARM: linux_arm_socket_level, QL_ARCH.ARM64: linux_arm_socket_level, - QL_ARCH.MIPS: linux_mips_socket_level + QL_ARCH.MIPS: linux_mips_socket_level, + QL_ARCH.MIPS64: linux_mips_socket_level }[archtype] return socket_level(value).name @@ -184,7 +189,8 @@ def socket_ip_option_mapping(value: int, archtype: QL_ARCH, ostype: QL_OS) -> st QL_ARCH.X8664: macos_socket_ip_options if ostype is QL_OS.MACOS else linux_socket_ip_options, QL_ARCH.ARM: linux_socket_ip_options, QL_ARCH.ARM64: macos_socket_ip_options if ostype is QL_OS.MACOS else linux_socket_ip_options, - QL_ARCH.MIPS: linux_mips_socket_ip_options + QL_ARCH.MIPS: linux_mips_socket_ip_options, + QL_ARCH.MIPS64: linux_mips_socket_ip_options }[archtype] return socket_ip_option(value).name @@ -196,7 +202,8 @@ def socket_option_mapping(value: int, archtype: QL_ARCH) -> str: QL_ARCH.X8664: linux_x86_socket_options, QL_ARCH.ARM: linux_arm_socket_options, QL_ARCH.ARM64: linux_arm_socket_options, - QL_ARCH.MIPS: linux_mips_socket_options + QL_ARCH.MIPS: linux_mips_socket_options, + QL_ARCH.MIPS64: linux_mips_socket_options }[archtype] return socket_option(value).name diff --git a/qiling/os/posix/posix.py b/qiling/os/posix/posix.py index a0a7b6290..c86c0776d 100644 --- a/qiling/os/posix/posix.py +++ b/qiling/os/posix/posix.py @@ -70,6 +70,7 @@ def __init__(self, ql: Qiling): QL_ARCH.ARM64: arm.QlAArch64, QL_ARCH.ARM: arm.QlAArch32, QL_ARCH.MIPS: mips.QlMipsO32, + QL_ARCH.MIPS64: mips.QlMips64N64, QL_ARCH.X86: intel.QlIntel32, QL_ARCH.X8664: intel.QlIntel64, QL_ARCH.RISCV: riscv.QlRiscV32, diff --git a/qiling/os/posix/syscall/abi/mips.py b/qiling/os/posix/syscall/abi/mips.py index a8e3a562c..f1294160e 100644 --- a/qiling/os/posix/syscall/abi/mips.py +++ b/qiling/os/posix/syscall/abi/mips.py @@ -3,7 +3,10 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework from typing import Tuple -from unicorn.mips_const import UC_MIPS_REG_V0, UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_A2, UC_MIPS_REG_A3 +from unicorn.mips_const import ( + UC_MIPS_REG_V0, UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_A2, UC_MIPS_REG_A3, + UC_MIPS_REG_T0, UC_MIPS_REG_T1, UC_MIPS_REG_T2, UC_MIPS_REG_T3 +) from qiling.os.posix.syscall.abi import QlSyscallABI @@ -39,3 +42,31 @@ def set_return_value(self, value: int) -> None: self.arch.regs.v0 = v0 self.arch.regs.a3 = a3 + + +class QlMips64N64(QlMipsO32): + """System call ABI for MIPS64 n64 systems. + + n64 passes the first 8 arguments in registers (a0-a3 followed by a4-a7, + which are the physical registers $8-$11) and, unlike o32, reserves no + shadow space for them on the stack. + + See: https://www.linux-mips.org/wiki/Syscall + """ + + _idreg = UC_MIPS_REG_V0 + _argregs = ( + UC_MIPS_REG_A0, UC_MIPS_REG_A1, UC_MIPS_REG_A2, UC_MIPS_REG_A3, + UC_MIPS_REG_T0, UC_MIPS_REG_T1, UC_MIPS_REG_T2, UC_MIPS_REG_T3 + ) + _retreg = UC_MIPS_REG_V0 + + def get_params(self, count: int) -> Tuple[int, ...]: + num_reg_args = len(self._argregs) + + reg_args = QlSyscallABI.get_params(self, min(count, num_reg_args)) + stack_args = tuple( + self.arch.stack_read(self.arch.pointersize * i) for i in range(max(0, count - num_reg_args)) + ) + + return reg_args + stack_args diff --git a/qiling/os/posix/syscall/mman.py b/qiling/os/posix/syscall/mman.py index a89f98d39..361173a05 100755 --- a/qiling/os/posix/syscall/mman.py +++ b/qiling/os/posix/syscall/mman.py @@ -80,7 +80,7 @@ def __select_mmap_flags(archtype: QL_ARCH, ostype: QL_OS): """ osflags = { - QL_OS.LINUX: mips_mmap_flags if archtype == QL_ARCH.MIPS else linux_mmap_flags, + QL_OS.LINUX: mips_mmap_flags if archtype in (QL_ARCH.MIPS, QL_ARCH.MIPS64) else linux_mmap_flags, QL_OS.FREEBSD: freebsd_mmap_flags, QL_OS.MACOS: macos_mmap_flags, QL_OS.QNX: qnx_mmap_flags # FIXME: only for arm? diff --git a/qiling/os/posix/syscall/sched.py b/qiling/os/posix/syscall/sched.py index a7c297c15..b2073d17c 100644 --- a/qiling/os/posix/syscall/sched.py +++ b/qiling/os/posix/syscall/sched.py @@ -130,6 +130,38 @@ def ql_syscall_clone(ql: Qiling, flags: int, child_stack: int, parent_tidptr: in return regreturn +def ql_syscall_clone3(ql: Qiling, cl_args: int, size: int): + # clone3(struct clone_args *uargs, size_t size). Translate the struct into + # the legacy clone() argument set and delegate. Modern glibc (used by recent + # toolchains) issues clone3 from pthread_create; without this it falls through + # to the "not implemented" path and thread creation fails. + # + # struct clone_args (all fields __aligned_u64): + # 0:flags 8:pidfd 16:child_tid 24:parent_tid 32:exit_signal + # 40:stack 48:stack_size 56:tls 64:set_tid 72:set_tid_size 80:cgroup + flags = ql.mem.read_ptr(cl_args + 0, 8) + child_tid = ql.mem.read_ptr(cl_args + 16, 8) + parent_tid = ql.mem.read_ptr(cl_args + 24, 8) + exit_signal = ql.mem.read_ptr(cl_args + 32, 8) + stack = ql.mem.read_ptr(cl_args + 40, 8) + stack_size = ql.mem.read_ptr(cl_args + 48, 8) + tls = ql.mem.read_ptr(cl_args + 56, 8) + + # legacy clone() takes the highest stack address; clone3 gives the base + size + child_stack = (stack + stack_size) if stack else 0 + + # clone3 keeps exit_signal in its own field; legacy clone packs it into the + # low CSIGNAL byte of flags + flags |= exit_signal & 0xff + + # ql_syscall_clone swaps newtls<->child_tidptr for x8664 to undo that arch's + # raw-syscall register order. clone3 hands us already-logical args, so on + # x8664 we pre-swap to cancel that out. + if ql.arch.type == QL_ARCH.X8664: + return ql_syscall_clone(ql, flags, child_stack, parent_tid, child_tid, tls) + + return ql_syscall_clone(ql, flags, child_stack, parent_tid, tls, child_tid) + def ql_syscall_sched_yield(ql: Qiling): def _sched_yield(cur_thread): gevent.sleep(0) diff --git a/qiling/os/posix/syscall/shm.py b/qiling/os/posix/syscall/shm.py index 5578feeae..24ad35d2c 100644 --- a/qiling/os/posix/syscall/shm.py +++ b/qiling/os/posix/syscall/shm.py @@ -88,6 +88,7 @@ def ql_syscall_shmat(ql: Qiling, shmid: int, shmaddr: int, shmflg: int): # select the appropriate SHMLBA value, based on the platform shmlba = { QL_ARCH.MIPS: 0x40000, + QL_ARCH.MIPS64: 0x40000, QL_ARCH.ARM: ql.mem.pagesize * 4, QL_ARCH.ARM64: ql.mem.pagesize * 4, QL_ARCH.X86: ql.mem.pagesize, diff --git a/qiling/os/posix/syscall/signal.py b/qiling/os/posix/syscall/signal.py index 1591cb558..bbbf08df8 100644 --- a/qiling/os/posix/syscall/signal.py +++ b/qiling/os/posix/syscall/signal.py @@ -29,6 +29,7 @@ def __make_sigset(arch: QlArch): QL_ARCH.ARM: native_type, QL_ARCH.ARM64: native_type, QL_ARCH.MIPS: ctypes.c_uint32 * (128 // (4 * 8)), + QL_ARCH.MIPS64: ctypes.c_uint32 * (128 // (4 * 8)), QL_ARCH.CORTEX_M: native_type } @@ -108,6 +109,7 @@ class mips_sigaction(Struct): QL_ARCH.ARM: arm_sigaction, QL_ARCH.ARM64: arm_sigaction, QL_ARCH.MIPS: mips_sigaction, + QL_ARCH.MIPS64: mips_sigaction, QL_ARCH.CORTEX_M: arm_sigaction } @@ -119,7 +121,7 @@ class mips_sigaction(Struct): def ql_syscall_rt_sigaction(ql: Qiling, signum: int, act: int, oldact: int): SIGKILL = 9 - SIGSTOP = 23 if ql.arch.type is QL_ARCH.MIPS else 19 + SIGSTOP = 23 if ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64) else 19 if signum not in range(NSIG) or signum in (SIGKILL, SIGSTOP): return -1 # EINVAL @@ -182,7 +184,7 @@ def __sigprocmask_mips(ql: Qiling, how: int, newset: int, oldset: int): def ql_syscall_rt_sigprocmask(ql: Qiling, how: int, newset: int, oldset: int): - impl = __sigprocmask_mips if ql.arch.type is QL_ARCH.MIPS else __sigprocmask + impl = __sigprocmask_mips if ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64) else __sigprocmask return impl(ql, how, newset, oldset) diff --git a/qiling/os/posix/syscall/socket.py b/qiling/os/posix/syscall/socket.py index ea6ee93ba..9774ac774 100644 --- a/qiling/os/posix/syscall/socket.py +++ b/qiling/os/posix/syscall/socket.py @@ -146,7 +146,7 @@ def __host_socket_option(vsock_level: int, vsock_opt: int, arch_type: QL_ARCH, o vsock_opt_name = socket_option_mapping(vsock_opt, arch_type) # Fix for mips - if arch_type == QL_ARCH.MIPS: + if arch_type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): if vsock_opt_name.endswith(('_NEW', '_OLD')): vsock_opt_name = vsock_opt_name[:-4] diff --git a/qiling/os/posix/syscall/stat.py b/qiling/os/posix/syscall/stat.py index 5b153100a..79d097d71 100644 --- a/qiling/os/posix/syscall/stat.py +++ b/qiling/os/posix/syscall/stat.py @@ -1082,13 +1082,23 @@ class QNXARMStat64(ctypes.Structure): _pack_ = 8 def get_stat64_struct(ql: Qiling): - if ql.arch.bits == 64: + # MIPS64 (n64) has no separate stat64: it is reached only through the + # stat-family handlers that share pack_stat64_struct, and its layout is the + # 64-bit stat struct. handle it explicitly instead of warning + falling back + # to the (little-endian, 32-bit) x86 struct, which corrupts every field. + if ql.arch.bits == 64 and ql.arch.type != QL_ARCH.MIPS64: ql.log.warning(f"Trying to stat64 on a 64bit system with {ql.os.type} and {ql.arch.type}!") if ql.os.type == QL_OS.LINUX: if ql.arch.type == QL_ARCH.X86: return LinuxX86Stat64() - elif ql.arch.type == QL_ARCH.MIPS: - return LinuxMips32Stat64() + elif ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): + if ql.arch.bits == 64: + if ql.arch.endian == QL_ENDIAN.EL: + return LinuxMips64Stat() + else: + return LinuxMips64EBStat() + else: + return LinuxMips32Stat64() elif ql.arch.type == QL_ARCH.ARM: return LinuxARMStat64() elif ql.arch.type in (QL_ARCH.RISCV, QL_ARCH.RISCV64): @@ -1115,7 +1125,7 @@ def get_stat_struct(ql: Qiling): return LinuxX8664Stat() elif ql.arch.type == QL_ARCH.X86: return LinuxX86Stat() - elif ql.arch.type == QL_ARCH.MIPS: + elif ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): if ql.arch.bits == 64: if ql.arch.endian == QL_ENDIAN.EL: return LinuxMips64Stat() @@ -1413,17 +1423,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 +1476,9 @@ def minor(dev): 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 diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py index 09b86c380..3a073e4f6 100644 --- a/qiling/os/posix/syscall/unistd.py +++ b/qiling/os/posix/syscall/unistd.py @@ -751,7 +751,7 @@ def ql_syscall_pipe(ql: Qiling, pipefd: int): ql.os.fd[idx1] = rd ql.os.fd[idx2] = wd - if ql.arch.type == QL_ARCH.MIPS: + if ql.arch.type in (QL_ARCH.MIPS, QL_ARCH.MIPS64): ql.arch.regs.v1 = idx2 regreturn = idx1 else: @@ -926,7 +926,14 @@ def _type_mapping(ent): d_off = 0 d_name = (result.name if isinstance(result, os.DirEntry) else result._str).encode() + b'\x00' d_type = _type_mapping(result) - d_reclen = n + n + 2 + len(d_name) + 1 + + # The kernel rounds each record up to the alignment of its leading + # (pointer-sized) d_ino, so the next record's d_ino stays aligned. + # Without this, strict-alignment guests (e.g. MIPS) fault with an + # unaligned load when walking the buffer; x86 tolerates it, which is + # why it was never caught. + unaligned_reclen = n + n + 2 + len(d_name) + 1 + d_reclen = (unaligned_reclen + (n - 1)) & ~(n - 1) # TODO: Dirty fix for X8664 MACOS 11.6 APFS # For some reason MACOS return int value is 64bit @@ -936,6 +943,8 @@ def _type_mapping(ent): packed_d_ino = (ql.pack64(d_ino), n) if is_64: + # getdents64 places d_type *before* d_name, so the trailing pad + # bytes are inert. fields = ( (ql.pack64(d_ino), n), (ql.pack64(d_off), n), @@ -944,11 +953,18 @@ def _type_mapping(ent): (d_name, len(d_name)) ) else: + # legacy linux_dirent stores d_type in the record's last byte + # (offset d_reclen-1), so the alignment padding goes *between* + # d_name and d_type to keep d_type there. padding without + # relocating d_type is what got the earlier blanket fix + # (PR #1419) reverted. + pad = d_reclen - unaligned_reclen fields = ( packed_d_ino, (ql.pack(d_off), n), (ql.pack16(d_reclen), 2), (d_name, len(d_name)), + (b'\x00' * pad, pad), (d_type, 1) ) diff --git a/qiling/utils.py b/qiling/utils.py index 72e5c32df..57ef3e628 100644 --- a/qiling/utils.py +++ b/qiling/utils.py @@ -148,7 +148,8 @@ def __emu_env_from_elf(path: str) -> Tuple[Optional[QL_ARCH], Optional[QL_OS], O machines64 = { EM_X86_64 : QL_ARCH.X8664, EM_AARCH64 : QL_ARCH.ARM64, - EM_RISCV : QL_ARCH.RISCV64 + EM_RISCV : QL_ARCH.RISCV64, + EM_MIPS : QL_ARCH.MIPS64 } classes = { @@ -381,8 +382,8 @@ def select_arch(archtype: QL_ARCH, cputype: Optional[QL_CPU], endian: QL_ENDIAN, kwargs['endian'] = endian kwargs['thumb'] = thumb - # set endianness for mips arch - elif archtype is QL_ARCH.MIPS: + # set endianness for mips archs + elif archtype in (QL_ARCH.MIPS, QL_ARCH.MIPS64): kwargs['endian'] = endian module = { @@ -392,6 +393,7 @@ def select_arch(archtype: QL_ARCH, cputype: Optional[QL_CPU], endian: QL_ENDIAN, QL_ARCH.ARM : r'arm', QL_ARCH.ARM64 : r'arm64', QL_ARCH.MIPS : r'mips', + QL_ARCH.MIPS64 : r'mips64', QL_ARCH.CORTEX_M : r'cortex_m', QL_ARCH.RISCV : r'riscv', QL_ARCH.RISCV64 : r'riscv64', diff --git a/tests/test_debugger.py b/tests/test_debugger.py index c8df8ae0c..147d56060 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -127,6 +127,39 @@ def gdb_test_client(): ql.run() del ql + def test_gdbdebug_mips64(self): + ql = Qiling(["../examples/rootfs/mips64_linux/bin/mips64_hello_static"], "../examples/rootfs/mips64_linux", verbose=QL_VERBOSE.DEBUG) + ql.debugger = True + + # some random command test just to make sure we covered most of the command + def gdb_test_client(): + # yield to allow ql to launch its gdbserver + time.sleep(1.337 * 2) + + with SimpleGdbClient('127.0.0.1', 9999) as client: + client.send('qSupported:multiprocess+;swbreak+;hwbreak+;qRelocInsn+;fork-events+;vfork-events+;exec-events+;vContSupported+;QThreadEvents+;no-resumed+;xmlRegisters=mips') + client.send('vMustReplyEmpty') + client.send('QStartNoAckMode') + client.send('Hgp0.0') + # exercise the 64-bit target description served for MIPS64 + client.send('qXfer:features:read:target.xml:0,1000') + client.send('?') + client.send('qC') + client.send('g') + # pc (regnum 37) and sp (regnum 29) as 64-bit values + client.send('p25') + client.send('p1d') + client.send('c') + client.send('k') + + # yield to make sure ql gdbserver has enough time to receive our last command + time.sleep(1.337) + + threading.Thread(target=gdb_test_client, daemon=True).start() + + ql.run() + del ql + def test_gdbdebug_armeb(self): ql = Qiling(["../examples/rootfs/armeb_linux/bin/armeb_hello"], "../examples/rootfs/armeb_linux", verbose=QL_VERBOSE.DEBUG) ql.debugger = True diff --git a/tests/test_elf.py b/tests/test_elf.py index 2798028b7..bf3304add 100644 --- a/tests/test_elf.py +++ b/tests/test_elf.py @@ -19,7 +19,7 @@ from typing import Any, Sequence from qiling import Qiling -from qiling.const import QL_ARCH, QL_OS, QL_INTERCEPT, QL_STOP, QL_VERBOSE +from qiling.const import QL_ARCH, QL_OS, QL_INTERCEPT, QL_STOP, QL_VERBOSE, QL_ENDIAN from qiling.exception import * from qiling.extensions import pipe from qiling.os.const import STRING @@ -404,6 +404,33 @@ def my_puts(ql: Qiling): ql.run() del ql + def test_setsockopt_mips_so_rcvbuf(self): + # MIPS uses its own SO_* numbering (SO_RCVBUF = 0x1002, not the generic + # 0x08). The MIPS socket-option table had the wrong values, so e.g. + # busybox ping's setsockopt(SOL_SOCKET, SO_RCVBUF, ...) raised + # NotImplementedError ("Could not convert emulated socket option 4098"). + from qiling.const import QL_ENDIAN + from qiling.os.posix.syscall.socket import ql_syscall_socket, ql_syscall_setsockopt + + 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) + + AF_INET, SOCK_DGRAM = 2, 2 + SOL_SOCKET, SO_RCVBUF = 0xffff, 0x1002 # MIPS values + + fd = ql_syscall_socket(ql, AF_INET, SOCK_DGRAM, 0) + self.assertGreaterEqual(fd, 0) + + base = 0x100000 + ql.mem.map(base, 0x1000) + ql.mem.write_ptr(base, 16384, 4) # requested SO_RCVBUF size + + # must map SO_RCVBUF (0x1002) to the host option and succeed, not raise + self.assertEqual(ql_syscall_setsockopt(ql, fd, SOL_SOCKET, SO_RCVBUF, base, 4), 0) + + del ql + def test_elf_linux_arm_static(self): ql = Qiling(["../examples/rootfs/arm_linux/bin/arm_hello_static"], "../examples/rootfs/arm_linux", verbose=QL_VERBOSE.DEFAULT) all_mem = ql.mem.save() @@ -433,6 +460,136 @@ def test_elf_linux_mips32eb_static(self): ql.run() del ql + # statically-linked MIPS64 hello binaries. they link at the standard MIPS64 + # base 0x120000000 (>4GB), which only loads and runs thanks to QlArchMIPS64's + # virtual-TLB identity mapping, and they execute a movz instruction, which only + # decodes on a MIPS64R2-class core (unicorn's default MIPS64 core is an old + # MIPS III that lacks it) - so this also guards the default MIPS64 CPU model. + def test_elf_linux_mips64eb_static(self): + ql = Qiling(["../examples/rootfs/mips64_linux/bin/mips64_hello_static"], "../examples/rootfs/mips64_linux", verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + ql.run() + + self.assertEqual(ql.os.stdout.read(), b'Hello, MIPS64 from Qiling!\n') + + del ql + + def test_elf_linux_mips64el_static(self): + ql = Qiling(["../examples/rootfs/mips64el_linux/bin/mips64el_hello_static"], "../examples/rootfs/mips64el_linux", verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + ql.run() + + self.assertEqual(ql.os.stdout.read(), b'Hello, MIPS64 from Qiling!\n') + + del ql + + # Dynamically-linked MIPS64 BE. Unlike the *_static binaries above, this one + # is a non-PIE ET_EXEC that must be brought up by the dynamic loader and have + # libc.so.6 mapped and relocated before main() runs. It lives in its own + # rootfs (mips64_linux_buildroot) carrying the matching Buildroot ld.so.1 + + # glibc it was built against: both dynamic binaries hard-code the interpreter + # path /lib64/ld.so.1, so the Buildroot and default-mips64_linux glibc + # environments cannot share a single tree. + def test_elf_linux_mips64eb_buildroot_dynamic(self): + ql = Qiling(["../examples/rootfs/mips64_linux_buildroot/bin/mips64_hello_buildroot"], + "../examples/rootfs/mips64_linux_buildroot", verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + ql.run() + + self.assertEqual(ql.os.stdout.read(), b'hello from mips64 n64\n') + + del ql + + # Regression for statx() byte order on big-endian guests: statx must report + # the S_IFDIR bit for a directory. The statx struct used to be emitted + # little-endian regardless of guest endianness, so stx_mode was byte-swapped + # on MIPS64 EB and a directory looked like a plain file (src: statx.c). + def test_elf_linux_mips64eb_statx(self): + ql = Qiling(["../examples/rootfs/mips64_linux/bin/mips64_statx", "/"], "../examples/rootfs/mips64_linux", verbose=QL_VERBOSE.OFF) + ql.os.stdout = pipe.SimpleOutStream(1) + ql.run() + + self.assertEqual(ql.os.stdout.read(), b'DIR\n') + + del ql + + def test_elf_linux_mips64eb_stat64(self): + # the stat-family handlers route through pack_stat64_struct, whose + # get_stat64_struct lacked a MIPS64 branch and fell back to the + # little-endian x86 stat64 struct, byte-swapping/misplacing every field + # on a big-endian 64-bit guest. exercise the handler directly and check + # that the directory mode reads back correctly under the MIPS64 BE struct. + import stat as _stat + from qiling.os.posix.syscall.stat import ql_syscall_stat64, ql_syscall_lstat, LinuxMips64EBStat + + ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS64, ostype=QL_OS.LINUX, + endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips64_linux", verbose=QL_VERBOSE.OFF) + + base = 0x100000 + ql.mem.map(base, 0x4000) + buf = base + 0x100 + mode_off = LinuxMips64EBStat.st_mode.offset + + # both stat64 and the plain lstat handler (which busybox `ls -la` uses) + # share get_stat64_struct + for handler in (ql_syscall_stat64, ql_syscall_lstat): + ql.mem.write(base, b"/\x00") + ql.mem.write(buf, b"\x00" * 0x100) + self.assertEqual(handler(ql, base, buf), 0) + + mode = int.from_bytes(ql.mem.read(buf + mode_off, 4), 'big') + self.assertTrue(_stat.S_ISDIR(mode), f'{handler.__name__}: mode 0o{mode:o} is not a directory') + + del ql + + def test_elf_linux_mips64eb_getdents(self): + # legacy getdents (used by older glibc, e.g. the Octeon SDK) packed each + # linux_dirent record with a word-sized d_ino but did not align the + # record, so on n64 (8-byte d_ino) the next record's d_ino landed + # unaligned and a strict-alignment guest faulted while walking the + # buffer. records must be padded to the d_ino alignment, with d_type + # kept in the record's last byte (offset d_reclen-1). + from qiling.os.posix.syscall.fcntl import ql_syscall_open + from qiling.os.posix.syscall.unistd import ql_syscall_getdents + + ql = Qiling(code=b"\x00\x00\x00\x00", archtype=QL_ARCH.MIPS64, ostype=QL_OS.LINUX, + endian=QL_ENDIAN.EB, rootfs="../examples/rootfs/mips64_linux", verbose=QL_VERBOSE.OFF) + + base = 0x100000 + ql.mem.map(base, 0x8000) + ql.mem.write(base, b"/\x00") + buf = base + 0x1000 + + fd = ql_syscall_open(ql, base, 0, 0) + n = ql_syscall_getdents(ql, fd, buf, 0x2000) + self.assertGreater(n, 0) + + data = bytes(ql.mem.read(buf, n)) + seen = [] + off = 0 + while off < n: + # each record must start 8-byte aligned (n64 d_ino alignment) + self.assertEqual(off % 8, 0, f'record at {off} is not 8-byte aligned') + + d_reclen = int.from_bytes(data[off + 16:off + 18], 'big') # d_reclen @ 2*8 + self.assertGreater(d_reclen, 0) + + name = data[off + 18:data.index(b'\x00', off + 18)].decode() + seen.append(name) + + # legacy getdents stores d_type in the record's last byte; '.' and + # '..' must report DT_DIR (4) + if name in ('.', '..'): + self.assertEqual(data[off + d_reclen - 1], 4, f'{name}: d_type not DT_DIR') + + off += d_reclen + + self.assertEqual(off, n) # records tile the buffer exactly + self.assertIn('.', seen) + self.assertIn('..', seen) + + del ql + @staticmethod def random_generator(length: int): chars = string.ascii_uppercase + string.digits diff --git a/tests/test_elf_multithread.py b/tests/test_elf_multithread.py index 8923efa1b..d81811d42 100644 --- a/tests/test_elf_multithread.py +++ b/tests/test_elf_multithread.py @@ -30,6 +30,7 @@ ARM64_LINUX_ROOTFS = fr'{BASE_ROOTFS}/arm64_linux' MIPSEB_LINUX_ROOTFS = fr'{BASE_ROOTFS}/mips32_linux' MIPSEL_LINUX_ROOTFS = fr'{BASE_ROOTFS}/mips32el_linux' +MIPS64EB_LINUX_ROOTFS = fr'{BASE_ROOTFS}/mips64_linux' class ELFTest(unittest.TestCase): @@ -162,6 +163,25 @@ def check_write(ql: Qiling, fd: int, write_buf, count: int): 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_mips64eb(self): + 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.extend(content.decode().splitlines()) + + ql = Qiling([fr'{MIPS64EB_LINUX_ROOTFS}/bin/mips64_multithreading'], MIPS64EB_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.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): logged: List[str] = [] diff --git a/tests/test_shellcode.py b/tests/test_shellcode.py index 9b2b4e054..5ea75acc9 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_INTERCEPT, QL_VERBOSE, QL_ENDIAN # test = bytes.fromhex('cccc') @@ -22,6 +22,22 @@ 2f7368 ''') +# MIPS64 n64 shellcode: write(1, "MIPS64 hello\n", 13) then exit_group(0). +# uses the n64 syscall numbers (write=5001, exit_group=5205) and computes the +# message address with a bal/daddiu pc-relative trick. assembled with binutils +# mips64-linux-gnuabi64-as. +MIPS64EB_LIN = bytes.fromhex(''' + 2402138924040001041100010000000067e500182406000d0000000c24021455 + 240400000000000c4d49505336342068656c6c6f0a +''') + +# little-endian counterpart of MIPS64EB_LIN: instruction words byte-swapped, +# the trailing string left as-is +MIPS64EL_LIN = bytes.fromhex(''' + 891302240100042401001104000000001800e5670d0006240c00000055140224 + 000004240c0000004d49505336342068656c6c6f0a +''') + X86_WIN = bytes.fromhex(''' fce8820000006089e531c0648b50308b520c8b52148b72280fb74a2631ffac3c 617c022c20c1cf0d01c7e2f252578b52108b4a3c8b4c1178e34801d1518b5920 @@ -105,6 +121,16 @@ def test_linux_mips32(self): ql.os.set_syscall('execve', graceful_execve, QL_INTERCEPT.EXIT) ql.run() + def test_linux_mips64eb(self): + print("Linux MIPS 64bit EB Shellcode") + ql = Qiling(code=MIPS64EB_LIN, archtype=QL_ARCH.MIPS64, ostype=QL_OS.LINUX, endian=QL_ENDIAN.EB, verbose=QL_VERBOSE.OFF) + ql.run() + + def test_linux_mips64el(self): + print("Linux MIPS 64bit EL Shellcode") + ql = Qiling(code=MIPS64EL_LIN, archtype=QL_ARCH.MIPS64, ostype=QL_OS.LINUX, endian=QL_ENDIAN.EL, verbose=QL_VERBOSE.OFF) + ql.run() + # This shellcode needs to be changed to something non-blocking def test_linux_arm(self): print("Linux ARM 32bit Shellcode")