From 3c0636562bc8ccc7c7f17e79c0a464a38c7d602c Mon Sep 17 00:00:00 2001 From: Andrew Afflitto Date: Tue, 27 Jan 2026 23:43:05 +0000 Subject: [PATCH 1/7] Add FastSegmentInjectorModifier --- .../src/ofrak/core/patch_maker/modifiers.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/ofrak_core/src/ofrak/core/patch_maker/modifiers.py b/ofrak_core/src/ofrak/core/patch_maker/modifiers.py index a59c9d39c..5cc3bfd8e 100644 --- a/ofrak_core/src/ofrak/core/patch_maker/modifiers.py +++ b/ofrak_core/src/ofrak/core/patch_maker/modifiers.py @@ -22,6 +22,7 @@ from ofrak.service.resource_service_i import ResourceFilter, ResourceSort, ResourceSortDirection from ofrak_type.memory_permissions import MemoryPermissions from ofrak_type.error import NotFoundError +from ofrak_type.range import Range LOGGER = logging.getLogger(__file__) @@ -304,6 +305,88 @@ async def modify(self, resource: Resource, config: SegmentInjectorModifierConfig await asyncio.gather(*(r.delete() for r in to_delete)) +class FastSegmentInjectorModifier(Modifier[SegmentInjectorModifierConfig]): + """ + Optimized segment injection modifier that applies multiple segment patches in a single bulk + operation. Unlike SegmentInjectorModifier which runs BinaryInjectorModifier separately for each + segment, this modifier collects all segment patches and applies them to the root resource data + in one pass, improving performance for multi-segment injections. + + **WARNING: This modifier does NOT delete stale descendant resources.** After patching, the + resource tree may contain outdated descendants that no longer match the modified data. This is a + critical difference from SegmentInjectorModifier, which automatically cleans up invalidated + descendants. The resource tree may be in an inconsistent state after using this modifier. + + This modifier skips .bss, .rela, and .got segments, and applies all valid segment patches + directly to the program resource without invoking individual injection modifiers. Use when + applying complex patches with many segments. + """ + + targets = (Program,) + + async def modify(self, resource: Resource, config: SegmentInjectorModifierConfig) -> None: + segments_to_skip = [ + ".bss", + ".rela", + ".got", + ] + sorted_regions = list( + await resource.get_descendants_as_view( + MemoryRegion, + r_filter=ResourceFilter(include_self=True, tags=(MemoryRegion,)), + r_sort=ResourceSort( + attribute=MemoryRegion.Size, + direction=ResourceSortDirection.DESCENDANT, + ), + ) + ) + + patches_to_resource: List[Tuple[int, bytes]] = list() + + for segment, segment_data in config.segments_and_data: + # Skip bss, rela, got, etc + if segment.length == 0 or segment.vm_address == 0: + continue + if any([segment.segment_name.startswith(prefix) for prefix in segments_to_skip]): + continue + + LOGGER.info( + f' Injecting segment "{segment.segment_name}": {segment.length} ' + f"bytes @ {hex(segment.vm_address)}", + ) + + # Find and validate target region + target_region = MemoryRegion.get_mem_region_with_vaddr_from_sorted( + segment.vm_address, sorted_regions + ) + if target_region is None: + raise ValueError( + f"Cannot inject patch because the memory region at vaddr " + f"{hex(segment.vm_address)} is None" + ) + if target_region.resource.get_data_id() is None: + raise ValueError( + f"Cannot inject patch because the memory region at vaddr " + f"{hex(segment.vm_address)} is dataless" + ) + + # Calculate the offset in the root resource + range_in_root = await target_region.resource.get_data_range_within_root() + offset = range_in_root.start + segment.vm_address - target_region.virtual_address + patches_to_resource.append((offset, segment_data)) + + # Apply segment patches in-memory + resource_data = bytearray(await resource.get_data()) + for offset, segment_data in patches_to_resource: + resource_data[offset : offset + len(segment_data)] = segment_data + + # Patch OFRAK resource + resource.queue_patch( + Range.from_size(0, await resource.get_data_length()), + bytes(resource_data), + ) + + @dataclass class FunctionReplacementModifierConfig(ComponentConfig): """ From dbf44bfbda34b9c7b1005915d74126be6259ac52 Mon Sep 17 00:00:00 2001 From: Andrew Afflitto Date: Tue, 10 Feb 2026 20:26:37 +0000 Subject: [PATCH 2/7] Bump changelog and version --- ofrak_core/CHANGELOG.md | 1 + ofrak_core/version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ofrak_core/CHANGELOG.md b/ofrak_core/CHANGELOG.md index 1de7ea6ca..e17f401c8 100644 --- a/ofrak_core/CHANGELOG.md +++ b/ofrak_core/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## [Unreleased](https://github.com/redballoonsecurity/ofrak/tree/master) ### Added +- Add FastSegmentInjectorModifier for optimized bulk segment injection ([#685](https://github.com/redballoonsecurity/ofrak/pull/685)) - Add Android sparse image unpacker and packer ([#662](https://github.com/redballoonsecurity/ofrak/pull/662)) - Add OFRAK requirements, requirement to test mapping, test specifications ([#656](https://github.com/redballoonsecurity/ofrak/pull/656)) - Add `-V, --version` flag to ofrak cli ([#652](https://github.com/redballoonsecurity/ofrak/pull/652)) diff --git a/ofrak_core/version.py b/ofrak_core/version.py index ed1177993..ba047d0bd 100644 --- a/ofrak_core/version.py +++ b/ofrak_core/version.py @@ -1 +1 @@ -VERSION = "3.4.0rc5" +VERSION = "3.4.0rc6" From d9d1e0423179a22be3f6fd49064954b1f0a7fb79 Mon Sep 17 00:00:00 2001 From: Andrew Afflitto Date: Thu, 12 Feb 2026 22:16:39 +0000 Subject: [PATCH 3/7] Add FastSegmentInjector test --- .../components/test_patch_maker_component.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ofrak_core/tests/components/test_patch_maker_component.py b/ofrak_core/tests/components/test_patch_maker_component.py index 0dd51720d..d9596c3ce 100644 --- a/ofrak_core/tests/components/test_patch_maker_component.py +++ b/ofrak_core/tests/components/test_patch_maker_component.py @@ -39,6 +39,7 @@ FunctionReplacementModifier, SegmentInjectorModifierConfig, SegmentInjectorModifier, + FastSegmentInjectorModifier, SourceBundle, ) from ofrak_patch_maker.toolchain.model import ( @@ -330,3 +331,30 @@ async def test_segment_injector_deletes_patched_descendants(ofrak_context: OFRAK # check that resources have been deleted assert results.resources_deleted == expected_deleted_ids + + +async def test_fast_segment_injector_applies_patches(ofrak_context: OFRAKContext): + """ + Tests that FastSegmentInjectorModifier applies segment patches correctly (REQ6.1). + + Unlike SegmentInjectorModifier, this does NOT delete patched descendants. + """ + root_resource = await ofrak_context.create_root_resource_from_file(ARM32_PROGRAM_PATH) + await root_resource.unpack_recursively() + + main_start = 0x8068 + main_end = main_start + 40 + + target_program = await root_resource.view_as(Program) + await target_program.get_code_region_for_vaddr(main_start) + + cfg = SegmentInjectorModifierConfig( + ( + ( + Segment(".text", main_start, 0, False, main_end - main_start, MemoryPermissions.RX), + b"\x00" * (main_end - main_start), + ), + ) + ) + + await root_resource.run(FastSegmentInjectorModifier, cfg) From b821c08aa65af850d74ad95521147a9c1ef75a5c Mon Sep 17 00:00:00 2001 From: Andrew Afflitto Date: Fri, 27 Feb 2026 22:52:55 +0000 Subject: [PATCH 4/7] Bump rc version --- ofrak_core/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ofrak_core/version.py b/ofrak_core/version.py index 8c152bb2c..718192b1c 100644 --- a/ofrak_core/version.py +++ b/ofrak_core/version.py @@ -1 +1 @@ -VERSION = "3.4.0rc7" +VERSION = "3.4.0rc8" From 872f5e29a70405f00ae8fef21bf4fccdd5bf59c6 Mon Sep 17 00:00:00 2001 From: Andrew Afflitto Date: Tue, 3 Mar 2026 22:26:31 +0000 Subject: [PATCH 5/7] Helper to create SegmentInjectorConfig from list of FEMs --- ofrak_core/src/ofrak/core/patch_maker/modifiers.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ofrak_core/src/ofrak/core/patch_maker/modifiers.py b/ofrak_core/src/ofrak/core/patch_maker/modifiers.py index 5cc3bfd8e..40115e68a 100644 --- a/ofrak_core/src/ofrak/core/patch_maker/modifiers.py +++ b/ofrak_core/src/ofrak/core/patch_maker/modifiers.py @@ -3,7 +3,7 @@ import os import tempfile312 as tempfile from dataclasses import dataclass -from typing import Dict, List, Optional, Tuple, Type, Union, cast +from typing import Dict, Iterable, List, Optional, Tuple, Type, Union, cast from ofrak_patch_maker.model import PatchRegionConfig, FEM from ofrak_patch_maker.patch_maker import PatchMaker @@ -212,6 +212,18 @@ def from_fem(fem: FEM) -> "SegmentInjectorModifierConfig": extracted_segments.append((segment, segment_data)) return SegmentInjectorModifierConfig(tuple(extracted_segments)) + @staticmethod + def from_fems(fems: Iterable[FEM]) -> "SegmentInjectorModifierConfig": + """ + Automatically build a config from a list of FEMs by extracting each segment's bytes and metadata. + """ + extracted_segments: List[Tuple[Segment, bytes]] = [] + for fem in fems: + extracted_segments.extend( + list(SegmentInjectorModifierConfig.from_fem(fem).segments_and_data) + ) + return SegmentInjectorModifierConfig(tuple(extracted_segments)) + class SegmentInjectorModifier(Modifier[SegmentInjectorModifierConfig]): """ From 445cb051247b3ef1540b0bc3098af067956d44b5 Mon Sep 17 00:00:00 2001 From: Andrew Afflitto Date: Thu, 2 Apr 2026 16:01:04 +0000 Subject: [PATCH 6/7] Test from_fems --- .../ofrak_ghidra/tests/test_ghidra_program_analyzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disassemblers/ofrak_ghidra/tests/test_ghidra_program_analyzer.py b/disassemblers/ofrak_ghidra/tests/test_ghidra_program_analyzer.py index 3e3397f9e..e1343f6f1 100644 --- a/disassemblers/ofrak_ghidra/tests/test_ghidra_program_analyzer.py +++ b/disassemblers/ofrak_ghidra/tests/test_ghidra_program_analyzer.py @@ -211,5 +211,5 @@ async def _make_dummy_program(resource: Resource, arch_info): await resource.run( SegmentInjectorModifier, - SegmentInjectorModifierConfig.from_fem(fem), + SegmentInjectorModifierConfig.from_fems([fem]), ) From c5201e5052ad4c35037358856b50133e2021e76c Mon Sep 17 00:00:00 2001 From: Paul Date: Mon, 4 May 2026 15:30:55 +0200 Subject: [PATCH 7/7] Add test for from_fems --- .../components/test_patch_maker_component.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/ofrak_core/tests/components/test_patch_maker_component.py b/ofrak_core/tests/components/test_patch_maker_component.py index d9596c3ce..7aa0412a7 100644 --- a/ofrak_core/tests/components/test_patch_maker_component.py +++ b/ofrak_core/tests/components/test_patch_maker_component.py @@ -42,6 +42,7 @@ FastSegmentInjectorModifier, SourceBundle, ) +from ofrak_patch_maker.model import FEM, LinkedExecutable from ofrak_patch_maker.toolchain.model import ( CompilerOptimizationLevel, BinFileType, @@ -358,3 +359,50 @@ async def test_fast_segment_injector_applies_patches(ofrak_context: OFRAKContext ) await root_resource.run(FastSegmentInjectorModifier, cfg) + + +def test_segment_injector_modifier_config_from_fems(tmp_path): + """ + Tests that SegmentInjectorModifierConfig.from_fems correctly merges segment data extracted + from multiple FEMs into a single config (REQ6.1). + """ + exe_a_path = tmp_path / "exec_a" + exe_a_data = b"AAAABBBB" + exe_a_path.write_bytes(exe_a_data) + + exe_b_path = tmp_path / "exec_b" + exe_b_data = b"CCCCDDDDEEEE" + exe_b_path.write_bytes(exe_b_data) + + segment_a = Segment(".text", 0x1000, 0, False, 4, MemoryPermissions.RX) + segment_b_text = Segment(".text", 0x2000, 0, False, 4, MemoryPermissions.RX) + segment_b_data = Segment(".data", 0x3000, 4, False, 8, MemoryPermissions.RW) + + fem_a = FEM( + name="patch_a", + executable=LinkedExecutable( + path=str(exe_a_path), + file_format=BinFileType.ELF, + segments=(segment_a,), + symbols={}, + relocatable=False, + ), + ) + fem_b = FEM( + name="patch_b", + executable=LinkedExecutable( + path=str(exe_b_path), + file_format=BinFileType.ELF, + segments=(segment_b_text, segment_b_data), + symbols={}, + relocatable=False, + ), + ) + + cfg = SegmentInjectorModifierConfig.from_fems([fem_a, fem_b]) + + assert cfg.segments_and_data == ( + (segment_a, b"AAAA"), + (segment_b_text, b"CCCC"), + (segment_b_data, b"DDDDEEEE"), + )