.NET Ultimate Anti-debugging
This article introduces the anti-debugging principle under .NET, including the CLR internal debugging mechanism. Through this article, you can learn how to use the CLR debugging mechanism for detection and prevention of debuggers.
Introduction
0xd4d has written a project called antinet which is designed to be an anti-debugger and anti-profiler tool. The code is available on GitHub. This anti-debugging principle is not based on detection, but rather on active attacks. If successful, certain fields related to debugging in CLR will be directly destroyed, causing the debugger thread to exit and preventing other debuggers from attaching. In theory, this anti-debugging method is almost insoluble unless the identification mechanism within it is invalidated.
Therefore, I made some improvements to this code, such as adding Win32 function detection of debuggers and CLR internal function Hook detection, and removed the anti-analyzer code because it was not very useful for anti-debugging and did not look particularly stable. The improved code can be found here: https://github.com/wwh1004/antinet。
Here is the download address of sscli20, because sscli20 will be used in the article analysis. You can download it from GitHub.
Before reading this article, please make sure to open the modified antinet in the VS (the download link is provided above), otherwise it may be unclear what the article is about!!!
0xd4d’s AntiManagedDebugger
Overview
This is the anti-debugging class of 0xd4d’s antinet, which I have not modified and kept, and added comments to it.
Firstly, let’s take a look at how 0xd4d explains the principle of AntiManagedDebugger.
Open https://github.com/0xd4d/antinet and find “Anti-managed debugger”, below which is “Technical details” where the implementation principle is explained:
When the CLR starts, it creates a debugger class instance (called
Debugger
). This class will create aDebuggerRCThread
instance which is the .NET debugger thread. This thread is only killed when the CLR exits. To exit this thread, one must clear its “keep-looping” instance field, and signal its event to wake it up.Both of these instances are saved somewhere in the
.data
section.In order to find the interesting
DebuggerRCThread
instance, we must scan the.data
section for theDebugger
instance pointer. The reason I chose to find this one first is that it contains the currentpid
which makes finding it a little easier. When we’ve found something that appears to be theDebugger
instance and it has thepid
in the correct location, we get the pointer to theDebuggerRCThread
instance.The
DebuggerRCThread
instance also has a pointer back to theDebugger
instance. If it matches, then we can be very sure that we’ve found both of them.Once we have the
DebuggerRCThread
instance, it’s trivial to clear the keep-looping variable and signal the event so it wakes up and exits.To prevent a debugger from attaching, one can clear the debugger IPC block’s size field. If this is not an expected value,
CordbProcess::VerifyControlBlock()
inmscordbi.dll
will return an error and no debugger is able to attach.
If you don’t understand it, it’s okay to have a rough idea. Let’s take a look at the code of the AntiManagedDebugger class in VS.
The meaning of the code is exactly the same as 0xd4d’s own explanation, and they can be compared with each other. We won’t discuss the principle and idea of ending the debugging thread here; let’s take a closer look at what fields 0xd4d operated on.
Learn More in CLR Source Code
If I remember correctly, CoreCLR was open-sourced on the CLR v4.6 branch. Therefore, CLR v4.5 and later versions, as well as CoreCLR, are similar, and viewing the source code of CoreCLR is much better than IDA decompilation. However, CLR v4.0 is somewhere between CLR v2.0 and CLR v4.5; it can be regarded as a four-like system, which we will ignore for now because except for XP, all other systems can install .NET 4.5, and almost all of them have installed the latest .NET Framework.
SSCLI20 corresponds to CLR v2.0, which is .NET 2.0~3.5. Sometimes, it is better to view the SSCLI20 code than the IDA decompiled code of CLR v2.0.
Didn’t 0xd4d mention the “keep-looping” field? Let’s search for it in CoreCLR, and you will find that it cannot be found.
Did 0xd4d make a mistake? Or is CoreCLR different? Of course not, as a large-scale project, many parts of CLR cannot be changed easily. Let’s carefully search the declaration of DebuggerRCThread class, and you will find a field called “m_run”, which is the “keep-looping” field 0xd4d mentioned.
Now that we have found the “m_run” field, let’s take a look at what statement corresponds to the comment in AntiManagedDebugger.Initialize() that says “Signal debugger thread to exit”.
1 | // Signal debugger thread to exit |
In CoreCLR, we select the m_run field and click “Find All References” to quickly locate the function “HRESULT DebuggerRCThread::AsyncStop(void)”.
In this way, we understand that this piece of code is simulating DebuggerRCThread::AsyncStop(), which is called by Debugger::StopDebugger(), so it can achieve the goal of ending an existing debugger.
Of course, this cannot prevent a managed debugger from re-attaching to the current process. Therefore, before that, we need to prevent the managed debugger from attaching to the current process. That’s the meaning of the following code:
1 | // This isn't needed but it will at least stop debuggers from attaching. |
We directly go to CordbProcess::VerifyControlBlock() in CoreCLR to see what kind of verification it has.
Let’s also take a look at where m_DCBSize is defined and how to obtain it.
The code at 0xd4d checks if the version is .NET 2.0~3.5. After studying it, we can find some reasons through SSCLI20.
First, let’s open the SSCLI20 source code. In the class view, search for DebuggerRCThread and find the field m_rgDCB, which corresponds to the previous m_pDCB, but has an additional level of pointer.
Anti-anti-debugging
The code at 0xd4d obtains the address of the .data section through memory, and we can directly modify the section header to achieve anti-anti-debugging.
So we have many ways to bypass this anti-anti-debugging, such as:
- If the .data section does not exist, exit the process directly, because theoretically the .data section must exist.
- Read the RVA and Size of the .data section directly from the file, and then scan the corresponding position in memory.
- Verify if the PE header has been modified. If it has been modified, exit the process directly.
- …
Among them, verifying the PE header is the most effective method. Why? Since we cannot directly delete the .data feature, we can forge a fake section header, let AntiManagedDebugger modify somewhere else instead of the real DebuggerRCThread instance. If we ensure that the PE header is consistent with the one in the file, then we can determine that we have found the real and valid DebuggerRCThread instance through the .data section.
This anti-anti-debugging method is very easy to be detected again, so can we just directly modify all the places where this global variable is referenced? The answer is no. I have done various tests, such as directly copying objects, modifying before or after DllMain, all of which cause problems with the debugger.
These codes were written a long time ago, and I don’t want to test them again. This method is extremely troublesome, so it’s better to directly find the anti-debugging location and patch it.
Improved Antinet
AntiPatcher
Since there are some minor weaknesses in 0xd4d’s AntiManagedDebugger, we can add an AntiPatcher class to fix them.
This AntiPatcher class should be able to verify whether the PE header of the CLR module has been modified.
1 | private static void* _clrModuleHandle; |
Call Initialize() to get the CRC32 from the file.
Let’s write another method to verify whether there is such a PE header in memory.
1 | /// <summary> |
AntiDebugger
Firstly, this class should have the same function as the original AntiManagedDebugger, so we don’t delete the AntiManagedDebugger class, but wrap it directly.
1 | private static bool _isManagedDebuggerPrevented; |
Then we add a method to detect non-managed and managed debuggers.
1 | /// <summary> |
The implementation of HasUnmanagedDebugger is simple. We just need to delete the syscall part of xjun’s XAntiDebug. It takes some time to convert the syscall exploit code into C# code, so we will do it later. After all, debugging .NET programs using a non-managed debugger is extremely painful. Our anti-debugging target should mainly be managed debuggers such as dnSpy.
1 | /// <summary> |
Next is the implementation of HasManagedDebugger(), which is the main event. The most effective and convenient way to detect a managed debugger is to call Debugger.IsAttached. Unfortunately, this is too easy to modify, so we check whether it has been modified. A good news is that the implementation of Debugger.IsAttached is actually inside CLR and is a [MethodImpl(MethodImplOptions.InternalCall)], which means that the native code address of this method is the address of a function in the CLR module. As for why it is like this, it is not the focus of this article, so it will not be explained here. You can study CoreCLR by yourself.
We add initialization code to directly read the original code from clr.dll/mscorwks.dll and calculate its CRC32.
1 | private delegate bool IsDebuggerAttachedDelegate(); |
Here we used Ldasm, which I learned from xjun’s XAntiDebug project. This disassembly engine is very small, really only one function, and I attach the C# code translated by me.
1 | /// <summary> |
There are also a bunch of definitions that you can go to antinet to see for yourself. I won’t paste them here.
At this point, we can add code to check whether a managed debugger exists.
1 | /// <summary> |
Some may wonder why we don’t just copy the machine code to the buffer for verification, but only take the Opcode. This is because we need to consider the existence of relocation tables, so we can only detect whether the Opcode has been modified. It’s a bit complicated to check whether the operands have been modified.
Previously, we considered verifying the .text section of the entire CLR, but it failed. You can go to my GitHub commit history to see this part of the code. It is in AntiPatcher.cs and was commented out because it failed.
Why use
1 | if (_isDebuggerAttached()) |
instead of
1 | if (Debugger.IsAttched) |
Because the get property of Debugger.IsAttched from .NET 2.0 to 3.5 is a managed method, which may be directly patched, causing vulnerabilities in the detection of managed debuggers under .NET 2.0 to 3.5.
.NET Ultimate Anti-debugging