mirror of
https://github.com/mandiant/speakeasy.git
synced 2026-10-12 02:18:47 +00:00
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:
committed by
Willi Ballenthin
parent
1e6e622bb5
commit
deb08435b2
@@ -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"},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user