Harden your .NET Applications with Dotfuscator's Anti-Debug Protections
Beginning with Dotfuscator Professional Edition version 4.25, you've been able to add anti-debug protections to your .NET applications, and now that Visual Studio 2017 has shipped, Dotfuscator Community Edition (CE) users have access to those protections as well.
Let's take this occasion to talk more about these new capabilities and walk through an example using both Pro and CE. Along the way, we will outline some patterns for implementation.
If you are reading this, you likely already know that a debugger is a powerful tool for watching and interacting with an application as it runs, and like any powerful tool, it can be used for good or ill. For example, an application owner would likely consider any of the following debugging scenarios "ill use", when carried out by an unauthorized party:
- Stepping through the application in order to figure out how it works.
- Modifying the application state at runtime, in order to bypass licensing or other restrictions.
- Identifying and exploiting security flaws in the application and in other services it may access.
- Accessing secrets and other potentially sensitive data in memory as the application processes it.
Static protections, like obfuscation, can help mitigate the threat—it is significantly more difficult to debug a well-obfuscated application—but, as always in security, a layered approach is best. Runtime detections and defenses (like anti-debug), in conjunction with static protections, make for an even more effective deterrent.
In Dotfuscator, these runtime detections and defenses are generally known as "Checks". Anti-debug is one kind of Check. Dotfuscator includes other Checks as well, such as anti-tamper and expiration (a.k.a. "Shelf-Life"). In each case, Dotfuscator injects the detection code into your application where you tell it to (using a custom or extended attribute). At runtime, when the condition is detected, the application can then defend itself, based on how the Check was configured. The response options include:
Preconfigured Actions. These are simple to add, and require no coding. In addition, you can specify a probability of the action occurring, in order to get non-deterministic behavior. The preconfigured actions include:
- Throw an exception.
- Hang the application.
- Exit the application.
Application Notifications. These allow you to modify the state and behavior of your application in any way you wish, in response to a detection. A Check can:
- Call a method.
- Set a field or property.
- Invoke a delegate.
Send telemetry to a PreEmptive Analytics endpoint. This allows you to see when—and maybe even who—has triggered the check.
To see some specifics and get an idea of what's possible, let's dive in to a concrete example.
For demonstration purposes, we've created a simple WPF application: a front end to a remote expense approval service. You can find the source and documentation on Github.
This is a simplified version of the application that PreEmptive's Sebastian Holst showed in this video. Among other things, the video shows what a debugging attack might look like, and how a debugger can be used to exploit programming flaws in order to escalate privileges. You will get more out of this article if you go watch it right now and come back.
The sample comes with configuration files for both Dotfuscator Pro and CE. We will begin with the Pro configuration, then talk about CE and the differences between the two. If you want to follow along, you should pull the source, build the Release configuration using Visual Studio 2015 or 2017, then open the Dotfuscator configuration file in the Dotfuscator UI, using the configuration that corresponds with your Dotfuscator SKU. If you don't yet have Dotfuscator, you can find instructions for getting it on the Dotfuscator Downloads page. If you are using CE, you will need the latest version that comes with Visual Studio 2017.
Once open in Dotfuscator, build the project. The build will protect the input EXE and write it to the Dotfuscator output directory. To make the protected application runnable from that directory, you will need to copy the original dependencies manually if you are using CE (The Pro configuration has a post-build event that does this for you). To do so, copy the DLLs from the project's bin\Release directory into the Dotfuscator output directory. When run, the application should look like this:
Debug Defense with Dotfuscator Professional
After opening the Pro configuration file with the Dotfuscator Professional standalone UI, you can find the extended attributes used to configure the Checks on the Instrumentation tab.
We will go through each one and describe what it does, how it is implemented, and why it is useful.
Starting at the beginning, the App.Application_Startup() method has a DebuggingCheckAttribute that forces the application to exit 100% of the time if a debugger is attached. This is accomplished by setting the "Action" to "Exit" and leaving the other settings at their default values. Exiting at startup discourages launching the app via a debugger; an attacker must instead attach to the process after starting it.
Also at startup—this time in the App.Main() method—a Shelf-Life Check runs, and disables (i.e. "bricks") the app if debugging was detected in a previous run.
The details of the Shelf-Life Check are outside the scope of this article, but essentially the injected code determines if the application has expired, then sets an application defined field (_isExpired) appropriately. The developer has written code in the application that disables the UI based on the value of the _isExpired field.
An application that can "remember" that it has been attacked can change its behavior in subsequent runs, throwing new roadblocks in front of attackers. In this case, the entire application is disabled after debugging has been detected, and now the attacker must figure out why before proceeding.
When a user of the application changes tabs, the MainWindows.tabCtrl_SelectionChanged() event handler fires. That method in turn calls the MainWindows.InjectADebugCheckHere() method. The DebugCheckAttribute on this method is configured to call yet another method, MainWindow.DebugNotification(), if a debugger is detected. That method does three things:
- It shows the "Debugging Detected" dialog, which forces the attacker to select a "consequence".
- Exit the app immediately. This demonstrates how an application can exit at any time in response to debugging, not just at startup.
- Tweet shaming. An application can send its own alerts in response to debugging. In this case the alerts go to a service that can tweet to the @DangerTestDemo account. The tweet includes the application's synthesized username.
- "Run Ransomware". This is disabled (and not implemented!), but it hints that the application doesn't need to be passive in the face of an attack.
- Throw an exception—but only one third of the time—if the attacker presses "I'm feeling lucky", and the debugger is still attached. This is implemented with a DebugCheckAttribute on the OptionsDialog.btnOptionsLucky_Click() method. Its Action is set to "Exception" and its ActionProbability is set to "0.33". Inconsistent and non-deterministic behavior can deter attackers and automated tools.
- It sends a notification to the Application Insights service if that option is configured and selected. Again, this shows that notifications can go to any service, or even multiple services.
- It creates the "cookie" on the filesystem that, in conjunction with the Shelf-Life Check, triggers the bricking behavior the next time the application is run.
Moving on to the "Expense Request" tab, the event handler for the Submit button, MainWindow.Submit_Click(), has a DebugCheckAttribute that is configured to hang the application one third of the time while submitting an expense request. Again, behavior that varies with each run frustrates and deters attackers and their tools. In addition to the Action, the DebugCheckAttribute is also configured to call the MainWindows.DisableSubmit() method. So if the application doesn't hang during submitting, the "Submit" button is always subsequently disabled, preventing any further use of the feature in that session. Locking down sensitive features in response to debugging hinders attempts to exploit or corrupt those features.
Last, Debug Check messages are always sent to a PreEmptive Analytics endpoint when debugging is detected. The application's synthesized "License Key" is sent as part of the message. This is done entirely through code injection and Dotfuscator configuration:
- On Dotfuscator's Settings tab, the "Send Debug Check Messages" property is set to "Yes".
- On the Instrumentation tab, there are Application and Business Attributes at the assembly level, with the BusinessAttribute using the Company Key for PreEmptive's free Community Workbench.
- The App.Main method has a SetupAttribute, configured to use the endpoint for the Community Workbench. It also tells the injected code to retrieve the application's license key from the _licenseKey field.
This is another example of alerting, providing notification and auditing of debugging attempts. In this case, the application's debugging notifications are sent to the free Community Workbench, and you can see a dashboard showing Debugging notifications here.
Debug Defense with Dotfuscator Community Edition
Since the Professional Edition has more functionality than the Community Edition, the CE configuration is slightly different. To summarize the differences in functionality:
- Shelf-Life can’t be used to implement "bricking", because CE does not allow the required application sources.
- CE only allows Action probabilities of 100%, so there is no non-deterministic behavior.
- Debug Checks in CE do not have "Throw an exception" or "Hang" Actions. CE only allows "Exit" Actions.
- The application's license key is not sent to PreEmptive Analytics in the Debug alerts, because CE does not allow the required application source.
These limitations somewhat restrict what can be done in the CE configuration for the sample application, but the good news is that there is still a set of reasonably effective defenses:
- The application exits on startup 100% of the time if a debugger is attached. This is the same as the Pro configuration.
- Even if a debugger is not attached at startup, the injected debug check still looks for the "cookie", and disables the app if it is present. This implementation does not use Shelf-Life. Instead the DebuggingCheckAttribute is configured to call the App.CE_DebugAction() method, where the CE-specific "bricking" logic is.
- The MainWindow.DebugNotification() method is called under the same conditions as in the Pro configuration, and it behaves similarly:
- As in Pro, it shows the "Debugging Detected" dialog, which forces the user to select their "consequence". Consequences include:
- Exit the app immediately. This is the same as with the Pro configuration.
- Tweet shame. This is the same as with the Pro configuration.
- Exits the app 100% of the time if the user presses "I'm feeling lucky". Attackers are never lucky with the CE configuration.
- "Run Ransomware" (disabled). This is same as with the Pro configuration.
- It sends a notification to the Application Insights service if that option is configured and selected. This is the same as with the Pro configuration.
- It creates the "cookie" on the filesystem that triggers the bricking behavior the next time the application is run. This is the same as with the Pro configuration.
- As in Pro, it shows the "Debugging Detected" dialog, which forces the user to select their "consequence". Consequences include:
- The application does not hang 30% of the time if a debugger is detected while submitting an expense request. We could have configured CE to hang the application 100% of the time, but we opted not to implement the action at all, in order to keep the behavior that disables the "Submit" button.
- The "Submit" button is always disabled after the first click if a debugger is detected, preventing any further use of the feature in that session. This is the same as with the Pro configuration.
- DebugCheck alerts are always sent to the PreEmptive Analytics endpoint when debugging is detected. With CE, the application's "License Key" is not sent with the alert.
Verifying the Defenses
After building the application and running it through Dotfuscator, you will want to verify that the injected defenses are working. You can test them using the Visual Studio debugger.
First, we will test the startup behavior when the application is launched by the debugger. One quick way to get the Visual Studio debugger to launch an external application, is via a C# project's Debug properties: simply set the Start action as shown:
Once configured this way, pressing F5 or "Start Debugging" from the Debug menu will launch the protected version of the application under the VS Debugger. If the injected "Exit" action is working correctly, you won't see much. The debugger will start, then stop again, and you will see a notice that the "program has exited" in the Debug Output window.
You can test the rest of the defenses by starting the application outside the debugger, then attaching the debugger to it via Visual Studio's "Attach to Process" dialog. Try some of these tests with the debugger attached:
- Click a tab. You should see the "Consequences" dialog.
- Try the tweet shaming, and observe the results on twitter.
- When you restart the application, even outside the debugger, it should come up in the "bricked" state. To "unbrick" it, delete the "dotchecksample.dat" file in the ProgramData folder.
- Try submitting an expense request with the debugger attached. Your actual results will depend on luck, and on whether you are using Pro or CE, but in all cases, the Submit button will subsequently be disabled.
If your applications are worth protecting, and you consider unauthorized debugging a threat, you should be using Dotfuscator's anti-debug defense capability. When you do, keep in mind some of the general principles we've highlighted in this article:
- Prevent launching the application with a debugger in order to keep attackers from watching your application's startup code.
- Disable the application altogether after it has been debugged in order to hinder further debugging.
- Introduce randomized and non-deterministic behavior in order to frustrate attackers and prevent automated attacks.
- Sprinkle debug defenses throughout the application in order to force attackers to work harder.
- Disable sensitive features in order to prevent those code paths from executing under a debugger.
- Send notifications to an external service in order to create an audit trail and send alerts.
To learn more: