mirror of
https://github.com/angr/angr
synced 2026-08-17 12:23:11 -04:00
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:
parent
503b1be066
commit
99955ac0e3
2 changed files with 88 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
74
tests/analyses/cfg/test_cfg_coredump.py
Normal file
74
tests/analyses/cfg/test_cfg_coredump.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue