tl;dr
The Windows program loader isn’t the only PE parser in Windows. The .NET runtime has its own used for loading modules as well. We can find yester years code for on the Internet for the implementation which shows some interesting defensive properties. Examples include obvious defences against import, entry point and base location mischief.
Pre Introduction
This work was originally done in June of 2013 yet published in December of 2013. During this time Matt Greaber of exploit-monday.com published a blog titled ‘Reversing Install Call Methods‘ in November 2013 covering a similar and somewhat related.
Introduction
While trying to exploit an instance of unsafe usage of Assembly.Load in a .NET based application I got curious how Assembly.Load worked under the hood. As .NET modules are housed inside standard PE files I had incorrectly thought that they would have just used LoadLibrary and some voodoo. Turns out I was very wrong; the usage of PE files in the .NET runtime is described in ECMA-335 in section II.25 part of the ECMA C# and Common Language Infrastructure (CLI) Standards . This post is a summary of what I discovered along the way to answering my questions aroundAssmbly.Load and sources I used along the way.
Assembly.Load == Assembly.InternalLoad
A quick investigation with ILSpy shows that System.Reflection.Assembly.Load is actually System.Reflection.Assmbly.InternalLoad (formscorlib.dll v2.0.0)
// System.Reflection.Assembly
[MethodImpl(MethodImplOptions.NoInlining)]
public static Assembly Load(string assemblyString)
{
StackCrawlMark stackCrawlMark = StackCrawlMark.LookForMyCaller;
return Assembly.InternalLoad(assemblyString, null, ref stackCrawlMark, false);
}
If we then look at the actual implementation of Assembly.InternalLoad we see:
// System.Reflection.Assembly
internal static Assembly InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, ref StackCrawlMark stackMark, bool forIntrospection)
{
.. SNIP …
return Assembly.nLoad(assemblyRef, text, assemblySecurity, null, ref stackMark, true, forIntrospection);
}
For version 4.0.4 of mscorlib.dll we see some similar although a little different:
// System.Reflection.Assembly
[SecuritySafeCritical]
[MethodImpl(MethodImplOptions.NoInlining)]
public static Assembly Load(AssemblyName assemblyRef)
{
StackCrawlMark stackCrawlMark = StackCrawlMark.LookForMyCaller;
return RuntimeAssembly.InternalLoadAssemblyName(assemblyRef, null, ref stackCrawlMark, false, false);
}
Then:
// System.Reflection.RuntimeAssembly
[SecurityCritical]
internal static RuntimeAssembly InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity, ref StackCrawlMark stackMark, bool forIntrospection, bool suppressSecurityChecks)
{
... SNIP ...
return RuntimeAssembly.nLoad(assemblyRef, text, assemblySecurity, null, ref stackMark, true, forIntrospection, suppressSecurityChecks);
}
In both cases System.Reflection.RuntimeAssembly.nLoad simply acts as a wrapper to System.Reflection.RuntimeAssembly._nLoad which is defined as:
// System.Reflection.RuntimeAssembly
[SecurityCritical]
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern RuntimeAssembly _nLoad(AssemblyName fileName, string codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, ref StackCrawlMark stackMark, bool throwOnFileNotFound, bool forIntrospection, bool suppressSecurityChecks);
The key bit in the above is the MethodImplOptions.InternalCall which indicates that the method is implemented in the Common Language Runtime (CLR) – i.e. native code.
.NET Framework Libraries != CLR
So the first thing I quickly learnt on this little journey is that the .NET Framework Libraries that are released by Microsoft don’t include the entire VM and supporting infrastructure. The result is that if you look at the .NET Framework Libraries reference source code you will see something similar to the below when looking at Assembly.Load (.NET 4.54.5.50709.0netndpclrsrcBCLSystemReflectionAssembly.cs597531Assembly.cs)
[System.Security.SecurityCritical] // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private static extern RuntimeAssembly _nLoad
(AssemblyName fileName,
String codeBase,
Evidence assemblySecurity,
RuntimeAssembly locationHint,
ref StackCrawlMark stackMark,
bool throwOnFileNotFound,
bool forIntrospection,
bool suppressSecurityChecks);
Yet you there is no implementation for _nLoad anywhere… the answer? The Microsoft Shared Source CLI Implementation (Wikipedia for the history) which is version 2.0.0 of the .NET runtime basically released under a restricted open source license.
CLI Source Includes the CLR
You can get multiple versions of the CLI (1.0 and 2.0 there is even a book on it for those *really* interested in how a .NET VM in built and functions).
Using the source found in sscli20_20060311sscli20clrsrcvmecall.cpp we can locate the nLoad function we were after:
FCFuncStart(gAssemblyFuncs)
FCFuncElement("GetFullName", AssemblyNative::GetStringizedName)
FCFuncElement("GetLocation", AssemblyNative::GetLocation)
FCFuncElement("GetResource", AssemblyNative::GetResource)
FCFuncElement("nGetCodeBase", AssemblyNative::GetCodeBase)
FCFuncElement("nGetExecutingAssembly", AssemblyNative::GetExecutingAssembly)
FCFuncElement("nGetFlags", AssemblyNative::GetFlags)
FCFuncElement("nGetHashAlgorithm", AssemblyNative::GetHashAlgorithm)
FCFuncElement("nGetLocale", AssemblyNative::GetLocale)
FCFuncElement("nGetPublicKey", AssemblyNative::GetPublicKey)
FCFuncElement("nGetSimpleName", AssemblyNative::GetSimpleName)
FCFuncElement("nGetVersion", AssemblyNative::GetVersion)
FCFuncElement("nIsDynamic", AssemblyNative::IsDynamic)
FCFuncElement("nLoad", AssemblyNative::Load)
I’m not going to show you every function it bounces through but suffice to say we get a call flow along the lines of:
The function DomainFile::DoIncrementalLoad is quite interesting as it is just a big switch statement that represents the different loader stages. Suffice to say this is where it dips off into a custom a PE file parser (hence this blog post). This is implemented in the filesscli20_20060311sscli20clrsrcvmdomainfile.cpp where we see:
AppDomain *pAppDomain = GetAppDomain();
PEFile *pFile = GetFile();
_ASSERTE(pFile != NULL);
PEImage *pImage = pFile->GetILimage();
_ASSERTE(pImage != NULL);
_ASSERTE(!pImage->IsFile());
The PE Parser
The PE parser inside the .NET runtime is spread over a number of files, namely:
- sscli20_20060311sscli20clrsrcvmpefile.cpp / .h
- sscli20_20060311sscli20clrsrcvmpeimage.cpp / .h
- sscli20_20060311sscli20clrsrcutilcodepedecoder.cpp
- sscli20_20060311sscli20clrsrcincpedecoder.h
- sscli20_20060311sscli20clrsrcincpedecoder.inl
Firstly I wanted to validate that what I was seeing in the old code from 2006 was still applicable (or near enough) in 2013. Looking inside clr.dll fromC:WindowsMicrosoft.NETFrameworkv4.0.30319 in IDA using the Microsoft published debug symbols we see the same internal function names present:
This looks good, so let’s verify this in a debugger. We do this by starting a .NET binary inside WinDbg:
0:000> .restart /f # Restart the process
CommandLine: C:DataNCC!CodeGit.PublicdotnetpefuzzingWin.DotNetAssemblyLoadHarnessbinDebugWin.DotNetAssemblyLoadHarness.exe .DLLs
Starting directory: C:DataNCC!CodeGit.PublicdotnetpefuzzingWin.DotNetAssemblyLoadHarnessbinDebugSymbol search path is: SRV*c:symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
(18b8.1ce0): Break instruction exception - code 80000003 (first chance)
ntdll!LdrpDoDebuggerBreak+0x30:
00000000`7770cb60 cc int 3
0:000> sxe ld:clr # Tell it to break when we see the CLR
0:000> bm /a clr!!PEFile*::* # Try and fail to set a break match
No matching code symbols found, no breakpoints set.
0:000> g # Continue program
(18b8.1ce0): WOW64 breakpoint - code 4000001f (first chance)
# First chance exceptions are reported before any exception handling.
# This exception may be expected and handled.
ntdll32!LdrpDoDebuggerBreak+0x2c:
778e0fab cc int 3
0:000:x86> g # Continue program
ModLoad: 5ac90000 5b2ff000 C:WindowsMicrosoft.NETFrameworkv4.0.30319clr.dll
ntdll!NtMapViewOfSection+0xa:
00000000`776b159a c3 ret
0:000> bm /a clr!!PEFile*::* # Try and succeed in setting breakmatch
# for the classes we are interested in
1: 00000000`5ad966aa @!"clr!PEFile::GetNativeImageConfigFlags"
2: 00000000`5ace94d7 @!"clr!PEFile::GetResource"
3: 00000000`5ae2bf24 @!"clr!PEFile::EnsureValidPlatform"
4: 00000000`5acd8882 @!"clr!PEFile::IsResource"
5: 00000000`5afaad49 @!"clr!PEFile::ExternalLog"
6: 00000000`5adeaf1d @!"clr!PEFileSecurityDescriptor::QuickIsFullyTrusted"
7: 00000000`5adc1a3a @!"clr!PEFile::CheckRvaField"
8: 00000000`5acd7d19 @!"clr!PEFile::GetSimpleName"
9: 00000000`5ad479d6 @!"clr!PEFile::DefineEmitScope"
10: 00000000`5adead4a @!"clr!PEFileSecurityDescriptor::AllowBindingRedirects"
11: 00000000`5ace98c9 @!"clr!PEFile::GetEmbeddedResource"
12: 00000000`5afab55d @!"clr!PEFile::GetPathForErrorMessages"
13: 00000000`5ad53864 @!"clr!PEFile::CheckAuthenticodeSignature"
14: 00000000`5ae2a7b6 @!"clr!PEFile::OpenMDImport_Unsafe"
15: 00000000`5ad0c76a @!"clr!PEFile::SetNativeImage"
16: 00000000`5ad0c607 @!"clr!PEFile::CheckNativeImageVersion"
17: 00000000`5ae183cd @!"clr!PEFile::LoadAssembly"
18: 00000000`5acbaae7 @!"clr!PEFile::Release"
19: 00000000`5adb32ce @!"clr!PEFile::OpenImporter"
20: 00000000`5af0b762 @!"clr!PEFile::IsIbcOptimized"
21: 00000000`5ae2de87 @!"clr!PEFile::CheckForDisallowedInProcSxSLoadWorker"
22: 00000000`5ad701e1 @!"clr!PEFile::HasTls"
23: 00000000`5adeaeb5 @!"clr!PEFileSecurityDescriptor::ResolveWorker"
24: 00000000`5ad0d148 @!"clr!PEFile::ExternalLog"
25: 00000000`5ae2beba @!"clr!PEFile::EnsureReferenceAssemblyLoadedReflectionOnly"
26: 00000000`5ad0c1d6 @!"clr!PEFile::ExternalVLog"
27: 00000000`5afacd2e @!"clr!PEFile::GetCodeBaseOrName"
28: 00000000`5afab461 @!"clr!PEFile::OpenAssemblyImporter"
29: 00000000`5ad0c599 @!"clr!PEFile::CheckNativeImageTimeStamp"
30: 00000000`5afaad24 @!"clr!PEFile::SetLoadedHMODULE"
31: 00000000`5ae83168 @!"clr!PEFile::GetScopeName"
32: 00000000`5ad537c8 @!"clr!PEFile::HasSecurityDirectory"
33: 00000000`5adeae9c @!"clr!PEFileSecurityDescriptor::`vftable'"
34: 00000000`5ae49400 @!"clr!PEFile::`vector deleting destructor'"
35: 00000000`5afab24c @!"clr!PEFile::GetILImageTimeDateStamp"
36: 00000000`5adeb6a8 @!"clr!PEFileSecurityDescriptor::GetEvidence"
37: 00000000`5b2a5b08 @!"clr!PEFile::s_NGENDebugFlags"
38: 00000000`5ad53980 @!"clr!PEFile::GetSHA1Hash"
39: 00000000`5acd9038 @!"clr!PEFile::FlushExternalLog"
40: 00000000`5adeb7b1 @!"clr!PEFileSecurityDescriptor::BuildEvidence"
41: 00000000`5ae4534e @!"clr!PEFile::GetManagedFileContents"
breakpoint 34 redefined
34: 00000000`5ae49400 @!"clr!PEFile::`scalar deleting destructor'"
42: 00000000`5ace2820 @!"clr!PEFile::GetVersion"
43: 00000000`5ad48a49 @!"clr!PEFile::GetSecurityIdentity"
44: 00000000`5ae2a590 @!"clr!PEFile::PEFile"
45: 00000000`5ae38a77 @!"clr!PEFile::IsLoaded"
46: 00000000`5ae368f8 @!"clr!PEFile::GetFlags"
47: 00000000`5ae36a53 @!"clr!PEFile::GetMDImportWithRef"
48: 00000000`5ad64af9 @!"clr!PEFile::GetEntryPointToken"
49: 00000000`5adeaeac @!"clr!PEFileSecurityDescriptor::Resolve"
50: 00000000`5ace27cc @!"clr!PEFile::GetPublicKey"
51: 00000000`5afab4f5 @!"clr!PEFile::OpenAssemblyEmitter"
52: 00000000`5afc11c1 @!"clr!PEFileSecurityDescriptor::GetZone"
53: 00000000`5ad0cc98 @!"clr!PEFile::PassiveDomainOnly"
54: 00000000`5acbb419 @!"clr!PEFile::IsStrongNamed"
55: 00000000`5afab215 @!"clr!PEFile::GetNGENDebugFlags"
56: 00000000`5afab017 @!"clr!PEFile::ReleaseIL"
57: 00000000`5add0317 @!"clr!PEFile::IsRvaFieldTls"
58: 00000000`5ae14e95 @!"clr!PEFile::IsNativeLoaded"
59: 00000000`5ad70326 @!"clr!PEFile::IsDll"
60: 00000000`5afab4ab @!"clr!PEFile::OpenEmitter"
61: 00000000`5adeb74b @!"clr!PEFile::GetSafeHandle"
62: 00000000`5ad538b1 @!"clr!PEFile::CheckHash"
63: 00000000`5adeafbf @!"clr!PEFile::Open"
64: 00000000`5ae38ad7 @!"clr!PEFile::LoadLibrary"
65: 00000000`5ae2b556 @!"clr!PEFile::CanLoadLibrary"
66: 00000000`5ae36538 @!"clr!PEFile::EnsureImageOpened"
67: 00000000`5ae7d431 @!"clr!PEFile::GetFieldTlsOffset"
68: 00000000`5adcf052 @!"clr!PEFile::~PEFile"
69: 00000000`5ad5388f @!"clr!PEFile::GetHash"
70: 00000000`5afab45a @!"clr!PEFile::ConvertMetadataToRWForEnC"
71: 00000000`5b168aef @!"clr!PEFile::GetSubsystem"
72: 00000000`5adfb234 @!"clr!PEFile::Equals"
73: 00000000`5adeae6d @!"clr!PEFileSecurityDescriptor::PEFileSecurityDescriptor"
74: 00000000`5ad6be96 @!"clr!PEFile::GetHashAlgId"
75: 00000000`5ae30f9d @!"clr!PEFile::ReferencesManagedCRT"
76: 00000000`5acd886c @!"clr!PEFile::IsIntrospectionOnly"
77: 00000000`5adcecc3 @!"clr!PEFile::ReleaseMetadataInterfaces"
78: 00000000`5ad6bdd9 @!"clr!PEFile::GetLocale"
79: 00000000`5acd9024 @!"clr!PEFile::`vftable'"
80: 00000000`5acbacf7 @!"clr!PEFile::IsILOnly"
81: 00000000`5aca433b @!"clr!PEFile::GetLoadedIL"
82: 00000000`5acd7c2b @!"clr!PEFile::GetMDImport"
83: 00000000`5adcea85 @!"clr!PEFile::MarkNativeImageInvalidIfOwned"
84: 00000000`5ae2bfb8 @!"clr!PEFile::IsMarkedAsNoPlatform"
85: 00000000`5acd2424 @!"clr!PEFile::GetIL"
86: 00000000`5ae38b15 @!"clr!PEFile::CheckForDisallowedInProcSxSLoad"
87: 00000000`5afaab22 @!"clr!PEFile::GetAuthenticodeSignature"
88: 00000000`5ad0c23e @!"clr!PEFile::ExternalLog"
89: 00000000`5ae3641b @!"clr!PEFile::GetNativeImageWithRef"
90: 00000000`5adb3b6b @!"clr!PEFile::ConvertMDInternalToReadWrite"
91: 00000000`5ad6bf94 @!"clr!PEFile::GetPEKindAndMachine"
92: 00000000`5add50aa @!"clr!PEFile::ClearNativeImage"
93: 00000000`5afaad32 @!"clr!PEFile::SetNGENDebugFlags"
94: 00000000`5ad70152 @!"clr!PEFile::GetVTableFixups"
95: 00000000`5ae35f0c @!"clr!PEFileListLock::FindFileLock"
96: 00000000`5afaae66 @!"clr!PEFile::RestoreMDImport"
97: 00000000`5ad489e6 @!"clr!PEFile::InitializeSecurityManager"
98: 00000000`5ae2de62 @!"clr!PEFile::GetMetadata"
99: 00000000`5ae38ab4 @!"clr!PEFile::CheckLoaded"
0:000> g # Resume
(18b8.1ce0): Unknown exception - code 04242420 (first chance)
Breakpoint 39 hit
clr!EETypeHashTable::Iterator::~Iterator:
5acd9038 8bff mov edi,edi
0:000:x86> g # Resume
Breakpoint 1 hit
clr!PEFile::GetNativeImageConfigFlags:
5ad966aa 8bff mov edi,edi
0:000:x86> k # Dump call stack to show
# how we got here as expected
# but from system domain due
# to loading…
ChildEBP RetAddr
001ef2fc 5ad9993e clr!PEFile::GetNativeImageConfigFlags
001ef5ac 5ad99c60 clr!PEAssembly::DoOpenSystem+0x190
001ef61c 5ad9827d clr!PEAssembly::OpenSystem+0x6e
001ef678 5ad98171 clr!SystemDomain::LoadBaseSystemClasses+0xb6
001ef6ac 5ad97dea clr!SystemDomain::Init+0xa8
001ef7d8 5ad65b66 clr!EEStartupHelper+0x7e0
001ef810 5ad65b09 clr!EEStartup+0x52
001ef854 5ade9e2d clr!EnsureEEStarted+0xc4
001ef8a0 5adad010 clr!_CorExeMainInternal+0x11c
001ef8d8 727a55ab clr!_CorExeMain+0x4e
001ef8e4 72837f16 mscoreei!_CorExeMain+0x38
001ef8f4 72834de3 MSCOREE!ShellShim__CorExeMain+0x99
001ef8fc 768733aa MSCOREE!_CorExeMain_Exported+0x8
001ef908 77879ef2 KERNEL32!BaseThreadInitThunk+0xe
001ef948 77879ec5 ntdll32!__RtlUserThreadStart+0x70
001ef960 00000000 ntdll32!_RtlUserThreadStart+0x1b0:000:x86> g
Breakpoint 44 hit
clr!PEFile::PEFile: # We hit the constructor
5ae2a590 6a08 push 8
0:000:x86> g
Breakpoint 16 hit
clr!PEFile::CheckNativeImageVersion: # Method hit
5ad0c607 6830010000 push 130h
... snip ...
So the above shows we’re executing these functions during a .NET programs initial load! However in reality once the JIT compiler gets involved it can prove a little trickier to debug and track. If you setup breakpoints along the lines of:
0:009> bl
0 e x86 00000000`768716af 0001 (0001) 0:**** KERNEL32!CreateFileW
1 e x86 00000000`5af0a590 0001 (0001) 0:**** clr!PEFile::PEFile
2 e x86 00000000`5ae50326 0001 (0001) 0:**** clr!PEFile::IsDll
3 e x86 00000000`770db2c4 0001 (0001) 0:**** msvcrt!fopen
You’ll see that soon after clrjit.dll is loaded hits to CreateFileW and the PEFile class cease. This is the case even when we know our program is opening files off of the file system using the Assembly.Load method. Anyway enough of that rabbit hole for a moment; we’ll dig into this in more in part 2 as setting up WinDbg to do the debugging is a post in of itself.
Anyway before we wrap up let’s look a little bit at the .NET PE parsing code. We can see that Microsoft has put some defensive thought into it:
- They’ve run PREfix across the code.
- There are specific checks in the case of managed DLLs that they only import from mscoree.dll and it only has a single import (PEDecoder::CheckILOnlyImportDlls).
- The allowed bitmap for managed DLLs PE directory entries are IMPORT, RESOURCE, SECURITY, BASERELOC, DEBUG, IAT AND COMHEADER (PEDecoder::CheckILOnly).
- There are a maximum of two base relocations allowed on IA 64bit managed modules (PEDecoder::CheckILOnlyBaseRelocations) otherwise only one is allowed.
- There are checks that the entry point of the module is a JMP to the relative virtual address (RVA) of the only entry in the IAT (PEDecoder::CheckILOnlyEntryPoint).
So long and the short it is not the massive fun fest I had wished it would be at least on the face of it but still interesting enough.
Closing Thoughts
If nothing else I hope this blog post shows that not all research leads to the vulnerability fountain. During this little bit of research I learnt more than I expected with regards how to the .NET framework is put together under the hood which was kind of cool.
Bonus Feature: A Trivial Fuzzing Rig for .NET Assembly.Load Usage
To do some testing and debugging I wanted a little harness that would load .NET modules as they appeared in a directory. All (not very much) code associated with this can be found here – https://github.com/nccgroup/dotnetpefuzzing.
Published date: 09 December 2013
Written by: Ollie Whitehouse