Reflecting on Reflection: Microsoft's Greatest Feature

.NET Tradecraft: Welcome to the 21st Century

A lot of the offensive security industry started to move away from Powershell a few years ago after Microsoft added additional security and logging features into new versions of Powershell. These features gave defenders a great deal of visibility into Powershell, making it risky for attackers to use the language on mature organizations. Since C# and Powershell are both are .NET languages, C# was the offensive community's solution to the problems presented by Powershell's increased security. The offensive security community has embraced C# as an exploitation and post-exploitation language, replacing much of the beloved Powershell tradecraft pentesters relied on.

C# provides developers with high-level language features like automatic memory management, objects, and a wealth of libraries while still giving programmers the power to use the Win 32 API or access memory manually. If you haven't integrated tools written in C# into your pentesting toolkit, you are sorely missing out.

What is an Assembly?

To quote Microsoft,

An assembly is a collection of types and resources that are built to work together and form a logical unit of functionality. Assemblies take the form of executable (.exe) or dynamic link library (.dll) files, and are the building blocks of .NET applications.

What does that actually mean? An assembly is compiled .NET code that can be executed by the .NET interpreter. .NET assemblies are compiled into Intermediate Language (IL). This Intermediate Language is a sort of half-compiled bytecode that cannot be directly executed by the CPU yet.

When .NET assemblies are executed, the IL is Just-in-Time compiled into machine code. The Common Language Runtime (CLR) is the interpreter and execution environment that performs the translation between IL instructions and CPU instructions.

Execution flow from C# Source code to execution of CPU instructions
Execution flow from C# Source code to execution of CPU instructions

Reflection for Dummies

Reflection is a special feature built into .NET languages that allows a programmer to import assemblies into their .NET program, altering program functionality during runtime. For the purposes of this blog, there are two very important things about Reflection:

  • Reflection is a legitimate feature built into the .NET language often used by legitimate programs.
  • Reflection allows loading assemblies from arbitrary binary data in memory.

Let's make a simple C# Hello World program that can be loaded reflectively. For those following along at home, these programs were written and compiled in Visual Studio as .NET Framework Console Apps.

The only thing special about this program are the public access modifiers before the class name, Program and the Main method. This allows Main to be executed after the assembly is loaded reflectively.

Use Visual Studio to compile the Hello World program. Copy and paste the EXE path into a command prompt to make sure everything is working correctly.

Compiling and running the Hello World program
Compiling and running the Hello World program

We get the expected output and are ready to create a reflective loader for our Hello World assembly. Using the base64 linux command, convert the contents of HelloWorld.exe into a base64 encoded string.

Base64 encoding the Hello World assembly
Base64 encoding the Hello World assembly

We can use this base64 encoded string to embed the Hello World assembly directly into our reflective loader program below.

This simple loader only has 4 steps:

  1. Store the Hello World binary as a base64 encoded string.
  2. Decode the base64 string into a byte array.
  3. Load the Hello World assembly into our current program using System.Reflection.Assembly.Load() with our byte array.
  4. Execute the Main function of the Hello World program by doing assembly.EntryPoint.Invoke().

As expected, this loader has the same output as the Hello World program, since all it does is load and invoke that assembly.

Compiling and running the assembly loader code above
Compiling and running the assembly loader code above

What about Arguments?

Loading this Hello World program is great, but it would be a lot more useful if we could specify command line arguments for the loaded assembly. Luckily, the Invoke() function allows passing parameters to the invoked function.

I have created a simple example to demonstrate command line arguments.

When testing the Arg Echoer program we can see command line arguments are used inside the program and change the output depending on what the user specifies.

Testing the Arg Echoer code above
Testing the Arg Echoer code above

Arguments can be passed into an assembly based on the second parameter of Invoke(). We use the variable string[] args, which stores command line arguments to our program, and pass this variable through to the loaded assembly.

Compile the Arg_Echoer.cs script and base64 encode the resulting assembly. Simply swap out the Base64 encoded assembly in Reflection_Practice.cs for this assembly. Additionally, change the value new object[] { null } to new object[] { args }.

With these modifications, our reflective loader script should look like the following:

When we compile and run the above script, we see no difference in output compared to the Arg_Echoer script. Arg_Echoer is loaded reflectively, then command line arguments are passed through to Arg_Echoer's Main() function when it is invoked.

Running the script above and viewing its output
Running the script above and viewing its output

A note about arguments: the function signature you invoke reflectively must be compatible with the parameters passed in. This means if your invoked function is declared as Main(string[] args) and you try to pass a character array (char[] args) as a function parameter, your program will crash. Almost all C# programs are written with string[] args as input to the Main function, so when in doubt, pass a string array into the function.

Saving assembly output and moreĀ 

The building blocks for creating powerful C# malware are already in place above, but I want to end with a more practical example of how reflection can be used. I wrote up the script below to perform some reconnaissance on Windows.

Instead of base64 encoding the resulting assembly as before, host the EXE file on a web server. This can be done easily with python: python3 -m http.server 80. I also renamed the compiled Recon.exe assembly to assembly.exe.

Hosting the assembly file on a web server using Python HTTP server
Hosting the assembly file on a web server using Python HTTP server

Instead of loading assemblies embedded in base64 strings, the built-in .NET HttpClient allows us to load assemblies directly from the internet. The following code is build to download assemblies from a web server and reflectively execute them.

The following is output from the code above:

Output from the script: Execute_Assembly_From_Http_Server.cs
Output from the script: Execute_Assembly_From_Http_Server.cs

Additionally, the script sends execution results back to the web server after execution of the assembly completes.

Viewing web logs from the Python HTTP server
Viewing web logs from the Python HTTP server

The second GET request (shown above) contains the base64 encoded results. We can base64 decode this string to see the results from our web server:

Decoding and viewing the execution results of our loaded assembly
Decoding and viewing the execution results of our loaded assembly

With only a few modifications the code above could be used to make a homebrew RAT. Ignore the haters. C# is a great language for writing malware, and reflection is just the cherry on top.

This article was updated on July 11, 2022