Home Breaking Custom Encryption Using Frida (Mobile Application Pentesting)
Post
Cancel

Breaking Custom Encryption Using Frida (Mobile Application Pentesting)

Overview

It was a typical day at Cognisys, where we were engaged in routine Android application testing. However, this session took an intriguing turn when we encountered a unique encryption implementation affecting both the request and response bodies in the Android app.

In this blog, we will explore the challenges and triumphs of understanding this function to understand the internal workings of the mobile app. If you’re fascinated by in-depth discussions and understanding of application testing, this article is tailored for you. We believe you’ll find great value in the insights shared by our application security team.

  • Understanding Encrypted Requests in Mobile Apps
  • Creating a Frida script
  • Step-by-Step Guide to Decrypting Data Using Frida
  • Conclusion: The Impact of Dynamic Instrumentation
  • Recommendations for Developers

Understanding Encrypted Requests in Mobile Apps

Encrypted requests are designed to protect data in transit between the client (mobile app) and the server. Typically, these requests are secured using SSL/TLS or custom encryption routines. While SSL/TLS encryption is straightforward and robust, custom encryption mechanisms can vary in complexity and security, providing unique challenges during penetration tests.

In our assessment, the target Android application employed a custom encryption scheme for its API requests. This method encrypted the body data before transmission over the network, likely as an additional security measure beyond standard SSL/TLS. However, custom encryption can sometimes introduce vulnerabilities, either through faulty implementation or the use of weak algorithms. In this case, while the application implemented standard SSL pinning, we were able to bypass it using publicly available SSL pinning bypass scripts. The true challenge arose after we began intercepting the requests with Burp Suite. Below is an example of one of such request.

Request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

POST /api/authentication/validateAuthenticationCredentials HTTP/1.1
Host: acme.co.uk
Cookie: JSESSIONID=9717E2AE91B0AAFAC9B3AD2C197C7F4A
Accept: */*
Content-Type: application/json
App-Version: 1.1.3
Accept-Encoding: gzip, deflate, br
User-Agent: Acme/2.1.9 Dalvik/2.1.0 (Linux; U; Android 8.0;)
Content-Length: 798
Accept-Language: en-IN;q=1.0, gu-IN;q=0.9
Connection: close

  

{"payload":"AQACuscG32szhkGtzCxFlsnIesfxtB9HgKmsekSnIxn6xLtXlpkiuGX8Pd1TBpU5itYCHp30VH891UIABY0V1euUB53FW2EiLx2g7BwLyuf\/pGVV7CnBKgyyUgbzn5GTbhYGcnUbjSREVOKEDzXhPdaZdk3vbADuEZvaTPI8tHCvKam6FkvtV1Gf\/+wEy3NOJxGoq5lv9\/XNhFyXOYQOVhuRCdx0czv\/9ZWC66PPdr73NMojkmA\/WUNrOCKFB79kG1XgF+A4f2TQtARwnfRCcWQRvsfSki0mxZgTBppp5hVLJ7ey0s\/REVOKEDjV50vz92cs+JnEeeQWIB5778VwKrSkk+rR\/vJwWUoQmmWWC4DfXzdVrvjPrEpyVlytREVOKEDfybTUOEidMNcQ2tpufpaPvIGQHEYJ4AJ1Sgp9L5tImpXKRMfQXTxN\/nJAdYHZc8WW1JWyMnKy\/REVOKED+kQPqay5CmATKWGsL7UwiF880GGmGxcyH28F99wxMs02DyjMYbZNED6Q2Dci6YY48iPE3kZRcst6199tpXm3gDpMs1GrhaxG2rF+VshEd494mQXhKDCq+8IRZRz59VHf9HO0NYUN7wnyoewgSfwlReBI3QzfWe\/ztk3DPfhzVw+jxizG2cKRpRrGwe+foIOKq+\/tc7aEmVsImhqHFghGfAlUPsXXEeo\/REVOKEDY2kJDhq9gzY2b2Bu0="}

Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store
Keep-Alive: timeout=20
Pragma: no-cache
Content-Type: application/json;charset=UTF-8
Expires: -1
Vary: Access-Control-Request-Headers
Set-Cookie: JSESSIONID=D4EDE74F32A2C0634822D6F3DA40CF33; Path=/; HttpOnly
Date: Wed, 10 Apr 2024 16:32:51 GMT
Connection: close
Strict-Transport-Security: max-age=31536000
Content-Length: 609

{"payload":"a4Npgdh9sREVOKEDSTAIX4eO2JRnGgc1fP0OA4duT/fcCLdb5Adt6TENQ+C3EsvWKz7bzUHlfKSi3/k4AXgmCekBIi7dPNOsSCxkhAzq0qawBD4+ke7ld/ERZKC/nm68J2yA6gKruye1dWE3kP4C8oZs7tPlDxYREVOKEDwykPMmZif4gluvRRCKQ0p9ZkkFfKIREVOKEDQtfAqHwHBtpKkrM=","signature":"YUVYIrTYCBkAOJKQWREVOKREVOKED7lR+IxjgygaqsFWdtr3lIMZ3JqAB8+tpF6cAbAhiSPjGaknJ4g3l+3tf/K8NCPojRdbTDh1ICE9dxAUyR7EKBxj6K9c6BlJXLC2BKj+EQtGCQGt/n9aRJ+if48zDvDLPWIqnCPOMpUGiicuA5zIURAFG1Ms3dFoES5RyJXS8GKL3SFRVtU2UI5p60Dr/KcUP83XTQbE+G7kZJN0GC6JlpXB3EOjRkFNRruhJhNdoREVOKEDD/TdFPIyoI2uny1XI8xwNfP3J/lD0wLKKNRURNlI3RwXIVkeTXw=="}

The mobile application implemented this as an additional layer of security. From a security standpoint, this seems like a sound practice, providing an extra level of protection. However, this is precisely what hackers target—they aim to bypass these layers, peeling them back one by one, much like the layers of an onion. Now let’s explore how this encryption and decryption process was handled.

Screenshot 2024-04-16 at 4 11 17 PM

In this function observed that MobileUnsignedRequest is handling the body for all of the endpoints.

1
2
   @POST("authentication/validateAuthenticationCredentials")  
    Call<MobileClientResponse> validateAuthenticationCredentials(@Body MobileUnsignedRequest mobileUnsignedRequest); 

While searching the MobileUnsignedRequest function in JADx-GUI, it reveals that it originates from the MobileUtilitiesImpl functions. The validateAuthenticationCredentials function code also revealed that the application was utilising functions defined in the MobileUtilitiesImpl interface. The MobileUtilitiesImpl class revealed the process to encrypt and decrypt the body data as seen below. Below code snippet shows the functions we can hook using our Frida script.

Let’s create a Frida script to hook these functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public <T> MobileUnsignedRequest encryptPayload(T t, byte[] bArr, PublicKey publicKey, SecretKey secretKey, Class<T> cls) {

        Intrinsics.checkNotNullParameter(bArr, "iv");
        Intrinsics.checkNotNullParameter(publicKey, "publicKey");
        Intrinsics.checkNotNullParameter(secretKey, "secretKey");
        Intrinsics.checkNotNullParameter(cls, "requestClass");
        String json = this.jsonSerializer.toJson(t, cls);
        CryptoService cryptoService = this.cryptoService;
        byte[] encoded = secretKey.getEncoded();
        Intrinsics.checkNotNullExpressionValue(encoded, "secretKey.encoded");
        byte[] encryptAsymmetric = cryptoService.encryptAsymmetric(encoded, publicKey);
        int length = encryptAsymmetric.length;
        byte[] bArr2 = {(byte) (length >>> 8), (byte) (length & 255)};
        CryptoService cryptoService2 = this.cryptoService;
        byte[] bytes = json.getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");
        byte[] encryptSymmetric = cryptoService2.encryptSymmetric(bytes, secretKey, bArr);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(512);
        byteArrayOutputStream.write(bArr2);
        byteArrayOutputStream.write(encryptAsymmetric);
        byteArrayOutputStream.write(encryptSymmetric);
        String encodeToString = Base64.encodeToString(byteArrayOutputStream.toByteArray(), 0);
        Intrinsics.checkNotNullExpressionValue(encodeToString, "encryptedEncoded");
        return new MobileUnsignedRequest(encodeToString);
    }

public <T> MobileUnsignedRequest encryptPayload(T t, byte[] bArr, SecretKey secretKey, Class<T> cls) {

        Intrinsics.checkNotNullParameter(bArr, "iv");
        Intrinsics.checkNotNullParameter(secretKey, "sessionKey");
        Intrinsics.checkNotNullParameter(cls, "requestClass");
        String json = this.jsonSerializer.toJson(t, cls);
        CryptoService cryptoService = this.cryptoService;
        byte[] bytes = json.getBytes(Charsets.UTF_8);
        Intrinsics.checkNotNullExpressionValue(bytes, "this as java.lang.String).getBytes(charset)");

        String encodeToString = Base64.encodeToString(cryptoService.encryptSymmetric(bytes, secretKey, bArr), 0);
        Intrinsics.checkNotNullExpressionValue(encodeToString, "encryptedPayload");
        return new MobileUnsignedRequest(encodeToString);
    }

Creating a Frida script

By analysing the decompiled code of the application with tools such as JADX, we pinpointed the functions responsible for encrypting and decrypting data. These functions were custom-written and not derived from any standard library, a common scenario in applications that employ bespoke encryption schemes. Identifying the specific function of handling these tasks was challenging. We started by searching for API endpoints in JADX, which then guided us to the functions responsible for encryption.

Frida is a powerful dynamic instrumentation toolkit that allows us to inject our scripts into a running process. This capability is invaluable when dealing with encrypted data because it enables the tester to intercept function calls that handle encryption and decryption operations directly within the application.

However, this was challenging because it seemed we still had the encryption functions intact; the real issue was that the application kept crashing. We needed to make critical changes to the custom Frida script above to ensure it functioned properly and to prevent further crashes.

Step-by-Step Guide to Decrypting Data Using Frida

Step 1: Setting Up Frida

First, ensure that Frida is installed on both the testing machine and the Android device. The Android device should be rooted to allow Frida to inject scripts into the app processes effectively. If you don’t know how it works, please refer to our blog Here

Step 2: Identifying the Encryption Functions

As we’ve already identified the encryption function, it is time to write the script based on it

Step 3: Writing Frida Scripts

With the functions identified, the next step was to write Frida scripts that hook into these functions. The script was designed to log the parameters passed to the encryption functions and the output returned by the decryption functions. For example:

1
root@cognisys:~$ frida -U -l SSLPinBypass.js -l DecryptAll.js -f com.acme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
Java.perform(function() {
	var targetClassx = "com.acme.data.mobileclient.MobileUtilitiesImpl";
	var targetMethodx = "encryptPayload";
	try {
		var targetMethodRef = Java.use(targetClassx)[targetMethodx];
		targetMethodRef.overload('java.lang.Object', '[B', 'java.security.PublicKey', 'javax.crypto.SecretKey', 'java.lang.Class').implementation = function(t, iv, publicKey, secretKey, requestClass) {
		console.log("\nHooked: encryptPayload -> 1");
		console.log("t: ", t.toString());
		console.log("iv: ", iv);
		console.log("publicKey: ", publicKey);
		console.log("secretKey: ", secretKey);
		console.log("requestClass: ", requestClass);
		console.log("\n\n");
		var result = this.encryptPayload(t, iv, publicKey, secretKey, requestClass);
		return result;
		};
	}
	catch (error) {
		console.log("MobileUtilitiesImpl 1: Something Went Wrong!");
		console.log(error);
		}
	try
	{
		const target = Java.use('com.acme.data.mobileclient.MobileUtilitiesImpl');
		target.encryptPayload.overload('java.lang.Object', '[B', 'javax.crypto.SecretKey', 'java.lang.Class').implementation = function (t, iv, sessionKey,requestClass) {
		console.log("\nHooked: encryptPayload -> 2");
		console.log("t: --> ", t.toString());
		console.log("iv: --> ",iv);
		console.log("SessionKey: --> ",sessionKey);
		console.log("requestClass: --> ", requestClass);
		console.log("\nBreak\n")
		var result = this.encryptPayload(t, iv, sessionKey,requestClass)
		return result;
		};
		}
	catch(err) {
		console.log('MobileUtilitiesImpl 2: Something went wrong!');
		console.log(err);
	}
	try
	{
		const target = Java.use('com.acme.data.mobileclient.MobileUtilitiesImpl');
		target.encryptAndSignPayload.implementation = function(t, iv, sessionKey, keyStoreAlias, requestClass, pair) {
		console.log("\nHooked: encryptAndSignPayload");
		console.log("t: ", t.toString());
		console.log("iv: ", iv);
		console.log("sessionKey: ", sessionKey);
		console.log("keyStoreAlias: ", keyStoreAlias);
		console.log("requestClass: ", requestClass);
		console.log("pair: ", pair);
		console.log("\n\n")
		var result = this.encryptAndSignPayload(t, iv, sessionKey, keyStoreAlias, requestClass, pair);
		return result;
		};
	}
	catch(err) {
		console.log('MobileUtilitiesImpl 3: Something went wrong!');
		console.log(err);
	}
	
	try {
		var targetClass = "com.acme.data.mobileclient.MobileUtilitiesImpl";
		var targetMethod = "decryptAndDecodePayload";
		var targetMethodRef = Java.use(targetClass)[targetMethod];
		targetMethodRef.implementation = function(payload, responseClass) {
		console.log("\nHooked: decryptAndDecodePayload");
		console.log("payload: ", payload);
		console.log("responseClass: ", responseClass);
		console.log("\n\n")
		var result = this.decryptAndDecodePayload(payload, responseClass);
		console.log(result);
		return result;
		};
		} catch (error) {
		console.log("decryptAndDecodePayload: ",error)
		}
	
	try {
	
		var targetClass = "com.acme.data.mobileclient.MobileUtilitiesImpl";
		var targetMethod = "extractPayloadAndSignature";
		var targetMethodRef = Java.use(targetClass)[targetMethod];
		targetMethodRef.implementation = function(response) {
		console.log("\nHooked: extractPayloadAndSignature");
		console.log("response: ", response);
		var result = this.extractPayloadAndSignature(response);
		console.log("Result: ",result);
		return result;
	}; 
	} catch (error) {
		console.log("extractPayloadAndSignature: ",error)
	}
	});

Step 4: Intercepting the Encryption/Decryption

After deploying the Frida script, we were able to intercept the encrypted requests and responses. This interception provided us with both the plain text and the encrypted data, revealing how the data was being secured.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Hooked: encryptPayload  1
t: Request (channel=MA, deviceId=null, deviceInformation=DeviceInformation(deviceAlias=sdk aphone arm64, deviceName=sdk_gphone_arm64, devicePlatfo rm=Android, deviceType=SPH, deviceVersion=11), hardwareId=8be9a2b44389381,passwords={password=Password1},nonce=NQJkAJ5L35qfOdknnY25SWcaKVPp1c/q1No5mimIe0k=,sessionId=null, sessionKey=0CPCYbAXfS0bNjicRqDkQ==
usernames=(customerNumber=IB90406369}, tokenId=null, token0tp=null)
iv: 107,106,104,121, 55, 117, 116, 53, 114, 102, 103, 118, 98, 103, 104,52 publicKey:
lobject Objectl
secretKey: [object Objectl
requestClass: class com.acme.data.mobileclient.networkservice.ValidateRegistrationCredentials$Request  

Hooked: extractAndVerifyEncryptedResponse
response: Response{protocol=h2, code=200, message=, url=https://acme.co.uk/api/registration/validateRegistrationCredentials} publicKey: [object Objectl sessionKey: [object Objectl
iv: 107, 106, 104, 121, 55, 117, 116, 53, 114, 102, 103, 118, 98, 103, 104, 52 responseClass:
class com.acme.data.mobileclient.networkservice.ValidateRegistrationCredentials$Response
Hooked: extractPayloadAndSignature
response: Response{protocol=h2, code=200, message=, url=https://acme.co.uk/api/registration/validateRegistrationCredentials} Result:
com.acme.data.mobileclient.PayloadAndSignature@a9ffa4
Response (errorMessage=null, nonce=H1hswLGe8TYu/7pyT8EgLrzvxhrmQZ8/XUrIazn7+U=, responseCode=CREDENTIALS_INVALID, twoFactorOptions=[], willRemoveP
reviousRegos=null, mustChangePassword=null)

Conclusion: The Impact of Dynamic Instrumentation

Using Frida to break encryption in a mobile app not only demonstrates the power of dynamic instrumentation tools but also highlights the potential risks associated with custom encryption schemes. It underscores the importance of implementing well-known and tested cryptographic practices rather than relying on obscure, potentially vulnerable custom methods.

Through this technical exploration, developers and security professionals can gain insights into securing mobile applications more effectively. It also serves as a reminder that security through obscurity, such as using custom encryption methods, often provides a false sense of security.

Recommendations for Developers

  • Prefer Standard Encryption Protocols: Always opt for standard, well-reviewed encryption protocols like AES with secure keys.
  • Conduct Regular Security Audits: Regularly test your applications for security vulnerabilities using both static analysis tools and dynamic testing methods like Frida.
  • Educate Your Team: Ensure that your development team is aware of the best practices in secure coding, especially concerning data encryption and handling.
  • By integrating these practices into your development process, you can significantly enhance the security posture of your mobile applications, safeguarding both user data and your organisation’s reputation.
This post is licensed under CC BY 4.0 by the author.