Add version.dll handlers reporting no version resource

version.dll had no handlers, so its imports went to the signature
fallback, which returns a zero size with ERROR_SUCCESS from
GetFileVersionInfoSize and a successful VerQueryValue with a NULL
buffer that callers dereference. Model the documented failure for a
file without a version resource instead: GetFileVersionInfo[Size][Ex]
return 0 with ERROR_RESOURCE_TYPE_NOT_FOUND (zeroing *lpdwHandle), and
VerQueryValue returns FALSE without touching its outputs.
This commit is contained in:
tnel bee
2026-09-30 12:08:50 +01:00
committed by Willi Ballenthin
parent 1e6e622bb5
commit deb08435b2
4 changed files with 212 additions and 0 deletions
+1
View File
@@ -239,6 +239,7 @@ DEFAULT_CONFIG_DATA = {
"base_addr": "0x60000000",
"path": "C:\\Windows\\system32\\bcryptprimitives.dll",
},
{"name": "version", "base_addr": "0x60100000", "path": "C:\\Windows\\system32\\version.dll"},
],
},
}
+73
View File
@@ -0,0 +1,73 @@
# Copyright (C) 2026 Mandiant, Inc. All Rights Reserved.
import speakeasy.winenv.defs.windows.windows as windefs
from .. import api
class Version(api.ApiHandler):
"""
Implements exported functions from version.dll
Version resources are not modeled: every file reports that it has none.
Without a handler, the signature fallback reports a zero size with
ERROR_SUCCESS and a successful VerQueryValue with a NULL buffer, which
callers dereference. The documented "no version resource" failure keeps
callers on their own error path instead.
"""
name = "version"
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("GetFileVersionInfoSize", argc=2)
def GetFileVersionInfoSize(self, emu, argv, ctx={}):
"""
DWORD GetFileVersionInfoSize(LPCSTR lptstrFilename, LPDWORD lpdwHandle);
"""
lptstrFilename, lpdwHandle = argv
if lpdwHandle:
self.mem_write(lpdwHandle, (0).to_bytes(4, "little"))
emu.set_last_error(windefs.ERROR_RESOURCE_TYPE_NOT_FOUND)
return 0
@apihook("GetFileVersionInfoSizeEx", argc=3)
def GetFileVersionInfoSizeEx(self, emu, argv, ctx={}):
"""
DWORD GetFileVersionInfoSizeEx(DWORD dwFlags, LPCSTR lpwstrFilename, LPDWORD lpdwHandle);
"""
dwFlags, lpwstrFilename, lpdwHandle = argv
if lpdwHandle:
self.mem_write(lpdwHandle, (0).to_bytes(4, "little"))
emu.set_last_error(windefs.ERROR_RESOURCE_TYPE_NOT_FOUND)
return 0
@apihook("GetFileVersionInfo", argc=4)
def GetFileVersionInfo(self, emu, argv, ctx={}):
"""
BOOL GetFileVersionInfo(LPCSTR lptstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);
"""
emu.set_last_error(windefs.ERROR_RESOURCE_TYPE_NOT_FOUND)
return 0
@apihook("GetFileVersionInfoEx", argc=5)
def GetFileVersionInfoEx(self, emu, argv, ctx={}):
"""
BOOL GetFileVersionInfoEx(DWORD dwFlags, LPCSTR lpwstrFilename, DWORD dwHandle, DWORD dwLen, LPVOID lpData);
"""
emu.set_last_error(windefs.ERROR_RESOURCE_TYPE_NOT_FOUND)
return 0
@apihook("VerQueryValue", argc=4)
def VerQueryValue(self, emu, argv, ctx={}):
"""
BOOL VerQueryValue(LPCVOID pBlock, LPCSTR lpSubBlock, LPVOID *lplpBuffer, PUINT puLen);
"""
# No version block can exist, so no value is available.
return 0
+1
View File
@@ -19,6 +19,7 @@ ERROR_INVALID_LEVEL = 124
ERROR_MOD_NOT_FOUND = 126
ERROR_ALREADY_EXISTS = 183
ERROR_NO_MORE_ITEMS = 259
ERROR_RESOURCE_TYPE_NOT_FOUND = 1813
S_OK = 0
+137
View File
@@ -0,0 +1,137 @@
import struct
import pytest
from speakeasy import Speakeasy
from speakeasy.winenv.api.usermode.version import Version
from speakeasy.winenv.defs.windows.windows import ERROR_RESOURCE_TYPE_NOT_FOUND
class Memory:
def __init__(self):
self.writes = {}
self.error = None
def mem_write(self, address, data):
self.writes[address] = data
def set_last_error(self, value):
self.error = value
@pytest.mark.parametrize(
"func, argv, handle",
[
(Version.GetFileVersionInfoSize, [8, 16], 16),
(Version.GetFileVersionInfoSizeEx, [0, 8, 16], 16),
],
)
def test_size_reports_no_resource_and_zeroes_handle(func, argv, handle):
memory = Memory()
assert func(memory, memory, argv) == 0
assert memory.writes == {handle: b"\0\0\0\0"}
assert memory.error == ERROR_RESOURCE_TYPE_NOT_FOUND
def test_size_accepts_null_handle():
memory = Memory()
assert Version.GetFileVersionInfoSize(memory, memory, [8, 0]) == 0
assert not memory.writes
assert memory.error == ERROR_RESOURCE_TYPE_NOT_FOUND
@pytest.mark.parametrize(
"func, argv",
[
(Version.GetFileVersionInfo, [8, 0, 64, 16]),
(Version.GetFileVersionInfoEx, [0, 8, 0, 64, 16]),
],
)
def test_info_fails_without_touching_the_buffer(func, argv):
memory = Memory()
assert func(memory, memory, argv) == 0
assert not memory.writes
assert memory.error == ERROR_RESOURCE_TYPE_NOT_FOUND
def test_query_value_finds_nothing_and_leaves_outputs_untouched():
memory = Memory()
assert Version.VerQueryValue(memory, memory, [8, 16, 24, 32]) == 0
assert not memory.writes
def _x86_blob():
"""GetFileVersionInfoSizeA(name, &handle), then VerQueryValueA(0, 0, &ptr, &len); ret."""
code = bytearray(b"\xe8\x00\x00\x00\x00\x5b") # call $+5; pop ebx
fixups = []
def push_address(label): # lea eax, [ebx+disp32]; push eax
fixups.append((len(code) + 2, label))
code.extend(b"\x8d\x83\0\0\0\0\x50")
push_address("handle")
push_address("name")
calls = {"GetFileVersionInfoSizeA": len(code) + 1}
code.extend(b"\xb8\0\0\0\0\xff\xd0") # mov eax, imm32; call eax
push_address("length")
push_address("pointer")
code.extend(b"\x6a\x00\x6a\x00")
calls["VerQueryValueA"] = len(code) + 1
code.extend(b"\xb8\0\0\0\0\xff\xd0\xc3")
return code, fixups, calls, 4, 5
def _x64_blob():
"""Same calls with the x64 convention and shadow space."""
code = bytearray(b"\xe8\x00\x00\x00\x00\x5b") # call $+5; pop rbx
fixups = []
calls = {}
def lea(prefix, label): # lea reg, [rbx+disp32]
code.extend(prefix)
fixups.append((len(code), label))
code.extend(b"\0\0\0\0")
def call(name): # sub rsp, 0x28; mov rax, imm64; call rax; add rsp, 0x28
code.extend(b"\x48\x83\xec\x28\x48\xb8")
calls[name] = len(code)
code.extend(b"\0" * 8 + b"\xff\xd0\x48\x83\xc4\x28")
lea(b"\x48\x8d\x8b", "name") # rcx
lea(b"\x48\x8d\x93", "handle") # rdx
call("GetFileVersionInfoSizeA")
code.extend(b"\x31\xc9\x31\xd2") # xor ecx, ecx; xor edx, edx
lea(b"\x4c\x8d\x83", "pointer") # r8
lea(b"\x4c\x8d\x8b", "length") # r9
call("VerQueryValueA")
code.extend(b"\xc3")
return code, fixups, calls, 8, 5
@pytest.mark.parametrize("arch, build", [("x86", _x86_blob), ("x64", _x64_blob)])
def test_handler_takes_precedence_over_signature_fallback(arch, build):
code, fixups, calls, width, anchor = build()
labels = {}
for label, fill in (("handle", b"\xff" * 4), ("pointer", b"\xee" * width), ("length", b"\xdd" * 4)):
labels[label] = len(code)
code.extend(fill)
labels["name"] = len(code)
code.extend(b"C:\\Windows\\system32\\kernel32.dll\0")
for offset, label in fixups:
code[offset : offset + 4] = struct.pack("<i", labels[label] - anchor)
se = Speakeasy()
base = se.load_shellcode(data=bytes(code), arch=arch)
emu = se.emu
for name, offset in calls.items():
emu.mem_write(base + offset, emu.get_proc("version", name).to_bytes(width, "little"))
se.run_shellcode(base)
entry = se.get_report().entry_points[0]
assert entry.error is None
results = {event.api_name: event.ret_val for event in entry.events if event.api_name.startswith("version.")}
assert results == {"version.GetFileVersionInfoSizeA": "0x0", "version.VerQueryValueA": "0x0"}
assert emu.get_last_error() == ERROR_RESOURCE_TYPE_NOT_FOUND
assert emu.mem_read(base + labels["handle"], 4) == b"\0" * 4
assert emu.mem_read(base + labels["pointer"], width) == b"\xee" * width
assert emu.mem_read(base + labels["length"], 4) == b"\xdd" * 4