Analyzing .NET Assembly Injection and Reflection Attacks
In the ecosystem of the Common Language Runtime (CLR), the boundary between code and data is often thinner than developers realize. While static analysis tools excel at auditing source code, they frequently struggle to account for the dynamic nature of the .NET runtime. Two of the most potent techniques used by sophisticated actors to bypass security controls and execute arbitrary logic are Assembly Injection and Reflection-based manipulation.
When these two techniques are combined, the result is a highly flexible, "fileless" execution primitive that allows an attacker to bypass traditional perimeter defenses, evade signature-based detection, and manipulate the internal state of a running process.
The Mechanics of Assembly Injection
Assembly injection is the process of introducing new, unauthorized Intermediate Language (IL) code into a running .NET process. Unlike traditional DLL injection, which often relies on unmanaged Windows APIs like `CreateRemoteThread` and `LoadLibrary`, .NET assembly injection can occur entirely within the managed layer.
The primary vehicle for this is the `System.Reflection.Assembly` class. The CLR provides several methods for loading assemblies that do not require the assembly to exist as a discrete file on the disk:
- `Assembly.Load(byte[])`: This is the most dangerous method in a security context. It allows an attacker to pass a raw byte array-representing a fully formed PE (Portable Executable) file-directly into the runtime. Because the assembly is loaded from a memory buffer, there is no "file creation" event for EDR (Endpoint Detection and Response) solutions to intercept.
- `Assembly.Load(AssemblyName)`: While this typically points to existing files, if an attacker can influence the `AssemblyResolve` event, they can redirect the runtime to load a malicious assembly from an unexpected location or an embedded resource.
By using `Assembly.Load(byte[])`, an attacker can deliver a payload via an encrypted network stream or an obfuscated resource, effectively achieving "fileless" execution. The malicious code is then resident only in the process's private memory space.
Reflection: The Execution Engine
If assembly injection is the delivery mechanism, Reflection is the execution engine. Once a malicious assembly is loaded into the `AppDomain`, it exists as a collection of types, methods, and fields within the process's metadata. However, the attacker may not have a direct, statically-typed reference to this new assembly in their primary exploit code.
Reflection allows the attacker to traverse the metadata of the loaded assembly to find and invoke specific logic. This is achieved through the `System.Type` and `System.Reflection.MethodInfo` classes.
Breaking Encapsulation
The most critical capability of Reflection in an attack scenario is the ability to bypass access modifiers (`private`, `protected`, `internal`). By utilizing specific `BindingFlags`, an attacker can access the "hidden" internals of the application.
Consider the following implementation pattern used to invoke a private method within a loaded, injected assembly:
```csharp
// Assume 'payloadBytes' is the malicious assembly loaded via Assembly.Load(byte[])
Assembly maliciousAssembly = Assembly.Load(payloadBytes);
// Locate the target type within the injected assembly
Type targetType = malicious-assembly.GetType("MaliciousNamespace.PayloadCore");
// Create an instance of the type
object payloadInstance = Activator.CreateInstance(targetType);
// Use Reflection to find a private method
MethodInfo privateMethod = targetType.GetMethod("ExecuteMaliciousLogic",
BindingFlags.NonPublic | BindingFlags.Instance);
// Invoke the method, bypassing all visibility constraints
privateMethod.Invoke(payloadInstance, null);
```
In this snippet, the `BindingFlags.NonPublic` flag acts as a skeleton key, rendering the `private` modifier irrelevant. This allows an attacker to interact with sensitive objects-such as database connection strings, encryption keys, or authentication providers-that were never intended to be exposed.
The Exploit Pattern: Deserialization as a Catalyst
Assembly injection and reflection rarely happen in a vacuum. They are typically the payload of a larger vulnerability, most commonly Insecure Deserialization.
When an application uses a deserializer (like `BinaryFormatter`, `NetDataContractSerializer`, or `Json.NET` with `TypeNameHandling` enabled) to process untrusted input, an attacker can craft a serialized object graph that instructs the runtime to instantiate specific types.
The attack flow typically follows this sequence:
- Payload Preparation: The attacker compiles a malicious DLL containing the payload and converts it to a Base64 string.
- Injection Trigger: The attacker sends a serialized payload to the application. This payload contains instructions to a gadget chain (a sequence of existing, legitimate code) that eventually calls `Assembly.Load(byte[])`.
- Runtime Execution: The deserializer reconstructs the object, the CLR loads the byte array, and Reflection is used to trigger the malicious logic.
Operational Considerations and Risks
Performance Overheads
From a legitimate development perspective, heavy use of Reflection introduces significant latency. Every time `GetMethod` or `Invoke` is called, the runtime must perform metadata lookups and security checks. In high-throughput systems, this can lead to measurable CPU spikes and increased GC (Garbage Collection) pressure due to the allocation of `MethodInfo` and `object[]` objects.
The Complexity Trap
Relying on Reflection for "plugin" architectures or "extensible" frameworks introduces significant maintenance debt. Changes to the internal structure of a class (e.g., renaming a private field) will silently break the Reflection logic, often leading to runtime exceptions that are difficult to debug without deep visibility into the IL.
Security Trade-offs
While Reflection is a powerful tool for unit testing and ORMs (Object-Relational Mappers), it expands
Conclusion
As shown across "The Mechanics of Assembly Injection", "Reflection: The Execution Engine", "The Exploit Pattern: Deserialization as a Catalyst", a secure implementation for analyzing .net assembly injection and reflection attacks depends on execution discipline as much as design.
The practical hardening path is to enforce host hardening baselines with tamper-resistant telemetry, behavior-chain detection across process, memory, identity, and network telemetry, and unsafe-state reduction via parser hardening, fuzzing, and exploitability triage. This combination reduces both exploitability and attacker dwell time by forcing failures across multiple independent control layers.
Operational confidence should be measured, not assumed: track time from suspicious execution chain to host containment and reduction in reachable unsafe states under fuzzed malformed input, then use those results to tune preventive policy, detection fidelity, and response runbooks on a fixed review cadence.