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
- Mono Runtime Documentation: https://www.mono-project.com/docs/advanced/runtime/docs/
- Mono Compilation Modes: https://www.mono-project.com/docs/advanced/aot/
- Mono on Github: https://github.com/mono/mono
- ServicePointManager deprecation: https://github.com/xamarin/xamarin-android/issues/3682#issuecomment-535679023
- Mono Tiered Compilation: https://github.com/mono/mono/issues/16018
- Code Release: https://github.com/GoSecure/frida-xamarin-unpin
- Fridax – A Xamarin hacking framework: https://github.com/NorthwaveNL/fridax
CAS D'UTILISATION
Cyberrisques
Mesures de sécurité basées sur les risques
Sociétés de financement par capitaux propres
Prendre des décisions éclairées
Sécurité des données sensibles
Protéger les informations sensibles
Conformité en matière de cybersécurité
Respecter les obligations réglementaires
Cyberassurance
Une stratégie précieuse de gestion des risques
Rançongiciels
Combattre les rançongiciels grâce à une sécurité innovante
Attaques de type « zero-day »
Arrêter les exploits de type « zero-day » grâce à une protection avancée
Consolider, évoluer et prospérer
Prenez de l'avance et gagnez la course avec la Plateforme GoSecure TitanMC.
24/7 MXDR
Détection et réponse sur les terminaux GoSecure TitanMC (EDR)
Antivirus de nouvelle génération GoSecure TitanMC (NGAV)
Surveillance des événements liés aux informations de sécurité GoSecure TitanMC (SIEM)
Détection et réponse des boîtes de messagerie GoSecure TitanMC (IDR)
Intelligence GoSecure TitanMC
Notre SOC
Défense proactive, 24h/24, 7j/7