Flutteris Google's mobile UI framework, which can quickly build high-quality native user interfaces on iOS and Android. Flutter applications are written in Dart, a language created by Google more than 7 years ago.
In most cases, we would add Burp as an intercepting proxy to intercept the communication traffic between the mobile application and its backend (for security assessments, etc.). Although it may be a bit difficult to proxy Flutter applications, it is absolutely possible.
TL;DR
Flutter is written in Dart, so it does not use the system CA storage
Dart uses a CA list compiled into the application
Dart does not support proxy on Android, so please use ProxyDroid with iptables
Hook in x509.ccsession_verify_cert_chainFunctions with disabled chain validation
You can directly use the script at the bottom of this article, or follow the steps below to get the correct byte or offset.
Test settings
To perform my tests, I installedFlutter pluginAnd created a Flutter application, which comes with a default interactive button to increment the counter. I modified it to get the URL through the HttpClient class:
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 using flutter build aot and then pushed to the device via adb install.
Every time you press this button, it will send tohttp://www.nviso.euSend a call, and if successful, it will be printed to the device log.
On my device, I went throughMagisk-Frida-ServerI installed Frida, and my Burp certificate was passed throughMagiskTrustUserCertsModule added to the system CA storage. Unfortunately, no traffic was seen through Burp, even though the application logs show that the requests were successful.
Send traffic to the proxy through ProxyDroid/iptables
HttpClient has afindProxyThe method, whose documentation is very clear: by default, all traffic is sent directly to the target server without considering any proxy settings:
Set the function used to resolve the proxy server, which is used to open the HTTP connection to the specified URL. If this function 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 if the application is compiled with this implementation, it will be useless on Android because all applications are subprocesses of the initial zygote process, so there are no such environment variables.
You can also define a custom findProxy implementation that returns the preferred proxy. A quick modification to my test application indeed indicates that this configuration sends all HTTP data to my proxy server:
client.findProxy = (uri) {
return "PROXY 10.153.103.222:8888";
;
Of course, we cannot modify the application during the black-box evaluation period, so we need another method. Fortunately, we always have the iptables fallback to route all traffic from the device to our proxy. On rooted devices, ProxyDroid handles this problem well, and we can see that all HTTP traffic is flowing through Burp.
Intercept HTTPS traffic
This is a more tricky problem. If I change the URL to HTTPS, it will cause Burp SSL handshake failure. This is strange because my device is set to include my Burp certificate as a trusted root certificate.
After some research, I finally found one inGitHub issueI found an explanation of the problem on Windows, but it also applies to Android: Dart usesMozilla's NSS libraryGenerate andCompile your own Keystore.
This means we cannot bypass SSL verification by adding the proxy CA to the system CA storage. To solve this problem, we must delve into libflutter.so and find out what needs to be patched or hooked to verify our certificate. Dart uses Google's BoringSSL to handle all SSL-related content, fortunately both 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 inBoringSSL libraryFind this error. The error actually shows us the location of the trigger: handshake.cc:352.Handshake.ccIt is indeed part of the BoringSSL library and contains the logic for executing certificate verification. The code at line 352 is as follows, which is very likely the error we see. The line number does not match exactly, but this is likely due to version differences.
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 ssl_verify_result_t enumeration, located at line 2290:ssl.his defined in:
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), then we can continue. However, there are many things happening in this method, and Frida can only change the return value of the function. If we change this value, it will still fail due to the above ssl_send_alert() function call (believe me, I've tried it).
Let's find a better way to find a hook. The code snippet above handshake.cc is the actual part of the method to verify the chain:
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 on line 362ssl_x509.ccis defined. This function also returns the original data type (boolean) and is a better hook option. If the check in this function fails, it only reports the problem through OPENSSL_PUT_ERROR, but it does not have the same problem as the ssl_verify_peer_cert function. OPENSSL_PUT_ERROR isLine 418 in err.hThe defined macro, which includes the source file name. This is the same as the macro used for errors in Flutter applications.
#define OPENSSL_PUT_ERROR(library, reason) \
ERR_put_error(ERR_LIB_##library, 0, reason, __FILE__, __LINE__)
Since we know which function to hook, what we need to do now is to find it in libflutter.so. The session_verify_cert_chain function calls the OPENSSL_PUT_ERROR macro multiple times, which can be easily found with Ghidra. Therefore, import the library into Ghidra, use Search -> Find Strings and search for x509.cc.
There are only 4 XREFs here, so it is easy to find a function that looks like session_verify_cert_chain:
One of the functions takes 2 integers, 1 'undefined', and includes a separate call to OPENSSL_PUT_ERROR(FUN_00316500). In my libflutter.so version, it is FUN_0034b330. What you need to do now is to calculate the offset of this function from an export function and hook it. I usually take a lazy approach, copying the first 10 bytes of the function and checking the frequency of the pattern. If it appears only once, I know I have found the function and can hook it. This is very useful because I often can use the same script for different versions of the library. Using the offset-based method is more difficult. This is very useful because I can often use the same script for different versions of the library. For the offset-based method, it is more difficult.
所以,现在我们让Frida在libflutter.so库中搜索这个模式:
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")
}
});
在我的Flutter应用程序上运行此脚本的结果如下:
(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
现在,我们只需使用Interceptor将返回值更改为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());
// 添加0x01因为这是一个THUMB函数
// 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 proxydroid and starting the application with this script, we can now see the HTTP traffic:
I have tested this on some Flutter applications, and this method works for all applications. Since the BoringSSL library is relatively stable, this method may remain effective for a long time in the future.
Disable SSL Pinning (SecurityContext)
Finally, let's see how to bypass SSL Pinning. One method is to define a new SecurityContext containing a specific certificate.
For my application, I added the following code to make it only accept my Burp certificate.SecurityContext constructorAccept a parameter withTrustedRoots, the default is false.
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 proxy as a certificate for any website. If we switch it to the nviso.eu certificate now, we will not be able to intercept the connection (request and response).
Fortunately, the Frida script listed above has bypassed this root-ca-pinning implementation because the underlying logic still relies on the same method of the BoringSSL library.
Disable SSL Pinning (ssl_pinning_plugin)
Flutter developers can disable SSL pinning (ssl_pinning_plugin) through one of the following methods: ssl_pinning_plugin One of the ways Flutter developers can perform SSL pinning is by sending an HTTPS connection and verifying the certificate, after which the developer will trust the communication and execute non-pinned HTTPS requests:
void testPin() async
{
List<String> hashes = new List<String>();
hashes.add("randomhash");
try
{
await SslPinningPlugin.check(serverURL: "https://www.nviso.eu", headerHttp : new Map(), sha: SHA.SHA1, allowedSHAFingerprints: hashes, timeout : 50);
doImportanStuff()
}catch(e)
{
abortWithError(e);
}
}
This plugin isJava implementationWe can use Frida to hook easily:
function disablePinning()
{
var SslPinningPlugin = Java.use("com.macif.plugin.sslpinningplugin.SslPinningPlugin");
SslPinningPlugin.checkConnexion.implementation = function()
{
console.log("Disabled SslPinningPlugin");
return true;
}
}
Java.perform(disablePinning)
Conclusion
This is a very interesting process, because both Dart and BoringSSL are open source, so the process went very smoothly. Since there are not many strings, it is very easy to find the correct location of the disabled SSL verification logic without any symbols. My method of scanning the function prologue (function prologue) may not always be effective, but since BoringSSL is very stable, it should be effective for a period of time in the future.
*Reference source:nvisoFB editor secist compiled, please indicate the source as FreeBuf.COM when转载

评论已关闭