From 44e0ef1416d53f11b89d400c1ad161fc6682b94e Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 9 Mar 2026 11:34:16 +0100 Subject: [PATCH 1/7] ci: add permissions Co-authored-by: Moritz --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d99b6c52..8fdba415 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,8 @@ on: pull_request: branches: [master] +permissions: read-all + jobs: lint: runs-on: ubuntu-latest From 1d7d0c360e152927b538b4e25c4f7bac6ec4d8a2 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 9 Mar 2026 11:34:58 +0100 Subject: [PATCH 2/7] v2.0.0b1 --- speakeasy/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/speakeasy/version.py b/speakeasy/version.py index 4b3c3ea5..9b8d3ee1 100644 --- a/speakeasy/version.py +++ b/speakeasy/version.py @@ -1 +1 @@ -__version__ = "2.0.0a1" +__version__ = "2.0.0b1" From e6fd6049275e8952074418cba6a8bf7bc9cbfd54 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 9 Mar 2026 15:04:51 +0100 Subject: [PATCH 3/7] Add API stubs required for Go runtime emulation The Go runtime dynamically loads and calls several Windows APIs that speakeasy did not previously emulate, causing Go binaries to crash during initialization (issue #223). - Add winmm.dll to default module config so LoadLibraryExA succeeds - Add kernel32 stubs: AddVectoredContinueHandler, CreateWaitableTimerExW, GetProcessAffinityMask, SetConsoleCtrlHandler - Add ntdll stub: RtlGetNtVersionNumbers (returns Windows 10 version) - Add winmm stubs: timeBeginPeriod, timeEndPeriod Refs: #223 --- speakeasy/config.py | 1 + speakeasy/winenv/api/usermode/kernel32.py | 67 +++++++++++++++++++++++ speakeasy/winenv/api/usermode/ntdll.py | 17 ++++++ speakeasy/winenv/api/usermode/winmm.py | 14 +++++ 4 files changed, 99 insertions(+) diff --git a/speakeasy/config.py b/speakeasy/config.py index b02ba363..9d172755 100644 --- a/speakeasy/config.py +++ b/speakeasy/config.py @@ -232,6 +232,7 @@ {"name": "wkscli", "base_addr": "0x5fc00000", "path": "C:\\Windows\\system32\\wkscli.dll"}, {"name": "iphlpapi", "base_addr": "0x5fd00000", "path": "C:\\Windows\\system32\\iphlpapi.dll"}, {"name": "sfc_os", "base_addr": "0x5fe00000", "path": "C:\\Windows\\system32\\sfc_os.dll"}, + {"name": "winmm", "base_addr": "0x5ff00000", "path": "C:\\Windows\\system32\\winmm.dll"}, ], }, } diff --git a/speakeasy/winenv/api/usermode/kernel32.py b/speakeasy/winenv/api/usermode/kernel32.py index 38db845b..e2475800 100644 --- a/speakeasy/winenv/api/usermode/kernel32.py +++ b/speakeasy/winenv/api/usermode/kernel32.py @@ -6306,6 +6306,17 @@ def AddVectoredExceptionHandler(self, emu, argv, ctx={}): return Handler + @apihook("AddVectoredContinueHandler", argc=2) + def AddVectoredContinueHandler(self, emu, argv, ctx={}): + """ + PVOID AddVectoredContinueHandler( + ULONG First, + PVECTORED_EXCEPTION_HANDLER Handler + ); + """ + First, Handler = argv + return Handler + @apihook("RemoveVectoredExceptionHandler", argc=1) def RemoveVectoredExceptionHandler(self, emu, argv, ctx={}): """ @@ -6518,6 +6529,62 @@ def GetPhysicallyInstalledSystemMemory(self, emu, argv, ctx={}): self.mem_write(TotalMemoryInKilobytes, (0x200000).to_bytes(8, "little")) return 1 + @apihook("CreateWaitableTimerExW", argc=4) + def CreateWaitableTimerExW(self, emu, argv, ctx={}): + """ + HANDLE CreateWaitableTimerExW( + LPSECURITY_ATTRIBUTES lpTimerAttributes, + LPCWSTR lpTimerName, + DWORD dwFlags, + DWORD dwDesiredAccess + ); + """ + _attrs, name, _flags, _access = argv + + timer_name = None + obj = None + if name: + timer_name = self.read_mem_string(name, 2) + argv[1] = timer_name + obj = self.get_object_from_name(timer_name) + + if obj: + hnd = obj.get_handle() + emu.set_last_error(windefs.ERROR_ALREADY_EXISTS) + else: + hnd, _evt = emu.create_event(timer_name) + emu.set_last_error(windefs.ERROR_SUCCESS) + + return hnd + + @apihook("GetProcessAffinityMask", argc=3) + def GetProcessAffinityMask(self, emu, argv, ctx={}): + """ + BOOL GetProcessAffinityMask( + HANDLE hProcess, + PDWORD_PTR lpProcessAffinityMask, + PDWORD_PTR lpSystemAffinityMask + ); + """ + hProcess, lpProcessAffinityMask, lpSystemAffinityMask = argv + ptr_size = emu.get_ptr_size() + mask = 0x1 + if lpProcessAffinityMask: + self.mem_write(lpProcessAffinityMask, mask.to_bytes(ptr_size, "little")) + if lpSystemAffinityMask: + self.mem_write(lpSystemAffinityMask, mask.to_bytes(ptr_size, "little")) + return 1 + + @apihook("SetConsoleCtrlHandler", argc=2) + def SetConsoleCtrlHandler(self, emu, argv, ctx={}): + """ + BOOL SetConsoleCtrlHandler( + PHANDLER_ROUTINE HandlerRoutine, + BOOL Add + ); + """ + return 1 + @apihook("WTSGetActiveConsoleSessionId", argc=0) def WTSGetActiveConsoleSessionId(self, emu, argv, ctx={}): return emu.get_current_process().session diff --git a/speakeasy/winenv/api/usermode/ntdll.py b/speakeasy/winenv/api/usermode/ntdll.py index 6db37813..c1baaf78 100644 --- a/speakeasy/winenv/api/usermode/ntdll.py +++ b/speakeasy/winenv/api/usermode/ntdll.py @@ -361,3 +361,20 @@ def LdrAccessResource(self, emu, argv, ctx={}): emu.write_ptr(Resource, BaseAddress + offset) return ddk.STATUS_SUCCESS + + @apihook("RtlGetNtVersionNumbers", argc=3) + def RtlGetNtVersionNumbers(self, emu, argv, ctx={}): + """ + void RtlGetNtVersionNumbers( + DWORD *pNtMajorVersion, + DWORD *pNtMinorVersion, + DWORD *pNtBuildNumber + ); + """ + pMajor, pMinor, pBuild = argv + if pMajor: + self.mem_write(pMajor, (10).to_bytes(4, "little")) + if pMinor: + self.mem_write(pMinor, (0).to_bytes(4, "little")) + if pBuild: + self.mem_write(pBuild, (0xF0004A61).to_bytes(4, "little")) diff --git a/speakeasy/winenv/api/usermode/winmm.py b/speakeasy/winenv/api/usermode/winmm.py index ff7a0896..ab7068fd 100644 --- a/speakeasy/winenv/api/usermode/winmm.py +++ b/speakeasy/winenv/api/usermode/winmm.py @@ -17,6 +17,20 @@ def __init__(self, emu): super().__init__(emu) super().__get_hook_attrs__(self) + @apihook("timeBeginPeriod", argc=1) + def timeBeginPeriod(self, emu, argv, ctx={}): + """ + MMRESULT timeBeginPeriod(UINT uPeriod); + """ + return 0 # TIMERR_NOERROR + + @apihook("timeEndPeriod", argc=1) + def timeEndPeriod(self, emu, argv, ctx={}): + """ + MMRESULT timeEndPeriod(UINT uPeriod); + """ + return 0 # TIMERR_NOERROR + @apihook("timeGetTime", argc=0) def timeGetTime(self, emu, argv, ctx={}): """ From 9768a3c20488da27d9594342f868882dfc07131f Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 9 Mar 2026 15:19:11 +0100 Subject: [PATCH 4/7] Add remaining API stubs for Go runtime initialization Follow-up to the initial Go emulation fix. After testing with a real Go binary (Go 1.25), additional missing APIs were discovered: - Add bcryptprimitives.dll module + ProcessPrng stub (Go 1.20+ uses this instead of advapi32!SystemFunction036 for CSPRNG) - Add kernel32 stubs: GetErrorMode, WerGetFlags, WerSetFlags - Add ntdll stubs: RtlGetCurrentPeb, RtlGetVersion With these changes, Go runtime initialization proceeds through heap setup, thread creation, and environment loading. The runtime still panics (exit code 2) due to deeper emulation gaps (thread scheduling, timer resolution), but the unsupported_api crashes from #223 are fully resolved. Refs: #223 --- speakeasy/config.py | 5 ++++ .../winenv/api/usermode/bcryptprimitives.py | 29 +++++++++++++++++++ speakeasy/winenv/api/usermode/kernel32.py | 24 +++++++++++++++ speakeasy/winenv/api/usermode/ntdll.py | 24 +++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 speakeasy/winenv/api/usermode/bcryptprimitives.py diff --git a/speakeasy/config.py b/speakeasy/config.py index 9d172755..5dc38a99 100644 --- a/speakeasy/config.py +++ b/speakeasy/config.py @@ -233,6 +233,11 @@ {"name": "iphlpapi", "base_addr": "0x5fd00000", "path": "C:\\Windows\\system32\\iphlpapi.dll"}, {"name": "sfc_os", "base_addr": "0x5fe00000", "path": "C:\\Windows\\system32\\sfc_os.dll"}, {"name": "winmm", "base_addr": "0x5ff00000", "path": "C:\\Windows\\system32\\winmm.dll"}, + { + "name": "bcryptprimitives", + "base_addr": "0x60000000", + "path": "C:\\Windows\\system32\\bcryptprimitives.dll", + }, ], }, } diff --git a/speakeasy/winenv/api/usermode/bcryptprimitives.py b/speakeasy/winenv/api/usermode/bcryptprimitives.py new file mode 100644 index 00000000..37520be3 --- /dev/null +++ b/speakeasy/winenv/api/usermode/bcryptprimitives.py @@ -0,0 +1,29 @@ +import os + +from .. import api + + +class Bcryptprimitives(api.ApiHandler): + """ + Implements exported functions from bcryptprimitives.dll + """ + + name = "bcryptprimitives" + apihook = api.ApiHandler.apihook + impdata = api.ApiHandler.impdata + + def __init__(self, emu): + super().__init__(emu) + self.funcs = {} + self.data = {} + super().__get_hook_attrs__(self) + + @apihook("ProcessPrng", argc=2) + def ProcessPrng(self, emu, argv, ctx={}): + """ + BOOL ProcessPrng(PBYTE pbData, SIZE_T cbData); + """ + pbData, cbData = argv + rand_bytes = os.urandom(cbData) + self.mem_write(pbData, rand_bytes) + return 1 diff --git a/speakeasy/winenv/api/usermode/kernel32.py b/speakeasy/winenv/api/usermode/kernel32.py index e2475800..b740b43a 100644 --- a/speakeasy/winenv/api/usermode/kernel32.py +++ b/speakeasy/winenv/api/usermode/kernel32.py @@ -4428,6 +4428,30 @@ def SetErrorMode(self, emu, argv, ctx={}): """ return 0 + @apihook("GetErrorMode", argc=0) + def GetErrorMode(self, emu, argv, ctx={}): + """ + UINT GetErrorMode(); + """ + return 0 + + @apihook("WerGetFlags", argc=2) + def WerGetFlags(self, emu, argv, ctx={}): + """ + HRESULT WerGetFlags(HANDLE hProcess, DWORD *pdwFlags); + """ + hProcess, pdwFlags = argv + if pdwFlags: + self.mem_write(pdwFlags, (0).to_bytes(4, "little")) + return 0 # S_OK + + @apihook("WerSetFlags", argc=1) + def WerSetFlags(self, emu, argv, ctx={}): + """ + HRESULT WerSetFlags(DWORD dwFlags); + """ + return 0 # S_OK + @apihook("InterlockedCompareExchange", argc=3) def InterlockedCompareExchange(self, emu, argv, ctx={}): """ diff --git a/speakeasy/winenv/api/usermode/ntdll.py b/speakeasy/winenv/api/usermode/ntdll.py index c1baaf78..95694b58 100644 --- a/speakeasy/winenv/api/usermode/ntdll.py +++ b/speakeasy/winenv/api/usermode/ntdll.py @@ -378,3 +378,27 @@ def RtlGetNtVersionNumbers(self, emu, argv, ctx={}): self.mem_write(pMinor, (0).to_bytes(4, "little")) if pBuild: self.mem_write(pBuild, (0xF0004A61).to_bytes(4, "little")) + + @apihook("RtlGetCurrentPeb", argc=0) + def RtlGetCurrentPeb(self, emu, argv, ctx={}): + """ + PPEB RtlGetCurrentPeb(); + """ + proc = emu.get_current_process() + if proc and proc.peb: + return proc.peb.address + return 0 + + @apihook("RtlGetVersion", argc=1) + def RtlGetVersion(self, emu, argv, ctx={}): + """ + NTSTATUS RtlGetVersion(PRTL_OSVERSIONINFOW lpVersionInformation); + """ + (lpVersionInformation,) = argv + # RTL_OSVERSIONINFOW: dwOSVersionInfoSize(4), dwMajorVersion(4), + # dwMinorVersion(4), dwBuildNumber(4), dwPlatformId(4), szCSDVersion(256) + import struct + info = struct.pack(" Date: Mon, 9 Mar 2026 15:40:36 +0100 Subject: [PATCH 5/7] Populate KUSER_SHARED_DATA and fix GetStdHandle on amd64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause of Go runtime panic "nanotime returning zero": Go reads time directly from KUSER_SHARED_DATA (0x7FFE0000) without calling QueryPerformanceCounter. Speakeasy mapped this page but left it zero-filled. - Populate KUSER_SHARED_DATA with realistic values: InterruptTime, SystemTime, QpcFrequency, NtMajorVersion, TickCount - Fix GetStdHandle on amd64: mask the DWORD argument to 32 bits so STD_ERROR_HANDLE (0xFFFFFFF4) matches when sign-extended to 64-bit (0xFFFFFFFFFFFFFFF4) - Add SwitchToThread stub With these fixes, Go runtime initialization proceeds past nanotime into the goroutine scheduler, where it hits "stoplockedm: not runnable" — a fundamental threading limitation (Go requires concurrent thread execution, speakeasy executes threads serially). Refs: #223 --- speakeasy/windows/objman.py | 2 ++ speakeasy/windows/winemu.py | 30 +++++++++++++++++++++++ speakeasy/winenv/api/usermode/kernel32.py | 5 ++++ 3 files changed, 37 insertions(+) diff --git a/speakeasy/windows/objman.py b/speakeasy/windows/objman.py index 5274e798..87566e3a 100644 --- a/speakeasy/windows/objman.py +++ b/speakeasy/windows/objman.py @@ -552,6 +552,8 @@ def get_std_handle(self, dev): STD_OUTPUT_HANDLE = 0xFFFFFFF5 STD_ERROR_HANDLE = 0xFFFFFFF4 + dev = dev & 0xFFFFFFFF + for k, v in ( (STD_INPUT_HANDLE, self.stdin), (STD_OUTPUT_HANDLE, self.stdout), diff --git a/speakeasy/windows/winemu.py b/speakeasy/windows/winemu.py index ea8f1145..3d7e52ba 100644 --- a/speakeasy/windows/winemu.py +++ b/speakeasy/windows/winemu.py @@ -496,6 +496,36 @@ def setup_user_shared_data(self): # This is a read-only address for KUSER_SHARED_DATA, # and this is the same address for 32-bit and 64-bit. self.mem_map(self.page_size, base=0x7FFE0000, tag="emu.struct.KUSER_SHARED_DATA") + self._populate_user_shared_data(0x7FFE0000) + + def _populate_user_shared_data(self, base): + import struct + import time + + now_100ns = int(time.time() * 10_000_000) + 116444736000000000 + tick_ms = int(time.monotonic() * 1000) & 0xFFFFFFFF + + data = bytearray(0x400) + + # InterruptTime (offset 0x008): KSYSTEM_TIME {LowPart, High1Time, High2Time} + interrupt_100ns = int(time.monotonic() * 10_000_000) + struct.pack_into("> 32, interrupt_100ns >> 32) + # SystemTime (offset 0x014): KSYSTEM_TIME + struct.pack_into("> 32, now_100ns >> 32) + # NtMajorVersion (offset 0x260) + struct.pack_into(" Date: Mon, 9 Mar 2026 15:45:13 +0100 Subject: [PATCH 6/7] style: ruff formatting --- speakeasy/windows/winemu.py | 8 ++++---- speakeasy/winenv/api/usermode/ntdll.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/speakeasy/windows/winemu.py b/speakeasy/windows/winemu.py index 3d7e52ba..30e0fcac 100644 --- a/speakeasy/windows/winemu.py +++ b/speakeasy/windows/winemu.py @@ -509,11 +509,11 @@ def _populate_user_shared_data(self, base): # InterruptTime (offset 0x008): KSYSTEM_TIME {LowPart, High1Time, High2Time} interrupt_100ns = int(time.monotonic() * 10_000_000) - struct.pack_into("> 32, interrupt_100ns >> 32) + struct.pack_into( + "> 32, interrupt_100ns >> 32 + ) # SystemTime (offset 0x014): KSYSTEM_TIME - struct.pack_into("> 32, now_100ns >> 32) + struct.pack_into("> 32, now_100ns >> 32) # NtMajorVersion (offset 0x260) struct.pack_into(" Date: Wed, 25 Mar 2026 09:03:11 +0100 Subject: [PATCH 7/7] populate KUSER_SHARED_DATA OS version from config --- speakeasy/windows/winemu.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/speakeasy/windows/winemu.py b/speakeasy/windows/winemu.py index 30e0fcac..47787a7f 100644 --- a/speakeasy/windows/winemu.py +++ b/speakeasy/windows/winemu.py @@ -515,11 +515,11 @@ def _populate_user_shared_data(self, base): # SystemTime (offset 0x014): KSYSTEM_TIME struct.pack_into("> 32, now_100ns >> 32) # NtMajorVersion (offset 0x260) - struct.pack_into("