feat: Add native binary analyzer test utilities and implement SM2 signing tests
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Manifest Integrity / Audit SHA256SUMS Files (push) Has been cancelled
Manifest Integrity / Validate Schema Integrity (push) Has been cancelled
Manifest Integrity / Validate Contract Documents (push) Has been cancelled
Manifest Integrity / Validate Pack Fixtures (push) Has been cancelled
Manifest Integrity / Verify Merkle Roots (push) Has been cancelled
Scanner Analyzers / Build Analyzers (push) Has been cancelled
Scanner Analyzers / Discover Analyzers (push) Has been cancelled
Scanner Analyzers / Test Language Analyzers (push) Has been cancelled
Scanner Analyzers / Validate Test Fixtures (push) Has been cancelled
Scanner Analyzers / Verify Deterministic Output (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Export Center CI / export-ci (push) Has been cancelled
Notify Smoke Test / Notify Unit Tests (push) Has been cancelled
Notify Smoke Test / Notifier Service Tests (push) Has been cancelled
Notify Smoke Test / Notification Smoke Test (push) Has been cancelled

- Introduced `NativeTestBase` class for ELF, PE, and Mach-O binary parsing helpers and assertions.
- Created `TestCryptoFactory` for SM2 cryptographic provider setup and key generation.
- Implemented `Sm2SigningTests` to validate signing functionality with environment gate checks.
- Developed console export service and store with comprehensive unit tests for export status management.
This commit is contained in:
StellaOps Bot
2025-12-07 13:12:41 +02:00
parent d907729778
commit e53a282fbe
387 changed files with 21941 additions and 1518 deletions

View File

@@ -1,20 +1,19 @@
using System.Text;
using FluentAssertions;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class ElfDynamicSectionParserTests
public class ElfDynamicSectionParserTests : NativeTestBase
{
[Fact]
public void ParsesMinimalElfWithNoDynamicSection()
{
// Minimal ELF64 with no program headers (static binary scenario)
var buffer = new byte[64];
SetupElf64Header(buffer, littleEndian: true);
// Minimal ELF64 with no dependencies (static binary scenario)
var elf = ElfBuilder.Static().Build();
using var stream = new MemoryStream(buffer);
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
var result = TryParseElf(elf, out var info);
result.Should().BeTrue();
info.Dependencies.Should().BeEmpty();
@@ -25,72 +24,13 @@ public class ElfDynamicSectionParserTests
[Fact]
public void ParsesElfWithDtNeeded()
{
// Build a minimal ELF64 with PT_DYNAMIC containing DT_NEEDED entries
var buffer = new byte[2048];
SetupElf64Header(buffer, littleEndian: true);
// Build ELF with DT_NEEDED entries using the builder
var elf = ElfBuilder.LinuxX64()
.AddDependencies("libc.so.6", "libm.so.6", "libpthread.so.0")
.Build();
// String table at offset 0x400
var strtab = 0x400;
var str1Offset = 1; // Skip null byte at start
var str2Offset = str1Offset + WriteString(buffer, strtab + str1Offset, "libc.so.6") + 1;
var str3Offset = str2Offset + WriteString(buffer, strtab + str2Offset, "libm.so.6") + 1;
var strtabSize = str3Offset + WriteString(buffer, strtab + str3Offset, "libpthread.so.0") + 1;
var info = ParseElf(elf);
// Section headers at offset 0x600
var shoff = 0x600;
var shentsize = 64; // Elf64_Shdr size
var shnum = 2; // null + .dynstr
// Update ELF header with section header info
BitConverter.GetBytes((ulong)shoff).CopyTo(buffer, 40); // e_shoff
BitConverter.GetBytes((ushort)shentsize).CopyTo(buffer, 58); // e_shentsize
BitConverter.GetBytes((ushort)shnum).CopyTo(buffer, 60); // e_shnum
// Section header 0: null section
// Section header 1: .dynstr (type SHT_STRTAB = 3)
var sh1 = shoff + shentsize;
BitConverter.GetBytes((uint)3).CopyTo(buffer, sh1 + 4); // sh_type = SHT_STRTAB
BitConverter.GetBytes((ulong)0x400).CopyTo(buffer, sh1 + 16); // sh_addr (virtual address)
BitConverter.GetBytes((ulong)strtab).CopyTo(buffer, sh1 + 24); // sh_offset (file offset)
BitConverter.GetBytes((ulong)strtabSize).CopyTo(buffer, sh1 + 32); // sh_size
// Dynamic section at offset 0x200
var dynOffset = 0x200;
var dynEntrySize = 16; // Elf64_Dyn size
var dynIndex = 0;
// DT_STRTAB
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 5, 0x400); // DT_STRTAB = 5
// DT_STRSZ
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 10, (ulong)strtabSize); // DT_STRSZ = 10
// DT_NEEDED entries
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)str1Offset); // libc.so.6
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)str2Offset); // libm.so.6
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)str3Offset); // libpthread.so.0
// DT_NULL
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex, 0, 0);
var dynSize = dynEntrySize * (dynIndex + 1);
// Program header at offset 0x40 (right after ELF header)
var phoff = 0x40;
var phentsize = 56; // Elf64_Phdr size
var phnum = 1;
// Update ELF header with program header info
BitConverter.GetBytes((ulong)phoff).CopyTo(buffer, 32); // e_phoff
BitConverter.GetBytes((ushort)phentsize).CopyTo(buffer, 54); // e_phentsize
BitConverter.GetBytes((ushort)phnum).CopyTo(buffer, 56); // e_phnum
// PT_DYNAMIC program header
BitConverter.GetBytes((uint)2).CopyTo(buffer, phoff); // p_type = PT_DYNAMIC
BitConverter.GetBytes((ulong)dynOffset).CopyTo(buffer, phoff + 8); // p_offset
BitConverter.GetBytes((ulong)dynSize).CopyTo(buffer, phoff + 32); // p_filesz
using var stream = new MemoryStream(buffer);
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
result.Should().BeTrue();
info.Dependencies.Should().HaveCount(3);
info.Dependencies[0].Soname.Should().Be("libc.so.6");
info.Dependencies[0].ReasonCode.Should().Be("elf-dtneeded");
@@ -101,60 +41,14 @@ public class ElfDynamicSectionParserTests
[Fact]
public void ParsesElfWithRpathAndRunpath()
{
var buffer = new byte[2048];
SetupElf64Header(buffer, littleEndian: true);
// Build ELF with rpath and runpath using the builder
var elf = ElfBuilder.LinuxX64()
.WithRpath("/opt/lib", "/usr/local/lib")
.WithRunpath("$ORIGIN/../lib")
.Build();
// String table at offset 0x400
var strtab = 0x400;
var rpathOffset = 1;
var runpathOffset = rpathOffset + WriteString(buffer, strtab + rpathOffset, "/opt/lib:/usr/local/lib") + 1;
var strtabSize = runpathOffset + WriteString(buffer, strtab + runpathOffset, "$ORIGIN/../lib") + 1;
var info = ParseElf(elf);
// Section headers
var shoff = 0x600;
var shentsize = 64;
var shnum = 2;
BitConverter.GetBytes((ulong)shoff).CopyTo(buffer, 40);
BitConverter.GetBytes((ushort)shentsize).CopyTo(buffer, 58);
BitConverter.GetBytes((ushort)shnum).CopyTo(buffer, 60);
var sh1 = shoff + shentsize;
BitConverter.GetBytes((uint)3).CopyTo(buffer, sh1 + 4);
BitConverter.GetBytes((ulong)0x400).CopyTo(buffer, sh1 + 16);
BitConverter.GetBytes((ulong)strtab).CopyTo(buffer, sh1 + 24);
BitConverter.GetBytes((ulong)strtabSize).CopyTo(buffer, sh1 + 32);
// Dynamic section at offset 0x200
var dynOffset = 0x200;
var dynEntrySize = 16;
var dynIndex = 0;
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 5, 0x400); // DT_STRTAB
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 10, (ulong)strtabSize); // DT_STRSZ
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 15, (ulong)rpathOffset); // DT_RPATH
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 29, (ulong)runpathOffset); // DT_RUNPATH
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex, 0, 0); // DT_NULL
var dynSize = dynEntrySize * (dynIndex + 1);
// Program header
var phoff = 0x40;
var phentsize = 56;
var phnum = 1;
BitConverter.GetBytes((ulong)phoff).CopyTo(buffer, 32);
BitConverter.GetBytes((ushort)phentsize).CopyTo(buffer, 54);
BitConverter.GetBytes((ushort)phnum).CopyTo(buffer, 56);
BitConverter.GetBytes((uint)2).CopyTo(buffer, phoff);
BitConverter.GetBytes((ulong)dynOffset).CopyTo(buffer, phoff + 8);
BitConverter.GetBytes((ulong)dynSize).CopyTo(buffer, phoff + 32);
using var stream = new MemoryStream(buffer);
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
result.Should().BeTrue();
info.Rpath.Should().BeEquivalentTo(["/opt/lib", "/usr/local/lib"]);
info.Runpath.Should().BeEquivalentTo(["$ORIGIN/../lib"]);
}
@@ -162,49 +56,13 @@ public class ElfDynamicSectionParserTests
[Fact]
public void ParsesElfWithInterpreterAndBuildId()
{
var buffer = new byte[1024];
SetupElf64Header(buffer, littleEndian: true);
// Build ELF with interpreter and build ID using the builder
var elf = ElfBuilder.LinuxX64()
.WithBuildId("deadbeef0102030405060708090a0b0c")
.Build();
// Program headers at offset 0x40
var phoff = 0x40;
var phentsize = 56;
var phnum = 2;
var info = ParseElf(elf);
BitConverter.GetBytes((ulong)phoff).CopyTo(buffer, 32);
BitConverter.GetBytes((ushort)phentsize).CopyTo(buffer, 54);
BitConverter.GetBytes((ushort)phnum).CopyTo(buffer, 56);
// PT_INTERP
var ph0 = phoff;
var interpOffset = 0x200;
var interpData = "/lib64/ld-linux-x86-64.so.2\0"u8;
BitConverter.GetBytes((uint)3).CopyTo(buffer, ph0); // p_type = PT_INTERP
BitConverter.GetBytes((ulong)interpOffset).CopyTo(buffer, ph0 + 8); // p_offset
BitConverter.GetBytes((ulong)interpData.Length).CopyTo(buffer, ph0 + 32); // p_filesz
interpData.CopyTo(buffer.AsSpan(interpOffset));
// PT_NOTE with GNU build-id
var ph1 = phoff + phentsize;
var noteOffset = 0x300;
BitConverter.GetBytes((uint)4).CopyTo(buffer, ph1); // p_type = PT_NOTE
BitConverter.GetBytes((ulong)noteOffset).CopyTo(buffer, ph1 + 8); // p_offset
BitConverter.GetBytes((ulong)32).CopyTo(buffer, ph1 + 32); // p_filesz
// Build note structure
BitConverter.GetBytes((uint)4).CopyTo(buffer, noteOffset); // namesz
BitConverter.GetBytes((uint)16).CopyTo(buffer, noteOffset + 4); // descsz
BitConverter.GetBytes((uint)3).CopyTo(buffer, noteOffset + 8); // type = NT_GNU_BUILD_ID
"GNU\0"u8.CopyTo(buffer.AsSpan(noteOffset + 12)); // name
var buildIdBytes = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C };
buildIdBytes.CopyTo(buffer, noteOffset + 16);
using var stream = new MemoryStream(buffer);
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
result.Should().BeTrue();
info.Interpreter.Should().Be("/lib64/ld-linux-x86-64.so.2");
info.BinaryId.Should().Be("deadbeef0102030405060708090a0b0c");
}
@@ -212,57 +70,17 @@ public class ElfDynamicSectionParserTests
[Fact]
public void DeduplicatesDtNeededEntries()
{
var buffer = new byte[2048];
SetupElf64Header(buffer, littleEndian: true);
// ElfBuilder deduplicates internally, so add "duplicates" via builder
// The builder will produce correct output, and we verify the parser handles it
var elf = ElfBuilder.LinuxX64()
.AddDependency("libc.so.6")
.AddDependency("libc.so.6") // Duplicate - builder should handle this
.AddDependency("libc.so.6") // Triple duplicate
.Build();
var strtab = 0x400;
var str1Offset = 1;
var strtabSize = str1Offset + WriteString(buffer, strtab + str1Offset, "libc.so.6") + 1;
var info = ParseElf(elf);
var shoff = 0x600;
var shentsize = 64;
var shnum = 2;
BitConverter.GetBytes((ulong)shoff).CopyTo(buffer, 40);
BitConverter.GetBytes((ushort)shentsize).CopyTo(buffer, 58);
BitConverter.GetBytes((ushort)shnum).CopyTo(buffer, 60);
var sh1 = shoff + shentsize;
BitConverter.GetBytes((uint)3).CopyTo(buffer, sh1 + 4);
BitConverter.GetBytes((ulong)0x400).CopyTo(buffer, sh1 + 16);
BitConverter.GetBytes((ulong)strtab).CopyTo(buffer, sh1 + 24);
BitConverter.GetBytes((ulong)strtabSize).CopyTo(buffer, sh1 + 32);
var dynOffset = 0x200;
var dynEntrySize = 16;
var dynIndex = 0;
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 5, 0x400);
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 10, (ulong)strtabSize);
// Duplicate DT_NEEDED entries for same library
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)str1Offset);
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)str1Offset);
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)str1Offset);
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex, 0, 0);
var dynSize = dynEntrySize * (dynIndex + 1);
var phoff = 0x40;
var phentsize = 56;
var phnum = 1;
BitConverter.GetBytes((ulong)phoff).CopyTo(buffer, 32);
BitConverter.GetBytes((ushort)phentsize).CopyTo(buffer, 54);
BitConverter.GetBytes((ushort)phnum).CopyTo(buffer, 56);
BitConverter.GetBytes((uint)2).CopyTo(buffer, phoff);
BitConverter.GetBytes((ulong)dynOffset).CopyTo(buffer, phoff + 8);
BitConverter.GetBytes((ulong)dynSize).CopyTo(buffer, phoff + 32);
using var stream = new MemoryStream(buffer);
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
result.Should().BeTrue();
// Whether builder deduplicates or not, parser should return unique deps
info.Dependencies.Should().HaveCount(1);
info.Dependencies[0].Soname.Should().Be("libc.so.6");
}
@@ -291,136 +109,47 @@ public class ElfDynamicSectionParserTests
result.Should().BeFalse();
}
private static void SetupElf64Header(byte[] buffer, bool littleEndian)
{
// ELF magic
buffer[0] = 0x7F;
buffer[1] = (byte)'E';
buffer[2] = (byte)'L';
buffer[3] = (byte)'F';
buffer[4] = 0x02; // 64-bit
buffer[5] = littleEndian ? (byte)0x01 : (byte)0x02;
buffer[6] = 0x01; // ELF version
buffer[7] = 0x00; // System V ABI
// e_type at offset 16 (2 bytes)
buffer[16] = 0x02; // ET_EXEC
// e_machine at offset 18 (2 bytes)
buffer[18] = 0x3E; // x86_64
}
private static void WriteDynEntry64(byte[] buffer, int offset, ulong tag, ulong val)
{
BitConverter.GetBytes(tag).CopyTo(buffer, offset);
BitConverter.GetBytes(val).CopyTo(buffer, offset + 8);
}
private static int WriteString(byte[] buffer, int offset, string str)
{
var bytes = Encoding.UTF8.GetBytes(str);
bytes.CopyTo(buffer, offset);
buffer[offset + bytes.Length] = 0; // null terminator
return bytes.Length;
}
[Fact]
public void ParsesElfWithVersionNeeds()
{
// Test that version needs (GLIBC_2.17, etc.) are properly extracted
var buffer = new byte[4096];
SetupElf64Header(buffer, littleEndian: true);
var elf = ElfBuilder.LinuxX64()
.AddDependency("libc.so.6")
.AddVersionNeed("libc.so.6", "GLIBC_2.17", isWeak: false)
.AddVersionNeed("libc.so.6", "GLIBC_2.28", isWeak: false)
.Build();
// String table at offset 0x400
var strtab = 0x400;
var libcOffset = 1; // "libc.so.6"
var glibc217Offset = libcOffset + WriteString(buffer, strtab + libcOffset, "libc.so.6") + 1;
var glibc228Offset = glibc217Offset + WriteString(buffer, strtab + glibc217Offset, "GLIBC_2.17") + 1;
var strtabSize = glibc228Offset + WriteString(buffer, strtab + glibc228Offset, "GLIBC_2.28") + 1;
var info = ParseElf(elf);
// Section headers at offset 0x800
var shoff = 0x800;
var shentsize = 64;
var shnum = 3; // null + .dynstr + .gnu.version_r
BitConverter.GetBytes((ulong)shoff).CopyTo(buffer, 40);
BitConverter.GetBytes((ushort)shentsize).CopyTo(buffer, 58);
BitConverter.GetBytes((ushort)shnum).CopyTo(buffer, 60);
// Section header 0: null
// Section header 1: .dynstr
var sh1 = shoff + shentsize;
BitConverter.GetBytes((uint)3).CopyTo(buffer, sh1 + 4); // sh_type = SHT_STRTAB
BitConverter.GetBytes((ulong)0x400).CopyTo(buffer, sh1 + 16); // sh_addr
BitConverter.GetBytes((ulong)strtab).CopyTo(buffer, sh1 + 24); // sh_offset
BitConverter.GetBytes((ulong)strtabSize).CopyTo(buffer, sh1 + 32); // sh_size
// Section header 2: .gnu.version_r (SHT_GNU_verneed = 0x6ffffffe)
var verneedFileOffset = 0x600;
var sh2 = shoff + shentsize * 2;
BitConverter.GetBytes((uint)0x6ffffffe).CopyTo(buffer, sh2 + 4); // sh_type = SHT_GNU_verneed
BitConverter.GetBytes((ulong)0x600).CopyTo(buffer, sh2 + 16); // sh_addr (vaddr)
BitConverter.GetBytes((ulong)verneedFileOffset).CopyTo(buffer, sh2 + 24); // sh_offset
// Version needs section at offset 0x600
// Verneed entry for libc.so.6 with two version requirements
// Elf64_Verneed: vn_version(2), vn_cnt(2), vn_file(4), vn_aux(4), vn_next(4)
var verneedOffset = verneedFileOffset;
BitConverter.GetBytes((ushort)1).CopyTo(buffer, verneedOffset); // vn_version = 1
BitConverter.GetBytes((ushort)2).CopyTo(buffer, verneedOffset + 2); // vn_cnt = 2 aux entries
BitConverter.GetBytes((uint)libcOffset).CopyTo(buffer, verneedOffset + 4); // vn_file -> "libc.so.6"
BitConverter.GetBytes((uint)16).CopyTo(buffer, verneedOffset + 8); // vn_aux = 16 (offset to first aux)
BitConverter.GetBytes((uint)0).CopyTo(buffer, verneedOffset + 12); // vn_next = 0 (last entry)
// Vernaux entries
// Elf64_Vernaux: vna_hash(4), vna_flags(2), vna_other(2), vna_name(4), vna_next(4)
var aux1Offset = verneedOffset + 16;
BitConverter.GetBytes((uint)0x0d696910).CopyTo(buffer, aux1Offset); // vna_hash for GLIBC_2.17
BitConverter.GetBytes((ushort)0).CopyTo(buffer, aux1Offset + 4); // vna_flags
BitConverter.GetBytes((ushort)2).CopyTo(buffer, aux1Offset + 6); // vna_other
BitConverter.GetBytes((uint)glibc217Offset).CopyTo(buffer, aux1Offset + 8); // vna_name -> "GLIBC_2.17"
BitConverter.GetBytes((uint)16).CopyTo(buffer, aux1Offset + 12); // vna_next = 16 (offset to next aux)
var aux2Offset = aux1Offset + 16;
BitConverter.GetBytes((uint)0x09691974).CopyTo(buffer, aux2Offset); // vna_hash for GLIBC_2.28
BitConverter.GetBytes((ushort)0).CopyTo(buffer, aux2Offset + 4);
BitConverter.GetBytes((ushort)3).CopyTo(buffer, aux2Offset + 6);
BitConverter.GetBytes((uint)glibc228Offset).CopyTo(buffer, aux2Offset + 8); // vna_name -> "GLIBC_2.28"
BitConverter.GetBytes((uint)0).CopyTo(buffer, aux2Offset + 12); // vna_next = 0 (last aux)
// Dynamic section at offset 0x200
var dynOffset = 0x200;
var dynEntrySize = 16;
var dynIndex = 0;
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 5, 0x400); // DT_STRTAB
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 10, (ulong)strtabSize); // DT_STRSZ
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 1, (ulong)libcOffset); // DT_NEEDED -> libc.so.6
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 0x6ffffffe, 0x600); // DT_VERNEED (vaddr)
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex++, 0x6fffffff, 1); // DT_VERNEEDNUM = 1
WriteDynEntry64(buffer, dynOffset + dynEntrySize * dynIndex, 0, 0); // DT_NULL
var dynSize = dynEntrySize * (dynIndex + 1);
// Program header
var phoff = 0x40;
var phentsize = 56;
var phnum = 1;
BitConverter.GetBytes((ulong)phoff).CopyTo(buffer, 32);
BitConverter.GetBytes((ushort)phentsize).CopyTo(buffer, 54);
BitConverter.GetBytes((ushort)phnum).CopyTo(buffer, 56);
BitConverter.GetBytes((uint)2).CopyTo(buffer, phoff); // PT_DYNAMIC
BitConverter.GetBytes((ulong)dynOffset).CopyTo(buffer, phoff + 8);
BitConverter.GetBytes((ulong)dynSize).CopyTo(buffer, phoff + 32);
using var stream = new MemoryStream(buffer);
var result = ElfDynamicSectionParser.TryParse(stream, out var info);
result.Should().BeTrue();
info.Dependencies.Should().HaveCount(1);
info.Dependencies[0].Soname.Should().Be("libc.so.6");
info.Dependencies[0].VersionNeeds.Should().HaveCount(2);
info.Dependencies[0].VersionNeeds.Should().Contain(v => v.Version == "GLIBC_2.17");
info.Dependencies[0].VersionNeeds.Should().Contain(v => v.Version == "GLIBC_2.28");
}
[Fact]
public void ParsesElfWithWeakVersionNeeds()
{
// Test that weak version requirements (VER_FLG_WEAK) are properly detected
var elf = ElfBuilder.LinuxX64()
.AddDependency("libc.so.6")
.AddVersionNeed("libc.so.6", "GLIBC_2.17", isWeak: false) // Required version
.AddVersionNeed("libc.so.6", "GLIBC_2.34", isWeak: true) // Weak/optional version
.Build();
var info = ParseElf(elf);
info.Dependencies.Should().HaveCount(1);
info.Dependencies[0].Soname.Should().Be("libc.so.6");
info.Dependencies[0].VersionNeeds.Should().HaveCount(2);
// GLIBC_2.17 should NOT be weak
var glibc217 = info.Dependencies[0].VersionNeeds.First(v => v.Version == "GLIBC_2.17");
glibc217.IsWeak.Should().BeFalse();
// GLIBC_2.34 should BE weak
var glibc234 = info.Dependencies[0].VersionNeeds.First(v => v.Version == "GLIBC_2.34");
glibc234.IsWeak.Should().BeTrue();
}
}

View File

@@ -0,0 +1,256 @@
using System.Buffers.Binary;
using System.Text;
namespace StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
/// <summary>
/// Low-level byte manipulation utilities for building binary fixtures.
/// All methods are deterministic and produce reproducible output.
/// </summary>
public static class BinaryBufferWriter
{
#region Little-Endian Writers
/// <summary>
/// Writes a 16-bit unsigned integer in little-endian format.
/// </summary>
public static void WriteU16LE(Span<byte> buffer, int offset, ushort value)
{
BinaryPrimitives.WriteUInt16LittleEndian(buffer.Slice(offset, 2), value);
}
/// <summary>
/// Writes a 32-bit unsigned integer in little-endian format.
/// </summary>
public static void WriteU32LE(Span<byte> buffer, int offset, uint value)
{
BinaryPrimitives.WriteUInt32LittleEndian(buffer.Slice(offset, 4), value);
}
/// <summary>
/// Writes a 64-bit unsigned integer in little-endian format.
/// </summary>
public static void WriteU64LE(Span<byte> buffer, int offset, ulong value)
{
BinaryPrimitives.WriteUInt64LittleEndian(buffer.Slice(offset, 8), value);
}
/// <summary>
/// Writes a 32-bit signed integer in little-endian format.
/// </summary>
public static void WriteI32LE(Span<byte> buffer, int offset, int value)
{
BinaryPrimitives.WriteInt32LittleEndian(buffer.Slice(offset, 4), value);
}
#endregion
#region Big-Endian Writers
/// <summary>
/// Writes a 16-bit unsigned integer in big-endian format.
/// </summary>
public static void WriteU16BE(Span<byte> buffer, int offset, ushort value)
{
BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(offset, 2), value);
}
/// <summary>
/// Writes a 32-bit unsigned integer in big-endian format.
/// </summary>
public static void WriteU32BE(Span<byte> buffer, int offset, uint value)
{
BinaryPrimitives.WriteUInt32BigEndian(buffer.Slice(offset, 4), value);
}
/// <summary>
/// Writes a 64-bit unsigned integer in big-endian format.
/// </summary>
public static void WriteU64BE(Span<byte> buffer, int offset, ulong value)
{
BinaryPrimitives.WriteUInt64BigEndian(buffer.Slice(offset, 8), value);
}
#endregion
#region Endian-Aware Writers
/// <summary>
/// Writes a 16-bit unsigned integer with specified endianness.
/// </summary>
public static void WriteU16(Span<byte> buffer, int offset, ushort value, bool bigEndian)
{
if (bigEndian)
WriteU16BE(buffer, offset, value);
else
WriteU16LE(buffer, offset, value);
}
/// <summary>
/// Writes a 32-bit unsigned integer with specified endianness.
/// </summary>
public static void WriteU32(Span<byte> buffer, int offset, uint value, bool bigEndian)
{
if (bigEndian)
WriteU32BE(buffer, offset, value);
else
WriteU32LE(buffer, offset, value);
}
/// <summary>
/// Writes a 64-bit unsigned integer with specified endianness.
/// </summary>
public static void WriteU64(Span<byte> buffer, int offset, ulong value, bool bigEndian)
{
if (bigEndian)
WriteU64BE(buffer, offset, value);
else
WriteU64LE(buffer, offset, value);
}
#endregion
#region String Writers
/// <summary>
/// Writes a null-terminated UTF-8 string and returns the number of bytes written (including null terminator).
/// </summary>
public static int WriteNullTerminatedString(Span<byte> buffer, int offset, string str)
{
var bytes = Encoding.UTF8.GetBytes(str);
bytes.CopyTo(buffer.Slice(offset));
buffer[offset + bytes.Length] = 0;
return bytes.Length + 1;
}
/// <summary>
/// Writes a null-terminated string from raw bytes and returns the number of bytes written.
/// </summary>
public static int WriteNullTerminatedBytes(Span<byte> buffer, int offset, ReadOnlySpan<byte> data)
{
data.CopyTo(buffer.Slice(offset));
buffer[offset + data.Length] = 0;
return data.Length + 1;
}
/// <summary>
/// Writes a UTF-8 string with padding to a fixed length.
/// </summary>
public static void WritePaddedString(Span<byte> buffer, int offset, string str, int totalLength)
{
var bytes = Encoding.UTF8.GetBytes(str);
if (bytes.Length > totalLength)
throw new ArgumentException($"String '{str}' is longer than {totalLength} bytes", nameof(str));
bytes.CopyTo(buffer.Slice(offset));
// Zero-fill the rest
buffer.Slice(offset + bytes.Length, totalLength - bytes.Length).Clear();
}
/// <summary>
/// Gets the UTF-8 byte length of a string.
/// </summary>
public static int GetUtf8Length(string str) => Encoding.UTF8.GetByteCount(str);
/// <summary>
/// Gets the UTF-8 byte length of a string plus null terminator.
/// </summary>
public static int GetNullTerminatedLength(string str) => Encoding.UTF8.GetByteCount(str) + 1;
#endregion
#region Alignment Utilities
/// <summary>
/// Rounds a value up to the next multiple of alignment.
/// </summary>
public static int AlignTo(int value, int alignment)
{
if (alignment <= 0)
throw new ArgumentOutOfRangeException(nameof(alignment), "Alignment must be positive");
return (value + alignment - 1) & ~(alignment - 1);
}
/// <summary>
/// Rounds a value up to the next 4-byte boundary.
/// </summary>
public static int AlignTo4(int value) => AlignTo(value, 4);
/// <summary>
/// Rounds a value up to the next 8-byte boundary.
/// </summary>
public static int AlignTo8(int value) => AlignTo(value, 8);
/// <summary>
/// Rounds a value up to the next 16-byte boundary.
/// </summary>
public static int AlignTo16(int value) => AlignTo(value, 16);
/// <summary>
/// Calculates the padding needed to align a value.
/// </summary>
public static int PaddingFor(int value, int alignment)
{
var aligned = AlignTo(value, alignment);
return aligned - value;
}
#endregion
#region Buffer Utilities
/// <summary>
/// Creates a zeroed buffer of the specified size.
/// </summary>
public static byte[] CreateBuffer(int size)
{
return new byte[size];
}
/// <summary>
/// Copies a span to a destination buffer at the specified offset.
/// </summary>
public static void CopyTo(ReadOnlySpan<byte> source, Span<byte> dest, int destOffset)
{
source.CopyTo(dest.Slice(destOffset));
}
/// <summary>
/// Fills a region of the buffer with a value.
/// </summary>
public static void Fill(Span<byte> buffer, int offset, int length, byte value)
{
buffer.Slice(offset, length).Fill(value);
}
/// <summary>
/// Clears a region of the buffer (fills with zeros).
/// </summary>
public static void Clear(Span<byte> buffer, int offset, int length)
{
buffer.Slice(offset, length).Clear();
}
#endregion
#region Raw Byte Writers
/// <summary>
/// Writes raw bytes to the buffer at the specified offset.
/// </summary>
public static void WriteBytes(Span<byte> buffer, int offset, ReadOnlySpan<byte> data)
{
data.CopyTo(buffer.Slice(offset));
}
/// <summary>
/// Writes a single byte to the buffer.
/// </summary>
public static void WriteByte(Span<byte> buffer, int offset, byte value)
{
buffer[offset] = value;
}
#endregion
}

View File

@@ -0,0 +1,604 @@
using System.Text;
namespace StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
/// <summary>
/// Specification for a version need entry in .gnu.version_r section.
/// </summary>
/// <param name="Version">The version string (e.g., "GLIBC_2.17").</param>
/// <param name="Hash">The ELF hash of the version string.</param>
/// <param name="IsWeak">True if VER_FLG_WEAK is set.</param>
public sealed record ElfVersionNeedSpec(string Version, uint Hash, bool IsWeak = false);
/// <summary>
/// Fluent builder for creating ELF binary fixtures.
/// Supports both 32-bit and 64-bit binaries with configurable endianness.
/// </summary>
public sealed class ElfBuilder
{
private bool _is64Bit = true;
private bool _isBigEndian = false;
private ushort _machine = 0x3E; // x86_64
private string? _interpreter;
private string? _buildId;
private readonly List<string> _dependencies = [];
private readonly List<string> _rpath = [];
private readonly List<string> _runpath = [];
private readonly Dictionary<string, List<ElfVersionNeedSpec>> _versionNeeds = new(StringComparer.Ordinal);
#region Configuration
/// <summary>
/// Sets whether to generate a 64-bit ELF.
/// </summary>
public ElfBuilder Is64Bit(bool value = true)
{
_is64Bit = value;
return this;
}
/// <summary>
/// Generates a 32-bit ELF.
/// </summary>
public ElfBuilder Is32Bit() => Is64Bit(false);
/// <summary>
/// Sets whether to use big-endian byte order.
/// </summary>
public ElfBuilder BigEndian(bool value = true)
{
_isBigEndian = value;
return this;
}
/// <summary>
/// Uses little-endian byte order.
/// </summary>
public ElfBuilder LittleEndian() => BigEndian(false);
/// <summary>
/// Sets the machine type (e_machine field).
/// </summary>
public ElfBuilder WithMachine(ushort machine)
{
_machine = machine;
return this;
}
#endregion
#region Basic Properties
/// <summary>
/// Sets the interpreter path (PT_INTERP).
/// </summary>
public ElfBuilder WithInterpreter(string path)
{
_interpreter = path;
return this;
}
/// <summary>
/// Sets the build ID (PT_NOTE with NT_GNU_BUILD_ID).
/// </summary>
/// <param name="hexBuildId">Hex-encoded build ID (e.g., "deadbeef01020304").</param>
public ElfBuilder WithBuildId(string hexBuildId)
{
_buildId = hexBuildId;
return this;
}
#endregion
#region Dependencies
/// <summary>
/// Adds a DT_NEEDED dependency.
/// </summary>
public ElfBuilder AddDependency(string soname)
{
_dependencies.Add(soname);
return this;
}
/// <summary>
/// Adds multiple DT_NEEDED dependencies.
/// </summary>
public ElfBuilder AddDependencies(params string[] sonames)
{
_dependencies.AddRange(sonames);
return this;
}
#endregion
#region Search Paths
/// <summary>
/// Adds DT_RPATH entries.
/// </summary>
public ElfBuilder WithRpath(params string[] paths)
{
_rpath.AddRange(paths);
return this;
}
/// <summary>
/// Adds DT_RUNPATH entries.
/// </summary>
public ElfBuilder WithRunpath(params string[] paths)
{
_runpath.AddRange(paths);
return this;
}
#endregion
#region Version Needs
/// <summary>
/// Adds a version need requirement for a dependency.
/// </summary>
/// <param name="soname">The shared library name (must be added as a dependency).</param>
/// <param name="version">The version string (e.g., "GLIBC_2.17").</param>
/// <param name="isWeak">Whether this is a weak (optional) version requirement.</param>
public ElfBuilder AddVersionNeed(string soname, string version, bool isWeak = false)
{
var hash = ComputeElfHash(version);
return AddVersionNeed(soname, new ElfVersionNeedSpec(version, hash, isWeak));
}
/// <summary>
/// Adds a version need requirement with explicit hash.
/// </summary>
public ElfBuilder AddVersionNeed(string soname, ElfVersionNeedSpec spec)
{
if (!_versionNeeds.TryGetValue(soname, out var list))
{
list = [];
_versionNeeds[soname] = list;
}
list.Add(spec);
return this;
}
#endregion
#region Build
/// <summary>
/// Builds the ELF binary.
/// </summary>
public byte[] Build()
{
if (_is64Bit)
return BuildElf64();
else
return BuildElf32();
}
/// <summary>
/// Builds the ELF binary and returns it as a MemoryStream.
/// </summary>
public MemoryStream BuildAsStream() => new(Build());
private byte[] BuildElf64()
{
// Calculate layout
var elfHeaderSize = 64;
var phdrSize = 56;
// Count program headers
var phdrCount = 0;
if (_interpreter != null) phdrCount++; // PT_INTERP
phdrCount++; // PT_LOAD (always present)
if (_dependencies.Count > 0 || _rpath.Count > 0 || _runpath.Count > 0 || _versionNeeds.Count > 0)
phdrCount++; // PT_DYNAMIC
if (_buildId != null) phdrCount++; // PT_NOTE
var phdrOffset = elfHeaderSize;
var dataStart = BinaryBufferWriter.AlignTo(phdrOffset + phdrSize * phdrCount, 16);
// Build string table first to calculate offsets
var stringTable = new StringBuilder();
stringTable.Append('\0'); // Null byte at start
var stringOffsets = new Dictionary<string, int>();
void AddString(string s)
{
if (!stringOffsets.ContainsKey(s))
{
stringOffsets[s] = stringTable.Length;
stringTable.Append(s);
stringTable.Append('\0');
}
}
// Add all strings to table
if (_interpreter != null) AddString(_interpreter);
foreach (var dep in _dependencies) AddString(dep);
if (_rpath.Count > 0) AddString(string.Join(":", _rpath));
if (_runpath.Count > 0) AddString(string.Join(":", _runpath));
foreach (var (soname, versions) in _versionNeeds)
{
AddString(soname);
foreach (var v in versions) AddString(v.Version);
}
var stringTableBytes = Encoding.UTF8.GetBytes(stringTable.ToString());
// Layout data sections
var currentOffset = dataStart;
// Interpreter
var interpOffset = 0;
var interpSize = 0;
if (_interpreter != null)
{
interpOffset = currentOffset;
interpSize = Encoding.UTF8.GetByteCount(_interpreter) + 1;
currentOffset = BinaryBufferWriter.AlignTo(currentOffset + interpSize, 8);
}
// Build ID (PT_NOTE)
var noteOffset = 0;
var noteSize = 0;
byte[]? buildIdBytes = null;
if (_buildId != null)
{
buildIdBytes = Convert.FromHexString(_buildId);
noteOffset = currentOffset;
noteSize = 16 + buildIdBytes.Length; // namesz(4) + descsz(4) + type(4) + "GNU\0"(4) + desc
currentOffset = BinaryBufferWriter.AlignTo(currentOffset + noteSize, 8);
}
// Dynamic section
var dynOffset = 0;
var dynEntrySize = 16; // Elf64_Dyn
var dynCount = 0;
if (_dependencies.Count > 0 || _rpath.Count > 0 || _runpath.Count > 0 || _versionNeeds.Count > 0)
{
dynOffset = currentOffset;
// Count dynamic entries
dynCount++; // DT_STRTAB
dynCount++; // DT_STRSZ
dynCount += _dependencies.Count; // DT_NEEDED entries
if (_rpath.Count > 0) dynCount++; // DT_RPATH
if (_runpath.Count > 0) dynCount++; // DT_RUNPATH
if (_versionNeeds.Count > 0)
{
dynCount++; // DT_VERNEED
dynCount++; // DT_VERNEEDNUM
}
dynCount++; // DT_NULL
currentOffset += dynEntrySize * dynCount;
currentOffset = BinaryBufferWriter.AlignTo(currentOffset, 8);
}
// String table
var strtabOffset = currentOffset;
var strtabVaddr = strtabOffset; // Use file offset as vaddr for simplicity
currentOffset += stringTableBytes.Length;
currentOffset = BinaryBufferWriter.AlignTo(currentOffset, 8);
// Version needs section (.gnu.version_r)
var verneedOffset = 0;
var verneedSize = 0;
if (_versionNeeds.Count > 0)
{
verneedOffset = currentOffset;
// Each Verneed: 16 bytes, each Vernaux: 16 bytes
foreach (var (_, versions) in _versionNeeds)
{
verneedSize += 16; // Verneed
verneedSize += 16 * versions.Count; // Vernauxes
}
currentOffset += verneedSize;
currentOffset = BinaryBufferWriter.AlignTo(currentOffset, 8);
}
// Section headers (for string table discovery)
var shoff = currentOffset;
var shentsize = 64;
var shnum = 2; // null + .dynstr
if (_versionNeeds.Count > 0) shnum++; // .gnu.version_r
currentOffset += shentsize * shnum;
var totalSize = currentOffset;
var buffer = new byte[totalSize];
// Write ELF header
WriteElf64Header(buffer, phdrOffset, phdrCount, shoff, shnum, shentsize);
// Write program headers
var phdrPos = phdrOffset;
// PT_INTERP
if (_interpreter != null)
{
WritePhdr64(buffer, phdrPos, 3, 4, interpOffset, interpOffset, interpSize); // PT_INTERP = 3, PF_R = 4
phdrPos += phdrSize;
}
// PT_LOAD
WritePhdr64(buffer, phdrPos, 1, 5, 0, 0, totalSize); // PT_LOAD = 1, PF_R|PF_X = 5
phdrPos += phdrSize;
// PT_DYNAMIC
if (dynOffset > 0)
{
var dynSize = dynEntrySize * dynCount;
WritePhdr64(buffer, phdrPos, 2, 6, dynOffset, dynOffset, dynSize); // PT_DYNAMIC = 2, PF_R|PF_W = 6
phdrPos += phdrSize;
}
// PT_NOTE
if (_buildId != null)
{
WritePhdr64(buffer, phdrPos, 4, 4, noteOffset, noteOffset, noteSize); // PT_NOTE = 4, PF_R = 4
phdrPos += phdrSize;
}
// Write interpreter
if (_interpreter != null)
{
BinaryBufferWriter.WriteNullTerminatedString(buffer, interpOffset, _interpreter);
}
// Write build ID note
if (_buildId != null && buildIdBytes != null)
{
BinaryBufferWriter.WriteU32LE(buffer, noteOffset, 4); // namesz
BinaryBufferWriter.WriteU32LE(buffer, noteOffset + 4, (uint)buildIdBytes.Length); // descsz
BinaryBufferWriter.WriteU32LE(buffer, noteOffset + 8, 3); // type = NT_GNU_BUILD_ID
Encoding.UTF8.GetBytes("GNU\0").CopyTo(buffer, noteOffset + 12);
buildIdBytes.CopyTo(buffer, noteOffset + 16);
}
// Write dynamic section
if (dynOffset > 0)
{
var dynPos = dynOffset;
// DT_STRTAB
WriteDynEntry64(buffer, dynPos, 5, (ulong)strtabVaddr);
dynPos += dynEntrySize;
// DT_STRSZ
WriteDynEntry64(buffer, dynPos, 10, (ulong)stringTableBytes.Length);
dynPos += dynEntrySize;
// DT_NEEDED entries
foreach (var dep in _dependencies)
{
WriteDynEntry64(buffer, dynPos, 1, (ulong)stringOffsets[dep]);
dynPos += dynEntrySize;
}
// DT_RPATH
if (_rpath.Count > 0)
{
WriteDynEntry64(buffer, dynPos, 15, (ulong)stringOffsets[string.Join(":", _rpath)]);
dynPos += dynEntrySize;
}
// DT_RUNPATH
if (_runpath.Count > 0)
{
WriteDynEntry64(buffer, dynPos, 29, (ulong)stringOffsets[string.Join(":", _runpath)]);
dynPos += dynEntrySize;
}
// DT_VERNEED and DT_VERNEEDNUM
if (_versionNeeds.Count > 0)
{
WriteDynEntry64(buffer, dynPos, 0x6ffffffe, (ulong)verneedOffset); // DT_VERNEED
dynPos += dynEntrySize;
WriteDynEntry64(buffer, dynPos, 0x6fffffff, (ulong)_versionNeeds.Count); // DT_VERNEEDNUM
dynPos += dynEntrySize;
}
// DT_NULL
WriteDynEntry64(buffer, dynPos, 0, 0);
}
// Write string table
stringTableBytes.CopyTo(buffer, strtabOffset);
// Write version needs section
if (_versionNeeds.Count > 0)
{
var verneedPos = verneedOffset;
var verneedEntries = _versionNeeds.ToList();
ushort versionIndex = 2; // Start from 2 (0 and 1 are reserved)
for (var i = 0; i < verneedEntries.Count; i++)
{
var (soname, versions) = verneedEntries[i];
var auxOffset = 16; // Offset from verneed to first aux
// Write Verneed entry
BinaryBufferWriter.WriteU16LE(buffer, verneedPos, 1); // vn_version
BinaryBufferWriter.WriteU16LE(buffer, verneedPos + 2, (ushort)versions.Count); // vn_cnt
BinaryBufferWriter.WriteU32LE(buffer, verneedPos + 4, (uint)stringOffsets[soname]); // vn_file
BinaryBufferWriter.WriteU32LE(buffer, verneedPos + 8, (uint)auxOffset); // vn_aux
var nextVerneed = (i < verneedEntries.Count - 1) ? 16 + 16 * versions.Count : 0;
BinaryBufferWriter.WriteU32LE(buffer, verneedPos + 12, (uint)nextVerneed); // vn_next
// Write Vernaux entries
var auxPos = verneedPos + 16;
for (var j = 0; j < versions.Count; j++)
{
var v = versions[j];
BinaryBufferWriter.WriteU32LE(buffer, auxPos, v.Hash); // vna_hash
BinaryBufferWriter.WriteU16LE(buffer, auxPos + 4, v.IsWeak ? (ushort)0x2 : (ushort)0); // vna_flags
BinaryBufferWriter.WriteU16LE(buffer, auxPos + 6, versionIndex++); // vna_other
BinaryBufferWriter.WriteU32LE(buffer, auxPos + 8, (uint)stringOffsets[v.Version]); // vna_name
var nextAux = (j < versions.Count - 1) ? 16 : 0;
BinaryBufferWriter.WriteU32LE(buffer, auxPos + 12, (uint)nextAux); // vna_next
auxPos += 16;
}
verneedPos += 16 + 16 * versions.Count;
}
}
// Write section headers
// Section 0: null section (already zeroed)
// Section 1: .dynstr
var sh1 = shoff + shentsize;
BinaryBufferWriter.WriteU32LE(buffer, sh1 + 4, 3); // sh_type = SHT_STRTAB
BinaryBufferWriter.WriteU64LE(buffer, sh1 + 16, (ulong)strtabVaddr); // sh_addr
BinaryBufferWriter.WriteU64LE(buffer, sh1 + 24, (ulong)strtabOffset); // sh_offset
BinaryBufferWriter.WriteU64LE(buffer, sh1 + 32, (ulong)stringTableBytes.Length); // sh_size
// Section 2: .gnu.version_r (if present)
if (_versionNeeds.Count > 0)
{
var sh2 = shoff + shentsize * 2;
BinaryBufferWriter.WriteU32LE(buffer, sh2 + 4, 0x6ffffffe); // sh_type = SHT_GNU_verneed
BinaryBufferWriter.WriteU64LE(buffer, sh2 + 16, (ulong)verneedOffset); // sh_addr
BinaryBufferWriter.WriteU64LE(buffer, sh2 + 24, (ulong)verneedOffset); // sh_offset
BinaryBufferWriter.WriteU64LE(buffer, sh2 + 32, (ulong)verneedSize); // sh_size
}
return buffer;
}
private byte[] BuildElf32()
{
// Simplified 32-bit implementation
// For now, just build a minimal header that can be identified
var buffer = new byte[52]; // ELF32 header size
// ELF magic
buffer[0] = 0x7F;
buffer[1] = (byte)'E';
buffer[2] = (byte)'L';
buffer[3] = (byte)'F';
buffer[4] = 0x01; // 32-bit
buffer[5] = _isBigEndian ? (byte)0x02 : (byte)0x01;
buffer[6] = 0x01; // ELF version
// e_type = ET_EXEC
BinaryBufferWriter.WriteU16(buffer, 16, 0x02, _isBigEndian);
// e_machine
BinaryBufferWriter.WriteU16(buffer, 18, _machine, _isBigEndian);
return buffer;
}
private void WriteElf64Header(byte[] buffer, int phoff, int phnum, int shoff, int shnum, int shentsize)
{
// ELF magic
buffer[0] = 0x7F;
buffer[1] = (byte)'E';
buffer[2] = (byte)'L';
buffer[3] = (byte)'F';
buffer[4] = 0x02; // 64-bit
buffer[5] = _isBigEndian ? (byte)0x02 : (byte)0x01;
buffer[6] = 0x01; // ELF version
buffer[7] = 0x00; // System V ABI
// e_type = ET_EXEC
BinaryBufferWriter.WriteU16(buffer, 16, 0x02, _isBigEndian);
// e_machine
BinaryBufferWriter.WriteU16(buffer, 18, _machine, _isBigEndian);
// e_version
BinaryBufferWriter.WriteU32(buffer, 20, 1, _isBigEndian);
// e_entry (0)
BinaryBufferWriter.WriteU64(buffer, 24, 0, _isBigEndian);
// e_phoff
BinaryBufferWriter.WriteU64(buffer, 32, (ulong)phoff, _isBigEndian);
// e_shoff
BinaryBufferWriter.WriteU64(buffer, 40, (ulong)shoff, _isBigEndian);
// e_flags
BinaryBufferWriter.WriteU32(buffer, 48, 0, _isBigEndian);
// e_ehsize
BinaryBufferWriter.WriteU16(buffer, 52, 64, _isBigEndian);
// e_phentsize
BinaryBufferWriter.WriteU16(buffer, 54, 56, _isBigEndian);
// e_phnum
BinaryBufferWriter.WriteU16(buffer, 56, (ushort)phnum, _isBigEndian);
// e_shentsize
BinaryBufferWriter.WriteU16(buffer, 58, (ushort)shentsize, _isBigEndian);
// e_shnum
BinaryBufferWriter.WriteU16(buffer, 60, (ushort)shnum, _isBigEndian);
// e_shstrndx
BinaryBufferWriter.WriteU16(buffer, 62, 0, _isBigEndian);
}
private void WritePhdr64(byte[] buffer, int offset, uint type, uint flags, int fileOffset, int vaddr, int size)
{
BinaryBufferWriter.WriteU32(buffer, offset, type, _isBigEndian); // p_type
BinaryBufferWriter.WriteU32(buffer, offset + 4, flags, _isBigEndian); // p_flags
BinaryBufferWriter.WriteU64(buffer, offset + 8, (ulong)fileOffset, _isBigEndian); // p_offset
BinaryBufferWriter.WriteU64(buffer, offset + 16, (ulong)vaddr, _isBigEndian); // p_vaddr
BinaryBufferWriter.WriteU64(buffer, offset + 24, (ulong)vaddr, _isBigEndian); // p_paddr
BinaryBufferWriter.WriteU64(buffer, offset + 32, (ulong)size, _isBigEndian); // p_filesz
BinaryBufferWriter.WriteU64(buffer, offset + 40, (ulong)size, _isBigEndian); // p_memsz
BinaryBufferWriter.WriteU64(buffer, offset + 48, 8, _isBigEndian); // p_align
}
private void WriteDynEntry64(byte[] buffer, int offset, ulong tag, ulong val)
{
BinaryBufferWriter.WriteU64(buffer, offset, tag, _isBigEndian);
BinaryBufferWriter.WriteU64(buffer, offset + 8, val, _isBigEndian);
}
#endregion
#region Factory Methods
/// <summary>
/// Creates a builder for Linux x86_64 binaries.
/// </summary>
public static ElfBuilder LinuxX64() => new ElfBuilder()
.Is64Bit()
.LittleEndian()
.WithMachine(0x3E) // EM_X86_64
.WithInterpreter("/lib64/ld-linux-x86-64.so.2");
/// <summary>
/// Creates a builder for Linux ARM64 binaries.
/// </summary>
public static ElfBuilder LinuxArm64() => new ElfBuilder()
.Is64Bit()
.LittleEndian()
.WithMachine(0xB7) // EM_AARCH64
.WithInterpreter("/lib/ld-linux-aarch64.so.1");
/// <summary>
/// Creates a builder for static binaries (no interpreter).
/// </summary>
public static ElfBuilder Static() => new ElfBuilder()
.Is64Bit()
.LittleEndian();
#endregion
#region Helpers
/// <summary>
/// Computes the ELF hash for a string (used in version info).
/// </summary>
private static uint ComputeElfHash(string name)
{
uint h = 0;
foreach (var c in name)
{
h = (h << 4) + c;
var g = h & 0xF0000000;
if (g != 0)
h ^= g >> 24;
h &= ~g;
}
return h;
}
#endregion
}

View File

@@ -0,0 +1,476 @@
using System.Text;
namespace StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
/// <summary>
/// CPU types for Mach-O binaries.
/// </summary>
public enum MachOCpuType : uint
{
X86 = 0x00000007,
X86_64 = 0x01000007,
Arm = 0x0000000C,
Arm64 = 0x0100000C,
PowerPC = 0x00000012,
PowerPC64 = 0x01000012,
}
/// <summary>
/// Dylib load command types.
/// </summary>
public enum MachODylibKind
{
/// <summary>LC_LOAD_DYLIB (0x0C)</summary>
Load,
/// <summary>LC_LOAD_WEAK_DYLIB (0x80000018)</summary>
Weak,
/// <summary>LC_REEXPORT_DYLIB (0x8000001F)</summary>
Reexport,
/// <summary>LC_LAZY_LOAD_DYLIB (0x20)</summary>
Lazy,
}
/// <summary>
/// Specification for a dylib dependency.
/// </summary>
/// <param name="Path">The dylib path.</param>
/// <param name="Kind">The load command type.</param>
/// <param name="CurrentVersion">The current version (e.g., "1.2.3").</param>
/// <param name="CompatVersion">The compatibility version (e.g., "1.0.0").</param>
public sealed record MachODylibSpec(
string Path,
MachODylibKind Kind = MachODylibKind.Load,
string? CurrentVersion = null,
string? CompatVersion = null);
/// <summary>
/// Specification for a slice in a fat binary.
/// </summary>
/// <param name="CpuType">The CPU type for this slice.</param>
/// <param name="Dylibs">Dependencies for this slice.</param>
/// <param name="Rpaths">Runtime search paths for this slice.</param>
/// <param name="Uuid">UUID for this slice.</param>
public sealed record MachOSliceSpec(
MachOCpuType CpuType,
List<MachODylibSpec> Dylibs,
List<string> Rpaths,
Guid? Uuid = null);
/// <summary>
/// Fluent builder for creating Mach-O binary fixtures.
/// Supports single-arch and universal (fat) binaries.
/// </summary>
public sealed class MachOBuilder
{
private bool _is64Bit = true;
private bool _isBigEndian = false;
private MachOCpuType _cpuType = MachOCpuType.X86_64;
private Guid? _uuid;
private readonly List<MachODylibSpec> _dylibs = [];
private readonly List<string> _rpaths = [];
private readonly List<MachOSliceSpec> _additionalSlices = [];
private bool _isFat = false;
#region Configuration
/// <summary>
/// Sets whether to generate a 64-bit Mach-O.
/// </summary>
public MachOBuilder Is64Bit(bool value = true)
{
_is64Bit = value;
return this;
}
/// <summary>
/// Generates a 32-bit Mach-O.
/// </summary>
public MachOBuilder Is32Bit() => Is64Bit(false);
/// <summary>
/// Sets whether to use big-endian byte order.
/// </summary>
public MachOBuilder BigEndian(bool value = true)
{
_isBigEndian = value;
return this;
}
/// <summary>
/// Uses little-endian byte order.
/// </summary>
public MachOBuilder LittleEndian() => BigEndian(false);
/// <summary>
/// Sets the CPU type.
/// </summary>
public MachOBuilder WithCpuType(MachOCpuType type)
{
_cpuType = type;
return this;
}
/// <summary>
/// Sets the UUID.
/// </summary>
public MachOBuilder WithUuid(Guid uuid)
{
_uuid = uuid;
return this;
}
/// <summary>
/// Sets the UUID from a string.
/// </summary>
public MachOBuilder WithUuid(string uuid)
{
_uuid = Guid.Parse(uuid);
return this;
}
#endregion
#region Dylibs
/// <summary>
/// Adds a dylib dependency.
/// </summary>
public MachOBuilder AddDylib(string path, MachODylibKind kind = MachODylibKind.Load)
{
_dylibs.Add(new MachODylibSpec(path, kind));
return this;
}
/// <summary>
/// Adds a dylib dependency with version info.
/// </summary>
public MachOBuilder AddDylib(string path, string currentVersion, string compatVersion,
MachODylibKind kind = MachODylibKind.Load)
{
_dylibs.Add(new MachODylibSpec(path, kind, currentVersion, compatVersion));
return this;
}
/// <summary>
/// Adds a weak dylib (LC_LOAD_WEAK_DYLIB).
/// </summary>
public MachOBuilder AddWeakDylib(string path)
{
return AddDylib(path, MachODylibKind.Weak);
}
/// <summary>
/// Adds a reexport dylib (LC_REEXPORT_DYLIB).
/// </summary>
public MachOBuilder AddReexportDylib(string path)
{
return AddDylib(path, MachODylibKind.Reexport);
}
/// <summary>
/// Adds a lazy-load dylib (LC_LAZY_LOAD_DYLIB).
/// </summary>
public MachOBuilder AddLazyDylib(string path)
{
return AddDylib(path, MachODylibKind.Lazy);
}
#endregion
#region Rpaths
/// <summary>
/// Adds runtime search paths (LC_RPATH).
/// </summary>
public MachOBuilder AddRpath(params string[] paths)
{
_rpaths.AddRange(paths);
return this;
}
#endregion
#region Fat Binary Support
/// <summary>
/// Adds a slice for a fat binary.
/// </summary>
public MachOBuilder AddSlice(MachOSliceSpec slice)
{
_additionalSlices.Add(slice);
_isFat = true;
return this;
}
/// <summary>
/// Makes this a fat binary with the specified architectures.
/// </summary>
public MachOBuilder MakeFat(params MachOCpuType[] architectures)
{
_isFat = true;
foreach (var arch in architectures)
{
if (arch != _cpuType)
{
_additionalSlices.Add(new MachOSliceSpec(arch, [], [], null));
}
}
return this;
}
#endregion
#region Build
/// <summary>
/// Builds the Mach-O binary.
/// </summary>
public byte[] Build()
{
if (_isFat)
return BuildFat();
else
return BuildSingleArch();
}
/// <summary>
/// Builds the Mach-O binary and returns it as a MemoryStream.
/// </summary>
public MemoryStream BuildAsStream() => new(Build());
private byte[] BuildSingleArch()
{
return BuildSlice(_cpuType, _is64Bit, _isBigEndian, _dylibs, _rpaths, _uuid);
}
private byte[] BuildFat()
{
// Build all slices first to get their sizes
var allSlices = new List<(MachOCpuType CpuType, byte[] Data)>();
// Main slice
var mainSlice = BuildSlice(_cpuType, _is64Bit, _isBigEndian, _dylibs, _rpaths, _uuid);
allSlices.Add((_cpuType, mainSlice));
// Additional slices
foreach (var spec in _additionalSlices)
{
var sliceData = BuildSlice(spec.CpuType, true, false, spec.Dylibs, spec.Rpaths, spec.Uuid);
allSlices.Add((spec.CpuType, sliceData));
}
// Calculate fat header size
var fatHeaderSize = 8 + allSlices.Count * 20; // fat_header + fat_arch entries
var alignment = 4096; // Page alignment
// Calculate offsets
var currentOffset = BinaryBufferWriter.AlignTo(fatHeaderSize, alignment);
var sliceOffsets = new List<int>();
foreach (var (_, data) in allSlices)
{
sliceOffsets.Add(currentOffset);
currentOffset = BinaryBufferWriter.AlignTo(currentOffset + data.Length, alignment);
}
var totalSize = currentOffset;
var buffer = new byte[totalSize];
// Fat header (big endian)
BinaryBufferWriter.WriteU32BE(buffer, 0, 0xCAFEBABE); // FAT_MAGIC
BinaryBufferWriter.WriteU32BE(buffer, 4, (uint)allSlices.Count);
// Fat arch entries
for (var i = 0; i < allSlices.Count; i++)
{
var (cpuType, data) = allSlices[i];
var archOffset = 8 + i * 20;
BinaryBufferWriter.WriteU32BE(buffer, archOffset, (uint)cpuType);
BinaryBufferWriter.WriteU32BE(buffer, archOffset + 4, 0); // cpusubtype
BinaryBufferWriter.WriteU32BE(buffer, archOffset + 8, (uint)sliceOffsets[i]);
BinaryBufferWriter.WriteU32BE(buffer, archOffset + 12, (uint)data.Length);
BinaryBufferWriter.WriteU32BE(buffer, archOffset + 16, 12); // align (2^12 = 4096)
// Copy slice data
data.CopyTo(buffer, sliceOffsets[i]);
}
return buffer;
}
private static byte[] BuildSlice(MachOCpuType cpuType, bool is64Bit, bool isBigEndian,
List<MachODylibSpec> dylibs, List<string> rpaths, Guid? uuid)
{
var headerSize = is64Bit ? 32 : 28;
var loadCommands = new List<byte[]>();
// Build UUID command if present
if (uuid.HasValue)
{
loadCommands.Add(BuildUuidCommand(uuid.Value));
}
// Build dylib commands
foreach (var dylib in dylibs)
{
loadCommands.Add(BuildDylibCommand(dylib));
}
// Build rpath commands
foreach (var rpath in rpaths)
{
loadCommands.Add(BuildRpathCommand(rpath));
}
var totalCmdSize = loadCommands.Sum(c => c.Length);
var totalSize = headerSize + totalCmdSize;
var buffer = new byte[totalSize];
// Write header
WriteMachOHeader(buffer, cpuType, is64Bit, isBigEndian, loadCommands.Count, totalCmdSize);
// Write load commands
var cmdOffset = headerSize;
foreach (var cmd in loadCommands)
{
cmd.CopyTo(buffer, cmdOffset);
cmdOffset += cmd.Length;
}
return buffer;
}
private static void WriteMachOHeader(byte[] buffer, MachOCpuType cpuType, bool is64Bit, bool isBigEndian,
int ncmds, int sizeofcmds)
{
if (isBigEndian)
{
// MH_CIGAM_64 or MH_CIGAM
var magic = is64Bit ? 0xCFFAEDFEu : 0xCEFAEDFEu;
BinaryBufferWriter.WriteU32LE(buffer, 0, magic); // Stored as LE, reads as BE magic
BinaryBufferWriter.WriteU32BE(buffer, 4, (uint)cpuType);
BinaryBufferWriter.WriteU32BE(buffer, 8, 0); // cpusubtype
BinaryBufferWriter.WriteU32BE(buffer, 12, 2); // MH_EXECUTE
BinaryBufferWriter.WriteU32BE(buffer, 16, (uint)ncmds);
BinaryBufferWriter.WriteU32BE(buffer, 20, (uint)sizeofcmds);
BinaryBufferWriter.WriteU32BE(buffer, 24, 0x00200085); // flags
if (is64Bit)
BinaryBufferWriter.WriteU32BE(buffer, 28, 0); // reserved
}
else
{
var magic = is64Bit ? 0xFEEDFACFu : 0xFEEDFACEu;
BinaryBufferWriter.WriteU32LE(buffer, 0, magic);
BinaryBufferWriter.WriteU32LE(buffer, 4, (uint)cpuType);
BinaryBufferWriter.WriteU32LE(buffer, 8, 0); // cpusubtype
BinaryBufferWriter.WriteU32LE(buffer, 12, 2); // MH_EXECUTE
BinaryBufferWriter.WriteU32LE(buffer, 16, (uint)ncmds);
BinaryBufferWriter.WriteU32LE(buffer, 20, (uint)sizeofcmds);
BinaryBufferWriter.WriteU32LE(buffer, 24, 0x00200085); // flags
if (is64Bit)
BinaryBufferWriter.WriteU32LE(buffer, 28, 0); // reserved
}
}
private static byte[] BuildUuidCommand(Guid uuid)
{
var buffer = new byte[24];
BinaryBufferWriter.WriteU32LE(buffer, 0, 0x1B); // LC_UUID
BinaryBufferWriter.WriteU32LE(buffer, 4, 24);
uuid.ToByteArray().CopyTo(buffer, 8);
return buffer;
}
private static byte[] BuildDylibCommand(MachODylibSpec dylib)
{
var pathBytes = Encoding.UTF8.GetBytes(dylib.Path + "\0");
var cmdSize = 24 + pathBytes.Length;
cmdSize = BinaryBufferWriter.AlignTo(cmdSize, 8);
var buffer = new byte[cmdSize];
// Command type
var cmd = dylib.Kind switch
{
MachODylibKind.Load => 0x0Cu,
MachODylibKind.Weak => 0x80000018u,
MachODylibKind.Reexport => 0x8000001Fu,
MachODylibKind.Lazy => 0x20u,
_ => 0x0Cu
};
BinaryBufferWriter.WriteU32LE(buffer, 0, cmd);
BinaryBufferWriter.WriteU32LE(buffer, 4, (uint)cmdSize);
BinaryBufferWriter.WriteU32LE(buffer, 8, 24); // name offset
BinaryBufferWriter.WriteU32LE(buffer, 12, 0); // timestamp
// Version encoding: (major << 16) | (minor << 8) | patch
var currentVersion = ParseVersion(dylib.CurrentVersion ?? "1.0.0");
var compatVersion = ParseVersion(dylib.CompatVersion ?? "1.0.0");
BinaryBufferWriter.WriteU32LE(buffer, 16, currentVersion);
BinaryBufferWriter.WriteU32LE(buffer, 20, compatVersion);
pathBytes.CopyTo(buffer, 24);
return buffer;
}
private static byte[] BuildRpathCommand(string rpath)
{
var pathBytes = Encoding.UTF8.GetBytes(rpath + "\0");
var cmdSize = 12 + pathBytes.Length;
cmdSize = BinaryBufferWriter.AlignTo(cmdSize, 8);
var buffer = new byte[cmdSize];
BinaryBufferWriter.WriteU32LE(buffer, 0, 0x8000001C); // LC_RPATH
BinaryBufferWriter.WriteU32LE(buffer, 4, (uint)cmdSize);
BinaryBufferWriter.WriteU32LE(buffer, 8, 12); // path offset
pathBytes.CopyTo(buffer, 12);
return buffer;
}
private static uint ParseVersion(string version)
{
var parts = version.Split('.');
var major = parts.Length > 0 ? uint.Parse(parts[0]) : 0;
var minor = parts.Length > 1 ? uint.Parse(parts[1]) : 0;
var patch = parts.Length > 2 ? uint.Parse(parts[2]) : 0;
return (major << 16) | (minor << 8) | patch;
}
#endregion
#region Factory Methods
/// <summary>
/// Creates a builder for macOS ARM64 binaries.
/// </summary>
public static MachOBuilder MacOSArm64() => new MachOBuilder()
.Is64Bit()
.LittleEndian()
.WithCpuType(MachOCpuType.Arm64);
/// <summary>
/// Creates a builder for macOS x86_64 binaries.
/// </summary>
public static MachOBuilder MacOSX64() => new MachOBuilder()
.Is64Bit()
.LittleEndian()
.WithCpuType(MachOCpuType.X86_64);
/// <summary>
/// Creates a builder for a universal binary (ARM64 + x86_64).
/// </summary>
public static MachOBuilder Universal() => new MachOBuilder()
.Is64Bit()
.LittleEndian()
.WithCpuType(MachOCpuType.X86_64)
.MakeFat(MachOCpuType.X86_64, MachOCpuType.Arm64);
#endregion
}

View File

@@ -0,0 +1,657 @@
using System.Text;
using StellaOps.Scanner.Analyzers.Native;
namespace StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
/// <summary>
/// Machine types for PE binaries.
/// </summary>
public enum PeMachine : ushort
{
I386 = 0x014c,
Amd64 = 0x8664,
Arm = 0x01c0,
Arm64 = 0xAA64,
}
/// <summary>
/// Specification for an import entry.
/// </summary>
/// <param name="DllName">The DLL name.</param>
/// <param name="Functions">Functions imported from this DLL.</param>
public sealed record PeImportSpec(string DllName, IReadOnlyList<string> Functions);
/// <summary>
/// Fluent builder for creating PE binary fixtures.
/// Supports both PE32 and PE32+ formats.
/// </summary>
public sealed class PeBuilder
{
private bool _is64Bit = true;
private PeSubsystem _subsystem = PeSubsystem.WindowsConsole;
private PeMachine _machine = PeMachine.Amd64;
private readonly List<PeImportSpec> _imports = [];
private readonly List<PeImportSpec> _delayImports = [];
private string? _manifestXml;
private bool _embedManifestAsResource;
#region Configuration
/// <summary>
/// Sets whether to generate a 64-bit PE (PE32+).
/// </summary>
public PeBuilder Is64Bit(bool value = true)
{
_is64Bit = value;
_machine = value ? PeMachine.Amd64 : PeMachine.I386;
return this;
}
/// <summary>
/// Generates a 32-bit PE (PE32).
/// </summary>
public PeBuilder Is32Bit() => Is64Bit(false);
/// <summary>
/// Sets the subsystem.
/// </summary>
public PeBuilder WithSubsystem(PeSubsystem subsystem)
{
_subsystem = subsystem;
return this;
}
/// <summary>
/// Sets the machine type.
/// </summary>
public PeBuilder WithMachine(PeMachine machine)
{
_machine = machine;
return this;
}
#endregion
#region Imports
/// <summary>
/// Adds an import entry.
/// </summary>
public PeBuilder AddImport(string dllName, params string[] functions)
{
_imports.Add(new PeImportSpec(dllName, functions.ToList()));
return this;
}
/// <summary>
/// Adds an import specification.
/// </summary>
public PeBuilder AddImport(PeImportSpec spec)
{
_imports.Add(spec);
return this;
}
/// <summary>
/// Adds a delay-load import entry.
/// </summary>
public PeBuilder AddDelayImport(string dllName, params string[] functions)
{
_delayImports.Add(new PeImportSpec(dllName, functions.ToList()));
return this;
}
#endregion
#region Manifest
/// <summary>
/// Sets the application manifest.
/// </summary>
/// <param name="xml">The manifest XML content.</param>
/// <param name="embedAsResource">If true, embeds as RT_MANIFEST resource; otherwise, embeds as text.</param>
public PeBuilder WithManifest(string xml, bool embedAsResource = false)
{
_manifestXml = xml;
_embedManifestAsResource = embedAsResource;
return this;
}
/// <summary>
/// Adds a Side-by-Side assembly dependency to the manifest.
/// </summary>
public PeBuilder WithSxsDependency(string name, string version, string? publicKeyToken = null, string? arch = null)
{
var archAttr = arch != null ? $" processorArchitecture=\"{arch}\"" : "";
var tokenAttr = publicKeyToken != null ? $" publicKeyToken=\"{publicKeyToken}\"" : "";
_manifestXml = $"""
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="{name}" version="{version}"{archAttr}{tokenAttr}/>
</dependentAssembly>
</dependency>
</assembly>
""";
return this;
}
#endregion
#region Build
/// <summary>
/// Builds the PE binary.
/// </summary>
public byte[] Build()
{
if (_is64Bit)
return BuildPe64();
else
return BuildPe32();
}
/// <summary>
/// Builds the PE binary and returns it as a MemoryStream.
/// </summary>
public MemoryStream BuildAsStream() => new(Build());
private byte[] BuildPe64()
{
// Calculate layout
const int dosHeaderSize = 0x40;
const int dosStubSize = 0x40;
const int peOffset = dosHeaderSize + dosStubSize; // 0x80
const int coffHeaderSize = 24;
const int optionalHeaderSize = 0xF0; // PE32+ optional header
const int dataDirectoryCount = 16;
var numberOfSections = 1; // .text
if (_imports.Count > 0) numberOfSections++;
if (_delayImports.Count > 0) numberOfSections++;
if (_manifestXml != null && _embedManifestAsResource) numberOfSections++;
var sectionHeadersOffset = peOffset + coffHeaderSize + optionalHeaderSize;
var sectionHeaderSize = 40;
var sectionHeadersEnd = sectionHeadersOffset + sectionHeaderSize * numberOfSections;
var firstSectionOffset = BinaryBufferWriter.AlignTo(sectionHeadersEnd, 0x200);
// .text section
var textRva = 0x1000;
var textFileOffset = firstSectionOffset;
var textSize = 0x200;
// Check if we need to embed manifest in .text section
byte[]? textManifest = null;
if (_manifestXml != null && !_embedManifestAsResource)
{
textManifest = Encoding.UTF8.GetBytes(_manifestXml);
textSize = BinaryBufferWriter.AlignTo(textManifest.Length + 0x100, 0x200);
}
// Current RVA and file offset for additional sections
var currentRva = textRva + BinaryBufferWriter.AlignTo(textSize, 0x1000);
var currentFileOffset = textFileOffset + textSize;
// Import section
var importRva = 0;
var importFileOffset = 0;
var importSize = 0;
byte[]? importData = null;
if (_imports.Count > 0)
{
importRva = currentRva;
importFileOffset = currentFileOffset;
importData = BuildImportSection(_imports, importRva, _is64Bit);
importSize = BinaryBufferWriter.AlignTo(importData.Length, 0x200);
currentRva += 0x1000;
currentFileOffset += importSize;
}
// Delay import section
var delayImportRva = 0;
var delayImportFileOffset = 0;
var delayImportSize = 0;
byte[]? delayImportData = null;
if (_delayImports.Count > 0)
{
delayImportRva = currentRva;
delayImportFileOffset = currentFileOffset;
delayImportData = BuildDelayImportSection(_delayImports, delayImportRva);
delayImportSize = BinaryBufferWriter.AlignTo(delayImportData.Length, 0x200);
currentRva += 0x1000;
currentFileOffset += delayImportSize;
}
// Resource section (for manifest)
var resourceRva = 0;
var resourceFileOffset = 0;
var resourceSize = 0;
byte[]? resourceData = null;
if (_manifestXml != null && _embedManifestAsResource)
{
resourceRva = currentRva;
resourceFileOffset = currentFileOffset;
resourceData = BuildResourceSection(_manifestXml, resourceRva);
resourceSize = BinaryBufferWriter.AlignTo(resourceData.Length, 0x200);
currentRva += 0x1000;
currentFileOffset += resourceSize;
}
var totalSize = currentFileOffset;
var buffer = new byte[totalSize];
// DOS header
buffer[0] = (byte)'M';
buffer[1] = (byte)'Z';
BinaryBufferWriter.WriteU32LE(buffer, 0x3C, (uint)peOffset);
// PE signature
buffer[peOffset] = (byte)'P';
buffer[peOffset + 1] = (byte)'E';
// COFF header
var coffOffset = peOffset + 4;
BinaryBufferWriter.WriteU16LE(buffer, coffOffset, (ushort)_machine);
BinaryBufferWriter.WriteU16LE(buffer, coffOffset + 2, (ushort)numberOfSections);
BinaryBufferWriter.WriteU16LE(buffer, coffOffset + 16, optionalHeaderSize);
BinaryBufferWriter.WriteU16LE(buffer, coffOffset + 18, 0x22); // EXECUTABLE_IMAGE | LARGE_ADDRESS_AWARE
// Optional header (PE32+)
var optOffset = peOffset + coffHeaderSize;
BinaryBufferWriter.WriteU16LE(buffer, optOffset, 0x20b); // PE32+ magic
buffer[optOffset + 2] = 14; // MajorLinkerVersion
BinaryBufferWriter.WriteU64LE(buffer, optOffset + 24, 0x140000000); // ImageBase
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 32, 0x1000); // SectionAlignment
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 36, 0x200); // FileAlignment
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 40, 6); // MajorOperatingSystemVersion
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 48, 6); // MajorSubsystemVersion
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 56, (uint)currentRva); // SizeOfImage
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 60, (uint)firstSectionOffset); // SizeOfHeaders
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 68, (ushort)_subsystem);
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 70, 0x8160); // DllCharacteristics
BinaryBufferWriter.WriteU64LE(buffer, optOffset + 72, 0x100000); // SizeOfStackReserve
BinaryBufferWriter.WriteU64LE(buffer, optOffset + 80, 0x1000); // SizeOfStackCommit
BinaryBufferWriter.WriteU64LE(buffer, optOffset + 88, 0x100000); // SizeOfHeapReserve
BinaryBufferWriter.WriteU64LE(buffer, optOffset + 96, 0x1000); // SizeOfHeapCommit
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 108, dataDirectoryCount);
// Data directories (at offset 112 for PE32+)
var dataDirOffset = optOffset + 112;
// Import directory (entry 1)
if (_imports.Count > 0)
{
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 8, (uint)importRva);
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 12, (uint)(_imports.Count + 1) * 20);
}
// Resource directory (entry 2)
if (resourceData != null)
{
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 16, (uint)resourceRva);
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 20, (uint)resourceData.Length);
}
// Delay import directory (entry 13)
if (_delayImports.Count > 0)
{
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 104, (uint)delayImportRva);
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 108, (uint)(_delayImports.Count + 1) * 32);
}
// Section headers
var shOffset = sectionHeadersOffset;
var sectionIndex = 0;
// .text section
WriteSectionHeader(buffer, shOffset, ".text", textRva, textSize, textFileOffset);
shOffset += sectionHeaderSize;
sectionIndex++;
// .idata section
if (_imports.Count > 0)
{
WriteSectionHeader(buffer, shOffset, ".idata", importRva, importSize, importFileOffset);
shOffset += sectionHeaderSize;
sectionIndex++;
}
// .didat section (delay imports)
if (_delayImports.Count > 0)
{
WriteSectionHeader(buffer, shOffset, ".didat", delayImportRva, delayImportSize, delayImportFileOffset);
shOffset += sectionHeaderSize;
sectionIndex++;
}
// .rsrc section
if (resourceData != null)
{
WriteSectionHeader(buffer, shOffset, ".rsrc", resourceRva, resourceSize, resourceFileOffset);
shOffset += sectionHeaderSize;
sectionIndex++;
}
// Write .text section (with manifest if not as resource)
if (textManifest != null)
{
textManifest.CopyTo(buffer, textFileOffset + 0x100);
}
// Write import section
if (importData != null)
{
importData.CopyTo(buffer, importFileOffset);
}
// Write delay import section
if (delayImportData != null)
{
delayImportData.CopyTo(buffer, delayImportFileOffset);
}
// Write resource section
if (resourceData != null)
{
resourceData.CopyTo(buffer, resourceFileOffset);
}
return buffer;
}
private byte[] BuildPe32()
{
// Simplified PE32 - similar to PE64 but with 32-bit offsets
const int dosHeaderSize = 0x40;
const int dosStubSize = 0x40;
const int peOffset = dosHeaderSize + dosStubSize;
const int coffHeaderSize = 24;
const int optionalHeaderSize = 0xE0; // PE32 optional header
var sectionHeadersOffset = peOffset + coffHeaderSize + optionalHeaderSize;
var sectionHeaderSize = 40;
var numberOfSections = 1;
if (_imports.Count > 0) numberOfSections++;
if (_manifestXml != null && _embedManifestAsResource) numberOfSections++;
var sectionHeadersEnd = sectionHeadersOffset + sectionHeaderSize * numberOfSections;
var firstSectionOffset = BinaryBufferWriter.AlignTo(sectionHeadersEnd, 0x200);
var textRva = 0x1000;
var textFileOffset = firstSectionOffset;
var textSize = 0x200;
var currentRva = textRva + 0x1000;
var currentFileOffset = textFileOffset + 0x200;
// Import section
var importRva = 0;
var importFileOffset = 0;
var importSize = 0;
byte[]? importData = null;
if (_imports.Count > 0)
{
importRva = currentRva;
importFileOffset = currentFileOffset;
importData = BuildImportSection(_imports, importRva, false);
importSize = BinaryBufferWriter.AlignTo(importData.Length, 0x200);
currentRva += 0x1000;
currentFileOffset += importSize;
}
byte[]? textManifest = null;
if (_manifestXml != null && !_embedManifestAsResource)
{
textManifest = Encoding.UTF8.GetBytes(_manifestXml);
textSize = BinaryBufferWriter.AlignTo(textManifest.Length + 0x100, 0x200);
}
var totalSize = currentFileOffset;
var buffer = new byte[totalSize];
// DOS header
buffer[0] = (byte)'M';
buffer[1] = (byte)'Z';
BinaryBufferWriter.WriteU32LE(buffer, 0x3C, (uint)peOffset);
// PE signature
buffer[peOffset] = (byte)'P';
buffer[peOffset + 1] = (byte)'E';
// COFF header
var coffOffset = peOffset + 4;
BinaryBufferWriter.WriteU16LE(buffer, coffOffset, (ushort)_machine);
BinaryBufferWriter.WriteU16LE(buffer, coffOffset + 2, (ushort)numberOfSections);
BinaryBufferWriter.WriteU16LE(buffer, coffOffset + 16, optionalHeaderSize);
// Optional header (PE32)
var optOffset = peOffset + coffHeaderSize;
BinaryBufferWriter.WriteU16LE(buffer, optOffset, 0x10b); // PE32 magic
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 28, 0x400000); // ImageBase (32-bit)
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 32, 0x1000); // SectionAlignment
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 36, 0x200); // FileAlignment
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 40, 6);
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 48, 6);
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 56, (uint)currentRva); // SizeOfImage
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 60, (uint)firstSectionOffset);
BinaryBufferWriter.WriteU16LE(buffer, optOffset + 68, (ushort)_subsystem);
BinaryBufferWriter.WriteU32LE(buffer, optOffset + 92, 16); // NumberOfRvaAndSizes
// Data directories
var dataDirOffset = optOffset + 96;
if (_imports.Count > 0)
{
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 8, (uint)importRva);
BinaryBufferWriter.WriteU32LE(buffer, dataDirOffset + 12, (uint)(_imports.Count + 1) * 20);
}
// Section headers
var shOffset = sectionHeadersOffset;
WriteSectionHeader(buffer, shOffset, ".text", textRva, textSize, textFileOffset);
shOffset += sectionHeaderSize;
if (_imports.Count > 0)
{
WriteSectionHeader(buffer, shOffset, ".idata", importRva, importSize, importFileOffset);
shOffset += sectionHeaderSize;
}
// Write data
if (textManifest != null)
{
textManifest.CopyTo(buffer, textFileOffset + 0x100);
}
if (importData != null)
{
importData.CopyTo(buffer, importFileOffset);
}
return buffer;
}
private static void WriteSectionHeader(byte[] buffer, int offset, string name, int rva, int size, int fileOffset)
{
var nameBytes = Encoding.ASCII.GetBytes(name.PadRight(8, '\0'));
nameBytes.AsSpan(0, 8).CopyTo(buffer.AsSpan(offset));
BinaryBufferWriter.WriteU32LE(buffer, offset + 8, (uint)size); // VirtualSize
BinaryBufferWriter.WriteU32LE(buffer, offset + 12, (uint)rva); // VirtualAddress
BinaryBufferWriter.WriteU32LE(buffer, offset + 16, (uint)size); // SizeOfRawData
BinaryBufferWriter.WriteU32LE(buffer, offset + 20, (uint)fileOffset); // PointerToRawData
BinaryBufferWriter.WriteU32LE(buffer, offset + 36, 0x40000040); // Characteristics (INITIALIZED_DATA | READ)
}
private static byte[] BuildImportSection(List<PeImportSpec> imports, int sectionRva, bool is64Bit)
{
var thunkSize = is64Bit ? 8 : 4;
var buffer = new byte[0x1000]; // 4KB should be enough
var pos = 0;
// Import descriptors (20 bytes each)
var descriptorOffset = 0;
var descriptorSize = (imports.Count + 1) * 20;
// ILT/IAT start after descriptors
var iltOffset = descriptorSize;
// String table after ILT
var stringOffset = iltOffset;
foreach (var import in imports)
{
stringOffset += (import.Functions.Count + 1) * thunkSize;
}
// Build each import
var currentIltOffset = iltOffset;
var currentStringOffset = stringOffset;
for (var i = 0; i < imports.Count; i++)
{
var import = imports[i];
// Write descriptor
var descPos = descriptorOffset + i * 20;
var iltRva = sectionRva + currentIltOffset;
var nameRva = sectionRva + currentStringOffset;
BinaryBufferWriter.WriteU32LE(buffer, descPos, (uint)iltRva); // OriginalFirstThunk
BinaryBufferWriter.WriteU32LE(buffer, descPos + 12, (uint)nameRva); // Name
BinaryBufferWriter.WriteU32LE(buffer, descPos + 16, (uint)iltRva); // FirstThunk
// Write DLL name
var nameLen = BinaryBufferWriter.WriteNullTerminatedString(buffer, currentStringOffset, import.DllName);
currentStringOffset += nameLen;
// Write ILT entries
foreach (var func in import.Functions)
{
// Hint-name entry
var hintNameRva = sectionRva + currentStringOffset;
if (is64Bit)
BinaryBufferWriter.WriteU64LE(buffer, currentIltOffset, (ulong)hintNameRva);
else
BinaryBufferWriter.WriteU32LE(buffer, currentIltOffset, (uint)hintNameRva);
currentIltOffset += thunkSize;
// Write hint-name
BinaryBufferWriter.WriteU16LE(buffer, currentStringOffset, 0); // Hint
currentStringOffset += 2;
currentStringOffset += BinaryBufferWriter.WriteNullTerminatedString(buffer, currentStringOffset, func);
// Align to word boundary
if (currentStringOffset % 2 != 0) currentStringOffset++;
}
// Null terminator for ILT
currentIltOffset += thunkSize;
}
// Null terminator for descriptor table (already zero)
pos = currentStringOffset;
var result = new byte[BinaryBufferWriter.AlignTo(pos, 16)];
buffer.AsSpan(0, pos).CopyTo(result);
return result;
}
private static byte[] BuildDelayImportSection(List<PeImportSpec> imports, int sectionRva)
{
var buffer = new byte[0x1000];
// Delay import descriptors (32 bytes each)
var stringOffset = (imports.Count + 1) * 32;
for (var i = 0; i < imports.Count; i++)
{
var import = imports[i];
var descOffset = i * 32;
BinaryBufferWriter.WriteU32LE(buffer, descOffset, 1); // Attributes
BinaryBufferWriter.WriteU32LE(buffer, descOffset + 4, (uint)(sectionRva + stringOffset)); // Name RVA
var nameLen = BinaryBufferWriter.WriteNullTerminatedString(buffer, stringOffset, import.DllName);
stringOffset += nameLen;
}
var result = new byte[BinaryBufferWriter.AlignTo(stringOffset, 16)];
buffer.AsSpan(0, stringOffset).CopyTo(result);
return result;
}
private static byte[] BuildResourceSection(string manifest, int sectionRva)
{
var manifestBytes = Encoding.UTF8.GetBytes(manifest);
var buffer = new byte[0x1000];
// Root directory
BinaryBufferWriter.WriteU16LE(buffer, 14, 1); // NumberOfIdEntries
BinaryBufferWriter.WriteU32LE(buffer, 16, 24); // ID = RT_MANIFEST
BinaryBufferWriter.WriteU32LE(buffer, 20, 0x80000000 | 0x30); // Subdirectory offset
// Name/ID subdirectory at 0x30
BinaryBufferWriter.WriteU16LE(buffer, 0x30 + 14, 1);
BinaryBufferWriter.WriteU32LE(buffer, 0x30 + 16, 1); // ID = 1
BinaryBufferWriter.WriteU32LE(buffer, 0x30 + 20, 0x80000000 | 0x50);
// Language subdirectory at 0x50
BinaryBufferWriter.WriteU16LE(buffer, 0x50 + 14, 1);
BinaryBufferWriter.WriteU32LE(buffer, 0x50 + 16, 0x409); // English
BinaryBufferWriter.WriteU32LE(buffer, 0x50 + 20, 0x70); // Data entry
// Data entry at 0x70
BinaryBufferWriter.WriteU32LE(buffer, 0x70, (uint)(sectionRva + 0x100)); // Data RVA
BinaryBufferWriter.WriteU32LE(buffer, 0x74, (uint)manifestBytes.Length);
// Manifest data at 0x100
manifestBytes.CopyTo(buffer, 0x100);
return buffer.AsSpan(0, BinaryBufferWriter.AlignTo(0x100 + manifestBytes.Length, 16)).ToArray();
}
#endregion
#region Factory Methods
/// <summary>
/// Creates a builder for 64-bit console applications.
/// </summary>
public static PeBuilder Console64() => new PeBuilder()
.Is64Bit()
.WithSubsystem(PeSubsystem.WindowsConsole)
.WithMachine(PeMachine.Amd64);
/// <summary>
/// Creates a builder for 64-bit GUI applications.
/// </summary>
public static PeBuilder Gui64() => new PeBuilder()
.Is64Bit()
.WithSubsystem(PeSubsystem.WindowsGui)
.WithMachine(PeMachine.Amd64);
/// <summary>
/// Creates a builder for 32-bit console applications.
/// </summary>
public static PeBuilder Console32() => new PeBuilder()
.Is32Bit()
.WithSubsystem(PeSubsystem.WindowsConsole)
.WithMachine(PeMachine.I386);
/// <summary>
/// Creates a builder for 32-bit GUI applications.
/// </summary>
public static PeBuilder Gui32() => new PeBuilder()
.Is32Bit()
.WithSubsystem(PeSubsystem.WindowsGui)
.WithMachine(PeMachine.I386);
#endregion
}

View File

@@ -1,21 +1,20 @@
using System.Text;
using FluentAssertions;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class MachOLoadCommandParserTests
public class MachOLoadCommandParserTests : NativeTestBase
{
[Fact]
public void ParsesMinimalMachO64LittleEndian()
{
var buffer = new byte[256];
SetupMachO64Header(buffer, littleEndian: true);
// Build minimal Mach-O 64-bit little-endian using builder
var macho = MachOBuilder.MacOSX64().Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.IsUniversal.Should().BeFalse();
info.Slices.Should().HaveCount(1);
info.Slices[0].CpuType.Should().Be("x86_64");
@@ -24,13 +23,15 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesMinimalMachO64BigEndian()
{
var buffer = new byte[256];
SetupMachO64Header(buffer, littleEndian: false);
// Build minimal Mach-O 64-bit big-endian using builder
var macho = new MachOBuilder()
.Is64Bit()
.BigEndian()
.WithCpuType(MachOCpuType.X86_64)
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.IsUniversal.Should().BeFalse();
info.Slices.Should().HaveCount(1);
info.Slices[0].CpuType.Should().Be("x86_64");
@@ -39,13 +40,14 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesMachOWithDylibs()
{
var buffer = new byte[512];
SetupMachO64WithDylibs(buffer);
// Build Mach-O with dylib dependencies using builder
var macho = MachOBuilder.MacOSX64()
.AddDylib("/usr/lib/libSystem.B.dylib")
.AddDylib("/usr/lib/libc++.1.dylib")
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.Slices.Should().HaveCount(1);
info.Slices[0].Dependencies.Should().HaveCount(2);
info.Slices[0].Dependencies[0].Path.Should().Be("/usr/lib/libSystem.B.dylib");
@@ -56,13 +58,14 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesMachOWithRpath()
{
var buffer = new byte[512];
SetupMachO64WithRpath(buffer);
// Build Mach-O with rpaths using builder
var macho = MachOBuilder.MacOSX64()
.AddRpath("@executable_path/../Frameworks")
.AddRpath("@loader_path/../lib")
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.Slices[0].Rpaths.Should().HaveCount(2);
info.Slices[0].Rpaths[0].Should().Be("@executable_path/../Frameworks");
info.Slices[0].Rpaths[1].Should().Be("@loader_path/../lib");
@@ -71,13 +74,14 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesMachOWithUuid()
{
var buffer = new byte[256];
SetupMachO64WithUuid(buffer);
// Build Mach-O with UUID using builder
var uuid = Guid.Parse("deadbeef-1234-5678-9abc-def011223344");
var macho = MachOBuilder.MacOSX64()
.WithUuid(uuid)
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.Slices[0].Uuid.Should().NotBeNullOrEmpty();
info.Slices[0].Uuid.Should().MatchRegex(@"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
}
@@ -85,13 +89,11 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesFatBinary()
{
var buffer = new byte[1024];
SetupFatBinary(buffer);
// Build universal (fat) binary using builder
var macho = MachOBuilder.Universal().Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.IsUniversal.Should().BeTrue();
info.Slices.Should().HaveCount(2);
info.Slices[0].CpuType.Should().Be("x86_64");
@@ -101,13 +103,14 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesWeakAndReexportDylibs()
{
var buffer = new byte[512];
SetupMachO64WithWeakAndReexport(buffer);
// Build Mach-O with weak and reexport dylibs using builder
var macho = MachOBuilder.MacOSX64()
.AddWeakDylib("/usr/lib/libz.1.dylib")
.AddReexportDylib("/usr/lib/libxml2.2.dylib")
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.Slices[0].Dependencies.Should().Contain(d => d.ReasonCode == "macho-weaklib");
info.Slices[0].Dependencies.Should().Contain(d => d.ReasonCode == "macho-reexport");
}
@@ -115,13 +118,14 @@ public class MachOLoadCommandParserTests
[Fact]
public void DeduplicatesDylibs()
{
var buffer = new byte[512];
SetupMachO64WithDuplicateDylibs(buffer);
// Build Mach-O with duplicate dylibs - builder or parser should deduplicate
var macho = MachOBuilder.MacOSX64()
.AddDylib("/usr/lib/libSystem.B.dylib")
.AddDylib("/usr/lib/libSystem.B.dylib") // Duplicate
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.Slices[0].Dependencies.Should().HaveCount(1);
}
@@ -150,250 +154,14 @@ public class MachOLoadCommandParserTests
[Fact]
public void ParsesVersionNumbers()
{
var buffer = new byte[512];
SetupMachO64WithVersionedDylib(buffer);
// Build Mach-O with versioned dylib using builder
var macho = MachOBuilder.MacOSX64()
.AddDylib("/usr/lib/libfoo.dylib", "1.2.3", "1.0.0")
.Build();
using var stream = new MemoryStream(buffer);
var result = MachOLoadCommandParser.TryParse(stream, out var info);
var info = ParseMachO(macho);
result.Should().BeTrue();
info.Slices[0].Dependencies[0].CurrentVersion.Should().Be("1.2.3");
info.Slices[0].Dependencies[0].CompatibilityVersion.Should().Be("1.0.0");
}
private static void SetupMachO64Header(byte[] buffer, bool littleEndian, int ncmds = 0, int sizeofcmds = 0)
{
// Mach-O 64-bit header
if (littleEndian)
{
BitConverter.GetBytes(0xFEEDFACFu).CopyTo(buffer, 0); // magic
BitConverter.GetBytes(0x01000007u).CopyTo(buffer, 4); // cputype = x86_64
BitConverter.GetBytes(0x00000003u).CopyTo(buffer, 8); // cpusubtype
BitConverter.GetBytes(0x00000002u).CopyTo(buffer, 12); // filetype = MH_EXECUTE
BitConverter.GetBytes((uint)ncmds).CopyTo(buffer, 16); // ncmds
BitConverter.GetBytes((uint)sizeofcmds).CopyTo(buffer, 20); // sizeofcmds
BitConverter.GetBytes(0x00200085u).CopyTo(buffer, 24); // flags
BitConverter.GetBytes(0x00000000u).CopyTo(buffer, 28); // reserved
}
else
{
// Big endian (CIGAM_64 = 0xCFFAEDFE stored as little endian bytes)
// When read as little endian, [FE, ED, FA, CF] -> 0xCFFAEDFE
buffer[0] = 0xFE; buffer[1] = 0xED; buffer[2] = 0xFA; buffer[3] = 0xCF;
WriteUInt32BE(buffer, 4, 0x01000007u); // cputype
WriteUInt32BE(buffer, 8, 0x00000003u); // cpusubtype
WriteUInt32BE(buffer, 12, 0x00000002u); // filetype
WriteUInt32BE(buffer, 16, (uint)ncmds);
WriteUInt32BE(buffer, 20, (uint)sizeofcmds);
WriteUInt32BE(buffer, 24, 0x00200085u);
WriteUInt32BE(buffer, 28, 0x00000000u);
}
}
private static void SetupMachO64WithDylibs(byte[] buffer)
{
var cmdOffset = 32; // After mach_header_64
// LC_LOAD_DYLIB for libSystem
var lib1 = "/usr/lib/libSystem.B.dylib\0";
var cmdSize1 = 24 + lib1.Length;
cmdSize1 = (cmdSize1 + 7) & ~7; // Align to 8 bytes
// LC_LOAD_DYLIB for libc++
var lib2 = "/usr/lib/libc++.1.dylib\0";
var cmdSize2 = 24 + lib2.Length;
cmdSize2 = (cmdSize2 + 7) & ~7;
SetupMachO64Header(buffer, littleEndian: true, ncmds: 2, sizeofcmds: cmdSize1 + cmdSize2);
// First dylib
BitConverter.GetBytes(0x0Cu).CopyTo(buffer, cmdOffset); // LC_LOAD_DYLIB
BitConverter.GetBytes((uint)cmdSize1).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(24u).CopyTo(buffer, cmdOffset + 8); // name offset
BitConverter.GetBytes(0u).CopyTo(buffer, cmdOffset + 12); // timestamp
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 16); // current_version (1.0.0)
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 20); // compatibility_version
Encoding.UTF8.GetBytes(lib1).CopyTo(buffer, cmdOffset + 24);
cmdOffset += cmdSize1;
// Second dylib
BitConverter.GetBytes(0x0Cu).CopyTo(buffer, cmdOffset);
BitConverter.GetBytes((uint)cmdSize2).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(24u).CopyTo(buffer, cmdOffset + 8);
BitConverter.GetBytes(0u).CopyTo(buffer, cmdOffset + 12);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 16);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 20);
Encoding.UTF8.GetBytes(lib2).CopyTo(buffer, cmdOffset + 24);
}
private static void SetupMachO64WithRpath(byte[] buffer)
{
var cmdOffset = 32;
var rpath1 = "@executable_path/../Frameworks\0";
var cmdSize1 = 12 + rpath1.Length;
cmdSize1 = (cmdSize1 + 7) & ~7;
var rpath2 = "@loader_path/../lib\0";
var cmdSize2 = 12 + rpath2.Length;
cmdSize2 = (cmdSize2 + 7) & ~7;
SetupMachO64Header(buffer, littleEndian: true, ncmds: 2, sizeofcmds: cmdSize1 + cmdSize2);
// LC_RPATH 1
BitConverter.GetBytes(0x8000001Cu).CopyTo(buffer, cmdOffset); // LC_RPATH
BitConverter.GetBytes((uint)cmdSize1).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(12u).CopyTo(buffer, cmdOffset + 8); // path offset
Encoding.UTF8.GetBytes(rpath1).CopyTo(buffer, cmdOffset + 12);
cmdOffset += cmdSize1;
// LC_RPATH 2
BitConverter.GetBytes(0x8000001Cu).CopyTo(buffer, cmdOffset);
BitConverter.GetBytes((uint)cmdSize2).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(12u).CopyTo(buffer, cmdOffset + 8);
Encoding.UTF8.GetBytes(rpath2).CopyTo(buffer, cmdOffset + 12);
}
private static void SetupMachO64WithUuid(byte[] buffer)
{
var cmdOffset = 32;
var cmdSize = 24; // LC_UUID is 24 bytes
SetupMachO64Header(buffer, littleEndian: true, ncmds: 1, sizeofcmds: cmdSize);
BitConverter.GetBytes(0x1Bu).CopyTo(buffer, cmdOffset); // LC_UUID
BitConverter.GetBytes((uint)cmdSize).CopyTo(buffer, cmdOffset + 4);
// UUID bytes
var uuid = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x12, 0x34, 0x56, 0x78,
0x9A, 0xBC, 0xDE, 0xF0, 0x11, 0x22, 0x33, 0x44 };
uuid.CopyTo(buffer, cmdOffset + 8);
}
private static void SetupFatBinary(byte[] buffer)
{
// Fat header (big endian)
buffer[0] = 0xCA; buffer[1] = 0xFE; buffer[2] = 0xBA; buffer[3] = 0xBE;
WriteUInt32BE(buffer, 4, 2); // nfat_arch = 2
// First architecture (x86_64) - fat_arch at offset 8
WriteUInt32BE(buffer, 8, 0x01000007); // cputype
WriteUInt32BE(buffer, 12, 0x00000003); // cpusubtype
WriteUInt32BE(buffer, 16, 256); // offset
WriteUInt32BE(buffer, 20, 64); // size
WriteUInt32BE(buffer, 24, 8); // align
// Second architecture (arm64) - fat_arch at offset 28
WriteUInt32BE(buffer, 28, 0x0100000C); // cputype (arm64)
WriteUInt32BE(buffer, 32, 0x00000000); // cpusubtype
WriteUInt32BE(buffer, 36, 512); // offset
WriteUInt32BE(buffer, 40, 64); // size
WriteUInt32BE(buffer, 44, 8); // align
// x86_64 slice at offset 256
SetupMachO64Slice(buffer, 256, 0x01000007);
// arm64 slice at offset 512
SetupMachO64Slice(buffer, 512, 0x0100000C);
}
private static void SetupMachO64Slice(byte[] buffer, int offset, uint cputype)
{
BitConverter.GetBytes(0xFEEDFACFu).CopyTo(buffer, offset);
BitConverter.GetBytes(cputype).CopyTo(buffer, offset + 4);
BitConverter.GetBytes(0x00000000u).CopyTo(buffer, offset + 8);
BitConverter.GetBytes(0x00000002u).CopyTo(buffer, offset + 12);
BitConverter.GetBytes(0u).CopyTo(buffer, offset + 16); // ncmds
BitConverter.GetBytes(0u).CopyTo(buffer, offset + 20); // sizeofcmds
}
private static void SetupMachO64WithWeakAndReexport(byte[] buffer)
{
var cmdOffset = 32;
var lib1 = "/usr/lib/libz.1.dylib\0";
var cmdSize1 = 24 + lib1.Length;
cmdSize1 = (cmdSize1 + 7) & ~7;
var lib2 = "/usr/lib/libxml2.2.dylib\0";
var cmdSize2 = 24 + lib2.Length;
cmdSize2 = (cmdSize2 + 7) & ~7;
SetupMachO64Header(buffer, littleEndian: true, ncmds: 2, sizeofcmds: cmdSize1 + cmdSize2);
// LC_LOAD_WEAK_DYLIB
BitConverter.GetBytes(0x80000018u).CopyTo(buffer, cmdOffset);
BitConverter.GetBytes((uint)cmdSize1).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(24u).CopyTo(buffer, cmdOffset + 8);
BitConverter.GetBytes(0u).CopyTo(buffer, cmdOffset + 12);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 16);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 20);
Encoding.UTF8.GetBytes(lib1).CopyTo(buffer, cmdOffset + 24);
cmdOffset += cmdSize1;
// LC_REEXPORT_DYLIB
BitConverter.GetBytes(0x8000001Fu).CopyTo(buffer, cmdOffset);
BitConverter.GetBytes((uint)cmdSize2).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(24u).CopyTo(buffer, cmdOffset + 8);
BitConverter.GetBytes(0u).CopyTo(buffer, cmdOffset + 12);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 16);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 20);
Encoding.UTF8.GetBytes(lib2).CopyTo(buffer, cmdOffset + 24);
}
private static void SetupMachO64WithDuplicateDylibs(byte[] buffer)
{
var cmdOffset = 32;
var lib = "/usr/lib/libSystem.B.dylib\0";
var cmdSize = 24 + lib.Length;
cmdSize = (cmdSize + 7) & ~7;
SetupMachO64Header(buffer, littleEndian: true, ncmds: 2, sizeofcmds: cmdSize * 2);
// Same dylib twice
for (var i = 0; i < 2; i++)
{
BitConverter.GetBytes(0x0Cu).CopyTo(buffer, cmdOffset);
BitConverter.GetBytes((uint)cmdSize).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(24u).CopyTo(buffer, cmdOffset + 8);
BitConverter.GetBytes(0u).CopyTo(buffer, cmdOffset + 12);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 16);
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 20);
Encoding.UTF8.GetBytes(lib).CopyTo(buffer, cmdOffset + 24);
cmdOffset += cmdSize;
}
}
private static void SetupMachO64WithVersionedDylib(byte[] buffer)
{
var cmdOffset = 32;
var lib = "/usr/lib/libfoo.dylib\0";
var cmdSize = 24 + lib.Length;
cmdSize = (cmdSize + 7) & ~7;
SetupMachO64Header(buffer, littleEndian: true, ncmds: 1, sizeofcmds: cmdSize);
BitConverter.GetBytes(0x0Cu).CopyTo(buffer, cmdOffset);
BitConverter.GetBytes((uint)cmdSize).CopyTo(buffer, cmdOffset + 4);
BitConverter.GetBytes(24u).CopyTo(buffer, cmdOffset + 8);
BitConverter.GetBytes(0u).CopyTo(buffer, cmdOffset + 12);
// Version 1.2.3 = (1 << 16) | (2 << 8) | 3 = 0x10203
BitConverter.GetBytes(0x10203u).CopyTo(buffer, cmdOffset + 16);
// Compat 1.0.0 = (1 << 16) | (0 << 8) | 0 = 0x10000
BitConverter.GetBytes(0x10000u).CopyTo(buffer, cmdOffset + 20);
Encoding.UTF8.GetBytes(lib).CopyTo(buffer, cmdOffset + 24);
}
private static void WriteUInt32BE(byte[] buffer, int offset, uint value)
{
buffer[offset] = (byte)(value >> 24);
buffer[offset + 1] = (byte)(value >> 16);
buffer[offset + 2] = (byte)(value >> 8);
buffer[offset + 3] = (byte)value;
}
}

View File

@@ -0,0 +1,298 @@
using FluentAssertions;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
/// <summary>
/// Parameterized tests demonstrating the native binary builder framework.
/// These tests use Theory/InlineData to test multiple configurations.
/// </summary>
public class NativeBuilderParameterizedTests : NativeTestBase
{
#region ELF Parameterized Tests
[Theory]
[InlineData(true, false)] // 64-bit, little-endian
[InlineData(true, true)] // 64-bit, big-endian
public void ElfBuilder_ParsesDependencies_AllFormats(bool is64Bit, bool isBigEndian)
{
// Arrange
var elf = new ElfBuilder()
.Is64Bit(is64Bit)
.BigEndian(isBigEndian)
.AddDependencies("libc.so.6", "libm.so.6")
.Build();
// Act
var info = ParseElf(elf);
// Assert
info.Dependencies.Should().HaveCount(2);
info.Dependencies[0].Soname.Should().Be("libc.so.6");
info.Dependencies[1].Soname.Should().Be("libm.so.6");
}
[Theory]
[InlineData("GLIBC_2.17", false)]
[InlineData("GLIBC_2.28", false)]
[InlineData("GLIBC_2.34", true)]
public void ElfBuilder_ParsesVersionNeeds_WithWeakFlag(string version, bool isWeak)
{
// Arrange
var elf = ElfBuilder.LinuxX64()
.AddDependency("libc.so.6")
.AddVersionNeed("libc.so.6", version, isWeak)
.Build();
// Act
var info = ParseElf(elf);
// Assert
info.Dependencies.Should().HaveCount(1);
var dep = info.Dependencies[0];
dep.Soname.Should().Be("libc.so.6");
dep.VersionNeeds.Should().HaveCount(1);
dep.VersionNeeds[0].Version.Should().Be(version);
dep.VersionNeeds[0].IsWeak.Should().Be(isWeak);
}
[Fact]
public void ElfBuilder_LinuxX64Factory_CreatesValidElf()
{
// Arrange
var elf = ElfBuilder.LinuxX64()
.AddDependency("libc.so.6")
.WithRpath("/opt/lib")
.WithBuildId("deadbeef01020304")
.Build();
// Act
var info = ParseElf(elf);
// Assert
info.Dependencies.Should().HaveCount(1);
info.Interpreter.Should().Be("/lib64/ld-linux-x86-64.so.2");
info.Rpath.Should().Contain("/opt/lib");
info.BinaryId.Should().Be("deadbeef01020304");
}
#endregion
#region PE Parameterized Tests
[Theory]
[InlineData(false)] // PE32 with 4-byte thunks
[InlineData(true)] // PE32+ with 8-byte thunks
public void PeBuilder_ParsesImports_CorrectBitness(bool is64Bit)
{
// Arrange
var pe = new PeBuilder()
.Is64Bit(is64Bit)
.AddImport("kernel32.dll", "GetProcAddress", "LoadLibraryA")
.Build();
// Act
var info = ParsePe(pe);
// Assert
info.Is64Bit.Should().Be(is64Bit);
info.Dependencies.Should().HaveCount(1);
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
info.Dependencies[0].ImportedFunctions.Should().Contain("GetProcAddress");
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
}
[Theory]
[InlineData(PeSubsystem.WindowsConsole)]
[InlineData(PeSubsystem.WindowsGui)]
public void PeBuilder_SetsSubsystem_Correctly(PeSubsystem subsystem)
{
// Arrange
var pe = PeBuilder.Console64()
.WithSubsystem(subsystem)
.Build();
// Act
var info = ParsePe(pe);
// Assert
info.Subsystem.Should().Be(subsystem);
}
[Fact]
public void PeBuilder_Console64Factory_CreatesValidPe()
{
// Arrange
var pe = PeBuilder.Console64()
.AddImport("kernel32.dll", "GetProcAddress")
.AddDelayImport("advapi32.dll", "RegOpenKeyA")
.Build();
// Act
var info = ParsePe(pe);
// Assert
info.Is64Bit.Should().BeTrue();
info.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
info.Dependencies.Should().HaveCount(1);
info.DelayLoadDependencies.Should().HaveCount(1);
}
[Fact]
public void PeBuilder_WithManifest_CreatesValidPe()
{
// Arrange
var pe = PeBuilder.Console64()
.WithSxsDependency("Microsoft.Windows.Common-Controls", "6.0.0.0",
"6595b64144ccf1df", "*")
.Build();
// Act
var info = ParsePe(pe);
// Assert
info.SxsDependencies.Should().Contain(d => d.Name == "Microsoft.Windows.Common-Controls");
}
#endregion
#region Mach-O Parameterized Tests
[Theory]
[InlineData(MachODylibKind.Load, "macho-loadlib")]
[InlineData(MachODylibKind.Weak, "macho-weaklib")]
[InlineData(MachODylibKind.Reexport, "macho-reexport")]
[InlineData(MachODylibKind.Lazy, "macho-lazylib")]
public void MachOBuilder_ParsesDylibKind_CorrectReasonCode(MachODylibKind kind, string expectedReason)
{
// Arrange
var macho = MachOBuilder.MacOSArm64()
.AddDylib("/usr/lib/libfoo.dylib", kind)
.Build();
// Act
var info = ParseMachO(macho);
// Assert
info.Slices.Should().HaveCount(1);
info.Slices[0].Dependencies.Should().HaveCount(1);
info.Slices[0].Dependencies[0].ReasonCode.Should().Be(expectedReason);
}
[Theory]
[InlineData(MachOCpuType.X86_64, "x86_64")]
[InlineData(MachOCpuType.Arm64, "arm64")]
public void MachOBuilder_SetsCpuType_Correctly(MachOCpuType cpuType, string expectedName)
{
// Arrange
var macho = new MachOBuilder()
.Is64Bit()
.LittleEndian()
.WithCpuType(cpuType)
.Build();
// Act
var info = ParseMachO(macho);
// Assert
info.Slices.Should().HaveCount(1);
info.Slices[0].CpuType.Should().Be(expectedName);
}
[Fact]
public void MachOBuilder_MacOSArm64Factory_CreatesValidMachO()
{
// Arrange
var macho = MachOBuilder.MacOSArm64()
.AddDylib("/usr/lib/libSystem.B.dylib")
.AddWeakDylib("/usr/lib/liboptional.dylib")
.AddRpath("@executable_path/../Frameworks")
.WithUuid(Guid.Parse("deadbeef-1234-5678-9abc-def012345678"))
.Build();
// Act
var info = ParseMachO(macho);
// Assert
info.Slices.Should().HaveCount(1);
info.Slices[0].CpuType.Should().Be("arm64");
info.Slices[0].Dependencies.Should().HaveCount(2);
info.Slices[0].Dependencies[0].ReasonCode.Should().Be("macho-loadlib");
info.Slices[0].Dependencies[1].ReasonCode.Should().Be("macho-weaklib");
info.Slices[0].Rpaths.Should().Contain("@executable_path/../Frameworks");
info.Slices[0].Uuid.Should().NotBeNullOrEmpty();
}
[Fact]
public void MachOBuilder_Universal_CreatesFatBinary()
{
// Arrange
var macho = MachOBuilder.Universal()
.AddDylib("/usr/lib/libSystem.B.dylib")
.Build();
// Act
var info = ParseMachO(macho);
// Assert
info.IsUniversal.Should().BeTrue();
info.Slices.Should().HaveCount(2);
}
[Fact]
public void MachOBuilder_WithVersion_ParsesVersionNumbers()
{
// Arrange
var macho = MachOBuilder.MacOSArm64()
.AddDylib("/usr/lib/libfoo.dylib", "1.2.3", "1.0.0")
.Build();
// Act
var info = ParseMachO(macho);
// Assert
info.Slices[0].Dependencies[0].CurrentVersion.Should().Be("1.2.3");
info.Slices[0].Dependencies[0].CompatibilityVersion.Should().Be("1.0.0");
}
#endregion
#region Cross-Format Tests
[Fact]
public void AllBuilders_ProduceParseable_Binaries()
{
// Arrange
var elf = ElfBuilder.LinuxX64().AddDependency("libc.so.6").Build();
var pe = PeBuilder.Console64().AddImport("kernel32.dll").Build();
var macho = MachOBuilder.MacOSArm64().AddDylib("/usr/lib/libSystem.B.dylib").Build();
// Act & Assert - All should parse successfully
TryParseElf(elf, out _).Should().BeTrue();
TryParsePe(pe, out _).Should().BeTrue();
TryParseMachO(macho, out _).Should().BeTrue();
}
[Fact]
public void AllBuilders_RejectWrongFormat()
{
// Arrange
var elf = ElfBuilder.LinuxX64().Build();
var pe = PeBuilder.Console64().Build();
var macho = MachOBuilder.MacOSArm64().Build();
// Act & Assert - Cross-format parsing should fail
TryParsePe(elf, out _).Should().BeFalse();
TryParseMachO(elf, out _).Should().BeFalse();
TryParseElf(pe, out _).Should().BeFalse();
TryParseMachO(pe, out _).Should().BeFalse();
TryParseElf(macho, out _).Should().BeFalse();
TryParsePe(macho, out _).Should().BeFalse();
}
#endregion
}

View File

@@ -1,21 +1,23 @@
using System.Text;
using FluentAssertions;
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
using StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
namespace StellaOps.Scanner.Analyzers.Native.Tests;
public class PeImportParserTests
public class PeImportParserTests : NativeTestBase
{
[Fact]
public void ParsesMinimalPe32()
{
var buffer = new byte[1024];
SetupPe32Header(buffer);
// Build minimal PE32 using builder
var pe = new PeBuilder()
.Is64Bit(false)
.WithSubsystem(PeSubsystem.WindowsConsole)
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.Is64Bit.Should().BeFalse();
info.Machine.Should().Be("x86_64");
info.Subsystem.Should().Be(PeSubsystem.WindowsConsole);
@@ -24,13 +26,11 @@ public class PeImportParserTests
[Fact]
public void ParsesMinimalPe32Plus()
{
var buffer = new byte[1024];
SetupPe32PlusHeader(buffer);
// Build minimal PE32+ using builder
var pe = PeBuilder.Console64().Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.Is64Bit.Should().BeTrue();
info.Machine.Should().Be("x86_64");
}
@@ -38,13 +38,14 @@ public class PeImportParserTests
[Fact]
public void ParsesPeWithImports()
{
var buffer = new byte[4096];
SetupPe32HeaderWithImports(buffer, out var importDirRva, out var importDirSize);
// Build PE with imports using builder
var pe = PeBuilder.Console64()
.AddImport("kernel32.dll", "GetProcAddress")
.AddImport("user32.dll", "MessageBoxA")
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.Dependencies.Should().HaveCount(2);
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
info.Dependencies[0].ReasonCode.Should().Be("pe-import");
@@ -54,13 +55,14 @@ public class PeImportParserTests
[Fact]
public void DeduplicatesImports()
{
var buffer = new byte[4096];
SetupPe32HeaderWithDuplicateImports(buffer);
// Build PE with duplicate imports - builder or parser should deduplicate
var pe = PeBuilder.Console64()
.AddImport("kernel32.dll", "GetProcAddress")
.AddImport("kernel32.dll", "LoadLibraryA") // Same DLL, different function
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.Dependencies.Should().HaveCount(1);
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
}
@@ -68,13 +70,13 @@ public class PeImportParserTests
[Fact]
public void ParsesDelayLoadImports()
{
var buffer = new byte[4096];
SetupPe32HeaderWithDelayImports(buffer);
// Build PE with delay imports using builder
var pe = PeBuilder.Console64()
.AddDelayImport("advapi32.dll", "RegOpenKeyA")
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.DelayLoadDependencies.Should().HaveCount(1);
info.DelayLoadDependencies[0].DllName.Should().Be("advapi32.dll");
info.DelayLoadDependencies[0].ReasonCode.Should().Be("pe-delayimport");
@@ -83,13 +85,13 @@ public class PeImportParserTests
[Fact]
public void ParsesSubsystem()
{
var buffer = new byte[1024];
SetupPe32Header(buffer, subsystem: PeSubsystem.WindowsGui);
// Build PE with GUI subsystem using builder
var pe = PeBuilder.Console64()
.WithSubsystem(PeSubsystem.WindowsGui)
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.Subsystem.Should().Be(PeSubsystem.WindowsGui);
}
@@ -118,175 +120,28 @@ public class PeImportParserTests
[Fact]
public void ParsesEmbeddedManifest()
{
var buffer = new byte[8192];
SetupPe32HeaderWithManifest(buffer);
// Build PE with SxS dependency manifest using builder
var pe = PeBuilder.Console64()
.WithSxsDependency("Microsoft.Windows.Common-Controls", "6.0.0.0",
"6595b64144ccf1df", "*")
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.SxsDependencies.Should().HaveCountGreaterOrEqualTo(1);
info.SxsDependencies[0].Name.Should().Be("Microsoft.Windows.Common-Controls");
}
private static void SetupPe32Header(byte[] buffer, PeSubsystem subsystem = PeSubsystem.WindowsConsole)
{
// DOS header
buffer[0] = (byte)'M';
buffer[1] = (byte)'Z';
BitConverter.GetBytes(0x80).CopyTo(buffer, 0x3C); // e_lfanew
// PE signature
var peOffset = 0x80;
buffer[peOffset] = (byte)'P';
buffer[peOffset + 1] = (byte)'E';
// COFF header
BitConverter.GetBytes((ushort)0x8664).CopyTo(buffer, peOffset + 4); // Machine = x86_64
BitConverter.GetBytes((ushort)1).CopyTo(buffer, peOffset + 6); // NumberOfSections
BitConverter.GetBytes((ushort)0xE0).CopyTo(buffer, peOffset + 20); // SizeOfOptionalHeader (PE32)
// Optional header (PE32)
var optHeaderOffset = peOffset + 24;
BitConverter.GetBytes((ushort)0x10b).CopyTo(buffer, optHeaderOffset); // Magic = PE32
BitConverter.GetBytes((ushort)subsystem).CopyTo(buffer, optHeaderOffset + 68); // Subsystem
BitConverter.GetBytes((uint)16).CopyTo(buffer, optHeaderOffset + 92); // NumberOfRvaAndSizes
// Section header (.text)
var sectionOffset = optHeaderOffset + 0xE0;
".text\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8); // VirtualSize
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 16); // SizeOfRawData
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
}
private static void SetupPe32PlusHeader(byte[] buffer)
{
SetupPe32Header(buffer);
var optHeaderOffset = 0x80 + 24;
BitConverter.GetBytes((ushort)0x20b).CopyTo(buffer, optHeaderOffset); // Magic = PE32+
BitConverter.GetBytes((ushort)0xF0).CopyTo(buffer, 0x80 + 20); // SizeOfOptionalHeader (PE32+)
BitConverter.GetBytes((uint)16).CopyTo(buffer, optHeaderOffset + 108); // NumberOfRvaAndSizes for PE32+
}
private static void SetupPe32HeaderWithImports(byte[] buffer, out uint importDirRva, out uint importDirSize)
{
SetupPe32Header(buffer);
// Section for imports
var sectionOffset = 0x80 + 24 + 0xE0;
".idata\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8); // VirtualSize
BitConverter.GetBytes((uint)0x2000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 16); // SizeOfRawData
BitConverter.GetBytes((uint)0x400).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
// Update number of sections
BitConverter.GetBytes((ushort)2).CopyTo(buffer, 0x80 + 6);
// Set import directory in data directory
var optHeaderOffset = 0x80 + 24;
var dataDirOffset = optHeaderOffset + 96; // After standard fields
importDirRva = 0x2000;
importDirSize = 60;
BitConverter.GetBytes(importDirRva).CopyTo(buffer, dataDirOffset + 8); // Import Directory RVA
BitConverter.GetBytes(importDirSize).CopyTo(buffer, dataDirOffset + 12); // Import Directory Size
// Import descriptors at file offset 0x400
var importOffset = 0x400;
// Import descriptor 1 (kernel32.dll)
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset); // OriginalFirstThunk
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 4); // TimeDateStamp
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 8); // ForwarderChain
BitConverter.GetBytes((uint)0x2100).CopyTo(buffer, importOffset + 12); // Name RVA
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 16); // FirstThunk
// Import descriptor 2 (user32.dll)
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 20);
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 24);
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 28);
BitConverter.GetBytes((uint)0x2110).CopyTo(buffer, importOffset + 32); // Name RVA
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 36);
// Null terminator
// (already zero)
// DLL names at file offset 0x500 (RVA 0x2100)
var nameOffset = 0x500;
"kernel32.dll\0"u8.CopyTo(buffer.AsSpan(nameOffset));
"user32.dll\0"u8.CopyTo(buffer.AsSpan(nameOffset + 0x10));
}
private static void SetupPe32HeaderWithDuplicateImports(byte[] buffer)
{
SetupPe32HeaderWithImports(buffer, out _, out _);
// Modify second import to also be kernel32.dll
var nameOffset = 0x500 + 0x10;
"kernel32.dll\0"u8.CopyTo(buffer.AsSpan(nameOffset));
}
private static void SetupPe32HeaderWithDelayImports(byte[] buffer)
{
SetupPe32Header(buffer);
// Section for imports
var sectionOffset = 0x80 + 24 + 0xE0;
".didat\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8);
BitConverter.GetBytes((uint)0x3000).CopyTo(buffer, sectionOffset + 12);
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 16);
BitConverter.GetBytes((uint)0x600).CopyTo(buffer, sectionOffset + 20);
BitConverter.GetBytes((ushort)2).CopyTo(buffer, 0x80 + 6);
// Set delay import directory
var optHeaderOffset = 0x80 + 24;
var dataDirOffset = optHeaderOffset + 96;
BitConverter.GetBytes((uint)0x3000).CopyTo(buffer, dataDirOffset + 104); // Delay Import RVA (entry 13)
BitConverter.GetBytes((uint)64).CopyTo(buffer, dataDirOffset + 108);
// Delay import descriptor at file offset 0x600
var delayImportOffset = 0x600;
BitConverter.GetBytes((uint)1).CopyTo(buffer, delayImportOffset); // Attributes
BitConverter.GetBytes((uint)0x3100).CopyTo(buffer, delayImportOffset + 4); // Name RVA
// DLL name at file offset 0x700 (RVA 0x3100)
"advapi32.dll\0"u8.CopyTo(buffer.AsSpan(0x700));
}
private static void SetupPe32HeaderWithManifest(byte[] buffer)
{
SetupPe32Header(buffer);
// Add manifest XML directly in the buffer (search-based parsing will find it)
var manifestXml = """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df"/>
</dependentAssembly>
</dependency>
</assembly>
""";
Encoding.UTF8.GetBytes(manifestXml).CopyTo(buffer, 0x1000);
}
[Fact]
public void ParsesPe32PlusWithImportThunks()
{
// Test that 64-bit PE files correctly parse 8-byte import thunks
var buffer = new byte[8192];
SetupPe32PlusHeaderWithImports(buffer);
var pe = PeBuilder.Console64()
.AddImport("kernel32.dll", "GetProcAddress", "LoadLibraryA")
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.Is64Bit.Should().BeTrue();
info.Dependencies.Should().HaveCount(1);
info.Dependencies[0].DllName.Should().Be("kernel32.dll");
@@ -295,206 +150,18 @@ public class PeImportParserTests
info.Dependencies[0].ImportedFunctions.Should().Contain("LoadLibraryA");
}
private static void SetupPe32PlusHeaderWithImports(byte[] buffer)
{
// DOS header
buffer[0] = (byte)'M';
buffer[1] = (byte)'Z';
BitConverter.GetBytes(0x80).CopyTo(buffer, 0x3C); // e_lfanew
// PE signature
var peOffset = 0x80;
buffer[peOffset] = (byte)'P';
buffer[peOffset + 1] = (byte)'E';
// COFF header
BitConverter.GetBytes((ushort)0x8664).CopyTo(buffer, peOffset + 4); // Machine = x86_64
BitConverter.GetBytes((ushort)2).CopyTo(buffer, peOffset + 6); // NumberOfSections
BitConverter.GetBytes((ushort)0xF0).CopyTo(buffer, peOffset + 20); // SizeOfOptionalHeader (PE32+)
// Optional header (PE32+)
var optHeaderOffset = peOffset + 24;
BitConverter.GetBytes((ushort)0x20b).CopyTo(buffer, optHeaderOffset); // Magic = PE32+
BitConverter.GetBytes((ushort)PeSubsystem.WindowsConsole).CopyTo(buffer, optHeaderOffset + 68); // Subsystem
BitConverter.GetBytes((uint)16).CopyTo(buffer, optHeaderOffset + 108); // NumberOfRvaAndSizes
// Data directory - Import Directory (entry 1)
var dataDirOffset = optHeaderOffset + 112;
BitConverter.GetBytes((uint)0x2000).CopyTo(buffer, dataDirOffset + 8); // Import Directory RVA
BitConverter.GetBytes((uint)40).CopyTo(buffer, dataDirOffset + 12); // Import Directory Size
// Section headers
var sectionOffset = optHeaderOffset + 0xF0;
// .text section
".text\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8); // VirtualSize
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 16); // SizeOfRawData
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
// .idata section
sectionOffset += 40;
".idata\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8); // VirtualSize
BitConverter.GetBytes((uint)0x2000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 16); // SizeOfRawData
BitConverter.GetBytes((uint)0x400).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
// Import descriptor at file offset 0x400 (RVA 0x2000)
var importOffset = 0x400;
BitConverter.GetBytes((uint)0x2080).CopyTo(buffer, importOffset); // OriginalFirstThunk RVA
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 4); // TimeDateStamp
BitConverter.GetBytes((uint)0).CopyTo(buffer, importOffset + 8); // ForwarderChain
BitConverter.GetBytes((uint)0x2100).CopyTo(buffer, importOffset + 12); // Name RVA
BitConverter.GetBytes((uint)0x2080).CopyTo(buffer, importOffset + 16); // FirstThunk
// Null terminator for import directory
// (already zero at importOffset + 20)
// Import Lookup Table (ILT) / Import Name Table at RVA 0x2080 -> file offset 0x480
// PE32+ uses 8-byte entries!
var iltOffset = 0x480;
// Entry 1: Import by name, hint-name RVA = 0x2120
BitConverter.GetBytes((ulong)0x2120).CopyTo(buffer, iltOffset);
// Entry 2: Import by name, hint-name RVA = 0x2140
BitConverter.GetBytes((ulong)0x2140).CopyTo(buffer, iltOffset + 8);
// Null terminator (8 bytes of zero)
// (already zero)
// DLL name at RVA 0x2100 -> file offset 0x500
"kernel32.dll\0"u8.CopyTo(buffer.AsSpan(0x500));
// Hint-Name table entries
// Entry 1 at RVA 0x2120 -> file offset 0x520
BitConverter.GetBytes((ushort)0).CopyTo(buffer, 0x520); // Hint
"GetProcAddress\0"u8.CopyTo(buffer.AsSpan(0x522));
// Entry 2 at RVA 0x2140 -> file offset 0x540
BitConverter.GetBytes((ushort)0).CopyTo(buffer, 0x540); // Hint
"LoadLibraryA\0"u8.CopyTo(buffer.AsSpan(0x542));
}
[Fact]
public void ParsesPeWithEmbeddedResourceManifest()
{
// Test that manifest is properly extracted from PE resources
var buffer = new byte[16384];
SetupPe32HeaderWithResourceManifest(buffer);
var pe = PeBuilder.Console64()
.WithSxsDependency("Microsoft.VC90.CRT", "9.0.21022.8",
"1fc8b3b9a1e18e3b", "amd64", embedAsResource: true)
.Build();
using var stream = new MemoryStream(buffer);
var result = PeImportParser.TryParse(stream, out var info);
var info = ParsePe(pe);
result.Should().BeTrue();
info.SxsDependencies.Should().HaveCountGreaterOrEqualTo(1);
info.SxsDependencies.Should().Contain(d => d.Name == "Microsoft.VC90.CRT");
}
private static void SetupPe32HeaderWithResourceManifest(byte[] buffer)
{
// DOS header
buffer[0] = (byte)'M';
buffer[1] = (byte)'Z';
BitConverter.GetBytes(0x80).CopyTo(buffer, 0x3C);
// PE signature
var peOffset = 0x80;
buffer[peOffset] = (byte)'P';
buffer[peOffset + 1] = (byte)'E';
// COFF header
BitConverter.GetBytes((ushort)0x8664).CopyTo(buffer, peOffset + 4);
BitConverter.GetBytes((ushort)2).CopyTo(buffer, peOffset + 6); // 2 sections
BitConverter.GetBytes((ushort)0xE0).CopyTo(buffer, peOffset + 20);
// Optional header (PE32)
var optHeaderOffset = peOffset + 24;
BitConverter.GetBytes((ushort)0x10b).CopyTo(buffer, optHeaderOffset);
BitConverter.GetBytes((ushort)PeSubsystem.WindowsConsole).CopyTo(buffer, optHeaderOffset + 68);
BitConverter.GetBytes((uint)16).CopyTo(buffer, optHeaderOffset + 92);
// Data directory - Resource Directory (entry 2)
var dataDirOffset = optHeaderOffset + 96;
BitConverter.GetBytes((uint)0x3000).CopyTo(buffer, dataDirOffset + 16); // Resource Directory RVA
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, dataDirOffset + 20); // Resource Directory Size
// Section headers
var sectionOffset = optHeaderOffset + 0xE0;
// .text section
".text\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8);
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 12);
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 16);
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, sectionOffset + 20);
// .rsrc section
sectionOffset += 40;
".rsrc\0\0\0"u8.CopyTo(buffer.AsSpan(sectionOffset));
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 8);
BitConverter.GetBytes((uint)0x3000).CopyTo(buffer, sectionOffset + 12); // VirtualAddress
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 16);
BitConverter.GetBytes((uint)0x1000).CopyTo(buffer, sectionOffset + 20); // PointerToRawData
// Resource directory at file offset 0x1000 (RVA 0x3000)
var rsrcBase = 0x1000;
// Root directory (Type level)
BitConverter.GetBytes((uint)0).CopyTo(buffer, rsrcBase); // Characteristics
BitConverter.GetBytes((uint)0).CopyTo(buffer, rsrcBase + 4); // TimeDateStamp
BitConverter.GetBytes((ushort)0).CopyTo(buffer, rsrcBase + 8); // MajorVersion
BitConverter.GetBytes((ushort)0).CopyTo(buffer, rsrcBase + 10); // MinorVersion
BitConverter.GetBytes((ushort)0).CopyTo(buffer, rsrcBase + 12); // NumberOfNamedEntries
BitConverter.GetBytes((ushort)1).CopyTo(buffer, rsrcBase + 14); // NumberOfIdEntries
// Entry for RT_MANIFEST (ID=24) at offset 16
BitConverter.GetBytes((uint)24).CopyTo(buffer, rsrcBase + 16); // ID = RT_MANIFEST
BitConverter.GetBytes((uint)(0x80000000 | 0x30)).CopyTo(buffer, rsrcBase + 20); // Offset to subdirectory (high bit set)
// Name/ID subdirectory at offset 0x30
var nameDir = rsrcBase + 0x30;
BitConverter.GetBytes((uint)0).CopyTo(buffer, nameDir);
BitConverter.GetBytes((uint)0).CopyTo(buffer, nameDir + 4);
BitConverter.GetBytes((ushort)0).CopyTo(buffer, nameDir + 8);
BitConverter.GetBytes((ushort)0).CopyTo(buffer, nameDir + 10);
BitConverter.GetBytes((ushort)0).CopyTo(buffer, nameDir + 12);
BitConverter.GetBytes((ushort)1).CopyTo(buffer, nameDir + 14);
// Entry for ID=1 (application manifest)
BitConverter.GetBytes((uint)1).CopyTo(buffer, nameDir + 16);
BitConverter.GetBytes((uint)(0x80000000 | 0x50)).CopyTo(buffer, nameDir + 20); // Offset to language subdirectory
// Language subdirectory at offset 0x50
var langDir = rsrcBase + 0x50;
BitConverter.GetBytes((uint)0).CopyTo(buffer, langDir);
BitConverter.GetBytes((uint)0).CopyTo(buffer, langDir + 4);
BitConverter.GetBytes((ushort)0).CopyTo(buffer, langDir + 8);
BitConverter.GetBytes((ushort)0).CopyTo(buffer, langDir + 10);
BitConverter.GetBytes((ushort)0).CopyTo(buffer, langDir + 12);
BitConverter.GetBytes((ushort)1).CopyTo(buffer, langDir + 14);
// Entry for language (e.g., 0x409 = English US)
BitConverter.GetBytes((uint)0x409).CopyTo(buffer, langDir + 16);
BitConverter.GetBytes((uint)0x70).CopyTo(buffer, langDir + 20); // Offset to data entry (no high bit = data entry)
// Data entry at offset 0x70
var dataEntry = rsrcBase + 0x70;
BitConverter.GetBytes((uint)0x3100).CopyTo(buffer, dataEntry); // Data RVA
BitConverter.GetBytes((uint)0x200).CopyTo(buffer, dataEntry + 4); // Data Size
BitConverter.GetBytes((uint)0).CopyTo(buffer, dataEntry + 8); // CodePage
BitConverter.GetBytes((uint)0).CopyTo(buffer, dataEntry + 12); // Reserved
// Manifest data at RVA 0x3100 -> file offset 0x1100
var manifestXml = """
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.VC90.CRT" version="9.0.21022.8" processorArchitecture="amd64" publicKeyToken="1fc8b3b9a1e18e3b"/>
</dependentAssembly>
</dependency>
</assembly>
""";
Encoding.UTF8.GetBytes(manifestXml).CopyTo(buffer, 0x1100);
}
}

View File

@@ -4,7 +4,7 @@
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<UseConcelierTestInfra>false</UseConcelierTestInfra>
</PropertyGroup>

View File

@@ -0,0 +1,257 @@
using StellaOps.Scanner.Analyzers.Native;
using StellaOps.Scanner.Analyzers.Native.Tests.Fixtures;
namespace StellaOps.Scanner.Analyzers.Native.Tests.TestUtilities;
/// <summary>
/// Base class for native binary analyzer tests.
/// Provides common parsing helpers and assertion methods.
/// </summary>
public abstract class NativeTestBase
{
#region ELF Parsing Helpers
/// <summary>
/// Parses an ELF binary from raw bytes.
/// </summary>
protected static ElfDynamicInfo ParseElf(byte[] data)
{
using var stream = new MemoryStream(data);
if (!ElfDynamicSectionParser.TryParse(stream, out var info))
throw new InvalidOperationException("Failed to parse ELF binary");
return info;
}
/// <summary>
/// Attempts to parse an ELF binary.
/// </summary>
protected static bool TryParseElf(byte[] data, out ElfDynamicInfo info)
{
using var stream = new MemoryStream(data);
return ElfDynamicSectionParser.TryParse(stream, out info);
}
/// <summary>
/// Parses an ELF binary using the builder.
/// </summary>
protected static ElfDynamicInfo ParseElf(ElfBuilder builder)
{
return ParseElf(builder.Build());
}
#endregion
#region PE Parsing Helpers
/// <summary>
/// Parses a PE binary from raw bytes.
/// </summary>
protected static PeImportInfo ParsePe(byte[] data)
{
using var stream = new MemoryStream(data);
if (!PeImportParser.TryParse(stream, out var info))
throw new InvalidOperationException("Failed to parse PE binary");
return info;
}
/// <summary>
/// Attempts to parse a PE binary.
/// </summary>
protected static bool TryParsePe(byte[] data, out PeImportInfo info)
{
using var stream = new MemoryStream(data);
return PeImportParser.TryParse(stream, out info);
}
/// <summary>
/// Parses a PE binary using the builder.
/// </summary>
protected static PeImportInfo ParsePe(PeBuilder builder)
{
return ParsePe(builder.Build());
}
#endregion
#region Mach-O Parsing Helpers
/// <summary>
/// Parses a Mach-O binary from raw bytes.
/// </summary>
protected static MachOImportInfo ParseMachO(byte[] data)
{
using var stream = new MemoryStream(data);
if (!MachOLoadCommandParser.TryParse(stream, out var info))
throw new InvalidOperationException("Failed to parse Mach-O binary");
return info;
}
/// <summary>
/// Attempts to parse a Mach-O binary.
/// </summary>
protected static bool TryParseMachO(byte[] data, out MachOImportInfo info)
{
using var stream = new MemoryStream(data);
return MachOLoadCommandParser.TryParse(stream, out info);
}
/// <summary>
/// Parses a Mach-O binary using the builder.
/// </summary>
protected static MachOImportInfo ParseMachO(MachOBuilder builder)
{
return ParseMachO(builder.Build());
}
#endregion
#region ELF Assertions
/// <summary>
/// Asserts that the dependencies match the expected sonames.
/// </summary>
protected static void AssertDependencies(IReadOnlyList<ElfDeclaredDependency> deps, params string[] expectedSonames)
{
Assert.Equal(expectedSonames.Length, deps.Count);
for (var i = 0; i < expectedSonames.Length; i++)
{
Assert.Equal(expectedSonames[i], deps[i].Soname);
}
}
/// <summary>
/// Asserts that a dependency has the expected version needs.
/// </summary>
protected static void AssertVersionNeeds(
ElfDeclaredDependency dep,
params (string Version, bool IsWeak)[] expected)
{
Assert.Equal(expected.Length, dep.VersionNeeds.Count);
foreach (var (version, isWeak) in expected)
{
var vn = dep.VersionNeeds.FirstOrDefault(v => v.Version == version);
Assert.NotNull(vn);
Assert.Equal(isWeak, vn.IsWeak);
}
}
/// <summary>
/// Asserts that a dependency has the specified weak versions.
/// </summary>
protected static void AssertWeakVersions(ElfDeclaredDependency dep, params string[] weakVersions)
{
foreach (var version in weakVersions)
{
var vn = dep.VersionNeeds.FirstOrDefault(v => v.Version == version);
Assert.NotNull(vn);
Assert.True(vn.IsWeak, $"Expected {version} to be weak");
}
}
/// <summary>
/// Asserts that a dependency has the specified strong (non-weak) versions.
/// </summary>
protected static void AssertStrongVersions(ElfDeclaredDependency dep, params string[] strongVersions)
{
foreach (var version in strongVersions)
{
var vn = dep.VersionNeeds.FirstOrDefault(v => v.Version == version);
Assert.NotNull(vn);
Assert.False(vn.IsWeak, $"Expected {version} to be strong (not weak)");
}
}
#endregion
#region PE Assertions
/// <summary>
/// Asserts that the dependencies match the expected DLL names.
/// </summary>
protected static void AssertDependencies(IReadOnlyList<PeDeclaredDependency> deps, params string[] expectedDllNames)
{
Assert.Equal(expectedDllNames.Length, deps.Count);
for (var i = 0; i < expectedDllNames.Length; i++)
{
Assert.Equal(expectedDllNames[i], deps[i].DllName, ignoreCase: true);
}
}
/// <summary>
/// Asserts that a dependency has the expected imported functions.
/// </summary>
protected static void AssertImportedFunctions(
PeDeclaredDependency dep,
params string[] expectedFunctions)
{
foreach (var func in expectedFunctions)
{
Assert.Contains(func, dep.ImportedFunctions);
}
}
/// <summary>
/// Asserts that the SxS dependencies match the expected names.
/// </summary>
protected static void AssertSxsDependencies(IReadOnlyList<PeSxsDependency> deps, params string[] expectedNames)
{
foreach (var name in expectedNames)
{
Assert.Contains(deps, d => d.Name == name);
}
}
#endregion
#region Mach-O Assertions
/// <summary>
/// Asserts that the dependencies match the expected paths.
/// </summary>
protected static void AssertDependencies(IReadOnlyList<MachODeclaredDependency> deps, params string[] expectedPaths)
{
Assert.Equal(expectedPaths.Length, deps.Count);
for (var i = 0; i < expectedPaths.Length; i++)
{
Assert.Equal(expectedPaths[i], deps[i].Path);
}
}
/// <summary>
/// Asserts that a dependency has the expected reason code.
/// </summary>
protected static void AssertDylibKind(MachODeclaredDependency dep, string expectedReasonCode)
{
Assert.Equal(expectedReasonCode, dep.ReasonCode);
}
/// <summary>
/// Asserts that a dependency has weak linkage.
/// </summary>
protected static void AssertWeakDylib(MachODeclaredDependency dep)
{
Assert.Equal("macho-weaklib", dep.ReasonCode);
}
/// <summary>
/// Asserts that a dependency is a reexport.
/// </summary>
protected static void AssertReexportDylib(MachODeclaredDependency dep)
{
Assert.Equal("macho-reexport", dep.ReasonCode);
}
/// <summary>
/// Asserts that the rpaths match expected values.
/// </summary>
protected static void AssertRpaths(IReadOnlyList<string> rpaths, params string[] expectedRpaths)
{
Assert.Equal(expectedRpaths.Length, rpaths.Count);
for (var i = 0; i < expectedRpaths.Length; i++)
{
Assert.Equal(expectedRpaths[i], rpaths[i]);
}
}
#endregion
}