
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.
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 worst 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? The answer to the second question is yes, it's possible. The 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 in 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) into 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;
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;
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.
Using them from C# and .NET can be easily done with the excellent work by Ren� Grob ("argee"). You can find his library on GotDotNet: Managed wrapper classes for memory mapped files and unmanaged memory access.
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.
|
|
 |
 | Program seems to crash on x64 OS when "patch" button is pressed. zespri | 19:21 15 Feb '10 |
|
 |
Just a heads up - ran on Win 7 x64, got a failure, copied program and target files to 32bit xp virtual box and it ran fine. Thanks.
|
|
|
|
 |
 | Great tool, but perhaps you could help us also! Member 4707218 | 9:23 1 Dec '09 |
|
 |
Hi,
- We have application common.dll using Nhibernate 1.0.2 that uses log4net.dll version 1.2.9. - We also use xxx.dll that MUST use log4net.dll version of 1.2.10.
At the moment we can't used xxx.dll in the aplication while it would mean we have to use two log4net.dlls.
We try to remove signed dll with your application doing next, while we try to insert log4net.dll v 1.2.10 into our application and trick nhiberante.
1. Remove strong naming from log4net.ddl v.1.2.10 2. We used for nhibernate.dll to point to unsigned assembly log4net v 1.2.10
But the Nhibernate discard to load log4net v 1.2.10.
"[System.IO.FileLoadException] {"Could not load file or assembly 'log4net, Version=1.2.9.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. A strongly-named assembly is required. (Exception from HRESULT: 0x80131044)":"log4net, Version=1.2.9.0, Culture=neutral, PublicKeyToken=null"} System.IO.FileLoadException"
Can it be that also version of referenced dll is checked.
Thank you for your help.
Regards,
Dusan
|
|
|
|
 |
|
 |
This one seems a little challenge. I really don't see how removing strong naming can help with your problem. Even if you could reference log4net 1.2.10 from nHibernate, xxx.dll should be patched too, because it would try to load a signed log4net dll. Moreover, removing strong signing from log4net 1.2.10, doesn't mean nHibernate can load it, even if patched, because it would look at another version (previous 1.2.9) - this can be patched too, but probably something wouldn't work at runtime, because subtle differences in log4net implementation. At last, I would try another approach: keep your application ad xxx.dll as they are. Start from nHibernate source code (same version you are working on or a recent one) and modify that, removing references to log4net 1.2.9 and adding 1.2.10. If nHibernate can compile without problems, you would get an nHibernate custom version using log4net 1.2.10, but also signed. If it can't compile anymore, you should try to understand how to upgrade it to new log4net methods (I'm not using log4net, so don't know how it changed from 1.2.9 to 1.2.10). Maybe you can also ask some help from nHibernate community and request them to bind to new log4net version. My general idea is to avoid byte patching if using open source code projects (and you are lucky enough to use them). Hope this helps!
Causing an automatic reboot is a feature for a program, not a bug.
|
|
|
|
 |
|
 |
Hi,
Thank you for very quick response. We have try to solve this as you proposed, but the compilation of NHibernate goes Ok, but in runtime we got problems, not with log4net but with nhibernate! We still investigate this problem ...
Thanks once more for help...
|
|
|
|
 |
 | Your utility has a SMALL problem Mackovic | 14:33 2 Nov '09 |
|
 |
Imagine there are three assemblies. Assembly A, assembly B and assembly C. Assembly C is referenced by assembly B and assembly B is referenced by assembly A. All assemblies are signed. If you unsign assembly B, your utility offers to unsign assembly C too by clicking on button "Patch references". But! If you want assembly B to work, you must patch the reference to it in assembly A too. And that is something your utility cannot do. At least programaticaly. Now I must find all assemblies referencing assembly B and patch them manually (or using your utility).
|
|
|
|
 |
|
 |
That is an intended problem and not so easy to solve. It's easy to know what assemplies are referenced by a DLL, but it's not immediate to know who is using current DLL (it could require scanning all DLL/EXE in a folder of perhaps complete disk). This could surely be done, but it's far away from my proof of concept tool.
Causing an automatic reboot is a feature for a program, not a bug.
|
|
|
|
 |
 | Two small bugs DanielRose1981 | 2:38 30 Oct '09 |
|
 |
Hi!
I found two small bugs, which I fixed locally:
1) If the selected file's path contains spaces, upon restarting elevated, the path is considered as several arguments.
Fix: In VistaSecurity.cs, method RestartElevated(string arguments):
startInfo.Arguments = string.Format("\"{0}\"", arguments);
2) In non-English Windows installations, the name of the administrator role can be different.
Fix: In VistaSecurity.cs, method IsAdmin():
return p.IsInRole(WindowsBuiltInRole.Administrator);
|
|
|
|
 |
|
 |
Thank you for spotting those out!
Causing an automatic reboot is a feature for a program, not a bug.
|
|
|
|
 |
 | Very Nice! jay_dubal | 2:02 14 Jul '09 |
|
|
 |
 | Problem I_gO_tO_schoOl_by_scoOter | 23:47 5 Jul '08 |
|
 |
The article is great, but the application didn't work with me. Any way, I've managed to patch the exe file manually. And I have this error on running the exe
"Could not load file or assembly 'Name, Version=1.0.869.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference. (Exception from HRESULT: 0x80131040)"
Also, I've altered its PublicKeyOrToken in the assembly ref of the exe to be null, as I edited this dll.
Got any clue what I've done wrong or what's missing ?!!
Familiarity sometimes keeps us from seeing the obvious.
|
|
|
|
 |
|
 |
This is normal exception from CLR when something is not correctly patched. Maybe it's another DLL reference or some DLL loading by reflection. You could send me an example by e-mail of what you are doing and I'll try to understand why my application is not working for you.
Causing an automatic reboot is a feature for a program, not a bug.
|
|
|
|
 |
 | InternalsVisibleTo causing issues fnfrich | 11:15 27 Jun '08 |
|
 |
When I try to remove strong signing from assemblies that make use of the InternalsVisibleTo attribute in their AssemblyInfo I end up getting FieldAccessExceptions. I'm guessing that since InternalsVisibleTo can't find the strong named dll it doesn't grant access to any internal fields. Is there any way around this?
By the way, thanks for the fantastic article.
|
|
|
|
 |
|
 |
As far as I know there could be a way to deal with this situation too. It requires patching and registering assembly for verification skipping. Maybe you can mail me your code and DLL and we can have a look at them.
Causing an automatic reboot is a feature for a program, not a bug.
|
|
|
|
 |
 | How Remove the CLI header ? LucianoNet | 17:56 4 Jun '08 |
|
 |
Hi,
How Remove the CLI header ? To protect my source code I need your help.
Thanks, Luciano - From Brazil
|
|
|
|
 |
|
 |
Removing a CLI header from a .NET executable means that is not a .NET program anymore. This could be achieved building a Win32 wrapper outside your executable (a stub), which loads file from disk, decrypt in memory and starts .NET application at last. This scheme is used by some commercial applications protectors and encrypters, but I personally don't like mixing Win32 techniques with .NET (and this kind of protection was already defeated too). A good .NET protection should not rely on Win32.
Causing an automatic reboot is a feature for a program, not a bug.
|
|
|
|
 |
 | well written lallous | 5:46 23 Oct '07 |
|
 |
Thanks a lot for this well written informative article!
|
|
|
|
 |
 | Software not working under XP 88443 | 10:56 14 Aug '07 |
|
 |
Because of the vistasecurity.isadmin check it won't work on my dutch XP SP2. I changed the following lines in the source:
if (VistaSecurity.IsAdmin())
to
if (VistaSecurity.IsAdmin() | VistaSecurity.IsVistaOrHigher())
so it will work on my version of windows.
Thanks for this nice software
|
|
|
|
 |
|
 |
I had a look at it. It seems a problem when using it in Windows XP SP2 and not being an admin. Anyway, proposed solution is not working for me. IsVistaOrHigher method in VistaSecurity class, incorrectly reports a Vista system even on Windows XP. So I also changed that method to return Environment.OSVersion.Version.Major >= 6;
Thanks for spotting this out!
|
|
|
|
 |
|
 |
Now is working for me - thank you for this FANTASTIC program !!!!
|
|
|
|
 |
|
 |
Hi! Please, check it. Now VistaSecurity contains:
internal static bool IsVistaOrHigher() { return Environment.OSVersion.Version.Major < 6; }
Also code p.IsInRole(@"BUILTIN\Administrators") not valid for localized versions of Windows.
Thanks!
modified on Saturday, October 25, 2008 10:25 AM
|
|
|
|
 |
 | Add a strong name Alois Kraus | 13:37 23 Jan '07 |
|
|
 |
|
 |
Thanks.
Finally... We have the very needed complimentary software for this.
Remove VS Add Strong Name.
I like you, and I love programming more. in C# & Java
|
|
|
|
 |
|
 |
Nice tool but I havent managed to test it successfully
C:\Program Files\Test>signer -k test.snk -outdir .\build -a test.dll -debug Strong naming .\test.dll ... An error occured while processing file .\test.dll: The assem bly name was null or empty for type ##, parsed line: + "ib, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a"
|
|
|
|
 |
|
 |
Hi Verty,
I will have a look into this issue. It looks like the IL parser needs to become more robust when dealing with very long type names which are split into several lines.
Yours, Alois Kraus
|
|
|
|
 |
|
 |
Hi,
this error should go away if you download the latest source release and patch with it the version 1.0 to get rid of this error. I am currently working on a solution to represent the contents of an IL file as an object model for the most important parts. This new parser which will fix this error once and for all along with many more benefits.
Yours, Alois
|
|
|
|
 |
|
|
Last Updated 19 Jul 2007 |
Advertise |
Privacy |
Terms of Use |
Copyright ©
CodeProject, 1999-2010