From c363dab2fb5cf3487ccc256f69ed219dd6b28707 Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 14 May 2025 13:04:15 +0300 Subject: [PATCH 01/54] Tidy up coverage classes and methods --- qiling/extensions/coverage/formats/base.py | 19 ++++--- qiling/extensions/coverage/formats/drcov.py | 49 ++++++++++--------- .../coverage/formats/drcov_exact.py | 7 +-- qiling/extensions/coverage/formats/ezcov.py | 35 +++++++------ qiling/extensions/tracing/formats/base.py | 18 +++---- 5 files changed, 64 insertions(+), 64 deletions(-) diff --git a/qiling/extensions/coverage/formats/base.py b/qiling/extensions/coverage/formats/base.py index d9fe7c34..32a6fc55 100644 --- a/qiling/extensions/coverage/formats/base.py +++ b/qiling/extensions/coverage/formats/base.py @@ -4,8 +4,11 @@ # from abc import ABC, abstractmethod +from typing import TYPE_CHECKING -from qiling import Qiling + +if TYPE_CHECKING: + from qiling import Qiling class QlBaseCoverage(ABC): @@ -15,25 +18,21 @@ class QlBaseCoverage(ABC): all the methods marked with the @abstractmethod decorator. """ + FORMAT_NAME: str + def __init__(self, ql: Qiling): super().__init__() self.ql = ql - @property - @staticmethod @abstractmethod - def FORMAT_NAME() -> str: - raise NotImplementedError - - @abstractmethod - def activate(self): + def activate(self) -> None: pass @abstractmethod - def deactivate(self): + def deactivate(self) -> None: pass @abstractmethod - def dump_coverage(self, coverage_file: str): + def dump_coverage(self, coverage_file: str) -> None: pass diff --git a/qiling/extensions/coverage/formats/drcov.py b/qiling/extensions/coverage/formats/drcov.py index 51a42194..d541837d 100644 --- a/qiling/extensions/coverage/formats/drcov.py +++ b/qiling/extensions/coverage/formats/drcov.py @@ -3,12 +3,18 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -from ctypes import Structure -from ctypes import c_uint32, c_uint16 +from __future__ import annotations + +from ctypes import Structure, c_uint32, c_uint16 +from typing import TYPE_CHECKING, BinaryIO from .base import QlBaseCoverage +if TYPE_CHECKING: + from qiling import Qiling + + # Adapted from https://www.ayrx.me/drcov-file-format class bb_entry(Structure): _fields_ = [ @@ -29,7 +35,7 @@ class QlDrCoverage(QlBaseCoverage): FORMAT_NAME = "drcov" - def __init__(self, ql): + def __init__(self, ql: Qiling): super().__init__(ql) self.drcov_version = 2 @@ -37,28 +43,27 @@ class QlDrCoverage(QlBaseCoverage): self.basic_blocks = [] self.bb_callback = None - @staticmethod - def block_callback(ql, address, size, self): - for mod_id, mod in enumerate(ql.loader.images): - if mod.base <= address <= mod.end: - ent = bb_entry(address - mod.base, size, mod_id) - self.basic_blocks.append(ent) - break + def activate(self) -> None: + self.bb_callback = self.ql.hook_block(self.block_callback) - def activate(self): - self.bb_callback = self.ql.hook_block(self.block_callback, user_data=self) + def deactivate(self) -> None: + if self.bb_callback: + self.ql.hook_del(self.bb_callback) - def deactivate(self): - self.ql.hook_del(self.bb_callback) + def dump_coverage(self, coverage_file: str) -> None: + def __write_line(bio: BinaryIO, line: str) -> None: + bio.write(f'{line}\n'.encode()) - def dump_coverage(self, coverage_file): with open(coverage_file, "wb") as cov: - cov.write(f"DRCOV VERSION: {self.drcov_version}\n".encode()) - cov.write(f"DRCOV FLAVOR: {self.drcov_flavor}\n".encode()) - cov.write(f"Module Table: version {self.drcov_version}, count {len(self.ql.loader.images)}\n".encode()) - cov.write("Columns: id, base, end, entry, checksum, timestamp, path\n".encode()) + __write_line(cov, f"DRCOV VERSION: {self.drcov_version}") + __write_line(cov, f"DRCOV FLAVOR: {self.drcov_flavor}") + __write_line(cov, f"Module Table: version {self.drcov_version}, count {len(self.ql.loader.images)}") + __write_line(cov, "Columns: id, base, end, entry, checksum, timestamp, path") + for mod_id, mod in enumerate(self. ql.loader.images): - cov.write(f"{mod_id}, {mod.base}, {mod.end}, 0, 0, 0, {mod.path}\n".encode()) - cov.write(f"BB Table: {len(self.basic_blocks)} bbs\n".encode()) - for bb in self.basic_blocks: + __write_line(cov, f"{mod_id}, {mod.base}, {mod.end}, 0, 0, 0, {mod.path}") + + __write_line(cov, f"BB Table: {len(self.basic_blocks)} bbs") + + for bb in self.basic_blocks.values(): cov.write(bytes(bb)) diff --git a/qiling/extensions/coverage/formats/drcov_exact.py b/qiling/extensions/coverage/formats/drcov_exact.py index 685c6c04..afb6e844 100644 --- a/qiling/extensions/coverage/formats/drcov_exact.py +++ b/qiling/extensions/coverage/formats/drcov_exact.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # @@ -17,10 +17,7 @@ class QlDrCoverageExact(QlDrCoverage): FORMAT_NAME = "drcov_exact" - def __init__(self, ql): - super().__init__(ql) - - def activate(self): + def activate(self) -> None: # We treat every instruction as a block on its own. self.bb_callback = self.ql.hook_code(self.block_callback, user_data=self) \ No newline at end of file diff --git a/qiling/extensions/coverage/formats/ezcov.py b/qiling/extensions/coverage/formats/ezcov.py index b2521869..2fab67e9 100644 --- a/qiling/extensions/coverage/formats/ezcov.py +++ b/qiling/extensions/coverage/formats/ezcov.py @@ -1,14 +1,20 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +from __future__ import annotations from collections import namedtuple from os.path import basename +from typing import TYPE_CHECKING, List from .base import QlBaseCoverage +if TYPE_CHECKING: + from qiling import Qiling + + # Adapted from https://github.com/nccgroup/Cartographer/blob/main/EZCOV.md#coverage-data class bb_entry(namedtuple('bb_entry', 'offset size mod_id')): def csvline(self): @@ -27,27 +33,24 @@ class QlEzCoverage(QlBaseCoverage): FORMAT_NAME = "ezcov" - def __init__(self, ql): + def __init__(self, ql: Qiling): super().__init__(ql) + self.ezcov_version = 1 - self.ezcov_flavor = 'ezcov' - self.basic_blocks = [] - self.bb_callback = None + self.ezcov_flavor = 'ezcov' + self.basic_blocks: List[bb_entry] = [] + self.bb_callback = None - @staticmethod - def block_callback(ql, address, size, self): - mod = ql.loader.find_containing_image(address) - if mod is not None: - ent = bb_entry(address - mod.base, size, basename(mod.path)) - self.basic_blocks.append(ent) + def block_callback(self, ql: Qiling, address: int, size: int): - def activate(self): - self.bb_callback = self.ql.hook_block(self.block_callback, user_data=self) + def activate(self) -> None: + self.bb_callback = self.ql.hook_block(self.block_callback) - def deactivate(self): - self.ql.hook_del(self.bb_callback) + def deactivate(self) -> None: + if self.bb_callback: + self.ql.hook_del(self.bb_callback) - def dump_coverage(self, coverage_file): + def dump_coverage(self, coverage_file: str) -> None: with open(coverage_file, "w") as cov: cov.write(f"EZCOV VERSION: {self.ezcov_version}\n") cov.write("# Qiling EZCOV exporter tool\n") diff --git a/qiling/extensions/tracing/formats/base.py b/qiling/extensions/tracing/formats/base.py index 14594434..dbb9c78a 100644 --- a/qiling/extensions/tracing/formats/base.py +++ b/qiling/extensions/tracing/formats/base.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # This code structure is copied and modified from the coverage extension @@ -12,24 +12,20 @@ class QlBaseTrace(ABC): To add support for a new coverage format, just derive from this class and implement all the methods marked with the @abstractmethod decorator. """ - + + FORMAT_NAME: str + def __init__(self): super().__init__() - @property - @staticmethod @abstractmethod - def FORMAT_NAME(): - raise NotImplementedError - - @abstractmethod - def activate(self): + def activate(self) -> None: pass @abstractmethod - def deactivate(self): + def deactivate(self) -> None: pass @abstractmethod - def dump_trace(self, trace_file): + def dump_trace(self, trace_file: str) -> None: pass \ No newline at end of file From be824ad0d4671347c6750331c54212933f6aaf1c Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 14 May 2025 13:05:53 +0300 Subject: [PATCH 02/54] Remove drcove bb dups and speed it up --- qiling/extensions/coverage/formats/drcov.py | 32 +++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/qiling/extensions/coverage/formats/drcov.py b/qiling/extensions/coverage/formats/drcov.py index d541837d..bed0f870 100644 --- a/qiling/extensions/coverage/formats/drcov.py +++ b/qiling/extensions/coverage/formats/drcov.py @@ -6,13 +6,15 @@ from __future__ import annotations from ctypes import Structure, c_uint32, c_uint16 -from typing import TYPE_CHECKING, BinaryIO +from functools import lru_cache +from typing import TYPE_CHECKING, BinaryIO, Dict, Tuple from .base import QlBaseCoverage if TYPE_CHECKING: from qiling import Qiling + from qiling.loader.loader import QlLoader # Adapted from https://www.ayrx.me/drcov-file-format @@ -40,9 +42,35 @@ class QlDrCoverage(QlBaseCoverage): self.drcov_version = 2 self.drcov_flavor = 'drcov' - self.basic_blocks = [] + self.basic_blocks: Dict[int, bb_entry] = {} self.bb_callback = None + @lru_cache(maxsize=64) + def _get_img_base(self, loader: QlLoader, address: int) -> Tuple[int, int]: + """Retrieve the containing image of a given address. + + Addresses are expected to be aligned to page boundary, and cached for faster retrieval. + """ + + return next((i, img.base) for i, img in enumerate(loader.images) if img.base <= address < img.end) + + def block_callback(self, ql: Qiling, address: int, size: int): + if address not in self.basic_blocks: + try: + # we rely on the fact that images are allocated on page size boundary and + # use it to speed up image retrieval. we align the basic block address to + # page boundary, knowing basic blocks within the same page belong to the + # same image. then we use the aligned address to retreive the containing + # image. returned values are cached so subsequent retrievals for basic + # blocks within the same page will return the cached value instead of + # going through the retreival process again (up to maxsize cached pages) + + i, img_base = self._get_img_base(ql.loader, address & ~(0x1000 - 1)) + except StopIteration: + pass + else: + self.basic_blocks[address] = bb_entry(address - img_base, size, i) + def activate(self) -> None: self.bb_callback = self.ql.hook_block(self.block_callback) From 104a3f3399c13cdf9f4eb487fcad8d3d5aa8eded Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 14 May 2025 13:06:07 +0300 Subject: [PATCH 03/54] Additional housekeeping --- .../coverage/formats/drcov_exact.py | 3 +-- qiling/extensions/coverage/formats/ezcov.py | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/qiling/extensions/coverage/formats/drcov_exact.py b/qiling/extensions/coverage/formats/drcov_exact.py index afb6e844..4f708278 100644 --- a/qiling/extensions/coverage/formats/drcov_exact.py +++ b/qiling/extensions/coverage/formats/drcov_exact.py @@ -19,5 +19,4 @@ class QlDrCoverageExact(QlDrCoverage): def activate(self) -> None: # We treat every instruction as a block on its own. - self.bb_callback = self.ql.hook_code(self.block_callback, user_data=self) - \ No newline at end of file + self.bb_callback = self.ql.hook_code(self.block_callback) diff --git a/qiling/extensions/coverage/formats/ezcov.py b/qiling/extensions/coverage/formats/ezcov.py index 2fab67e9..46290e4c 100644 --- a/qiling/extensions/coverage/formats/ezcov.py +++ b/qiling/extensions/coverage/formats/ezcov.py @@ -4,9 +4,9 @@ # from __future__ import annotations -from collections import namedtuple -from os.path import basename -from typing import TYPE_CHECKING, List + +import os +from typing import Any, TYPE_CHECKING, List, NamedTuple from .base import QlBaseCoverage @@ -16,10 +16,15 @@ if TYPE_CHECKING: # Adapted from https://github.com/nccgroup/Cartographer/blob/main/EZCOV.md#coverage-data -class bb_entry(namedtuple('bb_entry', 'offset size mod_id')): - def csvline(self): - offset = '0x{:08x}'.format(self.offset) +class bb_entry(NamedTuple): + offset: int + size: int + mod_id: Any + + def as_csv(self) -> str: + offset = f'{self.offset:#010x}' mod_id = f"[ {self.mod_id if self.mod_id is not None else ''} ]" + return f"{offset},{self.size},{mod_id}\n" class QlEzCoverage(QlBaseCoverage): @@ -42,6 +47,10 @@ class QlEzCoverage(QlBaseCoverage): self.bb_callback = None def block_callback(self, ql: Qiling, address: int, size: int): + img = ql.loader.find_containing_image(address) + + if img is not None: + self.basic_blocks.append(bb_entry(address - img.base, size, os.path.basename(img.path))) def activate(self) -> None: self.bb_callback = self.ql.hook_block(self.block_callback) @@ -54,5 +63,5 @@ class QlEzCoverage(QlBaseCoverage): with open(coverage_file, "w") as cov: cov.write(f"EZCOV VERSION: {self.ezcov_version}\n") cov.write("# Qiling EZCOV exporter tool\n") - for bb in self.basic_blocks: - cov.write(bb.csvline()) \ No newline at end of file + + cov.writelines(bb.as_csv() for bb in self.basic_blocks) From 43b8b3c5088bb69dd58b8319142f46039b2a39e9 Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 14 May 2025 13:23:44 +0300 Subject: [PATCH 04/54] Add forgotten import --- qiling/extensions/coverage/formats/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiling/extensions/coverage/formats/base.py b/qiling/extensions/coverage/formats/base.py index 32a6fc55..4ca162cb 100644 --- a/qiling/extensions/coverage/formats/base.py +++ b/qiling/extensions/coverage/formats/base.py @@ -3,6 +3,8 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +from __future__ import annotations + from abc import ABC, abstractmethod from typing import TYPE_CHECKING From 70255afd5a5a31ea36b69fd20b832065ab9070fb Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 14 May 2025 13:24:16 +0300 Subject: [PATCH 05/54] Have blob loader contain an image --- qiling/loader/blob.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index 382dbb33..b8831a55 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -4,9 +4,10 @@ # from qiling import Qiling -from qiling.loader.loader import QlLoader +from qiling.loader.loader import QlLoader, Image from qiling.os.memory import QlMemoryHeap + class QlLoaderBLOB(QlLoader): def __init__(self, ql: Qiling): super().__init__(ql) @@ -16,13 +17,19 @@ class QlLoaderBLOB(QlLoader): def run(self): self.load_address = self.ql.os.entry_point # for consistency - self.ql.mem.map(self.ql.os.entry_point, self.ql.os.code_ram_size, info="[code]") - self.ql.mem.write(self.ql.os.entry_point, self.ql.code) + code_begins = self.load_address + code_size = self.ql.os.code_ram_size + code_ends = code_begins + code_size - heap_address = self.ql.os.entry_point + self.ql.os.code_ram_size + self.ql.mem.map(code_begins, code_size, info="[code]") + self.ql.mem.write(code_begins, self.ql.code) + + # allow image-related functionalities + self.images.append(Image(code_begins, code_ends, 'blob_code')) + + # FIXME: heap starts above end of ram?? + heap_base = code_ends heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16) - self.ql.os.heap = QlMemoryHeap(self.ql, heap_address, heap_address + heap_size) + self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) - self.ql.arch.regs.arch_sp = heap_address - 0x1000 - - return + self.ql.arch.regs.arch_sp = code_ends - 0x1000 From b1ad033720ca6df4659acfbb3eae2f3d050181f4 Mon Sep 17 00:00:00 2001 From: elicn Date: Sat, 21 Jun 2025 22:39:08 +0300 Subject: [PATCH 06/54] Have qdb use xPSR for Cortex M instead of CPSR --- qiling/debugger/qdb/arch/arch_arm.py | 6 +++++- .../debugger/qdb/branch_predictor/branch_predictor_arm.py | 2 +- qiling/debugger/qdb/render/render_arm.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qiling/debugger/qdb/arch/arch_arm.py b/qiling/debugger/qdb/arch/arch_arm.py index cbf63c2a..72a2979d 100644 --- a/qiling/debugger/qdb/arch/arch_arm.py +++ b/qiling/debugger/qdb/arch/arch_arm.py @@ -3,12 +3,14 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -from typing import Dict, Optional +from typing import ClassVar, Dict, Optional from .arch import Arch class ArchARM(Arch): + _flags_reg: ClassVar[str] = 'cpsr' + def __init__(self) -> None: regs = ( 'r0', 'r1', 'r2', 'r3', @@ -134,6 +136,8 @@ class ArchARM(Arch): class ArchCORTEX_M(ArchARM): + _flags_reg: ClassVar[str] = 'xpsr' + def __init__(self): super().__init__() diff --git a/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py b/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py index ea5dde0e..eda5163a 100644 --- a/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py +++ b/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py @@ -31,7 +31,7 @@ class BranchPredictorARM(BranchPredictor, ArchARM): """Get flags map of CPSR. """ - cpsr = self.read_reg('cpsr') + cpsr = self.read_reg(self._flags_reg) return ( (cpsr & (0b1 << 28)) != 0, # V, overflow flag diff --git a/qiling/debugger/qdb/render/render_arm.py b/qiling/debugger/qdb/render/render_arm.py index 5f5adb50..0e042f7f 100644 --- a/qiling/debugger/qdb/render/render_arm.py +++ b/qiling/debugger/qdb/render/render_arm.py @@ -15,7 +15,7 @@ class ContextRenderARM(ContextRender, ArchARM): """ def print_mode_info(self) -> None: - cpsr = self.read_reg('cpsr') + cpsr = self.read_reg(self._flags_reg) flags = ArchARM.get_flags(cpsr) mode = ArchARM.get_mode(cpsr) From 95d7b8f6d9087727219231660f7bc658c7cc2a2f Mon Sep 17 00:00:00 2001 From: elicn Date: Sat, 21 Jun 2025 22:39:37 +0300 Subject: [PATCH 07/54] Generalize code to meet both Cortex A and M --- .../qdb/branch_predictor/branch_predictor_arm.py | 12 ++++++------ qiling/debugger/qdb/render/render_arm.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py b/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py index eda5163a..f3cd0529 100644 --- a/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py +++ b/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py @@ -27,8 +27,8 @@ class BranchPredictorARM(BranchPredictor, ArchARM): stop = 'udf' - def get_cpsr(self) -> Tuple[bool, bool, bool, bool]: - """Get flags map of CPSR. + def get_cond_flags(self) -> Tuple[bool, bool, bool, bool]: + """Get condition status flags from CPSR / xPSR. """ cpsr = self.read_reg(self._flags_reg) @@ -122,9 +122,9 @@ class BranchPredictorARM(BranchPredictor, ArchARM): def __is_taken(cc: int) -> Tuple[bool, Tuple[bool, ...]]: pred = predicate[cc] - cpsr = self.get_cpsr() + flags = self.get_cond_flags() - return pred(*cpsr), cpsr + return pred(*flags), flags # conditions predicate selector predicate: Dict[int, Callable[..., bool]] = { @@ -215,13 +215,13 @@ class BranchPredictorARM(BranchPredictor, ArchARM): where = __parse_op(operands[1], **msize[suffix]) elif iname in binop: - going, cpsr = __is_taken(insn.cc) + going, flags = __is_taken(insn.cc) if going: operator = binop[iname] op1 = __parse_op(operands[1]) op2 = __parse_op(operands[2]) - carry = int(cpsr[1]) + carry = int(flags[1]) where = (op1 and op2) and operator(op1, op2, carry) diff --git a/qiling/debugger/qdb/render/render_arm.py b/qiling/debugger/qdb/render/render_arm.py index 0e042f7f..f08e39fa 100644 --- a/qiling/debugger/qdb/render/render_arm.py +++ b/qiling/debugger/qdb/render/render_arm.py @@ -3,7 +3,7 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -from typing import Iterator, Optional +from typing import Iterator from .render import Render, ContextRender from ..arch import ArchARM, ArchCORTEX_M From 58487ee10e7bd8f40cb9b4b4d22e6c36682263b2 Mon Sep 17 00:00:00 2001 From: elicn Date: Tue, 24 Jun 2025 17:35:13 +0300 Subject: [PATCH 08/54] Add missing CortexM base class --- qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py b/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py index f3cd0529..553c5ed7 100644 --- a/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py +++ b/qiling/debugger/qdb/branch_predictor/branch_predictor_arm.py @@ -17,7 +17,7 @@ from capstone.arm_const import ( from unicorn.arm_const import UC_ARM_REG_PC from .branch_predictor import BranchPredictor, Prophecy -from ..arch import ArchARM +from ..arch import ArchARM, ArchCORTEX_M from ..misc import InvalidInsn @@ -261,6 +261,6 @@ class BranchPredictorARM(BranchPredictor, ArchARM): return Prophecy(going, where) -class BranchPredictorCORTEX_M(BranchPredictorARM): +class BranchPredictorCORTEX_M(BranchPredictorARM, ArchCORTEX_M): """Branch Predictor for ARM Cortex-M. """ From 327f475ba3d7fb9bed89c3c516b3ad1c8bd10d86 Mon Sep 17 00:00:00 2001 From: elicn Date: Sun, 6 Jul 2025 18:23:07 +0300 Subject: [PATCH 09/54] Decouple BLOB entry point and load address --- examples/uboot_bin.ql | 1 + qiling/loader/blob.py | 5 ++++- qiling/os/blob/blob.py | 12 ++++++++---- qiling/os/os.py | 1 + tests/profiles/uboot_bin.ql | 1 + 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/uboot_bin.ql b/examples/uboot_bin.ql index b7f7216c..c33a7d23 100644 --- a/examples/uboot_bin.ql +++ b/examples/uboot_bin.ql @@ -1,5 +1,6 @@ [CODE] ram_size = 0xa00000 +load_address = 0x80800000 entry_point = 0x80800000 heap_size = 0x300000 diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index b8831a55..f17b80a9 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -15,7 +15,8 @@ class QlLoaderBLOB(QlLoader): self.load_address = 0 def run(self): - self.load_address = self.ql.os.entry_point # for consistency + self.load_address = self.ql.os.load_address + self.entry_point = self.ql.os.entry_point code_begins = self.load_address code_size = self.ql.os.code_ram_size @@ -28,8 +29,10 @@ class QlLoaderBLOB(QlLoader): self.images.append(Image(code_begins, code_ends, 'blob_code')) # FIXME: heap starts above end of ram?? + # FIXME: heap should be allocated by OS, not loader heap_base = code_ends heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16) self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) + # FIXME: stack pointer should be a configurable profile setting self.ql.arch.regs.arch_sp = code_ends - 0x1000 diff --git a/qiling/os/blob/blob.py b/qiling/os/blob/blob.py index 02e6f94d..e4a02256 100644 --- a/qiling/os/blob/blob.py +++ b/qiling/os/blob/blob.py @@ -9,6 +9,7 @@ from qiling.const import QL_ARCH, QL_OS from qiling.os.fcall import QlFunctionCall from qiling.os.os import QlOs + class QlOsBlob(QlOs): """ QlOsBlob for bare barines. @@ -21,7 +22,7 @@ class QlOsBlob(QlOs): type = QL_OS.BLOB def __init__(self, ql: Qiling): - super(QlOsBlob, self).__init__(ql) + super().__init__(ql) self.ql = ql @@ -39,11 +40,14 @@ class QlOsBlob(QlOs): self.fcall = QlFunctionCall(ql, cc) def run(self): - if self.ql.entry_point: + # if entry point was set explicitly, override the default one + if self.ql.entry_point is not None: self.entry_point = self.ql.entry_point - self.exit_point = self.ql.loader.load_address + len(self.ql.code) - if self.ql.exit_point: + self.exit_point = self.load_address + len(self.ql.code) + + # if exit point was set explicitly, override the default one + if self.ql.exit_point is not None: self.exit_point = self.ql.exit_point self.ql.emu_start(self.entry_point, self.exit_point, self.ql.timeout, self.ql.count) diff --git a/qiling/os/os.py b/qiling/os/os.py index 636e089c..dd9f3856 100644 --- a/qiling/os/os.py +++ b/qiling/os/os.py @@ -89,6 +89,7 @@ class QlOs: if self.ql.code: # this shellcode entrypoint does not work for windows # windows shellcode entry point will comes from pe loader + self.load_address = self.profile.getint('CODE', 'load_address') self.entry_point = self.profile.getint('CODE', 'entry_point') self.code_ram_size = self.profile.getint('CODE', 'ram_size') diff --git a/tests/profiles/uboot_bin.ql b/tests/profiles/uboot_bin.ql index b7f7216c..c33a7d23 100644 --- a/tests/profiles/uboot_bin.ql +++ b/tests/profiles/uboot_bin.ql @@ -1,5 +1,6 @@ [CODE] ram_size = 0xa00000 +load_address = 0x80800000 entry_point = 0x80800000 heap_size = 0x300000 From d2b4867f21c9c913cce38da1248a61834132d9b7 Mon Sep 17 00:00:00 2001 From: elicn Date: Sun, 6 Jul 2025 18:23:36 +0300 Subject: [PATCH 10/54] Adjust test and example --- examples/hello_arm_uboot.py | 104 ++++++++++++++++++++---------------- tests/test_blob.py | 40 +++++++++----- 2 files changed, 87 insertions(+), 57 deletions(-) diff --git a/examples/hello_arm_uboot.py b/examples/hello_arm_uboot.py index 9544fe0e..f97ff6ef 100644 --- a/examples/hello_arm_uboot.py +++ b/examples/hello_arm_uboot.py @@ -8,68 +8,82 @@ sys.path.append("..") from qiling.core import Qiling from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE -from qiling.os.const import STRING +from qiling.os.const import STRING, SIZE_T, POINTER -def get_kaimendaji_password(): - def my_getenv(ql: Qiling): - env = { - "ID" : b"000000000000000", - "ethaddr" : b"11:22:33:44:55:66" - } +def my_getenv(ql: Qiling): + env = { + "ID" : b"000000000000000", + "ethaddr" : b"11:22:33:44:55:66" + } - params = ql.os.resolve_fcall_params({'key': STRING}) - value = env.get(params["key"], b"") + params = ql.os.resolve_fcall_params({'key': STRING}) + value = env.get(params["key"], b"") - value_addr = ql.os.heap.alloc(len(value)) - ql.mem.write(value_addr, value) + value_addr = ql.os.heap.alloc(len(value)) + ql.mem.write(value_addr, value) - ql.arch.regs.r0 = value_addr - ql.arch.regs.arch_pc = ql.arch.regs.lr + ql.arch.regs.r0 = value_addr + ql.arch.regs.arch_pc = ql.arch.regs.lr - def get_password(ql: Qiling): - password_raw = ql.mem.read(ql.arch.regs.r0, ql.arch.regs.r2) - password = '' - for item in password_raw: - if 0 <= item <= 9: - password += chr(item + 48) - else: - password += chr(item + 87) +def get_password(ql: Qiling): + # we land on a memcmp call, where the real password is being compared to + # the one provided by the user. we can follow the arguments to read the + # real password - print("The password is: %s" % password) + params = ql.os.resolve_fcall_params({ + 'ptr1': POINTER, # points to real password + 'ptr2': POINTER, # points to user provided password + 'size': SIZE_T # comparison length + }) - def partial_run_init(ql: Qiling): - # argv prepare - ql.arch.regs.arch_sp -= 0x30 - arg0_ptr = ql.arch.regs.arch_sp - ql.mem.write(arg0_ptr, b"kaimendaji") + ptr1 = params['ptr1'] + size = params['size'] - ql.arch.regs.arch_sp -= 0x10 - arg1_ptr = ql.arch.regs.arch_sp - ql.mem.write(arg1_ptr, b"000000") # arbitrary password + password_raw = ql.mem.read(ptr1, size) - ql.arch.regs.arch_sp -= 0x20 - argv_ptr = ql.arch.regs.arch_sp - ql.mem.write_ptr(argv_ptr, arg0_ptr) - ql.mem.write_ptr(argv_ptr + ql.arch.pointersize, arg1_ptr) + def __hex_digit(ch: int) -> str: + off = ord('0') if ch in range(10) else ord('a') - 10 - ql.arch.regs.r2 = 2 - ql.arch.regs.r3 = argv_ptr + return chr(ch + off) - with open("../examples/rootfs/blob/u-boot.bin.img", "rb") as f: - uboot_code = f.read() + # should be: "013f1f" + password = "".join(__hex_digit(ch) for ch in password_raw) - ql = Qiling(code=uboot_code[0x40:], archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="uboot_bin.ql", verbose=QL_VERBOSE.OFF) + print(f'The password is: {password}') - image_base_addr = ql.loader.load_address - ql.hook_address(my_getenv, image_base_addr + 0x13AC0) - ql.hook_address(get_password, image_base_addr + 0x48634) - partial_run_init(ql) +def partial_run_init(ql: Qiling): + # argv prepare + ql.arch.regs.arch_sp -= 0x30 + arg0_ptr = ql.arch.regs.arch_sp + ql.mem.write(arg0_ptr, b"kaimendaji") - ql.run(image_base_addr + 0x486B4, image_base_addr + 0x48718) + ql.arch.regs.arch_sp -= 0x10 + arg1_ptr = ql.arch.regs.arch_sp + ql.mem.write(arg1_ptr, b"000000") # arbitrary password + + ql.arch.regs.arch_sp -= 0x20 + argv_ptr = ql.arch.regs.arch_sp + ql.mem.write_ptr(argv_ptr, arg0_ptr) + ql.mem.write_ptr(argv_ptr + ql.arch.pointersize, arg1_ptr) + + ql.arch.regs.r2 = 2 + ql.arch.regs.r3 = argv_ptr if __name__ == "__main__": - get_kaimendaji_password() + with open("../examples/rootfs/blob/u-boot.bin.img", "rb") as f: + uboot_code = f.read() + + ql = Qiling(code=uboot_code[0x40:], archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="uboot_bin.ql", verbose=QL_VERBOSE.DEBUG) + + imgbase = ql.loader.images[0].base + + ql.hook_address(my_getenv, imgbase + 0x13AC0) + ql.hook_address(get_password, imgbase + 0x48634) + + partial_run_init(ql) + + ql.run(imgbase + 0x486B4, imgbase + 0x48718) diff --git a/tests/test_blob.py b/tests/test_blob.py index bc191dc1..33e35751 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -10,13 +10,17 @@ sys.path.append("..") from qiling.core import Qiling from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE -from qiling.os.const import STRING +from qiling.os.const import STRING, POINTER, SIZE_T class BlobTest(unittest.TestCase): def test_uboot_arm(self): - def my_getenv(ql, *args, **kwargs): - env = {"ID": b"000000000000000", "ethaddr": b"11:22:33:44:55:66"} + def my_getenv(ql: Qiling): + env = { + "ID": b"000000000000000", + "ethaddr": b"11:22:33:44:55:66" + } + params = ql.os.resolve_fcall_params({'key': STRING}) value = env.get(params["key"], b"") @@ -26,12 +30,23 @@ class BlobTest(unittest.TestCase): ql.arch.regs.r0 = value_addr ql.arch.regs.arch_pc = ql.arch.regs.lr - def check_password(ql, *args, **kwargs): - passwd_output = ql.mem.read(ql.arch.regs.r0, ql.arch.regs.r2) - passwd_input = ql.mem.read(ql.arch.regs.r1, ql.arch.regs.r2) - self.assertEqual(passwd_output, passwd_input) + def check_password(ql: Qiling): + params = ql.os.resolve_fcall_params({ + 'ptr1': POINTER, # points to real password + 'ptr2': POINTER, # points to user provided password + 'size': SIZE_T # comparison length + }) - def partial_run_init(ql): + ptr1 = params['ptr1'] + ptr2 = params['ptr2'] + size = params['size'] + + real_password = ql.mem.read(ptr1, size) + user_password = ql.mem.read(ptr2, size) + + self.assertSequenceEqual(real_password, user_password, seq_type=bytearray) + + def partial_run_init(ql: Qiling): # argv prepare ql.arch.regs.arch_sp -= 0x30 arg0_ptr = ql.arch.regs.arch_sp @@ -56,13 +71,14 @@ class BlobTest(unittest.TestCase): ql = Qiling(code=uboot_code[0x40:], archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="profiles/uboot_bin.ql", verbose=QL_VERBOSE.DEBUG) - image_base_addr = ql.loader.load_address - ql.hook_address(my_getenv, image_base_addr + 0x13AC0) - ql.hook_address(check_password, image_base_addr + 0x48634) + imgbase = ql.loader.images[0].base + + ql.hook_address(my_getenv, imgbase + 0x13AC0) + ql.hook_address(check_password, imgbase + 0x48634) partial_run_init(ql) - ql.run(image_base_addr + 0x486B4, image_base_addr + 0x48718) + ql.run(imgbase + 0x486B4, imgbase + 0x48718) del ql From dacc8e001201471fb1e4ff0d8dcfbb56c0e6cac0 Mon Sep 17 00:00:00 2001 From: elicn Date: Sun, 6 Jul 2025 18:24:01 +0300 Subject: [PATCH 11/54] Remove redundant BLOB case --- qiling/debugger/qdb/qdb.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/qiling/debugger/qdb/qdb.py b/qiling/debugger/qdb/qdb.py index ae942139..7182b46d 100644 --- a/qiling/debugger/qdb/qdb.py +++ b/qiling/debugger/qdb/qdb.py @@ -136,10 +136,7 @@ class QlQdb(Cmd, QlDebugger): with self.__set_temp(self.ql, 'verbose', QL_VERBOSE.DISABLED): self.ql.os.run() - if self.ql.os.type is QL_OS.BLOB: - self.ql.loader.entry_point = self.ql.loader.load_address - - elif init_hook: + if init_hook: for each_hook in init_hook: self.do_breakpoint(each_hook) From bb21658e32eeb1ceaa45894126282ffbb1ff3601 Mon Sep 17 00:00:00 2001 From: elicn Date: Sun, 6 Jul 2025 18:25:00 +0300 Subject: [PATCH 12/54] Fix QDB crash on allocation boundaries --- qiling/debugger/qdb/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/debugger/qdb/context.py b/qiling/debugger/qdb/context.py index 34919754..344f7563 100644 --- a/qiling/debugger/qdb/context.py +++ b/qiling/debugger/qdb/context.py @@ -57,7 +57,7 @@ class Context: """Helper function for disassembling. """ - insn_bytes = self.read_insn(address) + insn_bytes = self.read_insn(address) or b'' insn = None if insn_bytes: @@ -75,7 +75,7 @@ class Context: A tuple of: instruction address, size, mnemonic and operands """ - insn_bytes = self.read_insn(address) + insn_bytes = self.read_insn(address) or b'' insn = None if insn_bytes: From e68c923abd685aeeb5212bff6ef204a0b6ad7c10 Mon Sep 17 00:00:00 2001 From: elicn Date: Sun, 6 Jul 2025 18:42:59 +0300 Subject: [PATCH 13/54] Patch profiles to meet new required key --- qiling/profiles/linux.ql | 1 + qiling/profiles/windows.ql | 1 + 2 files changed, 2 insertions(+) diff --git a/qiling/profiles/linux.ql b/qiling/profiles/linux.ql index eac82348..de828a32 100644 --- a/qiling/profiles/linux.ql +++ b/qiling/profiles/linux.ql @@ -1,6 +1,7 @@ [CODE] # ram_size 0xa00000 is 10MB ram_size = 0xa00000 +load_address = 0x1000000 entry_point = 0x1000000 diff --git a/qiling/profiles/windows.ql b/qiling/profiles/windows.ql index 15cc2f39..ac2cc668 100644 --- a/qiling/profiles/windows.ql +++ b/qiling/profiles/windows.ql @@ -23,6 +23,7 @@ KI_USER_SHARED_DATA = 0x7ffe0000 [CODE] # ram_size 0xa00000 is 10MB ram_size = 0xa00000 +load_address = 0x1000000 entry_point = 0x1000000 [KERNEL] From 7088d22a73eb165ac196cffdefab9ed68099de79 Mon Sep 17 00:00:00 2001 From: elicn Date: Sun, 6 Jul 2025 18:44:59 +0300 Subject: [PATCH 14/54] Allow tests to import relatively --- tests/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/__init__.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b From 8cd866b58b8f48310418b01ff536c6c92dd65a18 Mon Sep 17 00:00:00 2001 From: xwings Date: Mon, 7 Jul 2025 13:22:21 +0800 Subject: [PATCH 15/54] slight changes to readme --- README.md | 213 +++++++++++++++++++++++------------------------------- 1 file changed, 90 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 63f542f3..b17b3066 100644 --- a/README.md +++ b/README.md @@ -8,120 +8,68 @@

-[Qiling's use case, blog and related work](https://github.com/qilingframework/qiling/issues/134) +# Qiling Framework -Qiling is an advanced binary emulation framework, with the following features: +Qiling is an advanced binary emulation framework that allows you to emulate and sandbox code in an isolated environment across multiple platforms and architectures. Built on top of Unicorn Engine, Qiling provides a higher-level framework that understands operating system contexts, executable formats, and dynamic linking. -- Emulate multi-platforms: Windows, macOS, Linux, Android, BSD, UEFI, DOS, MBR. -- Emulate multi-architectures: 8086, X86, X86_64, ARM, ARM64, MIPS, RISC-V, PowerPC. -- Support multiple file formats: PE, Mach-O, ELF, COM, MBR. -- Support Windows Driver (.sys), Linux Kernel Module (.ko) & macOS Kernel (.kext) via [Demigod](https://groundx.io/demigod/). -- Emulates & sandbox code in an isolated environment. -- Provides a fully configurable sandbox. -- Provides in-depth memory, register, OS level and filesystem level API. -- Fine-grain instrumentation: allows hooks at various levels - (instruction/basic-block/memory-access/exception/syscall/IO/etc.) -- Provides virtual machine level API such as saving and restoring the current execution state. -- Supports cross architecture and platform debugging capabilities. -- Built-in debugger with reverse debugging capability. -- Allows dynamic hot patch on-the-fly running code, including the loaded library. -- True framework in Python, making it easy to build customized security analysis tools on top. +## Table of Contents -Qiling also made its way to various international conferences. +- [Features](#features) +- [Use Cases](#use-cases) +- [Quick Start](#quick-start) + - [Installation](#installation) + - [Basic Usage](#basic-usage) +- [Qiling vs. Other Emulators](#qiling-vs-other-emulators) + - [Qiling vs. Unicorn Engine](#qiling-vs-unicorn-engine) + - [Qiling vs. QEMU User Mode](#qiling-vs-qemu-user-mode) +- [Examples](#examples) +- [Qltool](#qltool) +- [Contributing](#contributing) +- [License](#license) +- [Contact](#contact) +- [Core Developers & Contributors](#core-developers--contributors) -2022: -- [Black Hat, EU](https://www.blackhat.com/eu-22/arsenal/schedule/#reversing-mcu-with-firmware-emulation-29553) -- [Black Hat, MEA](https://blackhatmea.com/node/724) +## Features -2021: -- [Black Hat, USA](https://www.blackhat.com/us-21/arsenal/schedule/index.html#bringing-the-x-complete-re-experience-to-smart-contract-24119) -- [Hack In The Box, Amsterdam](https://conference.hitb.org/hitbsecconf2021ams/sessions/when-qiling-framework-meets-symbolic-execution/) -- [Black Hat, Asia](https://www.blackhat.com/asia-21/arsenal/schedule/index.html#qiling-smart-analysis-for-smart-contract-22643) +- **Multi-platform Emulation**: Windows, macOS, Linux, Android, BSD, UEFI, DOS, MBR. +- **Multi-architecture Emulation**: 8086, X86, X86_64, ARM, ARM64, MIPS, RISC-V, PowerPC. +- **Multiple File Format Support**: PE, Mach-O, ELF, COM, MBR. +- **Kernel Module Emulation**: Supports Windows Driver (.sys), Linux Kernel Module (.ko) & macOS Kernel (.kext) via [Demigod](https://groundx.io/demigod/). +- **Isolated Sandboxing**: Emulates & sandboxes code in an isolated environment with a fully configurable sandbox. +- **In-depth API**: Provides in-depth memory, register, OS level, and filesystem level API. +- **Fine-grain Instrumentation**: Allows hooks at various levels (instruction/basic-block/memory-access/exception/syscall/IO/etc.). +- **Virtual Machine Level API**: Supports saving and restoring the current execution state. +- **Debugging Capabilities**: Supports cross-architecture and platform debugging, including a built-in debugger with reverse debugging capability. +- **Dynamic Hot Patching**: Allows dynamic hot patching of on-the-fly running code, including loaded libraries. +- **Python Framework**: A true framework in Python, making it easy to build customized security analysis tools. -2020: -- [Black Hat, Europe](https://www.blackhat.com/eu-20/arsenal/schedule/index.html#qiling-framework-deep-dive-into-obfuscated-binary-analysis-21781) -- [Black Hat, USA](https://www.blackhat.com/us-20/arsenal/schedule/index.html#qiling-framework-from-dark-to-dawn-----enlightening-the-analysis-of-the-most-mysterious-iot-firmware--21062) -- [Black Hat, USA (Demigod)](https://www.blackhat.com/us-20/briefings/schedule/#demigod-the-art-of-emulating-kernel-rootkits-20009) -- [Black Hat, Asia](https://www.blackhat.com/asia-20/arsenal/schedule/index.html#qiling-lightweight-advanced-binary-analyzer-19245) -- [Hack In The Box, Lockdown 001](https://conference.hitb.org/lockdown-livestream/) -- [Hack In The Box, Lockdown 002](https://conference.hitb.org/hitb-lockdown002/virtual-labs/virtual-lab-qiling-framework-learn-how-to-build-a-fuzzer-based-on-a-1day-bug/) -- [Hack In The Box, Cyberweek](https://cyberweek.ae/2020/lab-qiling-framework/) -- [Nullcon](https://nullcon.net/website/goa-2020/speakers/kaijern-lau.php) - -2019: +## Use Cases -- [DEFCON, USA](https://www.defcon.org/html/defcon-27/dc-27-demolabs.html#QiLing) -- [Hitcon](https://hitcon.org/2019/CMT/agenda) -- [Zeronights](https://zeronights.ru/report-en/qiling-io-advanced-binary-emulation-framework/) +Qiling has been presented at various international conferences, showcasing its versatility in: +- Binary analysis and reverse engineering. +- Malware analysis and sandboxing. +- Firmware analysis and emulation. +- Security research and vulnerability discovery. +- CTF challenges and exploit development. -Qiling is backed by [Unicorn Engine](http://www.unicorn-engine.org). +For more details on Qiling's use cases, blog posts, and related work, please refer to [Qiling's use case, blog and related work](https://github.com/qilingframework/qiling/issues/134). -Visit our [website](https://www.qiling.io) for more information. +## Quick Start ---- -#### License +### Installation -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. +Qiling requires Python 3.8 or newer. You can install it using pip: ---- +```bash +pip install qiling +``` -#### Qiling vs. other Emulators +For more detailed installation instructions and dependencies, please refer to the [official documentation](https://docs.qiling.io/en/latest/install/). -There are many open-source emulators, but two projects closest to Qiling -are [Unicorn](http://www.unicorn-engine.org) & [QEMU user mode](https://qemu.org). -This section explains the main differences of Qiling against them. +### Basic Usage -##### Qiling vs. Unicorn engine - -Built on top of Unicorn, but Qiling & Unicorn are two different animals. - -- Unicorn is just a CPU emulator, so it focuses on emulating CPU instructions, - that can understand emulator memory. - Beyond that, Unicorn is not aware of higher level concepts, such as dynamic - libraries, system calls, I/O handling or executable formats like PE, Mach-O - or ELF. As a result, Unicorn can only emulate raw machine instructions, - without Operating System (OS) context. -- Qiling is designed as a higher level framework, that leverages Unicorn to - emulate CPU instructions, but can understand OS: it has executable format - loaders (for PE, Mach-O & ELF currently), dynamic linkers (so we can - load & relocate shared libraries), syscall & IO handlers. For this reason, - Qiling can run executable binary without requiring its native OS. - -##### Qiling vs. QEMU user mode - -QEMU user mode does a similar thing to our emulator, that is, to emulate whole -executable binaries in a cross-architecture way. -However, Qiling offers some important differences against QEMU user mode: - -- Qiling is a true analysis framework, - that allows you to build your own dynamic analysis tools on top (in Python). - Meanwhile, QEMU is just a tool, not a framework. -- Qiling can perform dynamic instrumentation, and can even hot patch code at - runtime. QEMU does neither. -- Not only working cross-architecture, Qiling is also cross-platform. - For example, you can run Linux ELF file on top of Windows. - In contrast, QEMU user mode only runs binary of the same OS, such as Linux - ELF on Linux, due to the way it forwards syscall from emulated code to - native OS. -- Qiling supports more platforms, including Windows, macOS, Linux & BSD. QEMU - user mode can only handle Linux & BSD. - ---- - -#### Installation - -Please see [setup guide](https://docs.qiling.io/en/latest/install/) file for how to install Qiling Framework. - ---- - -#### Examples - -The example below shows how to use Qiling framework in the most -straightforward way to emulate a Windows executable. +The example below shows how to use Qiling framework in the most straightforward way to emulate a Windows executable. ```python from qiling import Qiling @@ -135,8 +83,30 @@ if __name__ == "__main__": ql.run() ``` -- The following example shows how a Windows crackme may be patched dynamically - to make it always display the “Congratulation” dialog. +## Qiling vs. Other Emulators + +There are many open-source emulators, but two projects closest to Qiling are [Unicorn](http://www.unicorn-engine.org) & [QEMU user mode](https://qemu.org). This section explains the main differences of Qiling against them. + +### Qiling vs. Unicorn Engine + +Built on top of Unicorn, but Qiling & Unicorn are two different animals. + +- **Unicorn** is just a CPU emulator, so it focuses on emulating CPU instructions, that can understand emulator memory. Beyond that, Unicorn is not aware of higher level concepts, such as dynamic libraries, system calls, I/O handling or executable formats like PE, Mach-O or ELF. As a result, Unicorn can only emulate raw machine instructions, without Operating System (OS) context. +- **Qiling** is designed as a higher level framework, that leverages Unicorn to emulate CPU instructions, but can understand OS: it has executable format loaders (for PE, Mach-O & ELF currently), dynamic linkers (so we can load & relocate shared libraries), syscall & IO handlers. For this reason, Qiling can run executable binary without requiring its native OS. + +### Qiling vs. QEMU User Mode + +QEMU user mode does a similar thing to our emulator, that is, to emulate whole executable binaries in a cross-architecture way. +However, Qiling offers some important differences against QEMU user mode: + +- **Qiling is a true analysis framework**, that allows you to build your own dynamic analysis tools on top (in Python). Meanwhile, QEMU is just a tool, not a framework. +- **Qiling can perform dynamic instrumentation**, and can even hot patch code at runtime. QEMU does neither. +- Not only working cross-architecture, **Qiling is also cross-platform**. For example, you can run Linux ELF file on top of Windows. In contrast, QEMU user mode only runs binary of the same OS, such as Linux ELF on Linux, due to the way it forwards syscall from emulated code to native OS. +- **Qiling supports more platforms**, including Windows, macOS, Linux & BSD. QEMU user mode can only handle Linux & BSD. + +## Examples + +- The following example shows how a Windows crackme may be patched dynamically to make it always display the “Congratulation” dialog. ```python from qiling import Qiling @@ -177,15 +147,13 @@ The below YouTube video shows how the above example works. #### Emulating ARM router firmware on Ubuntu x64 host -Qiling Framework hot-patches and emulates an ARM router's `/usr/bin/httpd` on -an x86_64 Ubuntu host. +Qiling Framework hot-patches and emulates an ARM router's `/usr/bin/httpd` on an x86_64 Ubuntu host. -[![Qiling Tutorial: Emulating and Fuzz ARM router firmware](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/fuzzer.jpg?raw=true)](https://www.youtube.com/watch?v=e3_T3KLh2NU) +[![Qiling Tutorial: Emulating and Fuzz ARM router firmware](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/fuzzer.jpg?raw=true)](https://www.youtube.com/watch?v=e3_T3KLhNUs) #### Qiling's IDA Pro Plugin: Instrument and Decrypt Mirai's Secret -This video demonstrates how Qiling's IDA Pro plugin can make IDA Pro run with -Qiling instrumentation engine. +This video demonstrates how Qiling's IDA Pro plugin can make IDA Pro run with Qiling instrumentation engine. [![Qiling's IDA Pro Plugin: Instrument and Decrypt Mirai's Secret](http://img.youtube.com/vi/ZWMWTq2WTXk/0.jpg)](http://www.youtube.com/watch?v=ZWMWTq2WTXk) @@ -195,63 +163,62 @@ Solving a simple CTF challenge with Qiling Framework and IDA Pro [![Solving a simple CTF challenge with Qiling Framework and IDA Pro](https://i.ytimg.com/vi/SPjVAt2FkKA/0.jpg)](https://www.youtube.com/watch?v=SPjVAt2FkKA) - #### Emulating MBR Qiling Framework emulates MBR [![Qiling DEMO: Emulating MBR](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/mbr.png?raw=true)](https://github.com/qilingframework/theme.qiling.io/blob/master/source/img/mbr.png?raw=true) ---- - -#### Qltool +## Qltool Qiling also provides a friendly tool named `qltool` to quickly emulate shellcode & executable binaries. With qltool, easy execution can be performed: - With shellcode: -``` +```bash $ ./qltool code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex ``` With binary file: -``` +```bash $ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs examples/rootfs/x8664_linux/ ``` With binary and GDB debugger enabled: -``` +```bash $ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux ``` With code coverage collection (UEFI only for now): -``` +```bash $ ./qltool run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --coverage-format drcov --coverage-file TcgPlatformSetupPolicy.cov ``` With JSON output (Windows, mainly): -``` +```bash $ ./qltool run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs examples/rootfs/x86_windows/ --console False --json ``` ---- +## Contributing -#### Contact +We welcome contributions from the community! If you're interested in contributing to Qiling Framework, please check out our [GitHub repository](https://github.com/qilingframework/qiling) and look for open issues or submit a pull request. -Get the latest info from our website https://www.qiling.io +## License -Contact us at email info@qiling.io, -via Twitter [@qiling_io](https://twitter.com/qiling_io). +This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. ---- +## Contact -#### Core developers, Key Contributors and etc. +Get the latest info from our website [https://www.qiling.io](https://www.qiling.io) -Please refer to [CREDITS.md](https://github.com/qilingframework/qiling/blob/dev/CREDITS.md). +Contact us at email [info@qiling.io](mailto:info@qiling.io), or via Twitter [@qiling_io](https://twitter.com/qiling_io). + +## Core Developers & Contributors + +Please refer to [CREDITS.md](https://github.com/qilingframework/qiling/blob/dev/CREDITS.md). \ No newline at end of file From 6a58ccc04ed7b885a9feca8e100aec5ec14c1a1b Mon Sep 17 00:00:00 2001 From: xwings Date: Mon, 7 Jul 2025 13:25:50 +0800 Subject: [PATCH 16/54] update readme with new wiku --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b17b3066..a3204570 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Documentation Status](https://readthedocs.org/projects/qilingframework/badge/?version=latest)](https://docs.qiling.io) +[![Documentation Status](https://github.com/qilingframework/qiling/wiki)](https://github.com/qilingframework/qiling/wiki) [![Downloads](https://pepy.tech/badge/qiling)](https://pepy.tech/project/qiling) [![Chat on Telegram](https://img.shields.io/badge/Chat%20on-Telegram-brightgreen.svg)](https://t.me/qilingframework) @@ -65,7 +65,7 @@ Qiling requires Python 3.8 or newer. You can install it using pip: pip install qiling ``` -For more detailed installation instructions and dependencies, please refer to the [official documentation](https://docs.qiling.io/en/latest/install/). +For more detailed installation instructions and dependencies, please refer to the [official documentation](https://github.com/qilingframework/qiling/wiki/Installation). ### Basic Usage From a1c3eaf4d245fcb48dca1dbf940f4bb15ead0736 Mon Sep 17 00:00:00 2001 From: xwings Date: Mon, 7 Jul 2025 13:28:12 +0800 Subject: [PATCH 17/54] update readme with new wiki --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index a3204570..8049a93a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Qiling is an advanced binary emulation framework that allows you to emulate and ## Table of Contents - [Features](#features) +- [Appearance](#Appearance) - [Use Cases](#use-cases) - [Quick Start](#quick-start) - [Installation](#installation) @@ -43,6 +44,35 @@ Qiling is an advanced binary emulation framework that allows you to emulate and - **Dynamic Hot Patching**: Allows dynamic hot patching of on-the-fly running code, including loaded libraries. - **Python Framework**: A true framework in Python, making it easy to build customized security analysis tools. +## Appearance + +Qiling also made its way to various international conferences. + +2022: +- [Black Hat, EU](https://www.blackhat.com/eu-22/arsenal/schedule/#reversing-mcu-with-firmware-emulation-29553) +- [Black Hat, MEA](https://blackhatmea.com/node/724) + +2021: +- [Black Hat, USA](https://www.blackhat.com/us-21/arsenal/schedule/index.html#bringing-the-x-complete-re-experience-to-smart-contract-24119) +- [Hack In The Box, Amsterdam](https://conference.hitb.org/hitbsecconf2021ams/sessions/when-qiling-framework-meets-symbolic-execution/) +- [Black Hat, Asia](https://www.blackhat.com/asia-21/arsenal/schedule/index.html#qiling-smart-analysis-for-smart-contract-22643) + +2020: +- [Black Hat, Europe](https://www.blackhat.com/eu-20/arsenal/schedule/index.html#qiling-framework-deep-dive-into-obfuscated-binary-analysis-21781) +- [Black Hat, USA](https://www.blackhat.com/us-20/arsenal/schedule/index.html#qiling-framework-from-dark-to-dawn-----enlightening-the-analysis-of-the-most-mysterious-iot-firmware--21062) +- [Black Hat, USA (Demigod)](https://www.blackhat.com/us-20/briefings/schedule/#demigod-the-art-of-emulating-kernel-rootkits-20009) +- [Black Hat, Asia](https://www.blackhat.com/asia-20/arsenal/schedule/index.html#qiling-lightweight-advanced-binary-analyzer-19245) +- [Hack In The Box, Lockdown 001](https://conference.hitb.org/lockdown-livestream/) +- [Hack In The Box, Lockdown 002](https://conference.hitb.org/hitb-lockdown002/virtual-labs/virtual-lab-qiling-framework-learn-how-to-build-a-fuzzer-based-on-a-1day-bug/) +- [Hack In The Box, Cyberweek](https://cyberweek.ae/2020/lab-qiling-framework/) +- [Nullcon](https://nullcon.net/website/goa-2020/speakers/kaijern-lau.php) + +2019: + +- [DEFCON, USA](https://www.defcon.org/html/defcon-27/dc-27-demolabs.html#QiLing) +- [Hitcon](https://hitcon.org/2019/CMT/agenda) +- [Zeronights](https://zeronights.ru/report-en/qiling-io-advanced-binary-emulation-framework/) + ## Use Cases Qiling has been presented at various international conferences, showcasing its versatility in: From b10c3eb2ea54edd76709dc286c1f81636296c72f Mon Sep 17 00:00:00 2001 From: xwings Date: Mon, 7 Jul 2025 13:28:36 +0800 Subject: [PATCH 18/54] Fix readme typo --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8049a93a..34a02ef6 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,6 @@ Qiling also made its way to various international conferences. - [Nullcon](https://nullcon.net/website/goa-2020/speakers/kaijern-lau.php) 2019: - - [DEFCON, USA](https://www.defcon.org/html/defcon-27/dc-27-demolabs.html#QiLing) - [Hitcon](https://hitcon.org/2019/CMT/agenda) - [Zeronights](https://zeronights.ru/report-en/qiling-io-advanced-binary-emulation-framework/) From 13e4569e276fdd9d1fe8f1394f9ef57b82a54083 Mon Sep 17 00:00:00 2001 From: elicn Date: Tue, 8 Jul 2025 18:08:33 +0300 Subject: [PATCH 19/54] Fix gdb regs reference for Cortex-M --- .../gdb/xml/{arm => cortex_m}/arm-m-profile.xml | 8 +++++++- qiling/debugger/gdb/xml/cortex_m/target.xml | 12 ++++++++++++ qiling/debugger/gdb/xmlregs.py | 9 ++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) rename qiling/debugger/gdb/xml/{arm => cortex_m}/arm-m-profile.xml (81%) create mode 100644 qiling/debugger/gdb/xml/cortex_m/target.xml diff --git a/qiling/debugger/gdb/xml/arm/arm-m-profile.xml b/qiling/debugger/gdb/xml/cortex_m/arm-m-profile.xml similarity index 81% rename from qiling/debugger/gdb/xml/arm/arm-m-profile.xml rename to qiling/debugger/gdb/xml/cortex_m/arm-m-profile.xml index f0584a20..a0707150 100644 --- a/qiling/debugger/gdb/xml/arm/arm-m-profile.xml +++ b/qiling/debugger/gdb/xml/cortex_m/arm-m-profile.xml @@ -25,4 +25,10 @@ - + + + + + + + \ No newline at end of file diff --git a/qiling/debugger/gdb/xml/cortex_m/target.xml b/qiling/debugger/gdb/xml/cortex_m/target.xml new file mode 100644 index 00000000..63591239 --- /dev/null +++ b/qiling/debugger/gdb/xml/cortex_m/target.xml @@ -0,0 +1,12 @@ + + + + + + armv7-m + + \ No newline at end of file diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index f569cd22..983ac6ef 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -13,14 +13,21 @@ from qiling.arch.arm_const import ( reg_map_q as arm_regs_q, reg_map_s as arm_regs_s ) + +from qiling.arch.cortex_m_const import ( + reg_map as conretx_m_regs +) + from qiling.arch.arm64_const import ( reg_map as arm64_regs, reg_map_v as arm64_regs_v, reg_map_fp as arm64_reg_map_fp ) + from qiling.arch.mips_const import ( reg_map as mips_regs_gpr ) + from qiling.arch.x86_const import ( reg_map_32 as x86_regs_32, reg_map_64 as x86_regs_64, @@ -133,7 +140,7 @@ class QlGdbFeatures: QL_ARCH.X86: dict(**x86_regs_32, **x86_regs_misc, **x86_regs_cr, **x86_regs_st, **x86_regs_xmm), QL_ARCH.X8664: dict(**x86_regs_64, **x86_regs_misc, **x86_regs_cr, **x86_regs_st, **x86_regs_xmm, **x86_regs_ymm), QL_ARCH.ARM: dict(**arm_regs, **arm_regs_vfp, **arm_regs_q, **arm_regs_s), - QL_ARCH.CORTEX_M: arm_regs, + QL_ARCH.CORTEX_M: dict(**conretx_m_regs), QL_ARCH.ARM64: dict(**arm64_regs, **arm64_regs_v, **arm64_reg_map_fp), QL_ARCH.MIPS: dict(**mips_regs_gpr) }[archtype] From 66f0fa366964b25dcd21f135aa8554fc9599d33d Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 9 Jul 2025 13:11:14 +0300 Subject: [PATCH 20/54] Typo fix --- qiling/debugger/gdb/xmlregs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 983ac6ef..1477d693 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -15,7 +15,7 @@ from qiling.arch.arm_const import ( ) from qiling.arch.cortex_m_const import ( - reg_map as conretx_m_regs + reg_map as coretx_m_regs ) from qiling.arch.arm64_const import ( @@ -140,7 +140,7 @@ class QlGdbFeatures: QL_ARCH.X86: dict(**x86_regs_32, **x86_regs_misc, **x86_regs_cr, **x86_regs_st, **x86_regs_xmm), QL_ARCH.X8664: dict(**x86_regs_64, **x86_regs_misc, **x86_regs_cr, **x86_regs_st, **x86_regs_xmm, **x86_regs_ymm), QL_ARCH.ARM: dict(**arm_regs, **arm_regs_vfp, **arm_regs_q, **arm_regs_s), - QL_ARCH.CORTEX_M: dict(**conretx_m_regs), + QL_ARCH.CORTEX_M: dict(**coretx_m_regs), QL_ARCH.ARM64: dict(**arm64_regs, **arm64_regs_v, **arm64_reg_map_fp), QL_ARCH.MIPS: dict(**mips_regs_gpr) }[archtype] From a2542f1dbca0c5761853a6a618cb95dd7cb7d174 Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 9 Jul 2025 14:50:11 +0300 Subject: [PATCH 21/54] Typo fix --- qiling/debugger/gdb/xmlregs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 1477d693..e1abf08d 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -15,7 +15,7 @@ from qiling.arch.arm_const import ( ) from qiling.arch.cortex_m_const import ( - reg_map as coretx_m_regs + reg_map as cortex_m_regs ) from qiling.arch.arm64_const import ( @@ -140,7 +140,7 @@ class QlGdbFeatures: QL_ARCH.X86: dict(**x86_regs_32, **x86_regs_misc, **x86_regs_cr, **x86_regs_st, **x86_regs_xmm), QL_ARCH.X8664: dict(**x86_regs_64, **x86_regs_misc, **x86_regs_cr, **x86_regs_st, **x86_regs_xmm, **x86_regs_ymm), QL_ARCH.ARM: dict(**arm_regs, **arm_regs_vfp, **arm_regs_q, **arm_regs_s), - QL_ARCH.CORTEX_M: dict(**coretx_m_regs), + 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) }[archtype] From d3cc47dc7ff5513c2b4d14695c2b4aa52a3283a1 Mon Sep 17 00:00:00 2001 From: technikelly <11539105+technikelly@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:55:50 -0400 Subject: [PATCH 22/54] adding support for raw binary blobs --- examples/blob_raw.ql | 4 ++ examples/hello_arm_blob_raw.py | 81 +++++++++++++++++++++++++++ examples/src/blob/Makefile | 57 +++++++++++++++++++ examples/src/blob/example_raw.c | 61 ++++++++++++++++++++ examples/src/blob/linker.ld | 45 +++++++++++++++ qiling/loader/blob.py | 44 +++++++++------ qiling/os/blob/blob.py | 7 ++- tests/profiles/blob_raw.ql | 4 ++ tests/test_blob.py | 99 +++++++++++++++++++++++++++++++++ 9 files changed, 382 insertions(+), 20 deletions(-) create mode 100644 examples/blob_raw.ql create mode 100644 examples/hello_arm_blob_raw.py create mode 100644 examples/src/blob/Makefile create mode 100644 examples/src/blob/example_raw.c create mode 100644 examples/src/blob/linker.ld create mode 100644 tests/profiles/blob_raw.ql diff --git a/examples/blob_raw.ql b/examples/blob_raw.ql new file mode 100644 index 00000000..164219a9 --- /dev/null +++ b/examples/blob_raw.ql @@ -0,0 +1,4 @@ +[BLOB_RAW] +load_address = 0x10000000 +image_size = 0xbc +image_name = example_raw.bin \ No newline at end of file diff --git a/examples/hello_arm_blob_raw.py b/examples/hello_arm_blob_raw.py new file mode 100644 index 00000000..6e44d6bf --- /dev/null +++ b/examples/hello_arm_blob_raw.py @@ -0,0 +1,81 @@ +############################################################################## +# Added example for raw binary blob +# Kelly Patterson - Cisco Talos +# Copyright (C) 2025 Cisco Systems Inc +############################################################################## +from qiling import Qiling +from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE +from qiling.extensions.coverage import utils as cov_utils + +BASE_ADDRESS = 0x10000000 +CHECKSUM_FUNC_ADDR = BASE_ADDRESS + 0x8 +END_ADDRESS = 0x100000ba +DATA_ADDR = 0xa0000000 # Arbitrary address for data +STACK_ADDR = 0xb0000000 # Arbitrary address for stack + +# Python implementation of the checksum function being emulated +def checksum_function(input_data_buffer: bytes): + expected_checksum_python = 0 + input_data_len = len(input_data_buffer) + if input_data_len >= 1 and input_data_buffer[0] == 0xDE: # MAGIC_VALUE_1 + for i in range(min(input_data_len, 4)): + expected_checksum_python += input_data_buffer[i] + expected_checksum_python += 0x10 + elif input_data_len >= 2 and input_data_buffer[1] == 0xAD: # MAGIC_VALUE_2 + for i in range(input_data_len): + expected_checksum_python ^= input_data_buffer[i] + expected_checksum_python += 0x20 + else: + for i in range(input_data_len): + expected_checksum_python += input_data_buffer[i] + expected_checksum_python &= 0xFF # Ensure it's a single byte + +def unmapped_handler(ql, type, addr, size, value): + + print(f"Unmapped Memory R/W, trying to access {hex(size)} bytes at {hex(addr)} from {hex(ql.arch.regs.pc)}") + +def emulate_checksum_function(input_data_buffer: bytes): + print(f"\n--- Testing with input: {input_data_buffer.hex()} ---") + + ql = Qiling(archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) + + input_data_len = len(input_data_buffer) + + # Map memory for the binary, data and stack + ql.mem.map(BASE_ADDRESS, 0x10000) + ql.mem.map(STACK_ADDR, 0x2000) + ql.mem.map(DATA_ADDR, ql.mem.align_up(input_data_len + 0x100)) # Map enough space for data + + # Write the binary into memory + ql.mem.write(BASE_ADDRESS, open("rootfs/blob/example_raw.bin", "rb").read()) + + # Write input data + ql.mem.write(DATA_ADDR, input_data_buffer) + + # Set up the stack pointer + ql.arch.regs.sp = STACK_ADDR + 0x2000 - 4 + # Set up argument registers + ql.arch.regs.r0 = DATA_ADDR + ql.arch.regs.r1 = input_data_len + + # Set the program counter to the function's entry point + ql.arch.regs.pc = CHECKSUM_FUNC_ADDR + + # Set the return address (LR) to a dummy address. + ql.arch.regs.lr = 0xbebebebe + + ql.hook_mem_unmapped(unmapped_handler) + #ql.debugger="gdb:127.0.0.1:9999" + + # Start emulation + print(f"Starting emulation at PC: {hex(ql.arch.regs.pc)}") + try: + ql.run(begin=CHECKSUM_FUNC_ADDR, end=END_ADDRESS) + except Exception as e: + print(f"Emulation error: {e}") + + print(f"Emulated checksum: {hex(ql.arch.regs.r0)}") + +if __name__ == "__main__": + data = b"\x01\x02\x03\x04\x05" # Example input data + emulate_checksum_function(data) \ No newline at end of file diff --git a/examples/src/blob/Makefile b/examples/src/blob/Makefile new file mode 100644 index 00000000..7f4252f7 --- /dev/null +++ b/examples/src/blob/Makefile @@ -0,0 +1,57 @@ +############################################################################## +# Added example for raw binary blob +# Kelly Patterson - Cisco Talos +# Copyright (C) 2025 Cisco Systems Inc +############################################################################## +# Makefile for Bare-Metal ARM Hash Calculator + +# --- Toolchain Definitions --- +TOOLCHAIN_PREFIX = arm-none-eabi + +# Compiler, Linker, and Objcopy executables +CC = $(TOOLCHAIN_PREFIX)-gcc +LD = $(TOOLCHAIN_PREFIX)-gcc +OBJCOPY = $(TOOLCHAIN_PREFIX)-objcopy + +# --- Source and Output Files --- +SRCS = example_raw.c +OBJS = $(SRCS:.c=.o) # Convert .c to .o +ELF = example_raw.elf +BIN = example_raw.bin + +# --- Linker Script --- +LDSCRIPT = linker.ld + +# --- Compiler Flags --- +CFLAGS = -c -O0 -mcpu=cortex-a7 -mthumb -ffreestanding -nostdlib + +# --- Linker Flags --- +LDFLAGS = -T $(LDSCRIPT) -nostdlib + +# --- Objcopy Flags --- +OBJCOPYFLAGS = -O binary + +# --- Default Target --- +.PHONY: all clean + +all: $(BIN) + +# Rule to build the raw binary (.bin) from the ELF file +$(BIN): $(ELF) + $(OBJCOPY) $(OBJCOPYFLAGS) $< $@ + @echo "Successfully created $(BIN)" + +# Rule to link the object file into an ELF executable +$(ELF): $(OBJS) $(LDSCRIPT) + $(LD) $(LDFLAGS) $(OBJS) -o $@ + @echo "Successfully linked $(ELF)" + +# Rule to compile the C source file into an object file +%.o: %.c + $(CC) $(CFLAGS) $< -o $@ + @echo "Successfully compiled $<" + +# --- Clean Rule --- +clean: + rm -f $(OBJS) $(ELF) $(BIN) + @echo "Cleaned build artifacts." diff --git a/examples/src/blob/example_raw.c b/examples/src/blob/example_raw.c new file mode 100644 index 00000000..5ac71cb3 --- /dev/null +++ b/examples/src/blob/example_raw.c @@ -0,0 +1,61 @@ + /* + * Added example for raw binary blob + * Kelly Patterson - Cisco Talos + * Copyright (C) 2025 Cisco Systems Inc + * + */ +// example_raw.c + +// Define some magic values +#define MAGIC_VALUE_1 0xDE +#define MAGIC_VALUE_2 0xAD + +// This function calculates a checksum with branches based on input data +// It takes a pointer to data and its length +// Returns the checksum (unsigned char to fit in a byte) +unsigned char calculate_checksum(const unsigned char *data, unsigned int length) { + unsigned char checksum = 0; + + // Branch 1: Check for MAGIC_VALUE_1 at the start + if (length >= 1 && data[0] == MAGIC_VALUE_1) { + // If first byte is MAGIC_VALUE_1, do a simple sum of first 4 bytes + // (or up to length if less than 4) + for (unsigned int i = 0; i < length && i < 4; i++) { + checksum += data[i]; + } + // Add a fixed offset to make this path distinct + checksum += 0x10; + } + // Branch 2: Check for MAGIC_VALUE_2 at the second byte + else if (length >= 2 && data[1] == MAGIC_VALUE_2) { + // If second byte is MAGIC_VALUE_2, do a XOR sum of all bytes + for (unsigned int i = 0; i < length; i++) { + checksum ^= data[i]; + } + // Add a fixed offset to make this path distinct + checksum += 0x20; + } + // Default Branch: Standard byte sum checksum + else { + for (unsigned int i = 0; i < length; i++) { + checksum += data[i]; + } + } + + return checksum; +} + +// Minimal entry point for bare-metal. +// This function will not be called directly during Qiling emulation, +// but it's needed for the linker to have an entry point. +__attribute__((section(".text.startup"))) +void _start() { + // In a real bare-metal application, this would initialize hardware, + // set up stacks, etc. For this example, it's just a placeholder. + // We'll call calculate_checksum directly from our Qiling script. + + while (1) { + // Do nothing, or perhaps put the CPU to sleep + asm volatile ("wfi"); // Wait For Interrupt (ARM instruction) + } +} \ No newline at end of file diff --git a/examples/src/blob/linker.ld b/examples/src/blob/linker.ld new file mode 100644 index 00000000..ea57d85e --- /dev/null +++ b/examples/src/blob/linker.ld @@ -0,0 +1,45 @@ +/* linker.ld */ + /* + * Added example for raw binary blob + * Kelly Patterson - Cisco Talos + * Copyright (C) 2025 Cisco Systems Inc + * + */ + +ENTRY(_start) /* Define the entry point of our program */ + +/* Define memory regions - simple RAM region for this example */ +MEMORY +{ + ram (rwx) : ORIGIN = 0x10000000, LENGTH = 64K /* 64KB of RAM for our program */ +} + +SECTIONS +{ + /* Define the start of our program in memory. + */ + . = 0x10000000; + + .text : { + KEEP(*(.text.startup)) /* Keep the _start function */ + *(.text) /* All other code */ + *(.text.*) + *(.rodata) /* Read-only data */ + *(.rodata.*) + . = ALIGN(4); + } > ram /* Place .text section in the 'ram' region */ + + .data : { + . = ALIGN(4); + *(.data) /* Initialized data */ + *(.data.*) + . = ALIGN(4); + } > ram + + .bss : { + . = ALIGN(4); + *(.bss) + *(.bss.*) + . = ALIGN(4); + } > ram +} \ No newline at end of file diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index f17b80a9..96168538 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -2,6 +2,9 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +# Added support for raw binary blob emulation +# Kelly Patterson - Cisco Talos +# Copyright (C) 2025 Cisco Systems Inc from qiling import Qiling from qiling.loader.loader import QlLoader, Image @@ -12,27 +15,32 @@ class QlLoaderBLOB(QlLoader): def __init__(self, ql: Qiling): super().__init__(ql) - self.load_address = 0 - def run(self): - self.load_address = self.ql.os.load_address - self.entry_point = self.ql.os.entry_point + if self.ql.os.profile.has_section("BLOB_RAW"): + # For raw binary blobs, user will handle memory mapping + self.load_address = int(self.ql.os.profile.get("BLOB_RAW", "load_address"), 16) + image_size = int(self.ql.os.profile.get("BLOB_RAW", "image_size"), 16) + image_name = self.ql.os.profile.get("BLOB_RAW", "image_name", fallback="blob.raw") + self.images.append(Image(self.load_address, self.load_address+image_size, image_name)) # used to collect coverage + else: + self.load_address = self.ql.os.load_address + self.entry_point = self.ql.os.entry_point - code_begins = self.load_address - code_size = self.ql.os.code_ram_size - code_ends = code_begins + code_size + code_begins = self.load_address + code_size = self.ql.os.code_ram_size + code_ends = code_begins + code_size - self.ql.mem.map(code_begins, code_size, info="[code]") - self.ql.mem.write(code_begins, self.ql.code) + self.ql.mem.map(code_begins, code_size, info="[code]") + self.ql.mem.write(code_begins, self.ql.code) - # allow image-related functionalities - self.images.append(Image(code_begins, code_ends, 'blob_code')) + # allow image-related functionalities + self.images.append(Image(code_begins, code_ends, 'blob_code')) - # FIXME: heap starts above end of ram?? - # FIXME: heap should be allocated by OS, not loader - heap_base = code_ends - heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16) - self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) + # FIXME: heap starts above end of ram?? + # FIXME: heap should be allocated by OS, not loader + heap_base = code_ends + heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16) + self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) - # FIXME: stack pointer should be a configurable profile setting - self.ql.arch.regs.arch_sp = code_ends - 0x1000 + # FIXME: stack pointer should be a configurable profile setting + self.ql.arch.regs.arch_sp = code_ends - 0x1000 diff --git a/qiling/os/blob/blob.py b/qiling/os/blob/blob.py index e4a02256..f2056e55 100644 --- a/qiling/os/blob/blob.py +++ b/qiling/os/blob/blob.py @@ -2,6 +2,9 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +# Added support for raw binary blob emulation +# Kelly Patterson - Cisco Talos +# Copyright (C) 2025 Cisco Systems Inc from qiling import Qiling from qiling.cc import QlCC, intel, arm, mips, riscv, ppc @@ -44,10 +47,10 @@ class QlOsBlob(QlOs): if self.ql.entry_point is not None: self.entry_point = self.ql.entry_point - self.exit_point = self.load_address + len(self.ql.code) - # if exit point was set explicitly, override the default one if self.ql.exit_point is not None: self.exit_point = self.ql.exit_point + elif self.ql.code is not None: # self.ql.code might not always be provided + self.exit_point = self.load_address + len(self.ql.code) self.ql.emu_start(self.entry_point, self.exit_point, self.ql.timeout, self.ql.count) diff --git a/tests/profiles/blob_raw.ql b/tests/profiles/blob_raw.ql new file mode 100644 index 00000000..164219a9 --- /dev/null +++ b/tests/profiles/blob_raw.ql @@ -0,0 +1,4 @@ +[BLOB_RAW] +load_address = 0x10000000 +image_size = 0xbc +image_name = example_raw.bin \ No newline at end of file diff --git a/tests/test_blob.py b/tests/test_blob.py index 33e35751..d284bbac 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -2,6 +2,9 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +# Added test for raw binary blob emulation +# Kelly Patterson - Cisco Talos +# Copyright (C) 2025 Cisco Systems Inc import unittest @@ -82,6 +85,102 @@ class BlobTest(unittest.TestCase): del ql + @unittest.skip("Temporarily disabled") + def test_blob_checksum_calculations(self): + def run_checksum_emu(input_data_buffer: bytes) -> int: + """ + Callable function that takes input data buffer and returns the checksum. + """ + BASE_ADDRESS = 0x10000000 + CHECKSUM_FUNC_ADDR = BASE_ADDRESS + 0x8 + END_ADDRESS = 0x100000ba + DATA_ADDR = 0xa0000000 + STACK_ADDR = 0xb0000000 + + ql = Qiling(archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="profiles/blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) + + input_data_len = len(input_data_buffer) + + # Map memory for the binary, data and stack + ql.mem.map(BASE_ADDRESS, 0x10000) + ql.mem.map(STACK_ADDR, 0x2000) + ql.mem.map(DATA_ADDR, ql.mem.align_up(input_data_len + 0x100)) + + # Write the binary into memory + ql.mem.write(BASE_ADDRESS, open("../examples/rootfs/blob/example_raw.bin", "rb").read()) + + # Write input data + ql.mem.write(DATA_ADDR, input_data_buffer) + + # Set up registers + ql.arch.regs.sp = STACK_ADDR + 0x2000 - 4 + ql.arch.regs.r0 = DATA_ADDR + ql.arch.regs.r1 = input_data_len + ql.arch.regs.pc = CHECKSUM_FUNC_ADDR + ql.arch.regs.lr = 0xbebebebe + + ql.run(begin=CHECKSUM_FUNC_ADDR, end=END_ADDRESS) + result = ql.arch.regs.r0 + + return result + + def calculate_expected_checksum(input_data_buffer: bytes) -> int: + """ + Python implementation of the expected checksum calculation. + """ + input_data_len = len(input_data_buffer) + expected_checksum = 0 + + if input_data_len >= 1 and input_data_buffer[0] == 0xDE: # MAGIC_VALUE_1 + for i in range(min(input_data_len, 4)): + expected_checksum += input_data_buffer[i] + expected_checksum += 0x10 + elif input_data_len >= 2 and input_data_buffer[1] == 0xAD: # MAGIC_VALUE_2 + for i in range(input_data_len): + expected_checksum ^= input_data_buffer[i] + expected_checksum += 0x20 + else: + for i in range(input_data_len): + expected_checksum += input_data_buffer[i] + + return expected_checksum & 0xFF + + # Test cases with descriptions + test_cases = { + "default_path": { + "data": b"\x01\x02\x03\x04\x05", + "description": "Default path - simple sum of all bytes" + }, + "magic_value_1": { + "data": b"\xDE\x01\x02\x03\x04\x05", + "description": "Magic Value 1 path (0xDE at data[0]) - sum first 4 bytes + 0x10" + }, + "magic_value_2": { + "data": b"\x01\xAD\x02\x03\x04\x05", + "description": "Magic Value 2 path (0xAD at data[1]) - XOR all bytes + 0x20" + }, + "edge_magic1_short": { + "data": b"\xDE\x01", + "description": "Edge case: Magic Value 1, but short data (only 2 bytes)" + }, + "edge_magic2_too_short": { + "data": b"\xAD", + "description": "Edge case: Magic Value 2, but too short (fallback to default)" + }, + "both_magic_values": { + "data": b"\xDE\xAD\x01\x02", + "description": "Both magic values present, DE at [0] takes precedence" + } + } + + # Assertions with descriptions - directly call functions with test data + self.assertEqual(run_checksum_emu(test_cases["default_path"]["data"]), calculate_expected_checksum(test_cases["default_path"]["data"])) # Default path + self.assertEqual(run_checksum_emu(test_cases["magic_value_1"]["data"]), calculate_expected_checksum(test_cases["magic_value_1"]["data"])) # Magic Value 1 path + self.assertEqual(run_checksum_emu(test_cases["magic_value_2"]["data"]), calculate_expected_checksum(test_cases["magic_value_2"]["data"])) # Magic Value 2 path + self.assertEqual(run_checksum_emu(test_cases["edge_magic1_short"]["data"]), calculate_expected_checksum(test_cases["edge_magic1_short"]["data"])) # Edge case: Magic Value 1, short data + self.assertEqual(run_checksum_emu(test_cases["edge_magic2_too_short"]["data"]), calculate_expected_checksum(test_cases["edge_magic2_too_short"]["data"])) # Edge case: Magic Value 2, too short + self.assertEqual(run_checksum_emu(test_cases["both_magic_values"]["data"]), calculate_expected_checksum(test_cases["both_magic_values"]["data"])) # Both magic values, DE takes precedence + if __name__ == "__main__": unittest.main() From 46866adb17a47a893a311f7464191bfdae861203 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 15 Aug 2025 17:09:30 +0300 Subject: [PATCH 23/54] Redesign MMIO support to be pickle-friendly --- qiling/hw/hw.py | 165 ++++++++++++++++++++++++++++--------------- qiling/loader/mcu.py | 21 +++--- qiling/os/memory.py | 36 +++++++--- 3 files changed, 145 insertions(+), 77 deletions(-) diff --git a/qiling/hw/hw.py b/qiling/hw/hw.py index 33081a05..e558584e 100644 --- a/qiling/hw/hw.py +++ b/qiling/hw/hw.py @@ -3,24 +3,68 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -import ctypes +from functools import cached_property +from typing import Any, Dict, List, Optional, Tuple -from qiling.core import Qiling +from qiling import Qiling from qiling.hw.peripheral import QlPeripheral from qiling.utils import ql_get_module_function from qiling.exception import QlErrorModuleFunctionNotFound +# should adhere to the QlMmioHandler interface, but not extend it directly to +# avoid potential pickling issues +class QlPripheralHandler: + def __init__(self, hwman: "QlHwManager", base: int, size: int, label: str) -> None: + self._hwman = hwman + self._base = base + self._size = size + self._label = label + + def __getstate__(self): + state = self.__dict__.copy() + del state['_hwman'] # remove non-pickleable reference + + return state + + @cached_property + def _mmio(self) -> bytearray: + """Get memory buffer used to back non-mapped hardware mmio regions. + """ + + return bytearray(self._size) + + def read(self, ql: Qiling, offset: int, size: int) -> int: + address = self._base + offset + hardware = self._hwman.find(address) + + if hardware: + return hardware.read(address - hardware.base, size) + + else: + ql.log.debug('[%s] read non-mapped hardware [%#010x]', self._label, address) + return int.from_bytes(self._mmio[offset:offset + size], byteorder='little') + + def write(self, ql: Qiling, offset: int, size: int, value: int) -> None: + address = self._base + offset + hardware = self._hwman.find(address) + + if hardware: + hardware.write(address - hardware.base, size, value) + + else: + ql.log.debug('[%s] write non-mapped hardware [%#010x] = %#010x', self._label, address, value) + self._mmio[offset:offset + size] = value.to_bytes(size, 'little') + + class QlHwManager: def __init__(self, ql: Qiling): self.ql = ql - self.entity = {} - self.region = {} + self.entity: Dict[str, QlPeripheral] = {} + self.region: Dict[str, List[Tuple[int, int]]] = {} - self.stepable = {} - - def create(self, label: str, struct: str=None, base: int=None, kwargs: dict={}) -> "QlPeripheral": + def create(self, label: str, struct: Optional[str] = None, base: Optional[int] = None, kwargs: Optional[Dict[str, Any]] = None) -> QlPeripheral: """ Create the peripheral accroding the label and envs. struct: Structure of the peripheral. Use defualt ql structure if not provide. @@ -30,39 +74,45 @@ class QlHwManager: if struct is None: struct, base, kwargs = self.load_env(label.upper()) - try: - - entity = ql_get_module_function('qiling.hw', struct)(self.ql, label, **kwargs) - - self.entity[label] = entity - if hasattr(entity, 'step'): - self.stepable[label] = entity + if kwargs is None: + kwargs = {} + try: + entity = ql_get_module_function('qiling.hw', struct)(self.ql, label, **kwargs) + + except QlErrorModuleFunctionNotFound: + self.ql.log.warning(f'could not create {struct}({label}): implementation not found') + + else: + assert isinstance(entity, QlPeripheral) + assert isinstance(base, int) + + self.entity[label] = entity self.region[label] = [(lbound + base, rbound + base) for (lbound, rbound) in entity.region] - return entity - except QlErrorModuleFunctionNotFound: - self.ql.log.debug(f'The {struct}({label}) has not been implemented') - def delete(self, label: str): + # FIXME: what should we do if struct is not implemented? is it OK to return None , or we fail? + + def delete(self, label: str) -> None: """ Remove the peripheral """ - if label in self.entity: - self.entity.pop(label) - self.region.pop(label) - if label in self.stepable: - self.stepable.pop(label) - def load_env(self, label: str): + if label in self.entity: + del self.entity[label] + + if label in self.region: + del self.region[label] + + def load_env(self, label: str) -> Tuple[str, int, Dict[str, Any]]: """ Get peripheral information (structure, base address, initialization list) from env. Args: label (str): Peripheral Label - + """ args = self.ql.env[label] - + return args['struct'], args['base'], args.get("kwargs", {}) def load_all(self): @@ -70,48 +120,30 @@ class QlHwManager: if args['type'] == 'peripheral': self.create(label.lower(), args['struct'], args['base'], args.get("kwargs", {})) - def find(self, address: int): + # TODO: this is wasteful. device mapping is known at creation time. at least we could cache lru entries + def find(self, address: int) -> Optional[QlPeripheral]: """ Find the peripheral at `address` """ - + for label in self.entity.keys(): for lbound, rbound in self.region[label]: if lbound <= address < rbound: return self.entity[label] + return None + def step(self): - """ Update all peripheral's state + """ Update all peripheral's state """ - for entity in self.stepable.values(): - entity.step() - def setup_mmio(self, begin, size, info=""): - mmio = ctypes.create_string_buffer(size) + for ent in self.entity.values(): + if hasattr(ent, 'step'): + ent.step() - def mmio_read_cb(ql, offset, size): - address = begin + offset - hardware = self.find(address) - - if hardware: - return hardware.read(address - hardware.base, size) - else: - ql.log.debug('%s Read non-mapped hardware [0x%08x]' % (info, address)) - - buf = ctypes.create_string_buffer(size) - ctypes.memmove(buf, ctypes.addressof(mmio) + offset, size) - return int.from_bytes(buf.raw, byteorder='little') + def setup_mmio(self, begin: int, size: int, info: str) -> None: + dev = QlPripheralHandler(self, begin, size, info) - def mmio_write_cb(ql, offset, size, value): - address = begin + offset - hardware = self.find(address) - - if hardware: - hardware.write(address - hardware.base, size, value) - else: - ql.log.debug('%s Write non-mapped hardware [0x%08x] = 0x%08x' % (info, address, value)) - ctypes.memmove(ctypes.addressof(mmio) + offset, (value).to_bytes(size, 'little'), size) - - self.ql.mem.map_mmio(begin, size, mmio_read_cb, mmio_write_cb, info=info) + self.ql.mem.map_mmio(begin, size, dev, info) def show_info(self): self.ql.log.info(f'{"Start":8s} {"End":8s} {"Label":8s} {"Class"}') @@ -131,8 +163,25 @@ class QlHwManager: return self.entity.get(key) def save(self): - return {label : entity.save() for label, entity in self.entity.items()} + return { + 'entity': {label: entity.save() for label, entity in self.entity.items()}, + 'region': self.region + } def restore(self, saved_state): - for label, data in saved_state.items(): + entity = saved_state['entity'] + assert isinstance(entity, dict) + + region = saved_state['region'] + assert isinstance(region, dict) + + for label, data in entity.items(): self.entity[label].restore(data) + + self.region = region + + # a dirty hack to rehydrate non-pickleable hwman + # a proper fix would require a deeper refactoring to how peripherals are created and managed + for ph in self.ql.mem.mmio_cbs.values(): + if isinstance(ph, QlPripheralHandler): + setattr(ph, '_hwman', self) diff --git a/qiling/loader/mcu.py b/qiling/loader/mcu.py index 8a91d633..204d17e4 100644 --- a/qiling/loader/mcu.py +++ b/qiling/loader/mcu.py @@ -114,20 +114,23 @@ class QlLoaderMCU(QlLoader): base = args['base'] self.ql.mem.map(base, size, info=f'[{name}]') - if memtype == 'remap': + # elif memtype == 'remap': + # size = args['size'] + # base = args['base'] + # alias = args['alias'] + # self.ql.hw.setup_remap(alias, base, size, info=f'[{name}]') + + elif memtype == 'mmio': size = args['size'] base = args['base'] - alias = args['alias'] - self.ql.hw.setup_remap(alias, base, size, info=f'[{name}]') + self.ql.hw.setup_mmio(base, size, name) - if memtype == 'mmio': - size = args['size'] - base = args['base'] - self.ql.hw.setup_mmio(base, size, info=f'[{name}]') - - if memtype == 'core': + elif memtype == 'core': self.ql.hw.create(name.lower()) + else: + self.ql.log.error(f'Unknown memory type "{memtype}" for {name}') + def run(self): self.load_profile() self.load_env() diff --git a/qiling/os/memory.py b/qiling/os/memory.py index 0939fc27..ec7aef19 100644 --- a/qiling/os/memory.py +++ b/qiling/os/memory.py @@ -6,7 +6,7 @@ import bisect import os import re -from typing import Any, Callable, Iterator, List, Mapping, Optional, Pattern, Sequence, Tuple, Union +from typing import Any, Callable, Dict, Iterator, List, Mapping, Optional, Pattern, Protocol, Sequence, Tuple, Union from unicorn import UC_PROT_NONE, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC, UC_PROT_ALL @@ -20,6 +20,22 @@ MmioReadCallback = Callable[[Qiling, int, int], int] MmioWriteCallback = Callable[[Qiling, int, int, int], None] +class QlMmioHandler(Protocol): + """A simple MMIO handler boilerplate that can be used to implement memory mapped devices. + + This should be extended to implement mapped devices state machines. Note that the read and write + methods are optional, where their existance indicates whether the device supports the corresponding + operation. That is, an unimplemented method means the corresponding operation will be silently + dropped. + """ + + def read(self, ql: Qiling, offset: int, size: int) -> int: + ... + + def write(self, ql: Qiling, offset: int, size: int, value: int) -> None: + ... + + class QlMemoryManager: """ some ideas and code from: @@ -29,7 +45,7 @@ class QlMemoryManager: def __init__(self, ql: Qiling, pagesize: int = 0x1000): self.ql = ql self.map_info: List[MapInfoEntry] = [] - self.mmio_cbs = {} + self.mmio_cbs: Dict[Tuple[int, int], QlMmioHandler] = {} bit_stuff = { 64: (1 << 64) - 1, @@ -272,7 +288,7 @@ class QlMemoryManager: for lbound, ubound, perm, label, is_mmio in self.map_info: if is_mmio: - mem_dict['mmio'].append((lbound, ubound, perm, label, *self.mmio_cbs[(lbound, ubound)])) + mem_dict['mmio'].append((lbound, ubound, perm, label, self.mmio_cbs[(lbound, ubound)])) else: data = self.read(lbound, ubound - lbound) mem_dict['ram'].append((lbound, ubound, perm, label, bytes(data))) @@ -294,12 +310,12 @@ class QlMemoryManager: self.ql.log.debug(f'writing {len(data):#x} bytes at {lbound:#08x}') self.write(lbound, data) - for lbound, ubound, perms, label, read_cb, write_cb in mem_dict['mmio']: + for lbound, ubound, perms, label, handler in mem_dict['mmio']: self.ql.log.debug(f"restoring mmio range: {lbound:#08x} {ubound:#08x} {label}") size = ubound - lbound if not self.is_mapped(lbound, size): - self.map_mmio(lbound, size, read_cb, write_cb, info=label) + self.map_mmio(lbound, size, handler, label) def read(self, addr: int, size: int) -> bytearray: """Read bytes from memory. @@ -619,15 +635,15 @@ class QlMemoryManager: self.ql.uc.mem_map(addr, size, perms) self.add_mapinfo(addr, addr + size, perms, info or '[mapped]', is_mmio=False) - def map_mmio(self, addr: int, size: int, read_cb: Optional[MmioReadCallback], write_cb: Optional[MmioWriteCallback], info: str = '[mmio]'): + def map_mmio(self, addr: int, size: int, handler: QlMmioHandler, info: str = '[mmio]'): # TODO: mmio memory overlap with ram? Is that possible? # TODO: Can read_cb or write_cb be None? How uc handle that access? prot = UC_PROT_NONE - if read_cb: + if hasattr(handler, 'read'): prot |= UC_PROT_READ - if write_cb: + if hasattr(handler, 'write'): prot |= UC_PROT_WRITE # generic mmio read wrapper @@ -642,10 +658,10 @@ class QlMemoryManager: cb(self.ql, offset, size, value) - self.ql.uc.mmio_map(addr, size, __mmio_read, read_cb, __mmio_write, write_cb) + self.ql.uc.mmio_map(addr, size, __mmio_read, handler.read, __mmio_write, handler.write) self.add_mapinfo(addr, addr + size, prot, info, is_mmio=True) - self.mmio_cbs[(addr, addr + size)] = (read_cb, write_cb) + self.mmio_cbs[(addr, addr + size)] = handler class Chunk: From c23928a2bf4843638718b03d72f0c025c317d1dd Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 15 Aug 2025 17:10:03 +0300 Subject: [PATCH 24/54] Cosmetics --- qiling/hw/hw.py | 2 +- qiling/loader/mcu.py | 41 +++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/qiling/hw/hw.py b/qiling/hw/hw.py index e558584e..3d869d56 100644 --- a/qiling/hw/hw.py +++ b/qiling/hw/hw.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # diff --git a/qiling/loader/mcu.py b/qiling/loader/mcu.py index 204d17e4..08f63558 100644 --- a/qiling/loader/mcu.py +++ b/qiling/loader/mcu.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework -# Built on top of Unicorn emulator (www.unicorn-engine.org) +# Built on top of Unicorn emulator (www.unicorn-engine.org) import io @@ -27,7 +27,7 @@ class IhexParser: if addr != begin + len(stream): self.segments.append((begin, stream)) begin, stream = addr, data - + else: stream += data @@ -36,13 +36,13 @@ class IhexParser: def parse_line(self, line): if len(line) < 9: return - + desc = line[7: 9] - size = int(line[1: 3], 16) - + size = int(line[1: 3], 16) + addr = bytes.fromhex(line[3: 7]) - data = bytes.fromhex(line[9: 9 + size * 2]) - + data = bytes.fromhex(line[9: 9 + size * 2]) + if desc == '00': # Data offset = int.from_bytes(addr, byteorder='big') self.mem.append((self.base + offset, data)) @@ -52,20 +52,20 @@ class IhexParser: elif desc == '04': # Extended Linear Address self.base = int.from_bytes(data, byteorder='big') * 0x10000 - + class QlLoaderMCU(QlLoader): def __init__(self, ql:Qiling): - super().__init__(ql) - + super().__init__(ql) + self.entry_point = 0 self.load_address = 0 self.filetype = self.guess_filetype() - + if self.filetype == 'elf': with open(self.ql.path, 'rb') as infile: self.elf = ELFFile(io.BytesIO(infile.read())) - + elif self.filetype == 'bin': self.map_address = self.argv[1] @@ -74,8 +74,8 @@ class QlLoaderMCU(QlLoader): def guess_filetype(self): if self.ql.path.endswith('.elf'): - return 'elf' - + return 'elf' + if self.ql.path.endswith('.bin'): return 'bin' @@ -83,7 +83,7 @@ class QlLoaderMCU(QlLoader): return 'hex' return 'elf' - + def reset(self): if self.filetype == 'elf': for segment in self.elf.iter_segments(type='PT_LOAD'): @@ -99,7 +99,7 @@ class QlLoaderMCU(QlLoader): for begin, data in self.ihex.segments: self.ql.mem.write(begin, data) - + self.ql.arch.init_context() self.entry_point = self.ql.arch.regs.read('pc') @@ -109,11 +109,12 @@ class QlLoaderMCU(QlLoader): def load_env(self): for name, args in self.env.items(): memtype = args['type'] + if memtype == 'memory': size = args['size'] base = args['base'] self.ql.mem.map(base, size, info=f'[{name}]') - + # elif memtype == 'remap': # size = args['size'] # base = args['base'] @@ -134,8 +135,8 @@ class QlLoaderMCU(QlLoader): def run(self): self.load_profile() self.load_env() - + ## Handle interrupt from instruction execution self.ql.hook_intr(self.ql.arch.unicorn_exception_handler) - + self.reset() From 7b98c01840065111abc781e3e810e5421ff92eaf Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 15 Aug 2025 17:39:03 +0300 Subject: [PATCH 25/54] Make MCU loader less noisy --- qiling/loader/mcu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/loader/mcu.py b/qiling/loader/mcu.py index 08f63558..3ad64c3b 100644 --- a/qiling/loader/mcu.py +++ b/qiling/loader/mcu.py @@ -130,7 +130,7 @@ class QlLoaderMCU(QlLoader): self.ql.hw.create(name.lower()) else: - self.ql.log.error(f'Unknown memory type "{memtype}" for {name}') + self.ql.log.debug(f'ignoring unknown memory type "{memtype}" for {name}') def run(self): self.load_profile() From 81c2a39f1f39fb4117db20ad5d931bcb20d5ba47 Mon Sep 17 00:00:00 2001 From: technikelly <11539105+technikelly@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:55:37 -0400 Subject: [PATCH 26/54] minimizing blob changes --- examples/blob_raw.ql | 6 ++--- examples/hello_arm_blob_raw.py | 26 ++++++++++++++------- qiling/loader/blob.py | 42 +++++++++++++++++----------------- qiling/os/blob/blob.py | 7 ++---- tests/profiles/blob_raw.ql | 6 ++--- tests/test_blob.py | 11 ++++----- 6 files changed, 52 insertions(+), 46 deletions(-) diff --git a/examples/blob_raw.ql b/examples/blob_raw.ql index 164219a9..23390130 100644 --- a/examples/blob_raw.ql +++ b/examples/blob_raw.ql @@ -1,4 +1,4 @@ -[BLOB_RAW] +[CODE] load_address = 0x10000000 -image_size = 0xbc -image_name = example_raw.bin \ No newline at end of file +entry_point = 0x10000008 +ram_size = 0xa00000 \ No newline at end of file diff --git a/examples/hello_arm_blob_raw.py b/examples/hello_arm_blob_raw.py index 6e44d6bf..f260a4e0 100644 --- a/examples/hello_arm_blob_raw.py +++ b/examples/hello_arm_blob_raw.py @@ -6,6 +6,7 @@ from qiling import Qiling from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE from qiling.extensions.coverage import utils as cov_utils +from qiling.loader.loader import Image BASE_ADDRESS = 0x10000000 CHECKSUM_FUNC_ADDR = BASE_ADDRESS + 0x8 @@ -29,6 +30,7 @@ def checksum_function(input_data_buffer: bytes): for i in range(input_data_len): expected_checksum_python += input_data_buffer[i] expected_checksum_python &= 0xFF # Ensure it's a single byte + return expected_checksum_python def unmapped_handler(ql, type, addr, size, value): @@ -37,18 +39,24 @@ def unmapped_handler(ql, type, addr, size, value): def emulate_checksum_function(input_data_buffer: bytes): print(f"\n--- Testing with input: {input_data_buffer.hex()} ---") - ql = Qiling(archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) + with open("rootfs/blob/example_raw.bin", "rb") as f: + raw_code = f.read() + + ql = Qiling(code=raw_code, archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) + + # monkeypatch - Correcting the loader image name, used for coverage collection + # Remove all images with name 'blob_code' that were created by the blob loader + ql.loader.images = [img for img in ql.loader.images if img.path != 'blob_code'] + # Add image back with correct info + ql.loader.images.append(Image(ql.loader.load_address, ql.loader.load_address + ql.os.code_ram_size, 'example_raw.bin')) + input_data_len = len(input_data_buffer) - # Map memory for the binary, data and stack - ql.mem.map(BASE_ADDRESS, 0x10000) + # Map memory for the data and stack ql.mem.map(STACK_ADDR, 0x2000) ql.mem.map(DATA_ADDR, ql.mem.align_up(input_data_len + 0x100)) # Map enough space for data - # Write the binary into memory - ql.mem.write(BASE_ADDRESS, open("rootfs/blob/example_raw.bin", "rb").read()) - # Write input data ql.mem.write(DATA_ADDR, input_data_buffer) @@ -70,7 +78,8 @@ def emulate_checksum_function(input_data_buffer: bytes): # Start emulation print(f"Starting emulation at PC: {hex(ql.arch.regs.pc)}") try: - ql.run(begin=CHECKSUM_FUNC_ADDR, end=END_ADDRESS) + with cov_utils.collect_coverage(ql, 'drcov', 'output.cov'): + ql.run(begin=CHECKSUM_FUNC_ADDR, end=END_ADDRESS) except Exception as e: print(f"Emulation error: {e}") @@ -78,4 +87,5 @@ def emulate_checksum_function(input_data_buffer: bytes): if __name__ == "__main__": data = b"\x01\x02\x03\x04\x05" # Example input data - emulate_checksum_function(data) \ No newline at end of file + emulate_checksum_function(data) + print(hex(checksum_function(data))) \ No newline at end of file diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index 96168538..c4a69de8 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -9,38 +9,38 @@ from qiling import Qiling from qiling.loader.loader import QlLoader, Image from qiling.os.memory import QlMemoryHeap +import configparser class QlLoaderBLOB(QlLoader): def __init__(self, ql: Qiling): super().__init__(ql) + self.load_address = 0 + def run(self): - if self.ql.os.profile.has_section("BLOB_RAW"): - # For raw binary blobs, user will handle memory mapping - self.load_address = int(self.ql.os.profile.get("BLOB_RAW", "load_address"), 16) - image_size = int(self.ql.os.profile.get("BLOB_RAW", "image_size"), 16) - image_name = self.ql.os.profile.get("BLOB_RAW", "image_name", fallback="blob.raw") - self.images.append(Image(self.load_address, self.load_address+image_size, image_name)) # used to collect coverage - else: - self.load_address = self.ql.os.load_address - self.entry_point = self.ql.os.entry_point + self.load_address = self.ql.os.load_address + self.entry_point = self.ql.os.entry_point - code_begins = self.load_address - code_size = self.ql.os.code_ram_size - code_ends = code_begins + code_size + code_begins = self.load_address + code_size = self.ql.os.code_ram_size + code_ends = code_begins + code_size - self.ql.mem.map(code_begins, code_size, info="[code]") - self.ql.mem.write(code_begins, self.ql.code) + self.ql.mem.map(code_begins, code_size, info="[code]") + self.ql.mem.write(code_begins, self.ql.code) - # allow image-related functionalities - self.images.append(Image(code_begins, code_ends, 'blob_code')) + # allow image-related functionalities + self.images.append(Image(code_begins, code_ends, 'blob_code')) - # FIXME: heap starts above end of ram?? - # FIXME: heap should be allocated by OS, not loader - heap_base = code_ends + # FIXME: heap starts above end of ram?? + # FIXME: heap should be allocated by OS, not loader + heap_base = code_ends + # if heap_size is defined, create the heap + try: heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16) self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) + except (configparser.NoSectionError, configparser.NoOptionError): + pass # heap_size is not required - # FIXME: stack pointer should be a configurable profile setting - self.ql.arch.regs.arch_sp = code_ends - 0x1000 + # FIXME: stack pointer should be a configurable profile setting + self.ql.arch.regs.arch_sp = code_ends - 0x1000 diff --git a/qiling/os/blob/blob.py b/qiling/os/blob/blob.py index f2056e55..e4a02256 100644 --- a/qiling/os/blob/blob.py +++ b/qiling/os/blob/blob.py @@ -2,9 +2,6 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -# Added support for raw binary blob emulation -# Kelly Patterson - Cisco Talos -# Copyright (C) 2025 Cisco Systems Inc from qiling import Qiling from qiling.cc import QlCC, intel, arm, mips, riscv, ppc @@ -47,10 +44,10 @@ class QlOsBlob(QlOs): if self.ql.entry_point is not None: self.entry_point = self.ql.entry_point + self.exit_point = self.load_address + len(self.ql.code) + # if exit point was set explicitly, override the default one if self.ql.exit_point is not None: self.exit_point = self.ql.exit_point - elif self.ql.code is not None: # self.ql.code might not always be provided - self.exit_point = self.load_address + len(self.ql.code) self.ql.emu_start(self.entry_point, self.exit_point, self.ql.timeout, self.ql.count) diff --git a/tests/profiles/blob_raw.ql b/tests/profiles/blob_raw.ql index 164219a9..23390130 100644 --- a/tests/profiles/blob_raw.ql +++ b/tests/profiles/blob_raw.ql @@ -1,4 +1,4 @@ -[BLOB_RAW] +[CODE] load_address = 0x10000000 -image_size = 0xbc -image_name = example_raw.bin \ No newline at end of file +entry_point = 0x10000008 +ram_size = 0xa00000 \ No newline at end of file diff --git a/tests/test_blob.py b/tests/test_blob.py index d284bbac..bd0b9ecf 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -97,18 +97,17 @@ class BlobTest(unittest.TestCase): DATA_ADDR = 0xa0000000 STACK_ADDR = 0xb0000000 - ql = Qiling(archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="profiles/blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) + with open("../examples/rootfs/blob/example_raw.bin", "rb") as f: + raw_code = f.read() + + ql = Qiling(code=raw_code, archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="profiles/blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) input_data_len = len(input_data_buffer) - # Map memory for the binary, data and stack - ql.mem.map(BASE_ADDRESS, 0x10000) + # Map memory for data and stack ql.mem.map(STACK_ADDR, 0x2000) ql.mem.map(DATA_ADDR, ql.mem.align_up(input_data_len + 0x100)) - # Write the binary into memory - ql.mem.write(BASE_ADDRESS, open("../examples/rootfs/blob/example_raw.bin", "rb").read()) - # Write input data ql.mem.write(DATA_ADDR, input_data_buffer) From 6d697b5d7fe0ebf8e28474fe737fe76594f43b0d Mon Sep 17 00:00:00 2001 From: technikelly <11539105+technikelly@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:12:54 -0400 Subject: [PATCH 27/54] updating copyright statements with license info --- examples/hello_arm_blob_raw.py | 1 + examples/src/blob/Makefile | 1 + examples/src/blob/example_raw.c | 1 + examples/src/blob/linker.ld | 1 + qiling/loader/blob.py | 1 + tests/test_blob.py | 1 + 6 files changed, 6 insertions(+) diff --git a/examples/hello_arm_blob_raw.py b/examples/hello_arm_blob_raw.py index f260a4e0..6096bbd9 100644 --- a/examples/hello_arm_blob_raw.py +++ b/examples/hello_arm_blob_raw.py @@ -2,6 +2,7 @@ # Added example for raw binary blob # Kelly Patterson - Cisco Talos # Copyright (C) 2025 Cisco Systems Inc +# Licensed under the GNU General Public License v2.0 or later ############################################################################## from qiling import Qiling from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE diff --git a/examples/src/blob/Makefile b/examples/src/blob/Makefile index 7f4252f7..66fe0b43 100644 --- a/examples/src/blob/Makefile +++ b/examples/src/blob/Makefile @@ -2,6 +2,7 @@ # Added example for raw binary blob # Kelly Patterson - Cisco Talos # Copyright (C) 2025 Cisco Systems Inc +# Licensed under the GNU General Public License v2.0 or later ############################################################################## # Makefile for Bare-Metal ARM Hash Calculator diff --git a/examples/src/blob/example_raw.c b/examples/src/blob/example_raw.c index 5ac71cb3..eae56b2b 100644 --- a/examples/src/blob/example_raw.c +++ b/examples/src/blob/example_raw.c @@ -2,6 +2,7 @@ * Added example for raw binary blob * Kelly Patterson - Cisco Talos * Copyright (C) 2025 Cisco Systems Inc + * Licensed under the GNU General Public License v2.0 or later * */ // example_raw.c diff --git a/examples/src/blob/linker.ld b/examples/src/blob/linker.ld index ea57d85e..9c9e7485 100644 --- a/examples/src/blob/linker.ld +++ b/examples/src/blob/linker.ld @@ -3,6 +3,7 @@ * Added example for raw binary blob * Kelly Patterson - Cisco Talos * Copyright (C) 2025 Cisco Systems Inc + * Licensed under the GNU General Public License v2.0 or later * */ diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index c4a69de8..7fa5e50a 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -5,6 +5,7 @@ # Added support for raw binary blob emulation # Kelly Patterson - Cisco Talos # Copyright (C) 2025 Cisco Systems Inc +# Licensed under the GNU General Public License v2.0 or later from qiling import Qiling from qiling.loader.loader import QlLoader, Image diff --git a/tests/test_blob.py b/tests/test_blob.py index bd0b9ecf..310d202a 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -5,6 +5,7 @@ # Added test for raw binary blob emulation # Kelly Patterson - Cisco Talos # Copyright (C) 2025 Cisco Systems Inc +# Licensed under the GNU General Public License v2.0 or later import unittest From a2223abb5a9eb6c601748e8f60e18f516b509b40 Mon Sep 17 00:00:00 2001 From: technikelly <11539105+technikelly@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:15:17 -0400 Subject: [PATCH 28/54] update description --- qiling/loader/blob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index 7fa5e50a..7a7e3325 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -2,7 +2,7 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -# Added support for raw binary blob emulation +# Heaps are optional for blobs # Kelly Patterson - Cisco Talos # Copyright (C) 2025 Cisco Systems Inc # Licensed under the GNU General Public License v2.0 or later From 313b907bdecb0787aea642af077d90f9b73fa808 Mon Sep 17 00:00:00 2001 From: rliebig Date: Sat, 23 Aug 2025 12:14:38 -0400 Subject: [PATCH 29/54] return EISDIR in case ql_file object has a directory path and a read syscall is attempted on it --- qiling/os/posix/syscall/unistd.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiling/os/posix/syscall/unistd.py b/qiling/os/posix/syscall/unistd.py index 8931f95e..09b86c38 100644 --- a/qiling/os/posix/syscall/unistd.py +++ b/qiling/os/posix/syscall/unistd.py @@ -415,6 +415,8 @@ def ql_syscall_read(ql: Qiling, fd: int, buf: int, length: int): try: data = f.read(length) + except IsADirectoryError: + return -EISDIR except ConnectionError: ql.log.debug('read failed due to a connection error') return -EIO From 5ec4a3b57a0cdf6e638026e04ab94cc60b1aabb5 Mon Sep 17 00:00:00 2001 From: technikelly <11539105+technikelly@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:34:45 -0400 Subject: [PATCH 30/54] addressing review comments, removing copyright notices --- examples/hello_arm_blob_raw.py | 47 ++++++++++++++++++++------------- examples/src/blob/Makefile | 8 +----- examples/src/blob/example_raw.c | 8 +----- examples/src/blob/linker.ld | 7 ----- examples/uboot_bin.ql | 1 + qiling/loader/blob.py | 16 ----------- qiling/os/blob/blob.py | 9 ++++++- tests/profiles/uboot_bin.ql | 1 + tests/test_blob.py | 43 +++--------------------------- 9 files changed, 43 insertions(+), 97 deletions(-) diff --git a/examples/hello_arm_blob_raw.py b/examples/hello_arm_blob_raw.py index 6096bbd9..4c257166 100644 --- a/examples/hello_arm_blob_raw.py +++ b/examples/hello_arm_blob_raw.py @@ -1,13 +1,13 @@ ############################################################################## -# Added example for raw binary blob -# Kelly Patterson - Cisco Talos -# Copyright (C) 2025 Cisco Systems Inc -# Licensed under the GNU General Public License v2.0 or later +# This example is meant to demonstrate the modifications necessary +# to enable code coverage when emulating small code snippets or bare-metal +# code. ############################################################################## from qiling import Qiling from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE from qiling.extensions.coverage import utils as cov_utils from qiling.loader.loader import Image +import os BASE_ADDRESS = 0x10000000 CHECKSUM_FUNC_ADDR = BASE_ADDRESS + 0x8 @@ -16,6 +16,8 @@ DATA_ADDR = 0xa0000000 # Arbitrary address for data STACK_ADDR = 0xb0000000 # Arbitrary address for stack # Python implementation of the checksum function being emulated +# This checksum function is intended to have different code paths based on the input +# which is useful for observing code coverage def checksum_function(input_data_buffer: bytes): expected_checksum_python = 0 input_data_len = len(input_data_buffer) @@ -33,26 +35,34 @@ def checksum_function(input_data_buffer: bytes): expected_checksum_python &= 0xFF # Ensure it's a single byte return expected_checksum_python -def unmapped_handler(ql, type, addr, size, value): +def unmapped_handler(ql: Qiling, type: int, addr: int, size: int, value: int) -> None: + print(f"Unmapped Memory R/W, trying to access {size:d} bytes at {addr:#010x} from {ql.arch.regs.pc:#010x}") - print(f"Unmapped Memory R/W, trying to access {hex(size)} bytes at {hex(addr)} from {hex(ql.arch.regs.pc)}") - -def emulate_checksum_function(input_data_buffer: bytes): +def emulate_checksum_function(input_data_buffer: bytes) -> None: print(f"\n--- Testing with input: {input_data_buffer.hex()} ---") - with open("rootfs/blob/example_raw.bin", "rb") as f: - raw_code = f.read() + test_file = "rootfs/blob/example_raw.bin" - ql = Qiling(code=raw_code, archtype=QL_ARCH.ARM, ostype=QL_OS.BLOB, profile="blob_raw.ql", verbose=QL_VERBOSE.DEBUG, thumb=True) + with open(test_file, "rb") as f: + raw_code: bytes = f.read() - # monkeypatch - Correcting the loader image name, used for coverage collection - # Remove all images with name 'blob_code' that were created by the blob loader + ql: Qiling = Qiling( + code=raw_code, + archtype=QL_ARCH.ARM, + ostype=QL_OS.BLOB, + profile="blob_raw.ql", + verbose=QL_VERBOSE.DEBUG, + thumb=True + ) + + ''' monkeypatch - Correcting the loader image name, used for coverage collection + removing all images with name 'blob_code' that were created by the blob loader. + This is necessary because some code coverage visualization tools require the + module name to match that of the input file ''' ql.loader.images = [img for img in ql.loader.images if img.path != 'blob_code'] - # Add image back with correct info - ql.loader.images.append(Image(ql.loader.load_address, ql.loader.load_address + ql.os.code_ram_size, 'example_raw.bin')) + ql.loader.images.append(Image(ql.loader.load_address, ql.loader.load_address + ql.os.code_ram_size, os.path.basename(test_file))) - - input_data_len = len(input_data_buffer) + input_data_len: int = len(input_data_buffer) # Map memory for the data and stack ql.mem.map(STACK_ADDR, 0x2000) @@ -88,5 +98,4 @@ def emulate_checksum_function(input_data_buffer: bytes): if __name__ == "__main__": data = b"\x01\x02\x03\x04\x05" # Example input data - emulate_checksum_function(data) - print(hex(checksum_function(data))) \ No newline at end of file + emulate_checksum_function(data) \ No newline at end of file diff --git a/examples/src/blob/Makefile b/examples/src/blob/Makefile index 66fe0b43..74966f26 100644 --- a/examples/src/blob/Makefile +++ b/examples/src/blob/Makefile @@ -1,10 +1,4 @@ -############################################################################## -# Added example for raw binary blob -# Kelly Patterson - Cisco Talos -# Copyright (C) 2025 Cisco Systems Inc -# Licensed under the GNU General Public License v2.0 or later -############################################################################## -# Makefile for Bare-Metal ARM Hash Calculator +# Makefile for Bare-Metal ARM Checksum Calculator # --- Toolchain Definitions --- TOOLCHAIN_PREFIX = arm-none-eabi diff --git a/examples/src/blob/example_raw.c b/examples/src/blob/example_raw.c index eae56b2b..13cd7077 100644 --- a/examples/src/blob/example_raw.c +++ b/examples/src/blob/example_raw.c @@ -1,10 +1,4 @@ - /* - * Added example for raw binary blob - * Kelly Patterson - Cisco Talos - * Copyright (C) 2025 Cisco Systems Inc - * Licensed under the GNU General Public License v2.0 or later - * - */ +// example checksum algorithm to demonstrate raw binary code coverage in qiling // example_raw.c // Define some magic values diff --git a/examples/src/blob/linker.ld b/examples/src/blob/linker.ld index 9c9e7485..ae31f2fa 100644 --- a/examples/src/blob/linker.ld +++ b/examples/src/blob/linker.ld @@ -1,11 +1,4 @@ /* linker.ld */ - /* - * Added example for raw binary blob - * Kelly Patterson - Cisco Talos - * Copyright (C) 2025 Cisco Systems Inc - * Licensed under the GNU General Public License v2.0 or later - * - */ ENTRY(_start) /* Define the entry point of our program */ diff --git a/examples/uboot_bin.ql b/examples/uboot_bin.ql index c33a7d23..1e95311f 100644 --- a/examples/uboot_bin.ql +++ b/examples/uboot_bin.ql @@ -2,6 +2,7 @@ ram_size = 0xa00000 load_address = 0x80800000 entry_point = 0x80800000 +heap_address = 0xa0000000 heap_size = 0x300000 diff --git a/qiling/loader/blob.py b/qiling/loader/blob.py index 7a7e3325..72844339 100644 --- a/qiling/loader/blob.py +++ b/qiling/loader/blob.py @@ -2,15 +2,9 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -# Heaps are optional for blobs -# Kelly Patterson - Cisco Talos -# Copyright (C) 2025 Cisco Systems Inc -# Licensed under the GNU General Public License v2.0 or later from qiling import Qiling from qiling.loader.loader import QlLoader, Image -from qiling.os.memory import QlMemoryHeap -import configparser class QlLoaderBLOB(QlLoader): @@ -33,15 +27,5 @@ class QlLoaderBLOB(QlLoader): # allow image-related functionalities self.images.append(Image(code_begins, code_ends, 'blob_code')) - # FIXME: heap starts above end of ram?? - # FIXME: heap should be allocated by OS, not loader - heap_base = code_ends - # if heap_size is defined, create the heap - try: - heap_size = int(self.ql.os.profile.get("CODE", "heap_size"), 16) - self.ql.os.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) - except (configparser.NoSectionError, configparser.NoOptionError): - pass # heap_size is not required - # FIXME: stack pointer should be a configurable profile setting self.ql.arch.regs.arch_sp = code_ends - 0x1000 diff --git a/qiling/os/blob/blob.py b/qiling/os/blob/blob.py index e4a02256..af52fa74 100644 --- a/qiling/os/blob/blob.py +++ b/qiling/os/blob/blob.py @@ -8,6 +8,7 @@ from qiling.cc import QlCC, intel, arm, mips, riscv, ppc from qiling.const import QL_ARCH, QL_OS from qiling.os.fcall import QlFunctionCall from qiling.os.os import QlOs +from qiling.os.memory import QlMemoryHeap class QlOsBlob(QlOs): @@ -49,5 +50,11 @@ class QlOsBlob(QlOs): # if exit point was set explicitly, override the default one if self.ql.exit_point is not None: self.exit_point = self.ql.exit_point - + + # if heap info is provided in profile, create heap + heap_base = self.profile.getint('CODE', 'heap_address', fallback=None) + heap_size = self.profile.getint('CODE', 'heap_size', fallback=None) + if heap_base is not None and heap_size is not None: + self.heap = QlMemoryHeap(self.ql, heap_base, heap_base + heap_size) + self.ql.emu_start(self.entry_point, self.exit_point, self.ql.timeout, self.ql.count) diff --git a/tests/profiles/uboot_bin.ql b/tests/profiles/uboot_bin.ql index c33a7d23..1e95311f 100644 --- a/tests/profiles/uboot_bin.ql +++ b/tests/profiles/uboot_bin.ql @@ -2,6 +2,7 @@ ram_size = 0xa00000 load_address = 0x80800000 entry_point = 0x80800000 +heap_address = 0xa0000000 heap_size = 0x300000 diff --git a/tests/test_blob.py b/tests/test_blob.py index 310d202a..753e0ef5 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -2,10 +2,6 @@ # # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -# Added test for raw binary blob emulation -# Kelly Patterson - Cisco Talos -# Copyright (C) 2025 Cisco Systems Inc -# Licensed under the GNU General Public License v2.0 or later import unittest @@ -87,7 +83,7 @@ class BlobTest(unittest.TestCase): del ql @unittest.skip("Temporarily disabled") - def test_blob_checksum_calculations(self): + def test_blob_raw(self): def run_checksum_emu(input_data_buffer: bytes) -> int: """ Callable function that takes input data buffer and returns the checksum. @@ -145,41 +141,8 @@ class BlobTest(unittest.TestCase): return expected_checksum & 0xFF - # Test cases with descriptions - test_cases = { - "default_path": { - "data": b"\x01\x02\x03\x04\x05", - "description": "Default path - simple sum of all bytes" - }, - "magic_value_1": { - "data": b"\xDE\x01\x02\x03\x04\x05", - "description": "Magic Value 1 path (0xDE at data[0]) - sum first 4 bytes + 0x10" - }, - "magic_value_2": { - "data": b"\x01\xAD\x02\x03\x04\x05", - "description": "Magic Value 2 path (0xAD at data[1]) - XOR all bytes + 0x20" - }, - "edge_magic1_short": { - "data": b"\xDE\x01", - "description": "Edge case: Magic Value 1, but short data (only 2 bytes)" - }, - "edge_magic2_too_short": { - "data": b"\xAD", - "description": "Edge case: Magic Value 2, but too short (fallback to default)" - }, - "both_magic_values": { - "data": b"\xDE\xAD\x01\x02", - "description": "Both magic values present, DE at [0] takes precedence" - } - } - - # Assertions with descriptions - directly call functions with test data - self.assertEqual(run_checksum_emu(test_cases["default_path"]["data"]), calculate_expected_checksum(test_cases["default_path"]["data"])) # Default path - self.assertEqual(run_checksum_emu(test_cases["magic_value_1"]["data"]), calculate_expected_checksum(test_cases["magic_value_1"]["data"])) # Magic Value 1 path - self.assertEqual(run_checksum_emu(test_cases["magic_value_2"]["data"]), calculate_expected_checksum(test_cases["magic_value_2"]["data"])) # Magic Value 2 path - self.assertEqual(run_checksum_emu(test_cases["edge_magic1_short"]["data"]), calculate_expected_checksum(test_cases["edge_magic1_short"]["data"])) # Edge case: Magic Value 1, short data - self.assertEqual(run_checksum_emu(test_cases["edge_magic2_too_short"]["data"]), calculate_expected_checksum(test_cases["edge_magic2_too_short"]["data"])) # Edge case: Magic Value 2, too short - self.assertEqual(run_checksum_emu(test_cases["both_magic_values"]["data"]), calculate_expected_checksum(test_cases["both_magic_values"]["data"])) # Both magic values, DE takes precedence + test_input = b"\x01\x02\x03\x04\x05" + self.assertEqual(run_checksum_emu(test_input), calculate_expected_checksum(test_input)) if __name__ == "__main__": From 04236d77b1941a505da9fe19f4abe755c15f9a0e Mon Sep 17 00:00:00 2001 From: technikelly <11539105+technikelly@users.noreply.github.com> Date: Wed, 27 Aug 2025 11:26:39 -0400 Subject: [PATCH 31/54] enabling test and updating rootfs submodule --- examples/rootfs | 2 +- tests/test_blob.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/rootfs b/examples/rootfs index f71f45fe..120fb6d3 160000 --- a/examples/rootfs +++ b/examples/rootfs @@ -1 +1 @@ -Subproject commit f71f45fe1a39d58d8b8cae717f55cebeb37f63c7 +Subproject commit 120fb6d37700a2d4c0e35ced599aaee7a8f98723 diff --git a/tests/test_blob.py b/tests/test_blob.py index 753e0ef5..0bd9a662 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -82,7 +82,6 @@ class BlobTest(unittest.TestCase): del ql - @unittest.skip("Temporarily disabled") def test_blob_raw(self): def run_checksum_emu(input_data_buffer: bytes) -> int: """ From f45eb7042ace11358d242814d8a6cea8ee7c1ab7 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:35:20 +0300 Subject: [PATCH 32/54] Cache Struct instances for faster conversions --- qiling/core_struct.py | 50 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/qiling/core_struct.py b/qiling/core_struct.py index 6c0d99cc..f10fd42f 100644 --- a/qiling/core_struct.py +++ b/qiling/core_struct.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # @@ -25,14 +25,14 @@ class QlCoreStructs: QL_ENDIAN.EB: '>' }[endian] - self._fmt8 = f'{modifier}B' - self._fmt8s = f'{modifier}b' - self._fmt16 = f'{modifier}H' - self._fmt16s = f'{modifier}h' - self._fmt32 = f'{modifier}I' - self._fmt32s = f'{modifier}i' - self._fmt64 = f'{modifier}Q' - self._fmt64s = f'{modifier}q' + self._fmt8 = struct.Struct(f'{modifier}B') + self._fmt8s = struct.Struct(f'{modifier}b') + self._fmt16 = struct.Struct(f'{modifier}H') + self._fmt16s = struct.Struct(f'{modifier}h') + self._fmt32 = struct.Struct(f'{modifier}I') + self._fmt32s = struct.Struct(f'{modifier}i') + self._fmt64 = struct.Struct(f'{modifier}Q') + self._fmt64s = struct.Struct(f'{modifier}q') handlers = { 64 : (self.pack64, self.pack64s, self.unpack64, self.unpack64s), @@ -51,49 +51,49 @@ class QlCoreStructs: self.unpacks = ups def pack64(self, x: int, /) -> bytes: - return struct.pack(self._fmt64, x) + return self._fmt64.pack(x) def pack64s(self, x: int, /) -> bytes: - return struct.pack(self._fmt64s, x) + return self._fmt64s.pack(x) def unpack64(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt64, x)[0] + return self._fmt64.unpack(x)[0] def unpack64s(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt64s, x)[0] + return self._fmt64s.unpack(x)[0] def pack32(self, x: int, /) -> bytes: - return struct.pack(self._fmt32, x) + return self._fmt32.pack(x) def pack32s(self, x: int, /) -> bytes: - return struct.pack(self._fmt32s, x) + return self._fmt32s.pack(x) def unpack32(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt32, x)[0] + return self._fmt32.unpack(x)[0] def unpack32s(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt32s, x)[0] + return self._fmt32s.unpack(x)[0] def pack16(self, x: int, /) -> bytes: - return struct.pack(self._fmt16, x) + return self._fmt16.pack(x) def pack16s(self, x: int, /) -> bytes: - return struct.pack(self._fmt16s, x) + return self._fmt16s.pack(x) def unpack16(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt16, x)[0] + return self._fmt16.unpack(x)[0] def unpack16s(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt16s, x)[0] + return self._fmt16s.unpack(x)[0] def pack8(self, x: int, /) -> bytes: - return struct.pack(self._fmt8, x) + return self._fmt8.pack(x) def pack8s(self, x: int, /) -> bytes: - return struct.pack(self._fmt8s, x) + return self._fmt8s.pack(x) def unpack8(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt8, x)[0] + return self._fmt8.unpack(x)[0] def unpack8s(self, x: ReadableBuffer, /) -> int: - return struct.unpack(self._fmt8s, x)[0] + return self._fmt8s.unpack(x)[0] From 59ebfdd0fd300e9038a0b9341e896ad12a495356 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:36:42 +0300 Subject: [PATCH 33/54] Cache memory accessors for faster access --- qiling/os/memory.py | 61 ++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/qiling/os/memory.py b/qiling/os/memory.py index ec7aef19..4c0f8654 100644 --- a/qiling/os/memory.py +++ b/qiling/os/memory.py @@ -64,6 +64,31 @@ class QlMemoryManager: # make sure pagesize is a power of 2 assert self.pagesize & (self.pagesize - 1) == 0, 'pagesize has to be a power of 2' + self._packers = { + (1, True): ql.pack8s, + (2, True): ql.pack16s, + (4, True): ql.pack32s, + (8, True): ql.pack64s, + + (1, False): ql.pack8, + (2, False): ql.pack16, + (4, False): ql.pack32, + (8, False): ql.pack64 + } + + self._unpackers = { + (1, True): ql.unpack8s, + (2, True): ql.unpack16s, + (4, True): ql.unpack32s, + (8, True): ql.unpack64s, + + (1, False): ql.unpack8, + (2, False): ql.unpack16, + (4, False): ql.unpack32, + (8, False): ql.unpack64 + } + + def __read_string(self, addr: int) -> str: ret = bytearray() c = self.read(addr, 1) @@ -344,22 +369,12 @@ class QlMemoryManager: if not size: size = self.ql.arch.pointersize - __unpack = ({ - 1: self.ql.unpack8s, - 2: self.ql.unpack16s, - 4: self.ql.unpack32s, - 8: self.ql.unpack64s - } if signed else { - 1: self.ql.unpack8, - 2: self.ql.unpack16, - 4: self.ql.unpack32, - 8: self.ql.unpack64 - }).get(size) - - if __unpack is None: + try: + _unpack = self._unpackers[(size, signed)] + except KeyError: raise QlErrorStructConversion(f"Unsupported pointer size: {size}") - return __unpack(self.read(addr, size)) + return _unpack(self.read(addr, size)) def write(self, addr: int, data: bytes) -> None: """Write bytes to a memory. @@ -385,22 +400,12 @@ class QlMemoryManager: if not size: size = self.ql.arch.pointersize - __pack = ({ - 1: self.ql.pack8s, - 2: self.ql.pack16s, - 4: self.ql.pack32s, - 8: self.ql.pack64s - } if signed else { - 1: self.ql.pack8, - 2: self.ql.pack16, - 4: self.ql.pack32, - 8: self.ql.pack64 - }).get(size) - - if __pack is None: + try: + _pack = self._packers[(size, signed)] + except KeyError: raise QlErrorStructConversion(f"Unsupported pointer size: {size}") - self.write(addr, __pack(value)) + self.write(addr, _pack(value)) def search(self, needle: Union[bytes, Pattern[bytes]], begin: Optional[int] = None, end: Optional[int] = None) -> List[int]: """Search for a sequence of bytes in memory. From 94aeb96350be281aa5611543083128fa51933752 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:38:20 +0300 Subject: [PATCH 34/54] Use disasm_lite for faster DISASM output --- qiling/arch/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/qiling/arch/utils.py b/qiling/arch/utils.py index 6782c932..4d44b545 100644 --- a/qiling/arch/utils.py +++ b/qiling/arch/utils.py @@ -48,7 +48,7 @@ class QlArchUtils: return addr, '-' def disassembler(self, ql: Qiling, address: int, size: int): - data = ql.mem.read(address, size) + data = memoryview(ql.mem.read(address, size)) # knowing that all binary sections are aligned to page boundary allows # us to 'cheat' and search for the containing image using the aligned @@ -64,11 +64,14 @@ class QlArchUtils: ba, name = self.get_base_and_name(ql.mem.align(address)) anibbles = ql.arch.bits // 4 + pos = 0 - for insn in ql.arch.disassembler.disasm(data, address): - offset = insn.address - ba + for iaddr, isize, mnem, ops in ql.arch.disassembler.disasm_lite(data, address): + offset = iaddr - ba + ibytes = data[pos:pos + isize] - ql.log.info(f'{insn.address:0{anibbles}x} [{name:20s} + {offset:#08x}] {insn.bytes.hex(" "):20s} {insn.mnemonic:20s} {insn.op_str}') + ql.log.info(f'{iaddr:0{anibbles}x} [{name:20s} + {offset:#08x}] {ibytes.hex():22s} {mnem:16s} {ops}') + pos += isize if ql.verbose >= QL_VERBOSE.DUMP: for reg in ql.arch.regs.register_mapping: From d846c75e591b11c2d5b1a9c561bfd19026edd4d7 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:40:00 +0300 Subject: [PATCH 35/54] Add a CC accessor to tell the return address --- qiling/cc/__init__.py | 6 ++++++ qiling/cc/arm.py | 7 ++++++- qiling/cc/intel.py | 3 +++ qiling/cc/mips.py | 3 +++ qiling/cc/ppc.py | 3 +++ qiling/cc/riscv.py | 3 +++ 6 files changed, 24 insertions(+), 1 deletion(-) diff --git a/qiling/cc/__init__.py b/qiling/cc/__init__.py index 99c9e564..a1f35481 100644 --- a/qiling/cc/__init__.py +++ b/qiling/cc/__init__.py @@ -70,6 +70,12 @@ class QlCC: raise NotImplementedError + def getReturnAddress(self) -> int: + """Get function return address. + """ + + raise NotImplementedError + def setReturnAddress(self, addr: int) -> None: """Set function return address. diff --git a/qiling/cc/arm.py b/qiling/cc/arm.py index 51d798b2..29ac126b 100644 --- a/qiling/cc/arm.py +++ b/qiling/cc/arm.py @@ -21,17 +21,22 @@ class QlArmBaseCC(QlCommonBaseCC): def getNumSlots(argbits: int) -> int: return 1 + def getReturnAddress(self) -> int: + return self.arch.regs.lr + def setReturnAddress(self, addr: int) -> None: self.arch.regs.lr = addr def unwind(self, nslots: int) -> int: # TODO: cleanup? - return self.arch.regs.lr + return self.getReturnAddress() + class aarch64(QlArmBaseCC): _retreg = UC_ARM64_REG_X0 _argregs = make_arg_list(UC_ARM64_REG_X0, UC_ARM64_REG_X1, UC_ARM64_REG_X2, UC_ARM64_REG_X3, UC_ARM64_REG_X4, UC_ARM64_REG_X5, UC_ARM64_REG_X6, UC_ARM64_REG_X7) + class aarch32(QlArmBaseCC): _retreg = UC_ARM_REG_R0 _argregs = make_arg_list(UC_ARM_REG_R0, UC_ARM_REG_R1, UC_ARM_REG_R2, UC_ARM_REG_R3) diff --git a/qiling/cc/intel.py b/qiling/cc/intel.py index ca179603..f2e6971d 100644 --- a/qiling/cc/intel.py +++ b/qiling/cc/intel.py @@ -15,6 +15,9 @@ class QlIntelBaseCC(QlCommonBaseCC): Supports arguments passing over registers and stack. """ + def getReturnAddress(self) -> int: + return self.arch.stack_read(0) + def setReturnAddress(self, addr: int) -> None: self.arch.stack_push(addr) diff --git a/qiling/cc/mips.py b/qiling/cc/mips.py index 9ebf2337..472b2a3e 100644 --- a/qiling/cc/mips.py +++ b/qiling/cc/mips.py @@ -12,6 +12,9 @@ class mipso32(QlCommonBaseCC): _shadow = 4 _retaddr_on_stack = False + def getReturnAddress(self) -> int: + return self.arch.regs.ra + def setReturnAddress(self, addr: int): self.arch.regs.ra = addr diff --git a/qiling/cc/ppc.py b/qiling/cc/ppc.py index 2440fab1..b4a88f79 100644 --- a/qiling/cc/ppc.py +++ b/qiling/cc/ppc.py @@ -22,5 +22,8 @@ class ppc(QlCommonBaseCC): def getNumSlots(argbits: int): return 1 + def getReturnAddress(self) -> int: + return self.arch.regs.lr + def setReturnAddress(self, addr: int): self.arch.regs.lr = addr diff --git a/qiling/cc/riscv.py b/qiling/cc/riscv.py index 3a360bd8..f9f09522 100644 --- a/qiling/cc/riscv.py +++ b/qiling/cc/riscv.py @@ -22,5 +22,8 @@ class riscv(QlCommonBaseCC): def getNumSlots(argbits: int): return 1 + def getReturnAddress(self) -> int: + return self.arch.regs.ra + def setReturnAddress(self, addr: int): self.arch.regs.ra = addr From a845b5ee2297089ae4db23f779e95bc0f76be948 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:42:41 +0300 Subject: [PATCH 36/54] Avoid using mutable object as default parameter value --- qiling/os/uefi/fncc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiling/os/uefi/fncc.py b/qiling/os/uefi/fncc.py index 83f999bf..3e354920 100644 --- a/qiling/os/uefi/fncc.py +++ b/qiling/os/uefi/fncc.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -from typing import Any, Mapping +from typing import Any, Mapping, Optional from qiling import Qiling from qiling.const import QL_INTERCEPT -def dxeapi(params: Mapping[str, Any] = {}): +def dxeapi(params: Optional[Mapping[str, Any]] = None): def decorator(func): def wrapper(ql: Qiling): pc = ql.arch.regs.arch_pc @@ -18,7 +18,7 @@ def dxeapi(params: Mapping[str, Any] = {}): onenter = ql.os.user_defined_api[QL_INTERCEPT.ENTER].get(fname) onexit = ql.os.user_defined_api[QL_INTERCEPT.EXIT].get(fname) - return ql.os.call(pc, f, params, onenter, onexit) + return ql.os.call(pc, f, params or {}, onenter, onexit) return wrapper From e14ccf1a8d903fe1099c075ac84e5de3a99801f6 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:43:29 +0300 Subject: [PATCH 37/54] Enable passthrough on dxeapi --- qiling/os/uefi/fncc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/os/uefi/fncc.py b/qiling/os/uefi/fncc.py index 3e354920..6294cbd7 100644 --- a/qiling/os/uefi/fncc.py +++ b/qiling/os/uefi/fncc.py @@ -8,7 +8,7 @@ from typing import Any, Mapping, Optional from qiling import Qiling from qiling.const import QL_INTERCEPT -def dxeapi(params: Optional[Mapping[str, Any]] = None): +def dxeapi(params: Optional[Mapping[str, Any]] = None, passthru: bool = False): def decorator(func): def wrapper(ql: Qiling): pc = ql.arch.regs.arch_pc @@ -18,7 +18,7 @@ def dxeapi(params: Optional[Mapping[str, Any]] = None): onenter = ql.os.user_defined_api[QL_INTERCEPT.ENTER].get(fname) onexit = ql.os.user_defined_api[QL_INTERCEPT.EXIT].get(fname) - return ql.os.call(pc, f, params or {}, onenter, onexit) + return ql.os.call(pc, f, params or {}, onenter, onexit, passthru) return wrapper From 18d812efc1b3e016b2979febebd5dbf8b4c59184 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:45:05 +0300 Subject: [PATCH 38/54] Add SimpleText protocols to UEFI --- qiling/os/uefi/UefiSpec.py | 14 +- .../uefi/protocols/EfiSimpleTextInProtocol.py | 56 ++++++++ .../protocols/EfiSimpleTextOutProtocol.py | 128 ++++++++++++++++++ qiling/os/uefi/st.py | 104 ++++++++------ 4 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 qiling/os/uefi/protocols/EfiSimpleTextInProtocol.py create mode 100644 qiling/os/uefi/protocols/EfiSimpleTextOutProtocol.py diff --git a/qiling/os/uefi/UefiSpec.py b/qiling/os/uefi/UefiSpec.py index 2259e8c3..583ef6d8 100644 --- a/qiling/os/uefi/UefiSpec.py +++ b/qiling/os/uefi/UefiSpec.py @@ -10,6 +10,10 @@ from .ProcessorBind import * from .UefiBaseType import * from .UefiMultiPhase import * +from .protocols.EfiSimpleTextInProtocol import EFI_SIMPLE_TEXT_INPUT_PROTOCOL +from .protocols.EfiSimpleTextOutProtocol import EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL + + # definitions for EFI_TIME.Daylight EFI_TIME_ADJUST_DAYLIGHT = (1 << 1) EFI_TIME_IN_DAYLIGHT = (1 << 2) @@ -223,14 +227,6 @@ class EFI_CONFIGURATION_TABLE(STRUCT): ('VendorTable', PTR(VOID)), ] -# TODO: to be implemented -# @see: MdePkg\Include\Protocol\SimpleTextIn.h -EFI_SIMPLE_TEXT_INPUT_PROTOCOL = STRUCT - -# TODO: to be implemented -# @see: MdePkg\Include\Protocol\SimpleTextOut.h -EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL = STRUCT - class EFI_SYSTEM_TABLE(STRUCT): _pack_ = 8 @@ -264,4 +260,4 @@ __all__ = [ 'EFI_DEVICE_PATH_PROTOCOL', 'EFI_OPEN_PROTOCOL_INFORMATION_ENTRY', 'EFI_IMAGE_UNLOAD' -] \ No newline at end of file +] diff --git a/qiling/os/uefi/protocols/EfiSimpleTextInProtocol.py b/qiling/os/uefi/protocols/EfiSimpleTextInProtocol.py new file mode 100644 index 00000000..1a8e3eed --- /dev/null +++ b/qiling/os/uefi/protocols/EfiSimpleTextInProtocol.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +from qiling.os.const import * +from qiling.os.uefi.fncc import dxeapi +from qiling.os.uefi.utils import * +from qiling.os.uefi.ProcessorBind import * +from qiling.os.uefi.UefiBaseType import EFI_STATUS, EFI_EVENT + + +# @see: MdePkg/Include/Protocol/SimpleTextIn.h +class EFI_INPUT_KEY(STRUCT): + _fields_ = [ + ('ScanCode', UINT16), + ('UnicodeChar', CHAR16) + ] + +class EFI_SIMPLE_TEXT_INPUT_PROTOCOL(STRUCT): + EFI_SIMPLE_TEXT_INPUT_PROTOCOL = STRUCT + + _fields_ = [ + ('Reset', FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_INPUT_PROTOCOL), BOOLEAN)), + ('ReadKeyStroke', FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_INPUT_PROTOCOL), PTR(EFI_INPUT_KEY))), + ('WaitForKey', EFI_EVENT) + ] + + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "ExtendedVerification": BOOL # IN BOOLEAN +}) +def hook_Input_Reset(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "Key": POINTER # OUT PTR(EFI_INPUT_KEY) +}) +def hook_Read_Key_Stroke(ql: Qiling, address: int, params): + pass + + +def initialize(ql: Qiling, gIP: int): + descriptor = { + 'struct': EFI_SIMPLE_TEXT_INPUT_PROTOCOL, + 'fields': ( + ('Reset', hook_Input_Reset), + ('ReadKeyStroke', hook_Read_Key_Stroke), + ('WaitForKey', None) + ) + } + + instance = init_struct(ql, gIP, descriptor) + instance.save_to(ql.mem, gIP) diff --git a/qiling/os/uefi/protocols/EfiSimpleTextOutProtocol.py b/qiling/os/uefi/protocols/EfiSimpleTextOutProtocol.py new file mode 100644 index 00000000..d69cd3a3 --- /dev/null +++ b/qiling/os/uefi/protocols/EfiSimpleTextOutProtocol.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +from qiling.os.const import * +from qiling.os.uefi.fncc import dxeapi +from qiling.os.uefi.utils import * +from qiling.os.uefi.ProcessorBind import * +from qiling.os.uefi.UefiBaseType import EFI_STATUS + + +# @see: MdePkg/Include/Protocol/SimpleTextOut.h +class SIMPLE_TEXT_OUTPUT_MODE(STRUCT): + _fields_ = [ + ("MaxMode", INT32), + ("Mode", INT32), + ("Attribute", INT32), + ("CursorColumn", INT32), + ("CursorRow", INT32), + ("CursorVisible", BOOLEAN), + ] + + +class EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL(STRUCT): + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL = STRUCT + + _fields_ = [ + ("Reset", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), BOOLEAN)), + ("OutputString", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), PTR(CHAR16))), + ("TestString", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), PTR(CHAR16))), + ("QueryMode", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), UINTN, PTR(UINTN), PTR(UINTN))), + ("SetMode", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), UINTN)), + ("SetAttribute", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), UINTN)), + ("ClearScreen", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL))), + ("SetCursorPosition", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), UINTN, UINTN)), + ("EnableCursor", FUNCPTR(EFI_STATUS, PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL), BOOLEAN)), + ("Mode", PTR(SIMPLE_TEXT_OUTPUT_MODE)) + ] + + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "ExtendedVerification": BOOL # IN BOOLEAN +}) +def hook_TextReset(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "String": WSTRING # IN PTR(CHAR16) +}) +def hook_OutputString(ql: Qiling, address: int, params): + print(params['String']) + + return EFI_SUCCESS + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "String": WSTRING # IN PTR(CHAR16) +}) +def hook_TestString(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "ModeNumber": ULONGLONG, # IN UINTN + "Columns": POINTER, # OUT PTR(UINTN) + "Rows": POINTER # OUT PTR(UINTN) +}) +def hook_QueryMode(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "ModeNumber": ULONGLONG # IN UINTN +}) +def hook_SetMode(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "Attribute": ULONGLONG # IN UINTN +}) +def hook_SetAttribute(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) +}) +def hook_ClearScreen(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "Column": ULONGLONG, # IN UINTN + "Row": ULONGLONG # IN UINTN +}) +def hook_SetCursorPosition(ql: Qiling, address: int, params): + pass + +@dxeapi(params={ + "This": POINTER, # IN PTR(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL) + "Visible": BOOL # IN BOOLEAN +}) +def hook_EnableCursor(ql: Qiling, address: int, params): + pass + + +def initialize(ql: Qiling, base: int): + descriptor = { + 'struct': EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL, + 'fields': ( + ('Reset', hook_TextReset), + ('OutputString', hook_OutputString), + ('TestString', hook_TestString), + ('QueryMode', hook_QueryMode), + ('SetMode', hook_SetMode), + ('SetAttribute', hook_SetAttribute), + ('ClearScreen', hook_ClearScreen), + ('SetCursorPosition', hook_SetCursorPosition), + ('EnableCursor', hook_EnableCursor), + ('Mode', None) + ) + } + + instance = init_struct(ql, base, descriptor) + instance.save_to(ql.mem, base) diff --git a/qiling/os/uefi/st.py b/qiling/os/uefi/st.py index b5fca922..6163b6c7 100644 --- a/qiling/os/uefi/st.py +++ b/qiling/os/uefi/st.py @@ -3,58 +3,79 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # -from qiling import Qiling +from typing import TYPE_CHECKING + from qiling.os.uefi import bs, rt, ds from qiling.os.uefi.context import UefiContext from qiling.os.uefi.utils import install_configuration_table -from qiling.os.uefi.UefiSpec import EFI_SYSTEM_TABLE, EFI_BOOT_SERVICES, EFI_RUNTIME_SERVICES +from qiling.os.uefi.UefiSpec import EFI_SYSTEM_TABLE, EFI_SIMPLE_TEXT_INPUT_PROTOCOL, EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL, EFI_BOOT_SERVICES, EFI_RUNTIME_SERVICES + +import qiling.os.uefi.protocols.EfiSimpleTextInProtocol as txt_in +import qiling.os.uefi.protocols.EfiSimpleTextOutProtocol as txt_out + + +if TYPE_CHECKING: + from qiling import Qiling # static mem layout: # -# +-- EFI_SYSTEM_TABLE ---------+ -# | | -# | ... | -# | RuntimeServices* -> (1) | -# | BootServices* -> (2) | -# | NumberOfTableEntries | -# | ConfigurationTable* -> (4) | -# +-----------------------------+ -# (1) +-- EFI_RUNTIME_SERVICES -----+ -# | | -# | ... | -# +-----------------------------+ -# (2) +-- EFI_BOOT_SERVICES --------+ -# | | -# | ... | -# +-----------------------------+ -# (3) +-- EFI_DXE_SERVICES ---------+ -# | | -# | ... | -# +-----------------------------+ -# (4) +-- EFI_CONFIGURATION_TABLE --+ of HOB_LIST -# | VendorGuid | -# | VendorTable* -> (5) | -# +-----------------------------+ -# +-- EFI_CONFIGURATION_TABLE --+ of DXE_SERVICE_TABLE -# | VendorGuid | -# | VendorTable* -> (3) | -# +-----------------------------+ +# +-- EFI_SYSTEM_TABLE -----------------+ +# | | +# | ... | +# | ConIn* -> (1) | +# | ConOut* -> (2) | +# | RuntimeServices* -> (3) | +# | BootServices* -> (4) | +# | NumberOfTableEntries | +# | ConfigurationTable* -> (6) | +# +-------------------------------------+ +# (1) +-- EFI_SIMPLE_TEXT_INPUT_PROTOCOL ---+ +# | | +# | ... | +# +-------------------------------------+ +# (2) +-- EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL --+ +# | | +# | ... | +# +-------------------------------------+ +# (3) +-- EFI_RUNTIME_SERVICES -------------+ +# | | +# | ... | +# +-------------------------------------+ +# (4) +-- EFI_BOOT_SERVICES ----------------+ +# | | +# | ... | +# +-------------------------------------+ +# (5) +-- EFI_DXE_SERVICES -----------------+ +# | | +# | ... | +# +-------------------------------------+ +# (6) +-- EFI_CONFIGURATION_TABLE ----------+ of HOB_LIST +# | VendorGuid | +# | VendorTable* -> (7) | +# +-------------------------------------+ +# +-- EFI_CONFIGURATION_TABLE ----------+ of DXE_SERVICE_TABLE +# | VendorGuid | +# | VendorTable* -> (5) | +# +-------------------------------------+ # # ... the remainder of the chunk may be used for additional EFI_CONFIGURATION_TABLE entries - +# # dynamically allocated (context.conf_table_data_ptr): # -# (5) +-- VOID* --------------------+ -# | ... | -# +-----------------------------+ +# (7) +-- VOID* ----------------------------+ +# | ... | +# +-------------------------------------+ + def initialize(ql: Qiling, context: UefiContext, gST: int): ql.loader.gST = gST - gBS = gST + EFI_SYSTEM_TABLE.sizeof() # boot services - gRT = gBS + EFI_BOOT_SERVICES.sizeof() # runtime services - gDS = gRT + EFI_RUNTIME_SERVICES.sizeof() # dxe services - cfg = gDS + ds.EFI_DXE_SERVICES.sizeof() # configuration tables array + sti = gST + EFI_SYSTEM_TABLE.sizeof() # input protocols + sto = sti + EFI_SIMPLE_TEXT_INPUT_PROTOCOL.sizeof() # output protocols + gRT = sto + EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.sizeof() # runtime services + gBS = gRT + EFI_RUNTIME_SERVICES.sizeof() # boot services + gDS = gBS + EFI_BOOT_SERVICES.sizeof() # dxe services + cfg = gDS + ds.EFI_DXE_SERVICES.sizeof() # configuration tables array ql.log.info(f'Global tables:') ql.log.info(f' | gST {gST:#010x}') @@ -63,11 +84,16 @@ def initialize(ql: Qiling, context: UefiContext, gST: int): ql.log.info(f' | gDS {gDS:#010x}') ql.log.info(f'') + txt_in.initialize(ql, sti) + txt_out.initialize(ql, sto) + bs.initialize(ql, gBS) rt.initialize(ql, gRT) ds.initialize(ql, gDS) EFI_SYSTEM_TABLE( + ConIn = sti, + ConOut = sto, RuntimeServices = gRT, BootServices = gBS, NumberOfTableEntries = 0, @@ -79,4 +105,4 @@ def initialize(ql: Qiling, context: UefiContext, gST: int): __all__ = [ 'initialize' -] \ No newline at end of file +] From 0af477cde5b1aad310332f1d37880cbac5d4a4eb Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:48:15 +0300 Subject: [PATCH 39/54] Fix the way input btyes are passed on AFL crash validation --- qiling/extensions/afl/afl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiling/extensions/afl/afl.py b/qiling/extensions/afl/afl.py index 4aef943e..4128af5a 100644 --- a/qiling/extensions/afl/afl.py +++ b/qiling/extensions/afl/afl.py @@ -96,8 +96,8 @@ def ql_afl_fuzz_custom(ql: Qiling, def __place_input_wrapper(uc: Uc, input_bytes: Array[c_char], iters: int, context: Any) -> bool: return place_input_callback(ql, input_bytes.raw, iters) - def __validate_crash_wrapper(uc: Uc, result: int, input_bytes: bytes, iters: int, context: Any) -> bool: - return validate_crash_callback(ql, result, input_bytes, iters) + def __validate_crash_wrapper(uc: Uc, result: int, input_bytes: Array[c_char], iters: int, context: Any) -> bool: + return validate_crash_callback(ql, result, input_bytes.raw, iters) def __fuzzing_wrapper(uc: Uc, context: Any) -> int: return fuzzing_callback(ql) From 6b136dda1dca825519d465c543c8762cbd4b4e52 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:50:25 +0300 Subject: [PATCH 40/54] Annotate QlDisk methods --- qiling/os/disk.py | 77 ++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/qiling/os/disk.py b/qiling/os/disk.py index 765712ac..ddc68e4a 100644 --- a/qiling/os/disk.py +++ b/qiling/os/disk.py @@ -1,23 +1,33 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +from typing import AnyStr, Optional, Union from .mapper import QlFsMappedObject +ReadableBuffer = Union[bytes, bytearray, memoryview] + + # Open a file as a Disk # host_path: The file path on the host machine. # drive_path: The drive path on the emulated system. e.g. /dev/sda \\.\PHYSICALDRIVE0 0x80 -# +# # Note: CHS and LBA support is very limited since a raw file doesn't contain enough information. # We simply assume that it is a disk with 1 head, 1 cylinder and (filesize/512) sectors. +# # See: https://en.wikipedia.org/wiki/Cylinder-head-sector # https://en.wikipedia.org/wiki/Logical_block_addressing # http://www.uruk.org/orig-grub/PC_partitioning.txt + class QlDisk(QlFsMappedObject): - def __init__(self, host_path, drive_path, n_heads=1, n_cylinders=1, sector_size=512): - self._host_path = host_path + # 512 bytes/sector + # 63 sectors/track + # 255 heads (tracks/cylinder) + # 1024 cylinders + + def __init__(self, host_path: AnyStr, drive_path, n_cylinders: int = 1, n_heads: int = 1, sector_size: int = 512): self._drive_path = drive_path self._fp = open(host_path, "rb+") self._n_heads = n_heads @@ -25,7 +35,7 @@ class QlDisk(QlFsMappedObject): self._sector_size = sector_size self.lseek(0, 2) self._filesize = self.tell() - self._n_sectors = (self._filesize - 1)// self.sector_size + 1 + self._n_sectors = (self._filesize - 1) // self.sector_size + 1 def __del__(self): if not self.fp.closed: @@ -51,50 +61,43 @@ class QlDisk(QlFsMappedObject): def sector_size(self): return self._sector_size - @property - def host_path(self): - return self._host_path - - @property - def drive_path(self): - return self._drive_path - @property def fp(self): return self._fp # Methods from FsMappedObject - def read(self, l): - return self.fp.read(l) - - def write(self, bs): - return self.fp.write(bs) + def read(self, size: Optional[int]) -> bytes: + return self.fp.read(size) - def lseek(self, offset, origin): + def write(self, buffer: ReadableBuffer) -> int: + return self.fp.write(buffer) + + def lseek(self, offset: int, origin: int) -> int: return self.fp.seek(offset, origin) - - def tell(self): + + def tell(self) -> int: return self.fp.tell() - def close(self): - return self.fp.close() - + def close(self) -> None: + self.fp.close() + # Methods for QlDisk - def lba(self, cylinder, head, sector): + def lba(self, cylinder: int, head: int, sector: int) -> int: return (cylinder * self.n_heads + head) * self._n_sectors + sector - 1 - - def read_sectors(self, lba, cnt): + + def read_sectors(self, lba: int, cnt: int) -> bytes: self.lseek(self.sector_size * lba, 0) - return self.read(self.sector_size*cnt) - - def read_chs(self, cylinder, head, sector, cnt): + + return self.read(self.sector_size * cnt) + + def read_chs(self, cylinder: int, head: int, sector: int, cnt: int) -> bytes: return self.read_sectors(self.lba(cylinder, head, sector), cnt) - def write_sectors(self, lba, cnt, buffer): - if len(buffer) > self.sector_size * cnt: - buffer = buffer[:self.sector_size*cnt] + def write_sectors(self, lba: int, cnt: int, buffer: ReadableBuffer) -> int: + buffer = memoryview(buffer) self.lseek(self.sector_size * lba, 0) - return self.write(buffer) - - def write_chs(self, cylinder, head, sector, cnt, buffer): - return self.write_sectors(self.lba(cylinder, head, sector), cnt, buffer) \ No newline at end of file + + return self.write(buffer[:self.sector_size * cnt]) + + def write_chs(self, cylinder: int, head: int, sector: int, cnt: int, buffer: ReadableBuffer): + return self.write_sectors(self.lba(cylinder, head, sector), cnt, buffer) From 7531c330aace8b3ed034608fa8fafc25b278f947 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:50:59 +0300 Subject: [PATCH 41/54] Misc DOS additions --- qiling/os/dos/interrupts/int21.py | 44 +++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/qiling/os/dos/interrupts/int21.py b/qiling/os/dos/interrupts/int21.py index da9ea64e..0b3dc02f 100644 --- a/qiling/os/dos/interrupts/int21.py +++ b/qiling/os/dos/interrupts/int21.py @@ -9,11 +9,6 @@ from qiling import Qiling from .. import utils -# exit -def __leaf_4c(ql: Qiling): - ql.log.info("Program terminated gracefully") - ql.emu_stop() - # write a character to screen def __leaf_02(ql: Qiling): ch = ql.arch.regs.dl @@ -131,6 +126,45 @@ def __leaf_43(ql: Qiling): ql.arch.regs.cx = 0xffff ql.os.clear_cf() + +def __leaf_48(ql: Qiling): + """Allocate memory. + """ + + size = ql.arch.regs.bx * 0x10 + + # announce it but do not do anything really + ql.log.debug(f'allocating memory block at {addr:#06x} to {size:#x} bytes') + + # success + ql.os.clear_cf() + + +def __leaf_49(ql: Qiling): + """Deallocate memory. + """ + ... + + +def __leaf_4a(ql: Qiling): + """Modify memory allocation. + """ + + addr = ql.arch.regs.es + size = ql.arch.regs.bx * 0x10 + + # announce it but do not do anything really + ql.log.debug(f'resizing memory block at {addr:#06x} to {size:#x} bytes') + + # success + ql.os.clear_cf() + + +def __leaf_4c(ql: Qiling): + ql.log.info("Program terminated gracefully") + ql.emu_stop() + + def handler(ql: Qiling): ah = ql.arch.regs.ah From 0d9c722aa7f434a4af9ef94205f7e9e1457cedc7 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:51:22 +0300 Subject: [PATCH 42/54] Misc Linux kernel API additions --- qiling/os/linux/kernel_api/kernel_api.py | 78 +++++++++++++++++------- 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/qiling/os/linux/kernel_api/kernel_api.py b/qiling/os/linux/kernel_api/kernel_api.py index c16ed325..43510e3e 100644 --- a/qiling/os/linux/kernel_api/kernel_api.py +++ b/qiling/os/linux/kernel_api/kernel_api.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# +# # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # @@ -53,31 +53,63 @@ def hook_mcount(ql, address, params): return 0 -@linux_kernel_api(params={ - "Ptr": POINTER -}) -def hook___x86_indirect_thunk_rax(ql, address, params): - return 0 +def __x86_indirect_thunk(ql: Qiling, dest: int): + ql.log.debug('retpoline to %#010x', dest) + + ql.arch.regs.arch_pc = dest + +# using passthru as a hack to avoid syscall handler overwrite instruction pointer +@linux_kernel_api(passthru=True) +def hook___x86_indirect_thunk_rax(ql: Qiling, address: int, params): + __x86_indirect_thunk(ql, ql.arch.regs.rax) -@linux_kernel_api(params={ - "Ptr": POINTER -}) -def hook__copy_to_user(ql, address, params): - return 0 - - -@linux_kernel_api(params={ - "Ptr": POINTER -}) -def hook__copy_from_user(ql, address, params): - return 0 - - -@linux_kernel_api(params={ - "Ptr": POINTER -}) +@linux_kernel_api(passthru=True) def hook___x86_indirect_thunk_r14(ql, address, params): + __x86_indirect_thunk(ql, ql.arch.regs.r14) + + +@linux_kernel_api(params={ + "ubuf": POINTER, + "kbuf": POINTER, + "count": SIZE_T +}) +def hook__copy_to_user(ql: Qiling, address: int, params) -> int: + ubuf = params['ubuf'] + kbuf = params['kbuf'] + count = params['count'] + + # if user-mode buffer is not available, fail + # TODO: also fail if destination is not writeable + if not ql.mem.is_mapped(ubuf, count): + return count + + data = ql.mem.read(kbuf, count) + + ql.mem.write(ubuf, data) + + return 0 + + +@linux_kernel_api(params={ + "kbuf": POINTER, + "ubuf": POINTER, + "count": SIZE_T +}) +def hook__copy_from_user(ql: Qiling, address: int, params) -> int: + ubuf = params['ubuf'] + kbuf = params['kbuf'] + count = params['count'] + + # if user-mode buffer is not available, fail + # TODO: also fail if source is not readable + if not ql.mem.is_mapped(ubuf, count): + return count + + data = ql.mem.read(ubuf, count) + + ql.mem.write(kbuf, data) + return 0 From b0d6384468c110e45e5b8f22667767d22ec97770 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 12:51:59 +0300 Subject: [PATCH 43/54] Misc examples fixes --- .../dlink_dir815/dir815_mips32el_linux.py | 46 +++++++++---------- examples/sality.py | 2 +- examples/tendaac1518_httpd.py | 2 + 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/examples/fuzzing/dlink_dir815/dir815_mips32el_linux.py b/examples/fuzzing/dlink_dir815/dir815_mips32el_linux.py index 28afce92..22ea02ff 100644 --- a/examples/fuzzing/dlink_dir815/dir815_mips32el_linux.py +++ b/examples/fuzzing/dlink_dir815/dir815_mips32el_linux.py @@ -5,7 +5,7 @@ # Everything about the bug and firmware https://www.exploit-db.com/exploits/33863 -import os,sys +import sys sys.path.append("../../..") from qiling import Qiling @@ -13,7 +13,7 @@ from qiling.const import QL_VERBOSE from qiling.extensions.afl import ql_afl_fuzz -def main(input_file, enable_trace=False): +def main(input_file: str): env_vars = { "REQUEST_METHOD": "POST", @@ -24,40 +24,36 @@ def main(input_file, enable_trace=False): # "CONTENT_LENGTH": "8", # no needed } - ql = Qiling(["./rootfs/htdocs/web/hedwig.cgi"], "./rootfs", - verbose=QL_VERBOSE.DEBUG, env=env_vars, console=enable_trace) + ql = Qiling(["./rootfs/htdocs/web/hedwig.cgi"], "./rootfs", verbose=QL_VERBOSE.DISABLED, env=env_vars) - def place_input_callback(ql: Qiling, input: bytes, _: int): - env_var = ("HTTP_COOKIE=uid=1234&password=").encode() - env_vars = env_var + input + b"\x00" + (ql.path).encode() + b"\x00" - ql.mem.write(ql.target_addr, env_vars) + def place_input_callback(ql: Qiling, data: bytes, _: int) -> bool: + # construct the payload + payload = b''.join((b"HTTP_COOKIE=uid=1234&password=", bytes(data), b"\x00", ql_path, b"\x00")) + + # patch the value of 'HTTP_COOKIE' in memory + ql.mem.write(target_addr, payload) + + # payload is in place, we are good to go + return True def start_afl(_ql: Qiling): - """ Callback from inside """ + ql_afl_fuzz(_ql, input_file=input_file, place_input_callback=place_input_callback, exits=[ql.os.exit_point]) - addr = ql.mem.search("HTTP_COOKIE=uid=1234&password=".encode()) - ql.target_addr = addr[0] + addr = ql.mem.search(b"HTTP_COOKIE=uid=1234&password=") + target_addr = addr[0] + ql_path = ql.path.encode() - main_addr = ql.loader.elf_entry - ql.hook_address(callback=start_afl, address=main_addr) + ql.hook_address(start_afl, ql.loader.elf_entry) - try: - ql.run() - os._exit(0) - except: - if enable_trace: - print("\nFuzzer Went Shit") - os._exit(0) + ql.run() if __name__ == "__main__": - if len(sys.argv) == 1: + if len(sys.argv) < 2: raise ValueError("No input file provided.") - if len(sys.argv) > 2 and sys.argv[1] == "-t": - main(sys.argv[2], enable_trace=True) - else: - main(sys.argv[1]) + + main(sys.argv[1]) diff --git a/examples/sality.py b/examples/sality.py index 22d6f651..be05753b 100644 --- a/examples/sality.py +++ b/examples/sality.py @@ -159,7 +159,7 @@ def hook_StartServiceA(ql: Qiling, address: int, params): init_unseen_symbols(ql.amsint32_driver, ntoskrnl.base+0xb7695, b"NtTerminateProcess", 0, "ntoskrnl.exe") #ql.amsint32_driver.debugger= ":9999" try: - ql.amsint32_driver.load() + ql.amsint32_driver.run() return 1 except UcError as e: print("Load driver error: ", e) diff --git a/examples/tendaac1518_httpd.py b/examples/tendaac1518_httpd.py index 0a32fd27..165aff1f 100644 --- a/examples/tendaac1518_httpd.py +++ b/examples/tendaac1518_httpd.py @@ -78,6 +78,8 @@ def my_sandbox(path, rootfs): ql.os.set_syscall('vfork', __vfork) + os.unlink(fr'{ROOTFS}/proc/sys/kernel/core_pattern') + ql.run() From 44087e16b0e9362f7078c77b6d96cdc4a9aaee65 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 10 Oct 2025 13:03:40 +0300 Subject: [PATCH 44/54] Add missing import --- qiling/os/uefi/st.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qiling/os/uefi/st.py b/qiling/os/uefi/st.py index 6163b6c7..305c4664 100644 --- a/qiling/os/uefi/st.py +++ b/qiling/os/uefi/st.py @@ -3,6 +3,8 @@ # Cross Platform and Multi Architecture Advanced Binary Emulation Framework # +from __future__ import annotations + from typing import TYPE_CHECKING from qiling.os.uefi import bs, rt, ds From a8edbe1323f7b2f0c2d98c711d66cddd34b46760 Mon Sep 17 00:00:00 2001 From: juliangrtz Date: Mon, 13 Oct 2025 21:42:38 +0200 Subject: [PATCH 45/54] Support IDA 9.x --- qiling/extensions/idaplugin/qilingida.py | 28 ++++++++++-------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/qiling/extensions/idaplugin/qilingida.py b/qiling/extensions/idaplugin/qilingida.py index 9aaebc35..045d8009 100644 --- a/qiling/extensions/idaplugin/qilingida.py +++ b/qiling/extensions/idaplugin/qilingida.py @@ -38,8 +38,8 @@ import ida_netnode import ida_hexrays import ida_range # PyQt -from PyQt5 import QtCore, QtWidgets -from PyQt5.QtWidgets import (QPushButton, QHBoxLayout) +from PySide6 import QtCore, QtWidgets +from PySide6.QtWidgets import (QPushButton, QHBoxLayout) # Qiling from qiling import Qiling @@ -293,25 +293,21 @@ class IDA: def get_input_file_path(): return ida_nalt.get_input_file_path() - @staticmethod - def get_info_structure(): - return ida_idaapi.get_inf_structure() - @staticmethod def get_main_address(): - return IDA.get_info_structure().main + return ida_ida.inf_get_main() @staticmethod def get_max_address(): - return IDA.get_info_structure().max_ea + return ida_ida.inf_get_max_ea() @staticmethod def get_min_address(): - return IDA.get_info_structure().min_ea + return ida_ida.inf_get_min_ea() @staticmethod def is_big_endian(): - return IDA.get_info_structure().is_be() + return ida_ida.inf_is_be() @staticmethod def is_little_endian(): @@ -319,8 +315,7 @@ class IDA: @staticmethod def get_filetype(): - info = IDA.get_info_structure() - ftype = info.filetype + ftype = ida_ida.inf_get_filetype() if ftype == ida_ida.f_MACHO: return "macho" elif ftype == ida_ida.f_PE or ftype == ida_ida.f_EXE or ftype == ida_ida.f_EXE_old: # is this correct? @@ -332,18 +327,17 @@ class IDA: @staticmethod def get_ql_arch_string(): - info = IDA.get_info_structure() - proc = info.procname.lower() + proc = ida_ida.inf_get_procname().lower() result = None if proc == "metapc": result = "x86" - if info.is_64bit(): + if ida_ida.inf_is_64bit(): result = "x8664" elif "mips" in proc: result = "mips" elif "arm" in proc: result = "arm32" - if info.is_64bit(): + if ida_ida.inf_is_64bit(): result = "arm64" # That's all we support :( return result @@ -1006,7 +1000,7 @@ class QlEmuPlugin(plugin_t, UI_Hooks): def init(self): # init data logging.info('---------------------------------------------------------------------------------------') - logging.info('Qiling Emulator Plugin For IDA, by Qiling Team. Version {0}, 2020'.format(QLVERSION)) + logging.info('Qiling Emulator Plugin For IDA, by Qiling Team. Version {0}, 2025'.format(QLVERSION)) logging.info('Based on Qiling v{0}'.format(QLVERSION)) logging.info('Find more information about Qiling at https://qiling.io') logging.info('---------------------------------------------------------------------------------------') From bfbc1c83b0a14428371c52e9d8e49ab526ec8f44 Mon Sep 17 00:00:00 2001 From: juliangrtz Date: Tue, 14 Oct 2025 10:11:17 +0200 Subject: [PATCH 46/54] =?UTF-8?q?Implement=20backwards=20compatibility=20f?= =?UTF-8?q?or=20IDA=207=E2=80=938?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qiling/extensions/idaplugin/qilingida.py | 31 ++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/qiling/extensions/idaplugin/qilingida.py b/qiling/extensions/idaplugin/qilingida.py index 045d8009..c77ba75a 100644 --- a/qiling/extensions/idaplugin/qilingida.py +++ b/qiling/extensions/idaplugin/qilingida.py @@ -15,6 +15,8 @@ from elftools.elf.elffile import ELFFile from json import load # IDA Python SDK +IDA_VERSION = IDAPYTHON_VERSION[0] + from idaapi import * from idc import * from idautils import * @@ -38,8 +40,12 @@ import ida_netnode import ida_hexrays import ida_range # PyQt -from PySide6 import QtCore, QtWidgets -from PySide6.QtWidgets import (QPushButton, QHBoxLayout) +if IDA_VERSION >= 9: + from PySide6 import QtCore, QtWidgets + from PySide6.QtWidgets import (QPushButton, QHBoxLayout) +else: + from PyQt5 import QtCore, QtWidgets + from PyQt5.QtWidgets import (QPushButton, QHBoxLayout) # Qiling from qiling import Qiling @@ -293,21 +299,25 @@ class IDA: def get_input_file_path(): return ida_nalt.get_input_file_path() + @staticmethod + def get_info_structure(): + return ida_idaapi.get_inf_structure() + @staticmethod def get_main_address(): - return ida_ida.inf_get_main() + return ida_ida.inf_get_main() if IDA_VERSION >= 9 else IDA.get_info_structure().main @staticmethod def get_max_address(): - return ida_ida.inf_get_max_ea() + return ida_ida.inf_get_max_ea() if IDA_VERSION >= 9 else IDA.get_info_structure().max_ea @staticmethod def get_min_address(): - return ida_ida.inf_get_min_ea() + return ida_ida.inf_get_min_ea() if IDA_VERSION >= 9 else IDA.get_info_structure().min_ea @staticmethod def is_big_endian(): - return ida_ida.inf_is_be() + return ida_ida.inf_is_be() if IDA_VERSION >= 9 else IDA.get_info_structure().is_be @staticmethod def is_little_endian(): @@ -315,7 +325,7 @@ class IDA: @staticmethod def get_filetype(): - ftype = ida_ida.inf_get_filetype() + ftype = ida_ida.inf_get_filetype() if IDA_VERSION >= 9 else IDA.get_info_structure().filetype if ftype == ida_ida.f_MACHO: return "macho" elif ftype == ida_ida.f_PE or ftype == ida_ida.f_EXE or ftype == ida_ida.f_EXE_old: # is this correct? @@ -327,17 +337,18 @@ class IDA: @staticmethod def get_ql_arch_string(): - proc = ida_ida.inf_get_procname().lower() + proc = (ida_ida.inf_get_procname() if IDA_VERSION >= 9 else IDA.get_info_structure().procname).lower() result = None + is_64_bit = ida_ida.inf_is_64bit() if IDA_VERSION >= 9 else IDA.get_info_structure().is_64bit() if proc == "metapc": result = "x86" - if ida_ida.inf_is_64bit(): + if is_64_bit: result = "x8664" elif "mips" in proc: result = "mips" elif "arm" in proc: result = "arm32" - if ida_ida.inf_is_64bit(): + if is_64_bit: result = "arm64" # That's all we support :( return result From 2a4bf75ea94434c93a175574d1ed21b34c89bd51 Mon Sep 17 00:00:00 2001 From: juliangrtz Date: Wed, 15 Oct 2025 22:12:21 +0200 Subject: [PATCH 47/54] Separate IDA class into IDABase, IDA7 and IDA9 --- qiling/extensions/idaplugin/qilingida.py | 270 ++++++++++++++--------- 1 file changed, 162 insertions(+), 108 deletions(-) diff --git a/qiling/extensions/idaplugin/qilingida.py b/qiling/extensions/idaplugin/qilingida.py index c77ba75a..cdbcbf6b 100644 --- a/qiling/extensions/idaplugin/qilingida.py +++ b/qiling/extensions/idaplugin/qilingida.py @@ -5,7 +5,6 @@ UseAsScript = True import sys import collections -import time import struct import re import logging @@ -15,8 +14,6 @@ from elftools.elf.elffile import ELFFile from json import load # IDA Python SDK -IDA_VERSION = IDAPYTHON_VERSION[0] - from idaapi import * from idc import * from idautils import * @@ -39,13 +36,6 @@ import ida_auto import ida_netnode import ida_hexrays import ida_range -# PyQt -if IDA_VERSION >= 9: - from PySide6 import QtCore, QtWidgets - from PySide6.QtWidgets import (QPushButton, QHBoxLayout) -else: - from PyQt5 import QtCore, QtWidgets - from PyQt5.QtWidgets import (QPushButton, QHBoxLayout) # Qiling from qiling import Qiling @@ -61,7 +51,6 @@ from qiling import __version__ as QLVERSION from qiling.os.filestruct import ql_file from keystone import * - QilingHomePage = 'https://www.qiling.io' QilingStableVersionURL = 'https://raw.githubusercontent.com/qilingframework/qiling/master/qiling/__version__.py' logging.basicConfig(level=logging.INFO, format='[%(levelname)s][%(module)s:%(lineno)d] %(message)s') @@ -75,7 +64,27 @@ class Colors(Enum): Gray = 0xd9d9d9 Beige = 0xCCF2FF -class IDA: +def _load_qt_bindings(): + if IDA_SDK_VERSION >= 900: + try: + from PySide6 import QtCore, QtWidgets + from PySide6.QtWidgets import (QPushButton, QHBoxLayout) + logging.info("Using PySide6 for Qt bindings (IDA >= 9).") + return QtCore, QtWidgets, QPushButton, QHBoxLayout + except Exception as e: + logging.warning("Failed to import PySide6: %s. Trying PyQt5 fallback.", e) + try: + from PyQt5 import QtCore, QtWidgets + from PyQt5.QtWidgets import (QPushButton, QHBoxLayout) + logging.info("Using PyQt5 for Qt bindings (IDA < 9 or fallback).") + return QtCore, QtWidgets, QPushButton, QHBoxLayout + except Exception as e: + logging.error("Failed to import PyQt bindings: %s", e) + raise + +QtCore, QtWidgets, QPushButton, QHBoxLayout = _load_qt_bindings() + +class IDABase: def __init__(self): pass @@ -85,15 +94,15 @@ class IDA: @staticmethod def get_function_start(addr): - return IDA.get_function(addr).start_ea + return IDABase.get_function(addr).start_ea @staticmethod def get_function_end(addr): - return IDA.get_function(addr).end_ea + return IDABase.get_function(addr).end_ea @staticmethod def get_function_framesize(addr): - return IDA.get_function(addr).frsize + return IDABase.get_function(addr).frsize @staticmethod def get_function_name(addr): @@ -101,7 +110,7 @@ class IDA: @staticmethod def get_functions(): - return [IDA.get_function(func) for func in idautils.Functions()] + return [IDABase.get_function(func) for func in idautils.Functions()] @staticmethod def set_color(addr, what, color): @@ -110,7 +119,7 @@ class IDA: @staticmethod def color_block(bb, color): for i in range(bb.start_ea, bb.end_ea): - IDA.set_color(i, idc.CIC_ITEM, color) + IDABase.set_color(i, idc.CIC_ITEM, color) # note: # corresponds to IDA graph view @@ -119,8 +128,8 @@ class IDA: # arg can be a function or a (start, end) tuple or an address in the function @staticmethod def get_flowchart(arg): - if type(arg) is int: - func = IDA.get_function(arg) + if isinstance(arg, int): + func = IDABase.get_function(arg) if func is None: return None return ida_gdl.FlowChart(func) @@ -128,7 +137,9 @@ class IDA: @staticmethod def get_block(addr): - flowchart = IDA.get_flowchart(addr) + flowchart = IDABase.get_flowchart(addr) + if flowchart is None: + return None for bb in flowchart: if bb.start_ea <= addr and addr < bb.end_ea: return bb @@ -149,10 +160,10 @@ class IDA: @staticmethod def get_starting_block(addr): - flowchart = IDA.get_flowchart(addr) + flowchart = IDABase.get_flowchart(addr) if flowchart is None: return None - func = IDA.get_function(addr) + func = IDABase.get_function(addr) for bb in flowchart: if bb.start_ea == func.start_ea: return bb @@ -160,8 +171,10 @@ class IDA: @staticmethod def get_terminating_blocks(addr): - flowchart = IDA.get_flowchart(addr) - return [bb for bb in flowchart if IDA.block_is_terminating(bb)] + flowchart = IDABase.get_flowchart(addr) + if flowchart is None: + return [] + return [bb for bb in flowchart if IDABase.block_is_terminating(bb)] @staticmethod def get_prev_head(addr, minea=0): @@ -186,46 +199,45 @@ class IDA: @staticmethod def __addr_in_seg(addr): - segs = IDA.get_segments() + segs = IDABase.get_segments() for seg in segs: if addr < seg.end_ea and addr >= seg.start_ea: return seg return None - # note: accept name and address in the segment @staticmethod def get_segment(arg): - if type(arg) is int: - return IDA.__addr_in_seg(arg) - else: # str - return IDA.get_segment_by_name(arg) + if isinstance(arg, int): + return IDABase.__addr_in_seg(arg) + else: + return IDABase.get_segment_by_name(arg) @staticmethod def get_segment_start(arg): - seg = IDA.get_segment(arg) + seg = IDABase.get_segment(arg) if seg is not None: return seg.start_ea return None @staticmethod def get_segment_end(arg): - seg = IDA.get_segment(arg) + seg = IDABase.get_segment(arg) if seg is not None: return seg.end_ea return None @staticmethod def get_segment_perm(arg): - seg = IDA.get_segment(arg) + seg = IDABase.get_segment(arg) if seg is not None: - return seg.perm # RWX e.g. 0b101 = R + X + return seg.perm return None @staticmethod def get_segment_type(arg): - seg = IDA.get_segment(arg) + seg = IDABase.get_segment(arg) if seg is not None: - return seg.type # 0x1 SEG_DATA 0x2 SEG_CODE See doc for details + return seg.type return None @staticmethod @@ -235,12 +247,10 @@ class IDA: return None return r - # immidiate value @staticmethod def get_operand(addr, n): return (idc.get_operand_type(addr, n), idc.get_operand_value(addr, n)) - # eax, ecx, etc @staticmethod def print_operand(addr, n): return idc.print_operand(addr, n) @@ -254,7 +264,7 @@ class IDA: p = begin cnt = 0 while p < end: - sz = IDA.get_instruction_size(p) + sz = IDABase.get_instruction_size(p) cnt += 1 p += sz return cnt @@ -299,95 +309,34 @@ class IDA: def get_input_file_path(): return ida_nalt.get_input_file_path() - @staticmethod - def get_info_structure(): - return ida_idaapi.get_inf_structure() - - @staticmethod - def get_main_address(): - return ida_ida.inf_get_main() if IDA_VERSION >= 9 else IDA.get_info_structure().main - - @staticmethod - def get_max_address(): - return ida_ida.inf_get_max_ea() if IDA_VERSION >= 9 else IDA.get_info_structure().max_ea - - @staticmethod - def get_min_address(): - return ida_ida.inf_get_min_ea() if IDA_VERSION >= 9 else IDA.get_info_structure().min_ea - - @staticmethod - def is_big_endian(): - return ida_ida.inf_is_be() if IDA_VERSION >= 9 else IDA.get_info_structure().is_be - - @staticmethod - def is_little_endian(): - return not IDA.is_big_endian() - - @staticmethod - def get_filetype(): - ftype = ida_ida.inf_get_filetype() if IDA_VERSION >= 9 else IDA.get_info_structure().filetype - if ftype == ida_ida.f_MACHO: - return "macho" - elif ftype == ida_ida.f_PE or ftype == ida_ida.f_EXE or ftype == ida_ida.f_EXE_old: # is this correct? - return "pe" - elif ftype == ida_ida.f_ELF: - return "elf" - else: - return None - - @staticmethod - def get_ql_arch_string(): - proc = (ida_ida.inf_get_procname() if IDA_VERSION >= 9 else IDA.get_info_structure().procname).lower() - result = None - is_64_bit = ida_ida.inf_is_64bit() if IDA_VERSION >= 9 else IDA.get_info_structure().is_64bit() - if proc == "metapc": - result = "x86" - if is_64_bit: - result = "x8664" - elif "mips" in proc: - result = "mips" - elif "arm" in proc: - result = "arm32" - if is_64_bit: - result = "arm64" - # That's all we support :( - return result - @staticmethod def get_current_address(): return ida_kernwin.get_screen_ea() - # return (?, start, end) @staticmethod def get_last_selection(): return ida_kernwin.read_range_selection(None) - # Use with skipcalls - # note that the address is the end of target instruction - # e.g.: - # 0x1 push eax - # 0x4 mov eax, 0 - # call get_frame_sp_delta(0x4) and get -4. @staticmethod def get_frame_sp_delta(addr): - return ida_frame.get_sp_delta(IDA.get_function(addr), addr) + return ida_frame.get_sp_delta(IDABase.get_function(addr), addr) @staticmethod def patch_bytes(addr, bs): return ida_bytes.patch_bytes(addr, bs) @staticmethod - def fill_bytes(start, end, bs = b'\x90'): + def fill_bytes(start, end, bs=b'\x90'): return ida_bytes.patch_bytes(start, bs*(end-start)) @staticmethod def nop_selection(): - _, start, end = IDA.get_last_selection() - return IDA.fill_bytes(start, end) + _, start, end = IDABase.get_last_selection() + return IDABase.fill_bytes(start, end) @staticmethod def fill_block(bb, bs=b'\x90'): - return IDA.fill_bytes(bb.start_ea, bb.end_ea, bs) + return IDABase.fill_bytes(bb.start_ea, bb.end_ea, bs) @staticmethod def assemble(ea, cs, ip, use32, line): @@ -399,7 +348,7 @@ class IDA: @staticmethod def create_bytes_array(start, end): - return IDA.create_data(start, ida_bytes.byte_flag(), end-start) + return IDABase.create_data(start, ida_bytes.byte_flag(), end-start) @staticmethod def create_byte(ea, length, force=False): @@ -423,13 +372,12 @@ class IDA: @staticmethod def get_item(ea): - return (IDA.get_item_head(ea), IDA.get_item_end(ea)) + return (IDABase.get_item_head(ea), IDABase.get_item_end(ea)) @staticmethod def is_colored_item(ea): return ida_nalt.is_colored_item(ea) - # NOTE: The [start, end) range should include all control flows except long calls. @staticmethod def get_micro_code_mba(start, end, decomp_flags=ida_hexrays.DECOMP_WARNINGS, maturity=7): mbrgs = ida_hexrays.mba_ranges_t() @@ -449,6 +397,112 @@ class IDA: cur = cur.next return +class IDA7(IDABase): + @staticmethod + def get_info_structure(): + return ida_idaapi.get_inf_structure() + + @staticmethod + def get_main_address(): + return IDA7.get_info_structure().main + + @staticmethod + def get_max_address(): + return IDA7.get_info_structure().max_ea + + @staticmethod + def get_min_address(): + return IDA7.get_info_structure().min_ea + + @staticmethod + def is_big_endian(): + return IDA7.get_info_structure().is_be + + @staticmethod + def is_little_endian(): + return not IDA7.is_big_endian() + + @staticmethod + def get_filetype(): + ftype = IDA7.get_info_structure().filetype + if ftype in (ida_ida.f_PE, ida_ida.f_EXE, ida_ida.f_EXE_old): + return "pe" + elif ftype == ida_ida.f_MACHO: + return "macho" + elif ftype == ida_ida.f_ELF: + return "elf" + return None + + @staticmethod + def get_ql_arch_string(): + proc = IDA7.get_info_structure().procname.lower() + is_64_bit = IDA7.get_info_structure().is_64bit() + if proc == "metapc": + return "x8664" if is_64_bit else "x86" + if "mips" in proc: + return "mips" + if "arm" in proc: + return "arm64" if is_64_bit else "arm32" + return None + +class IDA9(IDABase): + @staticmethod + def get_info_structure(): + return ida_idaapi.get_inf_structure() + + @staticmethod + def get_main_address(): + return ida_ida.inf_get_main() + + @staticmethod + def get_max_address(): + return ida_ida.inf_get_max_ea() + + @staticmethod + def get_min_address(): + return ida_ida.inf_get_min_ea() + + @staticmethod + def is_big_endian(): + return ida_ida.inf_is_be() + + @staticmethod + def is_little_endian(): + return not ida_ida.inf_is_be() + + @staticmethod + def get_filetype(): + ftype = ida_ida.inf_get_filetype() + if ftype in (ida_ida.f_PE, ida_ida.f_EXE, ida_ida.f_EXE_old): + return "pe" + elif ftype == ida_ida.f_MACHO: + return "macho" + elif ftype == ida_ida.f_ELF: + return "elf" + return None + + @staticmethod + def get_ql_arch_string(): + proc = ida_ida.inf_get_procname().lower() + is_64_bit = ida_ida.inf_is_64bit() + if proc == "metapc": + return "x8664" if is_64_bit else "x86" + if "mips" in proc: + return "mips" + if "arm" in proc: + return "arm64" if is_64_bit else "arm32" + return None + +def get_ida_instance(): + if IDA_SDK_VERSION >= 900: + logging.info("Using IDA9 compatibility layer") + return IDA9() + else: + logging.info("Using IDA7 compatibility layer") + return IDA7() + +IDA = get_ida_instance() + ### View Class class QlEmuRegView(simplecustviewer_t): From 147ec7c18c81871cf465e9f2c783ff487438d33a Mon Sep 17 00:00:00 2001 From: elicn Date: Wed, 29 Oct 2025 14:43:10 +0200 Subject: [PATCH 48/54] Make Windows compatible to latest image --- qiling/loader/pe.py | 1 + tests/test_pe_sys.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qiling/loader/pe.py b/qiling/loader/pe.py index 8ea5884b..30959d0f 100644 --- a/qiling/loader/pe.py +++ b/qiling/loader/pe.py @@ -644,6 +644,7 @@ class Process: # DLLs that seem to contain most of the requested symbols key_dlls = ( + 'kernel32.dll', 'ntdll.dll', 'kernelbase.dll', 'ucrtbase.dll' diff --git a/tests/test_pe_sys.py b/tests/test_pe_sys.py index 546c0dee..17af4290 100644 --- a/tests/test_pe_sys.py +++ b/tests/test_pe_sys.py @@ -219,8 +219,8 @@ class PETest(unittest.TestCase): fcall.writeParams(((DWORD, 0),)) # run until third stop - # TODO: Should stop at 0x10423, but for now just stop at 0x0001066a - amsint32.hook_address(hook_third_stop_address, 0x0001066a, stops) + # TODO: Should stop at 0x10423, but for now just stop at 0x10430 + amsint32.hook_address(hook_third_stop_address, 0x10430, stops) amsint32.run(begin=0x102D0) self.assertTrue(stops[0]) From c5d7a8533044ae33cb9cb4436107b1959257c644 Mon Sep 17 00:00:00 2001 From: elicn Date: Thu, 30 Oct 2025 10:50:18 +0200 Subject: [PATCH 49/54] Add rseq syscall dummy implementation --- qiling/os/posix/syscall/__init__.py | 1 + qiling/os/posix/syscall/rseq.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 qiling/os/posix/syscall/rseq.py diff --git a/qiling/os/posix/syscall/__init__.py b/qiling/os/posix/syscall/__init__.py index 38b10e64..1ed1125e 100644 --- a/qiling/os/posix/syscall/__init__.py +++ b/qiling/os/posix/syscall/__init__.py @@ -14,6 +14,7 @@ from .prctl import * from .ptrace import * from .random import * from .resource import * +from .rseq import * from .sched import * from .select import * from .sendfile import * diff --git a/qiling/os/posix/syscall/rseq.py b/qiling/os/posix/syscall/rseq.py new file mode 100644 index 00000000..403595a6 --- /dev/null +++ b/qiling/os/posix/syscall/rseq.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# +# Cross Platform and Multi Architecture Advanced Binary Emulation Framework +# + +from qiling import Qiling + + +def ql_syscall_rseq(ql: Qiling, rseq: int, rseq_len: int, flags: int, sig: int): + # indicate rseq is not supported by this kernel + # return -ENOSYS + + return 0 From 45d87c5b69a6e51f7d95152bca608f39e1815e7f Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 31 Oct 2025 14:54:19 +0200 Subject: [PATCH 50/54] Consolidate mmio bookeeping with ram --- qiling/hw/hw.py | 2 +- qiling/os/memory.py | 58 +++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/qiling/hw/hw.py b/qiling/hw/hw.py index 3d869d56..15213a7b 100644 --- a/qiling/hw/hw.py +++ b/qiling/hw/hw.py @@ -182,6 +182,6 @@ class QlHwManager: # a dirty hack to rehydrate non-pickleable hwman # a proper fix would require a deeper refactoring to how peripherals are created and managed - for ph in self.ql.mem.mmio_cbs.values(): + for *_, ph in self.ql.mem.map_info: if isinstance(ph, QlPripheralHandler): setattr(ph, '_hwman', self) diff --git a/qiling/os/memory.py b/qiling/os/memory.py index 4c0f8654..5c85a4b2 100644 --- a/qiling/os/memory.py +++ b/qiling/os/memory.py @@ -13,12 +13,6 @@ from unicorn import UC_PROT_NONE, UC_PROT_READ, UC_PROT_WRITE, UC_PROT_EXEC, UC_ from qiling import Qiling from qiling.exception import * -# tuple: range start, range end, permissions mask, range label, is mmio? -MapInfoEntry = Tuple[int, int, int, str, bool] - -MmioReadCallback = Callable[[Qiling, int, int], int] -MmioWriteCallback = Callable[[Qiling, int, int, int], None] - class QlMmioHandler(Protocol): """A simple MMIO handler boilerplate that can be used to implement memory mapped devices. @@ -36,6 +30,13 @@ class QlMmioHandler(Protocol): ... +# tuple: range start, range end, permissions mask, range label, mmio hander object (if mmio range) +MapInfoEntry = Tuple[int, int, int, str, Optional[QlMmioHandler]] + +MmioReadCallback = Callable[[Qiling, int, int], int] +MmioWriteCallback = Callable[[Qiling, int, int, int], None] + + class QlMemoryManager: """ some ideas and code from: @@ -45,7 +46,6 @@ class QlMemoryManager: def __init__(self, ql: Qiling, pagesize: int = 0x1000): self.ql = ql self.map_info: List[MapInfoEntry] = [] - self.mmio_cbs: Dict[Tuple[int, int], QlMmioHandler] = {} bit_stuff = { 64: (1 << 64) - 1, @@ -121,7 +121,7 @@ class QlMemoryManager: self.__write_string(addr, value, encoding) - def add_mapinfo(self, mem_s: int, mem_e: int, mem_p: int, mem_info: str, is_mmio: bool = False): + def add_mapinfo(self, mem_s: int, mem_e: int, mem_p: int, mem_info: str, mmio_ctx: Optional[QlMmioHandler] = None): """Add a new memory range to map. Args: @@ -129,10 +129,10 @@ class QlMemoryManager: mem_e: memory range end mem_p: permissions mask mem_info: map entry label - is_mmio: memory range is mmio + mmio_ctx: mmio handler object; if specified the range will be treated as mmio """ - bisect.insort(self.map_info, (mem_s, mem_e, mem_p, mem_info, is_mmio)) + bisect.insort(self.map_info, (mem_s, mem_e, mem_p, mem_info, mmio_ctx)) def del_mapinfo(self, mem_s: int, mem_e: int): """Subtract a memory range from map. @@ -146,13 +146,13 @@ class QlMemoryManager: def __split_overlaps(): for idx in overlap_ranges: - lbound, ubound, perms, label, is_mmio = self.map_info[idx] + lbound, ubound, perms, label, mmio_ctx = self.map_info[idx] if lbound < mem_s: - yield (lbound, mem_s, perms, label, is_mmio) + yield (lbound, mem_s, perms, label, mmio_ctx) if mem_e < ubound: - yield (mem_e, ubound, perms, label, is_mmio) + yield (mem_e, ubound, perms, label, mmio_ctx) # indices of first and last overlapping ranges. since map info is always # sorted, we know that all overlapping rages are consecutive, so i1 > i0 @@ -209,18 +209,18 @@ class QlMemoryManager: return ''.join(val if idx & ps else '-' for idx, val in perms_d.items()) - def __process(lbound: int, ubound: int, perms: int, label: str, is_mmio: bool) -> Tuple[int, int, str, str, str]: - perms_str = __perms_mapping(perms) + def __process(entry: MapInfoEntry) -> Tuple[int, int, str, str, str]: + lbound, ubound, perms, label, mmio_ctx = entry if hasattr(self.ql, 'loader'): image = self.ql.loader.find_containing_image(lbound) - container = image.path if image and not is_mmio else '' + container = image.path if image and mmio_ctx is None else '' else: container = '' - return (lbound, ubound, perms_str, label, container) + return (lbound, ubound, __perms_mapping(perms), label, container) - return tuple(__process(*entry) for entry in self.map_info) + return tuple(__process(entry) for entry in self.map_info) def get_formatted_mapinfo(self) -> Sequence[str]: """Get memory map info in a nicely formatted table. @@ -311,12 +311,13 @@ class QlMemoryManager: "mmio" : [] } - for lbound, ubound, perm, label, is_mmio in self.map_info: - if is_mmio: - mem_dict['mmio'].append((lbound, ubound, perm, label, self.mmio_cbs[(lbound, ubound)])) + for lbound, ubound, perm, label, mmio_ctx in self.map_info: + if mmio_ctx is None: + key, data = 'ram', bytes(self.read(lbound, ubound - lbound)) else: - data = self.read(lbound, ubound - lbound) - mem_dict['ram'].append((lbound, ubound, perm, label, bytes(data))) + key, data = 'mmio', mmio_ctx + + mem_dict[key].append((lbound, ubound, perm, label, data)) return mem_dict @@ -429,7 +430,7 @@ class QlMemoryManager: assert begin < end, 'search arguments do not make sense' # narrow the search down to relevant ranges; mmio ranges are excluded due to potential read side effects - ranges = [(max(begin, lbound), min(ubound, end)) for lbound, ubound, _, _, is_mmio in self.map_info if not (end < lbound or ubound < begin or is_mmio)] + ranges = [(max(begin, lbound), min(ubound, end)) for lbound, ubound, _, _, mmio_ctx in self.map_info if not (end < lbound or ubound < begin or mmio_ctx is not None)] results = [] # if needle is a bytes sequence use it verbatim, not as a pattern @@ -455,9 +456,6 @@ class QlMemoryManager: self.del_mapinfo(addr, addr + size) self.ql.uc.mem_unmap(addr, size) - if (addr, addr + size) in self.mmio_cbs: - del self.mmio_cbs[(addr, addr+size)] - def unmap_between(self, mem_s: int, mem_e: int) -> None: """Reclaim any allocated memory region within the specified range. @@ -638,7 +636,7 @@ class QlMemoryManager: raise QlMemoryMappedError('Requested memory is unavailable') self.ql.uc.mem_map(addr, size, perms) - self.add_mapinfo(addr, addr + size, perms, info or '[mapped]', is_mmio=False) + self.add_mapinfo(addr, addr + size, perms, info or '[mapped]', None) def map_mmio(self, addr: int, size: int, handler: QlMmioHandler, info: str = '[mmio]'): # TODO: mmio memory overlap with ram? Is that possible? @@ -664,9 +662,7 @@ class QlMemoryManager: cb(self.ql, offset, size, value) self.ql.uc.mmio_map(addr, size, __mmio_read, handler.read, __mmio_write, handler.write) - self.add_mapinfo(addr, addr + size, prot, info, is_mmio=True) - - self.mmio_cbs[(addr, addr + size)] = handler + self.add_mapinfo(addr, addr + size, prot, info, handler) class Chunk: From b6f085c4d2148a15870ff1f99ea6be4e3344c4b3 Mon Sep 17 00:00:00 2001 From: elicn Date: Fri, 31 Oct 2025 15:28:34 +0200 Subject: [PATCH 51/54] Minor bug fix in change_mapinfo --- qiling/os/memory.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/qiling/os/memory.py b/qiling/os/memory.py index 5c85a4b2..76043895 100644 --- a/qiling/os/memory.py +++ b/qiling/os/memory.py @@ -170,27 +170,30 @@ class QlMemoryManager: for entry in new_entries: bisect.insort(self.map_info, entry) - def change_mapinfo(self, mem_s: int, mem_e: int, mem_p: Optional[int] = None, mem_info: Optional[str] = None): - tmp_map_info: Optional[MapInfoEntry] = None - info_idx: int = -1 + def change_mapinfo(self, mem_s: int, mem_e: int, *, new_perms: Optional[int] = None, new_info: Optional[str] = None) -> None: + if new_perms is None and new_info is None: + # nothing to do + return - for idx, map_info in enumerate(self.map_info): - if mem_s >= map_info[0] and mem_e <= map_info[1]: - tmp_map_info = map_info - info_idx = idx - break - - if tmp_map_info is None: + try: + # locate the map info entry to change + entry = next(entry for entry in self.map_info if mem_s >= entry[0] and mem_e <= entry[1]) + except StopIteration: self.ql.log.error(f'Cannot change mapinfo at {mem_s:#08x}-{mem_e:#08x}') return - if mem_p is not None: - self.del_mapinfo(mem_s, mem_e) - self.add_mapinfo(mem_s, mem_e, mem_p, mem_info if mem_info else tmp_map_info[3]) - return + _, _, perms, info, mmio_ctx = entry - if mem_info is not None: - self.map_info[info_idx] = (tmp_map_info[0], tmp_map_info[1], tmp_map_info[2], mem_info, tmp_map_info[4]) + # caller wants to change perms? + if new_perms is not None: + perms = new_perms + + # caller wants to change info? + if new_info is not None: + info = new_info + + self.del_mapinfo(mem_s, mem_e) + self.add_mapinfo(mem_s, mem_e, perms, info, mmio_ctx) def get_mapinfo(self) -> Sequence[Tuple[int, int, str, str, str]]: """Get memory map info. @@ -614,7 +617,7 @@ class QlMemoryManager: aligned_size = self.align_up((addr & (self.pagesize - 1)) + size) self.ql.uc.mem_protect(aligned_address, aligned_size, perms) - self.change_mapinfo(aligned_address, aligned_address + aligned_size, perms) + self.change_mapinfo(aligned_address, aligned_address + aligned_size, new_perms=perms) def map(self, addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None): """Map a new memory range. From 29c552e05d33f7a58c37a1aec7e2e45c9d10d2cd Mon Sep 17 00:00:00 2001 From: golem9247 Date: Tue, 4 Nov 2025 14:57:00 +0100 Subject: [PATCH 52/54] add ppc xml basic target , add a no yield coverage --- qiling/debugger/gdb/xml/ppc/ppc-core.xml | 46 +++++++++++++++++++++ qiling/debugger/gdb/xml/ppc/target.xml | 13 ++++++ qiling/debugger/gdb/xmlregs.py | 7 +++- qiling/extensions/coverage/formats/drcov.py | 11 +++++ qiling/extensions/coverage/utils.py | 9 ++++ 5 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 qiling/debugger/gdb/xml/ppc/ppc-core.xml create mode 100644 qiling/debugger/gdb/xml/ppc/target.xml diff --git a/qiling/debugger/gdb/xml/ppc/ppc-core.xml b/qiling/debugger/gdb/xml/ppc/ppc-core.xml new file mode 100644 index 00000000..c40c52ec --- /dev/null +++ b/qiling/debugger/gdb/xml/ppc/ppc-core.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/qiling/debugger/gdb/xml/ppc/target.xml b/qiling/debugger/gdb/xml/ppc/target.xml new file mode 100644 index 00000000..6839b3e6 --- /dev/null +++ b/qiling/debugger/gdb/xml/ppc/target.xml @@ -0,0 +1,13 @@ + + + + + + ppc + + + \ No newline at end of file diff --git a/qiling/debugger/gdb/xmlregs.py b/qiling/debugger/gdb/xmlregs.py index 4749b211..fe200fce 100644 --- a/qiling/debugger/gdb/xmlregs.py +++ b/qiling/debugger/gdb/xmlregs.py @@ -30,6 +30,10 @@ from qiling.arch.x86_const import ( reg_map_ymm as x86_regs_ymm ) +from qiling.arch.ppc_const import ( + reg_map as ppc_regs +) + from qiling.const import QL_ARCH, QL_OS RegEntry = Tuple[Optional[int], int, int] @@ -134,7 +138,8 @@ class QlGdbFeatures: QL_ARCH.ARM: dict(**arm_regs, **arm_regs_vfp, **arm_regs_q, **arm_regs_s), QL_ARCH.CORTEX_M: arm_regs, QL_ARCH.ARM64: dict(**arm64_regs, **arm64_regs_v), - QL_ARCH.MIPS: dict(**mips_regs_gpr) + QL_ARCH.MIPS: dict(**mips_regs_gpr), + QL_ARCH.PPC: dict(**ppc_regs), }[archtype] regsinfo = sorted(QlGdbFeatures.__walk_xml_regs(xmltree)) diff --git a/qiling/extensions/coverage/formats/drcov.py b/qiling/extensions/coverage/formats/drcov.py index 51a42194..fa9a3017 100644 --- a/qiling/extensions/coverage/formats/drcov.py +++ b/qiling/extensions/coverage/formats/drcov.py @@ -17,6 +17,17 @@ class bb_entry(Structure): ("mod_id", c_uint16) ] + def __init__(self, start, size, module_id=None): + self.start = start + self.size = size + self.module_id = module_id + + def __eq__(self, other): + return (self.start, self.size, self.module_id) == (other.start, other.size, other.module_id) + + def __hash__(self): + return hash((self.start, self.size, self.module_id)) + class QlDrCoverage(QlBaseCoverage): """ diff --git a/qiling/extensions/coverage/utils.py b/qiling/extensions/coverage/utils.py index 4293689b..fa1f4494 100644 --- a/qiling/extensions/coverage/utils.py +++ b/qiling/extensions/coverage/utils.py @@ -61,3 +61,12 @@ def collect_coverage(ql: Qiling, name: str, coverage_file: str): finally: cov.deactivate() cov.dump_coverage(coverage_file) + +def collect_coverage_no_yield(ql: Qiling, name: str, coverage_folder: str): + cov = factory.get_coverage_collector(ql, name) + cov.coverage_folder = coverage_folder + cov.activate() + return cov + +def get_coverage_instance(ql, name): + return factory.get_coverage_collector(ql, name) From 4810ad7cfb17d7cd26213cd3fde74caed51f6e9f Mon Sep 17 00:00:00 2001 From: 0xMirasio Date: Tue, 4 Nov 2025 16:23:35 +0100 Subject: [PATCH 53/54] ppc xml fix --- qiling/debugger/gdb/gdb.py | 4 +++- qiling/debugger/gdb/xml/ppc/ppc-core.xml | 11 ++++++++--- qiling/debugger/gdb/xml/ppc/target.xml | 3 +-- qiling/extensions/coverage/utils.py | 9 --------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/qiling/debugger/gdb/gdb.py b/qiling/debugger/gdb/gdb.py index a26bf6d9..f6d6498d 100644 --- a/qiling/debugger/gdb/gdb.py +++ b/qiling/debugger/gdb/gdb.py @@ -183,6 +183,7 @@ class QlGdb(QlDebugger): from unicorn.arm_const import UC_ARM_REG_R11 from unicorn.arm64_const import UC_ARM64_REG_X29 from unicorn.mips_const import UC_MIPS_REG_INVALID + from unicorn.ppc_const import UC_PPC_REG_31 arch_uc_bp = { QL_ARCH.X86 : UC_X86_REG_EBP, @@ -191,7 +192,8 @@ class QlGdb(QlDebugger): QL_ARCH.ARM64 : UC_ARM64_REG_X29, QL_ARCH.MIPS : UC_MIPS_REG_INVALID, # skipped QL_ARCH.A8086 : UC_X86_REG_EBP, - QL_ARCH.CORTEX_M : UC_ARM_REG_R11 + QL_ARCH.CORTEX_M : UC_ARM_REG_R11, + QL_ARCH.PPC : UC_PPC_REG_31 }[self.ql.arch.type] def __get_reg_idx(ucreg: int) -> int: diff --git a/qiling/debugger/gdb/xml/ppc/ppc-core.xml b/qiling/debugger/gdb/xml/ppc/ppc-core.xml index c40c52ec..d695132a 100644 --- a/qiling/debugger/gdb/xml/ppc/ppc-core.xml +++ b/qiling/debugger/gdb/xml/ppc/ppc-core.xml @@ -6,7 +6,7 @@ notice and this notice are preserved. --> - + @@ -39,8 +39,13 @@ - - + + + + + + + \ No newline at end of file diff --git a/qiling/debugger/gdb/xml/ppc/target.xml b/qiling/debugger/gdb/xml/ppc/target.xml index 6839b3e6..977416a3 100644 --- a/qiling/debugger/gdb/xml/ppc/target.xml +++ b/qiling/debugger/gdb/xml/ppc/target.xml @@ -7,7 +7,6 @@ - ppc + powerpc:common - \ No newline at end of file diff --git a/qiling/extensions/coverage/utils.py b/qiling/extensions/coverage/utils.py index fa1f4494..4293689b 100644 --- a/qiling/extensions/coverage/utils.py +++ b/qiling/extensions/coverage/utils.py @@ -61,12 +61,3 @@ def collect_coverage(ql: Qiling, name: str, coverage_file: str): finally: cov.deactivate() cov.dump_coverage(coverage_file) - -def collect_coverage_no_yield(ql: Qiling, name: str, coverage_folder: str): - cov = factory.get_coverage_collector(ql, name) - cov.coverage_folder = coverage_folder - cov.activate() - return cov - -def get_coverage_instance(ql, name): - return factory.get_coverage_collector(ql, name) From 08650becba38daca5e0a04e2851a739863f65647 Mon Sep 17 00:00:00 2001 From: 0xMirasio Date: Wed, 5 Nov 2025 11:23:56 +0100 Subject: [PATCH 54/54] remove hashable drcov bb_entries --- qiling/extensions/coverage/formats/drcov.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/qiling/extensions/coverage/formats/drcov.py b/qiling/extensions/coverage/formats/drcov.py index d224d98a..bed0f870 100644 --- a/qiling/extensions/coverage/formats/drcov.py +++ b/qiling/extensions/coverage/formats/drcov.py @@ -25,17 +25,6 @@ class bb_entry(Structure): ("mod_id", c_uint16) ] - def __init__(self, start, size, module_id=None): - self.start = start - self.size = size - self.module_id = module_id - - def __eq__(self, other): - return (self.start, self.size, self.module_id) == (other.start, other.size, other.module_id) - - def __hash__(self): - return hash((self.start, self.size, self.module_id)) - class QlDrCoverage(QlBaseCoverage): """