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:
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:
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>
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.