diff --git a/analyzer/windows/modules/packages/archive.py b/analyzer/windows/modules/packages/archive.py index 562a3511510..7b358032e24 100644 --- a/analyzer/windows/modules/packages/archive.py +++ b/analyzer/windows/modules/packages/archive.py @@ -187,6 +187,9 @@ def start(self, path): log.warning("Couldn't copy %s to root of C: %s", d, str(e)) file_name = self.options.get(OPT_FILE) + if file_name: + file_name = file_name.replace("/", "\\\\") + # If no file name is provided via option, discover files to execute. if not file_name: ret_list = [] diff --git a/analyzer/windows/modules/packages/rar.py b/analyzer/windows/modules/packages/rar.py index b3cff87d952..9e73d5f4b41 100644 --- a/analyzer/windows/modules/packages/rar.py +++ b/analyzer/windows/modules/packages/rar.py @@ -121,6 +121,9 @@ def start(self, path): self.extract_rar(path, root, password) file_name = self.options.get(OPT_FILE) + if file_name: + file_name = file_name.replace("/", "\\\\") + # If no file name is provided via option, take the first file. if not file_name: # If no file names to choose from, bail diff --git a/analyzer/windows/modules/packages/zip.py b/analyzer/windows/modules/packages/zip.py index f129dee37b4..ca0dc4af81b 100644 --- a/analyzer/windows/modules/packages/zip.py +++ b/analyzer/windows/modules/packages/zip.py @@ -93,6 +93,9 @@ def start(self, path): extract_archive(seven_zip_path, nested_7z, root, password, try_multiple_passwords) file_name = self.options.get(OPT_FILE) + if file_name: + file_name = file_name.replace("/", "\\\\") + # If no file name is provided via option, discover files to execute. if not file_name: # If no file names to choose from, bail diff --git a/analyzer/windows/modules/packages/zip_compound.py b/analyzer/windows/modules/packages/zip_compound.py index 50fc0fdf211..0364c764501 100644 --- a/analyzer/windows/modules/packages/zip_compound.py +++ b/analyzer/windows/modules/packages/zip_compound.py @@ -74,6 +74,9 @@ def process_unzipped_contents(self, unzipped_directory: str, json_filename: str) # Enforce the requirement of having a specified file. No guessing. target_file = target_file or self.options.get(OPT_FILE) + if target_file: + target_file = target_file.replace("/", "\\\\") + if not target_file: raise CuckooPackageError("File must be specified in the JSON or the web submission UI!") diff --git a/conf/default/processing.conf.default b/conf/default/processing.conf.default index 4e3df106fac..29e362db7bc 100644 --- a/conf/default/processing.conf.default +++ b/conf/default/processing.conf.default @@ -259,6 +259,8 @@ targetinfo = yes dropped = yes # Ex procdump standalone module procdump = yes +# Automatically rebuild and reconstruct .NET PE headers for dumped assemblies to allow clean dnSpy parsing +dotnet_rebuild = yes # Amount of text to carve from plaintext files (bytes) buffer = 8192 # Process files not bigger than value below in Mb. We saw that after 90Mb it has biggest delay diff --git a/lib/cuckoo/common/demux.py b/lib/cuckoo/common/demux.py index ff5109d2e75..0377fdd41b9 100644 --- a/lib/cuckoo/common/demux.py +++ b/lib/cuckoo/common/demux.py @@ -367,7 +367,8 @@ def demux_sflock( # Find interesting files (exe/dll) and submit the ORIGINAL archive # with instructions to run specifically those files. execs = find_payload_to_run(unpacked.filepaths) - submit_opts = [f"file={runable}" for runable in execs] + execs_fixed = [r.replace("/", "\\\\") for r in execs] + submit_opts = [f"file={runable}" for runable in execs_fixed] # returning empty retlist so it will use parent file return [], "", submit_opts @@ -388,7 +389,8 @@ def demux_sflock( extracted = _sf_children(current_child) path = extracted[0] if path: - submit_opts += [f"file={runable}" for runable in execs] + execs_fixed = [r.replace("/", "\\\\") for r in execs] + submit_opts += [f"file={runable}" for runable in execs_fixed] retlist.append(extracted) else: # It's just a single regular file (e.g., malware.exe inside a zip). diff --git a/lib/cuckoo/common/dotnet_utils.py b/lib/cuckoo/common/dotnet_utils.py index 7573707f745..f689c10f012 100644 --- a/lib/cuckoo/common/dotnet_utils.py +++ b/lib/cuckoo/common/dotnet_utils.py @@ -67,3 +67,65 @@ def dotnet_user_strings(file: str = False, data: bytes = False, dn_whitelisting: dn.close() return dn_strings + +import struct + +def rebuild_dotnet_pe(data: bytes) -> bytes: + """ + Rebuilds a .NET PE file dumped from memory (Memory Layout). + Fixes section alignments and attempts to restore missing .NET headers + by locating the BSJB metadata signature. + """ + try: + import pefile + except ImportError: + return data + + try: + pe = pefile.PE(data=data, fast_load=False) + except pefile.PEFormatError: + return data + + modified = bytearray(data) + + # 1. Align Sections (Memory to File Layout conversion) + # Match FileAlignment to SectionAlignment + pe.OPTIONAL_HEADER.FileAlignment = pe.OPTIONAL_HEADER.SectionAlignment + + # Update Section Headers + for section in pe.sections: + section.PointerToRawData = section.VirtualAddress + # SizeOfRawData should be VirtualSize aligned to FileAlignment + alignment_mask = pe.OPTIONAL_HEADER.FileAlignment - 1 + section.SizeOfRawData = (section.Misc_VirtualSize + alignment_mask) & ~alignment_mask + + # Overwrite physical bytes in header + struct.pack_into(" 0 and metastrings[0].isdigit() and int(metastrings[0]) == TYPE_STRING: + if len(metastrings) > 4: + type_string = metastrings[3] # Optimize to not load all if duplicated, it stores sha256 in file object + if processing_conf.CAPE.get("dotnet_rebuild", False) and category in ("procdump", "dropped"): + if ".NET" in type_string: + try: + import os + file_size_limit = processing_conf.CAPE.get("max_file_size", 90) * 1024 * 1024 + if os.path.getsize(file_path) < file_size_limit: + with open(file_path, "rb") as f_in: + pe_data = f_in.read() + + # Apply synthetic memory layout alignment and BSJB headers + if pe_data.startswith(b"MZ") and b"BSJB" in pe_data: + from lib.cuckoo.common.dotnet_utils import rebuild_dotnet_pe + rebuilt_data = rebuild_dotnet_pe(pe_data) + if rebuilt_data and rebuilt_data != pe_data: + with open(file_path, "wb") as f_out: + f_out.write(rebuilt_data) + log.debug("Successfully dynamically rebuilt .NET PE headers for %s", file_path) + except Exception as e: + log.error("Failed to execute .NET PE rebuilder on %s: %s", file_path, str(e)) + f = File(file_path, metadata.get("metadata", "")) sha256 = f.get_sha256() @@ -212,6 +237,10 @@ def process_file(self, file_path, append_file, metadata: dict, *, category: str, options_match = db_file.get("options_hash", "") == options_hash file_info = db_file cached = True + + # we still need append flag + type_string, append_file = self._metadata_processing(metadata, file_info, append_file) + if yara_match and options_match: run_static = False if HAVE_VIRUSTOTAL: