Wednesday 31 March 2010

Strong Name Storage

Introduction

In Adding a Strong Name to a Third-Party Assembly I described how you can add a strong name to a third-party assembly by disassembling that assembly into IL using ILDASM and then re-assemble it with ILASM, specifying a strong name key.

I never really liked that approach. It seemed like using a sledge-hammer to crack a nut: just because we want to add a few bytes (i.e. the strong name) into an assembly, we have to break the entire assembly apart and put it back together again?

It always struck me as odd that the Strong Name Tool (SN.exe) provided no support for adding a strong name signature to a assembly, unless one already existed or the assembly had at least been delayed signed at compilation stage. Is adding a few bytes into the assembly to accommodate the strong name signature really that hard?

So for the last couple of weeks I've been studying both:

These documents describe the physical structure of .NET assemblies. Using the information contained therein, I've been able to establish just how a strong name is stored within an assembly. It's not pretty.

Portable Executable / Common Object File Format

A PE/COFF file (which all Windows EXEs/DLLs are) contains a series of headers followed by a series of sections. Each section must being on a boundary defined by the File Alignment field within one of the headers, which indicates that each section begins on either a 512-byte or 4096-byte boundary. We are only really interested in the section named ".text" as it is this section which contains the Strong Name Signature and the CLI Meta Data.

CLR Runtime Header / CLI Header

Within one of the headers exists a Data Directory which references various tables and strings within the image. The 14th entry in the Data Directory (counting from 0) contains the VirtualAddress and Size of the a header referred to variously as the CLR Runtime Header or CLI Header. I'll use the term CLI Header for the sake of consistency.

The VirtualAddress of the CLI Header is the address of the first byte of the header relative to the image base when the section is loaded into memory. As we're dealing with the file itself, not an in-memory image, we need to refer to the virtual address of the containing section and the file offset of the first byte of the section to establish the file offset of the CLI Header.

Strong Name Signature

The strong name itself is referenced by two fields within the CLI Header - StrongNameSignatureRVA and StrongNameSignatureSize. In a weak-named assembly both of these fields are set to 0. In a strong-named assembly they contain the relative virtual address (i.e. the offset relative to the start of the containing section) and size (in bytes) of the strong name signature. We could attempt to derive the strong name ourselves, but there seems little point. All we really need to do is set each byte in the strong name signature to 0x00 as that will represent a delay-signed assembly. We can then have the Strong Name Tool (SN.exe) perform the actual signing.

Given that each section is padded to a 512-byte (or 4096-byte) boundary anyway, it's possible that we can simply point StrongNameSignatureRVA to some location within this 'padding' area and make that the strong name signature. But it's also quite likely that there will be insufficient free space within the section to accommodate the strong name, so the section itself may need to be lengthened. Obviously this will mean the sections which follow it will need to be moved, and all the offsets within the headers will need to be updated to reference their new locations.

Public Key

Although we may leave the Strong Name Signature itself full of 0x00 bytes, we can't get away without setting the Public Key. Establishing where to locate the Strong Name Signature above was fairly easy - the location of the public key is a little more tricky.

First, we have to reference the MetaDataRVA and MetaDataSize fields within the CLI Header to establish the location of the CLI Meta Data within the image. The CLI Meta Data consists of a header plus five streams of data. One of these streams, #Blob (the Blob Heap), contains the 160-byte public key. Unfortunately, entries within the Blob Heap are not named - they are referenced by their offset relative to the start of the stream - so we can't just add a new entry to this stream and be done with it. The entry which refers to the public key within the Blob Heap in within a separate steam, #~, which consists of a series of tables with varying structures.

The simplest .NET application you can think of will contain the following tables: Module, TypeRef, TypeDef, MethodDef, MemberRef, CustomAttribute, Assembly and AssemblyRef. The reference to the public key is stored within the Assembly table. But to even find the Assembly table you need to seek past all the tables preceding it (the list above is in order, so Assembly is one of the last tables).

To establish the location of the Assembly table you need to know which tables precede it (the list above is a minimum set, there may be many other tables in a given assembly). This list of tables is readily available. You also need to know how many rows are in each table. This too, is readily available. Finally, you need to know how wide each row is. Establishing this is not so straight-forward. For example, the Extends column in the TypeDef table contains an index into the TypeDef, TypeRef, or TypeSpec tables. This index will occupy 2 or 4 bytes depending upon the number of rows in those tables. Such an index is referred to in the specification as a coded index "using 2 bytes if the maximum number of rows of tables t0, ...tn-1, is less than 2(16 – (log n)), and using 4 bytes otherwise". In this example n is 3 (as the index may reference one of 3 separate tables).

Method Implementations

One other thing we need to handle is the fact that the RVA field in reach row of the MethodDef table contains the relative virtual address of that method's implementation. Once we've inserted a zeroed-out strong name signature and a public key and into the file, these method implementations will now be at different locations so we'll have to update the RVAs too. A similar issue occurs with the RVA column in the FieldRVA table.

Summary

Given all the above, it's no surprise that the Strong Name tool opted not to support this scenario. I don't give up easily, so I'm going to pursue my goal of creating a tool which performs all the magic necessary to add a strong name where the Strong Name Tool cannot. But I suspect I won't achieve this any time soon.

See Also

No comments:

Post a Comment