Hacking Book | Free Online Hacking Learning


research on intercepting android flutter application traffic

Posted by truschel at 2020-02-27

Flutter is Google's new open source mobile development framework, which allows developers to write code base and build Android, IOS, web and desktop applications on this basis. The flutter application is written in dart, a language created seven years ago by Google.

Flutter Android、iOS、Web 桌面

Generally speaking, in the process of security assessment and bounty, we need to intercept the traffic between the mobile application and the backend, which is usually done by adding burp as the interception agent. Other agent applications are difficult to meet their needs, but burp has good performance.



Flutter uses dart, which does not use system CA to store data

Dart uses the list of CAS compiled into the application

Dart does not support proxy on Android, so we need to use proxydroid with iptables

Get the session verify cert chain function in x509.cc to disable chain related validation

We can use the script at the bottom of this article directly, or follow the steps below to get the correct bytes or offsets.

Test setup

To perform my tests, I installed the flutter plug-in and created a flutter application with a default interactive button and the ability to increment counters. I modify it to get the URL through the httpclient class:

flutter Flutter class _MyHomePageState extends State<MyHomePage> { int _counter = 0; HttpClient client; _MyHomePageState() { _start(); } void _start() async { client = HttpClient(); } void _incrementCounter() { setState(() { if(client != null) { client .getUrl(Uri.parse('http://www.nviso.eu')) // produces a request object .then((request) => request.close()) // sends the request .then((response) => print("SUCCESS - " + response.headers.value("date"))); _counter++; } }); }

The application can be compiled by using fluent build AOT and pushed to the device through ADB install.

Each time you press the button, a message is sent to http://www.nviso.eu and printed to the device log if successful.


On my device, I installed Frida through magisk Frida server, and my burp certificate was added to the system ca store through the magisktrustusercerts module. However, burp does not see any traffic even if the application log indicates that the request was successful.

Magisk-Frida-Server Frida Burp MagiskTrustUserCerts

Send traffic to agents through proxydroid / iptables

Httpclient has a findproxy method, which is explained clearly in the document: by default, all traffic is directly sent to the target server, regardless of any proxy settings:

Sets the function used to resolve the proxy so that the proxy can open an HTTP connection for the specified URL. If this feature is not set, a direct connection will always be used.

The application can set this property to httpclient.findproxyfromenvironment, which searches for specific environment variables, such as HTTP? Proxy and HTTPS? Proxy. Even though the application will be compiled using this implementation, it will be useless on Android, because all applications are children of the initial zygote process, without these environment variables.

HttpClient.findProxyFromEnvironment http_proxy https_proxy zygote

Here we can also define a custom findproxy that returns the preferred proxy. After modifying the test application, I found that this configuration sent all HTTP data to my proxy:

findProxy client.findProxy = (uri) { return "PROXY"; };

Of course, we can't modify the application during a black box evaluation, so an alternative approach is needed. However, we have iptables to route all traffic from the device to our agents. On the rooted device, the proxydroid handles it very well, and we can see that all HTTP traffic flows through burp.

rooted ProxyDroid

Block HTTPS traffic

This is the trickier part. If I change the URL to HTTPS, burp will show that the SSL handshake failed. This is strange because my device is set up to include my burp certificate as a trusted root ca.

After some research, I finally got a GitHub problem to explain windows, but it is also applicable to Android: dart uses Mozilla's NSS library to generate and compile its own keystore.

Windows GitHub Android:Dart

This means that we cannot bypass SSL authentication by adding a proxy CA to the system ca store. In order to solve this problem, we must go deep into libflitter.so and find out that we need to fix or hook to verify our certificate. Dart uses Google's boringssl to handle all SSL related content. Fortunately, dart and boringssl are open source.


When sending HTTPS traffic to burp, the flutter application actually throws an error, which we can use as a starting point:

E/flutter (10371): [ERROR:flutter/runtime/dart_isolate.cc(805)] Unhandled exception: E/flutter (10371): HandshakeException: Handshake error in client (OS Error: E/flutter (10371): NO_START_LINE(pem_lib.c:631) E/flutter (10371): PEM routines(by_file.c:146) E/flutter (10371): NO_START_LINE(pem_lib.c:631) E/flutter (10371): PEM routines(by_file.c:146) E/flutter (10371): CERTIFICATE_VERIFY_FAILED: self signed certificate in certificate chain(handshake.cc:352)) E/flutter (10371): #0 _rootHandleUncaughtError. (dart:async/zone.dart:1112:29) E/flutter (10371): #1 _microtaskLoop (dart:async/schedule_microtask.dart:41:21) E/flutter (10371): #2 _startMicrotaskLoop (dart:async/schedule_microtask.dart:50:5) E/flutter (10371): #3 _runPendingImmediateCallback (dart:isolate-patch/isolate_patch.dart:116:13) E/flutter (10371): #4 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:173:5)

The first thing we need to do is find this error in the boring SSL library. The error actually shows us where it was triggered: handshake.cc: 352. Handshake.cc is really part of the boringssl library and contains the logic to perform certificate validation. The code in line 352 looks like this, which is probably the error we saw. The line numbers do not match exactly, but this is most likely the result of version differences.

BoringSSL handshake.cc:352 Handshake.cc BoringSSL if (ret == ssl_verify_invalid) { OPENSSL_PUT_ERROR(SSL, SSL_R_CERTIFICATE_VERIFY_FAILED); ssl_send_alert(ssl, SSL3_AL_FATAL, alert); }

This is part of the SSL verify peer cert function, which returns the SSL verify result enumeration, which is defined in SSL. H in line 2290:

enum ssl_verify_result_t BORINGSSL_ENUM_INT { ssl_verify_ok, ssl_verify_invalid, ssl_verify_retry, };

If we can change the return value of SSL verify peer cert to SSL verify OK (= 0). However, this method is doing a lot of work, and Frida can only change the return value of the function. If we change this value, it will still fail because of the call to the ssl_send_alert() function above.

ssl_verify_peer_cert ssl_verify_ok(= 0) ssl_send_alert()

Let's find a better way. Just above the fragment from handshake.cc is the following code, which is part of the validation chain method:

ret = ssl->ctx->x509_method->session_verify_cert_chain( hs->new_session.get(), hs, &alert) ? ssl_verify_ok : ssl_verify_invalid;

The session verify cert chain function is defined in SSL x509.cc at line 362. This function returns the original data type and is a better alternative. If the check in this function fails, it reports the problem through OpenSSL? Put? Error, but it has no side effects like the SSL? Verify? Peer? Cert function. OpenSSL? Put? Error is the macro defined at line 418 in err. H, which includes the source filename. This is the same macro as the error used for the flutter application.

session_verify_cert_chain ssl_x509.cc ssl_verify_peer_cert OPENSSL_PUT_ERROR #define OPENSSL_PUT_ERROR(library, reason) \ ERR_put_error(ERR_LIB_##library, 0, reason, __FILE__, __LINE__)

Now that we know which function to lock, we need to find it in libflitter. So. The OpenSSL put error macro can be called multiple times in the session verify cert chain function to find the right method easily using ghidra. So import the library into ghidra, use search to find the string and search for x509.cc

There are only four xrefs, so it's easy to look at them and find a function that looks like session verify cert chain:


One of the functions requires 2 integers, 1 'undefined' and contains a single call to openssl_put_error (fun_00316500). In my version of libflitter.so, this is fun b330. What we usually do now is to calculate the offset of this function from one of the exported functions. I usually copy the first 10 bytes of the function and check the frequency of the pattern. If it only appears once, I know I found the feature and I can lock it. Because I can often use the same script for different versions of the library. However, it is more difficult to use the offset based method.

OPENSSL_PUT_ERROR(FUN_00316500) libflutter.so FUN_0034b330

So now let's let Frida search the libflitter.so library for this pattern:

var m = Process.findModuleByName("libflutter.so"); var pattern = "2d e9 f0 4f a3 b0 82 46 50 20 10 70" var res = Memory.scan(m.base, m.size, pattern, { onMatch: function(address, size){ console.log('[+] ssl_verify_result found at: ' + address.toString()); }, onError: function(reason){ console.log('[!] There was an error scanning memory'); }, onComplete: function() { console.log("All done") } });

Running this script on my fluent application produces a result:

(env) ~/D/Temp » frida -U -f be.nviso.flutter_app -l frida.js --no-pause [LGE Nexus 5::be.nviso.flutter_app]-> [+] ssl_verify_result found at: 0x9a7f7040 All done

Now we just need to use interceptor to change the return value to 1 (true):

function hook_ssl_verify_result(address) { Interceptor.attach(address, { onEnter: function(args) { console.log("Disabling SSL validation") }, onLeave: function(retval) { console.log("Retval: " + retval) retval.replace(0x1); } }); } function disablePinning() { var m = Process.findModuleByName("libflutter.so"); var pattern = "2d e9 f0 4f a3 b0 82 46 50 20 10 70" var res = Memory.scan(m.base, m.size, pattern, { onMatch: function(address, size){ console.log('[+] ssl_verify_result found at: ' + address.toString()); // Add 0x01 because it's a THUMB function // Otherwise, we would get 'Error: unable to intercept function at 0x9906f8ac; please file a bug' hook_ssl_verify_result(address.add(0x01)); }, onError: function(reason){ console.log('[!] There was an error scanning memory'); }, onComplete: function() { console.log("All done") } }); } setTimeout(disablePinning, 1000)

After setting up the proxydroid and launching the application using this script, we can now finally see the HTTP traffic:


I've tested this on a number of flutter applications, and this approach works for all of them. As the boring SSL library is likely to remain fairly stable, this approach may play a role in the future.

Flutter BoringSSL

Disable SSL fixation (securitycontext)

Finally, let's see how to bypass SSL pinning. One way to do this is to define a new securitycontext that contains a specific certificate. Although this is not technically SSL fixed, it is often implemented to prevent easy eavesdropping of communication channels.

SSL Pinning SecurityContext

For my application, I added the following code to accept only my burp certificate. The securitycontext constructor takes a parameter with trustedroots, which defaults to false.

SecurityContext withTrustedRoots ByteData data = await rootBundle.load('certs/burp.crt'); SecurityContext context = new SecurityContext(); context.setTrustedCertificatesBytes(data.buffer.asUint8List()); client = HttpClient(context: context);

The application will now automatically accept our burp agent as a certificate for any web site, indicating that this method can be used to specify specific certificates that the application must adhere to. If we switch it to the nviso.eu certificate now, we can't block its connection any more.


However, the Frida script listed above has bypassed this root CA pinning implementation because the underlying logic still relies on the same method as the boringssl library.


Disable SSL (SSL? Ping? Plugin)

One of the ways that flutter developers want to perform SSL pinning is through the SSL? Pinning? Plugin flutter plug-in. This plug-in is actually designed to send an HTTPS connection and verify the certificate, after which the developer will trust the channel and execute the HTTPS request:

`void testPin()` `async` `{` `List<String> hashes =` `new` `List<String>();` `hashes.add(``"randomhash"``);` `try` `{` `await` `SslPinningPlugin.check(serverURL:` `"[https://www.nviso.eu](https://www.nviso.eu/)"``, headerHttp :` `new` `Map(), sha: SHA.SHA1, allowedSHAFingerprints: hashes, timeout : 50);` `doImportanStuff()` `}``catch``(e)` `{` `abortWithError(e);` `}` `}`

This plug-in is the bridge of Java implementation, and we can easily connect with Frida:

function disablePinning() { var SslPinningPlugin = Java.use("com.macif.plugin.sslpinningplugin.SslPinningPlugin"); SslPinningPlugin.checkConnexion.implementation = function() { console.log("Disabled SslPinningPlugin"); return true; } } Java.perform(disablePinning)


The methods mentioned in this paper have research value. Since dart and boring SSL are open source, our experiment is very smooth. With only a few strings, it's easy to find the right place to disable SSL validation logic, even without any symbols. My method of scanning functions is not always effective, but because boringssl is very stable, the method mentioned in this article also has some effect.