using System.Buffers.Binary; using System.Text; namespace StellaOps.Scanner.Analyzers.Native; /// /// Parses Mach-O load commands to extract dependencies, rpaths, and UUIDs. /// public static class MachOLoadCommandParser { // Mach-O magic numbers private const uint MH_MAGIC = 0xFEEDFACE; // 32-bit little endian private const uint MH_CIGAM = 0xCEFAEDFE; // 32-bit big endian private const uint MH_MAGIC_64 = 0xFEEDFACF; // 64-bit little endian private const uint MH_CIGAM_64 = 0xCFFAEDFE; // 64-bit big endian private const uint FAT_MAGIC = 0xCAFEBABE; // Fat binary big endian private const uint FAT_CIGAM = 0xBEBAFECA; // Fat binary little endian // Load commands private const uint LC_LOAD_DYLIB = 0x0C; private const uint LC_LOAD_WEAK_DYLIB = 0x80000018; private const uint LC_REEXPORT_DYLIB = 0x8000001F; private const uint LC_LAZY_LOAD_DYLIB = 0x20; private const uint LC_RPATH = 0x8000001C; private const uint LC_UUID = 0x1B; /// /// Parses Mach-O load commands from a stream. /// public static bool TryParse(Stream stream, out MachOImportInfo importInfo, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(stream); importInfo = new MachOImportInfo(false, []); using var buffer = new MemoryStream(); stream.CopyTo(buffer); var data = buffer.ToArray(); var span = data.AsSpan(); if (span.Length < 4) { return false; } var magic = BinaryPrimitives.ReadUInt32BigEndian(span); // Check for fat binary if (magic == FAT_MAGIC || magic == FAT_CIGAM) { return TryParseFatBinary(span, magic == FAT_CIGAM, out importInfo); } // Check for single architecture binary if (magic == MH_MAGIC || magic == MH_CIGAM || magic == MH_MAGIC_64 || magic == MH_CIGAM_64) { if (TryParseSingleArchitecture(span, 0, out var slice)) { importInfo = new MachOImportInfo(false, [slice]); return true; } } return false; } private static bool TryParseFatBinary(ReadOnlySpan span, bool swapped, out MachOImportInfo importInfo) { importInfo = new MachOImportInfo(true, []); if (span.Length < 8) { return false; } var nfat_arch = swapped ? BinaryPrimitives.ReverseEndianness(BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4, 4))) : BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4, 4)); if (nfat_arch > 20) // Sanity check { return false; } var slices = new List(); var fatArchSize = 20; // sizeof(fat_arch) for 32-bit fat header var offset = 8; for (var i = 0; i < nfat_arch; i++) { if (offset + fatArchSize > span.Length) { break; } var archOffset = swapped ? BinaryPrimitives.ReverseEndianness(BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset + 8, 4))) : BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset + 8, 4)); if (archOffset < span.Length && TryParseSingleArchitecture(span, (int)archOffset, out var slice)) { slices.Add(slice); } offset += fatArchSize; } importInfo = new MachOImportInfo(true, slices); return slices.Count > 0; } private static bool TryParseSingleArchitecture(ReadOnlySpan span, int baseOffset, out MachOSlice slice) { slice = new MachOSlice(null, 0, null, [], []); if (baseOffset + 28 > span.Length) { return false; } var localSpan = span[baseOffset..]; var magic = BinaryPrimitives.ReadUInt32LittleEndian(localSpan); bool is64Bit; bool bigEndian; switch (magic) { case MH_MAGIC: is64Bit = false; bigEndian = false; break; case MH_CIGAM: is64Bit = false; bigEndian = true; break; case MH_MAGIC_64: is64Bit = true; bigEndian = false; break; case MH_CIGAM_64: is64Bit = true; bigEndian = true; break; default: return false; } // Parse mach_header(_64) var cputype = ReadUInt32(localSpan, 4, bigEndian); var cpusubtype = ReadUInt32(localSpan, 8, bigEndian); var ncmds = ReadUInt32(localSpan, 16, bigEndian); var sizeofcmds = ReadUInt32(localSpan, 20, bigEndian); var headerSize = is64Bit ? 32 : 28; var cmdOffset = headerSize; string? uuid = null; var rpaths = new List(); var dependencies = new List(); var seenPaths = new HashSet(StringComparer.Ordinal); for (var i = 0; i < ncmds; i++) { if (cmdOffset + 8 > localSpan.Length) { break; } var cmd = ReadUInt32(localSpan, cmdOffset, bigEndian); var cmdsize = ReadUInt32(localSpan, cmdOffset + 4, bigEndian); if (cmdsize < 8 || cmdOffset + cmdsize > localSpan.Length) { break; } switch (cmd) { case LC_UUID when cmdsize >= 24: uuid = ParseUuid(localSpan.Slice(cmdOffset + 8, 16)); break; case LC_RPATH: var rpathStr = ParseLoadCommandString(localSpan, cmdOffset, cmdsize, bigEndian); if (!string.IsNullOrEmpty(rpathStr)) { rpaths.Add(rpathStr); } break; case LC_LOAD_DYLIB: AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-loadlib", seenPaths, dependencies); break; case LC_LOAD_WEAK_DYLIB: AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-weaklib", seenPaths, dependencies); break; case LC_REEXPORT_DYLIB: AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-reexport", seenPaths, dependencies); break; case LC_LAZY_LOAD_DYLIB: AddDependency(localSpan, cmdOffset, cmdsize, bigEndian, "macho-lazylib", seenPaths, dependencies); break; } cmdOffset += (int)cmdsize; } var cpuTypeStr = MapCpuType(cputype); slice = new MachOSlice(cpuTypeStr, cpusubtype, uuid, rpaths, dependencies); return true; } private static void AddDependency( ReadOnlySpan span, int cmdOffset, uint cmdsize, bool bigEndian, string reasonCode, HashSet seenPaths, List dependencies) { // dylib_command structure: // uint32_t cmd, cmdsize // lc_str name (offset from start of load command) // uint32_t timestamp // uint32_t current_version // uint32_t compatibility_version if (cmdsize < 24) { return; } var nameOffset = ReadUInt32(span, cmdOffset + 8, bigEndian); var currentVersion = ReadUInt32(span, cmdOffset + 16, bigEndian); var compatVersion = ReadUInt32(span, cmdOffset + 20, bigEndian); var pathEnd = (int)Math.Min(cmdsize, (uint)(span.Length - cmdOffset)); var nameStart = cmdOffset + (int)nameOffset; if (nameStart >= span.Length || nameStart >= cmdOffset + pathEnd) { return; } var nameSpan = span.Slice(nameStart, Math.Min(pathEnd - (int)nameOffset, span.Length - nameStart)); var nullIndex = nameSpan.IndexOf((byte)0); var nameLength = nullIndex >= 0 ? nullIndex : nameSpan.Length; var path = Encoding.UTF8.GetString(nameSpan[..nameLength]); if (string.IsNullOrEmpty(path) || !seenPaths.Add(path)) { return; } var currentVersionStr = FormatVersion(currentVersion); var compatVersionStr = FormatVersion(compatVersion); dependencies.Add(new MachODeclaredDependency(path, reasonCode, currentVersionStr, compatVersionStr)); } private static string? ParseLoadCommandString(ReadOnlySpan span, int cmdOffset, uint cmdsize, bool bigEndian) { // LC_RPATH structure: // uint32_t cmd, cmdsize // lc_str path (offset from start of load command) if (cmdsize < 12) { return null; } var pathOffset = ReadUInt32(span, cmdOffset + 8, bigEndian); var pathEnd = (int)Math.Min(cmdsize, (uint)(span.Length - cmdOffset)); var pathStart = cmdOffset + (int)pathOffset; if (pathStart >= span.Length || pathStart >= cmdOffset + pathEnd) { return null; } var pathSpan = span.Slice(pathStart, Math.Min(pathEnd - (int)pathOffset, span.Length - pathStart)); var nullIndex = pathSpan.IndexOf((byte)0); var pathLength = nullIndex >= 0 ? nullIndex : pathSpan.Length; return Encoding.UTF8.GetString(pathSpan[..pathLength]); } private static string ParseUuid(ReadOnlySpan uuidBytes) { // Format as standard UUID: 8-4-4-4-12 (all lowercase) return ($"{Convert.ToHexString(uuidBytes[..4])}-{Convert.ToHexString(uuidBytes.Slice(4, 2))}-" + $"{Convert.ToHexString(uuidBytes.Slice(6, 2))}-{Convert.ToHexString(uuidBytes.Slice(8, 2))}-" + $"{Convert.ToHexString(uuidBytes.Slice(10, 6))}").ToLowerInvariant(); } private static string FormatVersion(uint version) { // Mach-O version format: xxxx.yy.zz encoded as (xxxx << 16) | (yy << 8) | zz var major = version >> 16; var minor = (version >> 8) & 0xFF; var patch = version & 0xFF; return $"{major}.{minor}.{patch}"; } private static uint ReadUInt32(ReadOnlySpan span, int offset, bool bigEndian) { return bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(offset, 4)); } private static string? MapCpuType(uint cpuType) => cpuType switch { 0x00000001 => "vax", 0x00000006 => "mc680x0", 0x00000007 => "x86", 0x01000007 => "x86_64", 0x0000000A => "mc98000", 0x0000000B => "hppa", 0x0000000C => "arm", 0x0100000C => "arm64", 0x0200000C => "arm64_32", 0x0000000D => "mc88000", 0x0000000E => "sparc", 0x0000000F => "i860", 0x00000012 => "powerpc", 0x01000012 => "powerpc64", _ => null, }; }