332 lines
11 KiB
C#
332 lines
11 KiB
C#
|
|
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Native;
|
|
|
|
/// <summary>
|
|
/// Parses Mach-O load commands to extract dependencies, rpaths, and UUIDs.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Parses Mach-O load commands from a stream.
|
|
/// </summary>
|
|
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<byte> 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<MachOSlice>();
|
|
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<byte> 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<string>();
|
|
var dependencies = new List<MachODeclaredDependency>();
|
|
var seenPaths = new HashSet<string>(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<byte> span, int cmdOffset, uint cmdsize, bool bigEndian,
|
|
string reasonCode, HashSet<string> seenPaths, List<MachODeclaredDependency> 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<byte> 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<byte> 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<byte> 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,
|
|
};
|
|
}
|