// -----------------------------------------------------------------------------
// 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;
///
/// 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.
///
public sealed class MachoHardeningExtractor : IHardeningExtractor
{
private readonly TimeProvider _timeProvider;
public MachoHardeningExtractor(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
// 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;
///
public BinaryFormat SupportedFormat => BinaryFormat.MachO;
///
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);
}
///
public bool CanExtract(ReadOnlySpan 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;
}
///
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
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);
}
///
/// Extract hardening info from the first slice of a universal (fat) binary.
///
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 BinaryHardeningFlags CreateResult(
string path,
string digest,
List flags,
List 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: _timeProvider.GetUtcNow());
}
}