It all started with the new xamarin build - maui (.NET Multi-platform App UI).
When I faced an engagement which was a must for me to bypass SSL pinning, where unfortunately I failed. I was new dealing with MAUI. I did not know such framework existed, all I knew was the old granny xamarin.
Anyways, I took it as a challenge and started grinding thru docs and vids, until I kinda understood how things work.
The most important thing that was introduced in MAUI was AOT, ahead of time compile. You can check more about it here.
NOTE
AOTmeans your C# code is compiled to native machine code before the app even runs. NoJITcompilation at runtime. The originalxamarinbypass scripts assumedJITwas happening, which is why they broke.
Anyways, when it comes to xamarin SSL pinning bypass, there is no way we could not mention gosecure and alxbl for their hard work on their blog post and frida-xamarin-unpin.
Now after reading carefully I saw in the last part of the blog he mentioned that he did not make this work on AOT yet, full or partial, and they are still not working with .NET Framework applications which use ServicePointManager.
Soo here lights up my mind and says why not? Dude we are in 2026, lets fire up some agents and dig up.
The Plan
So first I made some drafts of the plan and here is the conclusion:
- We needed to recreate the original
xamarinSSLpinning bypass to make it work withfrida17 apis. This was a prerequisite to be done, since it had a lot of functions and methods that do not work anymore infrida17. - We needed some app that we know to test on. Luckily, alxbl already provided the src code of the
xamarinsample app. Cloned it, rebuilt it using latestmaui, changed the deprecated functions and methods, added some verbose logging, and compiled usingJITfirst thenAOTsecond after gettingJITto work (at least we know that it should work) :“D - We needed to understand why the original script crashes on
MAUIusing theJITcompile, and build a generic bypass that does not hardcode any app-specific class names. - We needed to test it on an
AOTcompiled app and check its behaviour. - Document everything and try to understand it “still trying :“D”.
Step 1 - Fixing Frida 17 Compatibility
Before even touching maui, the original script would not even load on frida 17. It crashes immediately with:
TypeError: not a functionRoot cause: frida 17.0.0 removed the static Module.findExportByName helpers. The frida-mono-api library was calling them at two spots.
Fix: two-line patch.
Module.findExportByName(monoModule.name, sym)→monoModule.findExportByName(sym)Module.findExportByName(null, sym)→Module.findGlobalExportByName(sym)
I forked both repos and patched them:
- https://github.com/ymuuuu/frida-mono-api/tree/frida-17
- https://github.com/ymuuuu/frida-xamarin-unpin/tree/frida-17
Also opened upstream PRs back to GoSecure so the community can benefit:
- https://github.com/GoSecure/frida-mono-api/pull/1
- https://github.com/GoSecure/frida-xamarin-unpin/pull/19
Verified the patched script works on the original xamarin sample APK. Good, baseline established.
Step 2 - Building the MAUI Test Target
I rebuilt alxbl’s sample app for .NET MAUI (net9.0-android). The app registers an HttpClient with a custom ServerCertificateCustomValidationCallback, exactly like the xamarin sample but using the unified BCL.
Compiled two versions:
- JIT -
RunAOTCompilation=false(debug / default) - AOT -
RunAOTCompilation=true(release / optimized)
Both APKs are included in the repo if you want to test it: @ymuuuu/repo
Step 3 - The Crash
Ran the patched xamarin script against the maui JIT app. It loads, hooks SendAsync, first request fires - then:
Error: access violation accessing 0x10
at RuntimeInvoke (mono-api/src/mono-api-helper.js:71)
at onEnter (src/main.js:55)TIPWhy 0x10? The original script looks for a static factory method called
CreateDefaultHandler()insideHttpClientHandler. Onxamarin(monoBCLfork) this exists. Onmaui(unifiedBCL/dotnet/runtime) it does not.mono_class_get_method_from_namereturns NULL. Passing NULL tomono_runtime_invokedereferences offset+0x10of the_MonoMethodstruct - thesignaturefield. The fault address tells you exactly which field was hit.
The fix was not to just guard the NULL. We needed a completely different approach for maui because the HttpClientHandler internals changed.
Find more here Mono Docs
The MAUI Approach - Four Techniques in One Script
After a lot of trial and error (and enumerating fields at runtime to see what actually exists), the script now has four techniques depending on what it finds:
| # | Technique | When it applies |
|---|---|---|
| 1 | Modern path - CreateDefaultHandler() factory swap | Mono >= 6.0, xamarin classic |
| 2 | MAUI path - mono_object_new + ctor + _handler swap + validator transfer + delegate introspection | unified BCL, JIT or AOT |
| 3 | SocketsHttpHandler path - _settings._sslOptions walk | UseNativeHttpHandler=false or non-maui .NET android apps |
| 4 | Legacy path - ServicePointManager nulling | Mono < 6.0, best-effort |
How the MAUI Path Works (The Juicy Part)
When the script detects it is running on maui (no CreateDefaultHandler factory), it switches to the maui path:
1. Hook HttpMessageInvoker.SendAsync
This is the entry point. Every HttpClient request goes through here.
2. Unwrap DelegatingHandler Wrappers
Production apps love wrapping handlers:
LifetimeTrackingHttpMessageHandler -> LoggingScopeHttpMessageHandler -> ResilienceHandler -> SocketsHttpHandlerThe script walks the _innerHandler chain, resolving the field on the runtime class of each wrapper (not the base class - that was a bug that read wrong memory offsets).
3. Detect the Real Handler
- If the unwrapped handler is
SocketsHttpHandler→ use technique #3 - If it is
HttpClientHandler/AndroidMessageHandler→ use technique #2
4. The Swap + Transfer
For HttpClientHandler:
- Allocate a fresh
HttpClientHandlerviamono_object_new+ parameterless ctor - Swap it into
HttpMessageInvoker._handler - Transfer the old underlying handler’s
_serverCertificateCustomValidatoronto the fresh one
Why transfer? Without it, the fresh handler has a
NULLvalidator, soSetupSSL()falls back toandroidsystem trust - which onandroid7+ rejects user-installedCAs. With the transfer,SetupSSL()takes the custom-validator branch.
5. The Generic Hook - Stage 3 Delegate Introspection
This is the part I am most proud of. Instead of hardcoding the app’s namespace + class + method name (like SampleApp.MauiProgram.ValidateCertificate), the script introspects the live validator instance at runtime (Let’s say that it is a semi auto detector):
validator instance
-> <Callback>k__BackingField (the Func<...> delegate)
-> MonoDelegate.layout +0x28 = MonoMethod*
-> mono_compile_method = native entry point
-> Interceptor.attach onLeave -> retval.replace(ptr(1))The MonoDelegate layout on ARM64 LP64 is stable across mono/mono and dotnet/runtime:
| Offset | Field |
|---|---|
| +0x00 | MonoObject header |
| +0x10 | method_ptr |
| +0x18 | invoke_impl |
| +0x20 | target |
| +0x28 | method (MonoMethod)* |
So we read +0x28, compile the method, and hook it. No app-specific names anywhere in the script.
IMPORTANTThe hook is deduplicated via a
_hookedMethodsSet, so even if multipleHttpClientinstances or lazy-init validators appear, each callback method is hooked exactly once.
The AOT Surprise
Remember the blog said “iOS Full AOT not supported”? I tested against the AOT-compiled maui app expecting it to fail. It worked.
CAUTIONPlot twist: on
androidMonoVMAOT,mono_compile_methoddoes not return aJITstub. It returns the pre-compiledAOTnative entry point.Interceptor.attachpatches it directly, same asJIT. The Stage 3 hook successfully attached toValidateCertificate(AOT-compiled), forcedreturn=true, and the request returnedOK.This is an
android-specific finding.iOSusesLLVMAOTwith a different runtime layout - not addressed.
There was one small hiccup: a non-fatal access violation accessing 0x68 during script load. This was the ServicePointManager fallback block trying to load the System assembly (which does not exist in unified BCL) and dereferencing NULL. Fixed by guarding the entire legacy block behind if (!hooked).
SocketsHttpHandler Support
Some apps set UseNativeHttpHandler=false, which makes the bottom handler SocketsHttpHandler instead of AndroidMessageHandler. Its certificate callback lives at:
SocketsHttpHandler._settings._sslOptions.<RemoteCertificateValidationCallback>k__BackingFieldWhen the script detects SocketsHttpHandler, it walks this field chain and hooks the callback delegate using the same Stage 3 technique. If _sslOptions is null, the app has not configured a custom callback - the script logs this and returns cleanly.
DEBUG Flag
Tired of noisy frida consoles? The script has a DEBUG flag at the top:
const DEBUG = true; // set to false for quiet mode
function dbg(...args) { if (DEBUG) console.log(...args); }How to Use It
TIPPrerequisites: rooted
androiddevice,fridaserver running,MITMCAinstalled in the user trust store.
# 1. clone both repos
git clone https://github.com/ymuuuu/frida-mono-api-maui.git
git clone https://github.com/ymuuuu/frida-maui-unpin.git
# 2. build
cd frida-mono-api-maui && npm i
cd ../frida-maui-unpin && npm i && npm run build
# 3. launch the app, wait for mono init, then attach
frida -U -p $(adb shell "pidof -s com.test.sample.maui") -l ./dist/maui-unpin.js
# 4. trigger an HTTPS request in the appExpected output with DEBUG = false:
[+] Hooked HttpMessageInvoker.SendAsync with mono_object_new technique (MAUI / unified BCL)
[+] Done!
Make sure you have a valid MITM CA installed on the device and have fun.
[+] Stage 3: hooked validator method (MonoMethod=0x..., name=ValidateCertificate, JIT=0x...), forced return=true
Repos
frida-mono-api-maui standalone Mono API + MAUI helpers https://github.com/ymuuuu/frida-mono-api-maui
frida-maui-unpin the bypass script + sample APK source + pre-built JIT/AOT APKs https://github.com/ymuuuu/frida-maui-unpin
frida-mono-api (frida-17 fork) patched upstream dependency https://github.com/ymuuuu/frida-mono-api/tree/frida-17 originally by freehuntx, maintained by GoSecure
frida-xamarin-unpin (frida-17 fork) patched upstream script https://github.com/ymuuuu/frida-xamarin-unpin/tree/frida-17 originally by alxbl at GoSecure
Credits
- @freehuntx - original
frida-mono-apiand thexamarinbypass concept - GoSecure - maintained
frida-xamarin-unpinand theextrabranch offrida-mono-api - alxbl - original author of
frida-xamarin-unpin
I am still learning along the way, most of the work was done by digging thru docs and using some high thinking AI Agents, mistakes are meant to happen, if you have any edit, suggestion or an adjustment, please hit me up.
