packageorg.owasp.mastestappimportandroid.content.Contextimportandroid.util.Logimportjava.io.Fileimportjava.io.FileOutputStreamimportjava.io.IOExceptionimportandroid.content.ContentValuesimportandroid.os.Environmentimportandroid.provider.MediaStoreimportjava.io.OutputStreamclassMastgTest(privatevalcontext:Context){funmastgTest():String{mastgTestApi()mastgTestMediaStore()return"SUCCESS!!\n\nFiles have been written with API and MediaStore"}funmastgTestApi(){valexternalStorageDir=context.getExternalFilesDir(null)valfileName=File(externalStorageDir,"secret.txt")valfileContent="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)}}funmastgTestMediaStore(){try{valresolver=context.contentResolvervarrandomNum=(0..100).random().toString()valcontentValues=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)}valtextUri=resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI,contentValues)textUri?.let{valoutputStream: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)}}}
Make sure you have Frida for Android installed on your machine and the frida-server running on the device
Run run.sh to spawn the app with Frida
Click the Start button
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 910111213
#!/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\-forg.owasp.mastestapp\-lscript.js\-ooutput.txt
// It calls Java.performNow to ensure Java classes are available.functionprintBacktrace(maxLines=8){Java.performNow(()=>{letException=Java.use("java.lang.Exception");letstackTrace=Exception.$new().getStackTrace().toString().split(",");console.log("\nBacktrace:");for(leti=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 APIsInterceptor.attach(Module.getExportByName(null,'open'),{onEnter:function(args){constexternal_paths=['/sdcard','/storage/emulated'];constpath=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 URIJava.perform(()=>{letContentResolver=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()}`);}letresult=this.insert(uri,values);console.log(`\n[*] ContentResolver.insert returned URI: ${result.toString()}`);printBacktrace();returnresult;};});
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.
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.