Bypassing Xamarin Certificate Pinning on Android

Xamarin is a popular open-source and cross-platform mobile application development framework owned by Microsoft with more than 13M total downloads. This post describes how we analyzed an Android application developed in Xamarin that performed HTTP certificate pinning in managed .NET code. It documents the method we used to understand the framework and the Frida script we developed to bypass the protections to man-in-the-middle (MITM) the application. The script’s source code, as well as a sample Xamarin application, are provided for testing and further research.

When no Known Solution Exists

During a recent mobile application engagement, we ran into a challenging hurdle while setting up an HTTPS man-in-the-middle with Burp. The application under test was developed with the Xamarin framework and all our attempts at bypassing the certificate pinning implementation seemed to fail. Using one of the several available pinning bypass Frida scripts, we were able to intercept traffic to some telemetry sites, but the actual API calls of interest were not intercepted. Searching the Internet for similar work led us to a Frida library, frida-mono-api, which adds basic capabilities to interface with the mono runtime and an article describing how-to exfiltrate request signing keys in Xamarin/iOS applications. With the lack of an end-to-end solution, it quickly started to feel like a DIY moment.

Building a Test Environment

The first step taken to tackle the problem was to learn as much as possible about Xamarin, Mono and Android by re-creating a very simple application using the Visual Studio 2019 project template and implementing certificate pinning. This approach is interesting for multiple reasons:

Learn Xamarin from a developer’s perspective;

Solidify understanding of the framework;

Reading documentation will be required regardless;

Sources are available for debugging.

An additional benefit was that the application developed as part of this exploration phase could be used for demonstration purposes and to reliably validate our attempts to bypass certificate pinning. For this reason alone, the time spent upfront on development was more than worth it.

The logical progression towards a working bypass can be outlined as follows

Identify the interfaces that allow to customize the certificate validation routines; Identify how they are used by typical code bases; Determine how to alter them at runtime in a stable fashion; Write a proof of concept script and test it against the demo application.

Another important objective that we had with this work was that any improvements towards Mono support in Frida should be a contribution to existing projects.

Down the Rabbit Hole

After setting up an Android development environment inside a Windows VM and following along with the Xamarin Getting Started guide, we were able to build and sign a basic Android application. With the application working, we implemented code simulating a certificate pinning routine as shown in listing 1: A handler that flags all certificates as invalid. If we’re able to bypass this handler, then it implies that we should also be able to bypass a handler that verifies the public key against a hardcoded one.

Listing 1 – The simplest certificate “validation” handler.

static class App { // Global HttpClient as per MSDN: // https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient public static readonly HttpClient Http {get; private set;} static App() { var h = new HttpClientHandler(); h.ServerCertificateCustomValidationCallback = ValidateCertificate; Http = new HttpClient(hh); } // This would normally check the public key with a hardcoded key. // Here we simulate an invalid certificate by always returning false. private static bool ValidateCertificate(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => false; } // ... // Elsewhere in the code. private async void MakeHttpRequest(object obj) { // ... var r = await App.Http.GetAsync("https://www.example.org"); // ... }

Xamarin Concepts

Xamarin is designed to provide cross-platform support for Android and iOS and minimize code duplication as much as possible. The UI code uses Microsoft’s Window Presentation Framework (WPF) which is an arguably nice way to program frontend code. There are two major components in any given Xamarin application: A shared library with the common functionality that does not rely on native operating system features and a native application launcher project specific to each supported target operating system. In practice, this means that there are at least three projects in most Xamarin applications: The shared library, an Android launcher, and an iOS launcher.

Application code is written in C# and uses the .NET Framework implementation provided by Mono. The code output is generated as a regular .NET assembly (with the .dll extension) and can be decompiled reliably (barring obfuscation) with most type information kept intact using a decompiler, such as dnSpy.

Xamarin has support for three compilation models provided by the underlying Mono framework:

Just-in-Time (JIT): Code is compiled lazily as required

Partial Ahead-of-Time compilation (AOT): Code is natively compiled ahead-of-time during build for compiler-selected methods

Full AOT: All Intermediate Language (IL) code is compiled to native machine code (required for iOS)

The Mono Runtime

The Mono runtime is responsible for managing the memory heaps, performing garbage collection, JIT compiling methods when needed, and providing native functionality access to managed C# code. The runtime tracks metadata about all managed classes, methods objects, fields, and other states from the Xamarin application. It also exports a native API that enables native code to interact with managed code. While most of these methods are documented, some of them have empty or incomplete document strings and diving into the codebase has proven to be necessary multiple times while developing the Frida script.

Mono uses a tiered compilation process, which will become relevant later as we describe the implementation of certificate pinning. In the pure JIT case, a method starts off as IL bytecode, which gets a compilation pass on the initial call. The resulting native code is referred to as the tier0 code and is cached in memory for re-use. When a method is deemed critical, the JIT compiler can decide to optimize it and recompile it using more aggressive optimizations.

Mono is in fact much more complex than described here, but this overview covers the basics needed to understand the Frida script.

Hijacking Certificate Validation Callbacks

.NET has evolved over time and there are two entry points to override certificate validation routines, depending on whether .NET Framework or .NET Core is being used. Mono has recently moved to .NET Core APIs and rendered the .NET Framework method ineffective.

Prior to .NET Core (and Mono 6.0), validation occurs through System.Net.ServicePointManager.ServerCertificateValidationCallback , which is a static property containing the function to call when validating a certificate. All HttpClient instances will call the same function, so only one function needs to be hooked.

Starting with .NET Core, however, the HTTP stack has been refactored such that each HttpClient has its own HttpClientHandler exposing a ServerCertificateCustomValidationCallback property. This handler is injected into the HttpClient at construction time and is frozen after the first HTTP call to prevent modification. This scenario is much more difficult as it requires knowledge of every HttpClient instance and their location in memory at runtime.

Listing 2 – Certificate validation callback setter preventing callback hijacking

// https://github.com/mono/mono/blob/mono-6.8.0.96/mcs/class/System.Net.Http/HttpClientHandler.cs#L93 class HttpClientHandler { public Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> ServerCertificateCustomValidationCallback { get { return (_delegatingHandler.SslOptions.RemoteCertificateValidationCallback? .Target as ConnectHelper.CertificateCallbackMapper)? .FromHttpClientHandler; } set { ThrowForModifiedManagedSslOptionsIfStarted (); // <---- Validation here _delegatingHandler.SslOptions .RemoteCertificateValidationCallback = value != null ? new ConnectHelper.CertificateCallbackMapper(value) .ForSocketsHttpHandler : null; } } }

As seen in the previous listing, setting the callback after a request has been sent will throw an exception and most likely cause the application to crash. Fortunately for us, the base class of HttpClient is HttpMessageInvoker which contains a mutable reference to the HttpClientHandler that will perform the certificate validation so it’s possible to safely change the whole handler:

Listing 3 – HttpMessageInvoker request dispatch mechanism

// https://github.com/mono/mono/blob/mono-6.8.0.96/mcs/class/System.Net.Http/System.Net.Http/HttpMessageInvoker.cs public class HttpMessageInvoker : IDisposable { protected private HttpMessageHandler handler; readonly bool disposeHandler; // ... public virtual Task SendAsync (HttpRequestMessage request, CancellationToken cancellationToken) { return handler.SendAsync (request, cancellationToken); } }

Hooking Managed Methods

In the ServicePointManager case, intercepting the callback is as simple as hooking the static property’s get and set methods, so it will not be covered explicitly but is included with the bypass Frida script we are providing. Let’s focus on the more interesting HttpClientHandler case, which requires more than just method hooking. The idea is to replace the HttpClientHandler instance by one that we control that restores the default validation routine.

To do this, we can hook the HttpMessageInvoker.SendAsync implementation and replace the handler immediately before it gets called. Now, SendAsync is a managed method, so it could be in any given state at any given moment:

Not yet JIT compiled: The native code for hooking does not exist

Tier0 compiled: We can hook the method if we can find its address

AOT compiled: The method is in a memory mapped native image

To make matters trickier, if the Mono runtime were to decide to optimize a method that we hooked, it is likely that our hook might be removed in the newly generated code. Thankfully, the native function mono_compile_method allows us to take a class method and force the JIT compilation process. However, it is not clear whether the method is tier 0 compiled or optimized, so there could still potentially be issues with optimizations. The return value of mono_compile_method is a pointer to the cached native code corresponding to the original method, making it very straightforward to patch using existing Frida APIs.

Putting the Pieces Together

We forked frida-mono-api project as a starting point and added some new export signatures, along with the JIT compilation export and a MonoApiHelper method to wrap the boilerplate required to hook managed methods. The resulting code is very clean and in theory should allow to hook any managed method:

Listing 4 – Support for managed method hooking in frida-mono-api

function hookManagedMethod(klass, methodName, callbacks) { // Get the method descriptor corresponding to the method name. let md = MonoApiHelper.ClassGetMethodFromName(klass, methodName); if (!md) throw new Error('Method not found!'); // Force a JIT compilation to get a pointer to the cached native code. let impl = MonoApi.mono_compile_method(md) // Use the Frida interceptor to hook the native code. Interceptor.attach(impl, {...callbacks}); }

With the ability to hook managed methods, we can implement the approach described above and test the script on a rooted Android device.

Listing 5 – Final certificate pinning bypass script

import { MonoApiHelper, MonoApi } from 'frida-mono-api' const mono = MonoApi.module // Locate System.Net.Http.dll let status = Memory.alloc(0x1000); let http = MonoApi.mono_assembly_load_with_partial_name(Memory.allocUtf8String('System.Net.Http'), status); let img = MonoApi.mono_assembly_get_image(http); let hooked = false; let kHandler = MonoApi.mono_class_from_name(img, Memory.allocUtf8String('System.Net.Http'), Memory.allocUtf8String('HttpClientHandler')); if (kHandler) { let ctor = MonoApiHelper.ClassGetMethodFromName(kHandler, 'CreateDefaultHandler'); // Static method -> instance = NULL. let pClientHandler = MonoApiHelper.RuntimeInvoke(ctor, NULL); console.log(`[+] Created Default HttpClientHandler @ ${pClientHandler}`); // Hook HttpMessageInvoker.SendAsync let kInvoker = MonoApi.mono_class_from_name(img, Memory.allocUtf8String('System.Net.Http'), Memory.allocUtf8String('HttpMessageInvoker')); MonoApiHelper.Intercept(kInvoker, 'SendAsync', { onEnter: (args) => { console.log(`[*] HttpClientHandler.SendAsync called`); let self = args[0]; let handler = MonoApiHelper.ClassGetFieldFromName(kInvoker, '_handler'); let cur = MonoApiHelper.FieldGetValueObject(handler, self); if (cur.equals(pClientHandler)) return; // Already bypassed. MonoApi.mono_field_set_value(self, handler, pClientHandler); console.log(`[+] Replaced with default handler @ ${pClientHandler}`); } }); console.log('[+] Hooked HttpMessageInvoker.SendAsync'); hooked = true; } else { console.log('[-] HttpClientHandler not found'); }

Running the script gives the following output:

$ frida -U com.test.sample -l dist/xamarin-unpin.js --no-pause ____ / _ | Frida 12.8.7 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://www.frida.re/docs/home/ Attaching... [+] Created Default HttpClientHandler @ 0xa0120fc8 [+] Hooked HttpMessageInvoker.SendAsync with DefaultHttpClientHandler technique [-] ServicePointManager validation callback not found. [+] Done! Make sure you have a valid MITM CA installed on the device and have fun. [*] HttpClientHandler.SendAsync called [+] Replaced with default handler @ 0xa0120fc8

As seen above, the SendAsync hook has worked as expected and the HttpClientHandler got replaced by a default handler. Subsequent SendAsync calls will check the handler object and avoid replacing it if it is already hijacked. The screen capture below shows the sample application making a request before and after running the bypass script. The first request gives an SSL exception (as expected) because of the installed callback that always returns false. The second request triggers the hook, which replaces the client handler and returns execution to the HTTP client, hijacking the validation process generically for any HttpClient instance without having to scan memory to find them.

Conclusion

Xamarin and Mono are quickly evolving projects. This technique appears to work very well with the current (Mono 6.0+) framework versions but might require some modifications to work with older or future versions. We hope that sharing the method used to understand and tackle the problem will be useful to the security community in developing similar methods when performing mobile testing engagements.

The complete repository containing the code and pre-build Frida scripts can be found on Github.

Future Work

The Frida script has been tested on our sample application with regular build options, without ahead-of-time compilation and with the .NET Core method ( HttpClientHandler ) and works reliably. There are however many scenarios that can occur with Xamarin and we were not able to test all of them. More specifically, any of the following has not been tested and could be an area of future development:

.NET Framework applications which use ServicePointManager

iOS Applications with Full AOT

Android Applications with Partial AOT

Android Applications with Full AOT

If you try the script and run into issues, please open a bug on our issue tracker so we can improve it. Even better, if you end up fixing some issues, we’d be happy to merge your pull requests. And lastly, if you have APKs for one of the untested scenarios and feel like sharing them with us, it will help us ensure that the script works in more cases.

References