Skip to content
Last updated: July 10, 2024

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

Content in BETA

This content is in beta and still under active development, so it is subject to change any time (e.g. structure, IDs, content, URLs, etc.).

Send Feedback

Sample

The snippet below shows sample code that creates two files in the 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. Ensure the app is running on the target device.
  2. Execute run.sh.
  3. Close the app once you finish testing.

The run.sh script will inject a Frida script called script.js.

run.sh
 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

The Frida script will hook and log calls to open and android.content.ContentResolver.insert.

script.js
 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
// Intercept libc`open to make sure we cover all Java's APIs
Interceptor.attach(Module.getExportByName(null, 'open'), {
    onEnter(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('[WARNING] Opening a file from external storage at:', path)
                Java.performNow(function() {
                    console.log("Invoked from: "+Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))
                });
            }
        });
    }
});

Java.perform(function() {
    let ContentResolver = Java.use("android.content.ContentResolver");
    ContentResolver["insert"].implementation = function (uri, values) {
        var result = this["insert"](uri, values);
        console.log('[WARNING] Opening a file with MediaStore at:', result)
        Java.performNow(function() {
            console.log("Invoked from: "+Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))
        });
        return result
    };
})

Observation

In the output you can see the paths and the relevant stack trace which helps to identify the actual APIs 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
 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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
[WARNING] Opening a file from external storage at: /storage/emulated/0/Android/data/org.owasp.mastestapp/files/secret.txt
Invoked from: java.lang.Exception
    at libcore.io.Linux.open(Native Method)
    at libcore.io.ForwardingOs.open(ForwardingOs.java:167)
    at libcore.io.BlockGuardOs.open(BlockGuardOs.java:252)
    at libcore.io.ForwardingOs.open(ForwardingOs.java:167)
    at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7255)
    at libcore.io.IoBridge.open(IoBridge.java:482)
    at java.io.FileOutputStream.<init>(FileOutputStream.java:235)
    at java.io.FileOutputStream.<init>(FileOutputStream.java:186)
    at org.owasp.mastestapp.MastgTest.mastgTestApi(MastgTest.kt:26)
    at org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:16)
    at org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:73)
    at org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:71)
    at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:987)
    at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:981)
    at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:177)
    at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
    at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:665)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:544)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:566)
    at androidx.compose.foundation.AbstractClickablePointerInputNode.onPointerEvent-H0pRuoY(Clickable.kt:947)
    at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:795)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:317)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
    at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:185)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:104)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:113)
    at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1576)
    at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1527)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1466)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:465)
    at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1849)
    at android.app.Activity.dispatchTouchEvent(Activity.java:3993)
    at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:423)
    at android.view.View.dispatchPointerEvent(View.java:13674)
    at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5482)
    at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5285)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4841)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4807)
    at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4947)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4815)
    at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5004)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4841)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4807)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4815)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7505)
    at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7474)
    at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7435)
    at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7630)
    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188)
    at android.os.MessageQueue.nativePollOnce(Native Method)
    at android.os.MessageQueue.next(MessageQueue.java:336)
    at android.os.Looper.loop(Looper.java:174)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

[WARNING] Opening a file with MediaStore at: content://media/external/downloads/27
Invoked from: java.lang.Exception
    at android.content.ContentResolver.insert(Native Method)
    at org.owasp.mastestapp.MastgTest.mastgTestMediaStore(MastgTest.kt:44)
    at org.owasp.mastestapp.MastgTest.mastgTest(MastgTest.kt:17)
    at org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:73)
    at org.owasp.mastestapp.MainActivityKt$MyScreenContent$1$1$1.invoke(MainActivity.kt:71)
    at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke-k-4lQ0M(Clickable.kt:987)
    at androidx.compose.foundation.ClickablePointerInputNode$pointerInput$3.invoke(Clickable.kt:981)
    at androidx.compose.foundation.gestures.TapGestureDetectorKt$detectTapAndPress$2$1.invokeSuspend(TapGestureDetector.kt:255)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
    at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:177)
    at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
    at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:474)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:508)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:497)
    at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.kt:368)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl$PointerEventHandlerCoroutine.offerPointerEvent(SuspendingPointerInputFilter.kt:665)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.dispatchPointerEvent(SuspendingPointerInputFilter.kt:544)
    at androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNodeImpl.onPointerEvent-H0pRuoY(SuspendingPointerInputFilter.kt:566)
    at androidx.compose.foundation.AbstractClickablePointerInputNode.onPointerEvent-H0pRuoY(Clickable.kt:947)
    at androidx.compose.foundation.AbstractClickableNode.onPointerEvent-H0pRuoY(Clickable.kt:795)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:317)
    at androidx.compose.ui.input.pointer.Node.dispatchMainEventPass(HitPathTracker.kt:303)
    at androidx.compose.ui.input.pointer.NodeParent.dispatchMainEventPass(HitPathTracker.kt:185)
    at androidx.compose.ui.input.pointer.HitPathTracker.dispatchChanges(HitPathTracker.kt:104)
    at androidx.compose.ui.input.pointer.PointerInputEventProcessor.process-BIzXfog(PointerInputEventProcessor.kt:113)
    at androidx.compose.ui.platform.AndroidComposeView.sendMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1576)
    at androidx.compose.ui.platform.AndroidComposeView.handleMotionEvent-8iAsVTc(AndroidComposeView.android.kt:1527)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchTouchEvent(AndroidComposeView.android.kt:1466)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3060)
    at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2755)
    at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:465)
    at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1849)
    at android.app.Activity.dispatchTouchEvent(Activity.java:3993)
    at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:423)
    at android.view.View.dispatchPointerEvent(View.java:13674)
    at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5482)
    at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5285)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4841)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4807)
    at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4947)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4815)
    at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:5004)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
    at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4841)
    at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4807)
    at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4815)
    at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4788)
    at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7505)
    at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7474)
    at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7435)
    at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7630)
    at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:188)
    at android.os.MessageQueue.nativePollOnce(Native Method)
    at android.os.MessageQueue.next(MessageQueue.java:336)
    at android.os.Looper.loop(Looper.java:174)
    at android.app.ActivityThread.main(ActivityThread.java:7356)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)

There are two files written to the external storage:

  • /storage/emulated/0/Android/data/org.owasp.mastestapp/files/secret.txt written using java.io.FileOutputStream from org.owasp.mastestapp.MastgTest.mastgTestApi(MastgTest.kt:26)
  • content://media/external/downloads/27 written using android.content.ContentResolver.insert from org.owasp.mastestapp.MastgTest.mastgTestMediaStore(MastgTest.kt:44)

Note that the calls via ContentResolver.insert do not write directly to the file system, but to the MediaStore content provider, and therefore we can't see the actual file path, instead we see the content:// URI.

Evaluation

This test fails because the files are not encrypted and contain sensitive data (a password and an API key). You can further confirm this by reverse engineering the app and inspecting the code as well as retrieving the files from the device.