diff --git a/angr/analyses/cfg/cfg_fast.py b/angr/analyses/cfg/cfg_fast.py index 53a2caf35..27ebe31ae 100644 --- a/angr/analyses/cfg/cfg_fast.py +++ b/angr/analyses/cfg/cfg_fast.py @@ -4697,6 +4697,34 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase): # if node.addr in self.kb.functions.callgraph: # self.kb.functions.callgraph.remove_node(node.addr) + def _reached_only_by_call_fallthrough(self, cfg_node: CFGNode) -> bool: + """ + Is this block reachable only as the fall-through of a call to a function we recovered? + + Whatever a compiler leaves after a call it treats as non-returning is not executed: MSVC pads with + ``int3``, GCC emits the TOC restore after every PowerPC64 ``bl``, and both are followed by the + alignment of the next function. CFGFast recovers that block anyway, because the callee's returning + status is usually still unknown when the scan reaches the call site. What is beyond such a block says + nothing about whether the function it hangs off was decoded out of data. + + The callee has to be a function with blocks of its own. A stray ``int`` or ``syscall`` in decoded data + gets a fall-through edge too, and so does a call whose target is not in the image; a block behind one of + those is the tail of a decode rather than the padding after a call. + """ + graph = self.model.graph + in_edges = list(graph.in_edges(cfg_node, data=True)) + if not in_edges: + return False + for src, _, data in in_edges: + if data.get("jumpkind") != "Ijk_FakeRet": + return False + if not any( + edge_data.get("jumpkind") == "Ijk_Call" and self.kb.functions.get_func_block_count(dst.addr) + for _, dst, edge_data in graph.out_edges(src, data=True) + ): + return False + return True + def drop_bad_functions(self): # remove all functions that are bad, i.e., likely the result of decoding data as code # - if a function jumps to data, then it's likely bad @@ -4735,6 +4763,8 @@ class CFGFast(ForwardAnalysis[CFGNode, CFGNode, CFGJob, int, object], CFGBase): cfg_node = self.model.get_any_node(block_addr) if cfg_node is not None and cfg_node.size > 0: out_degree = self.model.graph.out_degree[cfg_node] + if out_degree < 2 and self._reached_only_by_call_fallthrough(cfg_node): + continue # is it jumping to data? if out_degree == 0: # might be size-limited during CFGNode generation; check the location after the end of the block diff --git a/tests/analyses/cfg/test_cfgfast.py b/tests/analyses/cfg/test_cfgfast.py index 6e13b78bf..03da1a5e2 100755 --- a/tests/analyses/cfg/test_cfgfast.py +++ b/tests/analyses/cfg/test_cfgfast.py @@ -1031,6 +1031,31 @@ class TestCfgfast(unittest.TestCase): assert len(cfg.kb.functions) < 150, f"32 KB of random data produced {len(cfg.kb.functions)} functions" + def test_msvc_function_ending_in_a_noreturning_call_is_kept(self): + # each of these five ends in a call MSVC treats as non-returning, and the block CFGFast recovers past + # that call is the single int3 MSVC leaves there. drop_bad_functions() used to read the run of int3 + # padding that follows as the function running into data and delete the whole function; the image's own + # exception directory names all five. + proj = angr.Project(os.path.join(test_location, "x86_64", "windows", "ipnathlp.dll"), auto_load_libs=False) + cfg = proj.analyses.CFGFast(normalize=True) + + for addr in (0x180004D20, 0x18001A3EC, 0x18001FDFC, 0x180024064, 0x180024080): + assert addr in cfg.kb.functions, f"{addr:#x} was dropped" + assert cfg.model.get_any_node(addr) is not None, f"no block covers {addr:#x}" + # the one-block int3 the linear scan picked up out of the padding is still not a function + assert 0x180004681 not in cfg.kb.functions + + def test_ppc64_function_ending_in_a_noreturning_call_is_kept(self): + # rejected(): puts() and then exit(). Its .opd descriptor at 0x10010e20 puts it at 0x100007bc with a + # size of 60, so all three blocks below are inside it. GCC emits the TOC restore after the bl to exit() + # and pads the rest of the section with zeroes, so the block past that call runs into bytes that do not + # decode -- which said nothing about the function, and cost it all three blocks. + proj = angr.Project(os.path.join(test_location, "ppc64", "fauxware"), auto_load_libs=False) + cfg = proj.analyses.CFGFast(normalize=True) + + assert 0x100007BC in cfg.kb.functions + assert {0x100007BC, 0x100007DC, 0x100007E8} <= cfg.kb.functions[0x100007BC].block_addrs_set + if __name__ == "__main__": unittest.main()