Skip to content

Unsorted ClassLayouts may silently break FieldRva #724

Description

@wondercrash

AsmResolver Version

6.0.0-beta.6

.NET Version

.NET 10.0

Operating System

Windows

Describe the Bug

When a PE has unsorted ClassLayout table rows with the sorted flag set to false, AsmResolver can silently fail to read and write FieldRvas

How To Reproduce

demo.zip

Reading:

var file = "./test.obf.dll";
var module = ModuleDefinition.FromFile(file);
var field = module.GetAllTypes()
    .SelectMany(t => t.Fields)
    .First(f => f.Name == "XorKey");

// check IsSorted
var ts = module.DotNetDirectory!.Metadata!.GetStream<TablesStream>();
var cl = ts.GetTable<ClassLayoutRow>(TableIndex.ClassLayout);
Console.WriteLine($"ClassLayout IsSorted: {cl.IsSorted}"); // false

Console.WriteLine("from FieldRva:");
var fromFieldRva = field.FieldRva?.WriteIntoArray() ?? [];
Console.WriteLine(fromFieldRva.Length); // prints 0

Console.WriteLine("from manually reading the table:");
var fieldType = field.Signature!.FieldType.Resolve(module.RuntimeContext);
var size = (int)(fieldType.ClassLayout?.ClassSize ?? 0); // get size from layout
var rvaTable = ts.GetTable<FieldRvaRow>(TableIndex.FieldRva);
for (var i = 0; i < rvaTable.Count; i++)
{
    var row = rvaTable.GetByRid((uint)(i + 1));
    if (row.Field == field.MetadataToken.Rid)
    {
        var raw = row.Data.CreateReader().ReadBytes(size);
        Console.WriteLine(raw.Length);                        // prints 22
        Console.WriteLine(Encoding.UTF8.GetString(raw));      // prints the key
        break;
    }
}

Writing:

var file = "./test.obf.dll";
var module = ModuleDefinition.FromFile(file);
module.Write("test.rebuilt.dll");

var rebuiltModule = ModuleDefinition.FromFile("test.rebuilt.dll");
var field = rebuiltModule.GetAllTypes()
    .SelectMany(t => t.Fields)
    .First(f => f.Name == "XorKey");

// check IsSorted
var ts = rebuiltModule.DotNetDirectory!.Metadata!.GetStream<TablesStream>();
var cl = ts.GetTable<ClassLayoutRow>(TableIndex.ClassLayout);
Console.WriteLine($"rebuilt ClassLayout IsSorted: {cl.IsSorted}"); // true since we re-built it (even if we use PreserveAll it will still be sorted)

var fromFieldRva = field.FieldRva?.WriteIntoArray() ?? [];
Console.WriteLine(fromFieldRva.Length); // prints 22
Console.WriteLine(Encoding.UTF8.GetString(fromFieldRva)); // garbage data (verifiable in a hex editor that it was overwritten)

Expected Behavior

Reading: field.FieldRva should have the data
Writing: It should preserve the field.FieldRva data and potentially stay unsorted?

Actual Behavior

Reading: field.FieldRva does not contain the data
Writing: The data stored in the field rva is overwritten

Additional Context

I haven't done much debugging, but it looks like whatever is reading it assumes sorted rows.

This doesn't seem like it'd ever output from the compiler, but the CLR runs it. I don't think this is compliant with the spec, but if it works, then it works.

The Ciphertext field rva is read and written correctly. I think that is just a coincidence though

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions