Home iOS - Application Backup Analysis
Post
Cancel

iOS - Application Backup Analysis

iOS backups are a frequently overlooked data exposure vector. When a user backs up their iPhone to a Mac or to iCloud, the backup includes every app’s Documents/, Library/Preferences/, and Library/Application Support/ directories. If an app stores session tokens, credentials, or PII in those locations without marking the files as backup-excluded, all of it is in the backup. The backup does not require device unlock to read on a Mac that has previously trusted the device.


What iOS backups include (and what they skip)

Included by default:

  • Documents/ - any files the app saves there
  • Library/Preferences/ - NSUserDefaults plist files
  • Library/Application Support/ - CoreData databases, config files
  • Library/WebKit/ - sometimes contains cached webview state

Excluded by default:

  • Library/Caches/ - system-designated cache directory
  • tmp/ - scratch space
  • Files explicitly marked with NSURLIsExcludedFromBackupKey = true

Keychain items are backed up in encrypted form keyed to the device if ThisDeviceOnly is not used. Items with kSecAttrAccessibleWhenUnlockedThisDeviceOnly are NOT backed up.


Taking a backup

Connect the device over USB. Trust the computer if prompted.

1
2
# Full backup of all apps
idevicebackup2 backup --full .

The backup lands in a directory named by the device UDID. Each file inside is a hash-named blob.

Extract the backup into a readable directory tree:

1
idevicebackup2 unback . restore/


Navigating the backup

The extracted tree groups files by domain. App sandbox data lands under:

1
2
3
4
5
6
7
8
9
restore/
└── AppDomain-com.vulnlab.iosapp/
    ├── Documents/
    ├── Library/
    │   ├── Preferences/
    │   │   └── com.vulnlab.iosapp.plist
    │   └── Application Support/
    │       └── VulnLabAppiOS.sqlite
    └── ...

AppDomain-com.vulnlab.iosapp maps directly to the app’s sandbox container. The bundle identifier is the last component.


Finding secrets in the plist

1
2
3
4
5
6
7
8
9
# Convert binary plist to XML and grep
plutil -convert xml1 \
  restore/AppDomain-com.vulnlab.iosapp/Library/Preferences/com.vulnlab.iosapp.plist \
  -o -

# Or just read it
cat restore/AppDomain-com.vulnlab.iosapp/Library/Preferences/com.vulnlab.iosapp.plist \
  | plutil -convert xml1 - -o - \
  | grep -E 'token|api_key|password|session'

In VulnLabAppiOS, the NSUserDefaults plist contains session_token and api_key written by LoginViewController. Both land in Library/Preferences/com.vulnlab.iosapp.plist and both appear in any iTunes or iCloud backup.


CoreData / SQLite extraction

1
2
3
4
5
6
7
sqlite3 \
  "restore/AppDomain-com.vulnlab.iosapp/Library/Application Support/VulnLabAppiOS.sqlite" \
  ".tables"

sqlite3 \
  "restore/AppDomain-com.vulnlab.iosapp/Library/Application Support/VulnLabAppiOS.sqlite" \
  "SELECT * FROM ZUSERDATA;"

Any encrypted-at-rest flag in the CoreData model is app-level encryption, not filesystem encryption. If the app did not encrypt the SQLite contents itself, the rows are plaintext.


Frida script listing backup-eligible files

At runtime, enumerate every file in the app sandbox and check whether it is excluded from backup:

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
const fm  = ObjC.classes.NSFileManager['+ defaultManager'].call(ObjC.classes.NSFileManager);
const home = ObjC.classes.NSProcessInfo['+ processInfo'].call(ObjC.classes.NSProcessInfo)
    .environment().objectForKey_('HOME').toString();

// Walk Documents, Library, and Application Support
['Documents', 'Library/Preferences', 'Library/Application Support'].forEach(function(subdir) {
    const dir = home + '/' + subdir;
    const error = Memory.alloc(Process.pointerSize);
    Memory.writePointer(error, NULL);
    const files = fm.contentsOfDirectoryAtPath_error_(
        ObjC.classes.NSString.stringWithString_(dir), error);
    if (!files) return;
    for (let i = 0; i < files.count(); i++) {
        const name = files.objectAtIndex_(i).toString();
        const full = ObjC.classes.NSString.stringWithString_(dir + '/' + name);
        const urlVal = Memory.alloc(Process.pointerSize);
        const url = ObjC.classes.NSURL.fileURLWithPath_(full);
        const resErr = Memory.alloc(Process.pointerSize);
        Memory.writePointer(resErr, NULL);
        url.getResourceValue_forKey_error_(urlVal, 'NSURLIsExcludedFromBackupKey', resErr);
        const excluded = new ObjC.Object(Memory.readPointer(urlVal));
        console.log('[backup] ' + dir + '/' + name +
            ' excluded=' + (excluded.isNil() ? 'false' : excluded.toString()));
    }
});

Files that print excluded=false are backed up. Any secret-bearing file that is not excluded is a finding.


The missing Info.plist flag

VulnLabAppiOS’s Info.plist does not include NSApplicationIsBackupAllowed = NO. That flag disables app backup entirely:

1
2
<key>NSApplicationIsBackupAllowed</key>
<false/>

Setting it to false prevents the app’s entire data container from being included in any backup. Use it when the app handles credentials, financial data, or health records. For apps where some files should be backed up (user-generated documents), leave the flag absent and mark sensitive files individually.


Fixing per-file exclusion

Mark individual sensitive files as backup-excluded after creating them:

1
2
3
4
var url = URL(fileURLWithPath: sensitiveFilePath)
var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true
try? url.setResourceValues(resourceValues)

Apply this to:

  • Any file containing session tokens or credentials.
  • The SQLite database if it stores sensitive user data.
  • The UserDefaults plist if it stores anything beyond display preferences.

For UserDefaults specifically, move sensitive values to Keychain instead of trying to exclude the plist after the fact. The plist path changes with app updates and the exclusion flag must be re-applied.


Closing

Backup analysis recovers plaintext secrets from any app that stores them in Documents/ or Library/Preferences/ without exclusion flags. The audit is a three-step loop: back up, navigate the domain directory, grep the plists. Any credential that appears in a backup is a finding.

Happy Hacking !!

This post is licensed under CC BY 4.0 by the author.