feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration
- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
@@ -6,6 +6,8 @@ namespace StellaOps.Scanner.Analyzers.Native.Hardening;
|
||||
/// <summary>
|
||||
/// Extracts hardening flags from ELF binaries.
|
||||
/// Per Sprint 3500.4 - Smart-Diff Binary Analysis.
|
||||
/// Tasks: SDIFF-BIN-003 (implemented), SDIFF-BIN-004 (PIE), SDIFF-BIN-005 (RELRO),
|
||||
/// SDIFF-BIN-006 (NX), SDIFF-BIN-007 (Stack Canary), SDIFF-BIN-008 (FORTIFY)
|
||||
/// </summary>
|
||||
public sealed class ElfHardeningExtractor : IHardeningExtractor
|
||||
{
|
||||
@@ -25,14 +27,26 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
|
||||
private const ushort ET_DYN = 3;
|
||||
|
||||
// Program header types
|
||||
private const uint PT_LOAD = 1;
|
||||
private const uint PT_DYNAMIC = 2;
|
||||
private const uint PT_GNU_STACK = 0x6474e551;
|
||||
private const uint PT_GNU_RELRO = 0x6474e552;
|
||||
private const uint PT_GNU_PROPERTY = 0x6474e553;
|
||||
|
||||
// Dynamic section tags
|
||||
private const ulong DT_FLAGS_1 = 0x6ffffffb;
|
||||
private const ulong DT_BIND_NOW = 24;
|
||||
private const ulong DT_NULL = 0;
|
||||
private const ulong DT_NEEDED = 1;
|
||||
private const ulong DT_STRTAB = 5;
|
||||
private const ulong DT_SYMTAB = 6;
|
||||
private const ulong DT_STRSZ = 10;
|
||||
private const ulong DT_RPATH = 15;
|
||||
private const ulong DT_BIND_NOW = 24;
|
||||
private const ulong DT_RUNPATH = 29;
|
||||
private const ulong DT_FLAGS = 30;
|
||||
private const ulong DT_FLAGS_1 = 0x6ffffffb;
|
||||
|
||||
// DT_FLAGS values
|
||||
private const ulong DF_BIND_NOW = 0x00000008;
|
||||
|
||||
// DT_FLAGS_1 values
|
||||
private const ulong DF_1_PIE = 0x08000000;
|
||||
@@ -43,6 +57,36 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
|
||||
private const uint PF_W = 2; // Write
|
||||
private const uint PF_R = 4; // Read
|
||||
|
||||
// Symbol table entry size (for 64-bit)
|
||||
private const int SYM64_SIZE = 24;
|
||||
private const int SYM32_SIZE = 16;
|
||||
|
||||
// Stack canary and FORTIFY symbol names
|
||||
private static readonly string[] StackCanarySymbols =
|
||||
[
|
||||
"__stack_chk_fail",
|
||||
"__stack_chk_guard"
|
||||
];
|
||||
|
||||
private static readonly string[] FortifySymbols =
|
||||
[
|
||||
"__chk_fail",
|
||||
"__memcpy_chk",
|
||||
"__memset_chk",
|
||||
"__strcpy_chk",
|
||||
"__strncpy_chk",
|
||||
"__strcat_chk",
|
||||
"__strncat_chk",
|
||||
"__sprintf_chk",
|
||||
"__snprintf_chk",
|
||||
"__vsprintf_chk",
|
||||
"__vsnprintf_chk",
|
||||
"__printf_chk",
|
||||
"__fprintf_chk",
|
||||
"__memmove_chk",
|
||||
"__gets_chk"
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
public BinaryFormat SupportedFormat => BinaryFormat.Elf;
|
||||
|
||||
@@ -81,73 +125,495 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
|
||||
var flags = new List<HardeningFlag>();
|
||||
var missing = new List<string>();
|
||||
|
||||
// Read ELF header
|
||||
var headerBuf = new byte[64];
|
||||
var bytesRead = await stream.ReadAsync(headerBuf, ct);
|
||||
if (bytesRead < 52) // Minimum ELF header size
|
||||
// Read full file into memory for parsing (required for seeking)
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms, ct);
|
||||
var elfData = ms.ToArray();
|
||||
|
||||
if (elfData.Length < 52) // Minimum ELF header size
|
||||
{
|
||||
return CreateResult(path, digest, [], ["Invalid ELF header"]);
|
||||
}
|
||||
|
||||
// Parse ELF header basics
|
||||
var is64Bit = headerBuf[EI_CLASS] == ELFCLASS64;
|
||||
var isLittleEndian = headerBuf[EI_DATA] == ELFDATA2LSB;
|
||||
var is64Bit = elfData[EI_CLASS] == ELFCLASS64;
|
||||
var isLittleEndian = elfData[EI_DATA] == ELFDATA2LSB;
|
||||
|
||||
// Read e_type to check if PIE
|
||||
var eType = ReadUInt16(headerBuf.AsSpan(16, 2), isLittleEndian);
|
||||
var isPie = eType == ET_DYN; // Shared object = could be PIE
|
||||
// Read e_type
|
||||
var eType = ReadUInt16(elfData.AsSpan(16, 2), isLittleEndian);
|
||||
|
||||
// For a full implementation, we'd parse:
|
||||
// 1. Program headers for PT_GNU_STACK (NX check) and PT_GNU_RELRO
|
||||
// 2. Dynamic section for DT_FLAGS_1 (PIE confirmation), DT_BIND_NOW (full RELRO)
|
||||
// 3. Symbol table for __stack_chk_fail (stack canary)
|
||||
// 4. Symbol table for __fortify_fail (FORTIFY)
|
||||
// Parse ELF header to get program header info
|
||||
var elfHeader = ParseElfHeader(elfData, is64Bit, isLittleEndian);
|
||||
|
||||
// PIE detection (simplified - full impl would check DT_FLAGS_1)
|
||||
// Parse program headers
|
||||
var programHeaders = ParseProgramHeaders(elfData, elfHeader, is64Bit, isLittleEndian);
|
||||
|
||||
// Parse dynamic section entries
|
||||
var dynamicEntries = ParseDynamicSection(elfData, programHeaders, is64Bit, isLittleEndian);
|
||||
|
||||
// Parse symbols for canary and FORTIFY detection
|
||||
var symbols = ParseSymbolNames(elfData, programHeaders, dynamicEntries, is64Bit, isLittleEndian);
|
||||
|
||||
// === TASK SDIFF-BIN-004: PIE Detection ===
|
||||
// PIE is detected by: e_type == ET_DYN AND DT_FLAGS_1 contains DF_1_PIE
|
||||
// OR e_type == ET_DYN for shared objects that could be PIE
|
||||
var hasDtFlags1Pie = dynamicEntries.TryGetValue(DT_FLAGS_1, out var flags1Value) && (flags1Value & DF_1_PIE) != 0;
|
||||
var isPie = eType == ET_DYN && (hasDtFlags1Pie || !dynamicEntries.ContainsKey(DT_FLAGS_1));
|
||||
|
||||
if (isPie)
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Pie, true, "DYN", "e_type"));
|
||||
var source = hasDtFlags1Pie ? "DT_FLAGS_1" : "e_type=ET_DYN";
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Pie, true, "enabled", source));
|
||||
}
|
||||
else
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Pie, false));
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Pie, false, null, "e_type=ET_EXEC"));
|
||||
missing.Add("PIE");
|
||||
}
|
||||
|
||||
// NX - would need to read PT_GNU_STACK and check for PF_X
|
||||
// For now, assume modern binaries have NX by default
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Nx, true, null, "assumed"));
|
||||
// === TASK SDIFF-BIN-006: NX Detection ===
|
||||
// NX is detected via PT_GNU_STACK program header
|
||||
// If PT_GNU_STACK exists and does NOT have PF_X flag, NX is enabled
|
||||
// If PT_GNU_STACK is missing, assume NX (modern default)
|
||||
var gnuStackHeader = programHeaders.FirstOrDefault(p => p.Type == PT_GNU_STACK);
|
||||
bool hasNx;
|
||||
string nxSource;
|
||||
|
||||
if (gnuStackHeader != null)
|
||||
{
|
||||
hasNx = (gnuStackHeader.Flags & PF_X) == 0; // No execute permission = NX enabled
|
||||
nxSource = hasNx ? "PT_GNU_STACK (no PF_X)" : "PT_GNU_STACK (has PF_X)";
|
||||
}
|
||||
else
|
||||
{
|
||||
hasNx = true; // Modern default
|
||||
nxSource = "assumed (no PT_GNU_STACK)";
|
||||
}
|
||||
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Nx, hasNx, hasNx ? "enabled" : "disabled", nxSource));
|
||||
if (!hasNx) missing.Add("NX");
|
||||
|
||||
// RELRO - would need to check PT_GNU_RELRO presence
|
||||
// Partial RELRO is common, Full RELRO requires BIND_NOW
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroPartial, true, null, "assumed"));
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroFull, false));
|
||||
missing.Add("RELRO_FULL");
|
||||
// === TASK SDIFF-BIN-005: RELRO Detection ===
|
||||
// Partial RELRO: PT_GNU_RELRO program header exists
|
||||
// Full RELRO: PT_GNU_RELRO exists AND (DT_BIND_NOW or DT_FLAGS contains DF_BIND_NOW or DT_FLAGS_1 contains DF_1_NOW)
|
||||
var hasRelroHeader = programHeaders.Any(p => p.Type == PT_GNU_RELRO);
|
||||
var hasBindNow = dynamicEntries.ContainsKey(DT_BIND_NOW) ||
|
||||
(dynamicEntries.TryGetValue(DT_FLAGS, out var flagsValue) && (flagsValue & DF_BIND_NOW) != 0) ||
|
||||
(dynamicEntries.TryGetValue(DT_FLAGS_1, out var flags1) && (flags1 & DF_1_NOW) != 0);
|
||||
|
||||
// Stack canary - would check for __stack_chk_fail symbol
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.StackCanary, false));
|
||||
missing.Add("STACK_CANARY");
|
||||
if (hasRelroHeader)
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroPartial, true, "enabled", "PT_GNU_RELRO"));
|
||||
|
||||
if (hasBindNow)
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroFull, true, "enabled", "PT_GNU_RELRO + BIND_NOW"));
|
||||
}
|
||||
else
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroFull, false, null, "missing BIND_NOW"));
|
||||
missing.Add("RELRO_FULL");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroPartial, false, null, "no PT_GNU_RELRO"));
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.RelroFull, false, null, "no PT_GNU_RELRO"));
|
||||
missing.Add("RELRO_PARTIAL");
|
||||
missing.Add("RELRO_FULL");
|
||||
}
|
||||
|
||||
// FORTIFY - would check for _chk suffixed functions
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Fortify, false));
|
||||
missing.Add("FORTIFY");
|
||||
// === TASK SDIFF-BIN-007: Stack Canary Detection ===
|
||||
// Stack canary is detected by presence of __stack_chk_fail or __stack_chk_guard symbols
|
||||
var hasStackCanary = symbols.Any(s => StackCanarySymbols.Contains(s));
|
||||
var canarySymbol = symbols.FirstOrDefault(s => StackCanarySymbols.Contains(s));
|
||||
|
||||
flags.Add(new HardeningFlag(
|
||||
HardeningFlagType.StackCanary,
|
||||
hasStackCanary,
|
||||
hasStackCanary ? "enabled" : null,
|
||||
hasStackCanary ? canarySymbol : "no __stack_chk_* symbols"));
|
||||
|
||||
if (!hasStackCanary) missing.Add("STACK_CANARY");
|
||||
|
||||
// RPATH - would check DT_RPATH/DT_RUNPATH in dynamic section
|
||||
// If present, it's a security concern
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Rpath, false)); // false = not present = good
|
||||
// === TASK SDIFF-BIN-008: FORTIFY Detection ===
|
||||
// FORTIFY is detected by presence of _chk suffixed functions
|
||||
var fortifySymbols = symbols.Where(s => FortifySymbols.Contains(s)).ToList();
|
||||
var hasFortify = fortifySymbols.Count > 0;
|
||||
|
||||
flags.Add(new HardeningFlag(
|
||||
HardeningFlagType.Fortify,
|
||||
hasFortify,
|
||||
hasFortify ? $"{fortifySymbols.Count} _chk functions" : null,
|
||||
hasFortify ? string.Join(",", fortifySymbols.Take(3)) : "no _chk functions"));
|
||||
|
||||
if (!hasFortify) missing.Add("FORTIFY");
|
||||
|
||||
// RPATH/RUNPATH Detection (security concern if present)
|
||||
var hasRpath = dynamicEntries.ContainsKey(DT_RPATH) || dynamicEntries.ContainsKey(DT_RUNPATH);
|
||||
flags.Add(new HardeningFlag(
|
||||
HardeningFlagType.Rpath,
|
||||
hasRpath,
|
||||
hasRpath ? "present (security risk)" : null,
|
||||
hasRpath ? "DT_RPATH/DT_RUNPATH" : "not set"));
|
||||
|
||||
// RPATH presence is a negative, so we add to missing if present
|
||||
if (hasRpath) missing.Add("NO_RPATH");
|
||||
|
||||
// === TASK SDIFF-BIN-009: CET/BTI Detection ===
|
||||
// CET (Intel) and BTI (ARM) are detected via PT_GNU_PROPERTY / .note.gnu.property
|
||||
var gnuPropertyHeader = programHeaders.FirstOrDefault(p => p.Type == PT_GNU_PROPERTY);
|
||||
var (hasCet, hasBti) = ParseGnuProperty(elfData, gnuPropertyHeader, is64Bit, isLittleEndian);
|
||||
|
||||
// CET - Intel Control-flow Enforcement Technology
|
||||
flags.Add(new HardeningFlag(
|
||||
HardeningFlagType.Cet,
|
||||
hasCet,
|
||||
hasCet ? "enabled" : null,
|
||||
hasCet ? ".note.gnu.property (GNU_PROPERTY_X86_FEATURE_1_AND)" : "not found"));
|
||||
if (!hasCet) missing.Add("CET");
|
||||
|
||||
// BTI - ARM Branch Target Identification
|
||||
flags.Add(new HardeningFlag(
|
||||
HardeningFlagType.Bti,
|
||||
hasBti,
|
||||
hasBti ? "enabled" : null,
|
||||
hasBti ? ".note.gnu.property (GNU_PROPERTY_AARCH64_FEATURE_1_AND)" : "not found"));
|
||||
if (!hasBti) missing.Add("BTI");
|
||||
|
||||
return CreateResult(path, digest, flags, missing);
|
||||
}
|
||||
|
||||
#region CET/BTI Detection
|
||||
|
||||
// GNU property note type
|
||||
private const uint NT_GNU_PROPERTY_TYPE_0 = 5;
|
||||
|
||||
// GNU property types
|
||||
private const uint GNU_PROPERTY_X86_FEATURE_1_AND = 0xc0000002;
|
||||
private const uint GNU_PROPERTY_AARCH64_FEATURE_1_AND = 0xc0000000;
|
||||
|
||||
// Feature flags
|
||||
private const uint GNU_PROPERTY_X86_FEATURE_1_IBT = 0x00000001; // Indirect Branch Tracking
|
||||
private const uint GNU_PROPERTY_X86_FEATURE_1_SHSTK = 0x00000002; // Shadow Stack
|
||||
private const uint GNU_PROPERTY_AARCH64_FEATURE_1_BTI = 0x00000001; // Branch Target Identification
|
||||
private const uint GNU_PROPERTY_AARCH64_FEATURE_1_PAC = 0x00000002; // Pointer Authentication
|
||||
|
||||
private static (bool HasCet, bool HasBti) ParseGnuProperty(
|
||||
byte[] data,
|
||||
ProgramHeader? gnuPropertyHeader,
|
||||
bool is64Bit,
|
||||
bool isLittleEndian)
|
||||
{
|
||||
if (gnuPropertyHeader is null || gnuPropertyHeader.FileSize == 0)
|
||||
return (false, false);
|
||||
|
||||
var offset = (int)gnuPropertyHeader.Offset;
|
||||
var end = offset + (int)gnuPropertyHeader.FileSize;
|
||||
|
||||
if (end > data.Length) return (false, false);
|
||||
|
||||
bool hasCet = false;
|
||||
bool hasBti = false;
|
||||
|
||||
// Parse note entries
|
||||
while (offset + 12 <= end)
|
||||
{
|
||||
var namesz = ReadUInt32(data.AsSpan(offset, 4), isLittleEndian);
|
||||
var descsz = ReadUInt32(data.AsSpan(offset + 4, 4), isLittleEndian);
|
||||
var noteType = ReadUInt32(data.AsSpan(offset + 8, 4), isLittleEndian);
|
||||
offset += 12;
|
||||
|
||||
// Align namesz to 4 bytes
|
||||
var nameszAligned = (namesz + 3) & ~3u;
|
||||
|
||||
if (offset + nameszAligned > end) break;
|
||||
|
||||
// Check if this is a "GNU\0" note
|
||||
if (namesz == 4 && offset + 4 <= data.Length)
|
||||
{
|
||||
var noteName = data.AsSpan(offset, 4);
|
||||
if (noteName.SequenceEqual("GNU\0"u8))
|
||||
{
|
||||
offset += (int)nameszAligned;
|
||||
|
||||
// Parse properties within this note
|
||||
var propEnd = offset + (int)descsz;
|
||||
while (offset + 8 <= propEnd && offset + 8 <= end)
|
||||
{
|
||||
var propType = ReadUInt32(data.AsSpan(offset, 4), isLittleEndian);
|
||||
var propDataSz = ReadUInt32(data.AsSpan(offset + 4, 4), isLittleEndian);
|
||||
offset += 8;
|
||||
|
||||
if (offset + propDataSz > end) break;
|
||||
|
||||
if (propType == GNU_PROPERTY_X86_FEATURE_1_AND && propDataSz >= 4)
|
||||
{
|
||||
var features = ReadUInt32(data.AsSpan(offset, 4), isLittleEndian);
|
||||
// CET requires both IBT (Indirect Branch Tracking) and SHSTK (Shadow Stack)
|
||||
hasCet = (features & GNU_PROPERTY_X86_FEATURE_1_IBT) != 0 ||
|
||||
(features & GNU_PROPERTY_X86_FEATURE_1_SHSTK) != 0;
|
||||
}
|
||||
else if (propType == GNU_PROPERTY_AARCH64_FEATURE_1_AND && propDataSz >= 4)
|
||||
{
|
||||
var features = ReadUInt32(data.AsSpan(offset, 4), isLittleEndian);
|
||||
hasBti = (features & GNU_PROPERTY_AARCH64_FEATURE_1_BTI) != 0;
|
||||
}
|
||||
|
||||
// Align to 8 bytes for 64-bit, 4 bytes for 32-bit
|
||||
var align = is64Bit ? 8u : 4u;
|
||||
var propDataSzAligned = (propDataSz + align - 1) & ~(align - 1);
|
||||
offset += (int)propDataSzAligned;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += (int)nameszAligned;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += (int)nameszAligned;
|
||||
}
|
||||
|
||||
// Align descsz to 4 bytes
|
||||
var descszAligned = (descsz + 3) & ~3u;
|
||||
offset += (int)descszAligned;
|
||||
}
|
||||
|
||||
return (hasCet, hasBti);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ELF Parsing Helpers
|
||||
|
||||
private record ElfHeader(
|
||||
bool Is64Bit,
|
||||
bool IsLittleEndian,
|
||||
ulong PhOffset,
|
||||
ushort PhEntSize,
|
||||
ushort PhNum);
|
||||
|
||||
private record ProgramHeader(
|
||||
uint Type,
|
||||
uint Flags,
|
||||
ulong Offset,
|
||||
ulong VAddr,
|
||||
ulong FileSize,
|
||||
ulong MemSize);
|
||||
|
||||
private static ElfHeader ParseElfHeader(byte[] data, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
if (is64Bit)
|
||||
{
|
||||
// 64-bit ELF header
|
||||
var phOffset = ReadUInt64(data.AsSpan(32, 8), isLittleEndian);
|
||||
var phEntSize = ReadUInt16(data.AsSpan(54, 2), isLittleEndian);
|
||||
var phNum = ReadUInt16(data.AsSpan(56, 2), isLittleEndian);
|
||||
return new ElfHeader(true, isLittleEndian, phOffset, phEntSize, phNum);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 32-bit ELF header
|
||||
var phOffset = ReadUInt32(data.AsSpan(28, 4), isLittleEndian);
|
||||
var phEntSize = ReadUInt16(data.AsSpan(42, 2), isLittleEndian);
|
||||
var phNum = ReadUInt16(data.AsSpan(44, 2), isLittleEndian);
|
||||
return new ElfHeader(false, isLittleEndian, phOffset, phEntSize, phNum);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<ProgramHeader> ParseProgramHeaders(byte[] data, ElfHeader header, bool is64Bit, bool isLittleEndian)
|
||||
{
|
||||
var result = new List<ProgramHeader>();
|
||||
var offset = (int)header.PhOffset;
|
||||
|
||||
for (int i = 0; i < header.PhNum && offset + header.PhEntSize <= data.Length; i++)
|
||||
{
|
||||
var phData = data.AsSpan(offset, header.PhEntSize);
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
// 64-bit program header
|
||||
var type = ReadUInt32(phData[..4], isLittleEndian);
|
||||
var flags = ReadUInt32(phData.Slice(4, 4), isLittleEndian);
|
||||
var pOffset = ReadUInt64(phData.Slice(8, 8), isLittleEndian);
|
||||
var vAddr = ReadUInt64(phData.Slice(16, 8), isLittleEndian);
|
||||
var fileSize = ReadUInt64(phData.Slice(32, 8), isLittleEndian);
|
||||
var memSize = ReadUInt64(phData.Slice(40, 8), isLittleEndian);
|
||||
|
||||
result.Add(new ProgramHeader(type, flags, pOffset, vAddr, fileSize, memSize));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 32-bit program header
|
||||
var type = ReadUInt32(phData[..4], isLittleEndian);
|
||||
var pOffset = ReadUInt32(phData.Slice(4, 4), isLittleEndian);
|
||||
var vAddr = ReadUInt32(phData.Slice(8, 4), isLittleEndian);
|
||||
var fileSize = ReadUInt32(phData.Slice(16, 4), isLittleEndian);
|
||||
var memSize = ReadUInt32(phData.Slice(20, 4), isLittleEndian);
|
||||
var flags = ReadUInt32(phData.Slice(24, 4), isLittleEndian);
|
||||
|
||||
result.Add(new ProgramHeader(type, flags, pOffset, vAddr, fileSize, memSize));
|
||||
}
|
||||
|
||||
offset += header.PhEntSize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<ulong, ulong> ParseDynamicSection(
|
||||
byte[] data,
|
||||
List<ProgramHeader> programHeaders,
|
||||
bool is64Bit,
|
||||
bool isLittleEndian)
|
||||
{
|
||||
var result = new Dictionary<ulong, ulong>();
|
||||
var dynamicHeader = programHeaders.FirstOrDefault(p => p.Type == PT_DYNAMIC);
|
||||
|
||||
if (dynamicHeader == null) return result;
|
||||
|
||||
var offset = (int)dynamicHeader.Offset;
|
||||
var endOffset = offset + (int)dynamicHeader.FileSize;
|
||||
var entrySize = is64Bit ? 16 : 8;
|
||||
|
||||
while (offset + entrySize <= endOffset && offset + entrySize <= data.Length)
|
||||
{
|
||||
ulong tag, value;
|
||||
|
||||
if (is64Bit)
|
||||
{
|
||||
tag = ReadUInt64(data.AsSpan(offset, 8), isLittleEndian);
|
||||
value = ReadUInt64(data.AsSpan(offset + 8, 8), isLittleEndian);
|
||||
}
|
||||
else
|
||||
{
|
||||
tag = ReadUInt32(data.AsSpan(offset, 4), isLittleEndian);
|
||||
value = ReadUInt32(data.AsSpan(offset + 4, 4), isLittleEndian);
|
||||
}
|
||||
|
||||
if (tag == DT_NULL) break;
|
||||
|
||||
result[tag] = value;
|
||||
offset += entrySize;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static HashSet<string> ParseSymbolNames(
|
||||
byte[] data,
|
||||
List<ProgramHeader> programHeaders,
|
||||
Dictionary<ulong, ulong> dynamicEntries,
|
||||
bool is64Bit,
|
||||
bool isLittleEndian)
|
||||
{
|
||||
var symbols = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Get string table and symbol table from dynamic entries
|
||||
if (!dynamicEntries.TryGetValue(DT_STRTAB, out var strTabAddr) ||
|
||||
!dynamicEntries.TryGetValue(DT_STRSZ, out var strTabSize) ||
|
||||
!dynamicEntries.TryGetValue(DT_SYMTAB, out var symTabAddr))
|
||||
{
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// Find the LOAD segment containing these addresses to calculate file offsets
|
||||
var strTabOffset = VAddrToFileOffset(programHeaders, strTabAddr);
|
||||
var symTabOffset = VAddrToFileOffset(programHeaders, symTabAddr);
|
||||
|
||||
if (strTabOffset < 0 || symTabOffset < 0 ||
|
||||
strTabOffset + (long)strTabSize > data.Length)
|
||||
{
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// Parse symbol table entries looking for relevant symbols
|
||||
var symEntrySize = is64Bit ? SYM64_SIZE : SYM32_SIZE;
|
||||
var currentOffset = (int)symTabOffset;
|
||||
var maxSymbols = 10000; // Safety limit
|
||||
|
||||
for (int i = 0; i < maxSymbols && currentOffset + symEntrySize <= data.Length; i++)
|
||||
{
|
||||
// Read st_name (always first 4 bytes)
|
||||
var stName = ReadUInt32(data.AsSpan(currentOffset, 4), isLittleEndian);
|
||||
|
||||
if (stName > 0 && stName < strTabSize)
|
||||
{
|
||||
var nameOffset = (int)strTabOffset + (int)stName;
|
||||
if (nameOffset < data.Length)
|
||||
{
|
||||
var name = ReadNullTerminatedString(data, nameOffset);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
symbols.Add(name);
|
||||
|
||||
// Early exit if we found all the symbols we care about
|
||||
if (symbols.IsSupersetOf(StackCanarySymbols) &&
|
||||
symbols.Intersect(FortifySymbols).Count() >= 3)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentOffset += symEntrySize;
|
||||
|
||||
// Stop if we hit another section or run past the string table
|
||||
if (currentOffset >= strTabOffset)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
private static long VAddrToFileOffset(List<ProgramHeader> programHeaders, ulong vAddr)
|
||||
{
|
||||
foreach (var ph in programHeaders.Where(p => p.Type == PT_LOAD))
|
||||
{
|
||||
if (vAddr >= ph.VAddr && vAddr < ph.VAddr + ph.MemSize)
|
||||
{
|
||||
return (long)(ph.Offset + (vAddr - ph.VAddr));
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string ReadNullTerminatedString(byte[] data, int offset)
|
||||
{
|
||||
var end = offset;
|
||||
while (end < data.Length && data[end] != 0)
|
||||
{
|
||||
end++;
|
||||
if (end - offset > 256) break; // Safety limit
|
||||
}
|
||||
return System.Text.Encoding.UTF8.GetString(data, offset, end - offset);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static BinaryHardeningFlags CreateResult(
|
||||
string path,
|
||||
string digest,
|
||||
List<HardeningFlag> flags,
|
||||
List<string> missing)
|
||||
{
|
||||
// Calculate score: enabled flags / total possible flags
|
||||
var enabledCount = flags.Count(f => f.Enabled && f.Name != HardeningFlagType.Rpath);
|
||||
var totalExpected = 6; // PIE, NX, RELRO_FULL, STACK_CANARY, FORTIFY, (not RPATH)
|
||||
// Calculate score: enabled positive flags / total expected positive flags
|
||||
// Exclude RPATH from positive scoring (it's a negative if present)
|
||||
var positiveFlags = new[] {
|
||||
HardeningFlagType.Pie,
|
||||
HardeningFlagType.Nx,
|
||||
HardeningFlagType.RelroFull,
|
||||
HardeningFlagType.StackCanary,
|
||||
HardeningFlagType.Fortify
|
||||
};
|
||||
|
||||
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(
|
||||
@@ -166,4 +632,18 @@ public sealed class ElfHardeningExtractor : IHardeningExtractor
|
||||
? BinaryPrimitives.ReadUInt16LittleEndian(span)
|
||||
: BinaryPrimitives.ReadUInt16BigEndian(span);
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(ReadOnlySpan<byte> span, bool littleEndian)
|
||||
{
|
||||
return littleEndian
|
||||
? BinaryPrimitives.ReadUInt32LittleEndian(span)
|
||||
: BinaryPrimitives.ReadUInt32BigEndian(span);
|
||||
}
|
||||
|
||||
private static ulong ReadUInt64(ReadOnlySpan<byte> span, bool littleEndian)
|
||||
{
|
||||
return littleEndian
|
||||
? BinaryPrimitives.ReadUInt64LittleEndian(span)
|
||||
: BinaryPrimitives.ReadUInt64BigEndian(span);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// MachoHardeningExtractor.cs
|
||||
// Sprint: SPRINT_3500_0004_0001_smart_diff_binary_output
|
||||
// Task: SDIFF-BIN-013a - Implement MachO hardening extractor (bonus)
|
||||
// Description: Extracts security hardening flags from macOS Mach-O binaries
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace StellaOps.Scanner.Analyzers.Native.Hardening;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts hardening flags from macOS Mach-O binaries.
|
||||
/// Detects PIE, code signing, RESTRICT, hardened runtime, and more.
|
||||
/// Per Sprint 3500.4 - Smart-Diff Binary Analysis.
|
||||
/// </summary>
|
||||
public sealed class MachoHardeningExtractor : IHardeningExtractor
|
||||
{
|
||||
// Mach-O magic numbers
|
||||
private const uint MH_MAGIC = 0xFEEDFACE; // 32-bit
|
||||
private const uint MH_CIGAM = 0xCEFAEDFE; // 32-bit (reversed)
|
||||
private const uint MH_MAGIC_64 = 0xFEEDFACF; // 64-bit
|
||||
private const uint MH_CIGAM_64 = 0xCFFAEDFE; // 64-bit (reversed)
|
||||
private const uint FAT_MAGIC = 0xCAFEBABE; // Universal binary
|
||||
private const uint FAT_CIGAM = 0xBEBAFECA; // Universal (reversed)
|
||||
|
||||
// Mach-O header flags (from mach/loader.h)
|
||||
private const uint MH_PIE = 0x00200000; // Position Independent Executable
|
||||
private const uint MH_NO_HEAP_EXECUTION = 0x01000000; // No heap execution
|
||||
private const uint MH_ALLOW_STACK_EXECUTION = 0x00020000; // Allow stack execution (bad!)
|
||||
private const uint MH_NOFIXPREBINDING = 0x00000400;
|
||||
private const uint MH_TWOLEVEL = 0x00000080; // Two-level namespace
|
||||
|
||||
// Load command types
|
||||
private const uint LC_SEGMENT = 0x01;
|
||||
private const uint LC_SEGMENT_64 = 0x19;
|
||||
private const uint LC_CODE_SIGNATURE = 0x1D;
|
||||
private const uint LC_ENCRYPTION_INFO = 0x21;
|
||||
private const uint LC_ENCRYPTION_INFO_64 = 0x2C;
|
||||
private const uint LC_DYLD_INFO = 0x22;
|
||||
private const uint LC_DYLD_INFO_ONLY = 0x80000022;
|
||||
private const uint LC_DYLIB_CODE_SIGN_DRS = 0x2F;
|
||||
private const uint LC_BUILD_VERSION = 0x32;
|
||||
private const uint LC_RPATH = 0x8000001C;
|
||||
|
||||
// Segment flags
|
||||
private const uint SG_PROTECTED_VERSION_1 = 0x08;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BinaryFormat SupportedFormat => BinaryFormat.MachO;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanExtract(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
// Mach-O can be .dylib, .bundle, or extensionless executables
|
||||
return ext is ".dylib" or ".bundle" or ".framework" or ""
|
||||
|| Path.GetFileName(path).StartsWith("lib", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanExtract(ReadOnlySpan<byte> header)
|
||||
{
|
||||
if (header.Length < 4) return false;
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(header);
|
||||
return magic is MH_MAGIC or MH_CIGAM or MH_MAGIC_64 or MH_CIGAM_64 or FAT_MAGIC or FAT_CIGAM;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryHardeningFlags> ExtractAsync(string path, string digest, CancellationToken ct = default)
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
return await ExtractAsync(stream, path, digest, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryHardeningFlags> ExtractAsync(Stream stream, string path, string digest, CancellationToken ct = default)
|
||||
{
|
||||
var flags = new List<HardeningFlag>();
|
||||
var missing = new List<string>();
|
||||
|
||||
// Read full file into memory
|
||||
using var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms, ct);
|
||||
var data = ms.ToArray();
|
||||
|
||||
if (data.Length < 28) // Minimum Mach-O header
|
||||
{
|
||||
return CreateResult(path, digest, [], ["Invalid Mach-O: too small"]);
|
||||
}
|
||||
|
||||
// Check magic and determine endianness
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(0, 4));
|
||||
var isLittleEndian = magic is MH_CIGAM or MH_CIGAM_64;
|
||||
var is64Bit = magic is MH_MAGIC_64 or MH_CIGAM_64;
|
||||
|
||||
// Handle universal binaries - just extract first architecture for now
|
||||
if (magic is FAT_MAGIC or FAT_CIGAM)
|
||||
{
|
||||
var fatResult = ExtractFromFat(data, path, digest);
|
||||
if (fatResult is not null)
|
||||
return fatResult;
|
||||
return CreateResult(path, digest, [], ["Universal binary: no supported architectures"]);
|
||||
}
|
||||
|
||||
// Normalize magic
|
||||
magic = isLittleEndian
|
||||
? BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(0, 4))
|
||||
: BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(0, 4));
|
||||
|
||||
if (magic is not (MH_MAGIC or MH_MAGIC_64))
|
||||
{
|
||||
return CreateResult(path, digest, [], ["Invalid Mach-O magic"]);
|
||||
}
|
||||
|
||||
// Parse header
|
||||
var headerSize = is64Bit ? 32 : 28;
|
||||
if (data.Length < headerSize)
|
||||
{
|
||||
return CreateResult(path, digest, [], ["Invalid Mach-O: truncated header"]);
|
||||
}
|
||||
|
||||
var headerFlags = ReadUInt32(data, is64Bit ? 24 : 24, isLittleEndian);
|
||||
var ncmds = ReadUInt32(data, is64Bit ? 16 : 16, isLittleEndian);
|
||||
var sizeofcmds = ReadUInt32(data, is64Bit ? 20 : 20, isLittleEndian);
|
||||
|
||||
// === Check PIE flag ===
|
||||
var hasPie = (headerFlags & MH_PIE) != 0;
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Pie, hasPie, hasPie ? "enabled" : null, "MH_FLAGS"));
|
||||
if (!hasPie) missing.Add("PIE");
|
||||
|
||||
// === Check for heap execution ===
|
||||
var noHeapExec = (headerFlags & MH_NO_HEAP_EXECUTION) != 0;
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Nx, noHeapExec, noHeapExec ? "no_heap_exec" : null, "MH_FLAGS"));
|
||||
|
||||
// === Check for stack execution (inverted - presence is BAD) ===
|
||||
var allowsStackExec = (headerFlags & MH_ALLOW_STACK_EXECUTION) != 0;
|
||||
if (allowsStackExec)
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Nx, false, "stack_exec_allowed", "MH_FLAGS"));
|
||||
missing.Add("NX");
|
||||
}
|
||||
|
||||
// === Parse load commands ===
|
||||
var hasCodeSignature = false;
|
||||
var hasEncryption = false;
|
||||
var hasRpath = false;
|
||||
var hasHardenedRuntime = false;
|
||||
var hasRestrict = false;
|
||||
|
||||
var offset = headerSize;
|
||||
for (var i = 0; i < ncmds && offset + 8 <= data.Length; i++)
|
||||
{
|
||||
var cmd = ReadUInt32(data, offset, isLittleEndian);
|
||||
var cmdsize = ReadUInt32(data, offset + 4, isLittleEndian);
|
||||
|
||||
if (cmdsize < 8 || offset + cmdsize > data.Length)
|
||||
break;
|
||||
|
||||
switch (cmd)
|
||||
{
|
||||
case LC_CODE_SIGNATURE:
|
||||
hasCodeSignature = true;
|
||||
break;
|
||||
|
||||
case LC_ENCRYPTION_INFO:
|
||||
case LC_ENCRYPTION_INFO_64:
|
||||
// Check if cryptid is non-zero (actually encrypted)
|
||||
var cryptid = ReadUInt32(data, offset + (cmd == LC_ENCRYPTION_INFO_64 ? 16 : 12), isLittleEndian);
|
||||
hasEncryption = cryptid != 0;
|
||||
break;
|
||||
|
||||
case LC_RPATH:
|
||||
hasRpath = true;
|
||||
break;
|
||||
|
||||
case LC_BUILD_VERSION:
|
||||
// Check for hardened runtime flag in build version
|
||||
if (cmdsize >= 24)
|
||||
{
|
||||
var ntools = ReadUInt32(data, offset + 20, isLittleEndian);
|
||||
// Hardened runtime is indicated by certain build flags
|
||||
// This is a simplification - full check requires parsing tool entries
|
||||
hasHardenedRuntime = ntools > 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case LC_SEGMENT:
|
||||
case LC_SEGMENT_64:
|
||||
// Check for __RESTRICT segment
|
||||
var nameLen = cmd == LC_SEGMENT_64 ? 16 : 16;
|
||||
if (cmdsize > nameLen + 8)
|
||||
{
|
||||
var segname = System.Text.Encoding.ASCII.GetString(data, offset + 8, nameLen).TrimEnd('\0');
|
||||
if (segname == "__RESTRICT")
|
||||
{
|
||||
hasRestrict = true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
offset += (int)cmdsize;
|
||||
}
|
||||
|
||||
// Add code signing flag
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Authenticode, hasCodeSignature, hasCodeSignature ? "signed" : null, "LC_CODE_SIGNATURE"));
|
||||
if (!hasCodeSignature) missing.Add("CODE_SIGN");
|
||||
|
||||
// Add RESTRICT flag (prevents DYLD_ env vars)
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Restrict, hasRestrict, hasRestrict ? "enabled" : null, "__RESTRICT segment"));
|
||||
|
||||
// Add RPATH flag (presence can be a security concern)
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.Rpath, hasRpath, hasRpath ? "present" : null, "LC_RPATH"));
|
||||
|
||||
// Add encryption flag
|
||||
if (hasEncryption)
|
||||
{
|
||||
flags.Add(new HardeningFlag(HardeningFlagType.ForceIntegrity, true, "encrypted", "LC_ENCRYPTION_INFO"));
|
||||
}
|
||||
|
||||
return CreateResult(path, digest, flags, missing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract hardening info from the first slice of a universal (fat) binary.
|
||||
/// </summary>
|
||||
private BinaryHardeningFlags? ExtractFromFat(byte[] data, string path, string digest)
|
||||
{
|
||||
if (data.Length < 8) return null;
|
||||
|
||||
var magic = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(0, 4));
|
||||
var isLittleEndian = magic == FAT_CIGAM;
|
||||
|
||||
var nfat = ReadUInt32(data, 4, isLittleEndian);
|
||||
if (nfat == 0 || data.Length < 8 + nfat * 20)
|
||||
return null;
|
||||
|
||||
// Get first architecture offset and size
|
||||
var archOffset = ReadUInt32(data, 16, isLittleEndian);
|
||||
var archSize = ReadUInt32(data, 20, isLittleEndian);
|
||||
|
||||
if (archOffset + archSize > data.Length)
|
||||
return null;
|
||||
|
||||
// Extract first architecture and re-parse
|
||||
var sliceData = data.AsSpan((int)archOffset, (int)archSize).ToArray();
|
||||
using var sliceStream = new MemoryStream(sliceData);
|
||||
return ExtractAsync(sliceStream, path, digest).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static uint ReadUInt32(byte[] data, int offset, bool littleEndian)
|
||||
{
|
||||
return littleEndian
|
||||
? BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(offset, 4))
|
||||
: BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset, 4));
|
||||
}
|
||||
|
||||
private static BinaryHardeningFlags CreateResult(
|
||||
string path,
|
||||
string digest,
|
||||
List<HardeningFlag> flags,
|
||||
List<string> missing)
|
||||
{
|
||||
// Calculate score based on key flags
|
||||
var positiveFlags = new[]
|
||||
{
|
||||
HardeningFlagType.Pie,
|
||||
HardeningFlagType.Nx,
|
||||
HardeningFlagType.Authenticode, // Code signing
|
||||
HardeningFlagType.Restrict
|
||||
};
|
||||
|
||||
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.MachO,
|
||||
Path: path,
|
||||
Digest: digest,
|
||||
Flags: [.. flags],
|
||||
HardeningScore: Math.Round(score, 2),
|
||||
MissingFlags: [.. missing],
|
||||
ExtractedAt: DateTimeOffset.UtcNow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class PeHardeningExtractor : IHardeningExtractor
|
||||
{
|
||||
// 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;
|
||||
|
||||
/// <inheritdoc />
|
||||
public BinaryFormat SupportedFormat => BinaryFormat.Pe;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanExtract(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
return ext is ".exe" or ".dll" or ".sys" or ".ocx" or ".scr";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CanExtract(ReadOnlySpan<byte> header)
|
||||
{
|
||||
if (header.Length < 2) return false;
|
||||
var magic = BinaryPrimitives.ReadUInt16LittleEndian(header);
|
||||
return magic == DOS_MAGIC;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryHardeningFlags> ExtractAsync(string path, string digest, CancellationToken ct = default)
|
||||
{
|
||||
await using var stream = File.OpenRead(path);
|
||||
return await ExtractAsync(stream, path, digest, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<BinaryHardeningFlags> ExtractAsync(Stream stream, string path, string digest, CancellationToken ct = default)
|
||||
{
|
||||
var flags = new List<HardeningFlag>();
|
||||
var missing = new List<string>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if PE has Authenticode signature by examining Security Directory.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check for /GS buffer security by examining Load Config Directory.
|
||||
/// </summary>
|
||||
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 static BinaryHardeningFlags CreateResult(
|
||||
string path,
|
||||
string digest,
|
||||
List<HardeningFlag> flags,
|
||||
List<string> 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: DateTimeOffset.UtcNow);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user