A Step-by-Step Guide on Obfuscating WinUI 3 (Windows App SDK) Projects

In this article, we will explore the process of obfuscating a WinUI 3 project using a .NET obfuscator. Obfuscation is an essential technique for protecting your code from reverse engineering and unauthorized access. By obfuscating your WinUI 3 project, you can safeguard your intellectual property and prevent malicious actors from understanding the inner workings of your application.

Contents

What is WinUI 3 and Why Obfuscate?

WinUI 3, also known as Windows UI Library, is a modern user interface framework for developing Windows apps. It allows developers to create user interfaces that are consistent across different Windows 10 devices. WinUI 3 provides a set of controls, styles, and other UI components to create visually appealing and interactive apps. It is based on the Universal Windows Platform (UWP) and can be used with various programming languages like C++, C#, and XAML.

Obfuscation plays a crucial role in securing WinUI 3 projects by making it significantly more difficult for malicious actors to understand and reverse engineer the code. Here are several key reasons why obfuscation is important in the context of WinUI 3 projects:

  • Intellectual Property Protection: WinUI 3 projects often contain valuable intellectual property, such as unique algorithms, business logic, or trade secrets. Obfuscation helps safeguard this intellectual property by making it extremely challenging for unauthorized individuals to decipher and steal the underlying code or algorithms.
  • Preventing Reverse Engineering: Once an application is published, it becomes susceptible to reverse engineering. By obfuscating the code, developers can make it exceedingly difficult for attackers to unravel the inner workings of the application. Obfuscated code is typically transformed into a complex and convoluted form, making it time-consuming and arduous for attackers to understand the logic behind the code.
  • Protecting Sensitive Data: WinUI 3 projects may involve the handling of sensitive data, such as user credentials, API keys, or confidential business information. Obfuscation can help mitigate the risk of exposing such data by obscuring the code responsible for handling or storing sensitive information. This added layer of security makes it more challenging for attackers to locate and exploit vulnerabilities related to data handling.
  • Strengthening Software License Enforcement: Obfuscation can be used to improve the protection of licensing mechanisms within WinUI 3 projects. By obfuscating licensing-related code, developers can make it harder for unauthorized users to bypass or manipulate the licensing system. This ensures that only legitimate users can access and utilize the application, helping software developers protect their revenue streams.
  • Resisting Automated Analysis and Automated Attacks: Automated tools and scripts are often used by attackers to scan and exploit vulnerabilities in software applications. Obfuscation significantly hinders the effectiveness of such tools by obfuscating code, making it harder for automated analysis and exploitation techniques to identify exploitable weaknesses.
  • Deterrent Against Intellectual Property Theft: Obfuscated code acts as a deterrent against potential intellectual property theft. When attackers encounter obfuscated code, they are more likely to move on to easier targets rather than investing large amounts of time and resources in attempting to reverse engineer it. Obfuscation acts as an additional layer of protection, discouraging attackers from stealing your code.

While obfuscation enhances the security of WinUI 3 projects, it’s important to note that it should not be considered the sole solution for software security. It should be used in conjunction with other security practices, such as secure coding techniques, strong authentication mechanisms, and regular security updates, to create a robust and comprehensive security posture for your WinUI 3 projects.

Let’s explore how to choose the best .NET obfuscation for your WinUI 3 project.

Selecting an Obfuscator for WinUI 3

A reliable obfuscator should, of course, support WinUI 3 projects. It should offer a wide range of code obfuscation techniques, including control flow obfuscation and virtualization. Additionally, it should be able to protect embedded resources that contain assets of your application. It would be great if such an obfuscator were cross-platform, as it is common nowadays to build and test your application on one platform but deploy it on another. After all, .NET itself is cross-platform.

Enter ArmDot! It is compatible with Windows, Linux, and macOS. It provides full support for WinUI 3 projects, including control flow obfuscation and virtualization. It can also rename your classes and methods to make them harder to read.

ArmDot can be seamlessly integrated into the build process thanks to its NuGet packages. Let’s take a closer look at how it works.

Obfuscating Your WinUI 3 Project

To start, open Visual Studio and click on “Create a new Project”. Then, select “Blank App, Packaged (WinUI 3 in Desktop)”. Name the project “App1”. Build and run the project to ensure it is working properly. You should see a window with a button.

Our application will validate the entered string by comparing its hash with the correct one.

Open the MainWindow.xaml file and add a text box for the password input and another one to display the status of the validation:

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="App1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
        <TextBox x:Name="passwordTextBox" />
        <Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
        <TextBox x:Name="statusLabel" IsReadOnly="True" />
    </StackPanel>
</Window>

Next, open the MainWindow.xaml.cs file and update the onclick handler:

private void myButton_Click(object sender, RoutedEventArgs e)
{
    var hash = SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(passwordTextBox.Text));
    var readableHash = Convert.ToBase64String(hash);
    statusLabel.Text = readableHash;
}

This will help us obtain the value of the correct hash, which will be displayed in the status text box.

Run the application, enter “ArmDot” and click the button. Then, copy the hash value.

Modify the myButton_Click method to display the password checking status:

private void myButton_Click(object sender, RoutedEventArgs e)
{
    var hash = SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(passwordTextBox.Text));
    var readableHash = Convert.ToBase64String(hash);

    if (readableHash == "hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0=")
        statusLabel.Text = "Correct";
    else
        statusLabel.Text = "Wrong";
}

Build and run the application. Enter “ArmDot” to ensure that the displayed status is “Correct”. Enter another password to see “Wrong”.

What problems do we have with this code? It seems to be working fine, but it is completely “visible” to reverse engineers. Take a look at the generated IL code using tools like ILSpy:

IL_0000: nop
IL_0001: call class [System.Security.Cryptography.Algorithms]System.Security.Cryptography.SHA256 [System.Security.Cryptography.Algorithms]System.Security.Cryptography.SHA256::Create()
IL_0006: call class [System.Runtime]System.Text.Encoding [System.Runtime]System.Text.Encoding::get_UTF8()
IL_000b: ldarg.0
IL_000c: ldfld class [Microsoft.WinUI]Microsoft.UI.Xaml.Controls.TextBox App1.MainWindow::passwordTextBox
IL_0011: callvirt instance string [Microsoft.WinUI]Microsoft.UI.Xaml.Controls.TextBox::get_Text()
IL_0016: callvirt instance uint8[] [System.Runtime]System.Text.Encoding::GetBytes(string)
IL_001b: callvirt instance uint8[] [System.Security.Cryptography.Primitives]System.Security.Cryptography.HashAlgorithm::ComputeHash(uint8[])
IL_0020: stloc.0
IL_0021: ldloc.0
IL_0022: call string [System.Runtime]System.Convert::ToBase64String(uint8[])
IL_0027: stloc.1
IL_0028: ldloc.1
IL_0029: ldstr "hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0="
IL_002e: call bool [System.Runtime]System.String::op_Equality(string, string)
IL_0033: stloc.2
IL_0034: ldloc.2
IL_0035: brfalse.s IL_004a
IL_0037: ldarg.0
IL_0038: ldfld class [Microsoft.WinUI]Microsoft.UI.Xaml.Controls.TextBox App1.MainWindow::statusLabel
IL_003d: ldstr "Correct"
IL_0042: callvirt instance void [Microsoft.WinUI]Microsoft.UI.Xaml.Controls.TextBox::set_Text(string)
IL_0047: nop
IL_0048: br.s IL_005b
IL_004a: ldarg.0
IL_004b: ldfld class [Microsoft.WinUI]Microsoft.UI.Xaml.Controls.TextBox App1.MainWindow::statusLabel
IL_0050: ldstr "Wrong"
IL_0055: callvirt instance void [Microsoft.WinUI]Microsoft.UI.Xaml.Controls.TextBox::set_Text(string)
IL_005a: nop

The logic is obvious, as method calls and string literals are clearly visible. This is not what you need if you want to protect your code. Let’s move on and see how to obfuscate this code.

Configuring .NET Obfuscator

It’s time to add obfuscation to the project. You need to add two NuGet packages: ArmDot.Client.dll and ArmDot.Engine.MSBuildTasks. The first package contains obfuscation attributes that you will use to instruct the .NET obfuscator on how to obfuscate the code, while the second package provides the obfuscation task that will be called during the building process.

To add the packages, right-click on the project and select “Manage NuGet Packages”. Switch to the “Browse” tab, search for and install ArmDot.Client.dll and ArmDot.Engine.MSBuildTasks packages.

Once the packages are installed, you need to enable the obfuscation task. Double-click on the project node to open the project file for editing, and add the obfuscation task as shown below:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net6.0-windows10.0.19041.0</TargetFramework>
    <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
    <RootNamespace>App1</RootNamespace>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    <Platforms>x86;x64;ARM64</Platforms>
    <RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
    <PublishProfile>win10-$(Platform).pubxml</PublishProfile>
    <UseWinUI>true</UseWinUI>
    <EnableMsixTooling>true</EnableMsixTooling>
  </PropertyGroup>

<Target Name="Protect" AfterTargets="AfterCompile" BeforeTargets="BeforePublish">
    <ItemGroup>
        <Assemblies Include="$(ProjectDir)$(IntermediateOutputPath)$(TargetFileName)" />
    </ItemGroup>
    <ArmDot.Engine.MSBuildTasks.ObfuscateTask
    Inputs="@(Assemblies)"
    ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')"
    SkipAlreadyObfuscatedAssemblies="true"
    />
</Target>

We have just added the target “Protect” which calls the obfuscation task after the compiler places the assembly in the intermediate directory.

To enable control flow obfuscation for the method myButton_Click, add the appropriate attribute as shown below:

[ArmDot.Client.ObfuscateControlFlow]
private void myButton_Click(object sender, RoutedEventArgs e)
{
    var hash = SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(passwordTextBox.Text));
    var readableHash = Convert.ToBase64String(hash);

    if (readableHash == "hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0=")
        statusLabel.Text = "Correct";
    else
        statusLabel.Text = "Wrong";
}

Rebuild the project and return to ILSpy. Oh, what has happened to the method? Look:

IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: ldc.i4.0
IL_0003: stloc.1
IL_0004: ldc.i4.0
IL_0005: stloc.1
// loop start (head: IL_0006)
	IL_0006: nop
	IL_0007: ldloc.1
	IL_0008: ldc.i4.4
	IL_0009: bne.un.s IL_000f

	IL_000b: ldc.i4.0
	IL_000c: stloc.1
	IL_000d: br.s IL_002b

	IL_000f: nop
	IL_0010: ldloca.s 0
	IL_0012: ldloca.s 1
	IL_0014: ldloca.s 2
	IL_0016: ldloca.s 3
	IL_0018: ldloca.s 4
	IL_001a: ldloca.s 5
	IL_001c: ldarg.0
	IL_001d: ldsfld native int[] App1.MainWindow::IOCompletionCallbackDirectoryInfo
	IL_0022: ldloc.0
	IL_0023: ldelem.i
	IL_0024: calli void(int32&, int32&, int32&, uint8[]&, string&, bool&, class App1.MainWindow)
	IL_0029: br.s IL_0006
// end loop

IL_002b: nop
IL_002c: ret

That’s really hard to understand what’s going on!

Since the method is not computationally intensive, you can consider virtualizing it instead:

[ArmDot.Client.VirtualizeCode]
private void myButton_Click(object sender, RoutedEventArgs e)
{
    var hash = SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(passwordTextBox.Text));
    var readableHash = Convert.ToBase64String(hash);

    if (readableHash == "hzTbLivrxGSCjSfDqNEIDuXEbj1W6IXdPikhanYqTu0=")
        statusLabel.Text = "Correct";
    else
        statusLabel.Text = "Wrong";
}

Now the method became even more “frighteningly”, this is decompiled code:

int num3;
byte[] array;
object[] array2;
sbyte[] array5;
long[] array7;
object[] array8;
byte* ptr;
int num5 = default(int);
int num6 = default(int);
object[] array9;
int num7;
byte* num8;
ref sbyte reference;
byte* num10;
int num11;
byte* ptr2;
fixed (byte[] array10 = new byte[90])
{
    int num = 1;
    int num2 = *(sbyte*)(&num);
    num3 = num2 * 4;
    int num4 = num2 * 8;
    array = new byte[3];
    array2 = new object[3];
    int[] array3 = new int[3];
    sbyte[] array4 = new sbyte[1];
    fixed (sbyte[] array12 = array4)
    {
        array5 = array4;
        long[] array6 = new long[3];
        fixed (long[] array11 = array6)
        {
            array7 = array6;
            array8 = new object[3] { this, null, null };
            ptr = (byte*)Unsafe.AsPointer(ref TypeNAssemblyUnregisterDynamicProperty.GetInvocationListUserProfile);
            ptr2 = ptr;
            byte* ptr3 = ptr2;
            while (true)
            {
                if (num5 == 1)
                {
                    return;
                }
                ptr3 = ptr2;
                byte b = *ptr2;
                ptr2++;
                if (b < 1 || b > 22)
                {
                    continue;
                }
                if (11 >= b)
                {
                    if (11 > b)
                    {
                        if (5 >= b)
                        {
…
                            ptr2 = ((*(int*)Unsafe.AsPointer(ref array7[num6 - 1]) == 0) ? 1 : 0) * (*(int*)(ptr2 + num3) - *(int*)(ptr2 + 8 + num3)) + *(int*)(ptr2 + 8 + num3) + ptr;
                            num6--;
                        }
                    }
                    else if (19 >= b && 19 <= b)
                    {
                        array2[num6 - 1] = ((delegate*<MainWindow, TextBox>)getIsSetgetListSeparator[*(int*)(ptr2 + num3)])((MainWindow)array2[num6 - 1]);
                        array[num6 - 1] = 5;
                        ptr2 += 8;
                    }
                }
                else
                {
                    ((delegate*<TextBox, string, void>)getIsSetgetListSeparator[*(int*)(ptr2 + num3)])((TextBox)array2[num6 - 2], (string)array2[num6 - 1]);
                    num6 -= 2;
                    ptr2 += 8;
                }
            }
            else if (21 >= b)
            {
                if (21 <= b)
                {
                    ptr2 = ptr + *(int*)(ptr2 + num3);
                }
            }
            else if (22 >= b && 22 <= b)
            {
                num10 = ptr2;
                num11 = global::< Module >.IgnoreWidthDXNNY - 45;
                num5 = 1;
                ptr2 = num10 + num11;
            }
        }
        array9 = array2;
        num7 = num6;
    }
    while (array10 != null);
}

Well, who can say what’s going on in this code?!

Conclusion

Obfuscating your WinUI 3 projects using a .NET obfuscator is vital for protecting your intellectual property and ensuring the security of your application. By following the steps outlined in this article, you can successfully obfuscate your WinUI 3 project and minimize the risk of unauthorized access or reverse engineering.

Remember, always prioritize the security of your application, and continually update and enhance your obfuscation techniques to stay ahead of potential threats.