Skip to content

Commit 4ea0f6e

Browse files
Bound the remaining reads sized by untrusted counts
Reviewing the load command bounds turned up three more of the same kind elsewhere. A symbol, indirect symbol or relocation count was multiplied by an entry size in 32-bit arithmetic. A count large enough to wrap that product gave a small, plausible length that passed the coverage check, and the loop then read past what had been read. Those products are now computed in 64 bits, so the wrap cannot happen and an impossible length is reported instead. The symbol table count reached the caller as an ArgumentOutOfRangeException rather than a diagnostic. A universal binary header's slice count was used to size an allocation before anything checked the file was big enough to hold that many. It is now checked against the bytes actually there. Signing changed a good deal before it could fail: it removed the previous signature's content, moved the command, resized __LINKEDIT and cleared the stale flag, then wrote the image to take the digests, and that write can fail. What it changes is now put back if it does, so a failed signing leaves the image exactly as it was rather than half signed. - src/LibObjectFile/MachO/MachOFile.Symbols.cs: lengths computed in 64 bits - src/LibObjectFile/MachO/MachOFatFile.cs: the slice count checked before it sizes anything - src/LibObjectFile/MachO/MachOFile.Sign.cs: signing restores what it changed if it fails
1 parent 69804c7 commit 4ea0f6e

7 files changed

Lines changed: 182 additions & 58 deletions

File tree

‎src/LibObjectFile.Tests/MachO/MachOSigningTests.cs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,23 @@ public void RejectsWhatCannotBeSigned()
209209
// An object file has no __LINKEDIT, so there is nowhere for a signature to live.
210210
Assert.ThrowsExactly<InvalidOperationException>(() => LoadMachO("helloworld_x86_64.o").AdHocSign("helloworld"));
211211
Assert.ThrowsExactly<ArgumentException>(() => LoadMachO("unixthread_i386").AdHocSign(string.Empty));
212+
213+
// Signing changes a good deal before it can fail, and a half-signed image is worse than
214+
// an unsigned one, so a failure has to leave the image exactly as it was.
215+
var file = LoadMachO("unixthread_i386");
216+
var before = WriteToArray(file);
217+
var contents = file.Content.Count;
218+
var commands = file.LoadCommands.Count;
219+
220+
var section = file.FindSegment("__TEXT")!.Sections[0];
221+
section.Address += 4;
222+
Assert.ThrowsExactly<ObjectFileException>(() => file.AdHocSign("swkotor"));
223+
section.Address -= 4;
224+
225+
Assert.AreEqual(contents, file.Content.Count, "a failed signing left content behind");
226+
Assert.AreEqual(commands, file.LoadCommands.Count, "a failed signing left its load command behind");
227+
Assert.IsFalse(file.IsCodeSignatureStale, "a failed signing left the image marked stale");
228+
ByteArrayAssert.AreEqual(before, WriteToArray(file), "a failed signing changed the image");
212229
}
213230

214231
private static Span<byte> FindCodeDirectory(byte[] image, uint signatureOffset)

‎src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,19 @@ static byte[] Patch(Action<byte[]> corrupt)
496496
var tooManySections = Patch(b => Write(b, 28 + 56 + 48, 40));
497497
Assert.IsFalse(MachOFile.TryRead(new MemoryStream(tooManySections), out _, out var d3));
498498
Assert.IsTrue(d3.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidLoadCommandSize), string.Join("; ", d3.Messages));
499+
500+
// A count large enough that multiplying it by an entry size wraps a 32-bit product would
501+
// otherwise pass a length check as a small number and then be read past.
502+
var overflowing = LoadMachO("helloworld_x86_64");
503+
overflowing.LoadCommands.OfType<MachOSymbolTableCommand>().Single().SymbolCount = 0x10000001;
504+
Assert.ThrowsExactly<ObjectFileException>(() => overflowing.ReadSymbolTable());
505+
506+
// A universal binary header claiming more slices than the file holds must not be trusted
507+
// to size anything before that is checked.
508+
var fat = File.ReadAllBytes(GetFile("helloworld_fat"));
509+
BitConverter.GetBytes(0x10000000u).Reverse().ToArray().CopyTo(fat, 4);
510+
Assert.IsFalse(MachOFatFile.TryRead(new MemoryStream(fat), out _, out var d4));
511+
Assert.IsTrue(d4.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidFatHeader), string.Join("; ", d4.Messages));
499512
}
500513

501514
[TestMethod]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System; using System.IO; using System.Linq;
2+
using LibObjectFile.MachO;
3+
namespace LibObjectFile.Tests.MachO;
4+
[TestClass] public class TempClass {
5+
static string F(string n)=>Path.Combine(AppContext.BaseDirectory,"MachO",n);
6+
static byte[] W(MachOFile f){var m=new MemoryStream();f.Write(m);return m.ToArray();}
7+
[TestMethod] public void SignFailureIsAtomic() {
8+
var file=MachOFile.Read(new MemoryStream(File.ReadAllBytes(F("unixthread_i386"))));
9+
var before=W(file);
10+
var contentBefore=file.Content.Count;
11+
var cmdsBefore=file.LoadCommands.Count;
12+
// Break the image so signing's internal write fails after it has mutated.
13+
file.FindSegment("__TEXT")!.Sections[0].Address += 4;
14+
try{ file.AdHocSign("probe"); Console.WriteLine("signing did not fail (unexpected)"); }
15+
catch(Exception e){ Console.WriteLine($"signing failed as expected: {e.GetType().Name}"); }
16+
// Put the image back the way it was and check nothing else changed.
17+
file.FindSegment("__TEXT")!.Sections[0].Address -= 4;
18+
Console.WriteLine($"content {contentBefore}->{file.Content.Count}, commands {cmdsBefore}->{file.LoadCommands.Count}, stale={file.IsCodeSignatureStale}");
19+
var after=W(file);
20+
Console.WriteLine($"image unchanged after failed signing: {before.SequenceEqual(after)}");
21+
}}

‎src/LibObjectFile/MachO/MachOFatFile.cs‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,14 @@ public static bool TryRead(Stream stream, [NotNullWhen(true)] out MachOFatFile?
133133
var count = BinaryPrimitives.ReadUInt32BigEndian(header.Slice(4));
134134

135135
var entrySize = GetSliceEntrySize(file.Is64BitOffsets);
136-
var entries = new byte[count * (uint)entrySize];
136+
if ((ulong)count * (ulong)entrySize > (ulong)(stream.Length - stream.Position))
137+
{
138+
bag.Error(DiagnosticId.MACHO_ERR_InvalidFatHeader, $"The universal binary header claims {count} architectures, which do not fit in the file");
139+
file = null;
140+
return false;
141+
}
142+
143+
var entries = new byte[count * entrySize];
137144
try
138145
{
139146
stream.ReadExactly(entries);

‎src/LibObjectFile/MachO/MachOFile.Relocations.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public IReadOnlyList<MachORelocation> ReadRelocations(MachOSection section)
3030

3131
if (section.NumberOfRelocations == 0) return [];
3232

33-
var bytes = ReadFileBytes(section.RelocationOffset, section.NumberOfRelocations * MachORelocation.EntrySize, $"relocations of {section.SegmentName},{section.Name}");
33+
var bytes = ReadFileBytes(section.RelocationOffset, (ulong)section.NumberOfRelocations * MachORelocation.EntrySize, $"relocations of {section.SegmentName},{section.Name}");
3434
var relocations = new MachORelocation[section.NumberOfRelocations];
3535
for (var i = 0; i < relocations.Length; i++)
3636
{

‎src/LibObjectFile/MachO/MachOFile.Sign.cs‎

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.IO;
7+
using System.Collections.Generic;
78
using System.Linq;
89
using LibObjectFile.MachO.CodeSign;
910
using LibObjectFile.Utils;
@@ -41,72 +42,121 @@ public void AdHocSign(string identifier)
4142
var text = FindSegment("__TEXT")
4243
?? throw new InvalidOperationException("The image has no __TEXT segment, so the signature cannot describe its executable range.");
4344

45+
// Everything below can fail, and a half-signed image is worse than an unsigned one, so
46+
// what changes is remembered first and put back if it does. The state is captured before
47+
// the command is added, since adding one is itself a change to undo.
48+
var previousFileSize = linkEdit.FileSize;
49+
var previousVmSize = linkEdit.VmSize;
50+
var previousStale = IsCodeSignatureStale;
51+
var added = new List<MachOContent>();
52+
MachOContent? removed = null;
53+
var removedIndex = -1;
54+
MachOLinkEditDataCommand? addedCommand = null;
55+
4456
var command = CodeSignature;
45-
if (command is null)
57+
var previousOffset = command?.DataOffset ?? 0;
58+
var previousSize = command?.DataSize ?? 0;
59+
60+
try
4661
{
47-
command = new MachOLinkEditDataCommand
62+
if (command is null)
63+
{
64+
command = new MachOLinkEditDataCommand
65+
{
66+
Type = MachOLoadCommandType.CodeSignature,
67+
Size = MachOLinkEditDataCommand.CommandSize,
68+
};
69+
AppendCommand(command);
70+
addedCommand = command;
71+
}
72+
// A signature covers everything before it, so it has to be the last thing in the file.
73+
// Whatever occupied that place before, including a previous signature, is replaced.
74+
if (command.DataOffset != 0)
4875
{
49-
Type = MachOLoadCommandType.CodeSignature,
50-
Size = MachOLinkEditDataCommand.CommandSize,
76+
for (var i = Content.Count - 1; i >= 0; i--)
77+
{
78+
if (Content[i].Position == command.DataOffset)
79+
{
80+
removed = Content[i];
81+
removedIndex = i;
82+
Content.RemoveAt(i);
83+
break;
84+
}
85+
}
86+
}
87+
88+
var contentEnd = ComputeFileSize();
89+
var signatureOffset = AlignHelper.AlignUp((uint)contentEnd, (uint)MachOCodeSignatureConstants.SignatureAlignment);
90+
91+
var builder = new MachOAdHocSignatureBuilder(identifier)
92+
{
93+
CodeLimit = signatureOffset,
94+
ExecSegmentBase = text.FileOffset,
95+
ExecSegmentLimit = text.FileSize,
96+
ExecSegmentFlags = FileType == MachOFileType.Execute ? MachOCodeSignatureConstants.ExecSegMainBinary : 0,
5197
};
52-
AppendCommand(command);
53-
}
98+
var signatureSize = builder.ComputeSize();
5499

55-
// A signature covers everything before it, so it has to be the last thing in the file.
56-
// Whatever occupied that place before, including a previous signature, is replaced.
57-
if (command.DataOffset != 0)
58-
{
59-
for (var i = Content.Count - 1; i >= 0; i--)
100+
command.DataOffset = signatureOffset;
101+
command.DataSize = signatureSize;
102+
103+
linkEdit.FileSize = signatureOffset + signatureSize - linkEdit.FileOffset;
104+
linkEdit.VmSize = Math.Max(linkEdit.VmSize, linkEdit.FileSize);
105+
106+
// Aligning the signature can leave a gap, and every byte of the file has to belong to
107+
// some content, so the padding is added rather than left as a hole in the list.
108+
if (signatureOffset > contentEnd)
60109
{
61-
if (Content[i].Position == command.DataOffset)
110+
var gap = new MachOStreamContent(new MemoryStream(new byte[signatureOffset - contentEnd]))
62111
{
63-
Content.RemoveAt(i);
64-
break;
65-
}
112+
Position = contentEnd,
113+
};
114+
Content.Add(gap);
115+
added.Add(gap);
66116
}
67-
}
68117

69-
var contentEnd = ComputeFileSize();
70-
var signatureOffset = AlignHelper.AlignUp((uint)contentEnd, (uint)MachOCodeSignatureConstants.SignatureAlignment);
118+
var placeholder = new MachOStreamContent(new MemoryStream(new byte[signatureSize])) { Position = signatureOffset };
119+
Content.Add(placeholder);
120+
added.Add(placeholder);
71121

72-
var builder = new MachOAdHocSignatureBuilder(identifier)
73-
{
74-
CodeLimit = signatureOffset,
75-
ExecSegmentBase = text.FileOffset,
76-
ExecSegmentLimit = text.FileSize,
77-
ExecSegmentFlags = FileType == MachOFileType.Execute ? MachOCodeSignatureConstants.ExecSegMainBinary : 0,
78-
};
79-
var signatureSize = builder.ComputeSize();
80-
81-
command.DataOffset = signatureOffset;
82-
command.DataSize = signatureSize;
83-
84-
linkEdit.FileSize = signatureOffset + signatureSize - linkEdit.FileOffset;
85-
linkEdit.VmSize = Math.Max(linkEdit.VmSize, linkEdit.FileSize);
86-
87-
// Aligning the signature can leave a gap, and every byte of the file has to belong to
88-
// some content, so the padding is added rather than left as a hole in the list.
89-
if (signatureOffset > contentEnd)
122+
// Signing is what makes the image match its signature again, so the edit is settled here
123+
// rather than after the digests are taken. Writing checks this, and the digests are taken
124+
// by writing the image out.
125+
IsCodeSignatureStale = false;
126+
127+
// The digests cover the image as it will finally be written, so the header, the command
128+
// table and the signature's own offsets all have to be settled before they are taken.
129+
var image = new MemoryStream();
130+
Write(image);
131+
placeholder.Content = new MemoryStream(builder.Build(image.GetBuffer().AsSpan(0, (int)signatureOffset)));
132+
}
133+
catch
90134
{
91-
Content.Add(new MachOStreamContent(new MemoryStream(new byte[signatureOffset - contentEnd]))
135+
foreach (var content in added)
92136
{
93-
Position = contentEnd,
94-
});
95-
}
137+
Content.Remove(content);
138+
}
96139

97-
var placeholder = new MachOStreamContent(new MemoryStream(new byte[signatureSize])) { Position = signatureOffset };
98-
Content.Add(placeholder);
140+
if (removed is not null)
141+
{
142+
Content.Insert(removedIndex, removed);
143+
}
99144

100-
// Signing is what makes the image match its signature again, so the edit is settled here
101-
// rather than after the digests are taken. Writing checks this, and the digests are taken
102-
// by writing the image out.
103-
IsCodeSignatureStale = false;
145+
if (addedCommand is not null)
146+
{
147+
LoadCommands.Remove(addedCommand);
148+
}
149+
else if (command is not null)
150+
{
151+
command.DataOffset = previousOffset;
152+
command.DataSize = previousSize;
153+
}
104154

105-
// The digests cover the image as it will finally be written, so the header, the command
106-
// table and the signature's own offsets all have to be settled before they are taken.
107-
var image = new MemoryStream();
108-
Write(image);
109-
placeholder.Content = new MemoryStream(builder.Build(image.GetBuffer().AsSpan(0, (int)signatureOffset)));
155+
linkEdit.FileSize = previousFileSize;
156+
linkEdit.VmSize = previousVmSize;
157+
IsCodeSignatureStale = previousStale;
158+
throw;
159+
}
110160
}
111161

112162
}

‎src/LibObjectFile/MachO/MachOFile.Symbols.cs‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public IReadOnlyList<MachOSymbol> ReadSymbolTable()
3333
if (command is null || command.SymbolCount == 0) return [];
3434

3535
var entrySize = MachOSymbolTableCommand.GetSymbolSize(Is64Bit);
36-
var entries = ReadFileBytes(command.SymbolOffset, command.SymbolCount * entrySize, "symbol table");
36+
var entries = ReadFileBytes(command.SymbolOffset, (ulong)command.SymbolCount * entrySize, "symbol table");
3737
var strings = ReadFileBytes(command.StringOffset, command.StringSize, "string table");
3838

3939
var symbols = new List<MachOSymbol>((int)command.SymbolCount);
@@ -63,7 +63,7 @@ public IReadOnlyList<uint> ReadIndirectSymbolTable()
6363
var command = LoadCommands.OfType<MachODynamicSymbolTableCommand>().FirstOrDefault();
6464
if (command is null || command.IndirectSymbolCount == 0) return [];
6565

66-
var bytes = ReadFileBytes(command.IndirectSymbolOffset, command.IndirectSymbolCount * MachODynamicSymbolTableCommand.IndirectSymbolEntrySize, "indirect symbol table");
66+
var bytes = ReadFileBytes(command.IndirectSymbolOffset, (ulong)command.IndirectSymbolCount * MachODynamicSymbolTableCommand.IndirectSymbolEntrySize, "indirect symbol table");
6767
var indices = new uint[command.IndirectSymbolCount];
6868
for (var i = 0; i < indices.Length; i++)
6969
{
@@ -118,10 +118,18 @@ private static string ReadString(byte[] strings, uint offset)
118118
/// <summary>
119119
/// Reads a run of bytes at a file offset out of whichever content covers it.
120120
/// </summary>
121-
private byte[] ReadFileBytes(uint offset, uint length, string what)
121+
private byte[] ReadFileBytes(uint offset, ulong length, string what)
122122
{
123123
if (length == 0) return [];
124124

125+
// The length is a count from the file multiplied by an entry size, so it is computed in
126+
// 64 bits: a count large enough to wrap a 32-bit product would otherwise pass this check
127+
// as a small length and then be read past.
128+
if (length > int.MaxValue || offset + length > uint.MaxValue)
129+
{
130+
Throw(offset, length, what);
131+
}
132+
125133
foreach (var content in Content)
126134
{
127135
if (content is not MachOStreamContent stream || offset < content.Position) continue;
@@ -135,8 +143,16 @@ private byte[] ReadFileBytes(uint offset, uint length, string what)
135143
return buffer;
136144
}
137145

146+
Throw(offset, length, what);
147+
return [];
148+
}
149+
150+
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
151+
private static void Throw(uint offset, ulong length, string what)
152+
{
153+
var message = $"The {what} at 0x{offset:X} for 0x{length:X} bytes is not covered by any content of this image.";
138154
var diagnostics = new DiagnosticBag();
139-
diagnostics.Error(DiagnosticId.MACHO_ERR_DataOutsideImage, $"The {what} at 0x{offset:X} for 0x{length:X} bytes is not covered by any content of this image.");
140-
throw new ObjectFileException($"The {what} at 0x{offset:X} for 0x{length:X} bytes is not covered by any content of this image.", diagnostics);
155+
diagnostics.Error(DiagnosticId.MACHO_ERR_DataOutsideImage, message);
156+
throw new ObjectFileException(message, diagnostics);
141157
}
142158
}

0 commit comments

Comments
 (0)