Reverse Engineering for Load
Let’s begin the process of locating a Load call inside
System.Workflow. ComponentModel.dll.
Since we are reverse-engineering managed code,
we’ll need a new tool.
We’ll use dnSpy which is the tool of choice for disassembling
& performing reverse engineering on compiled .NET code.
This tool has been installed on the Windows 10 victim machine,
and a shortcut has been placed on the taskbar.
Thanks to application whitelisting,
we must launch dnSpy as an administrative user.
After launching dnSpy we’ll navigate to File ->
Open,
browse to the target assembly
and select Open.
This will load System.Workflow. ComponentModel.dll,
automatically decompile it and add it to the Assembly Explorer.
Based on its name alone, the
System.Workflow.ComponentModel. Compiler namespace
is worth investigating since compilation
often involves loading a file or data.
Expanding the namespace reveals the WorkflowCompiler class
which contains the Compile method.
Based on the class and method name, this seems a good starting
point for our analysis as we are trying to leverage existing
functionalities within the code base to compile our own C#
source code and load it into memory.
There are multiple steps
we have to perform as part of this analysis.
First we will begin by determining if the Compile
method does indeed lead to compilation of source code.
If so, we must ensure that we are able to invoke this function
and supply the source code.
Finally, we must determine if and how the code is executed.
The code begins with various argument checks,
and eventually executes these statements
Line 57 calls into the internal Compile method in the
WorkflowCompilerInternal namespace.
If we click on the method name, dnSpy will jump to that code
and display it.
The initial instructions validate the arguments.
Futher down in the code, we find the following instructions
The GenerateLocalAssembly and InternalCompileFromDomBatch
methods are especially interesting given that we're
searching for a code segment
responsible for compiling managed code.
Let’s start with GenerateLocalAssembly
and follow it with dnSpy.
Eventually we reach the following code
Based on the name alone, the CompileAssemblyFromFile method
from the CodeDomProvider namespace is worth investigating
Following the call to this method, reveals that this is a
small wrapper method for CompileAssemblyFromFileBatch:
At this point, we are quite deep into the code.
Let’s take a moment to investigate this method
to validate that we’re on the right track.
MSDN confirms that CompileAssemblyFromFileBatch
does indeed compile an assembly, as the name suggests.
Good. We’re headed in the right direction.
Let’s continue.
If we follow the call into CompileAssemblyFromFileBatch,
we only find the method definition.
There is no function implementation because
this is part of an interface.
To find the implementation of the interface from the
System.CodeDom.Compiler namespace,
we can right-click the method name and select Analyze,
or press ctrl + shift + r,
which will open a pane
in the lower right-hand side of the application.
We can expand this by clicking Implemented By.
Now we can select the second entry to find the
implementation of CompileAssemblyFromFileBatch.
The method validates the supplied file paths
and then calls FromFileBatch.
Within FromFileBatch, we find a call to the Compile method,
where the C# code supplied through fileNames
is finally compiled:
Further on after the source code has been compiled and stored
in the array variable,
we locate the code we have been searching for:
The code shown loads the now-compiled assembly
with Assembly.Load.
Very nice.
The only caveat is that this call is only triggered
if the file integrity property is not enabled.
Let’s investigate FileIntegrity
to determine if it will be enabled in our scenario.
Matt Graeber writes on his blog that when the
FileIntegrity.IsEnabled property is evaluated,
a call is made to WldpIsDynamicCodePolicyEnabled,
which will only return true if WDAC is enabled
and certain specific properties are enforced.
Since we are only dealing with AppLocker,
this does not apply to us, and this code path will execute.
At this point, we have made significant progress reverse
engineering the
System.Workflow.Component Model.dll assembly.
We've found a code path that compiles and loads C# code
based on given input files.
Before we go any further with the analysis we must ensure that
we're actually able to invoke the Compile method from the
WorkflowCompiler class inside
System.Workflow.Component Model.dll.
Additionally we must determine if we are able to control
the arguments provided to it.
To find an executable that invokes the Compile method,
we’ll navigate back to it in dnSpy.
Next, we’ll right-click the method name
and select Analyze.
Next, we’ll expand the “Used By” section,
which reveals two entries including
Microsoft.Workflow. Compiler.Program.Main.
This is the Main method of an executable,
which means the Compile method
is called directly from this .NET application.
Double-clicking on the entry will open the relevant assembly
in dnSpy which will automatically present its code.
Additionally, the Assembly Explorer neatly displays the
application name as Microsoft.Workflow.Compiler:
To locate this file on disk we can right-click on the assembly
in Assembly Explorer and choose “Open Containing Folder”,
which opens the directory in File Explorer.
Alternatively, we could hover over the assembly name in
Assembly Explorer to display the path name.
At this point we have found a way to trigger the Compile
method directly from a native and signed Microsoft application
We must also determine if the arguments supplied to Compile
come directly from Microsoft.Workflow.Compiler.
We'll start this analysis from the Main method of
Microsoft.Workflow.Compiler.exe
and inspect the arguments it accepts.
This reveals that two arguments must be passed,
and that only the first is used with the Compile method.
The contents of the first argument are parsed by the
ReadCompilerInput method,
which returns an object
that contains compiler parameters and file names.
The information discovered here is very enlightening.
It tells us that any input to Compile
should be under user control.
However the input does go through some sort of validation
via ReadCompilerInput.
If we follow ReadCompilerInput, we find that the content of the
file passed as an argument is read into a stream,
after which it is used to create an XmlReader stream.
This shows us that we must supply an XML file
as the first argument to Microsoft.Workflow.Compiler.
After the XmlReader stream is created, the _ReadObject method
is used to deserialize the data of the stream and return it as
the CompilerInput type,
which is a custom type defined inside the
Microsoft.Workflow.Compiler.
If we click on the type,
we find that it contains only two elements:
parameters and files.
At this point, it seems that we should be able to pass a file
of our own choosing
to Microsoft.Workflow.Compiler as the first argument.
However the file must contain serialized XML data.
While it is not yet clear what the content of the file should
be and how the deserialization works, we have found that we
should be able to trigger execution
of Compile with arguments under our control.
There is still much work left to do in this analysis.
Simply compiling C# code and loading an assembly into memory
is not enough to acquire code execution.
In the next section, we must continue our reverse engineering
to determine whether or not the newly-compiled assembly
is actually executed.