Youtube Video on this post

Exporting your contacts and other data from an iPhone is much harder than it looks, especially with out an iCloud account. It’s not as easy as plugging your iPhone into a computer and clicking a button. This post goes over the steps I went through to export my media and contacts from an iTunes backup. I learned much of this while building an iOS forensics tool at a hackathon. If you’re looking for a somewhat out of the box solution go give it a shot.

Creating an iTunes backup

While MacOS provides a “phone file browser” built into Finder it only displays limited data. Excluding things like your contacts, photos, voice memos and messages.
It still technically backs up this data however it only does so in a format to be read by restoring the backup on to an iPhone. In order to make one of these backups on MacOS your device needs to have enough storage on disk and Finder does not provide a way to make backups on external drives. You can get around this by creating a symbolic link to an external drive in the ~/Library/Application\ Support/MobileSync/Backup directory. This can be done with the following command, substitute Untitled with the name of the drive you mounted.

sudo ln -s /Volumes/Untitled/Backup/ ~/Library/Application\ Support/MobileSync/Backup

Next plug in your iPhone and browse to the finder window where you phone pops up. It’s important that when you click Back Up Now that you have not selected the option encrypt local backup. Once your backup starts you files should be exporting to your external drive or whatever folder you chose for the backup.

Understanding the backup structure

After the back up is complete, if you navigate to the external folder you should see a file structure similar to this: 66a0b71e9af1f34b4a1561ca2b9f562e.png

I’m not sure what the .plist files do but you won’t need them for exporting contacts and media. Depending on the size of your backup you might have well over a hundred nondescript directories containing about a hundred individual files each. All named with 40 character strings and no file extensions. 2bebbf7a4ec1626b31e623eeccbb262d.png

If you preview these files with finder you’ll probably see some familiar images.


Even though these are all named as “Unix Executable” they all hold data that can be accessed by attaching the correct file extension. If we attached the correct file extension we can view the data as intended.


Check out this cool picture I got outside the RAWLS College of Business during a snow storm last year 12b767eacf2a3a922c2102f95af7c8e8.png


According to this blog post by Rich Infante, Manifest.db is an sqlite database file with two tables

  • Files
    • fileID TEXT PRIMARY KEY the name of the file in the backup (derived from a hash)
    • domain TEXT What sandbox the file is in
    • relativePath TEXT File relative to domain
    • flags INTEGER Unix file flags
    • file BLOB - Binary plist containing poperties
  • Properties
    • Two fields: key TEXT PRIMARY KEY,
    • value BLOB

If we open the Manifest.db with the sqlite3 CLI and query the Files table for the name of the file we just looked at, it returns a row with more detailed data about the file.

sqlite3 Manifest.db
sqlite> .mode markdown
sqlite> SELECT fileID,domain,relativePath,flags FROM Files WHERE fileID='31e61544715d13c76b3190aadd5dc679206348dc';


fileID domain relativePath flags
31e61544715d13c76b3190aadd5dc679206348dc CameraRollDomain Media/DCIM/104APPLE/IMG_4224.JPG 1

The CamraRollDomain in the domain column tells us that it’s probably a media file. We can use this method to write a script that helps us narrow down the file extensions for every file in our backup. But if we open up the database in a tool like DB Browser for SQLite and search for CamraRollDomain it quickly becomes apperent that CamraRollDomain is a broad label for several diffrent filetypes in the backup. 54179d6dfcff78804277e6bae7c3fe0a.png

Determining the datatypes with the file command

file is a unix command available on MacOS that will attempt to classify any filetype you feed it, file determines the filetype based off the internal data, it does not require a file extension and can even determine a file type with a false extension.

This is really helpful for checking sketchy files before opening them.


$ file totally_not_malicous.pdf
report.pdf: Mach-O 64-bit executable x86_64


We could write a shell script to run file on all the artifacts in our backup like this:

find . -type f -exec file {} \; | grep "JPEG"

But to prevent things from getting messy it’s better to access the file commands functionality with the python-magic library.

import magic
f = magic.Magic(mime=True)
ftype = f.from_file("31e61544715d13c76b3190aadd5dc679206348dc")
print(ftype) # image/jpeg

We can now write some code to get the file type of every artifact in the backup

# Recursively finds all files in the backup
def find_ftypes(filepath="/path/to/Backup"):
    for root, dir, files in os.walk(filepath):
        for file in files:
            full_path = f"{root}/{file}"
            f = magic.Magic(mime=True)
            ftype = f.from_file(full_path)

/path/to/Backup/34/346445c16e04454cd1375049cde7404cf6fd23ce video/quicktime
/path/to/Backup/34/3442966c9885693f90f8319c4f6a012fd178c7fa video/mp4
/path/to/Backup/34/34e444518662a86f6c0accba86c9d3a556607b8a image/png
/path/to/Backup/34/34e7273bd0bad66974b6f574e2de5943f724274b image/jpeg

And with a little more logic we can copy each filetype to another directory with the proper file extension:

import magic, os, shutil
def find_ftypes(filepath="/path/to/Backup"):
    for root, dir, files in os.walk(filepath):
        # Make directories

        for file in files:
            full_path = f"{root}/{file}"
            f = magic.Magic(mime=True)
            ftype = f.from_file(full_path)

						# check mime type and copy to respective directory
            if ftype == "image/jpeg":
                shutil.copyfile(full_path, f"./media/jpeg/{file}.jpeg")

            if ftype == "image/png":
                shutil.copyfile(full_path, f"./media/png/{file}.png")

            if ftype == "video/mp4":
                shutil.copyfile(full_path, f"./media/mp4/{file}.mp4")

            if ftype == "video/quicktime":
                shutil.copyfile(full_path, f"./media/mov/{file}.MOV")


Exporting Contacts

Retrieving your contacts is a bit more tricky. A search for the keyword contacts in the Manifest.db returns over 20 rows and none of them contain the right file path. To find the contacts file we need to search for a row with a colum value relativePath='Library/AddressBook/AddressBook.sqlitedb'. I found this thanks to the iLEAPP project (An iOS forensics tool written in python). The project has a very helpful dictionary with paths to many sqlite databases for common things like contacts, notes & safari bookmarks.

Reading the AdressBook database

We can find the file name of the AdressBook database with the following query.

sqlite3 Manifest.db "SELECT fileID FROM Files WHERE relativePath='Library/AddressBook/AddressBook.sqlitedb'"

Then we can find this file in the backup with the find command and save it to another directory to work on

cd /path/to/Backup
find . -name "31bb7ba8914766d4ba40d6dfb6113c8b614be442"
cp path/to/file/31bb7ba8914766d4ba40d6dfb6113c8b614be442 /path/to/other/AdressBook.db

When I opened this file in sqlite viewer I was amazed at the complexity It has over 20 tables and what I’m assuming are virtual tables for full text search features 34100da460da823ce2c38daa5d593167.png

I managed to find a table with phone number but they were missing the corresponding names 4a0a224a23a90dfd0df325b39bac5114.png

Fortunately the iLEAPP project already went through the hassle of building a query that will return the names, numbers and other notes you attached to the contact. We can borrow this query and run it in a python script like this:

import sqlite3, csv

con = sqlite3.connect("AdressBook.db")
cur = con.cursor()
LEFT OUTER JOIN ABPersonFullTextSearch_content on ABPerson.ROWID = ABPersonFullTextSearch_content.ROWID
res = cur.fetchall()
csv_list = []
for row in res:
	if row[1] is not None:
		numbers = row[1].split(" +")
		number = numbers[1].split(" ")
		phone_number = "+{}".format(number[0])
		phone_number = ''

	contact_id = row[0]
	first_name = row[2]
	last_name = row[4]

	csv_list.append([contact_id, phone_number, first_name, last_name])

with open("contacts.csv", "w") as f:
	writer = csv.writer(f, delimiter=",")
	writer.writerow(['contact_id','phone_number', 'first_name', 'last_name'])
	for row in csv_list:

This should write out a csv file named contacts.csv with the format:

contact_id phone_number first_name last_name