|
13 | 13 | import urllib.error
|
14 | 14 | import urllib.parse
|
15 | 15 | import urllib.request
|
16 |
| -from ctypes import byref, c_buffer, c_int, c_ulong, create_string_buffer, sizeof |
| 16 | +from ctypes import byref, c_buffer, c_int, c_ulong, create_string_buffer, sizeof, windll, ArgumentError |
17 | 17 | from pathlib import Path
|
18 | 18 | from shutil import copy
|
19 | 19 |
|
|
50 | 50 | TERMINATE_EVENT,
|
51 | 51 | TTD32_NAME,
|
52 | 52 | TTD64_NAME,
|
| 53 | + SIDELOADER32_NAME, |
| 54 | + SIDELOADER64_NAME, |
53 | 55 | )
|
54 | 56 | from lib.common.defines import (
|
55 | 57 | KERNEL32,
|
|
65 | 67 | from lib.core.compound import create_custom_folders
|
66 | 68 | from lib.core.config import Config
|
67 | 69 |
|
| 70 | +# CSIDL constants |
| 71 | +CSIDL_WINDOWS = 0x0024 |
| 72 | +CSIDL_SYSTEM = 0x0025 |
| 73 | +CSIDL_SYSTEMX86 = 0x0029 |
| 74 | +CSIDL_PROGRAM_FILES = 0x0026 |
| 75 | +CSIDL_PROGRAM_FILESX86 = 0x002a |
| 76 | + |
68 | 77 | IOCTL_PID = 0x222008
|
69 | 78 | IOCTL_CUCKOO_PATH = 0x22200C
|
70 | 79 | PATH_KERNEL_DRIVER = "\\\\.\\DriverSSDT"
|
@@ -94,6 +103,20 @@ def get_referrer_url(interest):
|
94 | 103 | return f"http://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd={itemidx}&ved={vedstr}&url={escapedurl}&ei={eistr}&usg={usgstr}"
|
95 | 104 |
|
96 | 105 |
|
| 106 | +def nt_path_to_dos_path_ansi(nt_path: str) -> str: |
| 107 | + drive_letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |
| 108 | + nt_path_bytes = nt_path.encode("utf-8", errors="ignore") |
| 109 | + for letter in drive_letters: |
| 110 | + drive = f"{letter}:" |
| 111 | + target = create_string_buffer(1024) |
| 112 | + res = KERNEL32.QueryDosDeviceA(drive.encode("ascii"), target, 1024) |
| 113 | + if res != 0: |
| 114 | + device_path = target.value |
| 115 | + if nt_path_bytes.startswith(device_path): |
| 116 | + converted = nt_path_bytes.replace(device_path, drive.encode("ascii"), 1) |
| 117 | + return converted.decode("utf-8", errors="ignore") |
| 118 | + return nt_path |
| 119 | + |
97 | 120 | def NT_SUCCESS(val):
|
98 | 121 | return val >= 0
|
99 | 122 |
|
@@ -228,6 +251,12 @@ def get_filepath(self):
|
228 | 251 |
|
229 | 252 | return ""
|
230 | 253 |
|
| 254 | + def get_folder_path(self, csidl): |
| 255 | + """Use SHGetFolderPathW to get the system folder path for a given CSIDL.""" |
| 256 | + buf = create_string_buffer(MAX_PATH) |
| 257 | + windll.shell32.SHGetFolderPathA(None, csidl, None, 0, buf) |
| 258 | + return buf.value.decode('utf-8', errors='ignore') |
| 259 | + |
231 | 260 | def get_image_name(self):
|
232 | 261 | """Get the image name; returns an empty string on error."""
|
233 | 262 | if not self.h_process:
|
@@ -278,6 +307,58 @@ def get_parent_pid(self):
|
278 | 307 |
|
279 | 308 | return None
|
280 | 309 |
|
| 310 | + def detect_dll_sideloading(self, directory_path: str) -> bool: |
| 311 | + """Detect potential DLL sideloading in the provided directory.""" |
| 312 | + try: |
| 313 | + directory = Path(directory_path) |
| 314 | + if not directory.is_dir(): |
| 315 | + return False |
| 316 | + |
| 317 | + if (directory/"capemon.dll").exists(): |
| 318 | + return False |
| 319 | + |
| 320 | + # Early exit if directory is a known system location |
| 321 | + try: |
| 322 | + system_dirs = { |
| 323 | + Path(self.get_folder_path(CSIDL_WINDOWS)).resolve(), |
| 324 | + Path(self.get_folder_path(CSIDL_SYSTEM)).resolve(), |
| 325 | + Path(self.get_folder_path(CSIDL_SYSTEMX86)).resolve(), |
| 326 | + Path(self.get_folder_path(CSIDL_PROGRAM_FILES)).resolve(), |
| 327 | + Path(self.get_folder_path(CSIDL_PROGRAM_FILESX86)).resolve() |
| 328 | + } |
| 329 | + if directory.resolve() in system_dirs: |
| 330 | + return False |
| 331 | + except (OSError, ArgumentError, ValueError) as e: |
| 332 | + log.warning("detect_dll_sideloading: failed to retrieve system paths: %s", e) |
| 333 | + return False |
| 334 | + |
| 335 | + try: |
| 336 | + local_dlls = {f.name.lower() for f in directory.glob("*.dll") if f.is_file()} |
| 337 | + if not local_dlls: |
| 338 | + return False |
| 339 | + except (OSError, PermissionError) as e: |
| 340 | + log.warning("detect_dll_sideloading: could not list DLLs in %s: %s", directory_path, e) |
| 341 | + return False |
| 342 | + |
| 343 | + # Build set of known system DLLs |
| 344 | + known_dlls = set() |
| 345 | + for sys_dir in system_dirs: |
| 346 | + try: |
| 347 | + if sys_dir.exists(): |
| 348 | + known_dlls.update(f.name.lower() for f in sys_dir.glob("*.dll") if f.is_file()) |
| 349 | + except (OSError, PermissionError) as e: |
| 350 | + log.debug("detect_dll_sideloading: skipping system dir %s: %s", sys_dir, e) |
| 351 | + |
| 352 | + suspicious = local_dlls & known_dlls |
| 353 | + if suspicious: |
| 354 | + for dll in suspicious: |
| 355 | + log.info("Potential dll side-loading detected in local directory: %s", dll) |
| 356 | + return bool(suspicious) |
| 357 | + |
| 358 | + except Exception as e: |
| 359 | + log.error("detect_dll_sideloading: unexpected error with path %s: %s", directory_path, e) |
| 360 | + return False |
| 361 | + |
281 | 362 | def kernel_analyze(self):
|
282 | 363 | """zer0m0n kernel analysis"""
|
283 | 364 | log.info("Starting kernel analysis")
|
@@ -535,7 +616,7 @@ def ttd_stop(self):
|
535 | 616 | if result.stderr:
|
536 | 617 | log.error(" ".join(result.stderr.split()))
|
537 | 618 |
|
538 |
| - log.info("Stopped TTD for %s process with pid %d: %s", bit_str, self.pid) |
| 619 | + log.info("Stopped TTD for %s process with pid %d", bit_str, self.pid) |
539 | 620 |
|
540 | 621 | return True
|
541 | 622 |
|
@@ -705,6 +786,12 @@ def inject(self, interest=None, nosleepskip=False):
|
705 | 786 |
|
706 | 787 | self.write_monitor_config(interest, nosleepskip)
|
707 | 788 |
|
| 789 | + path = os.path.dirname(nt_path_to_dos_path_ansi(self.get_filepath())) |
| 790 | + |
| 791 | + if self.detect_dll_sideloading(path) and self.has_msimg32(path): |
| 792 | + self.deploy_version_proxy(path) |
| 793 | + return True |
| 794 | + |
708 | 795 | log.info("%s DLL to inject is %s, loader %s", bit_str, dll, bin_name)
|
709 | 796 |
|
710 | 797 | try:
|
@@ -769,3 +856,41 @@ def __str__(self):
|
769 | 856 | """Get a string representation of this process."""
|
770 | 857 | image_name = self.get_image_name() or "???"
|
771 | 858 | return f"<{self.__class__.__name__} {self.pid} {image_name}>"
|
| 859 | + |
| 860 | + def has_msimg32(self, directory_path: str) -> bool: |
| 861 | + """Check if msimg32.dll exists in directory""" |
| 862 | + try: |
| 863 | + return any( |
| 864 | + f.name.lower() == "msimg32.dll" |
| 865 | + for f in Path(directory_path).glob("*") |
| 866 | + if f.is_file() |
| 867 | + ) |
| 868 | + except (OSError, PermissionError): |
| 869 | + return False |
| 870 | + |
| 871 | + def deploy_version_proxy(self, directory_path: str): |
| 872 | + """Deploy version.dll proxy loader""" |
| 873 | + if self.is_64bit(): |
| 874 | + dll = CAPEMON64_NAME |
| 875 | + side_dll = SIDELOADER64_NAME |
| 876 | + bit_str = "64-bit" |
| 877 | + else: |
| 878 | + dll = CAPEMON32_NAME |
| 879 | + side_dll = SIDELOADER32_NAME |
| 880 | + bit_str = "32-bit" |
| 881 | + |
| 882 | + dll = os.path.join(Path.cwd(), dll) |
| 883 | + |
| 884 | + if not os.path.exists(dll): |
| 885 | + log.warning("invalid path %s for monitor DLL to be sideloaded in %s, sideloading aborted", dll, self) |
| 886 | + return |
| 887 | + |
| 888 | + try: |
| 889 | + copy(dll, os.path.join(directory_path, "capemon.dll")) |
| 890 | + copy(side_dll, os.path.join(directory_path, "version.dll")) |
| 891 | + copy(os.path.join(Path.cwd(), "dll", f"{self.pid}.ini"), os.path.join(directory_path, "config.ini")) |
| 892 | + except OSError as e: |
| 893 | + log.error("Failed to copy DLL: %s", e) |
| 894 | + return |
| 895 | + log.info("%s DLL to sideload is %s, sideloader %s", bit_str, os.path.join(directory_path, "capemon.dll"), os.path.join(directory_path, "version.dll")) |
| 896 | + return |
0 commit comments