Quick edit for clarification: This appears to only work for Flutter apps that use the build-in Dart HTTP Client code, if the Flutter app calls out to external Java libraries using PlatformMessages then this wont work!
I recently started looking at Android apps based on the Flutter framework, I’d not come across any before and after a pub discussion about something entirely unrelated managed to find one to break.
I fired it up on my mobile testing WiFi network and hit the bane of every app testers existence:
Wonderful! They’re doing something right by preventing Burps root CA from being used and I could probably bypass this with Frida. I pulled the .apk apart and found…
..sod all. After a bit of reading about the Flutter Engine it seems the majority of the work is performed by “libflutter.so”. This was made more apparent after starting “frida-trace” and setting up intercept scripts for all the usual SSL methods that you’d need to bypass, although they libraries were loaded I didn’t see a single call to any of the methods and the SSL connection still failed to start.
So I dug further, after some further research I found out that Flutter bundles the BoringSSL libraries into “libflutter.so” and performs its own verification steps rather than trust the OS’s systems. It also forces the use of a known set of Root certificates which goes someway to explain why I couldn’t MITM the connection.
Breaking out Ghidra and loading the shared object wasn’t much better as the library had been stripped of symbols, making it tough to find the verification functions. I fired up the app again and watched the output of logcat to see if it gave me any clues:
See the line containing “handshake.cc”? After some trawling through strings and references in Ghidra I found some methods that contained it:
The 0x160 seems to correspond to the line number in the source that generate the error, I grabbed the BoringSSL code and started looking for x509 verification functions. One cropped up in “ssl_x509.cc”:
Searching Ghidra for this file name showed up the full path string as expected, tracing cross-references to that string dumped me in the middle of a call to “FUN_00316500” above with a line number value that roughly matched the source file. Bingo!
To get this check to pass all I’d have to do would be:
- Calculate the actual address of the function in the phones memory
- Build a Frida Interceptor script to trap it
- Alter the return value to “true”
Calculating the offset of the function could be done by finding the Virtual address of a function we know the name of, working out the target functions offset from it and then adding that to the the actual address of the first function.
The Flutter shared object exports one function, “JNI_OnLoad” which is called by the Android runtime during startup, Frida could find the address of this easily so that made for a good base-function to start with. The offset of the X509 function from this could be calculated and added to the base address easily, setting up an interception for this showed repeated calls to the method whenever I forced the app to make a request:
See how the “ret” value is “0x0”? Lets patch that out:
This was definitely a much harder one to crack than most apps, I’m expecting people will hide behind that as a form of “security” and forget to secure the API’s the app interfaces with 🙂
I grabbed a few more Flutter apps and check the MD5 hashes of their “libflutter.so” files, it appears they differ in most cases which means that each app will need the offset of “ssl_crypto_x509_session_verify_cert_chain” calculating, I’m not sure if this can be automated or not given the lack of debug symbols but honestly its not that much of a problem for testing only a single app.