Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

219 lines
8.1 KiB
Python

#!/usr/bin/env python3
""".NET malware reverse engineering agent using subprocess wrappers for dnSpy/de4dot."""
import subprocess
import os
import sys
import re
import hashlib
import struct
def compute_hashes(filepath):
"""Compute hashes for sample identification."""
with open(filepath, "rb") as f:
data = f.read()
return {
"md5": hashlib.md5(data).hexdigest(),
"sha256": hashlib.sha256(data).hexdigest(),
"size": len(data),
}
def detect_dotnet_assembly(filepath):
"""Check if file is a .NET assembly by looking for CLI header."""
with open(filepath, "rb") as f:
data = f.read(512)
if data[:2] != b"MZ":
return {"is_dotnet": False, "reason": "Not a PE file"}
try:
pe_offset = struct.unpack_from("<I", data, 0x3C)[0]
if pe_offset + 4 > len(data):
return {"is_dotnet": False, "reason": "Invalid PE header"}
if data[pe_offset:pe_offset + 4] != b"PE\x00\x00":
return {"is_dotnet": False, "reason": "Invalid PE signature"}
opt_offset = pe_offset + 24
magic = struct.unpack_from("<H", data, opt_offset)[0]
if magic == 0x10B:
clr_offset = opt_offset + 208
elif magic == 0x20B:
clr_offset = opt_offset + 224
else:
return {"is_dotnet": False, "reason": "Unknown PE format"}
if clr_offset + 8 <= len(data):
clr_rva, clr_size = struct.unpack_from("<II", data, clr_offset)
if clr_rva > 0 and clr_size > 0:
return {"is_dotnet": True, "clr_header_rva": clr_rva, "clr_size": clr_size}
return {"is_dotnet": False, "reason": "No CLR header"}
except (struct.error, IndexError):
return {"is_dotnet": False, "reason": "Parse error"}
def detect_obfuscator(filepath):
"""Detect .NET obfuscator using Detect It Easy."""
try:
result = subprocess.run(
["diec", filepath], capture_output=True, text=True, timeout=30
)
output = result.stdout
obfuscators = {
"ConfuserEx": "confuser" in output.lower(),
"SmartAssembly": "smartassembly" in output.lower(),
".NET Reactor": "reactor" in output.lower(),
"Dotfuscator": "dotfuscator" in output.lower(),
"Babel": "babel" in output.lower(),
"Eazfuscator": "eazfuscator" in output.lower(),
"Crypto Obfuscator": "crypto" in output.lower() and "obfuscator" in output.lower(),
}
detected = [name for name, found in obfuscators.items() if found]
return {"detected": detected, "raw_output": output.strip()}
except FileNotFoundError:
return {"detected": [], "raw_output": "diec not installed"}
def deobfuscate_with_de4dot(filepath, output_path):
"""Run de4dot to deobfuscate .NET assembly."""
try:
result = subprocess.run(
["de4dot", filepath, "-o", output_path],
capture_output=True, text=True, timeout=120
)
return {
"success": result.returncode == 0,
"output_path": output_path,
"stdout": result.stdout[-500:] if result.stdout else "",
}
except FileNotFoundError:
return {"success": False, "error": "de4dot not installed"}
def extract_strings(filepath, min_length=8):
"""Extract strings and classify for IOCs."""
with open(filepath, "rb") as f:
data = f.read()
unicode_strings = re.findall(
rb"(?:[\x20-\x7e]\x00){%d,}" % min_length, data
)
ascii_strings = re.findall(
rb"[\x20-\x7e]{%d,}" % min_length, data
)
all_strings = set()
for s in ascii_strings:
all_strings.add(s.decode("ascii", errors="ignore"))
for s in unicode_strings:
all_strings.add(s.decode("utf-16-le", errors="ignore"))
indicators = {
"urls": [], "ips": [], "emails": [],
"registry_keys": [], "file_paths": [],
"base64_strings": [], "suspicious_strings": [],
}
suspicious_keywords = [
"keylog", "screenshot", "clipboard", "password", "credential",
"smtp", "telegram", "discord", "webhook", "stealer",
"inject", "hook", "persist", "startup",
]
for s in all_strings:
if re.search(r"https?://", s):
indicators["urls"].append(s)
if re.search(r"\b(\d{1,3}\.){3}\d{1,3}\b", s):
indicators["ips"].append(s)
if re.search(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", s):
indicators["emails"].append(s)
if re.search(r"HKLM|HKCU|SOFTWARE\\", s, re.IGNORECASE):
indicators["registry_keys"].append(s)
if re.search(r"[A-Za-z0-9+/]{40,}={0,2}$", s):
indicators["base64_strings"].append(s[:100])
for kw in suspicious_keywords:
if kw in s.lower():
indicators["suspicious_strings"].append(s[:100])
break
for key in indicators:
indicators[key] = list(set(indicators[key]))[:20]
return indicators
def analyze_dotnet_metadata(filepath):
"""Extract .NET metadata using monodis or ilspy CLI if available."""
metadata = {}
try:
result = subprocess.run(
["monodis", "--assembly", filepath],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
metadata["assembly_info"] = result.stdout.strip()
except FileNotFoundError:
pass
try:
result = subprocess.run(
["monodis", "--typedef", filepath],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
types = re.findall(r"(\S+)\s+flags", result.stdout)
metadata["type_count"] = len(types)
metadata["types"] = types[:30]
except FileNotFoundError:
pass
try:
result = subprocess.run(
["monodis", "--method", filepath],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
methods = re.findall(r"(\S+)\s+\(", result.stdout)
metadata["method_count"] = len(methods)
except FileNotFoundError:
pass
return metadata
def analyze_dotnet_malware(filepath, output_dir="/tmp/dotnet_analysis"):
"""Full .NET malware analysis pipeline."""
os.makedirs(output_dir, exist_ok=True)
report = {"file": filepath}
report["hashes"] = compute_hashes(filepath)
report["dotnet_check"] = detect_dotnet_assembly(filepath)
if not report["dotnet_check"].get("is_dotnet"):
report["error"] = "Not a .NET assembly"
return report
report["obfuscator"] = detect_obfuscator(filepath)
deobf_path = os.path.join(output_dir, "deobfuscated.exe")
report["deobfuscation"] = deobfuscate_with_de4dot(filepath, deobf_path)
analysis_target = deobf_path if report["deobfuscation"].get("success") else filepath
report["strings"] = extract_strings(analysis_target)
report["metadata"] = analyze_dotnet_metadata(analysis_target)
return report
def print_report(report):
print(".NET Malware Analysis Report")
print("=" * 50)
print(f"File: {report['file']}")
print(f"SHA-256: {report['hashes']['sha256']}")
print(f".NET Assembly: {report['dotnet_check'].get('is_dotnet', False)}")
obf = report.get("obfuscator", {})
if obf.get("detected"):
print(f"Obfuscator: {', '.join(obf['detected'])}")
deobf = report.get("deobfuscation", {})
print(f"Deobfuscation: {'Success' if deobf.get('success') else 'Failed/Skipped'}")
meta = report.get("metadata", {})
if meta:
print(f"Types: {meta.get('type_count', 'N/A')}, Methods: {meta.get('method_count', 'N/A')}")
strings = report.get("strings", {})
if strings:
print("\nExtracted Indicators:")
for cat, values in strings.items():
if values:
print(f" {cat}: {len(values)}")
for v in values[:3]:
print(f" - {v[:80]}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python agent.py <dotnet_executable>")
sys.exit(1)
result = analyze_dotnet_malware(sys.argv[1])
print_report(result)