Files
git.stella-ops.org/src/Scanner/StellaOps.Scanner.Analyzers.Native/NativeFormatDetector.cs
master 00d2c99af9 feat: add Attestation Chain and Triage Evidence API clients and models
- 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.
2025-12-18 13:15:13 +02:00

408 lines
14 KiB
C#

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<byte> 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<byte> 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<byte> 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<byte> 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