Files
2026-05-06 09:11:30 +10:00

908 lines
38 KiB
C#

using BlackbirdInterface;
using BlackbirdInterface.Capture;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace BlackbirdRunner
{
internal static class Program
{
private static async Task<int> Main(string[] args)
{
if (!RunnerOptions.TryParse(args, out RunnerOptions options, out string error))
{
Console.Error.WriteLine(error);
Console.Error.WriteLine(
"Usage: BlackbirdRunner.exe --headless-capture --job-id <id> --job-dir <dir> --scan-seconds <seconds> [--integrity-level default|medium|system]");
return 2;
}
var runner = new CaptureRunner(options);
return await runner.RunAsync().ConfigureAwait(false);
}
}
internal sealed class CaptureRunner
{
private readonly RunnerOptions _options;
private readonly string _samplePath;
private readonly string _sampleNamePath;
private readonly string _requestPath;
private readonly string _progressPath;
private readonly string _resultPath;
private readonly string _workspacePath;
private readonly string _archiveBuildRoot;
private readonly string _runDirectory;
private readonly string _runnerLogPath;
private readonly object _logLock = new();
private int _ioctlEvents;
private int _etwEvents;
internal CaptureRunner(RunnerOptions options)
{
_options = options;
_samplePath = Path.Combine(options.JobDirectory, "sample.bin");
_sampleNamePath = Path.Combine(options.JobDirectory, "sample.name.txt");
_requestPath = Path.Combine(options.JobDirectory, "capture-request.json");
_progressPath = Path.Combine(options.JobDirectory, "progress.json");
_resultPath = Path.Combine(options.JobDirectory, "result.bkcap");
_workspacePath = Path.Combine(options.JobDirectory, "capture-workspace");
_archiveBuildRoot = ResolveArchiveBuildRoot(options.JobDirectory, options.JobId);
_runDirectory = Path.Combine(options.JobDirectory, "run");
_runnerLogPath = Path.Combine(options.JobDirectory, "runner.log");
}
internal async Task<int> RunAsync()
{
Directory.CreateDirectory(_options.JobDirectory);
WriteLog(
$"runner start job={_options.JobId} pid={Environment.ProcessId} scanSeconds={_options.ScanSeconds} integrity={_options.IntegrityLevel}");
WriteProgress("initializing", 16, "runner process initialized");
try
{
if (!Kernel32Native.EnableDebugPrivilege(out int privilegeError))
{
WriteLog($"SeDebugPrivilege enable failed err={privilegeError}");
WriteProgress("warning", 18, $"SeDebugPrivilege unavailable err={privilegeError}",
lastError: privilegeError);
}
if (!File.Exists(_samplePath))
{
throw new FileNotFoundException("Uploaded sample was not found.", _samplePath);
}
string displayName = ReadDisplayName();
PeKind peKind = PeImageInspector.ReadKind(_samplePath);
WriteLog($"sample name={displayName} kind={peKind}");
if (File.Exists(_resultPath))
{
File.Delete(_resultPath);
}
if (Directory.Exists(_workspacePath))
{
Directory.Delete(_workspacePath, recursive: true);
}
if (Directory.Exists(_runDirectory))
{
Directory.Delete(_runDirectory, recursive: true);
}
if (Directory.Exists(_archiveBuildRoot))
{
Directory.Delete(_archiveBuildRoot, recursive: true);
}
Directory.CreateDirectory(_runDirectory);
LaunchPlan plan = BuildLaunchPlan(displayName, peKind);
if (!RuntimeConfigService.TryApply(plan.Profile,
out BlackbirdNative.BkRuntimeConfigResponse runtimeConfig,
out string runtimeError))
{
WriteLog($"runtime config arm failed: {runtimeError}");
WriteProgress("failed", 100, $"failed to arm Blackbird runtime config: {runtimeError}",
terminalState: 2, lastError: 2);
return 2;
}
WriteLog("runtime config armed " +
$"effective={RuntimeConfigService.DescribeFlags(runtimeConfig.EffectiveFlags)} " +
$"persistent={RuntimeConfigService.DescribeFlags(runtimeConfig.PersistentFlags)} " +
$"mode={RuntimeConfigService.DescribeMode(runtimeConfig.Mode)}");
WriteProgress("launching", 22, $"launching {plan.KindLabel}");
if (!AnalysisLaunchService.TryLaunchWithUsermodeHooksAndPrepareSession(
plan.ImagePath,
useEarlyBirdApc: true,
plan.Profile,
out AnalysisLaunchResult? launch,
out string launchError) || launch == null)
{
WriteLog($"launch failed: {launchError}");
WriteProgress("failed", 100, launchError, terminalState: 2, lastError: 2);
return 2;
}
using BlackbirdBackendSession session = launch.Session;
WriteLog($"target launched pid={launch.ProcessId} image={plan.ImagePath}");
if (RuntimeConfigService.TryRead(out BlackbirdNative.BkRuntimeConfigResponse activeConfig,
out string activeConfigError))
{
WriteLog("runtime config after session " +
$"effective={RuntimeConfigService.DescribeFlags(activeConfig.EffectiveFlags)} " +
$"runtime={RuntimeConfigService.DescribeFlags(activeConfig.RuntimeFlags)} " +
$"persistent={RuntimeConfigService.DescribeFlags(activeConfig.PersistentFlags)} " +
$"mode={RuntimeConfigService.DescribeMode(activeConfig.Mode)}");
}
else
{
WriteLog($"runtime config after session read failed: {activeConfigError}");
}
WriteProgress("capturing", 34, $"target launched pid={launch.ProcessId}", targetPid: launch.ProcessId);
using BlackbirdCaptureLiveStore store =
CaptureArchiveStorage.OpenLiveStore(_workspacePath, launch.ProcessId, displayName);
using PackerDetectionService packerDetector = new(launch.ProcessId, _workspacePath, store, WriteLog);
var projection = new CaptureProjectionEngine(launch.ProcessId, _options.JobId, "BlackbirdRunner");
var performanceSampler = new PerformanceSampler();
performanceSampler.SetTargetPid(launch.ProcessId);
performanceSampler.SampleArrived += (_, sample) => projection.ObservePerformance(sample);
performanceSampler.Start();
DateTime captureStartedUtc = DateTime.UtcNow;
projection.CaptureStaticProcessSnapshot(plan.ImagePath, displayName, peKind.ToString(), plan.Profile);
session.IoctlEvent += record =>
{
Interlocked.Increment(ref _ioctlEvents);
DateTime nowUtc = DateTime.UtcNow;
store.AppendIoctl(nowUtc, record);
projection.ObserveIoctl(record, nowUtc);
packerDetector.ObserveIoctl(record);
};
session.EtwEvent += etw =>
{
Interlocked.Increment(ref _etwEvents);
BrokerEtwEventView mapped = BrokerEtwEventMapper.FromNative(etw);
store.AppendEtw(mapped);
projection.ObserveEtw(mapped);
packerDetector.ObserveEtw(mapped);
};
session.Status += line =>
{
WriteLog($"session: {line}");
projection.ObserveStatus(line);
};
int exitCode;
try
{
exitCode = await WaitForTargetAsync(launch.ProcessId, _options.ScanSeconds, projection)
.ConfigureAwait(false);
}
finally
{
performanceSampler.Stop();
}
session.Dispose();
WriteProgress("finalizing", 88,
$"finalizing capture ioctl={_ioctlEvents} etw={_etwEvents} targetExit={exitCode}",
targetPid: launch.ProcessId);
packerDetector.Flush(TimeSpan.FromSeconds(4));
projection.WaitForPendingStackCaptures(TimeSpan.FromSeconds(2));
store.Flush();
CaptureLoadedWorkspace loaded = CaptureArchiveStorage.LoadWorkspace(_workspacePath);
string runSummary =
projection.FinalizeRun(exitCode, _ioctlEvents, _etwEvents, DateTime.UtcNow - captureStartedUtc);
WriteLog($"capture summary: {runSummary}");
SessionFileTab? capturedTab = loaded.Archive.Tabs.FirstOrDefault(x => x.Pid == launch.ProcessId) ??
loaded.Archive.Tabs.FirstOrDefault();
if (capturedTab != null)
{
if (loaded.TabPaths.TryGetValue(capturedTab.Pid, out string? tabPath))
{
capturedTab.CaptureStorePath = tabPath;
}
projection.ApplyToTab(capturedTab, plan.Profile, plan.ImagePath, displayName, exitCode);
LogTabStats("tab-before-final-archive", capturedTab);
}
LogWorkspaceStats("workspace-before-final-archive", _workspacePath);
LogArchiveBuildRoot("archive-build-root", _archiveBuildRoot);
CaptureArchiveStorage.SaveWorkspace(_resultPath, loaded.Archive, _archiveBuildRoot);
FileInfo result = new(_resultPath);
if (!result.Exists || result.Length == 0)
{
throw new InvalidDataException("Capture archive was not created.");
}
string sha256 = ComputeSha256Hex(_resultPath);
WriteLog($"artifact ready path={_resultPath} bytes={result.Length} sha256={sha256}");
WriteProgress("artifact-ready", 100,
$"capture artifact finalized ioctl={_ioctlEvents} etw={_etwEvents}", terminalState: 1,
targetPid: launch.ProcessId, artifactReady: true, artifactSize: result.Length,
artifactSha256: sha256);
return 0;
}
catch (Exception ex)
{
WriteLog($"fatal: {ex}");
WriteProgress("failed", 100, ex.Message, terminalState: 2, lastError: 1);
return 1;
}
finally
{
TryDeleteSampleMaterial();
TryDeleteDirectory(_archiveBuildRoot);
}
}
private LaunchPlan BuildLaunchPlan(string displayName, PeKind peKind)
{
LaunchOptions launchOptions = ReadLaunchOptions();
LaunchIntegrityLevel integrityLevel = ResolveIntegrityLevel(launchOptions);
bool isDll = peKind == PeKind.Dll || displayName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase);
if (isDll)
{
string dllPath = Path.Combine(_runDirectory, EnsureExtension(SanitizeFileName(displayName), ".dll"));
File.Copy(_samplePath, dllPath, overwrite: true);
string dllHostPath = AnalysisLaunchService.ResolveDllHostPath();
if (!File.Exists(dllHostPath))
{
throw new FileNotFoundException("BlackbirdDllHost.exe is required for DLL capture.", dllHostPath);
}
var profile =
new LaunchProfile { TargetKind = LaunchTargetKind.Dll,
AnalysisSubjectPath = dllPath,
AnalysisHostPath = dllHostPath,
WorkingDirectory = ResolveWorkingDirectory(launchOptions.WorkingDirectory),
DllMode = ResolveDllMode(launchOptions.DllMode),
DllExportName = launchOptions.DllExportName ?? string.Empty,
DllExportOrdinal = launchOptions.DllExportOrdinal,
DllArgument = launchOptions.DllArgument ?? string.Empty,
DllLoadFlags = launchOptions.DllLoadFlags,
DllFreeOnExit = launchOptions.DllFreeOnExit,
DllInstallDisable = launchOptions.DllInstallDisable,
DllWaitMilliseconds =
checked((uint)Math.Min(uint.MaxValue, _options.ScanSeconds * 1000L)),
IntegrityLevel = integrityLevel };
ApplyCommonLaunchOptions(profile, launchOptions);
profile.CommandLineArguments = AnalysisLaunchService.BuildDllHostCommandLineArguments(profile);
return new LaunchPlan(dllHostPath, profile, "dll");
}
string exePath = Path.Combine(_runDirectory, EnsureExtension(SanitizeFileName(displayName), ".exe"));
File.Copy(_samplePath, exePath, overwrite: true);
var exeProfile =
new LaunchProfile { TargetKind = LaunchTargetKind.Executable,
WorkingDirectory = ResolveWorkingDirectory(launchOptions.WorkingDirectory),
CommandLineArguments = launchOptions.CommandLineArguments ?? string.Empty,
IntegrityLevel = integrityLevel };
ApplyCommonLaunchOptions(exeProfile, launchOptions);
return new LaunchPlan(exePath, exeProfile, "process");
}
private LaunchOptions ReadLaunchOptions()
{
if (!File.Exists(_requestPath))
{
return new LaunchOptions();
}
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(_requestPath));
if (!document.RootElement.TryGetProperty("launchOptions", out JsonElement optionsElement))
{
return new LaunchOptions();
}
return JsonSerializer.Deserialize<LaunchOptions>(
optionsElement.GetRawText(), new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ??
new LaunchOptions();
}
private string ResolveWorkingDirectory(string? workingDirectory)
{
string value = (workingDirectory ?? string.Empty).Trim();
if (value.Length == 0)
{
return _runDirectory;
}
return Path.IsPathRooted(value) ? value : Path.GetFullPath(Path.Combine(_runDirectory, value));
}
private LaunchIntegrityLevel ResolveIntegrityLevel(LaunchOptions launchOptions)
{
return RunnerOptions.TryParseIntegrityLevel(launchOptions.IntegrityLevel, out LaunchIntegrityLevel parsed)
? parsed
: _options.IntegrityLevel;
}
private static void ApplyCommonLaunchOptions(LaunchProfile profile, LaunchOptions launchOptions)
{
profile.EnvironmentOverridesText = launchOptions.EnvironmentOverrides ?? string.Empty;
profile.ParentProcessId = launchOptions.ParentProcessId;
profile.InheritHandles = launchOptions.InheritHandles;
profile.ConcealHookPresence = launchOptions.ConcealHookPresence;
profile.EnableKernelHooks = launchOptions.EnableKernelHooks;
profile.EnableAntiVirtualizationMasking = launchOptions.EnableAntiVirtualizationMasking;
profile.EnableControllerConcealment = launchOptions.EnableControllerConcealment;
profile.EnableInterfaceProtectedAccess = launchOptions.EnableInterfaceProtectedAccess;
profile.EnableControllerProtectedAccess = launchOptions.EnableControllerProtectedAccess;
profile.Priority = ResolvePriority(launchOptions.Priority);
if (LaunchProfile.TryParseAffinityMask(launchOptions.AffinityMask, out ulong affinityMask))
{
profile.AffinityMask = affinityMask;
}
}
private static LaunchPriorityPreset ResolvePriority(string? value)
{
return (value ?? string.Empty)
.Trim()
.ToLowerInvariant() switch { "idle" => LaunchPriorityPreset.Idle,
"below-normal" or "belownormal" => LaunchPriorityPreset.BelowNormal,
"normal" => LaunchPriorityPreset.Normal,
"above-normal" or "abovenormal" => LaunchPriorityPreset.AboveNormal,
"high" => LaunchPriorityPreset.High,
"realtime" or "real-time" => LaunchPriorityPreset.Realtime,
_ => LaunchPriorityPreset.Inherit };
}
private static DllLaunchMode ResolveDllMode(string? value)
{
return (value ?? string.Empty)
.Trim()
.ToLowerInvariant() switch { "export" => DllLaunchMode.Export,
"rundll" => DllLaunchMode.Rundll,
"register" => DllLaunchMode.Register,
"unregister" => DllLaunchMode.Unregister,
"install" => DllLaunchMode.Install,
_ => DllLaunchMode.Load };
}
private async Task<int> WaitForTargetAsync(int pid, uint scanSeconds, CaptureProjectionEngine projection)
{
DateTime captureStartUtc = DateTime.UtcNow;
TimeSpan scanLimit = TimeSpan.FromSeconds(Math.Max(1u, scanSeconds));
TimeSpan childDiscoveryGrace = TimeSpan.FromSeconds(3);
int exitCode = 0;
bool rootExited = false;
DateTime? noLiveTrackedSinceUtc = null;
while (true)
{
DateTime nowUtc = DateTime.UtcNow;
TimeSpan elapsed = nowUtc - captureStartUtc;
_ = projection.RefreshTrackedProcessTree("toolhelp-descendant");
int[] trackedPids = projection.SnapshotTrackedPids().Append(pid).Where(x => x > 0).Distinct().ToArray();
List<int> livePids = new();
foreach (int trackedPid in trackedPids)
{
using Process? process = TryGetProcess(trackedPid);
if (process == null)
{
if (trackedPid == pid)
{
rootExited = true;
}
continue;
}
if (process.HasExited)
{
if (trackedPid == pid)
{
rootExited = true;
exitCode = process.ExitCode;
}
continue;
}
livePids.Add(trackedPid);
}
if (livePids.Count == 0)
{
noLiveTrackedSinceUtc ??= nowUtc;
if (rootExited && nowUtc - noLiveTrackedSinceUtc >= childDiscoveryGrace)
{
return exitCode;
}
}
else
{
noLiveTrackedSinceUtc = null;
}
if (elapsed >= scanLimit)
{
break;
}
double elapsedSeconds = Math.Max(0, elapsed.TotalSeconds);
int progress = Math.Min(84, 34 + (int)(elapsedSeconds * 50 / Math.Max(1, scanSeconds)));
WriteProgress(
"capturing", progress,
$"capturing root={pid} live=[{string.Join(",", livePids.Take(12))}] tracked={trackedPids.Length} ioctl={Volatile.Read(ref _ioctlEvents)} etw={Volatile.Read(ref _etwEvents)}",
targetPid: pid);
TimeSpan remaining = scanLimit - elapsed;
TimeSpan delay = remaining < TimeSpan.FromSeconds(1) ? remaining : TimeSpan.FromSeconds(1);
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay).ConfigureAwait(false);
}
}
int[] terminatePids = projection.SnapshotTrackedPids().Append(pid).Where(x => x > 0).Distinct().ToArray();
WriteLog($"scan limit reached; terminating tracked pids={string.Join(",", terminatePids)}");
foreach (int terminatePid in terminatePids)
{
try
{
using Process? process = TryGetProcess(terminatePid);
if (process != null && !process.HasExited)
{
process.Kill(entireProcessTree: true);
}
}
catch (Exception ex)
{
WriteLog($"target termination failed pid={terminatePid}: {ex.Message}");
}
}
return exitCode;
}
private static Process? TryGetProcess(int pid)
{
try
{
return Process.GetProcessById(pid);
}
catch (ArgumentException)
{
return null;
}
catch (InvalidOperationException)
{
return null;
}
}
private void WriteProgress(string phase, int progressPercent, string detail, int terminalState = 0,
int lastError = 0, int targetPid = 0, bool artifactReady = false,
long artifactSize = 0, string artifactSha256 = "")
{
var payload = new { schema = "BK.capture.progress.v1",
jobId = _options.JobId,
phase,
progressPercent = Math.Clamp(progressPercent, 0, 100),
terminalState,
targetPid,
runnerPid = Environment.ProcessId,
scanSeconds = _options.ScanSeconds,
artifactReady,
artifactSize,
artifactSha256,
lastError,
timestampUtc = DateTime.UtcNow.ToString("O"),
detail };
string tempPath = _progressPath + ".tmp";
string json = JsonSerializer.Serialize(payload);
File.WriteAllText(tempPath, json);
if (File.Exists(_progressPath))
{
File.Replace(tempPath, _progressPath, destinationBackupFileName: null, ignoreMetadataErrors: true);
}
else
{
File.Move(tempPath, _progressPath);
}
}
private void WriteLog(string line)
{
string text = $"[{DateTime.UtcNow:O}] {line}{Environment.NewLine}";
lock (_logLock)
{
File.AppendAllText(_runnerLogPath, text);
}
Console.WriteLine(line);
}
private string ReadDisplayName()
{
try
{
if (File.Exists(_sampleNamePath))
{
string value = File.ReadAllText(_sampleNamePath).Trim();
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
}
catch
{
}
return "sample.exe";
}
private static string SanitizeFileName(string value)
{
string name = Path.GetFileName(value);
if (string.IsNullOrWhiteSpace(name))
{
return "sample";
}
foreach (char invalid in Path.GetInvalidFileNameChars())
{
name = name.Replace(invalid, '_');
}
return name;
}
private static string EnsureExtension(string fileName, string extension)
{
if (string.IsNullOrWhiteSpace(Path.GetExtension(fileName)))
{
return fileName + extension;
}
return fileName;
}
private void TryDeleteSampleMaterial()
{
TryDeleteFile(_samplePath);
TryDeleteFile(_sampleNamePath);
TryDeleteFile(_requestPath);
TryDeleteDirectory(_runDirectory);
TryDeleteDirectory(_workspacePath);
}
private void TryDeleteFile(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (Exception ex)
{
WriteLog($"cleanup file failed path={path} error={ex.Message}");
}
}
private void TryDeleteDirectory(string path)
{
try
{
if (Directory.Exists(path))
{
Directory.Delete(path, recursive: true);
}
}
catch (Exception ex)
{
WriteLog($"cleanup directory failed path={path} error={ex.Message}");
}
}
private static string ComputeSha256Hex(string path)
{
using FileStream stream = File.OpenRead(path);
return Convert.ToHexString(SHA256.HashData(stream)).ToLowerInvariant();
}
private static string ResolveArchiveBuildRoot(string jobDirectory, string jobId)
{
string? configuredRoot = Environment.GetEnvironmentVariable("BLACKBIRD_ARCHIVE_BUILD_ROOT");
if (string.IsNullOrWhiteSpace(configuredRoot))
{
return Path.Combine(jobDirectory, "archive-build");
}
return Path.Combine(configuredRoot, SanitizePathToken(jobId));
}
private static string SanitizePathToken(string value)
{
string sanitized = new(value.Where(ch => char.IsLetterOrDigit(ch) || ch == '-' || ch == '_').ToArray());
return string.IsNullOrWhiteSpace(sanitized) ? Guid.NewGuid().ToString("N") : sanitized;
}
private void LogArchiveBuildRoot(string label, string rootPath)
{
try
{
string fullRoot = Path.GetFullPath(rootPath);
string? driveRoot = Path.GetPathRoot(fullRoot);
long availableBytes = 0;
if (!string.IsNullOrWhiteSpace(driveRoot))
{
availableBytes = new DriveInfo(driveRoot).AvailableFreeSpace;
}
WriteLog($"{label}: path={fullRoot} availableBytes={availableBytes}");
}
catch (Exception ex)
{
WriteLog($"{label}: stats failed: {ex.Message}");
}
}
private void LogWorkspaceStats(string label, string workspacePath)
{
try
{
string tabRoot = Path.Combine(workspacePath, "tabs");
string[] segmentFiles = Directory.Exists(tabRoot)
? Directory.GetFiles(tabRoot, "*.bbseg", SearchOption.AllDirectories)
: Array.Empty<string>();
string[] blobFiles = Directory.Exists(tabRoot)
? Directory.GetFiles(tabRoot, "*.bbblob", SearchOption.AllDirectories)
: Array.Empty<string>();
string[] sectionFiles = Directory.Exists(tabRoot)
? Directory.GetFiles(tabRoot, "*.bbsec", SearchOption.AllDirectories)
: Array.Empty<string>();
long indexBytes =
Directory.Exists(tabRoot)
? Directory.GetFiles(tabRoot, "index.sqlite", SearchOption.AllDirectories).Sum(SafeFileLength)
: 0;
long materializedBytes =
Directory.Exists(tabRoot)
? Directory.GetFiles(tabRoot, "materialized.json", SearchOption.AllDirectories)
.Sum(SafeFileLength)
: 0;
WriteLog($"{label}: segments={segmentFiles.Length} segmentBytes={segmentFiles.Sum(SafeFileLength)} " +
$"blobs={blobFiles.Length} blobBytes={blobFiles.Sum(SafeFileLength)} " +
$"sections={sectionFiles.Length} sectionBytes={sectionFiles.Sum(SafeFileLength)} " +
$"indexBytes={indexBytes} materializedBytes={materializedBytes}");
}
catch (Exception ex)
{
WriteLog($"{label}: stats failed: {ex.Message}");
}
}
private void LogTabStats(string label, SessionFileTab tab)
{
WriteLog(
$"{label}: pid={tab.Pid} events={tab.Events.Count} " +
$"perf={tab.PerformanceHistory.Count} memoryAttribution={tab.MemoryRegionAttributionHistory.Count} " +
$"threadLifecycle={tab.ThreadLifecycleHistory.Count} etwGroups={tab.EtwGroups.Count} " +
$"heuristics={tab.HeuristicsGroups.Count} filesystem={tab.FilesystemGroups.Count} " +
$"registry={tab.RegistryGroups.Count} relations={tab.ProcessRelationsGroups.Count} " +
$"apiGraph={tab.ApiGraphRows.Count} extended={tab.ExtendedActivityRows.Count} " +
$"threadStacks={tab.ThreadStackHistories.Count}");
}
private static long SafeFileLength(string path)
{
try
{
return new FileInfo(path).Length;
}
catch
{
return 0;
}
}
private sealed record LaunchPlan(string ImagePath, LaunchProfile Profile, string KindLabel);
}
internal sealed class LaunchOptions
{
public string CommandLineArguments { get; init; } = string.Empty;
public string WorkingDirectory { get; init; } = string.Empty;
public string EnvironmentOverrides { get; init; } = string.Empty;
public string IntegrityLevel { get; init; } = string.Empty;
public string Priority { get; init; } = string.Empty;
public string AffinityMask { get; init; } = string.Empty;
public uint ParentProcessId { get; init; }
public bool InheritHandles { get; init; }
public bool ConcealHookPresence { get; init; }
public bool EnableKernelHooks { get; init; } = true;
public bool EnableAntiVirtualizationMasking { get; init; }
public bool EnableControllerConcealment { get; init; }
public bool EnableInterfaceProtectedAccess { get; init; }
public bool EnableControllerProtectedAccess { get; init; } = true;
public string DllMode { get; init; } = string.Empty;
public string DllExportName { get; init; } = string.Empty;
public uint DllExportOrdinal { get; init; }
public string DllArgument { get; init; } = string.Empty;
public uint DllLoadFlags { get; init; }
public bool DllFreeOnExit { get; init; }
public bool DllInstallDisable { get; init; }
}
internal sealed class RunnerOptions
{
public string JobId { get; init; } = string.Empty;
public string JobDirectory { get; init; } = string.Empty;
public uint ScanSeconds { get; init; }
public LaunchIntegrityLevel IntegrityLevel { get; init; } = LaunchIntegrityLevel.Default;
internal static bool TryParse(string[] args, out RunnerOptions options, out string error)
{
options = new RunnerOptions();
error = string.Empty;
bool headlessCapture = false;
string jobId = string.Empty;
string jobDirectory = string.Empty;
uint scanSeconds = 300;
LaunchIntegrityLevel integrityLevel = LaunchIntegrityLevel.Default;
for (int i = 0; i < args.Length; i += 1)
{
string arg = args[i];
if (arg.Equals("--headless-capture", StringComparison.OrdinalIgnoreCase))
{
headlessCapture = true;
continue;
}
if (arg.Equals("--job-id", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
jobId = args[++i];
continue;
}
if (arg.Equals("--job-dir", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
jobDirectory = args[++i];
continue;
}
if (arg.Equals("--scan-seconds", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
if (!uint.TryParse(args[++i], out scanSeconds))
{
error = "--scan-seconds must be an unsigned integer.";
return false;
}
continue;
}
if (arg.Equals("--integrity-level", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
if (!TryParseIntegrityLevel(args[++i], out integrityLevel))
{
error = "--integrity-level must be default, untrusted, low, medium, high, or system.";
return false;
}
continue;
}
error = $"Unknown or incomplete argument: {arg}";
return false;
}
if (!headlessCapture)
{
error = "--headless-capture is required.";
return false;
}
if (string.IsNullOrWhiteSpace(jobId))
{
error = "--job-id is required.";
return false;
}
if (string.IsNullOrWhiteSpace(jobDirectory))
{
error = "--job-dir is required.";
return false;
}
options =
new RunnerOptions { JobId = jobId, JobDirectory = Path.GetFullPath(jobDirectory),
ScanSeconds = Math.Clamp(scanSeconds, 5u, 3600u), IntegrityLevel = integrityLevel };
return true;
}
internal static bool TryParseIntegrityLevel(string? value, out LaunchIntegrityLevel level)
{
level = LaunchIntegrityLevel.Default;
switch ((value ?? string.Empty).Trim().ToLowerInvariant())
{
case "":
case "default":
case "asinvoker":
case "as-invoker":
level = LaunchIntegrityLevel.Default;
return true;
case "untrusted":
level = LaunchIntegrityLevel.Untrusted;
return true;
case "low":
level = LaunchIntegrityLevel.Low;
return true;
case "normal":
case "user":
case "medium":
level = LaunchIntegrityLevel.Medium;
return true;
case "high":
case "elevated":
level = LaunchIntegrityLevel.High;
return true;
case "system":
case "localsystem":
case "local-system":
level = LaunchIntegrityLevel.System;
return true;
default:
if (!uint.TryParse(value, out uint raw) || raw > (uint)LaunchIntegrityLevel.System)
{
return false;
}
level = (LaunchIntegrityLevel)raw;
return true;
}
}
}
internal enum PeKind
{
Unknown,
Executable,
Dll
}
internal static class PeImageInspector
{
private const ushort ImageFileDll = 0x2000;
internal static PeKind ReadKind(string path)
{
Span<byte> header = stackalloc byte[4096];
using FileStream stream = File.OpenRead(path);
int read = stream.Read(header);
if (read < 0x40 || header[0] != (byte)'M' || header[1] != (byte)'Z')
{
return PeKind.Unknown;
}
int peOffset = BitConverter.ToInt32(header.Slice(0x3C, 4));
if (peOffset < 0 || peOffset + 24 > read)
{
return PeKind.Unknown;
}
if (header[peOffset] != (byte)'P' || header[peOffset + 1] != (byte)'E' || header[peOffset + 2] != 0 ||
header[peOffset + 3] != 0)
{
return PeKind.Unknown;
}
ushort characteristics = BitConverter.ToUInt16(header.Slice(peOffset + 22, 2));
return (characteristics & ImageFileDll) != 0 ? PeKind.Dll : PeKind.Executable;
}
}
}