Last active
February 4, 2026 10:25
-
-
Save stevehansen/b75865b836000d2639486dca8daa0f4c to your computer and use it in GitHub Desktop.
Chrysalis IOC Scanner
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env dotnet run | |
| // ============================================================================= | |
| // Chrysalis IOC Scanner | |
| // Scans for known Indicators of Compromise (file hashes, services, registry, network) | |
| // ============================================================================= | |
| // | |
| // REQUIREMENTS: .NET 10 SDK (https://dot.net/download) | |
| // | |
| // RUN DIRECTLY FROM GIST: | |
| // dotnet run https://gist.githubusercontent.com/stevehansen/b75865b836000d2639486dca8daa0f4c/raw/IOCScanner.cs | |
| // | |
| // OR DOWNLOAD AND RUN: | |
| // curl -O https://gist.githubusercontent.com/stevehansen/b75865b836000d2639486dca8daa0f4c/raw/IOCScanner.cs | |
| // dotnet run IOCScanner.cs | |
| // | |
| // SCAN SPECIFIC PATHS: | |
| // dotnet run IOCScanner.cs C:\ D:\Temp | |
| // dotnet run IOCScanner.cs /home /var/tmp | |
| // | |
| // EXIT CODES: | |
| // 0 = No threats detected | |
| // 1 = Threats detected | |
| // 2 = Scan cancelled by user | |
| // | |
| // ============================================================================= | |
| #:package Spectre.Console@0.50.* | |
| using System.Net.NetworkInformation; | |
| using System.Runtime.InteropServices; | |
| using System.Security.Cryptography; | |
| using Microsoft.Win32; | |
| using Spectre.Console; | |
| // --- IOC DEFINITIONS --- | |
| var targetIOCs = new Dictionary<string, string> | |
| { | |
| ["update.exe"] = "a511be5164dc1122fb5a7daa3eef9467e43d8458425b15a640235796006590c9", | |
| ["update.exe_sha1_1"] = "8e6e505438c21f3d281e1cc257abdbf7223b7f5a", | |
| ["update.exe_sha1_2"] = "90e677d7ff5844407b9c073e3b7e896e078e11cd", | |
| ["update.exe_sha1_3"] = "573549869e84544e3ef253bdba79851dcde4963a", | |
| ["update.exe_sha1_4"] = "13179c8f19fbf3d8473c49983a199e6cb4f318f0", | |
| ["update.exe_sha1_5"] = "4c9aac447bf732acc97992290aa7a187b967ee2c", | |
| ["update.exe_sha1_6"] = "821c0cafb2aab0f063ef7e313f64313fc81d46cd", | |
| ["load"] = "06a6a5a39193075734a32e0235bde0e979c27228", | |
| ["load_2"] = "9c3ba38890ed984a25abb6a094b5dbf052f22fa7", | |
| ["alien.ini"] = "ca4b6fe0c69472cd3d63b212eb805b7f65710d33", | |
| ["alien.ini_alt1"] = "0d0f315fd8cf408a483f8e2dd1e69422629ed9fd", | |
| ["alien.ini_alt2"] = "2a476cfb85fbf012fdbe63a37642c11afa5cf020", | |
| ["BluetoothService.exe"] = "2da00de67720f5f13b17e9d985fe70f10f153da60c9ab1086fe58f069a156924", | |
| ["BluetoothService"] = "77bfea78def679aa1117f569a35e8fd1542df21f7e00e27f192c907e61d63a2e", | |
| ["log.dll"] = "3bdc4c0637591533f1d4198a72a33426c01f69bd2e15ceee547866f65e26b7ad", | |
| ["u.bat"] = "9276594e73cda1c69b7d265b3f08dc8fa84bf2d6599086b9acc0bb3745146600", | |
| ["conf.c"] = "f4d829739f2d6ba7e3ede83dad428a0ced1a703ec582fc73a4eee3df3704629a", | |
| ["libtcc.dll"] = "4a52570eeaf9d27722377865df312e295a7a23c3b6eb991944c2ecd707cc9906", | |
| ["admin"] = "831e1ea13a1bd405f5bda2b9d8f2265f7b1db6c668dd2165ccc8a9c4c15ea7dd", | |
| ["loader1"] = "0a9b8df968df41920b6ff07785cbfebe8bda29e6b512c94a3b2a83d10014d2fd", | |
| ["loader2"] = "e7cd605568c38bd6e0aba31045e1633205d0598c607a855e2e1bca4cca1c6eda", | |
| ["system"] = "7add554a98d3a99b319f2127688356c1283ed073a084805f14e33b4f6a6126fd", | |
| ["s047t5g.exe"] = "fcc2765305bcd213b7558025b2039df2265c3e0b6401e4833123c461df2de51a", | |
| ["[NSIS.nsi]"] = "8ea8b83645fba6e23d48075a0d3fc73ad2ba515b4536710cda4f1f232718f53e", | |
| ["ConsoleApplication2.exe"] = "b4169a831292e245ebdffedd5820584d73b129411546e7d3eccf4663d5fc5be3", | |
| ["uffhxpSy"] = "4c2ea8193f4a5db63b897a2d3ce127cc5d89687f380b97a1d91e0c8db542e4f8", | |
| ["3yzr31vk"] = "078a9e5c6c787e5532a7e728720cbafee9021bfec4a30e3c2be110748d7c43c5", | |
| ["install.exe"] = "951792130", | |
| ["AutoUpdater.exe"] = "951792130" | |
| }; | |
| var maliciousIPs = new[] { "45.76.155.202", "45.32.144.255", "95.179.213.0", "45.77.31.210", "59.110.7.32", "124.222.137.114" }; | |
| var maliciousDomains = new[] { "self-dns.it.com", "cdncheck.it.com", "safe-dns.it.com", "api.skycloudcenter.com", "api.wiresguard.com", "update.notepads.top" }; | |
| var maliciousIdentifiers = new[] { "BluetoothService", "UpdateService", "WindowsUpdater" }; | |
| // --- PRE-SCAN INFO & CONFIRMATION --- | |
| var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); | |
| var isElevated = IsRunningElevated(); | |
| var scanPaths = args.Length > 0 ? args : GetDefaultScanPaths(); | |
| var uniqueFileNames = targetIOCs.Keys | |
| .Select(k => k.Split('_')[0]) // Remove suffixes like _sha1_1, _alt1 | |
| .Distinct() | |
| .ToArray(); | |
| AnsiConsole.Write(new FigletText("IOC Scanner").Color(Color.Cyan1)); | |
| AnsiConsole.Write(new Rule("[bold]Chrysalis IOC Scanner - Pre-Scan Summary[/]").RuleStyle("cyan")); | |
| AnsiConsole.WriteLine(); | |
| // Platform & elevation status | |
| var platformPanel = new Panel( | |
| new Markup(isWindows | |
| ? $"[bold]Platform:[/] Windows\n[bold]Elevated:[/] {(isElevated ? "[green]Yes (Administrator)[/]" : "[yellow]No (Limited access)[/]")}" | |
| : $"[bold]Platform:[/] {RuntimeInformation.OSDescription}\n[bold]Elevated:[/] {(isElevated ? "[green]Yes (root)[/]" : "[yellow]No (Limited access)[/]")}") | |
| ).Header("[bold]System Info[/]").Border(BoxBorder.Rounded); | |
| AnsiConsole.Write(platformPanel); | |
| AnsiConsole.WriteLine(); | |
| // Elevation warning | |
| if (!isElevated) | |
| { | |
| AnsiConsole.Write(new Panel( | |
| new Markup(isWindows | |
| ? "[yellow]⚠ Running without Administrator privileges.\nSome registry keys, services, and protected files may not be accessible.\nFor a complete scan, right-click and select 'Run as Administrator'.[/]" | |
| : "[yellow]⚠ Running without root privileges.\nSome system directories and files may not be accessible.\nFor a complete scan, run with: sudo dotnet run IOCScanner.cs[/]") | |
| ).Header("[bold yellow]Warning[/]").Border(BoxBorder.Rounded).BorderColor(Color.Yellow)); | |
| AnsiConsole.WriteLine(); | |
| } | |
| // Actions table | |
| var actionsTable = new Table() | |
| .Border(TableBorder.Rounded) | |
| .AddColumn("[bold]Action[/]") | |
| .AddColumn("[bold]What it reads[/]") | |
| .AddColumn("[bold]Why[/]"); | |
| if (isWindows) | |
| { | |
| actionsTable.AddRow( | |
| "[cyan]Registry Check[/]", | |
| "HKCU & HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run\nHKLM\\SYSTEM\\CurrentControlSet\\Services\\{name}", | |
| $"Detect persistence mechanisms for: {string.Join(", ", maliciousIdentifiers)}" | |
| ); | |
| actionsTable.AddRow( | |
| "[cyan]Network Check[/]", | |
| "Active TCP connections (via .NET API)\nDNS cache (via 'ipconfig /displaydns')", | |
| $"Detect active C2 connections to {maliciousIPs.Length} known malicious IPs\nand DNS lookups to {maliciousDomains.Length} malicious domains" | |
| ); | |
| } | |
| actionsTable.AddRow( | |
| "[cyan]Filesystem Scan[/]", | |
| $"Paths: {string.Join(", ", scanPaths)}", | |
| $"Scan all files, compute SHA256/SHA1 hash for {uniqueFileNames.Length} suspicious filenames:\n{string.Join(", ", uniqueFileNames.Take(10))}{(uniqueFileNames.Length > 10 ? "..." : "")}" | |
| ); | |
| AnsiConsole.Write(new Panel(actionsTable).Header("[bold]Planned Actions (READ-ONLY)[/]").Border(BoxBorder.Rounded)); | |
| AnsiConsole.WriteLine(); | |
| // IOC summary | |
| AnsiConsole.MarkupLine($"[dim]IOC Database: {targetIOCs.Count} hashes, {maliciousIPs.Length} IPs, {maliciousDomains.Length} domains, {maliciousIdentifiers.Length} service identifiers[/]"); | |
| AnsiConsole.WriteLine(); | |
| // Confirmation | |
| if (!AnsiConsole.Confirm("Proceed with scan?", defaultValue: true)) | |
| { | |
| AnsiConsole.MarkupLine("[yellow]Scan cancelled by user.[/]"); | |
| return 2; | |
| } | |
| AnsiConsole.WriteLine(); | |
| AnsiConsole.Write(new Rule("[bold cyan on darkblue] STARTING FULL CHRYSALIS IOC SCAN [/]").RuleStyle("cyan")); | |
| AnsiConsole.WriteLine(); | |
| var totalMatches = 0; | |
| // Only run Windows-specific checks on Windows | |
| if (isWindows) | |
| { | |
| totalMatches += ScanPersistence(); | |
| totalMatches += ScanNetwork(); | |
| } | |
| else | |
| { | |
| AnsiConsole.MarkupLine("[yellow]⚠ Skipping Windows-specific persistence and network checks (not running on Windows)[/]"); | |
| AnsiConsole.WriteLine(); | |
| } | |
| // Filesystem scan works on all platforms | |
| totalMatches += ScanFilesystem(scanPaths); | |
| // --- RESULTS --- | |
| AnsiConsole.WriteLine(); | |
| var resultColor = totalMatches > 0 ? "red" : "green"; | |
| var resultText = totalMatches > 0 ? $"⚠ THREATS DETECTED: {totalMatches}" : "✓ NO THREATS DETECTED"; | |
| AnsiConsole.Write(new Rule($"[bold {resultColor}] SCAN COMPLETE - {resultText} [/]").RuleStyle("cyan")); | |
| return totalMatches > 0 ? 1 : 0; | |
| // --- FUNCTIONS --- | |
| bool IsRunningElevated() | |
| { | |
| if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | |
| { | |
| using var identity = System.Security.Principal.WindowsIdentity.GetCurrent(); | |
| var principal = new System.Security.Principal.WindowsPrincipal(identity); | |
| return principal.IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator); | |
| } | |
| else | |
| { | |
| // Unix: check if running as root (UID 0) | |
| try | |
| { | |
| return Environment.GetEnvironmentVariable("EUID") == "0" | |
| || Environment.GetEnvironmentVariable("UID") == "0" | |
| || (Environment.UserName == "root"); | |
| } | |
| catch | |
| { | |
| return false; | |
| } | |
| } | |
| } | |
| string[] GetDefaultScanPaths() | |
| { | |
| if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | |
| { | |
| return DriveInfo.GetDrives() | |
| .Where(d => d.IsReady && d.DriveType is DriveType.Fixed or DriveType.Removable or DriveType.Network) | |
| .Select(d => d.RootDirectory.FullName) | |
| .ToArray(); | |
| } | |
| // Linux/macOS - scan common paths | |
| return ["/home", "/tmp", "/var", "/opt"]; | |
| } | |
| int ScanPersistence() | |
| { | |
| var matches = 0; | |
| AnsiConsole.MarkupLine("[cyan]▶ Scanning Persistence (Registry & Services)...[/]"); | |
| foreach (var id in maliciousIdentifiers) | |
| { | |
| // Service check via registry | |
| var serviceExists = CheckServiceExists(id); | |
| if (serviceExists == true) | |
| { | |
| AnsiConsole.MarkupLine($" [bold red on yellow]⚠ SERVICE MATCH: '{id}' installed![/]"); | |
| matches++; | |
| } | |
| else if (serviceExists is null) | |
| { | |
| AnsiConsole.MarkupLine($" [yellow]⚠ Access denied checking service: {id} (run as Administrator)[/]"); | |
| } | |
| // Registry checks | |
| var registryPaths = new[] | |
| { | |
| @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", | |
| @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run" | |
| }; | |
| foreach (var regPath in registryPaths) | |
| { | |
| foreach (var hive in new[] { Registry.CurrentUser, Registry.LocalMachine }) | |
| { | |
| var hiveName = hive == Registry.CurrentUser ? "HKCU" : "HKLM"; | |
| try | |
| { | |
| using var key = hive.OpenSubKey(regPath); | |
| if (key is null) | |
| continue; // Key doesn't exist, that's fine | |
| if (key.GetValue(id) is not null) | |
| { | |
| AnsiConsole.MarkupLine($" [bold red on yellow]⚠ REGISTRY MATCH: '{id}' in {hiveName}\\{regPath}[/]"); | |
| matches++; | |
| } | |
| } | |
| catch (System.Security.SecurityException) | |
| { | |
| AnsiConsole.MarkupLine($" [yellow]⚠ Access denied: {hiveName}\\{regPath} (run as Administrator)[/]"); | |
| } | |
| } | |
| } | |
| } | |
| if (matches == 0) | |
| AnsiConsole.MarkupLine(" [green]✓ No persistence mechanisms found[/]"); | |
| AnsiConsole.WriteLine(); | |
| return matches; | |
| } | |
| bool? CheckServiceExists(string serviceName) | |
| { | |
| try | |
| { | |
| using var key = Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Services\{serviceName}"); | |
| return key is not null; | |
| } | |
| catch (System.Security.SecurityException) | |
| { | |
| return null; // Access denied | |
| } | |
| } | |
| int ScanNetwork() | |
| { | |
| var matches = 0; | |
| AnsiConsole.MarkupLine("[cyan]▶ Scanning Network & DNS Cache...[/]"); | |
| // Check active connections | |
| try | |
| { | |
| var connections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections(); | |
| foreach (var ip in maliciousIPs) | |
| { | |
| var match = connections.FirstOrDefault(c => c.RemoteEndPoint.Address.ToString() == ip); | |
| if (match is not null) | |
| { | |
| AnsiConsole.MarkupLine($" [bold red on yellow]⚠ ACTIVE C2 CONNECTION: To {ip}[/]"); | |
| matches++; | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| AnsiConsole.MarkupLine($" [yellow]⚠ Could not check network connections: {ex.Message}[/]"); | |
| } | |
| // DNS cache check - Windows only via ipconfig /displaydns | |
| try | |
| { | |
| var psi = new System.Diagnostics.ProcessStartInfo("ipconfig", "/displaydns") | |
| { | |
| RedirectStandardOutput = true, | |
| UseShellExecute = false, | |
| CreateNoWindow = true | |
| }; | |
| using var process = System.Diagnostics.Process.Start(psi); | |
| if (process is not null) | |
| { | |
| var output = process.StandardOutput.ReadToEnd(); | |
| process.WaitForExit(); | |
| foreach (var domain in maliciousDomains) | |
| { | |
| if (output.Contains(domain, StringComparison.OrdinalIgnoreCase)) | |
| { | |
| AnsiConsole.MarkupLine($" [bold red on yellow]⚠ DNS CACHE TRACE: Found {domain}[/]"); | |
| matches++; | |
| } | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| AnsiConsole.MarkupLine($" [yellow]⚠ Could not check DNS cache: {ex.Message}[/]"); | |
| } | |
| if (matches == 0) | |
| AnsiConsole.MarkupLine(" [green]✓ No malicious network activity found[/]"); | |
| AnsiConsole.WriteLine(); | |
| return matches; | |
| } | |
| int ScanFilesystem(string[] scanPaths) | |
| { | |
| var matches = 0; | |
| AnsiConsole.MarkupLine("[cyan]▶ Scanning Filesystem...[/]"); | |
| AnsiConsole.WriteLine(); | |
| foreach (var rootPath in scanPaths) | |
| { | |
| if (!Directory.Exists(rootPath)) | |
| { | |
| AnsiConsole.MarkupLine($" [yellow]⚠ Path not accessible: {rootPath}[/]"); | |
| continue; | |
| } | |
| AnsiConsole.MarkupLine($"[white on grey]▶ Scanning: {rootPath}[/]"); | |
| var queue = new Queue<string>(); | |
| queue.Enqueue(rootPath); | |
| var filesScanned = 0; | |
| var pathMatches = 0; | |
| var accessDeniedDirs = 0; | |
| var accessDeniedFiles = 0; | |
| AnsiConsole.Status() | |
| .Spinner(Spinner.Known.Dots) | |
| .Start($"Scanning {rootPath}...", ctx => | |
| { | |
| while (queue.Count > 0) | |
| { | |
| var currentFolder = queue.Dequeue(); | |
| try | |
| { | |
| foreach (var filePath in Directory.GetFiles(currentFolder)) | |
| { | |
| filesScanned++; | |
| if (filesScanned % 1000 == 0) | |
| ctx.Status($"Scanned {filesScanned:N0} files in {rootPath}..."); | |
| var fileName = Path.GetFileName(filePath); | |
| var matchKeys = targetIOCs.Keys | |
| .Where(k => k.Equals(fileName, StringComparison.OrdinalIgnoreCase) || | |
| k.StartsWith(fileName, StringComparison.OrdinalIgnoreCase)) | |
| .ToList(); | |
| if (matchKeys.Count > 0) | |
| { | |
| ctx.Status($"Checking hash: {filePath}"); | |
| var maliciousFound = false; | |
| var couldNotRead = false; | |
| foreach (var key in matchKeys) | |
| { | |
| var expectedHash = targetIOCs[key]; | |
| // Skip dummy/name-only entries | |
| if (expectedHash == "951792130") | |
| continue; | |
| try | |
| { | |
| using var stream = File.OpenRead(filePath); | |
| var actualHash = expectedHash.Length == 40 | |
| ? ComputeSha1(stream) | |
| : ComputeSha256(stream); | |
| if (actualHash.Equals(expectedHash, StringComparison.OrdinalIgnoreCase)) | |
| { | |
| maliciousFound = true; | |
| break; | |
| } | |
| } | |
| catch (UnauthorizedAccessException) | |
| { | |
| couldNotRead = true; | |
| accessDeniedFiles++; | |
| } | |
| catch (IOException) | |
| { | |
| couldNotRead = true; // File locked | |
| } | |
| } | |
| // Only output if there's something worth reporting | |
| if (maliciousFound) | |
| { | |
| AnsiConsole.MarkupLine($" [bold red on yellow]⚠ MALWARE CONFIRMED: {Markup.Escape(filePath)}[/]"); | |
| pathMatches++; | |
| } | |
| else if (couldNotRead) | |
| { | |
| AnsiConsole.MarkupLine($" [yellow]? Suspicious filename, but could not verify: {Markup.Escape(filePath)}[/]"); | |
| } | |
| // Hash clean = no output, just continue | |
| } | |
| } | |
| foreach (var dir in Directory.GetDirectories(currentFolder)) | |
| { | |
| queue.Enqueue(dir); | |
| } | |
| } | |
| catch (UnauthorizedAccessException) | |
| { | |
| accessDeniedDirs++; | |
| } | |
| catch (IOException) { /* Other I/O error, skip */ } | |
| } | |
| }); | |
| matches += pathMatches; | |
| var summary = $"Scanned {filesScanned:N0} files, found {pathMatches} matches"; | |
| if (accessDeniedDirs > 0 || accessDeniedFiles > 0) | |
| summary += $" [yellow](skipped: {accessDeniedDirs} dirs, {accessDeniedFiles} files - access denied)[/]"; | |
| AnsiConsole.MarkupLine($" [dim]{summary}[/]"); | |
| AnsiConsole.WriteLine(); | |
| } | |
| return matches; | |
| } | |
| string ComputeSha256(Stream stream) | |
| { | |
| var hash = SHA256.HashData(stream); | |
| return Convert.ToHexStringLower(hash); | |
| } | |
| string ComputeSha1(Stream stream) | |
| { | |
| var hash = SHA1.HashData(stream); | |
| return Convert.ToHexStringLower(hash); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment