Skip to content

Commit

Permalink
Fix rewriting of the __LINKEDIT section.
Browse files Browse the repository at this point in the history
There was an incorrect assumption that __LINKEDIT section can only
change when contents of MachLinkEditData change. However, that's not
the only case when that can happen. It also changes when load commands
are added or removed.

Added basic test for code signing an a.out file to ensure that it does
not regress.
  • Loading branch information
filipnavara committed Oct 8, 2022
1 parent 5278c2c commit 27b5e9c
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 75 deletions.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dotnet-test-explorer.testProjectPath": "**/*Tests.@(csproj|vbproj|fsproj)"
}
Binary file added Melanzana.CodeSign.Tests/Data/a.out
Binary file not shown.
44 changes: 44 additions & 0 deletions Melanzana.CodeSign.Tests/ResignTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Xunit;
using System.IO;
using System.Linq;
using Melanzana.CodeSign.Requirements;
using Melanzana.MachO;
using Melanzana.Streams;

namespace Melanzana.CodeSign.Tests
{
public class ResignTest
{
[Fact]
public void Resign()
{
// Read the test executable
var aOutStream = typeof(ResignTest).Assembly.GetManifestResourceStream("Melanzana.CodeSign.Tests.Data.a.out")!;
var objectFile = MachReader.Read(aOutStream).FirstOrDefault();
Assert.NotNull(objectFile);

// Strip the signature
var codeSignature = objectFile!.LoadCommands.OfType<MachCodeSignature>().FirstOrDefault();
Assert.NotNull(codeSignature);
var originalSignatureSize = codeSignature!.FileSize;
objectFile!.LoadCommands.Remove(codeSignature);

// Write the stripped file to disk
var tempFileName = Path.GetTempFileName();
using (var tempFile = new FileStream(tempFileName, FileMode.Create))
{
MachWriter.Write(tempFile, objectFile);
Assert.Equal(aOutStream.Length - originalSignatureSize, tempFile.Length);
}

// Ad-hoc sign the file
var codeSignOptions = new CodeSignOptions { };
var signer = new Signer(codeSignOptions);
signer.Sign(tempFileName);

// TODO: Check signature

File.Delete(tempFileName);
}
}
}
25 changes: 10 additions & 15 deletions Melanzana.CodeSign/Signer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,8 @@ private void SignMachO(string executablePath, string? bundleIdentifier = null, b

/// TODO: Hic sunt leones (all code below)

// Rewrite LC_CODESIGNATURE data
var codeSignatureCommand = objectFile.LoadCommands.OfType<MachCodeSignature>().First();
// Rewrite __LINKEDIT section
var linkEditSegment = objectFile.LoadCommands.OfType<MachSegment>().First(s => s.Name == "__LINKEDIT");

using var oldLinkEditContent = linkEditSegment.GetReadStream();
using var newLinkEditContent = linkEditSegment.GetWriteStream();

long bytesToCopy = oldLinkEditContent.Length - codeSignatureCommand.FileSize;
oldLinkEditContent.Slice(0, bytesToCopy).CopyTo(newLinkEditContent);

// FIXME: Adjust the size to match LinkEdit section?
long size = blobs.Sum(b => b.Data != null ? b.Data.Length + 8 : 0);
Expand All @@ -201,19 +194,21 @@ private void SignMachO(string executablePath, string? bundleIdentifier = null, b
BinaryPrimitives.WriteUInt32BigEndian(embeddedSignatureBuffer.AsSpan(8, 4), (uint)blobs.Count);
int bufferOffset = 12;
int dataOffset = embeddedSignatureBuffer.Length;
foreach (var blob in blobs)
foreach (var (slot, data) in blobs)
{
BinaryPrimitives.WriteUInt32BigEndian(embeddedSignatureBuffer.AsSpan(bufferOffset, 4), (uint)blob.Slot);
BinaryPrimitives.WriteUInt32BigEndian(embeddedSignatureBuffer.AsSpan(bufferOffset, 4), (uint)slot);
BinaryPrimitives.WriteUInt32BigEndian(embeddedSignatureBuffer.AsSpan(bufferOffset + 4, 4), (uint)dataOffset);
dataOffset += blob.Data.Length;
dataOffset += data.Length;
bufferOffset += 8;
}
newLinkEditContent.Write(embeddedSignatureBuffer);
foreach (var blob in blobs)
uint codeSignatureSize = codeSignatureCommand.FileSize;
using var codeSignatureStream = codeSignatureCommand.Data.GetWriteStream();
codeSignatureStream.Write(embeddedSignatureBuffer);
foreach (var (slot, data) in blobs)
{
newLinkEditContent.Write(blob.Data);
codeSignatureStream.Write(data);
}
newLinkEditContent.WritePadding(oldLinkEditContent.Length - newLinkEditContent.Position);
codeSignatureStream.WritePadding(codeSignatureSize - codeSignatureStream.Position);
}

using var outputFile = File.OpenWrite(executablePath);
Expand Down
2 changes: 1 addition & 1 deletion Melanzana.MachO/LoadCommands/MachSection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Melanzana.MachO
{
public class MachSection
{
private MachObjectFile objectFile;
private readonly MachObjectFile objectFile;
private Stream? dataStream;
private ulong size;

Expand Down
42 changes: 40 additions & 2 deletions Melanzana.MachO/LoadCommands/MachSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ namespace Melanzana.MachO
/// </remarks>
public class MachSegment : MachLoadCommand
{
private MachObjectFile objectFile;
private readonly MachObjectFile objectFile;
private Stream? dataStream;
private ulong size;

public MachSegment(MachObjectFile objectFile, string name)
: this(objectFile, name, null)
Expand Down Expand Up @@ -80,6 +81,11 @@ public ulong FileSize
}
}

if (IsLinkEditSegment)
{
return objectFile.LinkEditData.Select(d => d.FileOffset + d.Size).Max() - FileOffset;
}

return (ulong)(dataStream?.Length ?? 0);
}
}
Expand All @@ -97,7 +103,25 @@ public ulong FileSize
/// <summary>
/// Gets or sets the size in bytes occupied in memory by this segment.
/// </summary>
public ulong Size { get; set; }
public ulong Size
{
get
{
if (IsLinkEditSegment)
{
const uint pageAlignment = 0x4000 - 1;
return (FileSize + pageAlignment) & ~pageAlignment;
}

return size;
}
set
{
// NOTE: We silently ignore setting Size for __LINKEDIT to make it easier
// for the reader.
size = value;
}
}

/// <summary>
/// Gets or sets the maximum permitted protection of this segment.
Expand All @@ -116,13 +140,22 @@ public ulong FileSize
/// <summary>
public IList<MachSection> Sections { get; } = new List<MachSection>();

public bool IsLinkEditSegment => Name == "__LINKEDIT";

public Stream GetReadStream()
{
if (Sections.Count != 0)
{
throw new NotSupportedException("Segment can only be read directly if there are no sections");
}

if (IsLinkEditSegment)
{
// NOTE: We can support reading the link edit segment by constructing the stream
// from objectFile.LinkEditData on-demand.
throw new NotSupportedException("Reading __LINKEDIT segment is unsupported");
}

if (FileSize == 0 || dataStream == null)
{
return Stream.Null;
Expand All @@ -146,6 +179,11 @@ public Stream GetWriteStream()
throw new NotSupportedException("Segment can only be written to directly if there are no sections");
}

if (IsLinkEditSegment)
{
throw new NotSupportedException("Writing __LINKEDIT segment is unsupported");
}

dataStream = new UnclosableMemoryStream();
return dataStream;
}
Expand Down
45 changes: 18 additions & 27 deletions Melanzana.MachO/MachObjectFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ public Stream GetOriginalStream()

return stream.Slice(0, stream.Length);
}

public void UpdateLayout()
{
// TODO: Change the layout only if necessary!
Expand Down Expand Up @@ -258,40 +257,37 @@ public void UpdateLayout()
}

ulong virtualAddress = 0;
MachSegment? linkEditSegment = null;
foreach (var segment in Segments)
{
ulong segmentSize = 0;

segment.VirtualAddress = virtualAddress;
segment.FileOffset = (ulong)position;

foreach (var section in segment.Sections)
if (!segment.IsLinkEditSegment)
{
long alignment = 1 << (int)section.Log2Alignment;
var alignedSize = ((long)section.Size + alignment - 1) & ~(alignment - 1);

position = (position + alignment - 1) & ~(alignment - 1);
virtualAddress = (ulong)(((long)virtualAddress + alignment - 1) & ~(alignment - 1));

if (section.Type is not MachSectionType.ZeroFill or MachSectionType.GBZeroFill or MachSectionType.ThreadLocalZeroFill)
foreach (var section in segment.Sections)
{
section.FileOffset = (uint)position;
position += alignedSize;
}
long alignment = 1 << (int)section.Log2Alignment;
var alignedSize = ((long)section.Size + alignment - 1) & ~(alignment - 1);

// TODO: Calculate virtual addresses for non-object files
section.VirtualAddress = virtualAddress;
virtualAddress += (ulong)alignedSize;
position = (position + alignment - 1) & ~(alignment - 1);
virtualAddress = (ulong)(((long)virtualAddress + alignment - 1) & ~(alignment - 1));

segmentSize = Math.Max(segmentSize, virtualAddress - segment.VirtualAddress);
}
if (section.Type is not MachSectionType.ZeroFill or MachSectionType.GBZeroFill or MachSectionType.ThreadLocalZeroFill)
{
section.FileOffset = (uint)position;
position += alignedSize;
}

segment.Size = segmentSize;
// TODO: Calculate virtual addresses for non-object files
section.VirtualAddress = virtualAddress;
virtualAddress += (ulong)alignedSize;

if (segment.Name == "__LINKEDIT")
{
linkEditSegment = segment;
segmentSize = Math.Max(segmentSize, virtualAddress - segment.VirtualAddress);
}

segment.Size = segmentSize;
}
}

Expand All @@ -308,11 +304,6 @@ public void UpdateLayout()
position += (long)data.Size;
}

if (linkEditSegment != null)
{
linkEditSegment.Size = (ulong)position - linkEditSegment.FileOffset;
}

static int AlignedSize(int size, bool is64bit) => is64bit ? (size + 7) & ~7 : (size + 3) & ~3;
}
}
Expand Down
50 changes: 20 additions & 30 deletions Melanzana.MachO/MachWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,6 @@ public static void Write(Stream stream, MachObjectFile objectFile)
// and fill in the gaps as we go.
ulong currentOffset = (ulong)(stream.Position - initialOffset);
var orderedSegments = objectFile.Segments.OrderBy(s => s.FileOffset).ToList();
bool writeLinkEditDatas = true;

foreach (var segment in orderedSegments)
{
Expand All @@ -408,17 +407,11 @@ public static void Write(Stream stream, MachObjectFile objectFile)
currentOffset += paddingSize;
}

if (segment.Name == "__LINKEDIT")
if (segment.IsLinkEditSegment)
{
// __LINKEDIT always has to be the last segment in object file

// If any linker data were updated then bail out of the loop at this point and
// use the new data.
writeLinkEditDatas = objectFile.LinkEditData.Any(data => data.HasContentChanged);
if (writeLinkEditDatas)
{
break;
}
// __LINKEDIT always has to be the last segment in object file, so break
// out of the loop.
break;
}

using var segmentStream = segment.GetReadStream();
Expand Down Expand Up @@ -472,29 +465,26 @@ public static void Write(Stream stream, MachObjectFile objectFile)
}
}

// We are either writing an unlinked object file or a modified __LINKEDIT segment
if (writeLinkEditDatas)
{
var linkEditData = new List<MachLinkEditData>(objectFile.LinkEditData);
// We are either writing an unlinked object file or a __LINKEDIT segment
var linkEditData = new List<MachLinkEditData>(objectFile.LinkEditData);

// Sort by file offset first
linkEditData.Sort((sectionA, sectionB) =>
sectionA.FileOffset < sectionB.FileOffset ? -1 :
(sectionA.FileOffset > sectionB.FileOffset ? 1 : 0));
// Sort by file offset first
linkEditData.Sort((sectionA, sectionB) =>
sectionA.FileOffset < sectionB.FileOffset ? -1 :
(sectionA.FileOffset > sectionB.FileOffset ? 1 : 0));

foreach (var data in linkEditData)
foreach (var data in linkEditData)
{
if (data.FileOffset > currentOffset)
{
if (data.FileOffset > currentOffset)
{
ulong paddingSize = data.FileOffset - currentOffset;
stream.WritePadding((long)paddingSize);
currentOffset += paddingSize;
}

using var segmentStream = data.GetReadStream();
segmentStream.CopyTo(stream);
currentOffset += (ulong)segmentStream.Length;
ulong paddingSize = data.FileOffset - currentOffset;
stream.WritePadding((long)paddingSize);
currentOffset += paddingSize;
}

using var segmentStream = data.GetReadStream();
segmentStream.CopyTo(stream);
currentOffset += (ulong)segmentStream.Length;
}
}

Expand Down

0 comments on commit 27b5e9c

Please sign in to comment.