How to obfuscate Xamarin Applications?
This tutorial will guide you through creating an obfuscated Xamarin based application.
Contents
- What is Xamarin?
- Why do I obfuscate a Xamarin application?
- Create sample application
- How to enable obfuscation?
- Conclusion
What is Xamarin?
Xamarin provides tools for making mobile applications for iOS, Android, macOS, and Windows platforms. It has finally become trendy because C# developers now can use their favorite language to build an application using the same code base that runs on most platforms.
Why do I obfuscate a Xamarin application?
The details of a Xamarin application execution depend on the target platform. On iOS, IL code is converted into machine code. On Android and Windows, the Mono Runtime is included: when a method is about to execute, the runtime compiles its IL code to native instructions for the target CPU and runs after that. Hence it is no matter where your application is supposed to be run; the obfuscation gives you additional confidence that your code will not be retrieved as is.
In this guide, we will create a sample Android application and show how to enable obfuscation and integrate it with the building process.
Create sample application
You can find the complete source code of the sample on GitHub: https://github.com/Softanics/XamarinObfuscationTest
Run Visual Studio and select Android App (Xamarin), choose Single View App, and click OK.
The sample will check an entered password and report whether it is correct.
Switch to Solution Explorer, navigate to Resources/layout/content_main.xml and then add an edit box to enter a password, and a label to show its status as shown below:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:showIn="@layout/activity_main"> <EditText android:id="@+id/passwordtext" android:layout_width="match_parent" android:imeOptions="actionGo" android:inputType="text" android:layout_height="wrap_content" /> <TextView android:id="@+id/statuslabel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Wrong" /> </RelativeLayout>
To display entered password status, add the following code to the end of the OnCreate method in MainActivity.cs:
var passwordtext = FindViewById<EditText>(Resource.Id.passwordtext); passwordtext.KeyPress += (object sender, View.KeyEventArgs e) => { e.Handled = false; if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter) { var statuslabel = FindViewById<TextView>(Resource.Id.statuslabel); statuslabel.Text = passwordtext.Text + " is " + (CheckPassword(passwordtext.Text) ? "wrong" : "correct"); e.Handled = true; } };
CheckPassword is a method that checks a password. Now it just always returns false:
private bool CheckPassword(string text) { return false; }
Run the sample, enter some text, and press Enter:
Great, it works well!
It is to add the actual code to check an entered password. The idea is simple: compare the hash of a text with the correct value:
private bool CheckPassword(string text) { using (var hash = SHA256.Create()) { var result = hash.ComputeHash(Encoding.UTF8.GetBytes(text)); return result[0] == 0x99 && result[1] == 0x97 && result[2] == 0xee && result[3] == 0x6b && result[4] == 0xc0 && result[5] == 0x52 && result[6] == 0x40 && result[7] == 0x93 && result[8] == 0xf7 && result[9] == 0xdf && result[10] == 0xb2 && result[11] == 0xae && result[12] == 0x8f && result[13] == 0x80 && result[14] == 0xa9 && result[15] == 0x97 && result[16] == 0xd7 && result[17] == 0x55 && result[18] == 0x04 && result[19] == 0xbf && result[20] == 0xd2 && result[21] == 0xe8 && result[22] == 0x29 && result[23] == 0xf5 && result[24] == 0x4a && result[25] == 0x2a && result[26] == 0x0c && result[27] == 0xd3 && result[28] == 0x17 && result[29] == 0x2a && result[30] == 0xda && result[31] == 0xd8; } }
Build the project and run again:
Great, it works!
What if I said now that anyone could easily break our application by just removing a few instructions? Start Developer Command Prompt for VS 2019 from the Start Menu, change directory to \bin\Debug and enter the following command to dump the IL code:
ildasm XamarinObfuscationTest.dll /out:XamarinObfuscationTest.il
You can find XamarinObfuscationTest.il here:
https://gist.github.com/Softanics/558ed19f3312ea7812cb877b4d9abddc
As you see, the code is plain: several calls (call, callvirt), conditional branches (bne.un, and others):
IL_0009: call class [mscorlib]System.Text.Encoding [mscorlib]System.Text.Encoding::get_UTF8() IL_000e: ldarg.1 IL_000f: callvirt instance uint8[] [mscorlib]System.Text.Encoding::GetBytes(string) IL_0014: callvirt instance uint8[] [mscorlib]System.Security.Cryptography.HashAlgorithm::ComputeHash(uint8[]) ... IL_004b: ldloc.1 IL_004c: ldc.i4.4 IL_004d: ldelem.u1 IL_004e: ldc.i4 0xc0 IL_0053: bne.un IL_0187
It is elementary to restore the algorithm and patch it! What is a solution? Well, obfuscators offer to convert original human-readable code to a form very hard to understand and to debug.
How to enable obfuscation?
Return to the project. Let’s add ArmDot packages: ArmDot.Client and ArmDot.Engine.MSBuildTasks. ArmDot.Client contains obfuscation attributes; ArmDot.Engine.MSBuildTasks provides an obfuscation task for MSBuild.
Right-click the project, select Manage NuGet packages… and install those packages.
Also you need to add an obfuscation task. Close the solution in Visual Studio. Then edit the project file (XamarinObfuscationTest.csproj); add the target Protect at the end of the file:
<Target Name="Protect" AfterTargets="Build"> <ItemGroup> <Assemblies Include="$(TargetDir)$(TargetFileName)" /> </ItemGroup> <ArmDot.Engine.MSBuildTasks.ObfuscateTask Inputs="@(Assemblies)" ReferencePaths="@(_ResolveAssemblyReferenceResolvedFiles->'%(RootDir)%(Directory)')" /> </Target>
Load the solution and rebuild the project. Then switch to Output Window. ArmDot didn’t obfuscate the assembly as we have not told what method we would like to obfuscate:
Rebuild started... 1>------ Rebuild All started: Project: XamarinObfuscationTest, Configuration: Debug Any CPU ------ I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest.csproj (in 78 ms). 1> XamarinObfuscationTest -> I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\bin\Debug\XamarinObfuscationTest.dll 1> [ArmDot] ArmDot [Engine Version 2021.22.0.0] (c) Softanics. All Rights Reserved 1> [ArmDot] Reading license key from C:\ProgramData\ArmDot\ArmDotLicense.key 1> [ArmDot] THIS PROGRAM IN UNREGISTERED. Buy a license at https://www.armdot.com/order.html 1> [ArmDot] ------ Build started: Assembly (1 of 1): XamarinObfuscationTest.dll (I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\bin\Debug\XamarinObfuscationTest.dll) ------ 1>I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest.csproj(123,5): warning : [ArmDot] warning ARMDOT0003: No methods to protect in the assembly I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\bin\Debug\XamarinObfuscationTest.dll 1> [ArmDot] Writing protected assembly to I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\bin\Debug\XamarinObfuscationTest.dll... 1>I:\Projects\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest\XamarinObfuscationTest.csproj(123,5): warning : [ArmDot] warning ARMDOT0002: The assembly XamarinObfuscationTest.dll will stop working in 7 days because it is protected by the ArmDot demo version 1> [ArmDot] Finished ========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========
Let’s virtualize the method CheckPassword. Add the attribute ArmDot.Client.VirtualizeCode as shown below:
[ArmDot.Client.VirtualizeCode] private bool CheckPassword(string text)
Rebuild the project. Re-run ildasm.exe once again to see how the obfuscated code looks after virtualization. The method contains ~8000 instructions after obfuscation; it is tough to decode! You can look at it on GitHub: https://gist.github.com/Softanics/ea425382e17be7227734de75fa544464
Conclusion
A code generated by C# is human-readable; it is easy to understand what each method is doing. Obfuscators, like ArmDot, convert intermediate code to a form that is hard to decode; at the same time, the original code and obfuscated one being executed produce the same result. Additionally, ArmDot can protect embedded resources and even merge an assembly with dependencies, including unmanaged DLLs.