mirror of
https://github.com/angr/angr
synced 2026-08-17 12:23:11 -04:00
CFGFast: Use a Mach-O function-start table for what it does not otherwise find
A linker's function-start table is the only record of where a stripped image's functions begin, and CFGFast had no use for one. Fed in unfiltered it is worse than nothing: ld64 records the address of every atom it placed in an executable section, so a Haskell closure's info table and a Swift offset table are entries beside the functions, and seeding those puts function heads on data. Consume FunctionHintSource.FUNCTION_STARTS hints the way the .eh_frame ones are consumed -- after the worklist drains, skipping an address already decoded -- with one added test: the bytes at the address have to be a range something enters as code. The decode runs over the distance to the next recorded address, which is not a size and is never used as one, and stops where a real decoder would: at a byte capstone refuses, at a word it names undefined, and at an instruction encoded entirely in zero bytes, which is what a table is padded and filled with. The range is code if the decode reaches an instruction after which control does not fall through, or consumes the extent without one of those stops. Neither half of that is sufficient alone -- an info table decodes as x86 arithmetic to the end of a range it fits exactly, and an AArch64 function ending in a call that does not return reaches no terminator. An architecture whose terminators are not established here gets no hints at all, rather than a decision made with another architecture's set.
This commit is contained in:
parent
503b1be066
commit
e272212d6f
3 changed files with 142 additions and 1 deletions
|
|
@ -232,6 +232,7 @@ class CFGBase(Analysis):
|
|||
self._function_addresses_from_symbols = self._load_func_addrs_from_symbols()
|
||||
self._function_addresses_from_eh_frame = self._load_func_addrs_from_eh_frame()
|
||||
self._function_addr_and_names_from_hints = self._load_func_addr_and_names_from_hints()
|
||||
self._function_addresses_from_function_starts = self._load_func_addrs_from_function_starts()
|
||||
|
||||
# Cache if an object has executable sections or not
|
||||
self._object_to_executable_sections = {}
|
||||
|
|
@ -1096,10 +1097,27 @@ class CFGBase(Analysis):
|
|||
|
||||
addrs_and_names = set()
|
||||
for function_hint in self._binary.function_hints:
|
||||
if function_hint.source != FunctionHintSource.EH_FRAME:
|
||||
if function_hint.source not in (FunctionHintSource.EH_FRAME, FunctionHintSource.FUNCTION_STARTS):
|
||||
addrs_and_names.add((function_hint.addr, function_hint.name))
|
||||
return addrs_and_names
|
||||
|
||||
def _load_func_addrs_from_function_starts(self) -> set[int]:
|
||||
"""
|
||||
Get the addresses that a linker's function-start table records.
|
||||
|
||||
Mach-O's LC_FUNCTION_STARTS names every atom ld64 placed in an executable section. That
|
||||
includes the data atoms a producer emits among its functions - a Haskell closure's info
|
||||
table, a Swift offset table - so these addresses are candidates and not function starts.
|
||||
|
||||
:return: A set of addresses that may be functions.
|
||||
"""
|
||||
|
||||
return {
|
||||
function_hint.addr
|
||||
for function_hint in self._binary.function_hints
|
||||
if function_hint.source == FunctionHintSource.FUNCTION_STARTS
|
||||
}
|
||||
|
||||
#
|
||||
# Analyze function features
|
||||
#
|
||||
|
|
|
|||
|
|
@ -85,6 +85,22 @@ VEX_IRSB_MAX_SIZE = 400
|
|||
# the minimum interval (in seconds) between two consecutive progress notifications
|
||||
PROGRESS_NOTIFY_INTERVAL = 0.05
|
||||
|
||||
# Instructions after which control does not fall through, used to tell the code a linker's
|
||||
# function-start table records from the data atoms it records beside it. Keyed by architecture: one
|
||||
# whose terminators are not listed here gets no function-start hints, rather than a decision made
|
||||
# with another architecture's set. Privileged and far-transfer instructions are deliberately absent,
|
||||
# because no compiler emits them into a user-space function, so finding one is evidence that the
|
||||
# bytes are a table and not evidence of the end of a function.
|
||||
FALLTHROUGH_TERMINATORS = {
|
||||
"X86": frozenset({"ret", "jmp", "ud2"}),
|
||||
"AMD64": frozenset({"ret", "jmp", "ud2"}),
|
||||
"AARCH64": frozenset({"ret", "retaa", "retab", "b", "br", "braa", "brab", "braaz", "brabz", "brk"}),
|
||||
}
|
||||
|
||||
# What capstone emits where it has no instruction for the bytes. On a variable-width instruction set
|
||||
# it stops; on a fixed-width one it does not, and every undecodable AArch64 word comes back as udf.
|
||||
UNDEFINED_MNEMONICS = frozenset({"udf", "invalid", "undefined", ".byte"})
|
||||
|
||||
|
||||
l = logging.getLogger(name=__name__)
|
||||
|
||||
|
|
@ -464,6 +480,7 @@ class CFGJobType(Enum):
|
|||
IFUNC_HINTS = 3
|
||||
DATAREF_HINTS = 4
|
||||
EH_FRAME_HINTS = 5
|
||||
FUNCTION_START_HINTS = 6
|
||||
|
||||
|
||||
class CFGJob:
|
||||
|
|
@ -884,6 +901,8 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
self._write_addr_to_run = defaultdict(list)
|
||||
|
||||
self._remaining_eh_frame_addrs: list[int] | None = None
|
||||
self._remaining_function_start_addrs: list[int] | None = None
|
||||
self._function_start_extents: dict[int, int] = {}
|
||||
self._remaining_function_prologue_addrs: list[int] | None = None
|
||||
self._used_function_prologue_addrs: set[int] | None = None
|
||||
self._ptr_hints: SortedDict | None = None
|
||||
|
|
@ -1716,6 +1735,9 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
if self._use_eh_frame:
|
||||
self._remaining_eh_frame_addrs = sorted(self._function_addresses_from_eh_frame, reverse=True)
|
||||
|
||||
self._function_start_extents = self._measure_function_start_extents()
|
||||
self._remaining_function_start_addrs = sorted(self._function_start_extents, reverse=True)
|
||||
|
||||
if self._use_function_prologues and self.project.concrete_target is None:
|
||||
func_addrs_from_prologs = self._func_addrs_from_prologues()
|
||||
if self._ptr_hints:
|
||||
|
|
@ -2338,6 +2360,19 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
self._register_analysis_job(eh_addr, job)
|
||||
return
|
||||
|
||||
if self._remaining_function_start_addrs:
|
||||
while self._remaining_function_start_addrs:
|
||||
start_addr = self._remaining_function_start_addrs.pop()
|
||||
if self._seg_list.is_occupied(start_addr):
|
||||
continue
|
||||
if not self._function_start_holds_code(start_addr):
|
||||
continue
|
||||
|
||||
job = CFGJob(start_addr, start_addr, "Ijk_Boring", job_type=CFGJobType.FUNCTION_START_HINTS)
|
||||
self._insert_job(job)
|
||||
self._register_analysis_job(start_addr, job)
|
||||
return
|
||||
|
||||
if self._use_function_prologues and self._remaining_function_prologue_addrs:
|
||||
while self._remaining_function_prologue_addrs:
|
||||
prolog_addr = self._remaining_function_prologue_addrs.pop()
|
||||
|
|
@ -2875,6 +2910,71 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase):
|
|||
|
||||
# Methods to get start points for scanning
|
||||
|
||||
def _measure_function_start_extents(self) -> dict[int, int]:
|
||||
"""
|
||||
How far each address a linker's function-start table records reaches before the next one.
|
||||
|
||||
The distance to the next recorded address is not a function size: the table records atoms,
|
||||
and one function can hold several. It is how many bytes there are to read when asking whether
|
||||
anything ever entered the address as code, where reading too far costs an answer and can
|
||||
never place a boundary. Addresses outside an executable section are dropped, along with every
|
||||
address on an architecture whose terminators are not established.
|
||||
|
||||
:return: A map from address to the number of bytes to decode there.
|
||||
"""
|
||||
|
||||
if not self._function_addresses_from_function_starts:
|
||||
return {}
|
||||
if self.project.arch.name not in FALLTHROUGH_TERMINATORS or not self.project.arch.capstone_support:
|
||||
return {}
|
||||
|
||||
ordered = sorted(self._function_addresses_from_function_starts)
|
||||
extents = {}
|
||||
for index, addr in enumerate(ordered):
|
||||
if not self._inside_regions(addr):
|
||||
continue
|
||||
section = self.project.loader.find_section_containing(addr)
|
||||
if section is None or not section.is_executable or section.only_contains_uninitialized_data:
|
||||
continue
|
||||
end = section.vaddr + section.memsize
|
||||
if index + 1 < len(ordered) and ordered[index + 1] < end:
|
||||
end = ordered[index + 1]
|
||||
if end > addr:
|
||||
extents[addr] = end - addr
|
||||
return extents
|
||||
|
||||
def _function_start_holds_code(self, addr: int) -> bool:
|
||||
"""
|
||||
Whether the bytes a linker recorded a function start at are a range something enters as code.
|
||||
|
||||
The decode runs forward from the address and stops where a real decoder would: at a byte
|
||||
capstone refuses, at a word it names undefined, and at an instruction encoded entirely in
|
||||
zero bytes, which is what a table is padded and filled with. The range is code if the decode
|
||||
reaches an instruction after which control does not fall through, or consumes the whole
|
||||
extent without one of those stops. Neither half is sufficient alone: a Haskell info table
|
||||
decodes as x86 arithmetic to the end of a range it fits exactly, and an AArch64 function that
|
||||
ends in a call which does not return reaches no terminator.
|
||||
"""
|
||||
|
||||
extent = self._function_start_extents.get(addr)
|
||||
if not extent:
|
||||
return False
|
||||
try:
|
||||
data = self.project.loader.memory.load(addr, extent)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
terminators = FALLTHROUGH_TERMINATORS[self.project.arch.name]
|
||||
consumed = 0
|
||||
for insn_addr, size, mnemonic, _operands in self.project.arch.capstone.disasm_lite(data, addr):
|
||||
offset = insn_addr - addr
|
||||
if size <= 0 or mnemonic in UNDEFINED_MNEMONICS or not any(data[offset : offset + size]):
|
||||
return False
|
||||
if mnemonic in terminators:
|
||||
return True
|
||||
consumed = offset + size
|
||||
return consumed == len(data)
|
||||
|
||||
def _func_addrs_from_prologues(self):
|
||||
"""
|
||||
Scan the entire program image for function prologues, and start code scanning at those positions
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import random
|
|||
import unittest
|
||||
|
||||
import archinfo
|
||||
from cle.backends.backend import FunctionHintSource
|
||||
|
||||
import angr
|
||||
from angr.analyses.cfg.indirect_jump_resolvers import mips_elf_fast
|
||||
|
|
@ -968,6 +969,28 @@ class TestCfgfast(unittest.TestCase):
|
|||
assert "_accepted" in func_names
|
||||
assert "_authenticate" in func_names
|
||||
|
||||
def test_macho_function_starts_seed_functions(self):
|
||||
# LC_FUNCTION_STARTS records the address of every atom ld64 placed in an executable section.
|
||||
# With every other source of starting points turned off, the table is all CFGFast has.
|
||||
path = os.path.join(test_location, "aarch64", "dyld_ios15.macho")
|
||||
proj = angr.Project(path, auto_load_libs=False)
|
||||
recorded = {
|
||||
hint.addr
|
||||
for hint in proj.loader.main_object.function_hints
|
||||
if hint.source == FunctionHintSource.FUNCTION_STARTS
|
||||
}
|
||||
assert len(recorded) == 36
|
||||
|
||||
cfg = proj.analyses.CFGFast(
|
||||
symbols=False,
|
||||
start_at_entry=False,
|
||||
function_prologues=False,
|
||||
force_smart_scan=False,
|
||||
force_complete_scan=False,
|
||||
data_references=False,
|
||||
)
|
||||
assert recorded <= set(cfg.kb.functions)
|
||||
|
||||
def test_syscalls_resolved_with_constant_propagation(self):
|
||||
for arch in ["x86", "x86_64"]:
|
||||
with self.subTest(arch=arch):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue