Android applications security — part 2, client-server communication

Adrian Defus
Skyrise
Published in
7 min readJan 7, 2019

Mobile applications commonly use resources containing some sensitive data stored in a backend service. To obtain them, not only our application (client) needs to query an endpoint, but it also has to authenticate itself, so the server knows who specifically needs this information.

App[client] querying backend

One of the methods of authentication is sending a client-specific key as a parameter directly in the url address, as an HTTP Header or in a POST body. However, in the first part of this article I showed that keys that are stored in the application are easily accessible after decompiling the .apk file of our application. Even if we use very complicated methods to hide our keys and use additional security, eg. check whether our application is debugged or if the ADB mode is active, the attacker can try to ‘sniff’ the connection which is established between the client and the server and read the token directly from the query — such a method is called the Man-in-the-Middle attack.

Basic client-server communication

Man-in-the-Middle attack

When the MitM attack is carried out, packets that should go directly between our application and the server are additionally intercepted by the third entity located between them.

Man-in-the-Middle attack

In the case of an Android system, the easiest way to perform this attack on applications installed on the device is to use the Packet Capture app. After it’s launched, all network traffic coming to/from the selected application is intercepted and logged. The screenshot below shows the captured packet from our sample application that queried the OpenWeatherMap API using a standard HTTP connection:

HTTP query capture using the Packet Capture app

There are several methods we can use to protect our apps against this type of attack. The most common one is to establish a secure HTTPS connection. When you request an HTTPS connection to a server, it will initiate its SSL certificate to the client [app]. This certificate contains the public key required to begin the secure session. Based on this initial exchange, your app and the server then initiate the ‘SSL handshake’, which involves the generation of shared secrets to establish a unique secure connection between them.

One of the features of Packet Capture is the ability to install its own certificate, which it shows to the client if it tries to establish an https connection. Fortunately, Android has built-in security that verifies whether the presented certificate is issued by a verified publisher. Thanks to it, when we try to catch packets using Packet Capture with its own certificate, the CertPathValidatorException will be thrown. Moreover, if our app had certificate pinning implemented and we somehow used a trusted certificate in the MitM attack, its SHA fingerprint wouldn’t have matched the one hard-coded in the application.

Is there any way to get around this security? Of course there is 😉. By using reverse engineering techniques, we can decompile our application and then inject the code supplying our implementation overwriting the method used to verify the certificates in the place where the client object is initialized, in such a way that all of them are accepted — this is called the all-trusting-client. The simplest way to achieve this is by using a class containing a static function, which requires only one line to be injected in the .smali file (to which our class has been decompiled).

Connecting Packet Capture into our rebuilt app leads to the successful capturing of the HTTPS query.

A successful (top) and unsuccessful (bottom) https query capture using the Packet Capture app

The thing worth noticing is that every modification in the source code of the application requires us to build a new APK file and sign it with our own certificate (it is not possible to install an unsigned application on Android). Therefore, we can consider an additional validation of the certificate at the NDK level (where our encrypted key is located) and if the application is signed with a different fingerprint, the native function will return an incorrect value, so the client verification will fail.

To bypass this check, the attacker can decompile the native-lib.so file to the assembly code and change the definition of the certificate validation function so that it always returns true — it requires more of his involvement, but of course it’s not impossible (below: the definition of an assembler x86 checkCert() function always returning true)

Challenge-response

Since we came to the conclusion that a token-only authentication of our application is insufficient, even with additional security against its visibility after the decompilation, we should consider adding another step — challenge-response.

Challenge-response is generally a family of protocols in which one party presents a question (“challenge”), while the other should provide a correct answer (“response”). In challenge-response authentication, the client, after sending the initial request to the server (usually containing a specific app key, although the third party can access it), gets a random challenge — some kind of data that should be calculated using some cryptographic hash functions, and then sends the response back to the server, which also performs the same calculation on its side. If the transferred value is verified correctly, it is very likely that the application is trusted.

Challenge-response authentication with a separated resource server

Salted Challenge Response Authentication Mechanism (SCRAM)

SCRAM is a method of secure authentication of both the client and the server, which greatly limits brute-force attacks compared to the standard challenge-response method, which doesn’t meet the requirements necessary for universal deployment and is successful only to a limited extent. Generally, the full SCRAM procedure consists of four messages:

1: client first message → Hash (client_id, client_nonce)

  • client_id — the same key that we would send in the initial request for a standard challenge-response mechanism,
  • client_nonce — randomly generated client hash.

2: server first message → Hash (“${client_nonce} + {server_nonce}”, salt, iteration_count)

  • salt — A random octet string that is combined with a password before applying a one-way encryption function [Hi()],
  • iteration_count — iteration count used in [Hi()] function, usually at least 4096,
  • Hi() Password-Base-Key-Derivation-Function 2 with HMAC() as a pseudorandom function, which generates a derivated key using [iteration_count] iterations

3: client final message → Hash (client_server_nonce, client_proof)

  • client_proofclient_key XOR client_signature,
  • client_key — HMAC(salted_password, “Client Key”),
  • salted_passwordHi() result,
  • client_signature — HMAC(stored_key, auth_message),
  • stored_key — Hash(client_key),
  • auth_message — “${client-first-message}, ${server-first-message}, ${client-final-message-without-proof}”.

4: server final message → Hash (server_signature, session_token)

  • server_signature — HMAC(server_key, auth_message),
  • server_key — HMAC(salted_password, “Server Key”),
  • salted_passwordHi() result,
  • auth_message — “${client-first-message}, ${server-first-message}, ${client-final-message-without-proof}”.
  • session_token — time-limited piece of data used in further communication to identify a session, which may be written as a JSON Web Token.

JSON Web Token

A JSON Web Token (JWT) is a JSON object that is defined in RCS 7519 as a safe way to represent a set of information between two parties. It is encrypted on the server side and only the server has the key to verify its authenticity. JWT is composed of three parts separated by dots:

header.payload.signature

The header component contains base64 encoded JSON information about how the signature should be computed:

typ” communicates that the object is a JSON Web Token, and “alg” specifies the algorithm used to create a signature component.

Payload is simply the JSON data stored inside the JWT (referred to as the “claims”). In our example, the payload component contains the issuer_id iss”, JWT issue time “iat”, and an expiration timestamp “exp”:

A signature component is generated using the algorithm defined in the header as both the header and payload (base64 encoded) with an additional secret included:

HMACSHA256(base64Encode(header) + “.” + base64Encode(payload), secret)

The main purpose of using JWT is not to hide data in any way — they’re just used to prove that the data was sent from an authentic source — an attacker cannot modify JWT claims without invalidating the signature. With basic access authentication, a simple session token is used to get the state stored in back-end, so if there are multiple services which can handle the client query, then accessing this state and synchronizing it between them can be a significant bottleneck. Thanks to JWT, the client is the side which maintains and provides the session state with every call, so the API protocol is stateless and each service is able to handle the request regardless of others.

Summary

Android mobile applications, due to their specificity and ease of access to the source code, require an appropriate security approach both in the storage of any sensitive data in them, as well as in the case of communication with servers. An attacker can use innumerable ways to steal our sensitive data and use it in an undesirable way, potentially exposing us to huge losses. Only the proper protection of all foreseeable areas exposed to attacks, and a good knowledge of the vulnerabilities of our applications can help us create a product that ensures the high security and integrity of our data.

Want to read more exciting tech articles? Visit Skyrise blog!

--

--