Bypassing Android’s Network Security Configuration

With the release of Android Nougat (Android 7) came a new security feature called Network Security Configuration [1].

This new feature arrived with the intention of allowing developers to customise their network security settings without modifying app code. Additional modification was also included in the default configuration for connections to SSL/TLS services; if the application targets an SDK higher or equal to 24, only the system certificates are trusted.

All of the above has an impact in the way an Android mobile application assessment is performed. If the HTTPS traffic needs to be intercepted, then a proxy certificate must be installed, but it is going to be installed in the ‘user certificates’ container, which is not trusted by default.

Here, we’ll aim to explain how the new mechanism works and how the default behaviour could be modified by either re-compiling the application or hooking the mechanism on runtime. During a mobile application assessment on Android 7 or higher, these procedures are essential in order to intercept HTTPS traffic between the application and the server.

How to use it as a developer

To modify the default configuration, an XML file that specifies the custom configuration has to be created on the resources directory. The piece of code below shows an example of a configuration file that uses the user certificates container for all HTTPS connections made by the application.

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<certificates src="system"/>
<certificates src="user"/>
</trust-anchors>
</base-config>
</network-security-config>

Additionally, the file has to be referenced from the Android Manifest file, which introduces the key android:networkSecurityConfig on the application tag, as shown here:

<?xml version="1.0" encoding="utf-8"?>
<manifest ... >
<application android_networkSecurityConfig="@xml/network_security_config"
... >
...
</application>
</manifest>

How to bypass it as a pentester

Re-compiling

If the assessed application is executed on Android 7 or higher - and the targetSdkVersion key is configured to version 24 (Android 7) or higher - the application probably uses the default configuration. Hence, the user certificates (i.e. proxy CA certificate) will not be trusted by the application.

The usual approach to modifying the default configuration would be to re-compile the application after inserting an XML, which activates the use of the certificate container. Once we have the APK, this process can be achieved using apktool [2], which allows the application to be manipulated.

The first task is to de-compile the application with the de-compile flag of apktool. After this process completes, an XML file has to be created on the resources directory and the AndroidManifest.xml file has to be modified to point to the Network Security Configuration file. At this point we can compile the application again with apktool and sign the generated APK file with the jarsigner tool, provided by the Java JDK.

When the APK is re-signed using an arbitrary certificate, it can be installed in the mobile phone using adb (Android Device Bridge). If the mobile is configured to send the traffic via an intermediate proxy, like Burp Suite, the HTTPS traffic could be intercepted as long as the CA certificate is installed on the system.

Hooking on runtime

However, in some situations the process described above is not possible. For example, if the application uses sharedId to share the same ID of another application, and hence access its data directly, then Android restricts our scenario to only applications signed by the same certificate. If an application is therefore re-compiled and re-signed, this feature is made redundant as it would not be possible to sign the modified APK with the original certificate used by the developers of the application.

For this kind of scenario dynamic instrumentation is useful because it allows the application’s behaviour at runtime to be modified without modifying the application itself. To perform this process, a Frida script will be created which adjusts the Network Security Configuration default behaviour on applications that target SDK version 24 or higher.

The android.security.net.config package [3] implements the Network Security Configuration module, while the main class, ManifestConfigSource, loads either the custom configuration specified in the XML file or the default configuration (if the resources file does not exist). You can see this below:

package android.security.net.config;

public class ManifestConfigSource implements ConfigSource {

. . .

private ConfigSource getConfigSource() {
synchronized (mLock) {

. . .

if (mConfigResourceId != 0) {

. . .

source = new XmlConfigSource(mContext, mConfigResourceId, debugBuild, mTargetSdkVersion, mTargetSandboxVesrsion);
} else {
. . .
source = new DefaultConfigSource(usesCleartextTraffic, mTargetSdkVersion, mTargetSandboxVesrsion);
}
mConfigSource = source;
return mConfigSource;
}
}
. . .
}

The DefaultConfigSource class, which is defined as a private class within the ManifestConfigSource class, is the one used if the configuration is not modified using an XML file.

package android.security.net.config;

public class ManifestConfigSource implements ConfigSource {
...

private static final class DefaultConfigSource implements ConfigSource {
private final NetworkSecurityConfig mDefaultConfig;
public DefaultConfigSource(boolean usesCleartextTraffic, int targetSdkVersion,
int targetSandboxVesrsion) {
mDefaultConfig = NetworkSecurityConfig.getDefaultBuilder(targetSdkVersion,
targetSandboxVesrsion)
.setCleartextTrafficPermitted(usesCleartextTraffic)
.build();
}
@Override
public NetworkSecurityConfig getDefaultConfig() {
return mDefaultConfig;
}
@Override
public Set<Pair<Domain, NetworkSecurityConfig>> getPerDomainConfigs() {
return null;
}
}
}

As can be seen in the constructor, it receives three parameters, one of them being the SDK version which targets the application. This value is used to build the NetworkSecurityConfig class, using the getDefaultBuilder() method, which is shown on the next piece of code. As it can be seen, the last block of code loads the user certificates if the targetSdkVersion is less than or equal to Android Marshmallow (Android 6.0), which is the SDK version 23.

package android.security.net.config;

public final class NetworkSecurityConfig {
...

public static final Builder getDefaultBuilder(int targetSdkVersion, int targetSandboxVesrsion) {
Builder builder = new Builder()
.setHstsEnforced(DEFAULT_HSTS_ENFORCED)
// System certificate store, does not bypass static pins.
.addCertificatesEntryRef(
new CertificatesEntryRef(SystemCertificateSource.getInstance(), false));
final boolean cleartextTrafficPermitted = targetSandboxVesrsion < 2;
builder.setCleartextTrafficPermitted(cleartextTrafficPermitted);
// Applications targeting N and above must opt in into trusting the user added certificate
// store.
if (targetSdkVersion <= Build.VERSION_CODES.M) {
// User certificate store, does not bypass static pins.
builder.addCertificatesEntryRef(
new CertificatesEntryRef(UserCertificateSource.getInstance(), false));
}
return builder;
}

...

With this in mind, a Frida script which hooks the constructor of the DefaultConfigSource class and changes the value of the targetSdkVersion variable could be created. In addition, the script will also hook to the getDefaultBuilder() method to ensure that the value is modified, even if the Android code changes this constructor in the future.

Java.perform(function(){
var ANDROID_VERSION_M = 23;

var DefaultConfigSource = Java.use("android.security.net.config.ManifestConfigSource$DefaultConfigSource");
var NetworkSecurityConfig = Java.use("android.security.net.config.NetworkSecurityConfig");

DefaultConfigSource.$init.overload("boolean", "int").implementation = function(usesCleartextTraffic, targetSdkVersion){
console.log("[+] Modifying DefaultConfigSource constructor");
return this.$init.overload("boolean", "int").call(this, usesCleartextTraffic, ANDROID_VERSION_M);
};

DefaultConfigSource.$init.overload("boolean", "int", "int").implementation = function(usesCleartextTraffic, targetSdkVersion, targetSandboxVersion){
console.log("[+] Modifying DefaultConfigSource constructor");
return this.$init.overload("boolean", "int", "int").call(this, usesCleartextTraffic, ANDROID_VERSION_M, targetSandboxVersion);
};

NetworkSecurityConfig.getDefaultBuilder.overload("int").implementation = function(targetSdkVersion){
console.log("[+] getDefaultBuilder original targetSdkVersion => " + targetSdkVersion.toString());
return this.getDefaultBuilder.overload("int").call(this, ANDROID_VERSION_M);
};

NetworkSecurityConfig.getDefaultBuilder.overload("int", "int").implementation = function(targetSdkVersion, targetSandboxVersion){
console.log("[+] getDefaultBuilder original targetSdkVersion => " + targetSdkVersion.toString());
return this.getDefaultBuilder.overload("int", "int").call(this, ANDROID_VERSION_M, targetSandboxVersion);
};
});

At this point, the Android application which targets SDK 24 or higher can be spawned by loading the above script with Frida, and the traffic can then be intercepted using an HTTP proxy such as Burp Suite.

$ frida -U -l ntc.js -f <package_name> --no-pause

We hope you liked the article and it helps you to overcome different barriers that could face during a research or an assessment.

If you have any question or feedback, you can contact me via Twitter @AdriVillaB.

References

[1] https://developer.android.com/training/articles/security-config.html?hl=en
[2] https://ibotpeaches.github.io/Apktool/
[3] https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/security/net/config

Written by Adrian Villa
First published on 03/11/17

Call us before you need us.

Our experts will help you.

Get in touch