Background
Describes a working tool designed to remove strong signing from .NET assemblies without recompiling code. This is version 2.1, bug fixed and Vista aware.
Introduction
This article describes how strong signing works in .NET Framework 1.1 and 2.0. In
particular, it is about how strong signing is implemented at file level - I mean
bytes in an assembly EXE or DLL. Knowing this allows me to best understand how security
should and can be implemented in managed code. Lastly, we must be aware strong signing
assemblies is not a definitive way against hackers, as official Microsoft documentation
says too.
This was originally posted on The Code Project
Background
I'm going to explain how ideas in this article came to life using a particular (imaginary)
scenario. John is a developer just been hired in a new company. His first task is
to fix some typos in an application developed by his company. Some employee previously
working there was not a native English speaker, so there were a lot of them (and
that employee resigned some time ago). Anyway, he is asked to complete his assigned
task by the next day. He first thinks it is a really easy job, but soon understands
that the previous employee has not checked-in the latest application version to
the source control. So, he only has the compiled application bits available, a hex
editor, and some hours left (OK, this is a very bad situation, but this is just
imaginary, so try to stay with me). He opens the executable and tries to find out
the typos - he gets them and he fixes them, at least the most bad ones, where the
customer name is incorrect ;) (yes, he can only overwrite existing bytes, but consider
this enough for this scenario). At last, he starts the application, discovering
it was a signed assembly ( Sign an Assembly with a Strong Name on MSDN) and it won't
load anymore. John has already read many articles on the assembly internal file
format, like those by Matt Pietrek ( part 1 and part 2) or Kevin Burton ( here), and he knows ILDASM and Asmex, and obviously, he has a CLI Reference downloaded and waiting. With
all that documentation available, he changes 6 bytes (!) in the file header,
removing or disabling strong signing from the assembly, getting a fully working
application with no more typos (but he has to complain about the lost source code
to his boss...).
Now, the article and the code... it's about those 6 bytes.
Points of interest
Questions are: what are the differences between a signed assembly and a normal one?
Can a signed assembly be brought back to unsigned status simply by patching it at
bytes level, without recompiling the source code? Answer to the second question
is yes, it's possible. Answer to the first question would reveal how. I would not
deal here with the complete NET header specifications, there are a lot of articles
explaining them (those already noted above and others like this).
For a complete Assembly Metadata reference, look at ECMA-335: CLI Partition II – Metadata (Word format) - this
is referred on the next discussion. What follows are particular data structures
and values related to assembly metadata. Patching (modifying) or removing (overwriting
with zeroes) them restores an assembly to the unsigned status.
- Runtime flags in CLI Header (documented in § 25.3.3.1) - this byte means
in plain words "Is assembly signed?" - if yes, we have a
COMIMAGE_FLAGS_STRONGNAMESIGNED
value there (8). To remove the strong signing, simply reset this byte back to its
current value minus COMIMAGE_FLAGS_STRONGNAMESIGNED. This usually leads
to a flag value of COMIMAGE_FLAGS_ILONLY (1).
- StrongNameSignature RVA in CLI Header (documented in § 25.3.3) - these are
8 bytes, giving an offset of public key hash data. Key is normally 160 (0xA0) bytes
itself. To remove strong signing, simply overwrite those 8 bytes in the header with
00. This suggests to the assembly loader that there's no signature in the file,
even if the original key is still there.
- Flags in Assembly Table (documented § 22.2) - this is a 4-byte bitmask of
type
AssemblyFlags (documented in § 23.1.2). A value of PublicKey
(0x0001) here means the assembly is strong signed. To remove strong signing, reset
the first byte back to its current value minus PublicKey. This usually
resets the flag to SideBySideCompatible value (0x0000).
- PublicKey index in Assembly Table (documented § 22.2) - this is an index
into the Blob heap where the Public Key is stored. To remove strong signing, overwrite
those 2 bytes with 00 (meaning, no Public Key available).
This process usually leads to a 6 to 12 bytes patching (depending on the bytes used
for RVA), and is just enough to fool the .NET loader (version 1.1 and 2.0) in believing
that the assembly is not strong signed at all. All you need is some reference and
a hex editor. This approach removes strong signing from an EXE assembly and DLLs
too. A bit more work (and experimenting) is needed if we are patching an assembly
referenced by another one (eventually strong signed itself).
This situation requires removing the strong signing from the DLL (using the described
method) and from the main executable (the one which references the DLL) and then
modifying the main executable references table to report the DLL as not signed.
The assembly reference table is named AssemblyRef (documented in §
22.5) and contains PublicKeyOrToken, an index into the Blob heap, indicating
the public key or token that identifies the author of the referenced assembly. Overwriting
the index (2 bytes) with 00 defines the assembly reference to the unsigned file.
Using the code
Having to manually deal with .NET assembly metadata and data tables is very boring.
The hard part there is to know the base table offset in the PE file, extract the
index from the table, and jump to the correct address (this is usually referred
to as RVA to Real Offsets). Experimenting at this using a hex editor was a very
interesting job, anyway we want a better way. At last, I found Asmex, a fantastic
tool written in C# and available for free with full source code ( official site and CodeProject). Having to work with all those .NET headers
and tables becomes really easy now.
Asmex source files are included unchanged, except for those exposing the two internal
fields in the class Table (file TableStream.cs) using properties.
We need that data so we can calculate the exact offsets in the file bytes for patching.
Retrieving the file offset of particular structures and bytes is, in most cases,
a simple property reading task using Asmex.
Getting file offsets and values from an assembly is done by the GetAssemblyData
method. This is a relevant part (in source code, it's a bit more complex with error
checking and comments):
MModule mod = new MModule(r);
cliHeaderFlag = mod.ModHeaders.COR20Header.Flags;
cliHeaderFlagOffset = mod.ModHeaders.COR20Header.Start + 16;
strongNameSignatureOffset =
mod.ModHeaders.COR20Header.StrongNameSignature.Start;
compiledRuntimeVersion =
mod.ModHeaders.MetaDataHeaders.StorageSigAndHeader.VersionString;
Table tableAssembly = mod.MDTables.GetTable(Types.Assembly);
publicKeyOffset =
mod.BlobHeap.Start + tableAssembly[0][6].RawData + 1;
assemblyFlag = (uint)tableAssembly[0][5].Data;
// next loop sum tables byte length till
// reaching Assembly Table - this would
// give Assembly Table start offset
long assemblyTableOffset =
mod.ModHeaders.MetaDataTableHeader.End;
for (int tablesCounter = 0; tablesCounter <
Int32.Parse(Enum.Format(typeof(Types),
Types.Assembly, "d")); tablesCounter++)
{
assemblyTableOffset +=
mod.MDTables.Tables[tablesCounter].RawData.Length;
}
publicKeyIndexOffset = assemblyTableOffset + 16;
assemblyFlagOffset = assemblyTableOffset + 12;
// next loop sum tables byte length till reaching
// Assembly References Table - this would give
// Assembly References Table start offset
long referenceTableOffset =
mod.ModHeaders.MetaDataTableHeader.End;
for (int tablesCounter = 0; tablesCounter <
Int32.Parse(Enum.Format(typeof(Types),
Types.AssemblyRef, "d")); tablesCounter++)
{
referenceTableOffset +=
mod.MDTables.Tables[tablesCounter].RawData.Length;
}
At this point, we have all the data (to produce a basic user interface, take a look
at the methods CLIHeaderFlagToString and AssemblyFlagToString
decoding flags values to human readable format according to metadata specifications)
and file offsets, so we can proceed with byte patching.
Memory-mapped files operation is my preferred approach in this case, so we can support
even big-sized files, with no performance issues.
MMF Win32 APIs are well documented in many books, though I prefer Advanced Windows
by Jeffrey Richter.
In the code, you'll find the PatchReference method to remove the Public
Key evidences from the assembly references and the PatchAssemblyStrongSigning
method to modify the CLI Header and the Assembly Table.
All the stated methods are contained in the class Utility. You'll also
find a helper class named AssemblyReference used to store assembly
references data while reading and decoding it.
History
Version 2.1 is a bug fix for Blob index sizing. Index was used as a fixed value,
but this is not true. Bug would happen on large files. Also changed application
graphics to be more Vista-style and added some code to be UAC aware.
Version 2.0 is the first public version. I experimented for a long time a 1.3 version,
which was based on .NET Framework 1.1, but wasn't able to patch references table.
|