diff --git a/analyzer/windows/dll/capemon.dll b/analyzer/windows/dll/capemon.dll index 28d09c5739e..adacef76cb9 100755 Binary files a/analyzer/windows/dll/capemon.dll and b/analyzer/windows/dll/capemon.dll differ diff --git a/analyzer/windows/dll/capemon_x64.dll b/analyzer/windows/dll/capemon_x64.dll index 51b61a1c0af..056b53b6046 100755 Binary files a/analyzer/windows/dll/capemon_x64.dll and b/analyzer/windows/dll/capemon_x64.dll differ diff --git a/analyzer/windows/lib/common/constants.py b/analyzer/windows/lib/common/constants.py index ec0e9811496..9f98c72bed7 100644 --- a/analyzer/windows/lib/common/constants.py +++ b/analyzer/windows/lib/common/constants.py @@ -51,8 +51,9 @@ OPT_SERVICEDESC = "servicedesc" OPT_RUNASX86 = "runasx86" OPT_UNPACKER = "unpacker" +OPT_RECURSION_DEPTH = "recursion_depth" -ARCHIVE_OPTIONS = (OPT_FILE, OPT_PASSWORD) +ARCHIVE_OPTIONS = (OPT_FILE, OPT_PASSWORD, OPT_RECURSION_DEPTH) DLL_OPTIONS = (OPT_ARGUMENTS, OPT_DLLLOADER, OPT_FUNCTION) SERVICE_OPTIONS = (OPT_SERVICENAME, OPT_SERVICEDESC, OPT_ARGUMENTS) diff --git a/analyzer/windows/lib/common/zip_utils.py b/analyzer/windows/lib/common/zip_utils.py index a8343d23a11..a75c3314054 100644 --- a/analyzer/windows/lib/common/zip_utils.py +++ b/analyzer/windows/lib/common/zip_utils.py @@ -52,6 +52,7 @@ def extract_archive(seven_zip_path, archive_path, extract_path, password="infect stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, stdout=subprocess.PIPE, + check=False ) stdoutput, stderr = p.stdout, p.stderr log.debug("%s %s", p.stdout, p.stderr) @@ -76,6 +77,7 @@ def extract_archive(seven_zip_path, archive_path, extract_path, password="infect stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, stdout=subprocess.PIPE, + check=False ) stdoutput, stderr = p.stdout, p.stderr log.debug("%s - %s", p.stdout, p.stderr) @@ -107,9 +109,11 @@ def get_file_names(seven_zip_path, archive_path): stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, stdout=subprocess.PIPE, + encoding="utf-8", + errors="replace", + check=False ) - stdoutput = p.stdout.decode("utf-8", errors="replace") - stdoutput_lines = stdoutput.splitlines() + stdoutput_lines = p.stdout.splitlines() in_table = False items_under_header = False file_names = [] @@ -264,6 +268,7 @@ def winrar_extractor(winrar_binary, extract_path, archive_path): stdin=subprocess.DEVNULL, stderr=subprocess.PIPE, stdout=subprocess.PIPE, + check=False ) # stdoutput, stderr = p.stdout, p.stderr log.debug("%s - %s", p.stdout, p.stderr) diff --git a/analyzer/windows/modules/packages/archive.py b/analyzer/windows/modules/packages/archive.py index 0f046b2ae9d..57206b36b94 100644 --- a/analyzer/windows/modules/packages/archive.py +++ b/analyzer/windows/modules/packages/archive.py @@ -5,6 +5,7 @@ import logging import os import shutil +import subprocess from pathlib import Path from lib.common.abstracts import Package @@ -16,6 +17,7 @@ OPT_FUNCTION, OPT_MULTI_PASSWORD, OPT_PASSWORD, + OPT_RECURSION_DEPTH, ) from lib.common.exceptions import CuckooPackageError from lib.common.zip_utils import ( @@ -44,6 +46,7 @@ class Archive(Package): ("SystemRoot", "system32", "xpsrchvw.exe"), ("ProgramFiles", "7-Zip", "7z.exe"), ("ProgramFiles", "WinRAR", "WinRAR.exe"), + ("ProgramFiles", "die", "diec.exe"), ("ProgramFiles", "Microsoft Office", "WINWORD.EXE"), ("ProgramFiles", "Microsoft Office", "Office*", "WINWORD.EXE"), ("ProgramFiles", "Microsoft Office*", "root", "Office*", "WINWORD.EXE"), @@ -65,6 +68,8 @@ class Archive(Package): Various options apply depending on the file type. The options '{OPT_FUNCTION}' and '{OPT_DLLLOADER}' will be applied to .DLL execution attempts. The option '{OPT_ARGUMENTS}' will be applied to a .DLL or a PE executable. + For recursive extraction guest Windows VM must contain die app (Detect It Easy) with extra + database in Program Files. """ option_names = sorted(set(DLL_OPTIONS + ARCHIVE_OPTIONS + (OPT_MULTI_PASSWORD,))) @@ -76,6 +81,8 @@ def start(self, path): seven_zip_path = self.get_path_app_in_path("7z.exe") password = self.options.get(OPT_PASSWORD, "infected") archive_name = Path(path).name + recursion_depth = max(0, int(self.options.get(OPT_RECURSION_DEPTH, 0))) + diec_path = self.get_path_app_in_path("diec.exe") if recursion_depth > 0 else None # We are extracting the archive to C:\\ rather than the TEMP directory because # actors are using LNK files that use relative directory traversal at arbitrary depth. @@ -102,15 +109,63 @@ def start(self, path): if not file_names: raise CuckooPackageError("Empty archive") + extracted_files = set() + extracted_archives = set() + for i in range(0, recursion_depth): + + packs = [] + target_words = {'Archive', 'Image', 'filesystem', 'ZIP', 'RAR', 'SFX'} + + for r, _, files in os.walk(root): + + for file in files: + file_path = os.path.join(r, file) + if file_path == path or file_path in extracted_files: + continue + + extracted_files.add(file_path) + + try: + result = subprocess.run([diec_path, "-p", file_path], capture_output=True, text=True, check=True, encoding="utf-8") + file_info = result.stdout + for word in target_words: + if word in file_info: + packs.append(file_path) + break + except subprocess.CalledProcessError: + continue + + if packs: + j = 0 + for p in packs: + pack_name = os.path.basename(p) + output_dir = os.path.join(root, str(i), str(j), pack_name) + os.makedirs(output_dir, exist_ok=True) + + try: + try_multiple_passwords = attempt_multiple_passwords(self.options, password) + extract_archive(seven_zip_path, p, output_dir, password, try_multiple_passwords) + extracted_archives.add(p) + except Exception as e: + log.warning("Extraction failed for %s: %s", p, e) + + j += 1 + else: + break + # Handle special characters that 7ZIP cannot # We have the file names according to 7ZIP output (file_names) # We have the file names that were actually extracted (files at root) # If these values are different, replace all - files_at_root = [os.path.join(r, f).replace(f"{root}\\", "") for r, _, files in os.walk(root) for f in files] + log.warning("Extracted archives:%s", extracted_archives) + files_at_root = [ + os.path.relpath(os.path.join(r, f), root) + for r, _, files in os.walk(root) + for f in files + if os.path.join(r, f) != path and os.path.join(r, f) not in extracted_archives + ] log.debug(files_at_root) - if set(file_names) != set(files_at_root): - log.debug("Replacing %s with %s", str(file_names), str(files_at_root)) - file_names = files_at_root + file_names = files_at_root upload_extracted_files(root, files_at_root) diff --git a/changelog.md b/changelog.md index 58dbe38be65..a2e6a1cf040 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,9 @@ +### [05.06.2026] +* Monitor updates: + * NtCreateUserProcess hook: Dynamically patch ping commandline to thwart ping delays (e.g. Formbook/Xloader) + * Debugger: Persistent software breakpoints via softbpmode=1 (default is one-shot) + * TLS capture improvements + ### [01.06.2026] * Monitor update: Fix standalone mode broken since August diff --git a/tests/web/test_submission_views.py b/tests/web/test_submission_views.py index 54f2a40fef1..2322d41033a 100644 --- a/tests/web/test_submission_views.py +++ b/tests/web/test_submission_views.py @@ -181,7 +181,7 @@ def test_get_lib_common_constants(self): actual = get_lib_common_constants(platform="windows") self.assertIsInstance(actual, dict) self.assertEqual("runasx86", actual["OPT_RUNASX86"]) - self.assertCountEqual(("file", "password"), actual["ARCHIVE_OPTIONS"]) + self.assertCountEqual(("file", "password", "recursion_depth"), actual["ARCHIVE_OPTIONS"]) self.assertCountEqual(("arguments", "dllloader", "function"), actual["DLL_OPTIONS"]) self.assertIn("SystemDrive", actual["TRUSTED_PATH_TEXT"]) self.assertIn("SystemDrive", actual["MSOFFICE_TRUSTED_PATH"]) diff --git a/web/templates/submission/index.html b/web/templates/submission/index.html index c0a4a188f22..a25ee730f45 100644 --- a/web/templates/submission/index.html +++ b/web/templates/submission/index.html @@ -616,6 +616,10 @@
Advance startup-time MS since system startup + + recursion_depth + Depth of archive extraction (nested archives) +