using System.Text; using FluentAssertions; using StellaOps.Scanner.Analyzers.Native; namespace StellaOps.Scanner.Analyzers.Native.Tests; public class ElfDynamicSectionParserTests { [Fact] public void ParsesMinimalElfWithNoDynamicSection() { // Minimal ELF64 with no program headers (static binary scenario) var buffer = new byte[64]; SetupElf64Header(buffer, littleEndian: true); using var stream = new MemoryStream(buffer); var result = ElfDynamicSectionParser.TryParse(stream, out var info); result.Should().BeTrue(); info.Dependencies.Should().BeEmpty(); info.Rpath.Should().BeEmpty(); info.Runpath.Should().BeEmpty(); } [Fact] public void ParsesElfWithDtNeeded() { // Build a minimal ELF64 with PT_DYNAMIC containing DT_NEEDED entries var buffer = new byte[2048]; SetupElf64Header(buffer, littleEndian: true); // 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; // 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"); info.Dependencies[1].Soname.Should().Be("libm.so.6"); info.Dependencies[2].Soname.Should().Be("libpthread.so.0"); } [Fact] public void ParsesElfWithRpathAndRunpath() { var buffer = new byte[2048]; SetupElf64Header(buffer, littleEndian: true); // 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; // 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"]); } [Fact] public void ParsesElfWithInterpreterAndBuildId() { var buffer = new byte[1024]; SetupElf64Header(buffer, littleEndian: true); // Program headers at offset 0x40 var phoff = 0x40; var phentsize = 56; var phnum = 2; 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"); } [Fact] public void DeduplicatesDtNeededEntries() { var buffer = new byte[2048]; SetupElf64Header(buffer, littleEndian: true); var strtab = 0x400; var str1Offset = 1; var strtabSize = str1Offset + WriteString(buffer, strtab + str1Offset, "libc.so.6") + 1; 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(); info.Dependencies.Should().HaveCount(1); info.Dependencies[0].Soname.Should().Be("libc.so.6"); } [Fact] public void ReturnsFalseForNonElfData() { var buffer = new byte[] { 0x00, 0x01, 0x02, 0x03 }; using var stream = new MemoryStream(buffer); var result = ElfDynamicSectionParser.TryParse(stream, out var info); result.Should().BeFalse(); } [Fact] public void ReturnsFalseForPeFile() { var buffer = new byte[256]; buffer[0] = (byte)'M'; buffer[1] = (byte)'Z'; using var stream = new MemoryStream(buffer); var result = ElfDynamicSectionParser.TryParse(stream, out var info); 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; } }