mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
9.6 KiB
253 lines
9.6 KiB
// Copyright (c) 2026 Christoph Wille |
|
// |
|
// Permission is hereby granted, free of charge, to any person obtaining a copy of this |
|
// software and associated documentation files (the "Software"), to deal in the Software |
|
// without restriction, including without limitation the rights to use, copy, modify, merge, |
|
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons |
|
// to whom the Software is furnished to do so, subject to the following conditions: |
|
// |
|
// The above copyright notice and this permission notice shall be included in all copies or |
|
// substantial portions of the Software. |
|
// |
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
|
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR |
|
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE |
|
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|
// DEALINGS IN THE SOFTWARE. |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Text; |
|
|
|
using ICSharpCode.Decompiler.Util; |
|
|
|
using NUnit.Framework; |
|
|
|
namespace ICSharpCode.Decompiler.Tests.Util |
|
{ |
|
/// <summary> |
|
/// Every count and length in a .resources file is attacker-controlled once a crafted |
|
/// assembly is opened. These tests pin down that each one is validated against the data |
|
/// that actually exists before it drives an allocation, so a malformed file fails with a |
|
/// BadImageFormatException instead of an out-of-memory condition. |
|
/// </summary> |
|
[TestFixture] |
|
public class ResourcesFileTests |
|
{ |
|
const string DefaultReaderType = "System.Resources.ResourceReader, mscorlib"; |
|
const string DeserializingReaderType = "System.Resources.Extensions.DeserializingResourceReader, System.Resources.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"; |
|
|
|
// ResourceTypeCode values as written into the data section. |
|
const int TypeCodeString = 1; |
|
const int TypeCodeByteArray = 0x20; |
|
const int TypeCodeStream = 33; |
|
const int TypeCodeStartOfUserTypes = 0x40; |
|
|
|
[Test] |
|
public void WellFormedFile_ReadsAllEntryKinds() |
|
{ |
|
var blob = Build(DeserializingReaderType, new[] { "MyType, MyAssembly" }, new[] { |
|
("text", StringPayload("hello")), |
|
("blob", LengthPrefixedPayload(TypeCodeByteArray, new byte[] { 1, 2, 3 })), |
|
("stream", LengthPrefixedPayload(TypeCodeStream, new byte[] { 4, 5 })), |
|
("obj", SerializedObjectPayload(typeIndex: 0, kind: 2, new byte[] { 9, 8, 7 })), |
|
}); |
|
|
|
var entries = new ResourcesFile(new MemoryStream(blob)).ToDictionary(e => e.Key, e => e.Value); |
|
|
|
Assert.That(entries["text"], Is.EqualTo("hello")); |
|
Assert.That(entries["blob"], Is.EqualTo(new byte[] { 1, 2, 3 })); |
|
Assert.That(((MemoryStream)entries["stream"]!).ToArray(), Is.EqualTo(new byte[] { 4, 5 })); |
|
var obj = (ResourceSerializedObject)entries["obj"]!; |
|
Assert.That(obj.TypeName, Is.EqualTo("MyType, MyAssembly")); |
|
using var objStream = obj.GetStream(); |
|
Assert.That(ReadAll(objStream), Is.EqualTo(new byte[] { 9, 8, 7 })); |
|
} |
|
|
|
[Test] |
|
public void TypeCountBeyondStream_ThrowsInsteadOfAllocating() |
|
{ |
|
// int.MaxValue string references is a multi-gigabyte allocation request; the |
|
// header only has room for a handful of type names. |
|
var blob = BuildHeaderOnly(numResources: 0, numTypes: int.MaxValue); |
|
|
|
Assert.Throws<BadImageFormatException>(() => new ResourcesFile(new MemoryStream(blob))); |
|
} |
|
|
|
[Test] |
|
public void ResourceCountBeyondStream_ThrowsBeforeReadingPositions() |
|
{ |
|
// Each resource needs at least a 4-byte name hash and a 4-byte name position, so a |
|
// count larger than the remaining bytes can be rejected up front rather than after |
|
// the position array has been allocated and the read has run off the end. |
|
var blob = BuildHeaderOnly(numResources: 100_000, numTypes: 0); |
|
|
|
Assert.Throws<BadImageFormatException>(() => new ResourcesFile(new MemoryStream(blob))); |
|
} |
|
|
|
[Test] |
|
public void ResourceNameLengthBeyondStream_ThrowsInsteadOfAllocating() |
|
{ |
|
var blob = Build(DefaultReaderType, Array.Empty<string>(), new[] { |
|
("name", StringPayload("value")), |
|
}, forcedNameByteLength: int.MaxValue); |
|
|
|
Assert.Throws<BadImageFormatException>(() => new ResourcesFile(new MemoryStream(blob)).ToList()); |
|
} |
|
|
|
[TestCase(TypeCodeByteArray)] |
|
[TestCase(TypeCodeStream)] |
|
public void BinaryResourceLengthBeyondStream_ThrowsInsteadOfAllocating(int typeCode) |
|
{ |
|
var payload = new MemoryStream(); |
|
using (var w = new BinaryWriter(payload, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
w.Write7BitEncodedInt(typeCode); |
|
w.Write(int.MaxValue); // declared length, far beyond the file |
|
w.Write(new byte[] { 1, 2, 3 }); |
|
} |
|
var blob = Build(DefaultReaderType, Array.Empty<string>(), new[] { ("bin", payload.ToArray()) }); |
|
|
|
Assert.Throws<BadImageFormatException>(() => new ResourcesFile(new MemoryStream(blob)).ToList()); |
|
} |
|
|
|
[Test] |
|
public void SerializedObjectLengthBeyondStream_ThrowsInsteadOfAllocating() |
|
{ |
|
var blob = Build(DeserializingReaderType, new[] { "MyType, MyAssembly" }, new[] { |
|
("obj", SerializedObjectPayload(typeIndex: 0, kind: 1, new byte[] { 1 }, forcedLength: int.MaxValue)), |
|
}); |
|
var obj = (ResourceSerializedObject)new ResourcesFile(new MemoryStream(blob)).Single().Value!; |
|
|
|
Assert.Throws<BadImageFormatException>(() => obj.GetStream()); |
|
} |
|
|
|
[Test] |
|
public void SerializedObjectWithUnknownFormatKind_Throws() |
|
{ |
|
// Only the four SerializationFormat kinds defined by System.Resources.Extensions |
|
// are valid; anything else means the data section is not what the header claims. |
|
var blob = Build(DeserializingReaderType, new[] { "MyType, MyAssembly" }, new[] { |
|
("obj", SerializedObjectPayload(typeIndex: 0, kind: 99, new byte[] { 1 })), |
|
}); |
|
var obj = (ResourceSerializedObject)new ResourcesFile(new MemoryStream(blob)).Single().Value!; |
|
|
|
Assert.Throws<BadImageFormatException>(() => obj.GetStream()); |
|
} |
|
|
|
static byte[] StringPayload(string value) |
|
{ |
|
var ms = new MemoryStream(); |
|
using (var w = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
w.Write7BitEncodedInt(TypeCodeString); |
|
w.Write(value); |
|
} |
|
return ms.ToArray(); |
|
} |
|
|
|
static byte[] LengthPrefixedPayload(int typeCode, byte[] data) |
|
{ |
|
var ms = new MemoryStream(); |
|
using (var w = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
w.Write7BitEncodedInt(typeCode); |
|
w.Write(data.Length); |
|
w.Write(data); |
|
} |
|
return ms.ToArray(); |
|
} |
|
|
|
static byte[] SerializedObjectPayload(int typeIndex, int kind, byte[] data, int? forcedLength = null) |
|
{ |
|
var ms = new MemoryStream(); |
|
using (var w = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
w.Write7BitEncodedInt(TypeCodeStartOfUserTypes + typeIndex); |
|
w.Write7BitEncodedInt(kind); |
|
w.Write7BitEncodedInt(forcedLength ?? data.Length); |
|
w.Write(data); |
|
} |
|
return ms.ToArray(); |
|
} |
|
|
|
static byte[] ReadAll(Stream stream) |
|
{ |
|
var ms = new MemoryStream(); |
|
stream.CopyTo(ms); |
|
return ms.ToArray(); |
|
} |
|
|
|
/// <summary> |
|
/// Writes the ResourceManager and RuntimeResourceSet headers up to and including the |
|
/// resource and type counts, then stops. The stream ends where the type names would be. |
|
/// </summary> |
|
static byte[] BuildHeaderOnly(int numResources, int numTypes) |
|
{ |
|
var ms = new MemoryStream(); |
|
using (var w = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
WriteHeader(w, DefaultReaderType, numResources, numTypes); |
|
} |
|
return ms.ToArray(); |
|
} |
|
|
|
static void WriteHeader(BinaryWriter w, string readerType, int numResources, int numTypes) |
|
{ |
|
w.Write(ResourcesFile.MagicNumber); |
|
w.Write(1); // ResourceManager header version |
|
w.Write(0); // bytes to skip (unused for header version 1) |
|
w.Write(readerType); |
|
w.Write("System.Resources.RuntimeResourceSet"); |
|
w.Write(2); // RuntimeResourceSet version |
|
w.Write(numResources); |
|
w.Write(numTypes); |
|
} |
|
|
|
/// <summary> |
|
/// Builds a complete version-2 .resources file. The data of each entry is the raw |
|
/// data section record (type code followed by payload). |
|
/// </summary> |
|
static byte[] Build(string readerType, string[] typeNames, IList<(string Name, byte[] Data)> entries, int? forcedNameByteLength = null) |
|
{ |
|
var nameSection = new MemoryStream(); |
|
var dataSection = new MemoryStream(); |
|
var namePositions = new List<int>(); |
|
using (var nameWriter = new BinaryWriter(nameSection, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
foreach (var (name, data) in entries) |
|
{ |
|
namePositions.Add((int)nameSection.Position); |
|
byte[] nameBytes = Encoding.Unicode.GetBytes(name); |
|
nameWriter.Write7BitEncodedInt(forcedNameByteLength ?? nameBytes.Length); |
|
nameWriter.Write(nameBytes); |
|
nameWriter.Write((int)dataSection.Position); |
|
dataSection.Write(data); |
|
} |
|
} |
|
|
|
var ms = new MemoryStream(); |
|
using (var w = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true)) |
|
{ |
|
WriteHeader(w, readerType, entries.Count, typeNames.Length); |
|
foreach (var typeName in typeNames) |
|
w.Write(typeName); |
|
while (ms.Position % 8 != 0) |
|
w.Write((byte)0); |
|
foreach (var (name, _) in entries) |
|
w.Write(name.GetHashCode()); // name hashes are not consulted by ResourcesFile |
|
foreach (int position in namePositions) |
|
w.Write(position); |
|
// name section starts right after this data section offset field |
|
int nameSectionStart = (int)ms.Position + sizeof(int); |
|
w.Write(nameSectionStart + (int)nameSection.Length); |
|
w.Write(nameSection.ToArray()); |
|
w.Write(dataSection.ToArray()); |
|
} |
|
return ms.ToArray(); |
|
} |
|
} |
|
}
|
|
|