CFG: Read a blob's executable map from its segments when it has one

CFGBase treats every Blob as entirely executable. That is right for a raw
firmware image, which carries no permission information at all, and wrong for a
blob cut out of something that does. A core dump is the case that matters:
ELFCore turns each mapping it cannot match to a child object into a Blob, so
the process heap, its stack and its guard pages all become code to scan.

Consult the blob's segments when it says its permissions are known, and fall
back to the old assumption when they are not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Yan 2026-08-17 06:18:12 +00:00
parent 503b1be066
commit 99955ac0e3
2 changed files with 88 additions and 3 deletions

View file

@ -866,9 +866,20 @@ class CFGBase(Analysis):
memory_regions.append((region_addr, region_addr + region_size))
elif isinstance(b, Blob):
# a blob is entirely executable
tpl = (b.min_addr, b.max_addr + 1)
memory_regions.append(tpl)
if all(segment.is_executable for segment in b.segments):
# a raw image carries no permissions and every segment answers the permissive default,
# so the blob is entirely executable
tpl = (b.min_addr, b.max_addr + 1)
memory_regions.append(tpl)
else:
# the blob was cut out of something that recorded its permissions, such as a core dump
for segment in b.segments:
if not segment.is_executable:
continue
# a segment can be longer than the bytes the blob actually holds
max_mapped_addr = min(segment.min_addr + min(segment.memsize, segment.filesize), b.max_addr + 1)
if max_mapped_addr > segment.min_addr:
memory_regions.append((segment.min_addr, max_mapped_addr))
elif isinstance(b, NamedRegion):
# NamedRegions have no content! Ignore

View file

@ -0,0 +1,74 @@
#!/usr/bin/env python3
# pylint: disable=missing-class-docstring,no-self-use,protected-access
from __future__ import annotations
__package__ = __package__ or "tests.analyses.cfg" # pylint:disable=redefined-builtin
import os
import unittest
import cle
import angr
from tests.common import bin_location
test_location = os.path.join(bin_location, "tests")
def load_coredump():
"""The core records mappings for binaries that are not at those paths here, so point it at ours."""
directory = os.path.join(test_location, "x86_64")
core = os.path.join(directory, "coredump", "true-libc.so.6-ld-linux-x86-64.so.2.core")
return angr.Project(
core,
main_opts={
"backend": "elfcore",
"remote_file_mapper": lambda path: path.replace("/tmp/foobar/does-not-exist", directory),
},
auto_load_libs=True,
)
class TestCfgCoredump(unittest.TestCase):
def test_core_mappings_are_not_all_code(self):
# A core dump records what every mapping was allowed to do. The mappings CLE cannot match to a child
# object become blobs, and before those carried their permissions the whole dump - heap, stack and
# guard pages included - reached CFGFast as executable memory.
project = load_coredump()
core = project.loader.elfcore_object
assert core is not None
executable = [
(segment.vaddr, segment.vaddr + segment.memsize) for segment in core.segments if segment.is_executable
]
assert executable
blobs = [obj for obj in project.loader.all_objects if isinstance(obj, cle.Blob)]
assert blobs, "the core's leftover mappings should have become blobs"
# the last blob that covers a mapping the process could not execute
target = None
for blob in blobs:
mapping = core.segments.find_region_containing(blob.min_addr)
if mapping is not None and not mapping.is_executable:
target = blob
assert target is not None
# Scanning it from end to end recovers nothing, because the process could not have executed it.
cfg = project.analyses.CFGFast(
regions=[(target.min_addr, target.max_addr + 1)],
force_complete_scan=True,
normalize=True,
)
recovered = [node for node in cfg.model.nodes() if node.size]
assert not recovered, f"{len(recovered)} blocks decoded out of a mapping the core recorded as data"
# The map CFGFast derives for itself covers the executable mappings and nothing else.
for start, end in cfg._exec_mem_regions:
assert any(low <= start and end <= high for low, high in executable), (
f"{start:#x}-{end:#x} is not inside any executable mapping of the core"
)
if __name__ == "__main__":
unittest.main()