- Implemented Attestation Chain API client with methods for verifying, fetching, and managing attestation chains. - Created models for Attestation Chain, including DSSE envelope structures and verification results. - Developed Triage Evidence API client for fetching finding evidence, including methods for evidence retrieval by CVE and component. - Added models for Triage Evidence, encapsulating evidence responses, entry points, boundary proofs, and VEX evidence. - Introduced mock implementations for both API clients to facilitate testing and development.
641 lines
21 KiB
C#
641 lines
21 KiB
C#
using System.Buffers.Binary;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace StellaOps.Scanner.Analyzers.Native;
|
|
|
|
/// <summary>
|
|
/// Result from parsing a Mach-O file.
|
|
/// </summary>
|
|
/// <param name="Path">File path.</param>
|
|
/// <param name="LayerDigest">Container layer digest if applicable.</param>
|
|
/// <param name="Identities">List of identities (one per slice in fat binary).</param>
|
|
public sealed record MachOParseResult(
|
|
string Path,
|
|
string? LayerDigest,
|
|
IReadOnlyList<MachOIdentity> Identities);
|
|
|
|
/// <summary>
|
|
/// Full Mach-O file reader with identity extraction.
|
|
/// Handles both single-arch and fat (universal) binaries.
|
|
/// </summary>
|
|
public static class MachOReader
|
|
{
|
|
// Mach-O magic numbers
|
|
private const uint MH_MAGIC = 0xFEEDFACE; // 32-bit, native endian
|
|
private const uint MH_CIGAM = 0xCEFAEDFE; // 32-bit, reversed endian
|
|
private const uint MH_MAGIC_64 = 0xFEEDFACF; // 64-bit, native endian
|
|
private const uint MH_CIGAM_64 = 0xCFFAEDFE; // 64-bit, reversed endian
|
|
|
|
// Fat binary magic numbers
|
|
private const uint FAT_MAGIC = 0xCAFEBABE; // Big-endian
|
|
private const uint FAT_CIGAM = 0xBEBAFECA; // Little-endian
|
|
|
|
// Load command types
|
|
private const uint LC_UUID = 0x1B;
|
|
private const uint LC_CODE_SIGNATURE = 0x1D;
|
|
private const uint LC_VERSION_MIN_MACOSX = 0x24;
|
|
private const uint LC_VERSION_MIN_IPHONEOS = 0x25;
|
|
private const uint LC_VERSION_MIN_WATCHOS = 0x30;
|
|
private const uint LC_VERSION_MIN_TVOS = 0x2F;
|
|
private const uint LC_BUILD_VERSION = 0x32;
|
|
private const uint LC_DYLD_INFO = 0x22;
|
|
private const uint LC_DYLD_INFO_ONLY = 0x80000022;
|
|
private const uint LC_DYLD_EXPORTS_TRIE = 0x80000033;
|
|
|
|
// Code signature blob types
|
|
private const uint CSMAGIC_CODEDIRECTORY = 0xFADE0C02;
|
|
private const uint CSMAGIC_EMBEDDED_SIGNATURE = 0xFADE0CC0;
|
|
private const uint CSMAGIC_EMBEDDED_ENTITLEMENTS = 0xFADE7171;
|
|
|
|
// CPU types
|
|
private const int CPU_TYPE_X86 = 7;
|
|
private const int CPU_TYPE_X86_64 = CPU_TYPE_X86 | 0x01000000;
|
|
private const int CPU_TYPE_ARM = 12;
|
|
private const int CPU_TYPE_ARM64 = CPU_TYPE_ARM | 0x01000000;
|
|
|
|
/// <summary>
|
|
/// Parse a Mach-O file and extract full identity information.
|
|
/// For fat binaries, returns identities for all slices.
|
|
/// </summary>
|
|
public static MachOParseResult? Parse(Stream stream, string path, string? layerDigest = null)
|
|
{
|
|
if (!TryReadBytes(stream, 4, out var magicBytes))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
stream.Position = 0;
|
|
var magic = BinaryPrimitives.ReadUInt32BigEndian(magicBytes);
|
|
|
|
// Check for fat binary
|
|
if (magic is FAT_MAGIC or FAT_CIGAM)
|
|
{
|
|
var identities = ParseFatBinary(stream);
|
|
return identities.Count > 0
|
|
? new MachOParseResult(path, layerDigest, identities)
|
|
: null;
|
|
}
|
|
|
|
// Single architecture binary
|
|
var identity = ParseSingleMachO(stream);
|
|
return identity is not null
|
|
? new MachOParseResult(path, layerDigest, [identity])
|
|
: null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to extract just the identity without full parsing.
|
|
/// </summary>
|
|
public static bool TryExtractIdentity(Stream stream, out MachOIdentity? identity)
|
|
{
|
|
identity = null;
|
|
|
|
if (!TryReadBytes(stream, 4, out var magicBytes))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
stream.Position = 0;
|
|
var magic = BinaryPrimitives.ReadUInt32BigEndian(magicBytes);
|
|
|
|
// Skip fat binary quick extraction for now
|
|
if (magic is FAT_MAGIC or FAT_CIGAM)
|
|
{
|
|
var identities = ParseFatBinary(stream);
|
|
identity = identities.Count > 0 ? identities[0] : null;
|
|
return identity is not null;
|
|
}
|
|
|
|
identity = ParseSingleMachO(stream);
|
|
return identity is not null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a fat binary and return all slice identities.
|
|
/// </summary>
|
|
public static IReadOnlyList<MachOIdentity> ParseFatBinary(Stream stream)
|
|
{
|
|
var identities = new List<MachOIdentity>();
|
|
|
|
if (!TryReadBytes(stream, 8, out var headerBytes))
|
|
{
|
|
return identities;
|
|
}
|
|
|
|
var magic = BinaryPrimitives.ReadUInt32BigEndian(headerBytes);
|
|
var swapBytes = magic == FAT_CIGAM;
|
|
var nfatArch = swapBytes
|
|
? BinaryPrimitives.ReadUInt32LittleEndian(headerBytes.AsSpan(4))
|
|
: BinaryPrimitives.ReadUInt32BigEndian(headerBytes.AsSpan(4));
|
|
|
|
if (nfatArch > 100)
|
|
{
|
|
// Sanity check
|
|
return identities;
|
|
}
|
|
|
|
for (var i = 0; i < nfatArch; i++)
|
|
{
|
|
if (!TryReadBytes(stream, 20, out var archBytes))
|
|
{
|
|
break;
|
|
}
|
|
|
|
// Fat arch structure is always big-endian (unless FAT_CIGAM)
|
|
uint offset, size;
|
|
if (swapBytes)
|
|
{
|
|
// cputype(4), cpusubtype(4), offset(4), size(4), align(4)
|
|
offset = BinaryPrimitives.ReadUInt32LittleEndian(archBytes.AsSpan(8));
|
|
size = BinaryPrimitives.ReadUInt32LittleEndian(archBytes.AsSpan(12));
|
|
}
|
|
else
|
|
{
|
|
offset = BinaryPrimitives.ReadUInt32BigEndian(archBytes.AsSpan(8));
|
|
size = BinaryPrimitives.ReadUInt32BigEndian(archBytes.AsSpan(12));
|
|
}
|
|
|
|
// Save position and parse the embedded Mach-O
|
|
var currentPos = stream.Position;
|
|
stream.Position = offset;
|
|
|
|
var sliceIdentity = ParseSingleMachO(stream, isFatSlice: true);
|
|
if (sliceIdentity is not null)
|
|
{
|
|
identities.Add(sliceIdentity);
|
|
}
|
|
|
|
stream.Position = currentPos;
|
|
}
|
|
|
|
return identities;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse a single Mach-O binary (not fat).
|
|
/// </summary>
|
|
private static MachOIdentity? ParseSingleMachO(Stream stream, bool isFatSlice = false)
|
|
{
|
|
var startOffset = stream.Position;
|
|
|
|
if (!TryReadBytes(stream, 4, out var magicBytes))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var magic = BinaryPrimitives.ReadUInt32LittleEndian(magicBytes);
|
|
bool is64Bit;
|
|
bool swapBytes;
|
|
|
|
switch (magic)
|
|
{
|
|
case MH_MAGIC:
|
|
is64Bit = false;
|
|
swapBytes = false;
|
|
break;
|
|
case MH_CIGAM:
|
|
is64Bit = false;
|
|
swapBytes = true;
|
|
break;
|
|
case MH_MAGIC_64:
|
|
is64Bit = true;
|
|
swapBytes = false;
|
|
break;
|
|
case MH_CIGAM_64:
|
|
is64Bit = true;
|
|
swapBytes = true;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
// Read rest of Mach header
|
|
var headerSize = is64Bit ? 32 : 28;
|
|
stream.Position = startOffset;
|
|
|
|
if (!TryReadBytes(stream, headerSize, out var headerBytes))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// Parse header
|
|
var cpuType = ReadInt32(headerBytes, 4, swapBytes);
|
|
var cpuSubtype = ReadUInt32(headerBytes, 8, swapBytes);
|
|
var ncmds = ReadUInt32(headerBytes, 16, swapBytes);
|
|
var sizeofcmds = ReadUInt32(headerBytes, 20, swapBytes);
|
|
|
|
var cpuTypeName = GetCpuTypeName(cpuType);
|
|
|
|
// Initialize identity fields
|
|
string? uuid = null;
|
|
var platform = MachOPlatform.Unknown;
|
|
string? minOsVersion = null;
|
|
string? sdkVersion = null;
|
|
MachOCodeSignature? codeSignature = null;
|
|
var exports = new List<string>();
|
|
|
|
// Read load commands
|
|
var loadCommandsStart = stream.Position;
|
|
var loadCommandsEnd = loadCommandsStart + sizeofcmds;
|
|
|
|
for (uint cmd = 0; cmd < ncmds && stream.Position < loadCommandsEnd; cmd++)
|
|
{
|
|
if (!TryReadBytes(stream, 8, out var cmdHeader))
|
|
{
|
|
break;
|
|
}
|
|
|
|
var cmdType = ReadUInt32(cmdHeader, 0, swapBytes);
|
|
var cmdSize = ReadUInt32(cmdHeader, 4, swapBytes);
|
|
|
|
if (cmdSize < 8)
|
|
{
|
|
break;
|
|
}
|
|
|
|
var cmdDataSize = (int)cmdSize - 8;
|
|
|
|
switch (cmdType)
|
|
{
|
|
case LC_UUID when cmdDataSize >= 16:
|
|
if (TryReadBytes(stream, 16, out var uuidBytes))
|
|
{
|
|
uuid = Convert.ToHexStringLower(uuidBytes);
|
|
}
|
|
|
|
stream.Position = loadCommandsStart + GetNextCmdOffset(cmd, ncmds, stream.Position - loadCommandsStart, cmdSize);
|
|
continue;
|
|
|
|
case LC_BUILD_VERSION when cmdDataSize >= 16:
|
|
if (TryReadBytes(stream, cmdDataSize, out var buildVersionBytes))
|
|
{
|
|
var platformValue = ReadUInt32(buildVersionBytes, 0, swapBytes);
|
|
platform = (MachOPlatform)platformValue;
|
|
|
|
var minos = ReadUInt32(buildVersionBytes, 4, swapBytes);
|
|
minOsVersion = FormatVersion(minos);
|
|
|
|
var sdk = ReadUInt32(buildVersionBytes, 8, swapBytes);
|
|
sdkVersion = FormatVersion(sdk);
|
|
}
|
|
|
|
continue;
|
|
|
|
case LC_VERSION_MIN_MACOSX:
|
|
case LC_VERSION_MIN_IPHONEOS:
|
|
case LC_VERSION_MIN_WATCHOS:
|
|
case LC_VERSION_MIN_TVOS:
|
|
if (TryReadBytes(stream, cmdDataSize, out var versionMinBytes))
|
|
{
|
|
if (platform == MachOPlatform.Unknown)
|
|
{
|
|
platform = cmdType switch
|
|
{
|
|
LC_VERSION_MIN_MACOSX => MachOPlatform.MacOS,
|
|
LC_VERSION_MIN_IPHONEOS => MachOPlatform.iOS,
|
|
LC_VERSION_MIN_WATCHOS => MachOPlatform.WatchOS,
|
|
LC_VERSION_MIN_TVOS => MachOPlatform.TvOS,
|
|
_ => MachOPlatform.Unknown
|
|
};
|
|
}
|
|
|
|
if (versionMinBytes.Length >= 8)
|
|
{
|
|
var version = ReadUInt32(versionMinBytes, 0, swapBytes);
|
|
if (minOsVersion is null)
|
|
{
|
|
minOsVersion = FormatVersion(version);
|
|
}
|
|
|
|
var sdk = ReadUInt32(versionMinBytes, 4, swapBytes);
|
|
if (sdkVersion is null)
|
|
{
|
|
sdkVersion = FormatVersion(sdk);
|
|
}
|
|
}
|
|
}
|
|
|
|
continue;
|
|
|
|
case LC_CODE_SIGNATURE:
|
|
if (TryReadBytes(stream, cmdDataSize, out var codeSignBytes) && codeSignBytes.Length >= 8)
|
|
{
|
|
var dataOff = ReadUInt32(codeSignBytes, 0, swapBytes);
|
|
var dataSize = ReadUInt32(codeSignBytes, 4, swapBytes);
|
|
|
|
// Parse code signature at offset
|
|
var currentPos = stream.Position;
|
|
stream.Position = startOffset + dataOff;
|
|
|
|
codeSignature = ParseCodeSignature(stream, (int)dataSize);
|
|
|
|
stream.Position = currentPos;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Skip remaining bytes of command
|
|
var remaining = cmdDataSize - (stream.Position - loadCommandsStart - 8);
|
|
if (remaining > 0)
|
|
{
|
|
stream.Position += remaining;
|
|
}
|
|
}
|
|
|
|
return new MachOIdentity(
|
|
cpuTypeName,
|
|
cpuSubtype,
|
|
uuid,
|
|
isFatSlice,
|
|
platform,
|
|
minOsVersion,
|
|
sdkVersion,
|
|
codeSignature,
|
|
exports);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse the code signature blob.
|
|
/// </summary>
|
|
private static MachOCodeSignature? ParseCodeSignature(Stream stream, int size)
|
|
{
|
|
if (!TryReadBytes(stream, 8, out var superBlobHeader))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var magic = BinaryPrimitives.ReadUInt32BigEndian(superBlobHeader);
|
|
if (magic != CSMAGIC_EMBEDDED_SIGNATURE)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var length = BinaryPrimitives.ReadUInt32BigEndian(superBlobHeader.AsSpan(4));
|
|
if (length > size || length < 12)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (!TryReadBytes(stream, 4, out var countBytes))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var count = BinaryPrimitives.ReadUInt32BigEndian(countBytes);
|
|
if (count > 100)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var blobStart = stream.Position - 12;
|
|
|
|
// Read blob index entries
|
|
var blobs = new List<(uint type, uint offset)>();
|
|
for (uint i = 0; i < count; i++)
|
|
{
|
|
if (!TryReadBytes(stream, 8, out var indexEntry))
|
|
{
|
|
break;
|
|
}
|
|
|
|
var blobType = BinaryPrimitives.ReadUInt32BigEndian(indexEntry);
|
|
var blobOffset = BinaryPrimitives.ReadUInt32BigEndian(indexEntry.AsSpan(4));
|
|
blobs.Add((blobType, blobOffset));
|
|
}
|
|
|
|
string? teamId = null;
|
|
string? signingId = null;
|
|
string? cdHash = null;
|
|
var hasHardenedRuntime = false;
|
|
var entitlements = new List<string>();
|
|
|
|
foreach (var (blobType, blobOffset) in blobs)
|
|
{
|
|
stream.Position = blobStart + blobOffset;
|
|
|
|
if (!TryReadBytes(stream, 8, out var blobHeader))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var blobMagic = BinaryPrimitives.ReadUInt32BigEndian(blobHeader);
|
|
var blobLength = BinaryPrimitives.ReadUInt32BigEndian(blobHeader.AsSpan(4));
|
|
|
|
switch (blobMagic)
|
|
{
|
|
case CSMAGIC_CODEDIRECTORY:
|
|
(teamId, signingId, cdHash, hasHardenedRuntime) = ParseCodeDirectory(stream, blobStart + blobOffset, (int)blobLength);
|
|
break;
|
|
|
|
case CSMAGIC_EMBEDDED_ENTITLEMENTS:
|
|
entitlements = ParseEntitlements(stream, (int)blobLength - 8);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (teamId is null && signingId is null && cdHash is null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return new MachOCodeSignature(teamId, signingId, cdHash, hasHardenedRuntime, entitlements);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse CodeDirectory blob.
|
|
/// </summary>
|
|
private static (string? TeamId, string? SigningId, string? CdHash, bool HasHardenedRuntime) ParseCodeDirectory(
|
|
Stream stream, long blobStart, int length)
|
|
{
|
|
// CodeDirectory has a complex structure, we'll extract key fields
|
|
stream.Position = blobStart;
|
|
|
|
if (!TryReadBytes(stream, Math.Min(length, 52), out var cdBytes))
|
|
{
|
|
return (null, null, null, false);
|
|
}
|
|
|
|
// Offsets in CodeDirectory (all big-endian)
|
|
// +8: version
|
|
// +12: flags
|
|
// +16: hashOffset
|
|
// +20: identOffset
|
|
// +28: nCodeSlots
|
|
// +32: codeLimit
|
|
// +36: hashSize
|
|
// +37: hashType
|
|
// +38: platform
|
|
// +39: pageSize
|
|
// +44: spare2
|
|
// +48: scatterOffset (v2+)
|
|
// +52: teamOffset (v2+)
|
|
|
|
var version = BinaryPrimitives.ReadUInt32BigEndian(cdBytes.AsSpan(8));
|
|
var flags = BinaryPrimitives.ReadUInt32BigEndian(cdBytes.AsSpan(12));
|
|
var identOffset = BinaryPrimitives.ReadUInt32BigEndian(cdBytes.AsSpan(20));
|
|
|
|
// Check for hardened runtime (flag 0x10000)
|
|
var hasHardenedRuntime = (flags & 0x10000) != 0;
|
|
|
|
// Read signing identifier
|
|
string? signingId = null;
|
|
if (identOffset > 0 && identOffset < length)
|
|
{
|
|
stream.Position = blobStart + identOffset;
|
|
signingId = ReadNullTerminatedString(stream, 256);
|
|
}
|
|
|
|
// Read team ID (version 0x20200 and later)
|
|
string? teamId = null;
|
|
if (version >= 0x20200 && cdBytes.Length >= 56)
|
|
{
|
|
var teamOffset = BinaryPrimitives.ReadUInt32BigEndian(cdBytes.AsSpan(52));
|
|
if (teamOffset > 0 && teamOffset < length)
|
|
{
|
|
stream.Position = blobStart + teamOffset;
|
|
teamId = ReadNullTerminatedString(stream, 20);
|
|
}
|
|
}
|
|
|
|
// Compute CDHash (SHA-256 of the entire CodeDirectory blob)
|
|
stream.Position = blobStart;
|
|
if (TryReadBytes(stream, length, out var fullCdBytes))
|
|
{
|
|
var hash = SHA256.HashData(fullCdBytes);
|
|
var cdHash = Convert.ToHexStringLower(hash);
|
|
return (teamId, signingId, cdHash, hasHardenedRuntime);
|
|
}
|
|
|
|
return (teamId, signingId, null, hasHardenedRuntime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse entitlements plist and extract keys.
|
|
/// </summary>
|
|
private static List<string> ParseEntitlements(Stream stream, int length)
|
|
{
|
|
var keys = new List<string>();
|
|
|
|
if (!TryReadBytes(stream, length, out var plistBytes))
|
|
{
|
|
return keys;
|
|
}
|
|
|
|
// Simple plist key extraction (looks for <key>...</key> patterns)
|
|
var plist = Encoding.UTF8.GetString(plistBytes);
|
|
|
|
var keyStart = 0;
|
|
while ((keyStart = plist.IndexOf("<key>", keyStart, StringComparison.Ordinal)) >= 0)
|
|
{
|
|
keyStart += 5;
|
|
var keyEnd = plist.IndexOf("</key>", keyStart, StringComparison.Ordinal);
|
|
if (keyEnd > keyStart)
|
|
{
|
|
var key = plist[keyStart..keyEnd];
|
|
if (!string.IsNullOrWhiteSpace(key))
|
|
{
|
|
keys.Add(key);
|
|
}
|
|
|
|
keyStart = keyEnd + 6;
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get CPU type name from CPU type value.
|
|
/// </summary>
|
|
private static string? GetCpuTypeName(int cpuType) => cpuType switch
|
|
{
|
|
CPU_TYPE_X86 => "i386",
|
|
CPU_TYPE_X86_64 => "x86_64",
|
|
CPU_TYPE_ARM => "arm",
|
|
CPU_TYPE_ARM64 => "arm64",
|
|
_ => $"cpu_{cpuType}"
|
|
};
|
|
|
|
/// <summary>
|
|
/// Format version number (major.minor.patch from packed uint32).
|
|
/// </summary>
|
|
private static string FormatVersion(uint version)
|
|
{
|
|
var major = (version >> 16) & 0xFFFF;
|
|
var minor = (version >> 8) & 0xFF;
|
|
var patch = version & 0xFF;
|
|
return patch == 0 ? $"{major}.{minor}" : $"{major}.{minor}.{patch}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read a null-terminated string from stream.
|
|
/// </summary>
|
|
private static string? ReadNullTerminatedString(Stream stream, int maxLength)
|
|
{
|
|
var bytes = new byte[maxLength];
|
|
var count = 0;
|
|
|
|
while (count < maxLength)
|
|
{
|
|
var b = stream.ReadByte();
|
|
if (b <= 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
bytes[count++] = (byte)b;
|
|
}
|
|
|
|
return count > 0 ? Encoding.UTF8.GetString(bytes, 0, count) : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Try to read exactly the specified number of bytes.
|
|
/// </summary>
|
|
private static bool TryReadBytes(Stream stream, int count, out byte[] bytes)
|
|
{
|
|
bytes = new byte[count];
|
|
var totalRead = 0;
|
|
while (totalRead < count)
|
|
{
|
|
var read = stream.Read(bytes, totalRead, count - totalRead);
|
|
if (read == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
totalRead += read;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read int32 with optional byte swapping.
|
|
/// </summary>
|
|
private static int ReadInt32(byte[] data, int offset, bool swap) =>
|
|
swap
|
|
? BinaryPrimitives.ReadInt32BigEndian(data.AsSpan(offset))
|
|
: BinaryPrimitives.ReadInt32LittleEndian(data.AsSpan(offset));
|
|
|
|
/// <summary>
|
|
/// Read uint32 with optional byte swapping.
|
|
/// </summary>
|
|
private static uint ReadUInt32(byte[] data, int offset, bool swap) =>
|
|
swap
|
|
? BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset))
|
|
: BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(offset));
|
|
|
|
/// <summary>
|
|
/// Calculate the offset for the next load command.
|
|
/// </summary>
|
|
private static long GetNextCmdOffset(uint currentCmd, uint totalCmds, long currentOffset, uint cmdSize) =>
|
|
currentOffset + cmdSize - 8;
|
|
}
|