Skip to content

Commit 71348a6

Browse files
authored
Improved allocation of .bss(-like) sections (#505)
* Improved allocation of .bss(-like) sections The existing strategy for allocating `.bss` in RAM has a few problems - only one contiguous `.bss` region per FEM is supported. - alignment was not properly calculated for `.bss`, leading to linker errors This commit generalizes our process for allocating `.text`, `.data`, `.rodata`, etc. to `.bss` and like as well. It also improves our support for different platforms by reducing the use of hardcoded section names for determining behavior in favor of attributes defined by the binary format like `sh_type` and `sh_flags` With the new system, instead of passing `unsafe_bss_segment` to `make_fem`, `.bss` sections are allocated during the `allocate_bom` step. This is supported by free space regions that don't map to any data on the parent resource. Changes (incomplete): - Use SHT_NOBITS type to detect .bss(-like) sections on ELF toolchains - Create `FreeSpaceWithoutData`` tag for allocating regions for .bss. These are created from `MemoryRegion` children of resources with `data_range=None` - Use SHF_ALLOC flag to determine whether section needs to be patched or not instead of hard coded list of names. - Pass flag to compiler to prevent `.eh_frame` generation - Backwards compatible with old bss allocation for now, emits deprecation warnings. * Rename FreeSpaceWithoutData to RuntimeFreeSpace and FreeSpaceAnyData to AnyFreeSpace * Rename NOBITS_LEGACY_VADDR to BSS_LEGACY_VADDR * Allocate non-bss segments before bss segments in case legacy bss allocation used. * Prevent allocate_bom from allocating sections that will be discarded * Test legacy .bss allocation in ofrak_patch_maker_test * Remove workaround for remove_tag bug (fixed in #513) * Fix test_symbol_resolution for new return value of _resolve_symbols_within_BOM * Update notebook to reflect new Segment and AssembledObject definition * Add tests for dataless bss allocation to test_allocatable_allocate.py * Add test cases for RuntimeFreeSpace in the Analyzer * Add test for RuntimeFreeSpace in test_allocation_modifier * Test FreeSpaceModifier for RuntimeFreeSpace creation * Test .bss allocation both the new and old way in patch_maker * Changelog entries
1 parent 8af6145 commit 71348a6

File tree

23 files changed

+573
-209
lines changed

23 files changed

+573
-209
lines changed

ofrak_core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1717
- Add generic DecompilationAnalysis classes. ([#453](https://github.com/redballoonsecurity/ofrak/pull/453))
1818
- `PatchFromSourceModifier` bundles src and header files into same temporary directory with BOM and FEM ([#517](https://github.com/redballoonsecurity/ofrak/pull/517))
1919
- Add support for running on Windows to the `Filesystem` component. ([#521](https://github.com/redballoonsecurity/ofrak/pull/521))
20+
- Add new method for allocating `.bss` sections using free space ranges that aren't mapped to data ranges. ([#505](https://github.com/redballoonsecurity/ofrak/pull/505))
2021

2122
### Fixed
2223
- Improved flushing of filesystem entries (including symbolic links and other types) to disk. ([#373](https://github.com/redballoonsecurity/ofrak/pull/373))

ofrak_core/ofrak/core/free_space.py

Lines changed: 194 additions & 83 deletions
Large diffs are not rendered by default.

ofrak_core/ofrak/core/patch_maker/modifiers.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ofrak.resource import Resource
2222
from ofrak.service.resource_service_i import ResourceFilter, ResourceSort, ResourceSortDirection
2323
from ofrak_type.memory_permissions import MemoryPermissions
24+
from ofrak_type.error import NotFoundError
2425

2526
LOGGER = logging.getLogger(__file__)
2627

@@ -189,7 +190,15 @@ def from_fem(fem: FEM) -> "SegmentInjectorModifierConfig":
189190
for segment in fem.executable.segments:
190191
if segment.length == 0:
191192
continue
192-
segment_data = exe_data[segment.offset : segment.offset + segment.length]
193+
194+
if segment.is_bss:
195+
# It's possible for NOBITS sections like .bss to be allocated into RW MemoryRegions
196+
# that are mapped to data. In that case, we should zero out the region instead
197+
# of patching the arbitrary data in the FEM
198+
segment_data = b"\0" * segment.length
199+
else:
200+
segment_data = exe_data[segment.offset : segment.offset + segment.length]
201+
193202
extracted_segments.append((segment, segment_data))
194203
return SegmentInjectorModifierConfig(tuple(extracted_segments))
195204

@@ -220,15 +229,14 @@ async def modify(self, resource: Resource, config: SegmentInjectorModifierConfig
220229
injection_tasks: List[Tuple[Resource, BinaryInjectorModifierConfig]] = []
221230

222231
for segment, segment_data in config.segments_and_data:
223-
if segment.length == 0 or segment.vm_address == 0:
232+
if segment.length == 0 or not segment.is_allocated:
224233
continue
225234
if segment.length > 0:
226235
LOGGER.debug(
227236
f" Segment {segment.segment_name} - {segment.length} "
228237
f"bytes @ {hex(segment.vm_address)}",
229238
)
230-
if segment.segment_name.startswith(".bss"):
231-
continue
239+
232240
if segment.segment_name.startswith(".rela"):
233241
continue
234242
if segment.segment_name.startswith(".got"):
@@ -238,18 +246,31 @@ async def modify(self, resource: Resource, config: SegmentInjectorModifierConfig
238246
# See PatchFromSourceModifier
239247
continue
240248

241-
patches = [(segment.vm_address, segment_data)]
242-
region = MemoryRegion.get_mem_region_with_vaddr_from_sorted(
243-
segment.vm_address, sorted_regions
244-
)
245-
if region is None:
249+
try:
250+
region = MemoryRegion.get_mem_region_with_vaddr_from_sorted(
251+
segment.vm_address, sorted_regions
252+
)
253+
except NotFoundError:
254+
# uninitialized section like .bss mapped to arbitrary memory range without corresponding
255+
# MemoryRegion resource, no patch needed.
256+
if segment.is_bss:
257+
continue
258+
raise
259+
260+
region_mapped_to_data = region.resource.get_data_id() is not None
261+
if region_mapped_to_data:
262+
patches = [(segment.vm_address, segment_data)]
263+
injection_tasks.append((region.resource, BinaryInjectorModifierConfig(patches)))
264+
else:
265+
if segment.is_bss:
266+
# uninitialized section like .bss mapped to arbitrary memory range without corresponding
267+
# data on a resource, no patch needed.
268+
continue
246269
raise ValueError(
247270
f"Cannot inject patch because the memory region at vaddr "
248-
f"{hex(segment.vm_address)} is None"
271+
f"{hex(segment.vm_address)} is not mapped to data"
249272
)
250273

251-
injection_tasks.append((region.resource, BinaryInjectorModifierConfig(patches)))
252-
253274
for injected_resource, injection_config in injection_tasks:
254275
result = await injected_resource.run(BinaryInjectorModifier, injection_config)
255276
# The above can patch data of any of injected_resources' descendants or ancestors

ofrak_core/test_ofrak/components/free_space_components_test/mock_tree_struct.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from ofrak import OFRAKContext
44
from ofrak.core.memory_region import MemoryRegion
55
from ofrak.resource import Resource
6-
from ofrak.core.free_space import Allocatable
6+
from ofrak.core.free_space import Allocatable, RuntimeFreeSpace
77

88
FreeSpaceTreeType = Tuple[MemoryRegion, Optional[List["FreeSpaceTreeType"]]]
99

@@ -28,7 +28,10 @@ async def inflate_tree(tree: FreeSpaceTreeType, ofrak_context: OFRAKContext) ->
2828

2929
async def _inflate_node(parent: MemoryRegion, node: FreeSpaceTreeType):
3030
raw_node_region, children = node
31-
node_r = await parent.create_child_region(raw_node_region)
31+
if isinstance(raw_node_region, RuntimeFreeSpace):
32+
node_r = await parent.resource.create_child_from_view(raw_node_region, data_range=None)
33+
else:
34+
node_r = await parent.create_child_region(raw_node_region)
3235
node_r.add_view(raw_node_region)
3336
await node_r.save()
3437
if children:

ofrak_core/test_ofrak/components/free_space_components_test/test_allocatable_allocate.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
)
2121
from ofrak_patch_maker.patch_maker import PatchMaker
2222
from ofrak_patch_maker.toolchain.model import (
23+
Segment,
2324
ToolchainConfig,
2425
BinFileType,
2526
CompilerOptimizationLevel,
@@ -58,15 +59,24 @@ def ofrak(ofrak):
5859
@pytest.fixture
5960
def mock_allocatable():
6061
return Allocatable(
61-
{
62+
free_space_ranges={
6263
MemoryPermissions.RX: [
6364
Range(0x100, 0x110),
6465
Range(0x80, 0xA0),
6566
Range(0xC0, 0xE0),
6667
Range(0x0, 0x40),
6768
Range(0x120, 0x200),
69+
],
70+
MemoryPermissions.RW: [
71+
Range(0x400, 0x410),
72+
],
73+
},
74+
dataless_free_space_ranges={
75+
MemoryPermissions.RW: [
76+
Range(0xD000, 0xD020),
77+
Range(0xD130, 0xD200),
6878
]
69-
}
79+
},
7080
)
7181

7282

@@ -79,6 +89,7 @@ class AllocateTestCase:
7989
alignment: Optional[int] = 4
8090
within_range: Optional[Range] = None
8191
mem_permissions: MemoryPermissions = MemoryPermissions.RX
92+
with_data: bool = True
8293

8394

8495
ALLOCATE_TEST_CASES = [
@@ -147,7 +158,28 @@ class AllocateTestCase:
147158
"allocate with memory permissions not present",
148159
0x100,
149160
None,
150-
mem_permissions=MemoryPermissions.W,
161+
mem_permissions=MemoryPermissions.RWX,
162+
),
163+
AllocateTestCase(
164+
"successful non-fragmented with_data=False allocation prefers dataless range",
165+
0x20,
166+
[
167+
Range(0xD000, 0xD020),
168+
],
169+
mem_permissions=MemoryPermissions.RW,
170+
with_data=False,
171+
),
172+
AllocateTestCase(
173+
"successful fragmented with_data=False allocation falls back to data mapped range",
174+
0x100,
175+
[
176+
Range(0x400, 0x410),
177+
Range(0xD000, 0xD020),
178+
Range(0xD130, 0xD200),
179+
],
180+
mem_permissions=MemoryPermissions.RW,
181+
min_fragment_size=0x10,
182+
with_data=False,
151183
),
152184
]
153185

@@ -166,6 +198,7 @@ async def test_allocate(ofrak_context: OFRAKContext, test_case: AllocateTestCase
166198
test_case.alignment,
167199
test_case.min_fragment_size,
168200
test_case.within_range,
201+
test_case.with_data,
169202
)
170203
assert all([r in test_case.expected_allocation for r in alloc])
171204
else:
@@ -176,6 +209,7 @@ async def test_allocate(ofrak_context: OFRAKContext, test_case: AllocateTestCase
176209
test_case.alignment,
177210
test_case.min_fragment_size,
178211
test_case.within_range,
212+
test_case.with_data,
179213
)
180214

181215

@@ -185,7 +219,8 @@ async def test_allocate_bom(ofrak_context: OFRAKContext, tmpdir):
185219
f.write(
186220
inspect.cleandoc(
187221
"""
188-
static int global_arr[256] = {0};
222+
static int global_arr[64] __attribute__((section(".bss.new"))) = {0};
223+
static int global_arr_legacy[256] __attribute__((section(".bss.legacy"))) = {0};
189224
190225
int main_supplement(int a, int b)
191226
{
@@ -208,7 +243,7 @@ async def test_allocate_bom(ofrak_context: OFRAKContext, tmpdir):
208243
int c = -38;
209244
int d = main_supplement(a, b) * c;
210245
(void) d;
211-
return foo(global_arr);
246+
return foo(global_arr_legacy);
212247
}
213248
214249
"""
@@ -230,7 +265,7 @@ async def test_allocate_bom(ofrak_context: OFRAKContext, tmpdir):
230265
no_jump_tables=True,
231266
no_bss_section=False,
232267
create_map_files=True,
233-
compiler_optimization_level=CompilerOptimizationLevel.FULL,
268+
compiler_optimization_level=CompilerOptimizationLevel.NONE,
234269
debug_info=True,
235270
)
236271

@@ -253,25 +288,29 @@ async def test_allocate_bom(ofrak_context: OFRAKContext, tmpdir):
253288
resource = await ofrak_context.create_root_resource("test_allocate_bom", b"\x00")
254289
resource.add_view(
255290
Allocatable(
256-
{
291+
free_space_ranges={
257292
MemoryPermissions.RX: [
258293
Range(0x100, 0x110),
259294
Range(0x80, 0xA0),
260295
Range(0xC0, 0xE0),
261296
Range(0x0, 0x40),
262297
Range(0x120, 0x200),
263298
]
264-
}
299+
},
300+
dataless_free_space_ranges={MemoryPermissions.RW: [Range(0xD000, 0xD100)]},
265301
)
266302
)
267303
await resource.save()
268304

269305
allocatable = await resource.view_as(Allocatable)
270306

271-
patch_config = await allocatable.allocate_bom(bom)
307+
with pytest.warns(DeprecationWarning):
308+
patch_config = await allocatable.allocate_bom(bom)
272309

273310
assert len(patch_config.segments) == 1
274-
for segments in patch_config.segments.values():
275-
seg = segments[0]
276-
assert seg.segment_name == ".text"
277-
assert seg.vm_address == 0x120
311+
(segments,) = patch_config.segments.values()
312+
segments_by_name = {seg.segment_name: seg for seg in segments}
313+
314+
assert segments_by_name[".text"].vm_address == 0x120
315+
assert segments_by_name[".bss.new"].vm_address == 0xD000
316+
assert segments_by_name[".bss.legacy"].vm_address == Segment.BSS_LEGACY_VADDR

0 commit comments

Comments
 (0)