mirror of
https://github.com/mandiant/speakeasy.git
synced 2026-10-12 02:18:47 +00:00
Synthetic API modules were decoy PE husks whose export stubs only returned an index. Import binding and GetProcAddress gave out sentinel addresses such as 0xFEEDFACE outside any mapping, and the emulator caught calls through the invalid-fetch callback and a code hook on each module. The export table and the IAT therefore gave different addresses for one function, and a guest or debugger could not read, hook, patch or break on the address of an imported API. A module without a handler loaded the default_exe or default_driver decoy PE. One session API registry now owns every synthesized entry. ApiModuleLoader in api_image.py builds a mapped image for each API module, from the exact declarations of the signature database and the explicit function and data handlers. The export table, the IAT and the resolvers give the same mapped entry for a function. Each function entry jumps to a private trap token in a permanently unmapped 1 MiB reservation. A fetch of a token stops Unicorn, and _run_api_engine dispatches the handler outside the native callback. Other accesses to the reservation raise an access violation in the guest. The pc of an API event is the call instruction when memory tracing is enabled, and the caller's return address otherwise. A module without a handler gets an empty placeholder image with a dynamic arena. The get_proc and resolve_export resolvers back GetProcAddress, LdrGetProcedureAddress and MmGetSystemRoutineAddress. They follow forwarders and create dynamic-only entries in the .dyn arena of a synthetic module. Import binding works on each IAT slot: a missing synthetic export gets a dynamic entry, and an invalid slot keeps its bytes. Data exports have writable storage in .data, and the CRT _fmode, _commode and __initenv exports share it with their pointer accessors. The per-run timeout and max_instructions limits now count across API yields, with exact instruction accounting when a limit is set. The sentinel addresses, import_table, impdata_queue, the module access hook and the default_exe/default_driver fallback are removed. Handler callbacks always run inside an API frame, so setup_callback no longer takes caller arguments, and the unused _normalize_mod_name is removed. A guest PE without relocation data cannot be rebased. get_symbols returns a snapshot, GDB breakpoints use the public entries, and doc/unified-api-addresses.md describes the design.
101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
"""
|
|
Call API handlers directly, with the context that dispatch builds.
|
|
"""
|
|
|
|
import struct
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from speakeasy import Speakeasy
|
|
from speakeasy.profiler import ApiCallbackFrame, Run
|
|
from speakeasy.windows import objman
|
|
from speakeasy.windows.win32 import Win32Emulator
|
|
from speakeasy.winenv import arch as e_arch
|
|
from speakeasy.winenv.api import sigdb
|
|
from speakeasy.winenv.api.api import ApiContext, HandlerArgs
|
|
|
|
|
|
def load_emu(config: dict[str, Any], data: bytes) -> Iterator[Speakeasy]:
|
|
if not sigdb.get_default_database().available:
|
|
pytest.skip("bundled signature database not generated")
|
|
se = Speakeasy(config=config)
|
|
try:
|
|
se.load_module(data=data)
|
|
assert se.emu is not None
|
|
se.emu.curr_run = Run()
|
|
yield se
|
|
finally:
|
|
se.shutdown()
|
|
|
|
|
|
def alloc(se: Speakeasy, data: bytes) -> int:
|
|
addr = se.mem_alloc(len(data), base=0x20000000)
|
|
se.mem_write(addr, data)
|
|
return addr
|
|
|
|
|
|
def unicode_string(se: Speakeasy, text: str) -> int:
|
|
"""Build an x86 UNICODE_STRING for ``text`` and return its address."""
|
|
buf = text.encode("utf-16le")
|
|
buf_addr = alloc(se, buf + b"\x00\x00")
|
|
return alloc(se, struct.pack("<HHI", len(buf), len(buf) + 2, buf_addr))
|
|
|
|
|
|
def object_attributes(se: Speakeasy, name: str) -> int:
|
|
"""Build an x86 OBJECT_ATTRIBUTES for ``name`` and return its address."""
|
|
return alloc(se, struct.pack("<IIIIII", 24, 0, unicode_string(se, name), 0, 0, 0))
|
|
|
|
|
|
def call(se: Speakeasy, dll: str, name: str, argv: list[int]) -> tuple[int, dict[str | int, str]]:
|
|
"""
|
|
Call a handler with the context and frame that dispatch builds. A variadic handler reads
|
|
``argv`` from the stack. Return the handler result and the displays, by
|
|
parameter name, or by slot index for a call without a signature.
|
|
"""
|
|
emu = se.emu
|
|
assert emu is not None and emu.api is not None
|
|
mod, func_attrs = emu.api.get_export_func_handler(dll, name)
|
|
if not func_attrs:
|
|
mod, func_attrs = emu.normalize_import_miss(dll, name)
|
|
_, func, argc, conv, _ = func_attrs
|
|
if argc == e_arch.VAR_ARGS:
|
|
emu.set_func_args(emu.stack_base, 0, *argv, conv=conv)
|
|
argv = []
|
|
assert argc in (e_arch.VAR_ARGS, len(argv))
|
|
sig = emu.get_handler_signature(dll, name, argc)
|
|
if sig is None:
|
|
args = HandlerArgs.from_slots(argv)
|
|
else:
|
|
args = HandlerArgs.from_signature(sig, emu.get_ptr_size(), emu._render_signature_args(sig, argv), argv)
|
|
ctx = ApiContext(func_name=f"{dll}.{name}", args=args)
|
|
previous = emu._active_api_frame
|
|
emu._active_api_frame = ApiCallbackFrame(emu.get_stack_ptr(), return_address=0)
|
|
try:
|
|
rv = func(mod, emu, list(argv), ctx)
|
|
finally:
|
|
emu._active_api_frame = previous
|
|
return rv, {i if a.name is None else a.name: a.display for i, a in enumerate(args.get_report_args())}
|
|
|
|
|
|
def start_process(se: Speakeasy) -> None:
|
|
"""
|
|
Create the process and thread that ``run_module`` creates, for handlers
|
|
that use the current process, thread, or module.
|
|
"""
|
|
emu = se.emu
|
|
assert isinstance(emu, Win32Emulator)
|
|
module = emu.modules[0]
|
|
emu.prepare_module_for_emulation(module, False)
|
|
if not emu.processes:
|
|
p = objman.Process(emu, path=module.emu_path, base=module.base, pe=module, cmdline=emu.command_line)
|
|
emu.processes.append(p)
|
|
emu.curr_process = p
|
|
emu.om.objects.update({p.address: p}) # type: ignore[union-attr]
|
|
t = objman.Thread(emu, stack_base=emu.stack_base, stack_commit=module.stack_commit)
|
|
emu.om.objects.update({t.address: t}) # type: ignore[union-attr]
|
|
emu.curr_process.threads.append(t) # type: ignore[union-attr]
|
|
emu.curr_thread = t
|
|
emu.curr_mod = module
|