Unicorn's vendored QEMU defines a 52-bit guest physical address space for x86-64 and truncates accesses that resolve above it, so loads from the canonical x64 alias (0xFFFFF78000000000) fault even though the page is mapped. Mirror the page at the address that executed accesses resolve to (the alias masked to 52 bits).
Build the user shared data page once and write the same bytes to every alias, so all views report the same time values. Skip aliases that another mapping already owns, so that a duplicate map does not break kernel-mode loading and the foreign page keeps its contents.
None of these imports breaks an import cycle. The import of types in
kernel_mods stays inside the function, because a module-level import
would add the types module to the globals that _get_kmods scans.
Remove methods, functions, classes, constants and attributes that have
no callers or readers in speakeasy, its tests, its examples and the
ida-sandbox-plugin consumer. This includes the leftover decoy module
handling in kernel32, fileman and objman, the unused report models
MemoryBlock and ProcessMemoryManifest, the export execution mode fields
on ExportEntry and LoadedImage, unused _PeParser helpers, and
write-only emulator, thread, SEH and handler state.
FileManager.get_emu_file still reports configured modules as existing
files without content.
Under GDB, a call to an unsupported API logs a diagnostic and ends the
run, so the user cannot add a hook or redirect the call. The timeout
does not apply while a debugger is attached.
Under GDB, a preflight now stops before the unsupported call is
recorded, with the original frame suspended. The user can inspect it,
add a hook, change PC or patch the entry, and then resume. The
per-run timeout applies under the debugger and excludes the time that
execution is paused.
timeout applies to each run, so an analysis with many queued runs can
take much longer than timeout seconds in total.
max_total_time (default 0, which disables it, --max-total-time on the
command line) caps active execution across all queued runs and nested
starts of one run_module, run_shellcode or call invocation. Each
public invocation gets a fresh budget, and debugger pauses do not
count. When the cap is reached, the remaining queue stops with a
max_total_time error. Native execution uses the earlier of the per-run
and total deadlines.
Native DLL dependencies of a guest image are mapped and bound, but
their TLS callbacks and DllMain do not run. Malware that relies on
initialization in a bundled DLL behaves differently than on Windows.
Dependencies now initialize under the outer scheduler in load order,
with TLS callbacks before DllMain, once per process. A runtime library
load runs the initializers on deferred guest callback frames. When
DllMain fails at run time, the load fails, new PEB attachments and
unowned mappings are rolled back, and LdrLoadDll returns
STATUS_DLL_INIT_FAILED. At startup, a failed initializer logs a
warning and the analysis continues.
A module load that raises WindowsEmuError, for example when the API
trap reservation is full, rolls back and makes LoadLibrary and
LdrLoadDll return NULL instead of ending the run.
Each PEB lists the modules that are loaded when the PEB is allocated,
and a runtime library load attaches the new module to the current
process. A module that a process loads again from the cache is not
attached to that process, and a child process sees modules that only
its parent loaded.
Core and default DLLs are now attached when the PEB is allocated, and
a later reinitialization keeps the actual membership of that process.
A load that reuses a cached module attaches it to the selected process
only. Kernel and invisible modules stay out of user PEBs.
Synthetic API modules export the names of the signature catalog and the
handlers, with generated ordinals. An import by ordinal reaches a
different function than on Windows, and names that only a real binary
exports are missing.
When a module has an export manifest, its names and ordinals now define
the export table, including ordinal-only entries. Handler names that
the manifest does not list get ordinals after the highest physical
ordinal. A data export in the manifest gets zeroed writable storage
unless a data handler initializes it. A procedure lookup for a name
that the manifest lacks still succeeds when the module declares the
name, because other Windows builds export it. Modules without a
manifest use the signature catalog as before.
The QueryServiceConfig, GetModuleBaseName and GetModuleFileNameEx
handlers keep one apihook decorator, and the A/W fallback dispatches
both variants to them. The loader no longer needs __apihooks__, and a
second apihook decorator on one function raises ApiEmuError.
get_symbols returns (dll, name) tuples, and a caller that wants the
"dll.name" labels that get_symbol_from_address prints has to format
each one. Speakeasy.get_api_symbols returns the same snapshot as
"dll.name" strings: mapped function and data exports, dynamic-only
entries and auxiliary kernel symbols, without private trap tokens or
forwarder strings.
Synthetic API modules are built by build_api_image, and no module has
the decoy type any more. JitPeFile, the EMPTY_PE_32 and EMPTY_PE_64
husks, the exported function stubs and both is_decoy methods have no
callers.
The default_exe and default_driver PEs in speakeasy/winenv/decoys have
no loader since the decoy fallback was removed. The package, its
package-data entry and the default module_directory_x86/x64 values
that pointed into it are removed. get_native_module_path searches only
the configured module directory, and returns None when that directory
does not exist.
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.
Synthetic API modules export the names of their handlers and invent
ordinals for them. Real modules export A and W names, fixed ordinals
and forwarders, and an import by ordinal must reach the same function
as on Windows.
speakeasy/resources/win32/exports holds the export directories of 121
x86 and 129 x64 Windows modules, with ordinals, names, forwarder strings
and the kind of each export. The files come from the Windows Server 2025
binaries in sogen's emulation root, and the README describes how they
were extracted. export_manifest.py defines the file format as pydantic
models with a description for each field, and get_export_manifest loads
and validates the manifest of one module. The test suite validates
every file and checks that each handler ordinal matches the manifest of
its module. The manifests ship in the sdist and the wheel.
Synthetic API modules are decoy PE husks whose export stubs only return
an index, and the emulator intercepts calls through sentinel addresses
outside any mapping. A guest that parses such a module, hooks an entry
or follows a forwarder does not see a real image.
build_api_image builds a deterministic PE for a synthetic module, with
the raw offsets equal to the RVAs so that the image is also a valid
file. It has a read-only header and export directory, an RX .text with
one 32-byte slot per function export, an RW .data with storage for data
exports, and an RX .dyn arena for entries that are created at run time.
The export directory keeps sparse ordinals, ordinal aliases and
forwarder strings, and sorts names by bytes. encode_api_stub encodes
the jump from a public entry to its private trap, with a five-byte
prologue on x86 so that inline-hook trampolines work. The unified API
commit uses the builder for every synthetic module.
The loader tests that need PEs with export directories, delay imports
or malformed inventories build them with this builder.
PeLoader trusted pefile's inventories. pefile drops import entries with
malformed names, which shifts later entries onto the wrong IAT slots,
decodes names as UTF-8 and fails on other bytes, and has no delay
import support. A malformed optional directory either stopped the load
or was used without bounds checks.
PeLoader now walks each import lookup table itself, so every IAT slot
keeps its own position, and reads delay import descriptors as well. Each
ImportEntry records whether it is a static or a delay import, and the
current binder still binds only static imports. A non-ASCII DLL name is
decoded as Latin-1 with a warning. Exports record their kind (function
or data, from the section permissions) and their forwarder string, which
validate_forwarder checks for DLL.name or DLL.#ordinal syntax. A
malformed delay import directory, export directory, import entry or
forwarder gives a warning and is left out, and the valid entries stay.
Machine, address space, header
and section extents are always checked. Sections and the mapped image
are clipped to SizeOfImage, and the mapped image keeps the raw delay
import descriptors. LoadedImage.source tells guest PEs and shellcode
apart.
The signature database answers only permissive lookups: a name can
match through DLL aliases, name prefixes or another DLL's declaration.
That is useful to format handler arguments, but it cannot tell which
functions a module really declares or whether the generic executor can
run a declaration.
SignatureSource now has iter_functions, which lists the declarations of
one DLL for the architecture in name order, with no aliases, name
prefixes or declarations of other DLLs. SignatureDatabase.get_functions
maps these declarations to their signatures with source precedence and
keeps the map until a source is added. Win32MetadataSource keeps a
per-DLL index so that a listing does not scan the whole table. FuncSig.supports_emulation
tells whether the integer-slot executor can run a declaration. An
explicit empty arch list now means no architecture in lookup as well.
The unified API commit uses these methods to build synthetic export
tables and to gate generic dispatch.
The win32metadata archive is now win32json_signatures.json.gz, so that
its name states its source next to phnt_signatures.json.gz. A source
checkout must run `just gen-signatures` or reinstall to create it.
For a \Driver\ name with no object, ObReferenceObjectByName called
create_driver_object without an image. create_driver_object then looked
up a default_sys module that does not exist and raised. A driver object
also needs an image for its DriverSection entry. ObReferenceObjectByName
now returns STATUS_OBJECT_NAME_NOT_FOUND for every unknown name, as
Windows does, and create_driver_object requires the driver image.
Configured drivers still have objects.
MapViewOfFile parses a SEC_IMAGE mapping with load_pe. load_pe also
replaced self.input and the profiler input metadata with the values of
the mapped file, so the report described the mapped PE instead of the
sample. load_pe now only parses the PE.
Module32First and Module32Next wrote szExePath only for modules with a
decoy_path attribute, which no module sets, so the field kept the bytes
the caller left in the buffer. hModule was never written, and it was
declared as a 32-bit field, so on x64 szModule and szExePath were at the
wrong offsets. A shared helper now writes both entries: hModule is a
pointer-sized field that holds the module base, szExePath holds the
emulated path of the module, and wide strings end with a two-byte NUL.
EmuStruct caches each generated ctypes type by class name and pointer
size. PROCESSENTRY32, MODULEENTRY32, WIN32_FIND_DATA and SID take a
second constructor argument, a character width or a sub-authority count,
that changes their fields. The first instance in the process fixed the
layout for every later instance, so a wide call after an ANSI call wrote
the ANSI layout. The cache key now includes the pack value and the field
list.
FlsGetValue2 called emu.get_fls_value, which does not exist. The
AttributeError went to a catch-all handler, so every call returned
0x1000. FlsGetValue2 now returns the value in the fiber local storage
slot of the current thread, or 0 for an index that is not allocated.
Like the Windows function, it does not change the last error.
ProcessIdToSessionId returned FALSE for a null output pointer or an
unknown process ID, but did not set the last error. The null-pointer
branch held a bare windefs.ERROR_INVALID_PARAMETER expression with no
effect. Both failures now set ERROR_INVALID_PARAMETER.
Queued runs chained inside one Unicorn start, so timeout and
max_instructions limited all runs together. A run that spun used the
whole limit and the runs after it did not execute. A spent timeout only
wrote a log message, and an instruction limit ended the run with no
error and no instruction count unless memory tracing was on.
Unicorn now stops when a run completes and the next run is prepared,
and each run starts with its own timeout and instruction limit. When a
limit is reached, the run gets a timeout or max_instructions error with
its PC and, for max_instructions, the limit, and the next queued run
starts. With max_instructions set, a code hook counts the instructions
of the run. start() clears emu_complete, so a later run_shellcode or
call also gets its limits and their reports.
phnt declares RtlWow64GetThreadContext, RtlWow64SetThreadContext and
RtlGetFunctionTableListHead inside _WIN64 blocks, but the phnt
generator does not evaluate preprocessor guards. The generated catalog
gave these names no architecture, so an x86 lookup returned a
signature for functions that 32-bit ntdll does not export. The
generator now stamps a curated architecture list on these entries, and
the signature lookup skips them for x86.
Every PE image that load_image mapped replaced the static strings in
the report. Setup loads a container PE for shellcode, and later loads
can map the default_exe decoy for a missing DLL. A shellcode report
therefore listed the strings of the decoy PE, such as its DOS stub
text, in place of the strings in the shellcode. The emulator now
extracts static strings only from the image whose load runs setup,
which is the input of the emulator.
Unicorn caches translated blocks and does not drop them when the host
writes to guest memory. A hook, an API handler or a GDB client that
patches code which already ran had no effect: the next execution of
that code, in the same run or in a later one, used the stale
translation. EmuEngine.mem_write now removes the cached translations
for the written range, so patched code runs as written.
A watchpoint stopped Unicorn from inside the memory hook. Unicorn then
returned with the PC still on the accessing instruction and the write
already done, so a resume ran the instruction again and repeated its
side effect. A watch stop now only records the stop reason. The code
hook, which is active while any watchpoint exists, stops before the
next instruction, and a single step reports the watch stop when it
returns.
GetProcAddress returned 0 for a procedure that the module does not
export, but left the last error unchanged. Guest code that calls
GetLastError after a failed lookup read a stale value. GetProcAddress
now sets ERROR_PROC_NOT_FOUND (127) when it returns 0.
The inet_ntop, inet_pton, getaddrinfo and freeaddrinfo handlers
declared ordinals 180, 181, 178 and 177. These do not match ws2_32 on
either architecture. x86 exports freeaddrinfo, getaddrinfo, inet_ntop
and inet_pton as #176, #177, #179 and #180, and x64 exports them as
#191, #192, #194 and #195. An x86 import of ws2_32 #177 (getaddrinfo)
ran the freeaddrinfo handler, which freed the caller's argument. A
single handler ordinal cannot be correct for both architectures, so
the four handlers now declare no ordinal and are found by name only.
The loader lists were not valid rings. The last entry linked back to
the first entry instead of to the PEB_LDR_DATA list head, the
initialization-order links pointed into the wrong fields of their
neighbors, and the back links did not match the forward links. Guest
code that walked a list until it reached the head again read past the
end. Each new PEB initialization also added a second entry for modules
that were already in the list.
A process now has one loader entry per mapped image, and the three
lists link each entry to its neighbors and to their own head in both
directions. The main executable is not in the initialization-order
list. An empty list points to its head.
alloc_peb returned nothing when the PEB was already active, and it
initialized the PEB of the current process instead of the process it
received. A child process therefore had empty loader lists, and the
parent PEB received the modules a second time. alloc_peb now returns
the PEB and initializes the PEB of the given process, and init_peb only
writes the PEB pointer when the process is the current one.
Module name normalization removed any extension with splitext, so
windows.storage became windows and kernel.appcore became kernel. A name
that was already normalized lost its last dotted component on the next
pass, and lookups by that name did not match the loaded module.
The normalizer now takes the base name, lowercases it, and removes the
extension only when it is a known image extension (.dll, .exe, .sys,
.drv, .ocx, .cpl). Normalizing a name twice gives the same result.
The x86 SEH replay guard counts identical faults and stops dispatch
after four of them, but continue_seh cleared the count each time a
handler returned. A handler that returned CONTINUE_EXECUTION without a
repair replayed the same fault until the emulation timeout. The first
run from start() or call() also kept the guard state of the previous
run, because only queued runs reset it.
continue_seh now keeps the count when the handler resumes at the
faulting PC, and the code hook resets it once the guest executes a
different instruction. A fault with a different PC or address also
resets it. Each run clears the guard state when it is prepared.
CodeQL flags the ARC4 call in the CryptDeriveKey test as a weak
cryptographic algorithm. A short RC4 in the test gives the same
ciphertext and does not depend on the library that the handler uses.
A key found in the config was built fresh on each open and never added
to the open keys. A value set through one handle was gone after the
next open. When a driver's service key is in the config, the kernel
setup lost the service values it wrote to that key. A parent of a
configured key, such as ...\Services, could not be opened, and
RegEnumKeyEx did not list the configured key. The registry now keeps a
config key once it is loaded, and counts config key paths when it
looks for parents and subkeys.
is_key_a_parent_key matched by a plain string prefix. After a sample
created HKCU\Software\Foo, opening the missing key HKCU\Soft succeeded
and created it. The check now requires a path separator after the
parent path.
With a NULL pbBinary, CryptStringToBinary returned the decoded length
as its BOOL result instead of TRUE. Input that decodes to no bytes
gave 0, which the caller read as failure. The handler now returns 1.
EnumServicesStatus returned TRUE but did not write *pcbBytesNeeded,
*lpServicesReturned or *lpResumeHandle. Callers looped over an
uninitialized service count. The handler now writes 0 to each of them,
which reports an empty list.
For TokenElevation, GetTokenInformation wrote TokenIsElevated only when
the configured user was an admin. For other users it returned TRUE and
left the field uninitialized, so the caller could conclude that the
process was elevated. The handler now writes 0 or 1.
The size query of LookupAccountName passes cbSid and
cchReferencedDomainName as 0. The handler wrote only cbSid and set no
last error, so the caller allocated an empty domain buffer and the
second call failed. A small cbSid gave no required size, and a small
domain buffer overflowed. The handler now writes both required sizes
and returns FALSE with ERROR_INSUFFICIENT_BUFFER when either buffer is
NULL or too small. On success it writes the domain name length.
CryptHashData returned FALSE when dwDataLen was 0. Windows accepts
empty data, for example when a caller hashes an empty string, and the
caller took its error path. The handler now returns TRUE and leaves
the hash unchanged.
CryptGetHashParam returned TRUE and wrote nothing, so callers that ask
for HP_HASHVAL or HP_HASHSIZE read an uninitialized buffer. The handler
now writes the digest or its size from the hash object, reports the
needed size in *pdwDataLen, and returns FALSE with ERROR_MORE_DATA
when the buffer is too small.
CryptDeriveKey wrote a 16 byte key (5 hash bytes and 11 zero bytes of
salt) but set the key length to the digest size. For SHA-1 the RC4 key
was 20 bytes, so CryptDecrypt gave garbage. The handler also ignored
the key length in the upper 16 bits of dwFlags, so a 128-bit key used
only 5 bytes of the hash, and it ignored CRYPT_NO_SALT. The key length
now matches the bytes written, a length above 40 bits takes that many
bytes of the hash, and CRYPT_NO_SALT drops the salt.
EqualSid compared only the first 10 bytes: the header and the low two
bytes of the first subauthority. S-1-5-32-544 and S-1-5-32-545 were
equal, so admin group checks gave the wrong answer. The handler now
compares the full SID length given by the subauthority count.
NCryptImportKey and BCryptImportKeyPair called import_key on the
result of crypt_get without a check. An unknown or closed provider
handle raised AttributeError and stopped the run. The handlers now
return NTE_INVALID_HANDLE and STATUS_INVALID_HANDLE for such a handle.
On x64 the cbData DWORD is passed in a stack slot whose upper 32 bits
can hold garbage. NCryptImportKey used the full slot as the read size,
which raised OverflowError and stopped the run. The handler now masks
the size to 32 bits, as BCryptImportKeyPair does.
NCryptOpenStorageProvider accepts a NULL pszProviderName to select the
default provider. The handler then wrote no handle but returned
success, so the caller used an uninitialized handle in later calls.
The handler now opens a provider and writes its handle in both cases.
crypt_close used dict.pop without a default, so CryptReleaseContext
and BCryptCloseAlgorithmProvider raised KeyError for an unknown or
already closed handle and stopped the run. Cleanup code often does
this. CryptReleaseContext now returns FALSE with ERROR_INVALID_HANDLE
and BCryptCloseAlgorithmProvider returns STATUS_INVALID_HANDLE for
such a handle.
GetUserName wrote the name without a terminator, ignored the buffer
size, and set *pcbBuffer to the length without the terminator. Callers
read past the name into uninitialized memory, and a NULL size query
succeeded. The handler now sets *pcbBuffer to the length including the
terminator, and returns FALSE with ERROR_INSUFFICIENT_BUFFER when the
buffer is NULL or too small.
GetUserNameEx wrote the name without reading *nSize or checking for a
NULL buffer. The size query passes NULL, so the write went to address
0 and stopped the run. The handler now returns FALSE with
ERROR_MORE_DATA and the required size, including the terminator, when
the buffer is NULL or too small.
LookupAccountSid wrote the account and domain names without reading
the buffer sizes. The usual size query passes NULL buffers, so the
write went to address 0 and stopped the run. The handler now returns
FALSE with ERROR_INSUFFICIENT_BUFFER and the required sizes when a
buffer is NULL or too small. On success it writes the name lengths and
peUse.
RtlGenRandom built its output with bytes(range(n)), which raises
ValueError when n is more than 256 and stops the run. The byte values
now wrap at 256.