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 1/6] 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 | 54 ++++++++++-------- qiling/os/blob/blob.py | 7 ++- tests/profiles/blob_raw.ql | 4 ++ tests/test_blob.py | 99 +++++++++++++++++++++++++++++++++ 9 files changed, 387 insertions(+), 25 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 000000000..164219a95 --- /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 000000000..6e44d6bf3 --- /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 000000000..7f4252f7e --- /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 000000000..5ac71cb34 --- /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 000000000..ea57d85e4 --- /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 f17b80a9d..96168538b 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 - - 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) - - # 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: stack pointer should be a configurable profile setting - self.ql.arch.regs.arch_sp = code_ends - 0x1000 + 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 + + 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?? + # 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 e4a022562..f2056e555 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 @@ def run(self): 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 000000000..164219a95 --- /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 33e35751a..d284bbac8 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 @@ def partial_run_init(ql: Qiling): 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 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 2/6] minimizing blob changes --- examples/blob_raw.ql | 6 ++-- examples/hello_arm_blob_raw.py | 26 ++++++++++++------ qiling/loader/blob.py | 50 +++++++++++++++++----------------- qiling/os/blob/blob.py | 7 ++--- tests/profiles/blob_raw.ql | 6 ++-- tests/test_blob.py | 11 ++++---- 6 files changed, 56 insertions(+), 50 deletions(-) diff --git a/examples/blob_raw.ql b/examples/blob_raw.ql index 164219a95..23390130a 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 6e44d6bf3..f260a4e05 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 96168538b..c4a69de8b 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 - - 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) - - # 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 + 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 + + 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?? + # 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 f2056e555..e4a022562 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 @@ def run(self): 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 164219a95..23390130a 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 d284bbac8..bd0b9ecfa 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -97,18 +97,17 @@ def run_checksum_emu(input_data_buffer: bytes) -> int: 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 3/6] 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 f260a4e05..6096bbd98 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 7f4252f7e..66fe0b435 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 5ac71cb34..eae56b2bb 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 ea57d85e4..9c9e74857 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 c4a69de8b..7fa5e50a4 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 bd0b9ecfa..310d202ac 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 4/6] 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 7fa5e50a4..7a7e33256 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 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 5/6] 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 6096bbd98..4c257166e 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 @@ 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.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: 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'] + 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 66fe0b435..74966f268 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 eae56b2bb..13cd70779 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 9c9e74857..ae31f2fa3 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 c33a7d238..1e95311fe 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 7a7e33256..728443391 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 @@ def run(self): # 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 e4a022562..af52fa74a 100644 --- a/qiling/os/blob/blob.py +++ b/qiling/os/blob/blob.py @@ -8,6 +8,7 @@ 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 @@ def run(self): # 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 c33a7d238..1e95311fe 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 310d202ac..753e0ef53 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 @@ def partial_run_init(ql: Qiling): 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 @@ def calculate_expected_checksum(input_data_buffer: bytes) -> int: 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 6/6] 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 f71f45fe1..120fb6d37 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 753e0ef53..0bd9a6629 100644 --- a/tests/test_blob.py +++ b/tests/test_blob.py @@ -82,7 +82,6 @@ def partial_run_init(ql: Qiling): del ql - @unittest.skip("Temporarily disabled") def test_blob_raw(self): def run_checksum_emu(input_data_buffer: bytes) -> int: """