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:
master
2025-12-17 18:02:37 +02:00
parent 394b57f6bf
commit 8bbfe4d2d2
211 changed files with 47179 additions and 1590 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}