- 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.
408 lines
14 KiB
C#
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
|