From 748fe53348b93bd14778818191518b33b8f6c288 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Mon, 9 Mar 2026 19:49:04 +0100 Subject: [PATCH] Add filename parameter to load_module and load_shellcode Allow users to specify a custom filename when loading modules or shellcode from bytes. This is useful when malware checks its own filename (e.g. via GetModuleFileName). Closes #16 --- speakeasy/speakeasy.py | 16 ++++++++++++---- speakeasy/windows/kernel.py | 11 +++++++++-- speakeasy/windows/win32.py | 17 ++++++++++------- tests/test_filename_override.py | 12 ++++++++++++ 4 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 tests/test_filename_override.py diff --git a/speakeasy/speakeasy.py b/speakeasy/speakeasy.py index 62e730b7..7743c1b4 100644 --- a/speakeasy/speakeasy.py +++ b/speakeasy/speakeasy.py @@ -237,9 +237,16 @@ def _auto_mount_target_directory(self, path: str) -> None: for e in entries: logger.debug(" %s -> %s", e["emu_path"], e["path"]) - def load_module(self, path=None, data=None): + def load_module(self, path=None, data=None, filename=None): """ Load a module into the speakeasy emulator. + + args: + path: file path to PE module + data: bytes object containing PE module + filename: override the module filename seen by the emulated program. + Useful when emulating from bytes and the malware checks its + own filename (e.g. via GetModuleFileName). """ if not path and not data: raise SpeakeasyError("No emulation target supplied") @@ -263,7 +270,7 @@ def load_module(self, path=None, data=None): self._init_emulator(path=path, data=data) - return self.emu.load_module(path=path, data=data) # type: ignore[union-attr] + return self.emu.load_module(path=path, data=data, filename=filename) # type: ignore[union-attr] def load_image(self, image): """ @@ -299,7 +306,7 @@ def run_module(self, module, all_entrypoints=False, emulate_children=False) -> N else: return self.emu.run_module(module=module, all_entrypoints=all_entrypoints) # type: ignore[no-any-return, union-attr] - def load_shellcode(self, fpath=None, arch=None, data=None) -> int: + def load_shellcode(self, fpath=None, arch=None, data=None, filename=None) -> int: """ Load a shellcode blob into emulation space @@ -307,13 +314,14 @@ def load_shellcode(self, fpath=None, arch=None, data=None) -> int: fpath: file path containing shellcode blob arch: Architecture (x86 | amd64) to load shellcode as data: bytes object containing shellcode blob + filename: override the module filename seen by the emulated program return: Address of the loaded shellcode in the emulation space """ self._init_emulator(is_raw_code=True) self.loaded_bins.append(fpath) - return self.emu.load_shellcode(fpath, arch, data=data) # type: ignore[no-any-return, union-attr] + return self.emu.load_shellcode(fpath, arch, data=data, filename=filename) # type: ignore[no-any-return, union-attr] @check_init def run_shellcode(self, sc_addr: int, stack_commit=0x4000, offset=0) -> None: diff --git a/speakeasy/windows/kernel.py b/speakeasy/windows/kernel.py index c3425f12..79dba527 100644 --- a/speakeasy/windows/kernel.py +++ b/speakeasy/windows/kernel.py @@ -100,10 +100,17 @@ def setup(self): self.setup_kernel_mode() self.setup_user_shared_data() - def load_module(self, path=None, data=None): + def load_module(self, path=None, data=None, filename=None): from speakeasy.windows.loaders import PeLoader - if not data: + if filename: + file_name = ntpath.basename(filename) + mod_name = os.path.splitext(file_name)[0] + if not data: + assert path is not None + with open(path, "rb") as f: + data = f.read() + elif not data: assert path is not None file_name = os.path.basename(path) mod_name = os.path.splitext(file_name)[0] diff --git a/speakeasy/windows/win32.py b/speakeasy/windows/win32.py index 3539d70f..c721a3b4 100644 --- a/speakeasy/windows/win32.py +++ b/speakeasy/windows/win32.py @@ -159,8 +159,8 @@ def init_processes(self, processes): self.processes.append(p) - def load_module(self, path=None, data=None): - self._init_name(path, data) + def load_module(self, path=None, data=None, filename=None): + self._init_name(path, data, filename=filename) if not data: assert path is not None @@ -344,8 +344,11 @@ def run_module(self, module, all_entrypoints=False, emulate_children=False): return - def _init_name(self, path, data=None): - if not data: + def _init_name(self, path, data=None, filename=None): + if filename: + self.file_name = ntpath.basename(filename) + self.mod_name = os.path.splitext(self.file_name)[0] + elif not data: self.file_name = os.path.basename(path) self.mod_name = os.path.splitext(self.file_name)[0] else: @@ -363,10 +366,10 @@ def emulate_module(self, path): mod = self.load_module(path) self.run_module(mod) - def load_shellcode(self, path, arch, data=None): + def load_shellcode(self, path, arch, data=None, filename=None): from speakeasy.windows.loaders import ShellcodeLoader - self._init_name(path, data) + self._init_name(path, data, filename=filename) if arch == "x86": arch = _arch.ARCH_X86 elif arch in ("x64", "amd64"): @@ -380,7 +383,7 @@ def load_shellcode(self, path, arch, data=None): loader = ShellcodeLoader(data=data, arch=arch) image = loader.make_image() - image.name = str(sc_hash) + image.name = self.mod_name if filename else str(sc_hash) rtmod = self.load_image(image) sc_addr = rtmod.base diff --git a/tests/test_filename_override.py b/tests/test_filename_override.py new file mode 100644 index 00000000..b2b47426 --- /dev/null +++ b/tests/test_filename_override.py @@ -0,0 +1,12 @@ +"""Tests for the ``filename`` parameter on load_module / load_shellcode.""" + +from speakeasy import Speakeasy + + +def test_load_module_filename_override(load_test_bin): + data = load_test_bin("dll_test_x86.dll.xz") + se = Speakeasy() + se.load_module(data=data, filename="malware.dll") + + assert se.emu.file_name == "malware.dll" + assert se.emu.mod_name == "malware"