using System; using System.Buffers.Binary; using System.IO; using System.Text; namespace StellaOps.Scanner.Analyzers.Native; public static class NativeFormatDetector { public static bool TryDetect(Stream stream, out NativeBinaryIdentity identity, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(stream); using var buffer = new MemoryStream(); stream.CopyTo(buffer); var data = buffer.ToArray(); var span = data.AsSpan(); if (span.Length < 4) { identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null); return false; } if (IsElf(span, out identity)) { return true; } if (IsPe(span, out identity)) { return true; } if (IsMachO(span, out identity)) { return true; } identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null); return false; } private static bool IsElf(ReadOnlySpan span, out NativeBinaryIdentity identity) { identity = default!; if (span.Length < 20) { return false; } if (span[0] != 0x7F || span[1] != (byte)'E' || span[2] != (byte)'L' || span[3] != (byte)'F') { return false; } var elfClass = span[4]; // 1=32,2=64 var dataEncoding = span[5]; // 1=LE,2=BE var osAbi = span[7]; var endianness = dataEncoding == 2 ? "be" : "le"; ushort machine; if (dataEncoding == 2) { machine = BinaryPrimitives.ReadUInt16BigEndian(span.Slice(18, 2)); } else { machine = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(18, 2)); } var arch = MapElfMachine(machine); var os = MapElfOs(osAbi); var phoff = dataEncoding == 2 ? BinaryPrimitives.ReadUInt64BigEndian(span.Slice(32, 8)) : BinaryPrimitives.ReadUInt64LittleEndian(span.Slice(32, 8)); var phentsize = dataEncoding == 2 ? BinaryPrimitives.ReadUInt16BigEndian(span.Slice(54, 2)) : BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(54, 2)); var phnum = dataEncoding == 2 ? BinaryPrimitives.ReadUInt16BigEndian(span.Slice(56, 2)) : BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(56, 2)); string? buildId = null; string? interp = null; if (phentsize > 0 && phnum > 0 && phoff > 0) { for (var i = 0; i < phnum; i++) { var entryOffset = (long)(phoff + (ulong)(i * phentsize)); if (entryOffset + phentsize > span.Length) { break; } var phSpan = span.Slice((int)entryOffset, phentsize); var pType = dataEncoding == 2 ? BinaryPrimitives.ReadUInt32BigEndian(phSpan) : BinaryPrimitives.ReadUInt32LittleEndian(phSpan); if (pType == 3 && interp is null) // PT_INTERP { ulong offset = dataEncoding == 2 ? BinaryPrimitives.ReadUInt64BigEndian(phSpan.Slice(8, 8)) : BinaryPrimitives.ReadUInt64LittleEndian(phSpan.Slice(8, 8)); uint fileSize = dataEncoding == 2 ? BinaryPrimitives.ReadUInt32BigEndian(phSpan.Slice(32, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(phSpan.Slice(32, 4)); if (fileSize > 0 && offset + fileSize <= (ulong)span.Length) { var interpSpan = span.Slice((int)offset, (int)fileSize); var terminator = interpSpan.IndexOf((byte)0); var count = terminator >= 0 ? terminator : interpSpan.Length; var str = Encoding.ASCII.GetString(interpSpan[..count]); if (!string.IsNullOrWhiteSpace(str)) { interp = str; } } } else if (pType == 4 && buildId is null) // PT_NOTE { ulong offset = dataEncoding == 2 ? BinaryPrimitives.ReadUInt64BigEndian(phSpan.Slice(8, 8)) : BinaryPrimitives.ReadUInt64LittleEndian(phSpan.Slice(8, 8)); uint fileSize = dataEncoding == 2 ? BinaryPrimitives.ReadUInt32BigEndian(phSpan.Slice(32, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(phSpan.Slice(32, 4)); if (fileSize > 0 && offset + fileSize <= (ulong)span.Length) { var noteSpan = span.Slice((int)offset, (int)fileSize); buildId ??= ParseElfNote(noteSpan, dataEncoding == 2); } } if (buildId is not null && interp is not null) { break; } } } identity = new NativeBinaryIdentity(NativeFormat.Elf, arch, os, endianness, buildId, null, interp); return true; } private static bool IsPe(ReadOnlySpan span, out NativeBinaryIdentity identity) { identity = default!; if (span.Length < 0x40) { return false; } if (span[0] != 'M' || span[1] != 'Z') { return false; } var peHeaderOffset = BinaryPrimitives.ReadInt32LittleEndian(span.Slice(0x3C, 4)); if (peHeaderOffset < 0 || peHeaderOffset + 6 > span.Length) { return false; } if (peHeaderOffset + 6 > span.Length) { identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null); return false; } if (span[peHeaderOffset] != 'P' || span[peHeaderOffset + 1] != 'E' || span[peHeaderOffset + 2] != 0 || span[peHeaderOffset + 3] != 0) { return false; } // Try full PE parsing for CodeView GUID and other identity info if (PeReader.TryExtractIdentity(span, out var peIdentity) && peIdentity is not null) { identity = new NativeBinaryIdentity( NativeFormat.Pe, peIdentity.Machine, "windows", Endianness: "le", BuildId: null, Uuid: null, InterpreterPath: null, CodeViewGuid: peIdentity.CodeViewGuid, CodeViewAge: peIdentity.CodeViewAge, ProductVersion: peIdentity.ProductVersion); return true; } // Fallback to basic parsing var machine = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(peHeaderOffset + 4, 2)); var arch = MapPeMachine(machine); identity = new NativeBinaryIdentity(NativeFormat.Pe, arch, "windows", Endianness: "le", BuildId: null, Uuid: null, InterpreterPath: null); return true; } private static bool IsMachO(ReadOnlySpan span, out NativeBinaryIdentity identity) { identity = default!; if (span.Length < 12) { return false; } var magic = BinaryPrimitives.ReadUInt32BigEndian(span); var isFat = magic is 0xCAFEBABE or 0xBEBAFECA; var is64 = magic is 0xFEEDFACF or 0xCFFAEDFE; var is32 = magic is 0xFEEDFACE or 0xCEFAEDFE; if (!(isFat || is64 || is32)) { return false; } // Try full parsing with MachOReader using var stream = new MemoryStream(span.ToArray()); if (MachOReader.TryExtractIdentity(stream, out var machOIdentity) && machOIdentity is not null) { var endianness = magic is 0xCAFEBABE or 0xFEEDFACE or 0xFEEDFACF ? "be" : "le"; var prefixedUuid = machOIdentity.Uuid is not null ? $"macho-uuid:{machOIdentity.Uuid}" : null; identity = new NativeBinaryIdentity( NativeFormat.MachO, machOIdentity.CpuType, "darwin", Endianness: endianness, BuildId: prefixedUuid, Uuid: prefixedUuid, InterpreterPath: null, MachOPlatform: machOIdentity.Platform, MachOMinOsVersion: machOIdentity.MinOsVersion, MachOSdkVersion: machOIdentity.SdkVersion, MachOCdHash: machOIdentity.CodeSignature?.CdHash, MachOTeamId: machOIdentity.CodeSignature?.TeamId); return true; } // Fallback to basic parsing bool bigEndian = magic is 0xCAFEBABE or 0xFEEDFACE or 0xFEEDFACF; uint cputype; if (isFat) { var cputypeOffset = 8; // first architecture entry if (span.Length < cputypeOffset + 4) { identity = new NativeBinaryIdentity(NativeFormat.Unknown, null, null, null, null, null, null); return false; } cputype = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(cputypeOffset, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(cputypeOffset, 4)); } else { cputype = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(4, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(4, 4)); } var arch = MapMachCpuType(cputype); var fallbackEndianness = bigEndian ? "be" : "le"; string? uuid = null; if (!isFat) { var headerSize = is64 ? 32 : 28; if (span.Length >= headerSize + 8) { var ncmds = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(16, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(16, 4)); var offset = headerSize; for (uint i = 0; i < ncmds && offset + 8 <= span.Length; i++) { var cmd = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(offset, 4)); var cmdsize = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(span.Slice(offset + 4, 4)) : BinaryPrimitives.ReadUInt32LittleEndian(span.Slice(offset + 4, 4)); if (cmd == 0x1B && cmdsize >= 24 && offset + cmdsize <= span.Length) // LC_UUID { var uuidSpan = span.Slice(offset + 8, 16); var rawUuid = Convert.ToHexString(uuidSpan.ToArray()).ToLowerInvariant(); uuid = $"macho-uuid:{rawUuid}"; break; } if (cmdsize == 0) { break; } offset += (int)cmdsize; } } } // Store Mach-O UUID in BuildId field (prefixed) and also in Uuid for backwards compatibility identity = new NativeBinaryIdentity(NativeFormat.MachO, arch, "darwin", Endianness: fallbackEndianness, BuildId: uuid, Uuid: uuid, InterpreterPath: null); return true; } private static string? MapElfMachine(ushort machine) => machine switch { 0x03 => "x86", 0x08 => "mips", 0x14 => "powerpc", 0x28 => "arm", 0x32 => "ia64", 0x3E => "x86_64", 0xB7 => "aarch64", _ => null, }; private static string? MapElfOs(byte abi) => abi switch { 0x00 => "linux", 0x03 => "linux", 0x06 => "solaris", 0x07 => "aix", 0x08 => "irix", 0x09 => "freebsd", 0x0C => "openbsd", _ => null, }; private static string? MapPeMachine(ushort machine) => machine switch { 0x014c => "x86", 0x0200 => "ia64", 0x8664 => "x86_64", 0x01c0 => "arm", 0x01c4 => "armv7", 0xAA64 => "arm64", _ => null, }; private static string? MapMachCpuType(uint cpuType) => cpuType switch { 0x00000007 => "x86", 0x01000007 => "x86_64", 0x0000000C => "arm", 0x0100000C => "arm64", _ => null, }; private static string? ParseElfNote(ReadOnlySpan note, bool bigEndian) { var offset = 0; while (offset + 12 <= note.Length) { var namesz = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(note.Slice(offset)) : BinaryPrimitives.ReadUInt32LittleEndian(note.Slice(offset)); var descsz = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(note.Slice(offset + 4)) : BinaryPrimitives.ReadUInt32LittleEndian(note.Slice(offset + 4)); var type = bigEndian ? BinaryPrimitives.ReadUInt32BigEndian(note.Slice(offset + 8)) : BinaryPrimitives.ReadUInt32LittleEndian(note.Slice(offset + 8)); var nameStart = offset + 12; var namePadded = AlignTo4(namesz); var descStart = nameStart + namePadded; var descPadded = AlignTo4(descsz); var next = descStart + descPadded; if (next > note.Length) { break; } if (type == 3 && namesz >= 3) { var name = note.Slice(nameStart, (int)Math.Min(namesz, (uint)(note.Length - nameStart))); if (name[0] == (byte)'G' && name[1] == (byte)'N' && name[2] == (byte)'U') { var desc = note.Slice(descStart, (int)Math.Min(descsz, (uint)(note.Length - descStart))); var rawBuildId = Convert.ToHexString(desc).ToLowerInvariant(); return $"gnu-build-id:{rawBuildId}"; } } offset = next; } return null; } private static int AlignTo4(uint value) => (int)((value + 3) & ~3u); } #pragma warning restore CA2022