Harden Your .NET Applications With Dotfuscator’s Anti-debug Protections

PP_dotfuscator_rectange_blue

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.

The threat

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 (such as anti-debugging), in conjunction with static protections, make for an even more effective deterrent.

The mitigations

In Dotfuscator, these runtime detections and defenses are commonly referred to 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 at the location you specify (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 who or what triggered the check.

    To see some specifics and get an idea of what’s possible, let’s dive into a concrete example.

    The application

    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 includes configuration files for both Dotfuscator Pro and Dotfuscator 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 in Visual Studio 2015 or 2017, then open the Dotfuscator configuration file in the Dotfuscator UI using the configuration that corresponds to 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 the protected EXE to the Dotfuscator output directory. To make the protected application runnable from that directory, you will need to manually copy the original dependencies 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 binRelease directory into the Dotfuscator output directory. When run, the application should look like this:

    dotchecksample

    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.

    DotfuscatorPro DebugInstrumentation

    We will go through each one, describing 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 prevents launching the app via a debugger; an attacker must instead attach to the process after it starts.

    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.

    dotchecksamplewpf

    The details of the Shelf-Life Check are outside the scope of this article, but essentially, the injected code determines whether the application has expired and then sets the 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 is detected, and the attacker must figure out why before proceeding.

    When a user changes tabs in the application, 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.

    It shows the “Debugging Detected” dialog, which forces the attacker to select a “consequence”.

    Consequences

    Consequences include:

    • 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 during 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.
      1. 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.
      2. 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 is 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 from run to 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 submission, the “Submit” button is always disabled afterward, preventing further use of the feature in that session. Locking down sensitive features during debugging hinders attempts to exploit or corrupt them.

    Last, Debug Check messages are always sent to a PreEmptive Analytics endpoint when debugging is detected. The application’s synthesized “License Key” is included in 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”.

    DotfuscatorPro SendDebugCheckMessages 1

    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.

    DotfuscatorPro BusinessAttribute 1
    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.

    DotfuscatorPro SetupAttribute 1

    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 [depreciated], and you can see a dashboard showing Debugging notifications here [depreciated].

    Debug defense with Dotfuscator Community Edition

    Since the Professional Edition offers more functionality than the Community Edition, the CE configuration differs slightly. 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:
      1. 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 the 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 a “cookie” on the filesystem that triggers the bricking behavior the next time the application runs. This is the same as with the Pro configuration.
    • 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 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. In CE, the application’s “License Key” is not included in the alert.

    Verifying the defenses

    After building the application and running it through Dotfuscator, 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:

    VSDebuggerStartup 1

    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 whether you are using Pro or CE, but in all cases, the Submit button will be disabled afterward.

    Next steps

    If your applications are worth protecting and you consider unauthorized debugging a threat, you should use Dotfuscator’s anti-debugging capabilities. 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 to prevent further debugging.
    • Introduce randomized, nondeterministic behavior 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 to create an audit trail and trigger alerts.

    To learn more:

    In This Article

    Try PreEmptive Today

    Strengthen your application security with PreEmptive’s advanced protection
    © 2026 PreEmptive. All Rights Reserved