Skip to content

MASTG-DEMO-0108: Bypassing Frida Detection in /proc/self/maps to Extract Sensitive Data

Download MASTG-DEMO-0108 APK Open MASTG-DEMO-0108 Folder Build MASTG-DEMO-0108 APK

Sample

This sample uses the same code as Detecting Frida hooks and terminating the application on response, which encrypts and decrypts a sensitive API key using AES/GCM via the Android KeyStore. The code includes a runtime hook detection mechanism that scans /proc/self/maps for Frida-related libraries and terminates the process via Process.killProcess() if any are found. This demo demonstrates bypassing the detection by hooking BufferedReader.readLine() to hide Frida entries from /proc/self/maps, causing detectHooking() to return false so the termination path is never reached.

See Detection of Reverse Engineering Tools and Runtime Integrity Verification for more context on bypassing runtime detection mechanisms.

Note

This is a series of correlated tests.

../MASTG-DEMO-0107/MastgTest.kt
 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
95
96
97
98
99
package org.owasp.mastestapp

import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import java.io.BufferedReader
import java.io.FileReader
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class MastgTest(private val context: Context) {

    private val sensitiveApiKey = "sk-OWASP-MAS-SuperSecretKey-1234567890"
    private val keyAlias = "mastgCipherKey"

    init {
        if (detectHooking()) {
            android.os.Process.killProcess(android.os.Process.myPid())
        }
    }

    private fun detectHooking(): Boolean {
        try {
            BufferedReader(FileReader("/proc/self/maps")).use { reader ->
                var line: String?
                while (reader.readLine().also { line = it } != null) {
                    val l = line!!.lowercase()
                    if (l.contains("frida") || l.contains("gadget")) {
                        return true
                    }
                }
            }
        } catch (_: Exception) {
            // Unable to read maps
        }
        return false
    }

    private fun getOrCreateSecretKey(): SecretKey {
        val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        return if (keyStore.containsAlias(keyAlias)) {
            (keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey
        } else {
            KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES,
                "AndroidKeyStore"
            ).apply {
                init(
                    KeyGenParameterSpec.Builder(
                        keyAlias,
                        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
                    )
                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                        .build()
                )
            }.generateKey()
        }
    }

    fun mastgTest(): String {
        // Check for hooking before performing cryptographic operations
        if (detectHooking()) {
            android.os.Process.killProcess(android.os.Process.myPid())
            return ""
        }

        return try {
            val key = getOrCreateSecretKey()

            // Encrypt the sensitive API key
            val encryptCipher = Cipher.getInstance("AES/GCM/NoPadding")
            encryptCipher.init(Cipher.ENCRYPT_MODE, key)
            val iv = encryptCipher.iv
            val encryptedBytes = encryptCipher.doFinal(sensitiveApiKey.toByteArray(Charsets.UTF_8))
            val encryptedData = Base64.encodeToString(iv + encryptedBytes, Base64.DEFAULT)

            // Decrypt to verify
            val decodedData = Base64.decode(encryptedData, Base64.DEFAULT)
            val ivFromData = decodedData.copyOfRange(0, 12)
            val ciphertext = decodedData.copyOfRange(12, decodedData.size)

            val decryptCipher = Cipher.getInstance("AES/GCM/NoPadding")
            decryptCipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, ivFromData))
            val decryptedBytes = decryptCipher.doFinal(ciphertext)
            val decryptedString = String(decryptedBytes, Charsets.UTF_8)

            "Encryption and decryption successful.\n" +
                    "Encrypted: $encryptedData\n" +
                    "Decrypted: $decryptedString"
        } catch (e: Exception) {
            "Error: ${e.message}"
        }
    }
}

Steps

  1. Install the app on a device ( Installing Apps)
  2. Run run.sh to spawn the app with the bypass script
  3. Click the Start button
  4. Stop the script by pressing Ctrl+C and/or q to quit the Frida CLI
 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
Java.perform(() => {

    // --- Bypass: Hook BufferedReader.readLine() to hide Frida from /proc/self/maps ---

    let BufferedReader = Java.use("java.io.BufferedReader");

    BufferedReader["readLine"].overload().implementation = function () {
        let line = this["readLine"]();
        while (line !== null) {
            let lower = line.toString().toLowerCase();
            if (lower.indexOf("frida") !== -1 || lower.indexOf("gadget") !== -1) {
                console.log("[*] Hiding line from /proc/self/maps: " + line);
                line = this["readLine"]();
            } else {
                break;
            }
        }
        return line;
    };

    console.log("[+] BufferedReader.readLine() hooked: filtering Frida-related entries from /proc/self/maps");

    // --- Hook Cipher.doFinal() to extract sensitive data ---

    function printBacktrace(maxLines = 8) {
        let Exception = Java.use("java.lang.Exception");
        let stackTrace = Exception.$new().getStackTrace().toString().split(",");

        console.log("\nBacktrace:");
        for (let i = 0; i < Math.min(maxLines, stackTrace.length); i++) {
            console.log(stackTrace[i]);
        }
    }

    function bytesToHex(bytes) {
        let hex = [];
        for (let i = 0; i < bytes.length; i++) {
            hex.push(("0" + (bytes[i] & 0xFF).toString(16)).slice(-2));
        }
        return hex.join("");
    }

    let Cipher = Java.use("javax.crypto.Cipher");
    let StringClass = Java.use("java.lang.String");

    // Hook Cipher.doFinal(byte[])
    Cipher["doFinal"].overload("[B").implementation = function (input) {
        let result = this["doFinal"](input);
        let algorithm = this.getAlgorithm();

        // Cipher.ENCRYPT_MODE = 1, Cipher.DECRYPT_MODE = 2
        let opmode = this.opmode.value;
        let modeStr = opmode === 1 ? "ENCRYPT_MODE" : opmode === 2 ? "DECRYPT_MODE" : "UNKNOWN(" + opmode + ")";

        console.log("\n\n[*] Cipher.doFinal(byte[]) called");
        console.log("    Algorithm: " + algorithm);
        console.log("    Mode: " + modeStr);

        if (opmode === 1) {
            try {
                console.log("    Input (plaintext): " + StringClass.$new(input));
            } catch (e) {
                console.log("    Input (hex): " + bytesToHex(input));
            }
            console.log("    Output (ciphertext hex): " + bytesToHex(result));
        }

        if (opmode === 2) {
            console.log("    Input (ciphertext hex): " + bytesToHex(input));
            try {
                console.log("    Output (plaintext): " + StringClass.$new(result));
            } catch (e) {
                console.log("    Output (hex): " + bytesToHex(result));
            }
        }

        printBacktrace();

        return result;
    };

    console.log("[+] Cipher.doFinal() hooked: extracting sensitive data\n");
});
1
2
#!/bin/bash
frida -U -f org.owasp.mastestapp -l bypass.js -o output.txt

Observation

The output contains eight frida-agent-64.so memory segments filtered from /proc/self/maps across two scans. Two Cipher.doFinal() calls are captured: one in ENCRYPT_MODE with the plaintext input sk-OWASP-MAS-SuperSecretKey-1234567890, and another in DECRYPT_MODE with the same plaintext as output.

output.txt
 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
[+] BufferedReader.readLine() hooked: filtering Frida-related entries from /proc/self/maps
[+] Cipher.doFinal() hooked: extracting sensitive data

[*] Hiding line from /proc/self/maps: 6d69a0b000-6d6a351000 r--p 00000000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d6a352000-6d6b092000 r-xp 00946000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d6b092000-6d6b163000 r--p 01685000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d6b164000-6d6b17f000 rw-p 01756000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d69a0b000-6d6a351000 r--p 00000000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d6a352000-6d6b092000 r-xp 00946000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d6b092000-6d6b163000 r--p 01685000 00:01 188                            /memfd:frida-agent-64.so (deleted)
[*] Hiding line from /proc/self/maps: 6d6b164000-6d6b17f000 rw-p 01756000 00:01 188                            /memfd:frida-agent-64.so (deleted)


[*] Cipher.doFinal(byte[]) called
    Algorithm: AES/GCM/NoPadding
    Mode: ENCRYPT_MODE
    Input (plaintext): sk-OWASP-MAS-SuperSecretKey-1234567890
    Output (ciphertext hex): e9ff1fd068d108fad6e3e64aca3ce460c169d956123a119ab596887d7f62d14245a24eeb93da5cb8395bbab4f8bc637d0852221fc96e

Backtrace:
javax.crypto.Cipher.doFinal(Native Method)
org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:79)
org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)
org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)
org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)
java.lang.Thread.run(Thread.java:1119)


[*] Cipher.doFinal(byte[]) called
    Algorithm: AES/GCM/NoPadding
    Mode: DECRYPT_MODE
    Input (ciphertext hex): e9ff1fd068d108fad6e3e64aca3ce460c169d956123a119ab596887d7f62d14245a24eeb93da5cb8395bbab4f8bc637d0852221fc96e
    Output (plaintext): sk-OWASP-MAS-SuperSecretKey-1234567890

Backtrace:
javax.crypto.Cipher.doFinal(Native Method)
org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:89)
org.owasp.mastestapp.MainActivityKt.MainScreen$lambda$12$lambda$11(MainActivity.kt:101)
org.owasp.mastestapp.MainActivityKt.$r8$lambda$Pm6AsbKBmypP53K-UABM21E_Xxk(Unknown Source:0)
org.owasp.mastestapp.MainActivityKt$$ExternalSyntheticLambda3.run(D8$$SyntheticClass:0)
java.lang.Thread.run(Thread.java:1119)

Evaluation

The test fails because the BufferedReader.readLine() hook successfully concealed all Frida memory segments from /proc/self/maps, causing detectHooking() to return false. With detection bypassed, the app proceeded with its cryptographic operations, which were intercepted by the Cipher.doFinal() hooks to extract the sensitive API key sk-OWASP-MAS-SuperSecretKey-1234567890 in plaintext.