Skip to content

MASTG-DEMO-0002: External Storage APIs Tracing with Frida

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

Sample

The snippet below shows sample code that creates two files in external storage using the getExternalFilesDir method and the MediaStore API.

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
package org.owasp.mastestapp

import android.content.Context
import android.util.Log
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import android.content.ContentValues
import android.os.Environment
import android.provider.MediaStore
import java.io.OutputStream

class MastgTest (private val context: Context){

    fun mastgTest(): String {
        mastgTestApi()
        mastgTestMediaStore()
        return "SUCCESS!!\n\nFiles have been written with API and MediaStore"
    }
    fun mastgTestApi() {
        val externalStorageDir = context.getExternalFilesDir(null)
        val fileName = File(externalStorageDir, "secret.txt")
        val fileContent = "secr3tPa\$\$W0rd\n"

        try {
            FileOutputStream(fileName).use { output ->
                output.write(fileContent.toByteArray())
                Log.d("WriteExternalStorage", "File written to external storage successfully.")
            }
        } catch (e: IOException) {
            Log.e("WriteExternalStorage", "Error writing file to external storage", e)
        }
    }

    fun mastgTestMediaStore() {
        try {
            val resolver = context.contentResolver
            var randomNum = (0..100).random().toString()
            val contentValues = ContentValues().apply {
                put(MediaStore.MediaColumns.DISPLAY_NAME, "secretFile$randomNum.txt")
                put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
                put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
            }
            val textUri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)

            textUri?.let {
                val outputStream: OutputStream? = resolver.openOutputStream(it)
                outputStream?.use {
                    it.write("MAS_API_KEY=8767086b9f6f976g-a8df76\n".toByteArray())
                    it.flush()
                }
                Log.d("MediaStore", "File written to external storage successfully.")
            } ?: run {
                Log.e("MediaStore", "Error inserting URI to MediaStore.")
            }
        } catch (exception: Exception) {
            Log.e("MediaStore", "Error writing file to URI from MediaStore", exception)
        }
    }
}

Steps

  1. Install the app on a device ( Installing Apps)
  2. Make sure you have Frida for Android installed on your machine and the frida-server running on the device
  3. Run run.sh to spawn the app with Frida
  4. Click the Start button
  5. Stop the script by pressing Ctrl+C

The run.sh script injects a Frida for Android script named script.js. This script hooks and logs calls to the native open function and to android.content.ContentResolver.insert. It logs the paths of files written to external storage, the caller's stack trace, and additional details such as the ContentValues provided.

Note: When apps write files using the ContentResolver.insert() method, the files are managed by Android's MediaStore and are identified by content:// URIs, not direct file system paths. This design abstracts the actual file locations, making them inaccessible through standard file system operations like the open function in libc. Consequently, when using Frida to hook into file operations, intercepting calls to open won't reveal these files.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

# SUMMARY: This script uses frida to trace files that an app has opened since it spawned
# The script filters the output of frida-trace to print only the paths belonging to external
# storage but the the predefined list of external storage paths might not be complete.
# A sample output is shown in "output.txt". If the output is empty, it indicates that no external
# storage is used.

frida \
    -U \
    -f org.owasp.mastestapp \
    -l script.js \
    -o 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
42
43
44
45
46
47
48
49
// It calls Java.performNow to ensure Java classes are available.
function printBacktrace(maxLines = 8) {
    Java.performNow(() => {
        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]);
        }
    });
}

// Intercept libc's open to make sure we cover all Java I/O APIs
Interceptor.attach(Module.getExportByName(null, 'open'), {
    onEnter: function(args) {
        const external_paths = ['/sdcard', '/storage/emulated'];
        const path = Memory.readCString(ptr(args[0]));
        external_paths.forEach(external_path => {
            if (path.indexOf(external_path) === 0) {
                console.log(`\n[*] open called to open a file from external storage at: ${path}`);
                printBacktrace(15);
            }
        });
    }
});

// Hook ContentResolver.insert to log ContentValues (including keys like _display_name, mime_type, and relative_path) and returned URI
Java.perform(() => {
    let ContentResolver = Java.use("android.content.ContentResolver");
    ContentResolver.insert.overload('android.net.Uri', 'android.content.ContentValues').implementation = function(uri, values) {
        console.log(`\n[*] ContentResolver.insert called with ContentValues:`);


        if (values.containsKey("_display_name")) {
            console.log(`\t_display_name: ${values.get("_display_name").toString()}`);
        }
        if (values.containsKey("mime_type")) {
            console.log(`\tmime_type: ${values.get("mime_type").toString()}`);
        }
        if (values.containsKey("relative_path")) {
            console.log(`\trelative_path: ${values.get("relative_path").toString()}`);
        }

        let result = this.insert(uri, values);
        console.log(`\n[*] ContentResolver.insert returned URI: ${result.toString()}`);
        printBacktrace();
        return result;
    };
});

Observation

In the output you can observe the file paths, the relevant stack traces, and other details that help identify which APIs were used to write to external storage and their respective callers.

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
[*] open called to open a file from external storage at: /storage/emulated/0/Android/data/org.owasp.mastestapp/files/secret.txt

Backtrace:
libcore.io.Linux.open(Native Method)
libcore.io.ForwardingOs.open(ForwardingOs.java:563)
libcore.io.BlockGuardOs.open(BlockGuardOs.java:274)
libcore.io.ForwardingOs.open(ForwardingOs.java:563)
android.app.ActivityThread$AndroidOs.open(ActivityThread.java:8063)
libcore.io.IoBridge.open(IoBridge.java:560)
java.io.FileOutputStream.<init>(FileOutputStream.java:236)
java.io.FileOutputStream.<init>(FileOutputStream.java:186)
org.owasp.mastestapp.MastgTest.mastgTestApi(MastgTest.kt:26)
org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:16)
org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:93)
org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:91)
androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:987)
androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:981)
androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)

[*] ContentResolver.insert called with ContentValues:
    _display_name: secretFile55.txt
    mime_type: text/plain
    relative_path: Download

[*] ContentResolver.insert returned URI: content://media/external/downloads/1000000108

Backtrace:
android.content.ContentResolver.insert(Native Method)
org.owasp.mastestapp.MastgTest.mastgTestMediaStore(MastgTest.kt:44)
org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:17)
org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:93)
org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:91)
androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:987)
androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:981)
androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)

Two files are written to external storage:

  • /storage/emulated/0/Android/data/org.owasp.mastestapp/files/secret.txt:
    • Written via java.io.FileOutputStream
    • Location: org.owasp.mastestapp.MastgTest.mastgTestApi(MastgTest.kt:26).
  • secretFile55.txt:
    • Written via android.content.ContentResolver.insert
    • Location: org.owasp.mastestapp.MastgTest.mastgTestMediaStore(MastgTest.kt:44).
    • Found as URI: content://media/external/downloads/1000000108.

The ContentResolver.insert call used the following ContentValues:

  • _display_name: secretFile55.txt
  • mime_type: text/plain
  • relative_path: Download

Using this information we can infer the path of the file written to external storage: /storage/emulated/0/Download/secretFile55.txt.

Evaluation

This test fails because the files are not encrypted and contain sensitive data (such as a password and an API key). This can be further confirmed by reverse-engineering the app to inspect its code and retrieving the files from the device.