0% found this document useful (0 votes)
53 views19 pages

Overriding Sealed Methods in C#: Key Takeaways

This document discusses how to override sealed methods in C# by modifying method metadata and handles at runtime. It provides background on how methods are implemented and compiled in .NET, explaining that methods have metadata and handles that can be accessed via reflection. This metadata can then be modified to change where a method's machine code points, allowing the method to effectively be overridden even if it is sealed.

Uploaded by

sovalax
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
53 views19 pages

Overriding Sealed Methods in C#: Key Takeaways

This document discusses how to override sealed methods in C# by modifying method metadata and handles at runtime. It provides background on how methods are implemented and compiled in .NET, explaining that methods have metadata and handles that can be accessed via reflection. This metadata can then be modified to change where a method's machine code points, allowing the method to effectively be overridden even if it is sealed.

Uploaded by

sovalax
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 19

Live

QCon Webinar and Q&A (Oct


San Francisco - Introducing
24-28):MARA: A Reference
Uncover emergingArchitecture
trends andfor Kubernetes
practices Microservices
from (Live Webinar
domain experts.
Sept 29th, 2022)
Save Your Seat

Overriding Sealed Methods in C#


This item in
japanese

Key Takeaways

Methods have runtime metadata that we can examine and modify.


A method handle can be modified to point to a different method.
We can generate machine code in C# and execute it directly.
We can override any method this way, including built-in ones.
We can use this trick to modify wrappers around the WinAPI (or any other wrappers).

A method is a block of code that contains a series of instructions. It can be declared within a class,
structure, or interface by specifying the method signature, which consists of the method’s name,
parameters, the return value, and modifiers like its access level, abstract, or sealed.

A method signature must uniquely determine the method used in a given execution context. Depending
on the context, the return value may be part of the signature (e.g., when determining the compatibility
between a delegate and the method to which it points), or it may be ignored (e.g., when the method is
overloaded). When we call a method, we need to provide both the method name and the method
parameters. We don’t specify the return type in C#, but we need to do it in Intermediate Language (IL).
This way, we can specify which method to call, and the .NET platform takes care of the rest of the process.

A method can also be virtual, which adds support for polymorphic execution via late-binding
mechanism (the method can be redefined in derived classes). This is one of the cornerstones of Object-
Oriented Programming, and it is widely used in C#. However, not all methods can be virtual and support
overriding - static methods, constructors, or operators do not support this mechanism. We can also mark
a method as sealed to stop it from being overridden in subclasses even if it is marked as virtual in the
base class.

There are, however, ways to modify how a sealed method is executing, overriding it to some extent.
Before we get into that, we need first to understand how methods are implemented in the .NET platform.

Method internals
Live Webinarcode
A method’s and Q&A - Introducing
is typically MARA:
compiled A Reference
twice. The C#Architecture for Kubernetes
compiler does Microservices
the first compilation. (Live
This Webinar
process
Sept 29th, 2022)
takes theYour
Save C# source
Seat code as an input and generates intermediate language (IL) code as an output. Later,
the IL code is compiled again, typically at runtime by the Just-In-Time (JIT) compiler. It may also be
compiled before an application gets executed in an Ahead-Of-Time (AOT) mode with ngen or
ReadyToRun (R2R) mechanisms. The second compilation takes the IL code as an input and generates
machine code matching the current hardware (CPU) architecture as an output. The machine code can be
later executed directly by the CPU, with no help from the .NET platform.

To call a method on a machine code level, we need to be aware of multiple things that we can safely
ignore when writing code in C#. Not only do we need to provide a name and parameters to a method, but
we also need to know how to pass values to the method (via registers or stack). Not only that but also who
cleans up the stack after the method finishes (callee or caller), how a value is returned, what’s the order of
parameters (left-to-right or right-to-left), and many more details. When writing in C#, we ignore these
details because the .NET platform takes care of them. However, on a machine code level, we need to
adhere to the binary protocol carefully. Otherwise, we’ll most likely get a segmentation fault or an access
violation.

JIT-compilation is a multi-step process, relying both on the internals of the .NET platform and on the
specifics of the CPU architecture. It must consider multiple aspects of the runtime:

1. How to pass parameters to a method? Depending on the architecture, a different set of registers is
used. In 32-bit architectures, the first two parameters are passed via the ecx and edx registers, and
all the others are passed via stack. In 64-bit architectures, the first four parameters are passed via
the rcx, rdx, r8, and r9 registers, and the others are passed via the stack. This, however, can be
changed at any time and is not guaranteed to remain the same between compiler versions.

2. How is the return value returned? Integer values are returned in the eax register, while floating-
point ones are returned through FP or XMM registers.

3. What is the order of parameters? Whether parameters are passed left-to-right or right-to-left
depends on the architecture and is controlled by the platform.

4. How is the memory for the machine code allocated? Since the machine code is not available when
the application starts, it must be written by the application and stored somewhere. Typically, a new
memory page is allocated and is then marked as executable with either the VirtualProtectEx or
mprotect functions from the operating system.

5. Who removes parameters from the stack? If it’s the caller, we risk significant code duplication
because every time we call a method, we need to remove its parameters from the stack when it
returns. However, if it’s the callee who cleans up, we cannot reliably implement methods with
variadic parameters, like printf (which can accept any number of parameters).

6. Is it worth calling the method? Should it be inlined? Maybe it cannot be inlined because it is either
too big or it uses try-catch, which may change the stack trace.
Live7.Webinar
Can weand Q&A - Introducing
optimize the method?MARA: A Reference
Precalculate Architecture
constants, for Kubernetes
remove dead code,Microservices (Live Webinar
reorder instructions?
Sept 29th, 2022)
Save Your Seat
8. What’s the endianness? How do we encode instructions and addresses?

We typically don’t need to think about these aspects when writing a C# code. They only become
important once we start calling methods from other platforms with the P/Invoke mechanism.

Starting with .NET Core 2.1, a method may be JIT-compiled multiple times due to the multi-tiered
compilation mechanism. The first compilation is rough and dirty. It generates non-optimized machine
code. The second compilation may happen after some time (e.g., when the .NET platform observes that
the method is on a hot path and is executed often). The compiler then spends more time in the
compilation and produces optimized code. This may result in using fewer registers, removing dead code,
or precalculating values and using constants. Multi-tiered compilation is enabled by default starting from
.NET Core 3. Effectively, a method always has one instance of an IL code, but it may have multiple
instances of a machine code.

Reflection also allows the programmer to query details of a method. It can be used to get the method
name, parameters, return types, and all other specifiers. Reflection uses method descriptors (metadata)
under the hood. Each descriptor is a structure providing a unique method handle (used for calling the
method), holding expensive metadata (like method modifiers), and capturing the runtime state of the
method. By querying the descriptor, we can determine if a method was already JIT-compiled and where
the generated machine code is. We can access the method descriptor via reflection with
Type.GetMethod(), and then access its method handle with the MethodHandle property. 

By examining the method handle, we can access the internal structures of a method. For instance, we can
find the actual pointer specifying where the machine code of the method is. This gives us a way to point a
method to some different code or modify the logic in place, which we can use to override a sealed method.
Let’s say we have a non-virtual, non-static method X, and we want to modify it to call method Y instead.
If X were virtual, we could inherit the method from the base class, override it with Y, and then use the
polymorphic invocation to achieve our goal. However, since X is non-virtual, we need to modify it on a
lower level.

Overriding sealed methods with metadata modification


The first approach we can use for overriding sealed methods is based on metadata modification. We want
to get the metadata for method X, find the pointer for the machine code of the method, and then modify it
to point to some other method with a matching signature. Let’s take the following code as an example:

using System;

using System.Linq;

using System.Numerics;

using System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

Live Webinar and Q&A - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
using
Sept 29th,System.Threading;

2022)

Save Your Seat
namespace OverridingSealedMethodNetCore
{

class Program

static void Main(string[] args)

Console.WriteLine($"Calling StaticString method before hacking:\t{TestCl


HijackMethod(typeof(TestClass), nameof(TestClass.StaticString), typeof(P
Console.WriteLine($"Calling StaticString method after hacking:\t{TestCla

Console.WriteLine();

var instance = new TestClass();

Console.WriteLine($"Calling InstanceString method before hacking:\t{inst


HijackMethod(typeof(TestClass), nameof(TestClass.InstanceString), typeof
Console.WriteLine($"Calling InstanceString method after hacking:\t{insta

Console.WriteLine();

Vector2 v = new Vector2(9.856331f, -2.2437377f);

for (int i = 1; i <= 35; i++)

{
MultiTieredClass.Test(v, i);

Thread.Sleep(100);

Console.WriteLine($"Examine MethodDescriptor: {typeof(MultiTieredClass).


Console.ReadLine();

public static void HijackMethod(Type sourceType, string sourceMethod, Type t


{

// Get methods using reflection

var source = sourceType.GetMethod(sourceMethod);

var target = targetType.GetMethod(targetMethod);

// Prepare methods to get machine code (not needed in this example, thou
RuntimeHelpers.PrepareMethod(source.MethodHandle);

RuntimeHelpers.PrepareMethod(target.MethodHandle);


Webinar and Q&A - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
Live
Sept 29th, 2022)
var sourceMethodDescriptorAddress = source.MethodHandle.Value;

Save Your Seat


var targetMethodMachineCodeAddress = target.MethodHandle.GetFunctionPoin

// Pointer is two pointers from the beginning of the method descriptor

Marshal.WriteIntPtr(sourceMethodDescriptorAddress, 2 * IntPtr.Size, targ


}

[MethodImpl(MethodImplOptions.NoInlining)]

public static string StaticStringHijacked()

return "Static string hijacked";

[MethodImpl(MethodImplOptions.NoInlining)]

public string InstanceStringHijacked()

return "Instance string hijacked";

class TestClass

[MethodImpl(MethodImplOptions.NoInlining)]

public static string StaticString()

return "Static string";

[MethodImpl(MethodImplOptions.NoInlining)]

public string InstanceString()

return "Instance string";

class MultiTieredClass

[MethodImpl(MethodImplOptions.NoInlining)]

public static void Test(Vector2 v, int i)

Live Webinar and v = -Vector2.Normalize(v);

Q&A Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
Sept 29th, 2022)
Console.WriteLine($"Vector iteration {i:0000}:\t{v}\t{TestClass.StaticSt
Save Your Seat
}

In the example above, we have a class named TestClass with two methods: StaticString (line 71) and
InstanceString (line 77). Both of them are non-virtual and return a hard-coded string. Our goal is to
hijack these methods so when calling StaticString the .NET platform executes the
StaticStringHijacked method (line 56). Similarly, when calling InstanceString, we want to get
InstanceStringHijacked (line 62) called.

The Main method proceeds in the following manner: it first calls the StaticString method and prints its
output, then it hijacks it with StaticStringHijacked, and then it calls StaticString again to see if it
was successfully overridden. After that, it does the same for the InstanceString method. The whole
magic happens in the method HijackMethod (line 38).

HijackMethod accepts four parameters. The first two define the source method to be overridden (method
X, or StaticString in our example). The last two parameters define the target method (method Y, or
StaticStringHijacked in our case). To specify a method we need the Type instance holding the method
and the method name. Since this is an example only, we don’t handle situations when there are multiple
methods of the same name with different sets of arguments, but the code above can be easily extended to
that end.

We start by taking method descriptors of the methods (lines 41-42) by calling the regular GetMethod
function from the reflection mechanism:

// Get methods using reflection

var source = sourceType.GetMethod(sourceMethod);

var target = targetType.GetMethod(targetMethod);

Since the methods may not be JIT-compiled yet, we trigger the compilation manually by calling
RuntimeHelpers.PrepareMethod (lines 45-46):

// Prepare methods to get machine code (not needed in this example, though)

RuntimeHelpers.PrepareMethod(source.MethodHandle);

RuntimeHelpers.PrepareMethod(target.MethodHandle);

Live
WithWebinar
that, weand
getQ&A - Introducing
actual MARA:
pointers that weAcan
Reference
modify.Architecture for Kubernetes
The first one Microservices
is the address (Live Webinar
of the internal method
Sept 29th, 2022)
descriptor of the
Save Your Seatsource method. It’s a structure that holds the address of the machine code backing the
method. The address is stored in two pointers from the beginning of the structure (8 bytes from the
beginning in a 32-bit application or 16 bytes from the beginning in a 64-bit application). This depends on
the .NET version as the internal representation may be changed at any time, but it is consistent from
.NET Framework 1 until .NET 5. We get the address of the structure in line 48

var sourceMethodDescriptorAddress = source.MethodHandle.Value;

Next, we get the address of the machine code of the target method. The .NET platform provides a method
named GetFunctionPointer that does exactly that, but we could as well extract this value manually by
getting the internal descriptor address and then reading the pointer, which is 8 or 16 bytes from the
beginning depending on the CPU architecture in use (line 49):

var targetMethodMachineCodeAddress = target.MethodHandle.GetFunctionPointer();

To override the method, we take the pointer and modify it directly in the internal descriptor structure
(line 52):

Marshal.WriteIntPtr(sourceMethodDescriptorAddress, 2 * IntPtr.Size, targetMethodMach

After this modification, we effectively changed the pointer of the StaticString method, so now it points
to the machine code of StatingStringHijacked. When we call the method, it will effectively execute the
latter’s machine code, as we can see in the application output:

Calling StaticString method before hacking: Static string

Calling StaticString method after hacking: Static string hijacked

So the behavior of our program is structured as follows:

Before the method hijacking:

Start calling
StaticString
method
Get the address of the machine code, which points to the actual code of the StaticString method
Execute the
StaticString
code
Live
AfterWebinar and Q&A
the method - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
hijacking:
Sept 29th, 2022)
Save Your Seat
Start calling
StaticString
method
Get the address of the machine code, which points to the code of the
StaticStringHijacked
method
Execute the
StaticStringHijacked
code

The same structure applies to the InstanceString method which we hijack afterwards. This hijacking
method works in .NET 5.0.102 in Windows 10 x64 and in .NET 5.0.401 in WSL2 Ubuntu 20.04. It works
for both Debug and Release configurations, for both x86 and x64.

However, it is not 100% bulletproof and may not work reliably. This is because .NET introduced a code
cache infrastructure for multi-tiered compilation, which is prone to the time effect. In other words, it may
be that the hijacked method will be picked up only “shortly after” we actually change pointers and the
first couple calls., which still point to the old method. To observe this effect, we will examine the second
part of the code.

We have a method named Test in line 86. It takes a two-dimensional vector as an argument, normalizes
it, and prints its value. The details of this mathematical operation are not important here. What is
important is the fact that the Vector2.Normalize() method may be highly optimized to use SSE
instructions, which we will observe due to multi-tiered compilation. In line 89, we print the following:

Console.WriteLine($"Vector iteration {i:0000}:\t{v}\t{TestClass.StaticString()}");

So we print the iteration number, normalized vector, and we call the StaticString method. We call the
method Test from the Main method multiple times, as seen in line 27:

Vector2 v = new Vector2(9.856331f, -2.2437377f);

for (int i = 1; i <= 35; i++)

MultiTieredClass.Test(v, i);

Thread.Sleep(100);

As the initial output of this code fragment we may obtain:

Vector iteration 0001: <0.9750545, -0.22196561> Static string

Vector iteration 0002: <0.9750545, -0.22196561> Static string

Live Webinar
Vector and Q&A -0003:
iteration Introducing MARA: A Reference
<0.9750545, Architecture for Kubernetes
-0.22196561> StaticMicroservices
string
(Live Webinar
Sept 29th, 2022)
Vector iteration
Save Your Seat 0004: <0.9750545, -0.22196561> Static string

Vector iteration 0005: <0.9750545, -0.22196561> Static string hijacked

We can see that even though we already hijacked the StaticString method to point to
StaticStringHijacked, the first iterations still call the regular code (not the hijacked one). However,
after half a second, we see that the output has changed. This is the code caching effect in practice.

However, things get even more interesting as we continue. Around the 35th iteration, a multi-tiered
compilation kicks in and recompiles the method. The output we get is:

Vector iteration 0034: <0.9750545, -0.22196561> Static string hijacked

Vector iteration 0035: <0.97505456, -0.22196563> Static string

Two important things happened here. First, the output of the Vector2.Normalize() method changed.
Previously it was printing 0.9750545, and now it returned 0.97505456 (note the additional 6 at the end of
the value). This shows that the code under the hood was recompiled and actually changed the value. The
reason is that the optimized version uses SSE instructions which provide higher precision. You can read
more about this behavior here. 

The second important observation is that in iteration 35 a regular StaticString method was called. This
is the effect of method inlining. If we debug the code and observe internal structures, we can see the
following:

Method Name: OverridingSealedMethodNetCore.MultiTieredClass.Test(System.Num


Class: 00007ffa38474978

MethodTable: 00007ffa38464d48

mdToken: 0000000006000009

Module: 00007ffa3843f888

IsJitted: yes

Current CodeAddr: 00007ffa383aded0

Version History:

ILCodeVersion: 0000000000000000

ReJIT ID: 0

IL Addr: 0000000000000000

CodeAddr: 00007ffa383aded0 (OptimizedTier1)

NativeCodeVersion: 0000018E3EF1D140

CodeAddr: 00007ffa383a7ae0 (QuickJitted)

NativeCodeVersion: 0000000000000000

Live
So weWebinar
can seeand Q&Aare
there - Introducing MARA:
two instances of Athe
Reference
machine Architecture
code. The for Kubernetes
first instance Microservices (Live Webinar
(labeled QuickJitted) calls
Sept 29th, 2022)
the method,
Save Yourbut
Seatthe second instance (labeled OptimizedTier1) inlines the string literal. Effectively neither
StaticString nor StaticStringHijacked is called.

This technique will not work for all scenarios. It may not work for methods compiled in the AOT manner.
It may not support all methods from the standard library as method descriptors differ for them. As we
can see, it may break due to multi-tiered compilation or code inlining.

Pros:

This technique doesn’t require an understanding of the machine code


it doesn’t destroy the original machine code

Cons:

It may not be reliable as it is prone to the time effect


Results may be reversed due to multi-tiered compilation
Not all methods can be modified this way
Inlining breaks this technique

Overriding sealed methods with machine code modification


The second technique we’ll use does not modify the runtime metadata. This time we will modify the
method’s machine code directly, making it jump to another place. We’ll find the machine code of the
source method and then modify it on a binary level to execute a jump instruction to move to the target
method.

To generate the machine code, first we need to understand how the jump instruction works. In x86
architectures, it uses one value as a parameter (4 or 8 bytes long), which is a numerical offset of how far
to move in the memory address (literally jumping to another place). Since it uses an offset instead of an
absolute memory address, it’s slightly harder to use as we need to calculate the distance (offset) to jump.
However, we can use a trick to move to an absolute address. In 32-bit mode, we can push the address into
the stack and then execute the return instruction, which takes the address from the stack and moves to it.
In 64-bit architectures, we can’t push the address directly (as there is no instruction to push 8 bytes into
the stack), so we first move the address to the register and then push the register on the stack.

We want to generate this code and put it at the beginning of the StaticString method to execute it.
Effectively, we’ll always execute the source method and only then jump to the target.

Let’s take the following code as an example:

using System;

using System.Linq;

using System.Numerics;

Live Webinar and Q&A - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
using
Sept 29th,System.Reflection;

2022)
Save Your
using Seat
System.Runtime.CompilerServices;

using System.Runtime.InteropServices;

using System.Threading;

namespace MethodHijackerNetCore

public class Program

static void Main(string[] args)

Console.WriteLine($"Calling StaticString method before hacking:\t{TestCl


HijackMethod(typeof(TestClass), nameof(TestClass.StaticString), typeof(P
Console.WriteLine($"Calling StaticString method after hacking:\t{TestCla

Console.WriteLine();

var instance = new TestClass();

Console.WriteLine($"Calling InstanceString method before hacking:\t{inst


HijackMethod(typeof(TestClass), nameof(TestClass.InstanceString), typeof
Console.WriteLine($"Calling InstanceString method after hacking:\t{insta

Console.WriteLine();

Vector2 v = new Vector2(9.856331f, -2.2437377f);

for (int i = 1; i <= 35 ; i++)

{
MultiTieredClass.Test(v, i);

Thread.Sleep(100);

Console.WriteLine($"Examine MethodDescriptor: {typeof(MultiTieredClass).


Console.ReadLine();

[MethodImpl(MethodImplOptions.NoInlining)]

public static string StaticStringHijacked()

return "Static string hijacked";

Live Webinar[MethodImpl(MethodImplOptions.NoInlining)]

and Q&A - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
Sept 29th, 2022)
public
Save Your Seat string InstanceStringHijacked()

return "Instance string hijacked";

public static void HijackMethod(Type sourceType, string sourceMethod, Type t


{

var source = sourceType.GetMethod(sourceMethod);

var target = targetType.GetMethod(targetMethod);

HijackMethod(source, target);

public static void HijackMethod(MethodBase source, MethodBase target)

RuntimeHelpers.PrepareMethod(source.MethodHandle);

RuntimeHelpers.PrepareMethod(target.MethodHandle);

var offset = 2 * IntPtr.Size;

IntPtr sourceAddress = Marshal.ReadIntPtr(source.MethodHandle.Value, off


IntPtr targetAddress = Marshal.ReadIntPtr(target.MethodHandle.Value, off

var is32Bit = IntPtr.Size == 4;

byte[] instruction;

if (is32Bit)

{
instruction = new byte[] {

0x68, // push <value>

.Concat(BitConverter.GetBytes((int)targetAddress))

.Concat(new byte[] {

0xC3 //ret

}).ToArray();

}
else

{
instruction = new byte[] {

0x48, 0xB8 // mov rax <value>

Live Webinar and Q&A -}

Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar


Sept 29th, 2022)
Save Your Seat .Concat(BitConverter.GetBytes((long)targetAddress))

.Concat(new byte[] {

0x50, // push rax

0xC3 // ret

}).ToArray();

Marshal.Copy(instruction, 0, sourceAddress, instruction.Length);

class TestClass

[MethodImpl(MethodImplOptions.NoInlining)]

public static string StaticString()

return "Static string";

[MethodImpl(MethodImplOptions.NoInlining)]

public string InstanceString()

return "Instance string";

class MultiTieredClass

[MethodImpl(MethodImplOptions.NoInlining)]

public static void Test(Vector2 v, int i)

v = Vector2.Normalize(v);

Console.WriteLine($"Vector iteration {i:0000}:\t{v}\t{TestClass.StaticSt


}

The important part happens in HijackMethod starting in line 59. We first compile both the source and
the target methods to generate the machine code.
Live Webinar
We get and Q&Acode
the machine - Introducing
addressMARA:
of bothA source
Reference
andArchitecture for Kubernetes
target methods (line 65).Microservices (Live
To do that, we Webinar
read the
Sept 29th, 2022)
address
Save from the method descriptor:
Your Seat

var offset = 2 * IntPtr.Size;

IntPtr sourceAddress = Marshal.ReadIntPtr(source.MethodHandle.Value, offset);

IntPtr targetAddress = Marshal.ReadIntPtr(target.MethodHandle.Value, offset);

If we run this on a 32-bit platform, the code pushes the address to the stack and returns. This is what
happens in lines 74-80. You can see that 0x68 is the number of the instruction for pushing the value onto
the stack. After that, we cast the address to integer (which is 4 bytes long, but we know that we are on a
32-bit platform) and convert it to bytes. The last instruction is 0xC3, which takes the address from the
stack, removes it and jumps to it.

instruction = new byte[] {

0x68, // push <value>

.Concat(BitConverter.GetBytes((int)targetAddress))

.Concat(new byte[] {

0xC3 //ret

}).ToArray();

On a 64-bit platform, we can’t push the value directly. We first need to move it to the rax register, then
push the register into the stack and return. This is done in lines 84-92. Notice that this time we cast the
address to a long instead of an integer (since on 64-bit platforms the addresses are 8-bytes long):

instruction = new byte[] {

0x68, // push <value>

.Concat(BitConverter.GetBytes((int)targetAddress))

.Concat(new byte[] {

0xC3 //ret

}).ToArray();

Finally, we copy the code to the beginning of the source method’s machine code.

Marshal.Copy(instruction, 0, sourceAddress, instruction.Length);

So the behavior of our program with this technique is as follows:

Before the hijacking:


Live Webinar and Q&A - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
Start calling
Sept 29th, 2022)
StaticString
Save Your Seat
method
Get the address of the machine code which points to the actual code of the
StaticString
method
Execute the
StaticString
code
When
StaticString
finishes, return to the caller

After the hijacking:

Start calling
StaticString
method
Get the address of the machine code which points to the same code address as before hijacking
Execute the
StaticString
code
The first part of the
StaticString
is a jump to the StaticStringHijacked, so we jump to the other method
Execute the
StaticStringHijacked
code
When
StaticStringHijacked
finishes, return to the caller directly (because the return address on the stack is the one used when
calling StaticString)

This method is not prone to the time effect as we don’t modify the metadata (we only modify the code
that gets executed). However, it is still prone to the inlining, as we can see with multi-tiered compilation
kicking in and inlining it directly:

Vector iteration 0033: <0.9750545, -0.22196561> Static string hijacked

Vector iteration 0034: <0.97505456, -0.22196563> Static string

Live
Also,Webinar and Q&Aworks
this technique - Introducing
for anyMARA: A Reference
method Architecture
as long as we can getfor
theKubernetes
address ofMicroservices
the machine(Live Webinar
code. It may
Sept 29th, 2022)
be harder for Seat
Save Your AOT-compiled methods (as the address is not stored directly in the 8th/16th byte of the
method descriptor) or external native code called with P/Invoke (as the code may not be writable and
we’ll need to call VirtualProtectEx or mprotect to modify it) but conceptually it works for all the cases.

Pros:

It is not prone to the time effect


It works for all methods.

Cons:

It destroys the original machine code


Results may be reversed due to multi-tiered compilation
Inlining breaks this technique
It requires a better understanding of the machine code and operating system

Practical application - modifying the wrapper for WinAPI


process creation
There are multiple situations when we can use method hijacking to provide some business values.
Examples from my experience (deployed into production) include:

Handling
StackOverflowException
via Vectored Exception Handling (VEH) mechanism to avoid stopping the test suite from being
killed
Injecting
try-catch
blocks for new thread creation to avoid getting the process terminated due to an unhandled
exception
Modifying the WinAPI wrapper to be able to execute the process in a different desktop. 

Let’s examine the last example in detail.

Windows supports multiple desktops to isolate applications. This mechanism has been in place for over
20 years, but it was never exposed in the UI. There is an application called Desktops that allows us to
control multiple desktops and switch between them. This may be useful when we need to automate
applications that capture user input or steal focus (e.g., automated UI tests with Puppeteer in a headful
mode).

In order to run an application on a different desktop, we need to specify the lpDesktop field in the
STARTUPINFO structure. 
Live Webinar
However, in and
C#,Q&A - Introducing
we don’t call thisMARA: A Reference
API directly: Architecture
we use for Kubernetes
the wrapping Microservices
code provided (Live Webinar
by the standard
Sept 29th, 2022)
library.
SaveUnfortunately,
Your Seat if we examine the .NET Framework code, it doesn’t allow us to set the value of
IpDesktop, and always initializes it to a null value. 

If we want to run the application on a different desktop in C#, there are a few solutions:

We can call the WinAPI directly, but then we lose the support of the .NET API and need to control
processes on our own (including marshaling and input/output redirection)
We can copy the wrapping code on the side and modify it, but then we need to maintain it and keep
updated when it changes in the standard library.
We can modify the code directly in place to inject the
lpDesktop
value. To do that, we can override sealed methods with the technique described earlier.

In order to hijack the code, we need to find a way to inject some of our code before the STARTUPINFO
structure gets passed to the WinAPI but after it is created. We can use the constructor for that. 

First get the method descriptor of the constructor:

var matchingType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetType


var constructor = matchingType.GetConstructor(new Type[0]);

var newConstructor = typeof(Program).GetMethod(nameof(NewConstructor), BindingFlags.

Now, we hijack it with the following constructor replacement:

public static void NewConstructor(object startupInfo)

startupInfo.GetType().GetField("cb", BindingFlags.Instance | BindingFlags.Public


startupInfo.GetType().GetField("lpDesktop", BindingFlags.Instance | BindingFlags
}

In the code above,the original constructor sets the cb field to the correct value directly. Then we provide a
new constructor, which sets the cb field via reflection and also sets the lpDesktop field to the name of the
desktop we want to use.

So the code after hijacking works in the following way:

We call the
Process.Start()
method
Live Webinar and Q&A - Introducing MARA: A Reference Architecture for Kubernetes Microservices (Live Webinar
Process.Start()
Sept 29th, 2022)
creates
Save Youran instance of the STARTUPINFO structure 
Seat
Instead of a regular constructor being called, our custom one gets executed, and we set the field
values via reflection

I have been using this technique with .NET Framework and Windows Server 2012/2016 for many years.

Summary
We can see that by getting our hands on the internal structures, we can change the behavior of the
platform. It requires an understanding of code generation, Operating System mechanisms and internals
of the .NET platform. However, at the end of the day, these are just bytes that we can modify to suit our
needs.

About the author


Adam Furmanek is a professional software engineer with over a decade of experience. In
his career he worked with all layers of software engineering and multiple types of
applications, including logistics, e-commerce, machine learning, data analysis and database
management. He is always interested in digging deeper, exploring machine code and going
through implementation details to better understand the internals of the technologies he
uses every day. That's why he likes debugging, decompiling and disassembling the code to understand
memory models, concurrency problems and other details hidden deeply inside. In his free time he plays
ping-pong, watches Woody Allen's movies and blogs stuff.

Inspired by this content? Write for InfoQ.


Becoming an editor for InfoQ was one of the best decisions of my career. It has challenged me and
helped me grow in so many ways. We'd love to have more people join our team.

Thomas Betts
Lead Editor, Software Architecture and Design @InfoQ; Senior Principal Engineer
Write for InfoQ

Discuss

Please see https://www.infoq.com for the latest version of this information.

You might also like