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.
256 lines
10 KiB
256 lines
10 KiB
// Copyright (c) 2026 Siegfried Pammer |
|
// |
|
// 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.Runtime.InteropServices; |
|
|
|
using ICSharpCode.Decompiler.Util; |
|
|
|
using NUnit.Framework; |
|
|
|
namespace ICSharpCode.Decompiler.Tests.Util |
|
{ |
|
// Exercises the bounds, recursion-depth and cycle guards in Win32Resources against crafted |
|
// .rsrc section bytes. Each test hands a hand-built directory tree to the parser through a |
|
// resolver that maps an RVA to an offset inside the same pinned buffer (a single-section PE). |
|
[TestFixture] |
|
public unsafe class Win32ResourcesTests |
|
{ |
|
const int DirectorySize = 16; // IMAGE_RESOURCE_DIRECTORY |
|
const int EntrySize = 8; // IMAGE_RESOURCE_DIRECTORY_ENTRY |
|
const int DataEntrySize = 16; // IMAGE_RESOURCE_DATA_ENTRY |
|
const uint SubdirectoryFlag = 0x80000000; |
|
|
|
// Pins the buffer, parses it as a resource section, and runs the assertions while the data |
|
// pointers captured during parsing still point into the pinned buffer. |
|
static void Parse(byte[] buffer, Action<Win32ResourceDirectory> assert) |
|
{ |
|
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); |
|
try |
|
{ |
|
byte* pRoot = (byte*)handle.AddrOfPinnedObject(); |
|
var resolver = new BufferResolver(pRoot, buffer.Length); |
|
var root = Win32ResourceDirectory.ReadDirectoryTree(pRoot, buffer.Length, resolver.Resolve); |
|
assert(root); |
|
} |
|
finally |
|
{ |
|
handle.Free(); |
|
} |
|
} |
|
|
|
// Resolves a data RVA to a pointer inside the buffer, returning the bytes that remain from |
|
// that offset to the end - the same "length to end of section" contract PEReader.GetSectionData |
|
// provides, so a crafted Size larger than the data can be bounded. |
|
sealed class BufferResolver |
|
{ |
|
readonly byte* pRoot; |
|
readonly int length; |
|
|
|
public BufferResolver(byte* pRoot, int length) |
|
{ |
|
this.pRoot = pRoot; |
|
this.length = length; |
|
} |
|
|
|
public byte* Resolve(int rva, out int dataLength) |
|
{ |
|
if (rva < 0 || rva > length) |
|
{ |
|
dataLength = 0; |
|
return null; |
|
} |
|
dataLength = length - rva; |
|
return pRoot + rva; |
|
} |
|
} |
|
|
|
static void WriteDirectory(byte[] buffer, int offset, ushort namedEntries, ushort idEntries) |
|
{ |
|
BitConverter.GetBytes(namedEntries).CopyTo(buffer, offset + 12); |
|
BitConverter.GetBytes(idEntries).CopyTo(buffer, offset + 14); |
|
} |
|
|
|
static void WriteEntry(byte[] buffer, int offset, uint name, uint offsetToData) |
|
{ |
|
BitConverter.GetBytes(name).CopyTo(buffer, offset); |
|
BitConverter.GetBytes(offsetToData).CopyTo(buffer, offset + 4); |
|
} |
|
|
|
static void WriteDataEntry(byte[] buffer, int offset, uint rva, uint size) |
|
{ |
|
BitConverter.GetBytes(rva).CopyTo(buffer, offset); |
|
BitConverter.GetBytes(size).CopyTo(buffer, offset + 4); |
|
} |
|
|
|
[Test] |
|
public void SelfReferentialSubdirectory_DoesNotRecurseInfinitely() |
|
{ |
|
// One directory with a single subdirectory entry that points back at itself (offset 0). |
|
// The unfixed parser follows it forever, yielding an uncatchable StackOverflowException. |
|
byte[] buffer = new byte[DirectorySize + EntrySize]; |
|
WriteDirectory(buffer, 0, namedEntries: 0, idEntries: 1); |
|
WriteEntry(buffer, DirectorySize, name: 1, offsetToData: SubdirectoryFlag /* offset 0 */); |
|
|
|
Parse(buffer, root => { |
|
Assert.That(root.Directories.Count, Is.EqualTo(1)); |
|
var child = root.Directories[0]; |
|
Assert.That(child.Directories.Count, Is.EqualTo(0), "the cycle back to the root must be cut"); |
|
Assert.That(child.Datas.Count, Is.EqualTo(0)); |
|
}); |
|
} |
|
|
|
[Test] |
|
public void DeeplyNestedDirectories_AreBoundedByDepthLimit() |
|
{ |
|
// A long chain of distinct nested directories. Even without a cycle this would recurse |
|
// as deep as the chain; the depth cap must stop it well before that. |
|
const int chainLength = 40; |
|
byte[] buffer = new byte[chainLength * (DirectorySize + EntrySize)]; |
|
for (int k = 0; k < chainLength; k++) |
|
{ |
|
int dirOffset = k * (DirectorySize + EntrySize); |
|
bool hasChild = k < chainLength - 1; |
|
WriteDirectory(buffer, dirOffset, namedEntries: 0, idEntries: (ushort)(hasChild ? 1 : 0)); |
|
if (hasChild) |
|
{ |
|
uint childOffset = (uint)((k + 1) * (DirectorySize + EntrySize)); |
|
WriteEntry(buffer, dirOffset + DirectorySize, name: (uint)(k + 1), offsetToData: SubdirectoryFlag | childOffset); |
|
} |
|
} |
|
|
|
Parse(buffer, root => { |
|
int depth = 0; |
|
var current = root; |
|
while (current != null && current.Directories.Count > 0) |
|
{ |
|
current = current.Directories[0]; |
|
depth++; |
|
} |
|
// The parser caps nesting at a small constant (well above any real resource tree), |
|
// so the measured depth must be far below the crafted chain length. |
|
Assert.That(depth, Is.LessThanOrEqualTo(17)); |
|
}); |
|
} |
|
|
|
[Test] |
|
public void EntryCountBeyondSection_IsClamped() |
|
{ |
|
// A directory header that claims far more entries than the section can hold. The unfixed |
|
// parser walks the declared count straight off the end of the section. |
|
byte[] buffer = new byte[DirectorySize]; |
|
WriteDirectory(buffer, 0, namedEntries: 0, idEntries: 0xFFFF); |
|
|
|
Parse(buffer, root => { |
|
Assert.That(root.Directories.Count, Is.EqualTo(0)); |
|
Assert.That(root.Datas.Count, Is.EqualTo(0)); |
|
}); |
|
} |
|
|
|
[Test] |
|
public void DataSizeBeyondSection_IsClampedToAvailable() |
|
{ |
|
// A data leaf whose declared Size dwarfs the bytes actually present. The unfixed Data |
|
// getter copies the full Size, reading gigabytes past the section base. |
|
const int dataBytes = 8; |
|
int dataEntryOffset = DirectorySize + EntrySize; |
|
int dataOffset = dataEntryOffset + DataEntrySize; |
|
byte[] buffer = new byte[dataOffset + dataBytes]; |
|
WriteDirectory(buffer, 0, namedEntries: 0, idEntries: 1); |
|
WriteEntry(buffer, DirectorySize, name: 1, offsetToData: (uint)dataEntryOffset /* data leaf */); |
|
WriteDataEntry(buffer, dataEntryOffset, rva: (uint)dataOffset, size: 0xFFFFFFF0); |
|
|
|
Parse(buffer, root => { |
|
Assert.That(root.Datas.Count, Is.EqualTo(1)); |
|
var data = root.Datas[0]; |
|
Assert.That(data.Size, Is.EqualTo(0xFFFFFFF0)); |
|
Assert.That(data.Data.Length, Is.EqualTo(dataBytes), "the copy must be bounded to the bytes that exist"); |
|
}); |
|
} |
|
|
|
[Test] |
|
public void NegativeDataRva_YieldsEmptyDataWithoutThrowing() |
|
{ |
|
// The data entry's RVA is a file uint; with the high bit set it casts to a negative int. |
|
// The resolver must reject it (PEReader.GetSectionData throws on a negative RVA) so the |
|
// leaf yields empty data rather than aborting the parse. |
|
int dataEntryOffset = DirectorySize + EntrySize; |
|
byte[] buffer = new byte[dataEntryOffset + DataEntrySize]; |
|
WriteDirectory(buffer, 0, namedEntries: 0, idEntries: 1); |
|
WriteEntry(buffer, DirectorySize, name: 1, offsetToData: (uint)dataEntryOffset /* data leaf */); |
|
WriteDataEntry(buffer, dataEntryOffset, rva: 0xFFFFFFFF /* negative as int */, size: 0x100); |
|
|
|
Parse(buffer, root => { |
|
Assert.That(root.Datas.Count, Is.EqualTo(1)); |
|
Assert.That(root.Datas[0].Data, Is.Empty); |
|
}); |
|
} |
|
|
|
[Test] |
|
public void OutOfRangeStringName_DoesNotReadOutOfBounds() |
|
{ |
|
// A named entry whose name-string offset lies past the section end. The unfixed parser |
|
// dereferences it directly, reading the length prefix and characters out of bounds. |
|
int dataEntryOffset = DirectorySize + EntrySize; |
|
byte[] buffer = new byte[dataEntryOffset + DataEntrySize]; |
|
WriteDirectory(buffer, 0, namedEntries: 1, idEntries: 0); |
|
WriteEntry(buffer, DirectorySize, name: SubdirectoryFlag | 0x100 /* string offset past the buffer */, offsetToData: (uint)dataEntryOffset); |
|
WriteDataEntry(buffer, dataEntryOffset, rva: 0, size: 0); |
|
|
|
Parse(buffer, root => { |
|
Assert.That(root.Datas.Count, Is.EqualTo(1)); |
|
var name = root.Datas[0].Name; |
|
Assert.That(name.HasName, Is.True); |
|
Assert.That(name.Name, Is.Empty, "an out-of-range string name must resolve to empty, not an OOB read"); |
|
}); |
|
} |
|
|
|
[Test] |
|
public void ValidResourceTree_ParsesAndReadsData() |
|
{ |
|
// A well-formed Type -> Name -> (language data leaf) tree, mirroring how a manifest is |
|
// laid out, to prove the bounds checks do not break normal parsing. |
|
const int RT_MANIFEST = 24; |
|
int typeDir = 0; |
|
int rootEntry = typeDir + DirectorySize; // 16 |
|
int nameDir = rootEntry + EntrySize; // 24 |
|
int typeEntry = nameDir + DirectorySize; // 40 |
|
int leafDir = typeEntry + EntrySize; // 48 |
|
int nameEntry = leafDir + DirectorySize; // 64 |
|
int dataEntry = nameEntry + EntrySize; // 72 |
|
int dataOffset = dataEntry + DataEntrySize; // 88 |
|
byte[] payload = { 0xDE, 0xAD, 0xBE, 0xEF }; |
|
byte[] buffer = new byte[dataOffset + payload.Length]; |
|
|
|
WriteDirectory(buffer, typeDir, namedEntries: 0, idEntries: 1); |
|
WriteEntry(buffer, rootEntry, name: RT_MANIFEST, offsetToData: SubdirectoryFlag | (uint)nameDir); |
|
WriteDirectory(buffer, nameDir, namedEntries: 0, idEntries: 1); |
|
WriteEntry(buffer, typeEntry, name: 1, offsetToData: SubdirectoryFlag | (uint)leafDir); |
|
WriteDirectory(buffer, leafDir, namedEntries: 0, idEntries: 1); |
|
WriteEntry(buffer, nameEntry, name: 1033, offsetToData: (uint)dataEntry /* data leaf */); |
|
WriteDataEntry(buffer, dataEntry, rva: (uint)dataOffset, size: (uint)payload.Length); |
|
payload.CopyTo(buffer, dataOffset); |
|
|
|
Parse(buffer, root => { |
|
var manifest = root.Find(new Win32ResourceName(RT_MANIFEST))?.FirstDirectory()?.FirstData()?.Data; |
|
Assert.That(manifest, Is.Not.Null); |
|
Assert.That(manifest, Is.EqualTo(payload)); |
|
}); |
|
} |
|
} |
|
}
|
|
|