up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user