Understanding Checks
Checks are runtime validations that detect and react to unauthorized uses of an application. For instance, a Debugging Check detects if the application is being run under a debugger.
Dotfuscator can add Checks to your application through code injection. There's no need to write any code yourself; Dotfuscator automatically injects the appropriate validation logic, and can also inject a pre-built response for when the Check detects unauthorized use. You can also configure the Check to notify your application of the validation result.
In contrast to obfuscation which protects the application "at rest" (i.e., as assembly files), Checks protect the application while it runs. Using obfuscation and Checks together provides a layered approach to protecting your application, defending both your application code and your business data from attack.
Configuring Checks
To have Dotfuscator inject Checks, first enable Checks.
To specify which Checks Dotfuscator will inject, use the Checks screen in the Dotfuscator Config Editor. If you prefer, you can also specify Checks by annotating your source code with Check Attributes. Both of these methods allow you to specify various properties that determine how the Check operates; for a full listing, see the Check Attributes page.
After Dotfuscator processes the assemblies, the resulting application will automatically perform Checks as specified methods are called at runtime.
Check Types
Each Check has a type. Each Check type performs a different set of validations, and may have slightly different properties to configure. A single application can have multiple Checks of the same type, provided they target distinct locations.
The Check types are:
- Tamper Check, which detects if the application code has been modified
- Debugging Check, which detects if a debugger is attached to the application
- Shelf Life Check, which detects if the application is being run after an expiration date
- Root Check, which detects if the application is being run on an Android device that has been rooted
You can find details on each Check type on the associated pages linked above. The rest of this page covers aspects that apply to multiple types of Checks.
Locations
Each Check targets one or more methods within the application code. These methods are known as a Check's locations.
Whenever a location is called at runtime, the associated Check runs, validating the state of the application at that instant per the Check's type and responding per the Check's properties.
A location may be associated with multiple Checks, provided each Check is of a different type; when such a location is called, each of its Checks run in sequence.
A Check may target multiple locations; if so, then the Check runs each time any of those locations is called.
After a Check runs, it will not run again until another location is called.
For instance, say a Debugging Check targets a single location, LoadFile(String path)
, which is called whenever the user loads a file in the application.
If an attacker runs the application, loads a file, and then attaches a debugger, the debugger will not be detected until the attacker loads a file again.
Properties
Each Check can be configured with various properties, which determine how the Check operates, including how it reacts to unauthorized states. The properties available depend on the type of Check, so see the Check Attributes page for a full list.
Different Checks of the same type may have different property values.
For instance, consider an application which has three methods of note: UserLogin()
, AdminLogin()
, and SupportLogin()
.
We want to prevent normal users and local administrators from attaching a debugger to the software, but in some scenarios it may be permissible for support personnel to be running the software in a debugger.
We can configure two Debugging Checks:
The first Debugging Check targets both
UserLogin()
andAdminLogin()
. We set the Check's Action property to Exit, causing the application to exit outright when a user or administrator runs the application under a debugger.The second Debugging Check targets
SupportLogin()
. We set the Check's Action property to None, allowing the application to continue when run under a debugger by a support member. (We may want to configure Application Notification to send us telemetry, so that we can monitor if it looks like a non-support user is using a support account).
Application Notification
Checks can notify the application code of the Check's result. In this way, the application can react to an unauthorized state in a customized way, such as by disabling application features or by sending third-party telemetry.
This notification happens before the specified Check Action.
The following properties of a Check determine how that Check notifies the application:
- ApplicationNotificationSinkElement
- ApplicationNotificationSinkName
- ApplicationNotificationSinkOwner
These properties specify a sink in the application code that receives a bool
value: true
if the unauthorized state was detected, and false
otherwise.
That is, if a sink is specified, the Check always calls it, even for negative results.
For details on specifying a sink, see the relevant properties on the Check Attributes page.
As an example, consider the following Tamper Check and sample code for the ApplicationNotificationExample
class:
internal class ApplicationNotificationExample
{
private bool? myFlag;
public void Run()
{
Console.WriteLine("Application logic...");
TamperCheckLocation();
Console.WriteLine("More Application logic...");
Console.WriteLine($"The CheckResult was '{myFlag}'.");
}
private void TamperCheckLocation()
{
Console.WriteLine("This method is a location of a Tamper Check, which will run before this text.");
}
private void TamperCheckSink(bool tamperingDetected)
{
Console.WriteLine("This method is a sink for the Tamper Check.");
if (tamperingDetected)
{
Console.WriteLine("This application has been tampered!");
}
else
{
Console.WriteLine("This application has not been tampered.");
}
myFlag = tamperingDetected;
}
}
After Dotfuscator injects the Check code, when other application code calls Run()
:
Run()
writes to the console and then callsTamperCheckLocation()
.Before
TamperCheckLocation()
executes, the Tamper Check runs:The Check determines if the application has been modified after it was processed by Dotfuscator.
As its application notification, the Check calls the
TamperCheckSink(bool)
method, supplying as the argumenttrue
if tampering was detected, andfalse
otherwise.TamperCheckSink(bool)
writes information about the Tamper Check to the console, sets themyFlag
field to the Check's result for later use, and returns control to the Check.The Check finishes running and returns control to the
TamperCheckLocation()
method.
TamperCheckLocation()
executes, then returns control toRun()
.Run()
writes additional information to the console, including a use of themyFlag
field.Run()
returns control to its caller.
Check Actions
Checks can themselves react to unauthorized application states in a number of built-in ways, such as by exiting the application or throwing a random exception. These reactions are known as Check Actions.
The Check Action occurs after the Application Notification behavior for the Check.
Each Check may have up to one Check Action. To define more complicated behavior, use application notification and have the application perform the behavior.
Which Check Action will be taken is determined by the Check's Action property.
The Check Actions available are:
None: The Check will return control to the application after running, even if the Check detected an unauthorized application state.
Exit: If the Check detected an unauthorized application state, the application will exit immediately with exit code 0.
Exception: If the Check detected an unauthorized application state, an exception is thrown.
If any Checks have this Check Action, Dotfuscator will add an exception type to the application that will be thrown by all instances of this Check Action. The exception will mimic a common .NET exception, such as
System.NullReferenceException
.The thrown exception will attempt to hide its stack trace, to prevent further reverse-engineering. However, the stack trace will be visible in some places, such as in the default .NET unhandled exception handler. You may want to catch and handle exceptions thrown by this Check Action, which will appear to have no stack trace when handled by user code.
Hang: If the Check detected an unauthorized application state, the current thread hangs indefinitely.
Action Probability
You can configure a Check Action to occur randomly. This makes the behavior of the application more frustrating and less predictable for an attacker; only sometimes will the application "misbehave", and other times it will operate normally.
The probability of a Check Action occurring when the Check runs is determined by the Check's ActionProbability property.
A setting of 1.00
means the Check Action will always occur, and a setting of 0.00
means the Check Action will never occur.
Choosing a setting between these values will produce random behavior; for instance, a setting of 0.25
means the Check Action will occur only 25% (one-quarter) of the times the Check runs.
Configuring the Check Library Code Location
Dotfuscator will inject library code into one of the input assemblies for each kind of Check you include in your application.
Dotfuscator performs a dependency analysis of the input assemblies in order to choose the best one to receive this code injection. It chooses in such a way as to minimize new dependencies among input assemblies; however, adding new dependencies is unavoidable in some cases.
You can override the assembly that this code will be injected into by adding a Config Property with the name "accesspoint" and setting its value to the name of the assembly where the code will be inserted. You can add this Config Property in the Config Editor.