2023-09-07 18:15:18 -07:00
|
|
|
#!/usr/bin/env python3
|
2024-08-28 18:31:43 -07:00
|
|
|
from __future__ import annotations
|
2025-05-19 09:38:17 -04:00
|
|
|
|
2022-03-26 01:52:33 -07:00
|
|
|
import os
|
2026-01-30 14:55:19 -07:00
|
|
|
from unittest import TestCase, main
|
2022-03-26 01:52:33 -07:00
|
|
|
|
|
|
|
|
import archinfo
|
2022-10-14 16:11:21 -07:00
|
|
|
|
2025-05-19 09:38:17 -04:00
|
|
|
import angr
|
2022-03-26 01:52:33 -07:00
|
|
|
|
|
|
|
|
test_location = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "..", "..", "..", "binaries", "tests")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=missing-class-docstring
|
|
|
|
|
# pylint: disable=no-self-use
|
|
|
|
|
class TestPcodeEngine(TestCase):
|
|
|
|
|
def test_shellcode(self):
|
|
|
|
|
"""
|
|
|
|
|
Test basic CFG recovery and symbolic/concrete execution paths.
|
|
|
|
|
"""
|
|
|
|
|
base_address = 0
|
|
|
|
|
prototype = "int node_d(long)"
|
|
|
|
|
code = archinfo.arch_from_id("AMD64").asm(
|
2023-01-12 16:07:58 -07:00
|
|
|
"""
|
2022-03-26 01:52:33 -07:00
|
|
|
node_a:
|
|
|
|
|
test rdi, rdi
|
|
|
|
|
jz node_c
|
|
|
|
|
node_b:
|
|
|
|
|
mov rax, 0x1234
|
|
|
|
|
jmp node_d
|
|
|
|
|
node_c:
|
|
|
|
|
mov rax, 0x5678
|
|
|
|
|
node_d:
|
|
|
|
|
ret
|
2023-01-12 16:07:58 -07:00
|
|
|
""",
|
2022-03-26 01:52:33 -07:00
|
|
|
base_address,
|
|
|
|
|
)
|
|
|
|
|
|
2023-01-24 18:57:25 -07:00
|
|
|
arch = archinfo.ArchPcode("x86:LE:64:default")
|
2022-03-26 01:52:33 -07:00
|
|
|
angr.calling_conventions.register_default_cc(arch.name, angr.calling_conventions.SimCCSystemVAMD64)
|
|
|
|
|
p = angr.load_shellcode(code, arch=arch, load_address=base_address, engine=angr.engines.UberEnginePcode)
|
|
|
|
|
|
|
|
|
|
# Recover the CFG
|
|
|
|
|
c = p.analyses.CFGFast(normalize=True)
|
2026-02-24 09:40:02 -07:00
|
|
|
assert len(list(c.model.nodes())) == 4
|
2022-03-26 01:52:33 -07:00
|
|
|
|
|
|
|
|
# Execute symbolically
|
|
|
|
|
s = p.factory.call_state(base_address, prototype=prototype)
|
|
|
|
|
simgr = p.factory.simulation_manager(s)
|
|
|
|
|
simgr.run()
|
2022-10-14 16:11:21 -07:00
|
|
|
assert sum(len(i) for i in simgr.stashes.values()) == 2
|
2022-03-26 01:52:33 -07:00
|
|
|
assert {s.solver.eval(s.regs.rax) for s in simgr.deadended} == {0x1234, 0x5678}
|
|
|
|
|
|
|
|
|
|
# Execute concretely
|
|
|
|
|
callable_func = p.factory.callable(base_address, prototype=prototype, concrete_only=True)
|
|
|
|
|
for input_, expected_output in [(0, 0x5678), (1, 0x1234), (0xFFFFFFFFFFFFFFFF, 0x1234)]:
|
|
|
|
|
assert (callable_func(input_) == expected_output).is_true()
|
|
|
|
|
|
|
|
|
|
def test_fauxware(self):
|
|
|
|
|
"""
|
|
|
|
|
Test basic fauxware execution.
|
|
|
|
|
"""
|
|
|
|
|
p = angr.Project(
|
|
|
|
|
os.path.join(test_location, "x86_64", "fauxware"), auto_load_libs=False, engine=angr.engines.UberEnginePcode
|
|
|
|
|
)
|
|
|
|
|
simgr = p.factory.simgr()
|
|
|
|
|
simgr.run()
|
|
|
|
|
|
2022-10-14 16:11:21 -07:00
|
|
|
assert sum(len(i) for i in simgr.stashes.values()) == len(simgr.deadended) == 3
|
2022-03-26 01:52:33 -07:00
|
|
|
|
|
|
|
|
grant_paths = [s for s in simgr.deadended if b"trusted" in s.posix.dumps(1)]
|
|
|
|
|
assert len(grant_paths) == 2
|
2024-08-29 16:56:53 -07:00
|
|
|
assert sum(s.posix.dumps(0) == b"\x00\x00\x00\x00\x00\x00\x00\x00\x00SOSNEAKY\x00" for s in grant_paths) == 1
|
2022-03-26 01:52:33 -07:00
|
|
|
|
|
|
|
|
deny_paths = [s for s in simgr.deadended if b"Go away!" in s.posix.dumps(1)]
|
|
|
|
|
assert len(deny_paths) == 1
|
|
|
|
|
|
2025-10-22 19:35:53 +02:00
|
|
|
def test_riscv64_int_right_behavior(self):
|
|
|
|
|
"""
|
|
|
|
|
Test the use of correct bitvector extension in behavior INT_RIGHT
|
|
|
|
|
"""
|
|
|
|
|
# beq x12, x0, 12 ; srliw x31, x5, 31
|
|
|
|
|
byte_code = 0x00060663_01F2DF9B.to_bytes(8, "little")
|
|
|
|
|
# abi names: t0 = x5, t6 = x31
|
|
|
|
|
|
2026-05-18 12:02:23 -07:00
|
|
|
arch = archinfo.ArchPcode("RISCV:LE:64:default")
|
2025-10-22 19:35:53 +02:00
|
|
|
p = angr.load_shellcode(byte_code, arch=arch, load_address=0, engine=angr.engines.UberEnginePcode)
|
|
|
|
|
|
|
|
|
|
entry_state = p.factory.entry_state()
|
|
|
|
|
entry_state.registers.store("t0", 2**32 - 1) # bits 31..0 are set
|
|
|
|
|
|
|
|
|
|
simgr = p.factory.simulation_manager(entry_state)
|
|
|
|
|
simgr = simgr.step()
|
|
|
|
|
|
|
|
|
|
# |-32bit-|
|
|
|
|
|
# 111...111 >>(logical) 31 = 1
|
|
|
|
|
|
|
|
|
|
assert simgr.active[0].regs.t6.concrete
|
|
|
|
|
assert simgr.active[0].regs.t6.concrete_value == 1
|
|
|
|
|
|
2026-04-25 00:08:31 +08:00
|
|
|
def test_callless_function_graph_consistency(self):
|
|
|
|
|
binary_path = os.path.join(test_location, "x86_64", "fauxware")
|
|
|
|
|
proj = angr.Project(
|
|
|
|
|
binary_path,
|
|
|
|
|
load_options={"auto_load_libs": False},
|
|
|
|
|
engine=angr.engines.UberEnginePcode,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Address 400550: ff 25 ca 0a 20 00 jmp *0x200aca(%rip) # 601020 <strcmp@GLIBC_2.2.5>
|
|
|
|
|
# This is a PLT stub. Current limitations in the P-Code engine cause it to
|
|
|
|
|
# misidentify this indirect jump as 'Ijk_Boring', leading to a disconnected
|
|
|
|
|
# function graph that creates a false negative in this test.
|
|
|
|
|
#
|
|
|
|
|
# Since the purpose of this test is specifically to verify the CALLLESS logic
|
|
|
|
|
# and not P-Code's jumpkind resolution, we manually hook this address with
|
|
|
|
|
# a SimProcedure. This bypasses the engine's parsing limitations and ensures
|
|
|
|
|
# the CALLLESS mechanism can correctly generate the expected FakeRet edge.
|
|
|
|
|
proj.hook(0x400550, angr.SimProcedure(return_value=0), length=6)
|
|
|
|
|
|
|
|
|
|
cfg = proj.analyses.CFGEmulated(
|
|
|
|
|
keep_state=True,
|
|
|
|
|
fail_fast=True,
|
|
|
|
|
starts=[0x400664], # authenticate
|
|
|
|
|
state_add_options={angr.options.CALLLESS},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# For each node in cfg.graph that has outgoing edges,
|
|
|
|
|
# verify that the corresponding node in function.graph also has outgoing edges.
|
|
|
|
|
# A node with successors in cfg.graph but none in function.graph indicates
|
|
|
|
|
# the bug where CALLLESS converts Ijk_Call to Ijk_Ret, causing
|
|
|
|
|
# _update_function_transition_graph to invoke _add_return_from instead of
|
|
|
|
|
# _add_fakeret_to, leaving call blocks disconnected in function.graph.
|
|
|
|
|
for cfg_node in cfg.graph.nodes():
|
|
|
|
|
cfg_out = cfg.graph.out_degree(cfg_node)
|
|
|
|
|
if cfg_out == 0:
|
|
|
|
|
continue
|
|
|
|
|
# look up the function this node belongs to
|
|
|
|
|
func = cfg.kb.functions.get_by_addr(cfg_node.function_address)
|
|
|
|
|
if func is None:
|
|
|
|
|
continue
|
|
|
|
|
# find the corresponding node in function.graph
|
|
|
|
|
func_node = next((n for n in func.graph.nodes() if n.addr == cfg_node.addr), None)
|
|
|
|
|
if func_node is None:
|
|
|
|
|
continue
|
|
|
|
|
func_out = func.graph.out_degree(func_node)
|
|
|
|
|
assert func_out > 0
|
|
|
|
|
|
2022-03-26 01:52:33 -07:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|