Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.Analyzers.Native.Tests/ElfDynamicSectionParserTests.cs
StellaOps Bot ea970ead2a
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
sdk-generator-smoke / sdk-smoke (push) Has been cancelled
SDK Publish & Sign / sdk-publish (push) Has been cancelled
api-governance / spectral-lint (push) Has been cancelled
oas-ci / oas-validate (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
up
2025-11-27 07:46:56 +02:00

325 lines
13 KiB
C#

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;
}
}