Skip to content

Commit 6261b5d

Browse files
authored
Merge pull request #49 from ProjectSynchro/dt-needed-injection
Add DT_NEEDED dependency injection
2 parents 0b89af9 + 3c45eb8 commit 6261b5d

3 files changed

Lines changed: 298 additions & 0 deletions

File tree

‎readme.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ elf.Write(outStream);
5252
- Dynamic Linking Table (`SHT_DYNAMIC`)
5353
- Other sections fallback to `ElfCustomSection`
5454
- Program headers with or without sections
55+
- `ElfFile.AddNeededLibrary` injects a `DT_NEEDED` dependency into an existing image without moving any section (address-preserving, `patchelf`-style)
5556
- Print with `readelf` similar output
5657
- Support for **DWARF debugging format**:
5758
- Partial support of Version 4 (currently still the default for GCC)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// This file is licensed under the BSD-Clause 2 license.
3+
// See the license.txt file in the project root for more information.
4+
5+
using System;
6+
using System.Buffers.Binary;
7+
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Linq;
10+
using LibObjectFile.Elf;
11+
12+
namespace LibObjectFile.Tests.Elf;
13+
14+
[TestClass]
15+
public class ElfDynamicEditingTests : ElfTestBase
16+
{
17+
[TestMethod]
18+
public void AddNeededLibraryInjectsDependency()
19+
{
20+
// libstdc++.so is a real shared object with existing DT_NEEDED entries and a PT_NOTE to repurpose.
21+
var elf = LoadElf("libstdc++.so");
22+
23+
var originalNeeded = elf.Sections.OfType<ElfDynamicLinkingTable>().Single().GetNeededLibraries().ToList();
24+
int notesBefore = elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Note);
25+
int loadsBefore = elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Load);
26+
ulong dynamicVaddrBefore = elf.Segments.Single(s => s.Type == ElfSegmentTypeCore.Dynamic).VirtualAddress;
27+
28+
elf.AddNeededLibrary("libinject.so");
29+
30+
// A PT_NOTE was repurposed into a PT_LOAD, and PT_DYNAMIC now points into the appended region.
31+
Assert.AreEqual(notesBefore - 1, elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Note));
32+
Assert.AreEqual(loadsBefore + 1, elf.Segments.Count(s => s.Type == ElfSegmentTypeCore.Load));
33+
Assert.IsTrue(elf.Segments.Single(s => s.Type == ElfSegmentTypeCore.Dynamic).VirtualAddress > dynamicVaddrBefore,
34+
"PT_DYNAMIC should have moved into the appended high-address region");
35+
36+
// Write, re-read, then resolve DT_NEEDED exactly as the loader would: follow PT_DYNAMIC to the
37+
// dynamic array, then DT_STRTAB to turn each DT_NEEDED offset into a name.
38+
var stream = new MemoryStream();
39+
elf.Write(stream);
40+
var fileBytes = stream.ToArray();
41+
var roundTrip = ElfFile.Read(new MemoryStream(fileBytes));
42+
43+
var needed = ResolveNeededViaProgramHeaders(roundTrip, fileBytes);
44+
45+
CollectionAssert.IsSubsetOf(originalNeeded, needed, "existing dependencies must be preserved");
46+
CollectionAssert.Contains(needed, "libinject.so", "the injected dependency must be resolvable via DT_STRTAB");
47+
Assert.AreEqual(originalNeeded.Count + 1, needed.Count, "exactly one DT_NEEDED should be added");
48+
49+
// The section view must agree with the segment/loader view: exactly one .dynamic section, and reading
50+
// DT_NEEDED through the section (its linked .dynstr) resolves the injected library too. This is what a
51+
// section-based re-reader (e.g. a patch applicator checking for idempotency) sees.
52+
var dynamicSections = roundTrip.Sections.OfType<ElfDynamicLinkingTable>().ToList();
53+
Assert.AreEqual(1, dynamicSections.Count, "there must be exactly one .dynamic section after injection");
54+
var neededViaSection = dynamicSections[0].GetNeededLibraries().ToList();
55+
CollectionAssert.AreEqual(needed, neededViaSection, "section view must match the program-header view");
56+
}
57+
58+
[TestMethod]
59+
public void AddNeededLibraryThrowsForNonDynamicFile()
60+
{
61+
// Relocatable object files have no .dynamic section, so injection is not applicable.
62+
var elf = LoadElf("small_debug.o");
63+
Assert.ThrowsExactly<InvalidOperationException>(() => elf.AddNeededLibrary("libinject.so"));
64+
}
65+
66+
// Emulates the dynamic loader: PT_DYNAMIC -> dynamic array -> DT_STRTAB -> DT_NEEDED names.
67+
private static List<string> ResolveNeededViaProgramHeaders(ElfFile elf, byte[] fileBytes)
68+
{
69+
bool little = elf.Encoding == ElfEncoding.Lsb;
70+
bool is32 = elf.FileClass == ElfFileClass.Is32;
71+
int entrySize = is32 ? 8 : 16;
72+
73+
var dynamicSegment = elf.Segments.Single(s => s.Type == ElfSegmentTypeCore.Dynamic);
74+
int count = (int)(dynamicSegment.Size / (ulong)entrySize);
75+
76+
ulong strTabVaddr = 0;
77+
var neededOffsets = new List<ulong>();
78+
for (int i = 0; i < count; i++)
79+
{
80+
int offset = (int)dynamicSegment.Position + i * entrySize;
81+
long tag = is32 ? ReadU32(fileBytes, offset, little) : (long)ReadU64(fileBytes, offset, little);
82+
ulong value = is32 ? ReadU32(fileBytes, offset + 4, little) : ReadU64(fileBytes, offset + 8, little);
83+
84+
if (tag == (long)ElfDynamicTag.StrTab) strTabVaddr = value;
85+
else if (tag == (long)ElfDynamicTag.Needed) neededOffsets.Add(value);
86+
}
87+
88+
ulong strTabFileOffset = VirtualToFileOffset(elf, strTabVaddr);
89+
return neededOffsets.Select(o => ReadCString(fileBytes, (int)(strTabFileOffset + o))).ToList();
90+
}
91+
92+
private static ulong VirtualToFileOffset(ElfFile elf, ulong virtualAddress)
93+
{
94+
foreach (var segment in elf.Segments)
95+
{
96+
if (segment.Type != ElfSegmentTypeCore.Load) continue;
97+
if (virtualAddress >= segment.VirtualAddress && virtualAddress < segment.VirtualAddress + segment.Size)
98+
{
99+
return segment.Position + (virtualAddress - segment.VirtualAddress);
100+
}
101+
}
102+
103+
throw new InvalidOperationException($"Virtual address 0x{virtualAddress:x} is not mapped by any PT_LOAD segment");
104+
}
105+
106+
private static uint ReadU32(byte[] bytes, int offset, bool little)
107+
=> little
108+
? BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(offset, 4))
109+
: BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(offset, 4));
110+
111+
private static ulong ReadU64(byte[] bytes, int offset, bool little)
112+
=> little
113+
? BinaryPrimitives.ReadUInt64LittleEndian(bytes.AsSpan(offset, 8))
114+
: BinaryPrimitives.ReadUInt64BigEndian(bytes.AsSpan(offset, 8));
115+
116+
private static string ReadCString(byte[] bytes, int offset)
117+
{
118+
int end = offset;
119+
while (end < bytes.Length && bytes[end] != 0) end++;
120+
return System.Text.Encoding.UTF8.GetString(bytes, offset, end - offset);
121+
}
122+
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright (c) Alexandre Mutel. All rights reserved.
2+
// This file is licensed under the BSD-Clause 2 license.
3+
// See the license.txt file in the project root for more information.
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.IO;
8+
using System.Linq;
9+
using LibObjectFile.Diagnostics;
10+
11+
namespace LibObjectFile.Elf;
12+
13+
/// <summary>
14+
/// Address-preserving edits of a dynamically-linked <see cref="ElfFile"/>.
15+
/// </summary>
16+
/// <remarks>
17+
/// These operations never change the byte offset of an existing section, so existing symbols, relocations and
18+
/// code stay valid. The sections that must grow (<c>.dynamic</c> and <c>.dynstr</c>) are relocated to the end of
19+
/// the file and mapped by a fresh <c>PT_LOAD</c> built from a spare <c>PT_NOTE</c> program header (so the
20+
/// program-header table does not grow). This is the same technique used by <c>patchelf --add-needed</c>.
21+
/// </remarks>
22+
public static class ElfDynamicEditing
23+
{
24+
private const ulong PageSize = 0x1000;
25+
26+
/// <summary>
27+
/// Injects a <c>DT_NEEDED</c> dependency on <paramref name="libraryName"/> into an existing dynamically-linked
28+
/// image, so the dynamic loader loads that library at startup. The <c>.dynamic</c> and <c>.dynstr</c> sections
29+
/// are relocated (as sections, kept consistent with <c>PT_DYNAMIC</c> and <c>DT_STRTAB</c>), while every other
30+
/// section keeps its byte offset. The vacated space is backfilled so no existing reference shifts.
31+
/// </summary>
32+
/// <exception cref="InvalidOperationException">
33+
/// The file is not dynamically linked, or has no <c>PT_NOTE</c> segment to repurpose as the new <c>PT_LOAD</c>.
34+
/// </exception>
35+
public static void AddNeededLibrary(this ElfFile elf, string libraryName)
36+
{
37+
ArgumentNullException.ThrowIfNull(elf);
38+
ArgumentException.ThrowIfNullOrEmpty(libraryName);
39+
40+
var dynamicSection = elf.Sections.OfType<ElfDynamicLinkingTable>().FirstOrDefault()
41+
?? throw new InvalidOperationException("The ELF file has no dynamic section (.dynamic); it is not dynamically linked.");
42+
var stringSection = dynamicSection.StringTable
43+
?? throw new InvalidOperationException("The dynamic section is not linked to a string table (.dynstr).");
44+
var dynamicSegment = elf.Segments.FirstOrDefault(s => s.Type == ElfSegmentTypeCore.Dynamic)
45+
?? throw new InvalidOperationException("The ELF file has no PT_DYNAMIC segment.");
46+
var noteSegment = elf.Segments.FirstOrDefault(s => s.Type == ElfSegmentTypeCore.Note)
47+
?? throw new InvalidOperationException("The ELF file has no PT_NOTE segment to repurpose as a new PT_LOAD.");
48+
49+
// Original layout of the sections we relocate, captured before any mutation.
50+
ulong dynamicOldVaddr = dynamicSection.VirtualAddress;
51+
ulong stringOldVaddr = stringSection.VirtualAddress;
52+
ulong dynamicOldSize = dynamicSection.Size;
53+
ulong stringOldSize = stringSection.Size;
54+
uint dynamicOldAlign = dynamicSection.FileAlignment;
55+
uint stringOldAlign = stringSection.FileAlignment;
56+
57+
// Grow .dynstr with the library name and add the DT_NEEDED entry to .dynamic.
58+
dynamicSection.AddNeededLibrary(libraryName);
59+
60+
// Relocate .dynamic and .dynstr to end-of-file, backfilling their old positions with equal-size padding
61+
// so every other section keeps its byte offset. .dynamic goes first so PT_DYNAMIC covers the start of the
62+
// new region; page-align it so the new PT_LOAD is vaddr/offset congruent.
63+
RelocateToEnd(elf, dynamicSection, dynamicOldSize, dynamicOldAlign, (uint)PageSize);
64+
RelocateToEnd(elf, stringSection, stringOldSize, stringOldAlign, 1);
65+
66+
var diagnostics = new DiagnosticBag();
67+
elf.UpdateLayout(diagnostics);
68+
if (diagnostics.HasErrors)
69+
{
70+
throw new ObjectFileException("Failed to update the ELF layout while injecting a dependency.", diagnostics);
71+
}
72+
73+
ulong baseVaddr = AlignUp(MaxLoadedVirtualAddressEnd(elf), PageSize);
74+
ulong firstPosition = dynamicSection.Position;
75+
dynamicSection.VirtualAddress = baseVaddr + (dynamicSection.Position - firstPosition);
76+
stringSection.VirtualAddress = baseVaddr + (stringSection.Position - firstPosition);
77+
78+
// Point DT_STRTAB/DT_STRSZ at the relocated .dynstr. These are value-only updates (no entry count change),
79+
// so the layout computed above stays valid.
80+
UpdateTagValue(dynamicSection.Entries, ElfDynamicTag.StrTab, stringSection.VirtualAddress);
81+
UpdateTagValue(dynamicSection.Entries, ElfDynamicTag.StrSz, stringSection.Size);
82+
83+
// The _DYNAMIC symbol (and any other symbol inside the moved sections) must follow the new address.
84+
RelocateSymbols(elf, dynamicSection, dynamicOldVaddr);
85+
RelocateSymbols(elf, stringSection, stringOldVaddr);
86+
87+
// Repurpose the PT_NOTE program header into a writable PT_LOAD mapping both relocated sections. It must be
88+
// writable because the loader writes DT_DEBUG back into .dynamic.
89+
noteSegment.Type = ElfSegmentTypeCore.Load;
90+
noteSegment.Flags = ElfSegmentFlagsCore.Readable | ElfSegmentFlagsCore.Writable;
91+
noteSegment.VirtualAddress = baseVaddr;
92+
noteSegment.PhysicalAddress = baseVaddr;
93+
noteSegment.VirtualAddressAlignment = PageSize;
94+
noteSegment.Range = new ElfContentRange(dynamicSection, 0, stringSection, 0);
95+
noteSegment.SizeInMemory = stringSection.Position + stringSection.Size - dynamicSection.Position;
96+
97+
// Point PT_DYNAMIC at the relocated .dynamic.
98+
dynamicSegment.VirtualAddress = dynamicSection.VirtualAddress;
99+
dynamicSegment.PhysicalAddress = dynamicSection.VirtualAddress;
100+
dynamicSegment.SizeInMemory = dynamicSection.Size;
101+
dynamicSegment.Range = new ElfContentRange(dynamicSection);
102+
}
103+
104+
/// <summary>
105+
/// Moves <paramref name="section"/> to the end of the content list and backfills the space it vacated with a
106+
/// zero-filled shadow content of the same size and alignment, so every following section keeps its byte offset.
107+
/// The section header keeps its index (driven by <see cref="ElfSection.OrderInSectionHeaderTable"/>), only its
108+
/// file offset moves.
109+
/// </summary>
110+
private static void RelocateToEnd(ElfFile elf, ElfSection section, ulong originalSize, uint originalAlignment, uint appendAlignment)
111+
{
112+
// RemoveAt (not Remove) is required: only RemoveAt fires the content hooks that keep the section list
113+
// in sync, otherwise the section would be registered twice and produce a duplicate section header.
114+
int index = elf.Content.IndexOf(section);
115+
elf.Content.RemoveAt(index);
116+
var padding = new ElfStreamContentData(new MemoryStream(new byte[originalSize]))
117+
{
118+
FileAlignment = originalAlignment,
119+
};
120+
elf.Content.Insert(index, padding);
121+
section.FileAlignment = appendAlignment;
122+
elf.Content.Add(section);
123+
}
124+
125+
private static void RelocateSymbols(ElfFile elf, ElfSection section, ulong oldVirtualAddress)
126+
{
127+
ulong newVirtualAddress = section.VirtualAddress;
128+
if (newVirtualAddress == oldVirtualAddress) return;
129+
130+
foreach (var symbolTable in elf.Sections.OfType<ElfSymbolTable>())
131+
{
132+
var entries = symbolTable.Entries;
133+
for (int i = 0; i < entries.Count; i++)
134+
{
135+
var entry = entries[i];
136+
if (ReferenceEquals(entry.SectionLink.Section, section))
137+
{
138+
entry.Value = newVirtualAddress + (entry.Value - oldVirtualAddress);
139+
entries[i] = entry;
140+
}
141+
}
142+
}
143+
}
144+
145+
private static void UpdateTagValue(List<ElfDynamic> entries, ElfDynamicTag tag, ulong value)
146+
{
147+
int index = entries.FindIndex(e => e.TagType == tag);
148+
if (index < 0)
149+
{
150+
throw new InvalidOperationException($"The dynamic section is missing the required {tag} entry.");
151+
}
152+
153+
var entry = entries[index];
154+
entry.Value = value;
155+
entries[index] = entry;
156+
}
157+
158+
private static ulong MaxLoadedVirtualAddressEnd(ElfFile elf)
159+
{
160+
ulong max = 0;
161+
foreach (var segment in elf.Segments)
162+
{
163+
if (segment.SizeInMemory == 0) continue;
164+
max = Math.Max(max, segment.VirtualAddress + segment.SizeInMemory);
165+
}
166+
foreach (var section in elf.Sections)
167+
{
168+
if ((section.Flags & ElfSectionFlags.Alloc) == 0) continue;
169+
max = Math.Max(max, section.VirtualAddress + section.Size);
170+
}
171+
return max;
172+
}
173+
174+
private static ulong AlignUp(ulong value, ulong align) => (value + align - 1) & ~(align - 1);
175+
}

0 commit comments

Comments
 (0)