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.
204 lines
7.6 KiB
204 lines
7.6 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.Collections.Generic; |
|
using System.Collections.Immutable; |
|
using System.IO; |
|
|
|
using ICSharpCode.Decompiler.Metadata; |
|
|
|
using NUnit.Framework; |
|
|
|
namespace ICSharpCode.Decompiler.Tests.Metadata |
|
{ |
|
// Exercises WebCilFile against crafted Wasm/WebCIL input. The section-header fields driving |
|
// the memory-mapped pointer arithmetic come straight from the file, so a crafted module must |
|
// be rejected rather than reading outside the mapped view (CWE-125) or crashing the loader. |
|
[TestFixture] |
|
public class WebCilFileTests |
|
{ |
|
const uint WASM_MAGIC = 0x6d736100u; // "\0asm" |
|
const uint WEBCIL_MAGIC = 0x4c496257u; // "WbIL" |
|
|
|
struct SectionHeaderRaw |
|
{ |
|
public uint VirtualSize; |
|
public uint VirtualAddress; |
|
public uint RawDataSize; |
|
public uint RawDataPtr; |
|
} |
|
|
|
static void WriteULEB128(List<byte> buffer, uint value) |
|
{ |
|
do |
|
{ |
|
byte b = (byte)(value & 0x7F); |
|
value >>= 7; |
|
if (value != 0) |
|
b |= 0x80; |
|
buffer.Add(b); |
|
} |
|
while (value != 0); |
|
} |
|
|
|
static byte[] BuildWebcilPayload(ushort coffSections, uint peCliHeaderRVA, SectionHeaderRaw[] sections) |
|
{ |
|
using var ms = new MemoryStream(); |
|
using var w = new BinaryWriter(ms); |
|
w.Write(WEBCIL_MAGIC); |
|
w.Write((ushort)0); // VersionMajor |
|
w.Write((ushort)0); // VersionMinor |
|
w.Write(coffSections); |
|
w.Write((ushort)0); // reserved0 |
|
w.Write(peCliHeaderRVA); |
|
w.Write(0u); // PECliHeaderSize |
|
w.Write(0u); // PEDebugRVA |
|
w.Write(0u); // PEDebugSize |
|
foreach (var s in sections) |
|
{ |
|
w.Write(s.VirtualSize); |
|
w.Write(s.VirtualAddress); |
|
w.Write(s.RawDataSize); |
|
w.Write(s.RawDataPtr); |
|
} |
|
w.Flush(); |
|
return ms.ToArray(); |
|
} |
|
|
|
// Wraps a WebCIL payload in the minimal Wasm container WebCilFile.FromFile recognizes: |
|
// a Data section whose two data segments are the (skipped) first segment and the WebCIL blob. |
|
static byte[] BuildWasmContainer(byte[] webcilPayload) |
|
{ |
|
var content = new List<byte>(); |
|
WriteULEB128(content, 2); // number of data segments |
|
content.Add(1); // segment 1 kind |
|
WriteULEB128(content, 0); // segment 1 length |
|
content.Add(1); // segment 2 kind |
|
WriteULEB128(content, (uint)webcilPayload.Length); // segment 2 length |
|
content.AddRange(webcilPayload); |
|
|
|
var file = new List<byte>(); |
|
file.AddRange(BitConverter.GetBytes(WASM_MAGIC)); |
|
file.AddRange(BitConverter.GetBytes(1u)); // Wasm version |
|
file.Add(11); // WasmSectionId.Data |
|
WriteULEB128(file, (uint)content.Count); |
|
file.AddRange(content); |
|
return file.ToArray(); |
|
} |
|
|
|
static WebCilFile FromBytes(byte[] bytes) |
|
{ |
|
string path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".wasm"); |
|
File.WriteAllBytes(path, bytes); |
|
try |
|
{ |
|
return WebCilFile.FromFile(path); |
|
} |
|
finally |
|
{ |
|
File.Delete(path); |
|
} |
|
} |
|
|
|
[Test] |
|
public void TruncatedSectionHeaders_ReturnsNullWithoutReadingPastView() |
|
{ |
|
// CoffSections claims far more section headers than the file (or mapped view) holds. |
|
byte[] payload = BuildWebcilPayload(coffSections: 0xFFFF, peCliHeaderRVA: 0, sections: Array.Empty<SectionHeaderRaw>()); |
|
Assert.That(FromBytes(BuildWasmContainer(payload)), Is.Null); |
|
} |
|
|
|
[Test] |
|
public void CliHeaderRvaInNoSection_ReturnsNull() |
|
{ |
|
var sections = new[] { |
|
new SectionHeaderRaw { VirtualAddress = 0, VirtualSize = 0x10, RawDataSize = 0x10, RawDataPtr = 0 } |
|
}; |
|
// PECliHeaderRVA points outside the single section, so RVA translation cannot resolve it. |
|
byte[] payload = BuildWebcilPayload(coffSections: 1, peCliHeaderRVA: 0x1000, sections: sections); |
|
Assert.That(FromBytes(BuildWasmContainer(payload)), Is.Null); |
|
} |
|
|
|
static ImmutableArray<SectionHeader> Section(uint virtualAddress, uint virtualSize, uint rawDataPtr, uint rawDataSize) |
|
{ |
|
return ImmutableArray.Create(new SectionHeader { |
|
VirtualAddress = virtualAddress, |
|
VirtualSize = virtualSize, |
|
RawDataPtr = rawDataPtr, |
|
RawDataSize = rawDataSize |
|
}); |
|
} |
|
|
|
[Test] |
|
public void OobSectionHeader_IsRejected() |
|
{ |
|
// The exploit shape from the finding: a section spanning the whole RVA space whose raw-data |
|
// pointer/size point far outside a small mapped view. |
|
var headers = Section(virtualAddress: 0, virtualSize: 0xFFFFFFFF, rawDataPtr: 0xFFFFFFFF, rawDataSize: 0x7FFFFFFF); |
|
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x1000, rva: 0x10, out _, out _); |
|
Assert.That(ok, Is.False); |
|
} |
|
|
|
[Test] |
|
public void RawDataSizeBeyondInt32_IsRejected() |
|
{ |
|
var headers = Section(virtualAddress: 0, virtualSize: 0x100, rawDataPtr: 0, rawDataSize: 0x80000000); |
|
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x100000000L, rva: 0, out _, out _); |
|
Assert.That(ok, Is.False); |
|
} |
|
|
|
[Test] |
|
public void RawDataRangePastView_IsRejected() |
|
{ |
|
var headers = Section(virtualAddress: 0, virtualSize: 0x100, rawDataPtr: 0xF0, rawDataSize: 0x20); |
|
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x100, rva: 0, out _, out _); |
|
Assert.That(ok, Is.False); |
|
} |
|
|
|
[Test] |
|
public void RvaInNoSection_IsRejected() |
|
{ |
|
var headers = Section(virtualAddress: 0x10, virtualSize: 0x10, rawDataPtr: 0, rawDataSize: 0x10); |
|
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x1000, rva: 0x100, out _, out _); |
|
Assert.That(ok, Is.False); |
|
} |
|
|
|
[Test] |
|
public void RvaPastRawData_IsRejected() |
|
{ |
|
// The RVA sits inside the section's virtual size but past its (smaller) raw data, so there |
|
// are no bytes to read from it. |
|
var headers = Section(virtualAddress: 0, virtualSize: 0x100, rawDataPtr: 0, rawDataSize: 0x10); |
|
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0, viewLength: 0x1000, rva: 0x20, out _, out _); |
|
Assert.That(ok, Is.False); |
|
} |
|
|
|
[Test] |
|
public void InBoundsSection_IsResolved() |
|
{ |
|
var headers = Section(virtualAddress: 0x20, virtualSize: 0x40, rawDataPtr: 0x80, rawDataSize: 0x40); |
|
bool ok = WebCilFile.TryGetSectionDataRange(headers, webcilOffset: 0x10, viewLength: 0x1000, rva: 0x30, out long offset, out int length); |
|
Assert.That(ok, Is.True); |
|
// offset = RawDataPtr + webcilOffset + (rva - VirtualAddress) = 0x80 + 0x10 + 0x10 |
|
Assert.That(offset, Is.EqualTo(0xA0)); |
|
// length = remaining raw data from the RVA = RawDataSize - (rva - VirtualAddress) = 0x40 - 0x10 |
|
Assert.That(length, Is.EqualTo(0x30)); |
|
} |
|
} |
|
}
|
|
|