Android app penetration testing is a must when developing an application, especially if you deal with sensitive user information. To identify the weak spots in your application’s security, it is good practice to have it tested by mobile security experts. The testing method they use for this is called penetration testing.
Penetration tests, aka pentests, are simulated cyber attacks on your application designed to find exploitable vulnerabilities. Android developers can then use the penetration test results to improve the app’s security.
The comprehensive guide to Android app penetration testing
In this blog post, we will concentrate on the networking part of Android app penetration testing – specifically on the TLS protocol, and tips on making your app as secure as possible when connecting to a specific web service.
Furthermore, this post is the first in a series of articles about pentesting, which will present an overview of what you can expect when having your app pentested. The pentesting articles will also provide recommendations on the necessary preparations ahead of it.
Disclaimer: In our examples, we will use OkHttp for the integration part, because it is a well established library and probably the most popular one in the Android community.
TLS connection
Most applications today communicate with a web service of some kind. Therefore, it’s very important to have a secure connection.
The worst case scenario is having an app that uses HTTP connection without the Transport Layer Security (TLS) protocol. It will inevitably fail because your data will be transferred in plain text – and therefore, very easy to track. TLS is a core part of encrypted communication, which makes HTTPS calls secure and authenticated.
Even though it is based on SSL 3.0, you will often hear people using SSL as a synonym for TLS- That’s not 100% correct as SSL, a predecessor for TLS, was deprecated in 2015 by the IETF.
What does Android OS offer, with regard to TLS?
According to the documentation, Android supports TLS 1.2 since API level 16, and is enabled by default since level 21. Unfortunately, this is not 100% correct either.
Yes, you can rely on TLS 1.2 from API level 21/22 and above. However, you cannot count on that for API levels 16 to 19. There is an excellent article written by Ankush Gupta that describes this problem in more detail, so feel free to check it out if you want to know more about the how and why.
With the knowledge of what Android OS offers and the goal of achieving a secure connection using at least TLS 1.2, we’re moving on to the implementation guide.
Preparation phase in Android app penetration testing – What you’ll need to know
We will cover two specific cases in this section of the Android app penetration testing guide. The first case is when your minSdk is between API levels 16 and 19. In the other case, your minSdk is API level 21 or higher.
MinSdk between API levels 16 and 19
If you want TLS 1.2 enabled on versions before Android 5, you will have to extend the SSLSocketFactory
and create your own implementation. The implementation is quite straightforward, and would look something like this:
class TlsSocketFactory constructor(private val socketFactory: SSLSocketFactory) : SSLSocketFactory() {
override fun getDefaultCipherSuites(): Array<String> {
return socketFactory.defaultCipherSuites
}
override fun getSupportedCipherSuites(): Array<String> {
return socketFactory.supportedCipherSuites
}
override fun createSocket(socket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket {
return socketFactory.createSocket(socket, host, port, autoClose).enableTls()
}
override fun createSocket(host: String?, port: Int): Socket {
return socketFactory.createSocket(host, port).enableTls()
}
override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int): Socket {
return socketFactory.createSocket(host, port, localHost, localPort).enableTls()
}
override fun createSocket(host: InetAddress?, port: Int): Socket {
return socketFactory.createSocket(host, port).enableTls()
}
override fun createSocket(address: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int): Socket {
return socketFactory.createSocket(address, port, localAddress, localPort).enableTls()
}
private fun Socket.enableTls(): Socket {
if (this is SSLSocket) enabledProtocols += TlsVersion.TLS_1_2.javaName()
return this
}
}
This implementation contains our own socketFactory
object, which we use as a delegate and simply update the enabled protocols.
Now that we have a custom SSLSocketFactory
implementation, we have to tell OkHttp to use it via the sslSocketFactory
builder parameter:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
val sslContext = SSLContext.getInstance(TlsVersion.TLS_1_2.javaName())
sslContext.init(null, *yourTrustManagers*, null)
sslSocketFactory(TlsSocketFactory(sslContext.socketFactory), *yourTrustManager*)
}
The yourTrustManagers represent an array of TrustManager
instances. If you don’t know how to create your own trust manager – or if you don’t need custom implementation, you can always load the default one. You will learn how to load the default trust manager in the next section, discussing certificates.
Note that the above-stated TlsSocketFactory
implementation only sets the specified protocol as the default. It does not cover the case in which the protocol is not installed on the device. To cover that instance, you will have to use the ProviderInstaller
from Google Play services.
Also, keep in mind that you can either call installIfNeeded(context)
or installIfNeededAsync(context)
, and that it has to be done before creating the TlsSocketFactory.
For a more specific implementation, you can check the sample implementation on these gists: installIfNeeded, installIfNeededAsync. These implementations are provided by the OWASP team and can be found in the OWASP-MSTG book 1.1.3 on pages 206, 207 and 208.
MinSdk API level 21+
If this is the case, you should use at least OkHttp 3.13.x – and it works only on Android 5+. If you’re already using that version of Android, you’re good to go, as this library version already uses TLS 1.2 as a default for all HTTPS calls.
If you are using an older version of the OkHttp library and cannot update it for whatever reason, you must follow the steps described in the section MinSdk between API levels 16 and 19 above.
By the way, if you are interested in the history of TLS configuration in OkHttp, this is a superb read.
Returning to the topic – now that your app uses a more secure version of the TLS protocol, it’s important to know how to successfully verify a TLS connection.
Verifying a TLS connection
Two parts are essential in the process of verifying a TLS connection:
- Certificate verification (Certificate pinning)
- Hostname verification
In the following paragraphs, we’ll go over the specifics of each one.
Certificate verification (Certificate pinning)
This section of the Android app penetration testing guide will focus on certificate pinning and how to implement it. In addition, we’ll briefly examine how certificate verification is done.
For a TLS connection to work as expected, the Client must have a way of verifying that a certificate used on the server is trusted and valid. In our case, the Client is the Android app. This is where Certificate Authorities (CA) enter the stage.
A CA is an entity that is eligible to issue a trusted, time-limited certificate. The certificate issued by a CA is a verifiable small data file that contains identity credentials to help websites, people, and devices represent their authentic online identity.
But how does our Android device differentiate between the certificates issued by a CA and those so-called self-signed certificates? With the help of a set of CAs, which the device has stored on the Android system level. As of 4.2, Android contains over 100 CAs, which are updated in each release.
To view all certificates in an Android device programmatically, you can load the default TrustManager
. That way, you will see all the file paths that correspond to system-level certificates and the user-installed certificates. These certificates are also known as root certificates. To retrieve the default TrustManager
you can use this code:
private fun getDefaultTrustManager(): Array<TrustManager>? {
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
if (trustManagers.size != 1 || trustManagers[0] !is X509TrustManager) {
throw IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers))
}
return trustManagers
}
When an Android device tries to establish a secure connection with a service, it goes through the TLS handshake process. During connection setup, the server can send an entire chain of certificates which the device has to verify with its set of trusted CAs.
The certificate chain is also known as the chain of trust, which is a linked path of verification and validation from the end user (in our case the Android device) to a root certificate.
The certificates in the chain consist of a root certificate, zero or more intermediate certificates and a leaf certificate.
Check out the image below to clarify this confusion a bit:
As you can see, the chain certificates depend on each other. This means that attackers can exploit the chain of trust even if just one of the certificates’ CA is compromised. Developers can use the compromised CA to issue a certificate that will be automatically trusted by your Android device due to the default TrustManager
.
To further protect your app from fraudulently issued certificates, you can use a technique known as certificate pinning. Certificate pinning is an extra defense mechanism against MITM (ie. man-in-the-middle) attacks, in which the developer implements a custom trust manager that contains only the certificates which can validate the web server that the app is communicating with. Your app will neither trust any other system-trusted CA nor the user-installed certificates.
Certificate pinning is one of the most common test cases in a penetration test, so the following paragraph will elaborate on how to prepare an app for it.
Preparation phase in Android app penetration testing – What you’ll need to know
This section will show you how to successfully implement certificate pinning for both self-signed certificates and those issued by a CA. To pin a certificate, the first step is to decide which certificate from the certificate chain to pin.
We recommend pining the leaf certificate as the attack surface is very small, thanks to the app interacting with that certificate first. The app will not be usable if that certificate can’t be verified.
Self-signed certificates
Pinning self-signed certificates is something you should only do for testing environments. To pin a certificate in an Android app, the first thing to do is to acquire the certificate, which can quickly be done with OpenSSL via the terminal:
$ openssl s_client -connect example.com:443 -showcerts
The command above will return a list of the entire certificate chain for the specified website. If you want to save the certificates in a local file or download one via a browser, find out how to do that here.
After acquiring the needed certificate, the next step is creating your custom TrustManager
containing the certificate. This can be done as follows:
private fun createCustomTrustManager(context: Context): Array<TrustManager>? {
val keyStore = KeyStore.getInstance("BKS").apply { load(null, null) }
val certificateFactory = CertificateFactory.getInstance(CERT_TYPE)
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
context.resources.openRawResource(R.raw.leaf_cert_expires_1_1_2077).use {
keyStore.setCertificateEntry("MyLeafCert", certificateFactory.generateCertificate(it))
}
trustManagerFactory.init(keyStore)
return trustManagerFactory.trustManagers
}
This code loads a leaf_cert_expires_1_1_2077.pem
file from the raw
resource folder and sets it as a certificate entry inside our empty keystore, under MyLeafCert alias. After setting the required certificate in the keystore, use the same keystore to initialize the trustManagers.
With everything ready, it’s time to pass the instance of our TrustManager
to OkHttp via the sslSocketFactory
builder function, as described in the TLS connection section above. We sometimes call this method hard certificate pinning because we bundle the pinned certificate in our app, and we have to upload a new version of the app that contains the new certificate every time the certificate changes or gets renewed.
Certificates signed by a trusted CA
Developers can similarly use the previous code sample for certificates signed by a trusted CA. This is a good approach if you want just one implementation for all cases. However, OkHttp does provide a different mechanism for pinning non self-signed certificates, using the CertificatePinner
class. Find more information about this class here.
CertificatePinner
employs a different kind of pinning, using the certificate’s subject cryptographic public key for verification. Unlike the previously described approach, in which you need to bundle the pinned certificate with your app, public key pinning only needs the base64 SHA-256 hash value of the certificate public – considering that the hash can be safely stored as a plain string in your app without causing security issues.
The implementation is much more straightforward:
const val MY_LEAF_CERT_EXPIRES_1_1_2077 = "sha256/A23dA8l41A46gjAcAiAA32AAA8juvAnvvlk85kub6A5="
private fun buildCertificatePinner(): CertificatePinner {
return CertificatePinner.Builder()
.add("yourHostname.com", MY_LEAF_CERT_EXPIRES_1_1_2077)
.build()
}
// Setting up OkHttp with the CertificatePinner object
OkHttpClient.Builder()
.certificatePinner(buildCertificatePinner())
.build()
So there you have it – a straightforward and intuitive approach. In addition to easier implementation, this approach has another noticeable benefit. If the certificate expires, the server can renew the certificate, without changing the cryptographic public key of the certificate.
You won’t need to update the app if the certificate gets renewed. One of the downsides of this method is that it does not support self-signed certificates.
Also, in the implementation above, you probably noticed the yourHostname.com string. This value is used for hostname verification and we will discuss this in the next section.
Finally, keep in mind that this is only a brief introduction to certificate pinning, just enough to understand the basics and to be able to prepare your app for penetration tests. For more straightforward implementation and maintenance, certificate pinning should be agreed upon with the administrators of the web service the app is communicating with, to see whether it is a good fit.
If you decide on certificate pinning, ensure a better understanding of the entire process.
Hostname verification
Finally, we’ve reached the second key part in verifying a TLS connection: hostname verification.
A common mistake developers make is setting a permissive hostname verifier, or even worse – accepting all hostnames. This means that the attacker can issue a valid certificate with a compromised CA, choose any domain name for it, and execute a MITM attack.
If you use OkHttp, the default implementation of the HostnameVerifier
will be enough to verify your connection to the host. Nevertheless, we will show you a couple of examples of how to implement HostnameVerifier
to gain a better understanding of how it works.
Preparation phase in Android app penetration testing – What you’ll need to know
If you aren’t using CertificatePinner
for hostname verification, you can use the Java’s HostnameVerifier
, as it is the base interface for hostname verification. OkHttp supports this interface, you just have to pass it to the hostnameVerifier builder function.
During the TLS handshake, the verification mechanism can call back to implementers of this interface to determine whether this connection should be allowed. The specific verification can be done using the OkHostnameVerifier
, although you will stumble upon some implementations where HttpsURLConnection.getDefaultHostnameVerifier()
is used.
Under the hood, this is still using the OkHostnameVerifier
but from an internal Android version of the same class.
We’re ready to check a sample implementation using OkHostnameVerifier.INSTANCE.verify
:
hostnameVerifier { _, session ->
OkHostnameVerifier.INSTANCE.verify("yourHostname.com", session)
}
The code above will check whether the entered hostname yourHostname.com is contained inside the certificate of the current SSLSession
. Only if it is, the verification will succeed. In case you want to trust an entire subdomain, you can use the wildcard pattern notation, but you will have to use the verifyHostname
method like this:
hostnameVerifier { hostname, _ ->
OkHostnameVerifier.INSTANCE.verifyHostname(hostname, "*.yourHostname.com")
}
Your chosen implementation will depend on your environment and all the services your app connects to.
Conclusion and notes
Hopefully, you will better understand the TLS connection and how to prepare your Android app penetration testing by this point. Look for the two critical parts of a TLS connection: certificate and hostname verification. If one of these two verifications is broken, the entire TLS connection is nullified and the app becomes an easy target for MITTM attacks.
To deepen your knowledge on handling a proper TLS connection in a WebView and more, check out the OWASP Mobile Security Testing Guide.
Also, if you (have) encounter(ed) any problems during the setup of a proper TLS connection, please check ssl-debugging for answers and solutions.
One last thing – in this post we did not cover the Android Network security configuration. This powerful tool lets apps customize their network security settings without modifying the app code.
Unfortunately, it is available only since Android 7. Still, if you are lucky enough to work on minSdk API level 24 or you are okay with having two separate pinning implementations, depending on the SDK level, then this approach should be the preferred way for handling your network configurations.
If you want to look into more ways to improve the security of your digital product, explore our cybersecurity services.