How to obfuscate Blazor App?

What is Blazor?

Blazor is a modern web framework from Microsoft aimed to let developers write web applications in C#. Blazor WebAssembly is one of the Blazor editions that allows to create single-page apps in C#, Razor, and HTML. C# code is compiled to intermediate code (IL) that is executed in a web browser by a .NET runtime implemented in WebAssembly.

As the code of a single-page app is downloaded by a web browser, it is generally available on the web. That is why developers often ask how to protect the code.

Why obfuscation matters?

.NET obfuscation hides used algorithms, converting original .NET code to a labyrinth of lots of instructions. It makes your intellectual property safe from prying eyes. Modern obfuscators not only change names of types, methods, fields, and properties, but also add garbage instructions, and transform original code beyond recognition.

The .NET platform is an open standard, so most obfuscators should support Blazor Apps obfuscation.

Learn how to protect Blazor App

In this tutorial, we will demonstrate how to integrate code obfuscation into the building process to protect a Blazor WebAssembly application.

You can find the complete source code of the project on GitHub: https://github.com/Softanics/BlazorAppObfuscationDemo

Start Visual Studio, click on Create a new project, find a template for Blazor app, type the project name, select Blazor WebAssembly App, keep the options as is and click on Create:
Create a Blazor app

Start debugging to make sure that it works as expected.

Now it’s time to enable the obfuscation. In this tutorial we use ArmDot, a modern obfuscator with complete support of all kinds of .NET applications. It offers names obfuscation, control flow obfuscation and code virtualization to protect your code.

In order to enable the obfuscation, add two ArmDot packages. Select the project, right-click and choose Manage NuGet Packages…, then click on Browse, type “ArmDot” and install both packages: Add ArmDot NuGet packages

The first package, ArmDot.Client, provides attributes to specify obfuscation types: they can be applied to the entire assembly, a type or a method.

The second package, ArmDot.Engine.MSBuildTasks, provides a task for obfuscation to include obfuscation to the build process.

Let’s enable this task. Right-click the project, select Edit Project File, and add the task as shown below:

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

Enable obfuscation task

The target “Protect” is executed after the assembly is built. The task “ArmDot.Engine.MSBuildTasks.ObfuscateTask” is used. The parameter “Inputs” specifies assemblies to obfuscate: we need to obfuscate only a single assembly whose path is $(TargetDir)$(TargetFileName). The last parameter, “ReferencePaths” helps ArmDot to find referenced assemblies.

Rebuild the project to check if the obfuscation task is really executed. You should get the following output:

Build started...
1>------ Build started: Project: BlazorAppObfuscationDemo, Configuration: Debug Any CPU ------
1>BlazorAppObfuscationDemo -> V:\Projects\BlazorAppObfuscationDemo\bin\Debug\netstandard2.1\BlazorAppObfuscationDemo.dll
1>BlazorAppObfuscationDemo (Blazor output) -> V:\Projects\BlazorAppObfuscationDemo\bin\Debug\netstandard2.1\wwwroot
1>[ArmDot] ArmDot [Engine Version 2021.1.0.0] (c) Softanics. All Rights Reserved
1>[ArmDot] No license key specified, or it is empty. ArmDot is working in demo mode.
1>[ArmDot] THIS PROGRAM IN UNREGISTERED. Buy a license at https://www.armdot.com/order.html
1>[ArmDot] ------ Build started: Assembly (1 of 1): BlazorAppObfuscationDemo.dll (V:\Projects\BlazorAppObfuscationDemo\bin\Debug\netstandard2.1\BlazorAppObfuscationDemo.dll) ------
1>V:\Projects\BlazorAppObfuscationDemo\BlazorAppObfuscationDemo.csproj(21,5): warning : [ArmDot] warning ARMDOT0003: No methods to protect in the assembly V:\Projects\BlazorAppObfuscationDemo\bin\Debug\netstandard2.1\BlazorAppObfuscationDemo.dll
1>[ArmDot] Writing protected assembly to V:\Projects\BlazorAppObfuscationDemo\bin\Debug\netstandard2.1\BlazorAppObfuscationDemo.dll...
1>V:\Projects\BlazorAppObfuscationDemo\BlazorAppObfuscationDemo.csproj(21,5): warning : [ArmDot] warning ARMDOT0002: The assembly BlazorAppObfuscationDemo.dll will stop working in 7 days because it is protected by the ArmDot demo version
1>[ArmDot] Finished
1>Done building project "BlazorAppObfuscationDemo.csproj".
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

Great, the task is executed successfully!

But ArmDot has not obfuscated the assembly yet. We need to enable obfuscation options in the code using the attributes provided by ArmDot.Client.

It’s always a good idea to obfuscate names. In order to do that, open Program.cs, and add the following code to apply this obfuscation option to the entire assembly:

[assembly: ArmDot.Client.ObfuscateNames()]

Rebuild the project. You should get the following output from ArmDot:

1>[ArmDot] Names obfuscation started
1>[ArmDot] Names obfuscation finished

Names obfuscation is a great way to confuse hackers as it costs nothing in terms of performance. At the same time, it doesn’t obfuscate the code itself, it just changes names stored in metadata.

So the second step is to obfuscate code using control flow obfuscation. This approach hides the logic of a method by splitting its code into a lot of small parts. Each part (a subset of original instructions) is moved to a separate method; also, each part has an index. Finally, the original method executes these small methods step by step in a large loop; each method sets the index of the next part.

Add the attribute ObfuscateControlFlow to Program.cs:

[assembly: ArmDot.Client.ObfuscateControlFlow()]

Rebuild the project and test that it is working well.

Let’s look at the obfuscated code:

.method family hidebysig virtual instance void
    BuildRenderTree(
      class [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder
    ) cil managed
  {
    .maxstack 7
    .locals init (
      [0] int32 index,
      [1] int32 num,
      [2] int32[] numArray
    )

    // [71 7 - 71 20]
    IL_0000: ldc.i4.0
    IL_0001: stloc.0      // index
    // start of loop, entry point: IL_0002
      IL_0002: nop

      // [72 7 - 72 25]
      IL_0003: ldloc.0      // index
      IL_0004: ldc.i4.1
      IL_0005: beq.s        IL_001e

      IL_0007: ldloca.s     index
      IL_0009: ldloca.s     num
      IL_000b: ldloca.s     numArray
      IL_000d: ldarg.1      // __builder
      IL_000e: ldarg.0      // this
      IL_000f: ldsfld       native int[] BlazorAppObfuscationDemo.Pages.Counter::ReRegisterForFinalizeChannelURI
      IL_0014: ldloc.0      // index
      IL_0015: ldelem.i
      IL_0016: calli        void (int32&, int32&, int32[]&, class [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder, class BlazorAppObfuscationDemo.Pages.Counter)
      IL_001b: nop
      IL_001c: br.s         IL_0002
    // end of loop
    IL_001e: ret

  } // end of method Counter::BuildRenderTree

Now let’s look at the original code:

  .method family hidebysig virtual instance void
    BuildRenderTree(
      class [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder
    ) cil managed
  {
    .maxstack 7

    IL_0000: nop
    IL_0001: ldarg.1      // __builder
    IL_0002: ldc.i4.0
    IL_0003: ldstr        "<h1>Counter</h1>\r\n\r\n"
    IL_0008: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddMarkupContent(int32, string)
    IL_000d: nop
    IL_000e: ldarg.1      // __builder
    IL_000f: ldc.i4.1
    IL_0010: ldstr        "p"
    IL_0015: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::OpenElement(int32, string)
    IL_001a: nop
    IL_001b: ldarg.1      // __builder
    IL_001c: ldc.i4.2
    IL_001d: ldstr        "Current count: "
    IL_0022: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddContent(int32, string)
    IL_0027: nop
    IL_0028: ldarg.1      // __builder
    IL_0029: ldc.i4.3
    IL_002a: ldarg.0      // this
    IL_002b: ldfld        int32 BlazorAppObfuscationDemo.Pages.Counter::currentCount
    IL_0030: box          [netstandard]System.Int32
    IL_0035: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddContent(int32, object)
    IL_003a: nop
    IL_003b: ldarg.1      // __builder
    IL_003c: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::CloseElement()
    IL_0041: nop
    IL_0042: ldarg.1      // __builder
    IL_0043: ldc.i4.4
    IL_0044: ldstr        "\r\n\r\n"
    IL_0049: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddMarkupContent(int32, string)
    IL_004e: nop
    IL_004f: ldarg.1      // __builder
    IL_0050: ldc.i4.5
    IL_0051: ldstr        "button"
    IL_0056: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::OpenElement(int32, string)
    IL_005b: nop
    IL_005c: ldarg.1      // __builder
    IL_005d: ldc.i4.6
    IL_005e: ldstr        "class"
    IL_0063: ldstr        "btn btn-primary"
    IL_0068: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddAttribute(int32, string, string)
    IL_006d: nop
    IL_006e: ldarg.1      // __builder
    IL_006f: ldc.i4.7
    IL_0070: ldstr        "onclick"
    IL_0075: ldsfld       class [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.EventCallbackFactory [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.EventCallback::Factory
    IL_007a: ldarg.0      // this
    IL_007b: ldarg.0      // this
    IL_007c: ldftn        instance void BlazorAppObfuscationDemo.Pages.Counter::IncrementCount()
    IL_0082: newobj       instance void [netstandard]System.Action::.ctor(object, native int)
    IL_0087: callvirt     instance valuetype [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.EventCallback`1<!!0/*class [Microsoft.AspNetCore.Components.Web]Microsoft.AspNetCore.Components.Web.MouseEventArgs*/> [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.EventCallbackFactory::Create<class [Microsoft.AspNetCore.Components.Web]Microsoft.AspNetCore.Components.Web.MouseEventArgs>(object, class [netstandard]System.Action)
    IL_008c: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddAttribute<class [Microsoft.AspNetCore.Components.Web]Microsoft.AspNetCore.Components.Web.MouseEventArgs>(int32, string, valuetype [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.EventCallback`1<!!0/*class [Microsoft.AspNetCore.Components.Web]Microsoft.AspNetCore.Components.Web.MouseEventArgs*/>)
    IL_0091: nop
    IL_0092: ldarg.1      // __builder
    IL_0093: ldc.i4.8
    IL_0094: ldstr        "Click me"
    IL_0099: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::AddContent(int32, string)
    IL_009e: nop
    IL_009f: ldarg.1      // __builder
    IL_00a0: callvirt     instance void [Microsoft.AspNetCore.Components]Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder::CloseElement()
    IL_00a5: nop
    IL_00a6: ret

  } // end of method Counter::BuildRenderTree

Good job!

ArmDot also provides the third and the best level of protection. It may affect performance and should be applied to the most important methods only. This protection approach is called “code virtualization”; it encodes each instruction in some binary format (that is different each time). The obfuscated method contains a code of an interpreter that reads the encoded instructions and executes them.

In this application, there is a sample page that shows how to increment the counter and displays it to the user. Let’s protect the code of this page. In order to do that, open Counter.razor, and add the attribute VirtualizeCode to the IncrementCount():

    [ArmDot.Client.VirtualizeCode()]
    private void IncrementCount()
    {
        currentCount++;
    }

Rebuild the project and run. It is working as expected! Let’s look at the obfuscated code, and it’s really hard to decode.

Conclusions

.NET developers have always wanted to write any type of application including web applications. Before Blazor was released, developers had to write client-side code in JavaScript or TypeScript. With Blazor, they are able to write both client-side and server-side code in their favorite C#.

As with any .NET application, the code of a Blazor App can be commonly available since it is downloaded from the web, which makes the obfuscation really essential.
ArmDot supports the obfuscation of Blazor Apps. It is easy to specify obfuscation options using ArmDot attributes, and automatically run the obfuscation task while building.