// -----------------------------------------------------------------------------
// 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());
}
}