Confusing .NET Debuggers: Proxy Objects
.NET decompilers and debuggers have become very good at helping reverse engineers figure out the inner workings of a program. However, they also make a lot of assumptions that can be used against them. In this post, we will explore a method that can be used to trick the debugger into hiding a lot of important information during a debugging session.
Don’t spy on people’s personal information!
A proof-of-concept implementation for this project can be found on my GitHub.
The Problem
Let’s say you are writing some code that uses the following model class:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person
{
public Person(string firstName, string lastName, float coolnessFactor)
{
FirstName = firstName;
LastName = lastName;
CoolnessFactor = coolnessFactor;
}
public string FirstName { get; set; }
public string LastName { get; set; }
public float CoolnessFactor { get; set; }
}
The model is very basic: It stores the first and last name of a person, as well as some random statistic indicating how cool this person is.
Let’s now consider this very simple application that you wish to protect from reverse engineers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static Person[] GetPeople() { /* ... some implementation ... */ }
public static void Main(string[] args)
{
foreach (var person in GetPeople())
{
// Print basic information about the person.
Console.WriteLine(string.Format("{0} {1} (Coolness: {2})",
person.FirstName, person.LastName, person.CoolnessFactor));
// Acknowledge the people that are really cool.
if (person.CoolnessFactor > 9000f)
Console.WriteLine(" Wow this person is really cool!");
}
}
If we open our program with a debugger such as dnSpy, everything is visible more or less in plain text:
The locals window is a powerful tool
The screenshot illustrates how powerful .NET debuggers can be.
Not only is the decompiler output very clear, but by stepping through the code we can also see every Person
object being processed in the Locals window, including their fields and properties.
Even if the code was obfuscated and the classes and properties were renamed, it does not take a rocket scientist to know that the two string
properties of the variable holds the first and last name of a person, and that the float
property attaches some statistics to the person.
Our job for this article is to make this a bit more difficult.
How do Debuggers display Information?
An important realization that we are going to (ab)use throughout the entirety of this post, is that debuggers are always trying to be as helpful as possible. In particular, a tool like dnSpy tries its best to show as much of the important information in a human-readable representation, while leaving out many implementation details of an object that usually do not matter.
A prime example of this in action is the List<T>
type.
When we debug an application, typically we do not care how List<T>
is implemented.
We only care about the elements within the list, and we never look at its private fields (such as the internal sizes, indices and capacities within the object).
And indeed, when we try to look up an instance of a List<T>
in the Locals window of dnSpy, we actually do not see this raw structure.
Instead, we see a display string that looks similar to Count = #
, where #
is replaced with the number of elements stored in the list.
Furthermore, in the dropdown, we only see the individual elements stored in the list as if it was a normal array instead:
The visualization of the List<T>
object in a debugger.
Debuggers do this all the time by trying to decide what is considered interesting to show while debugging and what to leave out. However, since debuggers are programs themselves and thus deterministic beasts, there is some structure to how this decision is made.
A structure that we can push to the limits :).
Our goal for this post is to trick the debugger into thinking that all the important bits of information are not important at all, and replace them with data that has nothing to do with the original code.
Proxy Objects
One method that we are going to explore in this post, is introducing so-called proxy types into our code.
A proxy type is a very simple wrapper class or structure that only holds the real object that we wish to hide.
Additionally, it defines two methods Box
and Unbox
that simply construct and deconstruct the proxy object respectively.
In the case of our Person
class, a PersonProxy
class may look like the following:
1
2
3
4
5
6
7
8
9
10
11
12
public sealed class PersonProxy
{
private readonly Person _value;
public Person(Person A_1)
{
_value = A_1;
}
public static PersonProxy Box(Person A_0) => new(A_0);
public static Person Unbox(PersonProxy A_0) => A_0._value;
}
If we construct a proxy type for every type that we use in our code, we can replace every local variable’s type in our original main method body with our proxy type.
To make everything still work as expected, we insert calls to our Box
and Unbox
methods to convert from the original object to the proxy object and back.
In simple terms, this means every assignment in the form T x = <value>;
will be replaced with TProxy x = TProxy.Box(<value>)
, and every use of a variable x
will be replaced with TProxy.Unbox(x)
:
The same code but with proxy types applied. The local variable values are still visible.
This alone is not really helpful yet.
If anything, it looks rather cluttered.
The only thing it did is change the foreach
into a while
loop, and the actual objects that we are trying to hide are still clearly visible in the Locals window.
While these two things are a slight annoyance for a reverse engineer, it is not something that will keep them away for long.
Furthermore, it is rather obvious that we are dealing with wrapper objects, given the Proxy
suffix name in our class, as well as the explicit calls to Box
and Unbox
.
So how does this help us?
Hiding our Tracks
It so turns out that a proxy object like this allows us to do many cool things that both the decompiler and debugger have a very hard time dealing with. There are a few tricks that we can do to hide our proxy types from both the decompiler and debugger display. In the following, we will go over them all in an iterative process.
Implicit operators
Let’s start simple and get rid of the Box
and Unbox
methods.
A nice feature of C## is that it allows for types to define their own custom operators.
There are a variety of operators that we can overload, such as addition (+
) and subtraction (-
), and it is one of the ways to implement non-native algebraic types (such as System.Decimal
, System.Numerics.Vector2
and System.Numerics.Matrix4x4
).
However, C## also supports overloading somewhat lesser-known operators, including conversion operators.
These operators allow programmers to assign values of one type to a variable of another without having to explicitly call a function that does the conversion.
For example, System.Decimal
defines an implicit
conversion operator to allow integers to be directly assigned to variables of type decimal
.
This is not a case where the developers of C## hardcoded this into the compiler, but rather it is something that the System.Decimal
structure declares by itself:
1
2
3
4
5
6
7
8
public readonly partial struct Decimal
{
// ...
public static implicit operator decimal(int value) => new decimal(value);
// ...
}
decimal x = 1337; // The constant is an int32 here.
We can use a similar construction for our proxy types to get rid of a lot of the clutter in our decompiler output.
Let us replace the Box
and Unbox
methods with implicit conversion operators:
1
2
3
4
5
6
7
8
9
10
11
12
13
public sealed class PersonProxy
{
private readonly Person _value;
public Person(Person A_1)
{
_value = A_1;
}
// Implicit conversion operators:
public static implicit operator PersonProxy(Person A_0) => new(A_0);
public static implicit operator Person(PersonProxy A_0) => A_0._value;
}
This allows us to assign values of type Person
to variables of type PersonProxy
without any explicit conversion or call, making code like the following perfectly valid:
1
PersonProxy proxy = new Person("Alice", "Average", 4.2f);
As mentioned before, since decompilers and debuggers try to be as helpful as possible and know about this C## feature, this modification gets rid of the explicit calls in our decompiler output:
Believe in the power of implicit operators
Interestingly enough, this code actually does not compile.
The ILSpy decompiler engine here actually makes a mistake by directly accessing properties from our Person
class on a variable of type PersonProxy
.
A nice bonus!
Name Obfuscation
But we can do better.
Let’s get rid of the Proxy
suffix in our type names.
To avoid outputting types with duplicated names (something that is prohibited by the CLR), we can replace some characters within the type’s name with homoglyphs.
Homoglyphs are characters that look almost identical to each other in many fonts, such as O
and 0
, I
and l
, as well as many Latin and Cyrillic characters such as a
and а
.
They have been used in the past as a method of obfuscation (e.g., one implementation by dr4k0nia), and we can implement something similar to generate proxy types that look indistinguishable from the original type:
Is Person
the original class or the proxy type?
We can do even better, by excluding types defined outside of the input module (such as System.Int32
) from our homoglyphic name obfuscation.
Interestingly enough, this will make many decompiler engines think an instance of our System.Int32
proxy is a normal int
:
All integer type definitions are treated equally
Now, our proxy types are virtually hidden from the decompiler window.
Adding Custom Display Formatters
Unfortunately, the Locals window isn’t fooled that easily. While the names look similar now, we still can see the proxy wrapper types in our debugger:
To tackle this, we go back to how IDEs and debuggers decide on how to visualize objects.
As mentioned before, many types in the standard library of .NET are actually not visualized in the way they are represented in memory.
We saw that for the List<T>
type that only the elements of the list were displayed as opposed to the raw structure of the type:
The visualization of the List<T>
object in a debugger.
If we look into the source code of List<T>
we can see that this is also not something that is hardcoded within the debugger, but rather in the declaration of List<T>
itself:
1
2
3
4
5
6
7
8
9
10
[DebuggerTypeProxy(typeof(ICollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public class List<T> : IList<T>, IList, IReadOnlyList<T>
{
// ...
public int Count => _size;
// ...
}
Note how the DebuggerDisplay attribute is used to let the object be displayed as "Count = {Count}"
, and how a separate type is registered via the DebuggerTypeProxy to implement the display of the individual array elements.
If you dig into the documentation on MSDN, you will find that there are a handful more of these types of attributes defined in the standard library, including CompilerGenerated and DebuggerBrowsable.
Each of them alters the behavior of object visualizers in the Locals windows of both IDEs as well as dnSpy.
Furthermore, if we rename fields to something like <XXX>k__BackingField
, many decompilers and debuggers will automatically treat it as a backing field of an auto-property and remove it from the list of items to show in the debugger.
Let’s apply our newly acquired knowledge by adding some randomly generated debugger display strings to our proxy types, and rename the private field to something that looks like a compiler-generated variable.
Our proxy type for Int32
now looks something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Diagnostics;
namespace System;
[DebuggerDisplay("{Display}")]
public struct Int32
{
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly int <0this>k__BackingField; // Renamed by our obfuscator
public Int32(int A_1)
{
this.<0this>k__BackingField = A_1;
}
// Implicit conversion operators:
public static implicit operator int(int A_0) => new(A_0);
public static implicit operator int(int A_0) => A_0.<0this>k__BackingField;
// Random display object:
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public int Display => 31; // Randomly selected by our obfuscator.
}
This has a very nice effect of completely getting rid of the tree-node expander, and also gives a very nice realistic-looking value in the string representation of our objects:
This num
integer is not a real integer. It is actually is not set to 31
but to 0
.
However, now it looks like our Person
object is empty with no properties defined.
Since Person
defines the three properties FirstName
LastName
and CoolnessFactor
, we as a reverse engineer would expect at least these three properties to be displayed in our Locals window.
Therefore to make it look more realistic, we can create new properties in our proxy class that mimic the original class by using the same name but returning some random value instead:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
public sealed class Реrsоn // The special highlighting shows the use of homoglyphs.
{
private readonly Person <0this>k__BackingField;
public Реrsоn(Person A_1)
{
this.<0this>k__BackingField = A_1;
}
// Implicit conversion operators:
public static implicit operator Реrsоn(Person A_0) => new Реrsоn(A_0);
public static implicit operator Person(Реrsоn A_0) => A_0.<0this>k__BackingField;
// Mimic properties of original type:
public string FirstName => "???"; // Randomly selected by our obfuscator.
public string LastName => "???"; // Randomly selected by our obfuscator.
public float CoolnessFactor => 0.198093534f; // Randomly selected by our obfuscator.
}
After this modification, the Locals window looks like the following:
This person
is not a real person.
Finally, we can also add the DebuggerTypeProxy
attribute to our original Person
class to also redirect the visualizer for true instances of Person
to our proxy type:
1
2
3
4
5
6
7
8
9
10
11
[DebuggerTypeProxy(typeof(Реrsоn))] // Indicate all instances of Person need to be visualized by our Реrsоn proxy
public class Person
{
/* ... original Person class implementation ... */
}
public sealed class Реrsоn
{
/* ... proxy class implementation ... */
}
This is particularly nice to cover objects residing in arrays:
These people are not a real people.
Unfortunately, it does not get rid of the Raw View
item in the window.
However, the majority of .NET reverse engineers do not bother looking there anyway [citation needed], making it still a very effective manner of hiding the true values stored within objects.
Success!
Variations
Here are a few more variations of this type of obfuscation that I came up with.
Moving Proxies to an Embedded Module
The astute reader would have noticed that inserting a lot of proxy types will clutter the Assembly Explorer of dnSpy a lot with a bunch of extra tree nodes.
To get rid of this clutter, we can move all the proxy types into a separate, embedded module that is dynamically resolved at runtime via the AppDomain.AssemblyResolve event.
This is a method that is used a lot by other obfuscators as well to embed dependencies.
We can also rename this embedded module to something like msсоrlіb
(mscorlib
with the same homoglyph obfuscation applied), making it look much less suspicious in the IL view and Modules window:
Who even looks at mscorlib itself?
This seems to work fine in dnSpy, but breaks the implicit operators in other decompilers that do more rigorous checks to determine whether a method is an implicit operator or not. For example, dotPeek doesn’t like this one bit:
dotPeek does not remove the op_Implicit
calls when our proxy types are embedded.
Lucky for us, as of writing this article, the vast majority of .NET reverse engineers use dnSpy [citation needed], and as such, it is still quite an effective option.
Crashing the Debugger
Since these proxy classes allow for arbitrary code to be executed, we can do anything we want, including letting the program crash the moment we initialize our proxy types.
We can for instance do this by calling FailFast
in our Display
string property, or by creating an infinite recursion causing a stack overflow:
1
2
3
4
5
6
7
8
9
10
11
12
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public string Display
{
get
{
Environment.FailFast("The CLR encountered an internal limitation.");
return null;
}
}
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public string Display => Display
This results in the following popup being displayed when a breakpoint hits:
The infamous no exception message
error of dnSpy.
Trying to continue the execution exits the debuggee process, effectively acting as a full anti-debug measure.
Changing the State of the Program
Finally, instead of crashing it directly with rather obvious signs, we can also change the state of the program the moment our display types are instantiated and visualized:
1
2
3
4
5
6
7
8
9
10
11
12
13
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public string Display
{
get
{
// Change the state of the actual object with random values selected by our obfuscator.
// No luck for the reverse engineer to ever find the original values back!
this.<0this>k__BackingField.FirstName = "???";
this.<0this>k__BackingField.LastName = "???";
this.<0this>k__BackingField.CoolnessFactor = 0.0324466228f;
return null;
}
}
This makes sure that whenever the application is stepped through, the program starts behaving differently but in a more subtle manner:
Stepping through makes the app behave differently than outside of the debugger.
Make this subtle enough, and you will let the reverse engineer waste a lot of time without them noticing!
Final words
In this post, we have seen that standard reverse engineering tools try to be as helpful as possible to the end user. In particular, optimizing decompilers try to adhere to high-level C## coding standards, by removing implicit operator calls and reducing fully qualified names of intrinsic types to shorter C## keywords. Additionally, object visualizers such as the Locals window in dnSpy try to distill as much of the implementation details as possible, by hiding compiler-generated variables and interpreting display formatting strings. By introducing carefully constructed proxy objects that exploit these types of features, we trick the debugger into hiding more information than it should and make the debugging experience of a reverse engineer significantly more confusing.
It is important to note that this type of obfuscation can be defeated simply by enabling and disabling a few settings in dnSpy. In particular, you may want to inspect the following settings and check/uncheck them as applicable:
- “Enable property evaluation and other implicit function calls”
- “Call string-conversion functions on object in variables window”
- “Show raw structures of objects in variables windows”
- “Hide compiler generated members”
- “Rsepect attributes that hide members”
For a more rigorous and automated removal of this protection, one could write a tool that searches in the metadata for all types annotated with one of these debugger attributes. Nonetheless, by creating these proxy types as realistically as possible, a reverse engineer may not notice that something weird is going on and waste a lot of time trying to figure out how a program works in the debugger.
As mentioned before, you can find a proof-of-concept obfuscator that implements this type of obfuscation on my GitHub.
Happy hacking!