// ----------------------------------------------------------------------------- // PeHardeningExtractor.cs // Sprint: SPRINT_3500_0004_0001_smart_diff_binary_output // Task: SDIFF-BIN-010 - Implement PeHardeningExtractor // Task: SDIFF-BIN-011 - Implement PE DllCharacteristics parsing // Task: SDIFF-BIN-012 - Implement PE Authenticode detection // Description: Extracts security hardening flags from Windows PE binaries // ----------------------------------------------------------------------------- using System.Buffers.Binary; using System.Collections.Immutable; namespace StellaOps.Scanner.Analyzers.Native.Hardening; /// /// Extracts hardening flags from Windows PE (Portable Executable) binaries. /// Detects ASLR, DEP, CFG, Authenticode, Safe SEH, and other security features. /// Per Sprint 3500.4 - Smart-Diff Binary Analysis. /// public sealed class PeHardeningExtractor : IHardeningExtractor { private readonly TimeProvider _timeProvider; public PeHardeningExtractor(TimeProvider? timeProvider = null) { _timeProvider = timeProvider ?? TimeProvider.System; } // PE magic bytes: MZ (DOS header) private const ushort DOS_MAGIC = 0x5A4D; // "MZ" private const uint PE_SIGNATURE = 0x00004550; // "PE\0\0" // PE Optional Header magic values private const ushort PE32_MAGIC = 0x10B; private const ushort PE32PLUS_MAGIC = 0x20B; // DllCharacteristics flags (PE32/PE32+) private const ushort IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA = 0x0020; private const ushort IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE = 0x0040; // ASLR private const ushort IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY = 0x0080; private const ushort IMAGE_DLLCHARACTERISTICS_NX_COMPAT = 0x0100; // DEP private const ushort IMAGE_DLLCHARACTERISTICS_NO_SEH = 0x0400; private const ushort IMAGE_DLLCHARACTERISTICS_GUARD_CF = 0x4000; // CFG // Data Directory indices private const int IMAGE_DIRECTORY_ENTRY_SECURITY = 4; // Authenticode certificate private const int IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG = 10; /// public BinaryFormat SupportedFormat => BinaryFormat.Pe; /// public bool CanExtract(string path) { var ext = Path.GetExtension(path).ToLowerInvariant(); return ext is ".exe" or ".dll" or ".sys" or ".ocx" or ".scr"; } /// public bool CanExtract(ReadOnlySpan header) { if (header.Length < 2) return false; var magic = BinaryPrimitives.ReadUInt16LittleEndian(header); return magic == DOS_MAGIC; } /// public async Task ExtractAsync(string path, string digest, CancellationToken ct = default) { await using var stream = File.OpenRead(path); return await ExtractAsync(stream, path, digest, ct); } /// public async Task ExtractAsync(Stream stream, string path, string digest, CancellationToken ct = default) { var flags = new List(); var missing = new List(); // Read full file into memory for parsing using var ms = new MemoryStream(); await stream.CopyToAsync(ms, ct); var peData = ms.ToArray(); if (peData.Length < 64) // Minimum DOS header size { return CreateResult(path, digest, [], ["Invalid PE: too small"]); } // Validate DOS header var dosMagic = BinaryPrimitives.ReadUInt16LittleEndian(peData.AsSpan(0, 2)); if (dosMagic != DOS_MAGIC) { return CreateResult(path, digest, [], ["Invalid PE: bad DOS magic"]); } // Get PE header offset from DOS header (e_lfanew at offset 0x3C) var peOffset = BinaryPrimitives.ReadInt32LittleEndian(peData.AsSpan(0x3C, 4)); if (peOffset < 0 || peOffset + 24 > peData.Length) { return CreateResult(path, digest, [], ["Invalid PE: bad PE offset"]); } // Validate PE signature var peSignature = BinaryPrimitives.ReadUInt32LittleEndian(peData.AsSpan(peOffset, 4)); if (peSignature != PE_SIGNATURE) { return CreateResult(path, digest, [], ["Invalid PE: bad PE signature"]); } // Parse COFF header (20 bytes after PE signature) var coffOffset = peOffset + 4; var machine = BinaryPrimitives.ReadUInt16LittleEndian(peData.AsSpan(coffOffset, 2)); var numberOfSections = BinaryPrimitives.ReadUInt16LittleEndian(peData.AsSpan(coffOffset + 2, 2)); var characteristics = BinaryPrimitives.ReadUInt16LittleEndian(peData.AsSpan(coffOffset + 18, 2)); // Parse Optional Header var optionalHeaderOffset = coffOffset + 20; if (optionalHeaderOffset + 2 > peData.Length) { return CreateResult(path, digest, [], ["Invalid PE: truncated optional header"]); } var optionalMagic = BinaryPrimitives.ReadUInt16LittleEndian(peData.AsSpan(optionalHeaderOffset, 2)); var isPe32Plus = optionalMagic == PE32PLUS_MAGIC; // DllCharacteristics offset differs between PE32 and PE32+ var dllCharacteristicsOffset = optionalHeaderOffset + (isPe32Plus ? 70 : 70); if (dllCharacteristicsOffset + 2 > peData.Length) { return CreateResult(path, digest, [], ["Invalid PE: truncated DllCharacteristics"]); } var dllCharacteristics = BinaryPrimitives.ReadUInt16LittleEndian(peData.AsSpan(dllCharacteristicsOffset, 2)); // === TASK SDIFF-BIN-011: Parse DllCharacteristics === // ASLR (Dynamic Base) var hasAslr = (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) != 0; flags.Add(new HardeningFlag(HardeningFlagType.Aslr, hasAslr, hasAslr ? "enabled" : null, "DllCharacteristics")); if (!hasAslr) missing.Add("ASLR"); // High Entropy VA (64-bit ASLR) var hasHighEntropyVa = (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA) != 0; flags.Add(new HardeningFlag(HardeningFlagType.HighEntropyVa, hasHighEntropyVa, hasHighEntropyVa ? "enabled" : null, "DllCharacteristics")); if (!hasHighEntropyVa && isPe32Plus) missing.Add("HIGH_ENTROPY_VA"); // DEP (NX Compatible) var hasDep = (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_NX_COMPAT) != 0; flags.Add(new HardeningFlag(HardeningFlagType.Dep, hasDep, hasDep ? "enabled" : null, "DllCharacteristics")); if (!hasDep) missing.Add("DEP"); // CFG (Control Flow Guard) var hasCfg = (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_GUARD_CF) != 0; flags.Add(new HardeningFlag(HardeningFlagType.Cfg, hasCfg, hasCfg ? "enabled" : null, "DllCharacteristics")); if (!hasCfg) missing.Add("CFG"); // Force Integrity var hasForceIntegrity = (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY) != 0; flags.Add(new HardeningFlag(HardeningFlagType.ForceIntegrity, hasForceIntegrity, hasForceIntegrity ? "enabled" : null, "DllCharacteristics")); // NO_SEH flag (indicates SafeSEH is not used, but NO_SEH means no SEH at all which is okay) var noSeh = (dllCharacteristics & IMAGE_DLLCHARACTERISTICS_NO_SEH) != 0; // SafeSEH is only for 32-bit binaries if (!isPe32Plus) { // For 32-bit, NO_SEH is acceptable (no SEH = can't exploit SEH) // If SEH is used, we'd need to check Load Config for SafeSEH var safeSehStatus = noSeh ? "no_seh" : "needs_verification"; flags.Add(new HardeningFlag(HardeningFlagType.SafeSeh, noSeh, safeSehStatus, "DllCharacteristics")); if (!noSeh) missing.Add("SAFE_SEH"); } // === TASK SDIFF-BIN-012: Authenticode Detection === var hasAuthenticode = CheckAuthenticode(peData, optionalHeaderOffset, isPe32Plus); flags.Add(new HardeningFlag(HardeningFlagType.Authenticode, hasAuthenticode, hasAuthenticode ? "signed" : null, "Security Directory")); if (!hasAuthenticode) missing.Add("AUTHENTICODE"); // GS (/GS buffer security check) - check Load Config for SecurityCookie var hasGs = CheckGsBufferSecurity(peData, optionalHeaderOffset, isPe32Plus); flags.Add(new HardeningFlag(HardeningFlagType.Gs, hasGs, hasGs ? "enabled" : null, "Load Config")); if (!hasGs) missing.Add("GS"); return CreateResult(path, digest, flags, missing); } /// /// Check if PE has Authenticode signature by examining Security Directory. /// private static bool CheckAuthenticode(byte[] peData, int optionalHeaderOffset, bool isPe32Plus) { try { // Data directories start after the standard optional header fields // PE32: offset 96 from optional header start // PE32+: offset 112 from optional header start var dataDirectoriesOffset = optionalHeaderOffset + (isPe32Plus ? 112 : 96); // Security directory is index 4 (each entry is 8 bytes: 4 for RVA, 4 for size) var securityDirOffset = dataDirectoriesOffset + (IMAGE_DIRECTORY_ENTRY_SECURITY * 8); if (securityDirOffset + 8 > peData.Length) return false; var securityRva = BinaryPrimitives.ReadUInt32LittleEndian(peData.AsSpan(securityDirOffset, 4)); var securitySize = BinaryPrimitives.ReadUInt32LittleEndian(peData.AsSpan(securityDirOffset + 4, 4)); // If security directory has non-zero size, there's a certificate return securitySize > 0 && securityRva > 0; } catch { return false; } } /// /// Check for /GS buffer security by examining Load Config Directory. /// private static bool CheckGsBufferSecurity(byte[] peData, int optionalHeaderOffset, bool isPe32Plus) { try { var dataDirectoriesOffset = optionalHeaderOffset + (isPe32Plus ? 112 : 96); var loadConfigDirOffset = dataDirectoriesOffset + (IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG * 8); if (loadConfigDirOffset + 8 > peData.Length) return false; var loadConfigRva = BinaryPrimitives.ReadUInt32LittleEndian(peData.AsSpan(loadConfigDirOffset, 4)); var loadConfigSize = BinaryPrimitives.ReadUInt32LittleEndian(peData.AsSpan(loadConfigDirOffset + 4, 4)); // If load config exists and has reasonable size, /GS is likely enabled // (Full verification would require parsing the Load Config structure) return loadConfigSize >= 64 && loadConfigRva > 0; } catch { return false; } } private BinaryHardeningFlags CreateResult( string path, string digest, List flags, List missing) { // Calculate score: enabled flags / total expected flags var positiveFlags = new[] { HardeningFlagType.Aslr, HardeningFlagType.Dep, HardeningFlagType.Cfg, HardeningFlagType.Authenticode, HardeningFlagType.Gs }; var enabledCount = flags.Count(f => f.Enabled && positiveFlags.Contains(f.Name)); var totalExpected = positiveFlags.Length; var score = totalExpected > 0 ? (double)enabledCount / totalExpected : 0.0; return new BinaryHardeningFlags( Format: BinaryFormat.Pe, Path: path, Digest: digest, Flags: [.. flags], HardeningScore: Math.Round(score, 2), MissingFlags: [.. missing], ExtractedAt: _timeProvider.GetUtcNow()); } }