CFG: Scan only the Mach-O sections that hold instructions

_executable_memory_regions took every section of an executable Mach-O segment
without asking anything about the section, unlike the ELF, PE/COFF and XBE
branches beside it. The whole of __TEXT is r-x, so the regions to analyze
covered the constant pools, string literals, unwind tables and Objective-C and
Swift metadata sitting beside the code: 73,172 bytes over eleven regions on
tests/aarch64/ReverseOneSignal.app/Frameworks/OneSignalCore.framework/OneSignalCore
against the 55,984 bytes in four that the file states hold instructions, and
2,195,844 against 1,699,668 on a larger dylib.

Requires the cle change that makes MachOSection.is_executable answer from the
section's own S_ATTR_*INSTRUCTIONS bits; against a cle without it this filter
is a no-op, because every section of an r-x segment still reports as executable.

Over every Mach-O in binaries the regions shrink to exactly the sections that
state instructions, and the blocks that go with them are 33 that began in
__unwind_info, __cstring, __objc_methname and __objc_classname, plus one on
armhf/FileProtection-05.armv7.macho that began in the last word of __stub_helper
and ran 194 bytes into __objc_methname. Eight fixtures gain a four-byte block at
the last word of __objc_stubs, a brk #1 the merged region caused the scan to
step over. No block that begins inside a section stating instructions is lost.
This commit is contained in:
Yan 2026-08-17 05:14:15 +00:00
parent 503b1be066
commit cd8e13da13
2 changed files with 64 additions and 4 deletions

View file

@ -854,11 +854,13 @@ class CFGBase(Analysis):
# Get all executable segments
for seg in b.segments:
if seg.is_executable:
# Take all sections from this segment (MachO style)
# The whole of __TEXT is r-x, so this segment covers its constant pools and string
# literals as well as its code.
for section in seg.sections:
max_mapped_addr = section.min_addr + min(section.memsize, section.filesize)
tpl = (section.min_addr, max_mapped_addr)
memory_regions.append(tpl)
if section.is_executable:
max_mapped_addr = section.min_addr + min(section.memsize, section.filesize)
tpl = (section.min_addr, max_mapped_addr)
memory_regions.append(tpl)
elif isinstance(b, (Hex, SRec)):
if b.regions:

View file

@ -0,0 +1,58 @@
#!/usr/bin/env python
# pylint: disable=missing-class-docstring,no-self-use
from __future__ import annotations
__package__ = __package__ or "tests.analyses.cfg" # pylint:disable=redefined-builtin
import os
import unittest
from archinfo.arch_arm import get_real_address_if_arm
import angr
from tests.common import bin_location
test_location = os.path.join(bin_location, "tests")
def sections_holding_blocks(project, cfg):
"""The names of the sections the blocks CFGFast produced begin in."""
names = set()
for node in cfg.model.nodes():
if node.is_simprocedure or not node.size:
continue
addr = get_real_address_if_arm(project.arch, node.addr)
section = project.loader.main_object.find_section_containing(addr)
if section is not None:
names.add(section.name)
return names
class TestCfgMachOSections(unittest.TestCase):
def test_cstring_and_unwind_info_are_not_scanned(self):
bin_path = os.path.join(test_location, "x86_64", "fauxware.macho")
project = angr.Project(bin_path, auto_load_libs=False)
cfg = project.analyses.CFGFast()
text = project.loader.main_object.sections_map["__TEXT,__text"]
assert (text.vaddr, text.vaddr + text.memsize) in cfg.regions
for name in ("__TEXT,__cstring", "__TEXT,__unwind_info"):
section = project.loader.main_object.sections_map[name]
assert (section.vaddr, section.vaddr + section.memsize) not in cfg.regions
assert cfg.kb.functions.get_by_addr(project.entry) is not None
assert sections_holding_blocks(project, cfg) == {"__text", "__stubs", "__stub_helper"}
def test_objc_string_sections_are_not_scanned(self):
# This image places the Objective-C name tables directly after __stub_helper, so a decode that runs off
# the end of the stubs marches through them.
bin_path = os.path.join(test_location, "armhf", "FileProtection-05.armv7.macho")
project = angr.Project(bin_path, auto_load_libs=False)
cfg = project.analyses.CFGFast()
assert len(cfg.kb.functions) > 200
assert sections_holding_blocks(project, cfg) == {"__text", "__stub_helper", "__symbolstub1"}
if __name__ == "__main__":
unittest.main()