Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Address several issues with reading specially crafted .NET metadata #557

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_new_line_before_open_brace = all
dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:none
dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none
Expand Down
11 changes: 10 additions & 1 deletion src/AsmResolver.DotNet/Builder/DotNetDirectoryFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ public IMethodBodySerializer MethodBodySerializer
ReorderMetadataStreams(serializedModule, result.Directory.Metadata!);
}

if (result.Directory.Metadata is { IsEncMetadata: true } metadata
&& metadata.TryGetStream(TablesStream.MinimalStreamName, out _))
{
result.Directory.Metadata.GetStream<TablesStream>().ForceLargeColumns = true;
}

return result;
}

Expand Down Expand Up @@ -179,7 +185,10 @@ private IMetadataBuffer CreateMetadataBuffer(ModuleDefinition module)
{
var metadataBuffer = new MetadataBuffer(module.RuntimeVersion)
{
OptimizeStringIndices = (MetadataBuilderFlags & MetadataBuilderFlags.NoStringsStreamOptimization) == 0
OptimizeStringIndices = (MetadataBuilderFlags & MetadataBuilderFlags.NoStringsStreamOptimization) == 0,
TablesStream = {
ForceEncMetadata = (MetadataBuilderFlags & MetadataBuilderFlags.ForceEncMetadata) != 0
}
};

// Check if there exists a .NET directory to base off the metadata buffer on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public bool IsEmpty

return true;
}
}
}

/// <summary>
/// Gets a value indicating whether the buffer contains edit-and-continue metadata tables.
Expand All @@ -98,6 +98,9 @@ public bool IsEncMetadata
{
get
{
if (ForceEncMetadata)
return true;

for (int i = 0; i < EncTables.Length; i++)
{
if (_tableBuffers[(int) EncTables[i]].Count > 0)
Expand All @@ -108,6 +111,15 @@ public bool IsEncMetadata
}
}

/// <summary>
/// Gets a value indicating whether the buffer should always use edit-and-continue metadata.
/// </summary>
public bool ForceEncMetadata
{
get;
set;
}

/// <summary>
/// Gets a table buffer by its table index.
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion src/AsmResolver.DotNet/Builder/MetadataBuilderFlags.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ public enum MetadataBuilderFlags
/// or depend on individual resource items to be present. Setting this flag will disable this optimization.
/// </para>
/// </summary>
NoResourceDataDeduplication = 0x40000
NoResourceDataDeduplication = 0x40000,

/// <summary>
/// Setting this flag will force the builder to emit edit-and-continue metadata, even if it is not required.
/// </summary>
ForceEncMetadata = 0x80000
}
}
47 changes: 25 additions & 22 deletions src/AsmResolver.PE/DotNet/Metadata/DefaultMetadataStreamReader.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using AsmResolver.IO;
using AsmResolver.PE.DotNet.Metadata.Blob;
using AsmResolver.PE.DotNet.Metadata.Guid;
Expand Down Expand Up @@ -25,33 +26,35 @@ public static DefaultMetadataStreamReader Instance

/// <inheritdoc />
public IMetadataStream ReadStream(MetadataReaderContext context,
MetadataStreamReaderFlags flags,
MetadataStreamHeader header,
ref BinaryStreamReader reader)
{
switch (header.Name)
{
case TablesStream.CompressedStreamName:
case TablesStream.EncStreamName:
return new SerializedTableStream(context, header.Name, reader);

case StringsStream.DefaultName:
return new SerializedStringsStream(header.Name, reader);

case UserStringsStream.DefaultName:
return new SerializedUserStringsStream(header.Name, reader);

case BlobStream.DefaultName:
return new SerializedBlobStream(header.Name, reader);
// The CLR performs a case-insensitive comparison for the names of the streams when ENC metadata is present.
var comparisonKind = (flags & MetadataStreamReaderFlags.IsEnc) != 0
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;

case GuidStream.DefaultName:
return new SerializedGuidStream(header.Name, reader);

case PdbStream.DefaultName:
return new SerializedPdbStream(header.Name, reader);

default:
return new CustomMetadataStream(header.Name, DataSegment.FromReader(ref reader));
if (string.Equals(header.Name, TablesStream.CompressedStreamName, comparisonKind) ||
string.Equals(header.Name, TablesStream.EncStreamName, comparisonKind))
{
bool forceLargeColumns = (flags & MetadataStreamReaderFlags.IsEnc) != 0 &&
(flags & MetadataStreamReaderFlags.HasJtdStream) != 0;
return new SerializedTableStream(context, header.Name, reader)
{ ForceLargeColumns = forceLargeColumns };
}
if (string.Equals(header.Name, StringsStream.DefaultName, comparisonKind))
return new SerializedStringsStream(header.Name, reader);
if (string.Equals(header.Name, UserStringsStream.DefaultName, comparisonKind))
return new SerializedUserStringsStream(header.Name, reader);
if (string.Equals(header.Name, BlobStream.DefaultName, comparisonKind))
return new SerializedBlobStream(header.Name, reader);
if (string.Equals(header.Name, GuidStream.DefaultName, comparisonKind))
return new SerializedGuidStream(header.Name, reader);
// Always perform a case-sensitive comparison for PdbStream since it is not a stream read by the CLR
if (header.Name == PdbStream.DefaultName)
return new SerializedPdbStream(header.Name, reader);
return new CustomMetadataStream(header.Name, DataSegment.FromReader(ref reader));
}
}
}
3 changes: 2 additions & 1 deletion src/AsmResolver.PE/DotNet/Metadata/IMetadataStreamReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ public interface IMetadataStreamReader
/// Reads the contents of a metadata stream.
/// </summary>
/// <param name="context">The reader context.</param>
/// <param name="flags">Flags describing the currently read metadata.</param>
/// <param name="header">The header of the metadata stream.</param>
/// <param name="reader">The input stream to read from.</param>
/// <returns>The read metadata stream.</returns>
IMetadataStream ReadStream(MetadataReaderContext context, MetadataStreamHeader header, ref BinaryStreamReader reader);
IMetadataStream ReadStream(MetadataReaderContext context, MetadataStreamReaderFlags flags, MetadataStreamHeader header, ref BinaryStreamReader reader);
}
}
6 changes: 5 additions & 1 deletion src/AsmResolver.PE/DotNet/Metadata/MetadataStreamList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,28 @@ public class MetadataStreamList : LazyList<IMetadataStream>
private readonly MetadataStreamHeader[] _streamHeaders;
private readonly IMetadata _owner;
private readonly BinaryStreamReader _directoryReader;
private readonly MetadataStreamReaderFlags _streamReaderFlags;

/// <summary>
/// Prepares a new lazy-initialized metadata stream list.
/// </summary>
/// <param name="owner">The owner of the metadata stream list.</param>
/// <param name="context">The reader context.</param>
/// <param name="streamReaderFlags">Flags describing the currently read metadata.</param>
/// <param name="streamHeaders">The stream headers.</param>
/// <param name="directoryReader">The input stream containing the metadata directory.</param>
public MetadataStreamList(
IMetadata owner,
MetadataReaderContext context,
MetadataStreamReaderFlags streamReaderFlags,
MetadataStreamHeader[] streamHeaders,
in BinaryStreamReader directoryReader)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_streamHeaders = streamHeaders;
_owner = owner;
_directoryReader = directoryReader;
_streamReaderFlags = streamReaderFlags;
}

/// <inheritdoc />
Expand All @@ -42,7 +46,7 @@ protected override void Initialize()
foreach (var header in _streamHeaders)
{
var streamReader = _directoryReader.ForkAbsolute(_directoryReader.Offset + header.Offset, header.Size);
var stream = _context.MetadataStreamReader.ReadStream(_context, header, ref streamReader);
var stream = _context.MetadataStreamReader.ReadStream(_context, _streamReaderFlags, header, ref streamReader);
Items.Add(stream);
}
}
Expand Down
27 changes: 27 additions & 0 deletions src/AsmResolver.PE/DotNet/Metadata/MetadataStreamReaderFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System;

namespace AsmResolver.PE.DotNet.Metadata
{
/// <summary>
/// Special flags used to provide information about metadata to <see cref="IMetadataStreamReader"/>.
/// </summary>
[Flags]
public enum MetadataStreamReaderFlags
{
/// <summary>
/// No special flags are set.
/// </summary>
None = 0,
/// <summary>
/// EnC metadata is being used.
/// </summary>
IsEnc = 1,
/// <summary>
/// A #JTD stream is present.
/// </summary>
/// <remarks>
/// If the #JTD stream is present and EnC metadata is being used, all indices in the tables stream are 4 bytes.
/// </remarks>
HasJtdStream = 2,
}
}
27 changes: 25 additions & 2 deletions src/AsmResolver.PE/DotNet/Metadata/SerializedMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public class SerializedMetadata : Metadata
private readonly MetadataReaderContext _context;
private readonly BinaryStreamReader _streamContentsReader;
private readonly MetadataStreamHeader[] _streamHeaders;
private readonly bool _hasJtdStream;
private readonly bool _isEncMetadata;

/// <summary>
/// Reads a metadata directory from an input stream.
Expand Down Expand Up @@ -75,12 +77,26 @@ public SerializedMetadata(MetadataReaderContext context, ref BinaryStreamReader

// Eagerly read stream headers to determine if we are EnC metadata.
_streamHeaders = new MetadataStreamHeader[numberOfStreams];

bool? isEncMetadata = null;
for (int i = 0; i < numberOfStreams; i++)
{
_streamHeaders[i] = MetadataStreamHeader.FromReader(ref directoryReader);
if (_streamHeaders[i].Name == TablesStream.EncStreamName)
IsEncMetadata = true;
string name = _streamHeaders[i].Name;
if (isEncMetadata is null)
{
if (name == TablesStream.CompressedStreamName)
isEncMetadata = false;
else if (name == TablesStream.EncStreamName)
isEncMetadata = true;
}
if (name == TablesStream.UncompressedStreamName)
isEncMetadata = true;
else if (name == TablesStream.MinimalStreamName)
_hasJtdStream = true;
}

IsEncMetadata = _isEncMetadata = isEncMetadata ?? false;
}

/// <inheritdoc />
Expand All @@ -89,8 +105,15 @@ protected override IList<IMetadataStream> GetStreams()
if (_streamHeaders.Length == 0)
return base.GetStreams();

var flags = MetadataStreamReaderFlags.None;
if (_isEncMetadata)
flags |= MetadataStreamReaderFlags.IsEnc;
if (_hasJtdStream)
flags |= MetadataStreamReaderFlags.HasJtdStream;

return new MetadataStreamList(this,
_context,
flags,
_streamHeaders,
_streamContentsReader);
}
Expand Down
8 changes: 0 additions & 8 deletions src/AsmResolver.PE/DotNet/Metadata/Tables/IMetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,6 @@ TableLayout Layout
get;
}

/// <summary>
/// Gets the size of an index into this table.
/// </summary>
IndexSize IndexSize
{
get;
}

/// <summary>
/// Gets or sets the row at the provided index.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/AsmResolver.PE/DotNet/Metadata/Tables/IndexEncoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public IndexSize IndexSize
{
get
{
if (_tableStream.ForceLargeColumns)
return IndexSize.Long;

uint maxCount = 0;
foreach (var table in _tables)
maxCount = Math.Max(maxCount, _tableStream.GetTableRowCount(table));
Expand Down
3 changes: 0 additions & 3 deletions src/AsmResolver.PE/DotNet/Metadata/Tables/MetadataTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ public TableLayout Layout
private set;
}

/// <inheritdoc />
public IndexSize IndexSize => Count > 0xFFFF ? IndexSize.Long : IndexSize.Short;

/// <inheritdoc cref="IMetadataTable" />
public TRow this[int index]
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ private IndexSize[] InitializeIndexSizes()

// Add index sizes for each table:
foreach (uint t in _rowCounts)
result.Add(t > 0xFFFF ? IndexSize.Long : IndexSize.Short);
result.Add(ForceLargeColumns || t > 0xFFFF ? IndexSize.Long : IndexSize.Short);

// Add index sizes for each coded index:
result.AddRange(new[]
Expand Down Expand Up @@ -212,6 +212,9 @@ private IndexSize GetCodedIndexSize(params TableIndex[] tables)
if (_combinedRowCounts is null)
throw new InvalidOperationException("Serialized tables stream is not fully initialized yet.");

if (ForceLargeColumns)
return IndexSize.Long;

int tableIndexBitCount = (int) Math.Ceiling(Math.Log(tables.Length, 2));
int maxSmallTableMemberCount = ushort.MaxValue >> tableIndexBitCount;

Expand Down
23 changes: 22 additions & 1 deletion src/AsmResolver.PE/DotNet/Metadata/Tables/TablesStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,19 @@ public uint ExtraData
set;
}

/// <summary>
/// Gets or sets a value whether to force large columns in the tables stream.
/// </summary>
/// <remarks>
/// This value is typically <c>false</c>, it is intended to be <c>true</c> for cases when
/// EnC metadata is used and a stream with the <see cref="MinimalStreamName"/> name is present.
/// </remarks>
public bool ForceLargeColumns
{
get;
set;
}

/// <summary>
/// Gets a collection of all tables in the tables stream.
/// </summary>
Expand Down Expand Up @@ -258,6 +271,9 @@ public uint GetTableRowCount(TableIndex table)
/// </remarks>
public IndexSize GetTableIndexSize(TableIndex table)
{
if (ForceLargeColumns)
return IndexSize.Long;

return GetTableRowCount(table) > 0xFFFF
? IndexSize.Long
: IndexSize.Short;
Expand Down Expand Up @@ -496,7 +512,12 @@ public virtual MetadataTable<TRow> GetTable<TRow>(TableIndex index)
return (MetadataTable<TRow>) (Tables[(int) index] ?? throw new ArgumentOutOfRangeException(nameof(index)));
}

private IndexSize GetStreamIndexSize(int bitIndex) => (IndexSize) (((((int) Flags >> bitIndex) & 1) + 1) * 2);
private IndexSize GetStreamIndexSize(int bitIndex)
{
if (ForceLargeColumns)
return IndexSize.Long;
return (IndexSize)(((((int)Flags >> bitIndex) & 1) + 1) * 2);
}

private void SetStreamIndexSize(int bitIndex, IndexSize newSize)
{
Expand Down
Loading