TL;DR: Apple iCLoud Notes are GZIP’d when stored and this script will decompress them for you.
To set the stage for this post and overall blog, I recently took the SANS advanced smartphone forensics course, FOR585. It was a great opportunity and this flip-phone using Luddite took away a ton of good information. One takeaway was a desire to contribute more to the forensic community and publishing code that I use myself which would benefit others, hence this blog. Another takeaway was that iCloud Notes needs to be better understood, hence this post.
Apple Notes itself appears to be decently parsed by the major tools so I won’t spend much time on it. The relevant SQLite database (notes.sqlite) stores data fairly simply. Even without any tools beyond hexdump, you can clearly find test notes saved in the database. The below image shows a note containing the text “I moved this note.”
iCloud Notes, on the other hand, is not parsed nicely (yet) by anything but Cellebrite’s Physical Analyzer. This is likely because the messages are not stored in plain text. For example, the same note above, when moved to iCloud Notes, looks like this:
Say what? While this looks a bit like garbage, what you’re seeing is the fact that in iCloud Notes, Apple compressed the text of its notes. The relevant SQLite database (NoteStore.sqlite) keeps some of the structure of the notes.sqlite, but the table that keeps the note contents is now ZICNOTEDATA with the note itself in the ZDATA blob column.
By looking at the values in that blob column, what jumps out is that all of the notes which were not locked start with the same 11 bytes (0x1F 0x8B 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x03 0xE3) as demonstrated below (a substring was used to hide the legitimate data for the owner of the iPhone). This is the first clue that there is information here to recover, we just have to figure out what it is.
If you’re ever faced with repeating patterns at the start of every blob, I would look up the file header on a list of magic numbers, just to see if it is an obvious file format. For example, a PNG image would start with .PNG (in plaintext) which would be obvious. In this case it is less obvious, but 0x1F 0x8B indicates the blob being stored is GZIP’d data and the bytes after that are also part of the GZIP header. That means it isn’t encrypted, just compressed, and we have to decompress it. One way to deal with that would be to:
- Open the NoteStore.sqlite in SQLite Browser to
- Click on each ZDATA entry from ZICNOTEDATA and on the right side click “Export”
- Save the blob as whatever_you_want.blob.gz
- Unzip the file using your preferred tool (gunzip on *nix, 7-Zip on Windows)
- Open the whatever_you_want.blob file and look for plaintext at the beginning (there’s more in the blob than just the text).
Saving notes off worked great to prove the point, but it is not scalable and is still manually intensive. Too much double-clicking for me. To encourage laziness, I wrote a Perl script to do the above steps and put the resulting values back into the table so existing SQLite queries still work. You can get fancier with a few options for those that are more comfortable with the command line but the easiest road to success is to export a NoteStore.sqlite and put it in the same directory as the script. You can then execute the script and everything should work. The script does the following:
- Creates a copy of the start file (“just in case”) with the same name as the original file, but with “.decompressed” inserted before .sqlite
- Queries the ZICNOTEDATA table to retrieve the Z_PK and ZDATA columns from the ZICNOTEDATA table
- Saves the resulting blobs as files named note_[Z_PK].blob.gz in the same directory as the script
- Gunzips each .blog.gz file in that directory
- Walks through the resulting files and updates the ZICNOTEDATA table (in the copied database, not the original) with the new gunzipped note.
- Pulls out the plaintext stored for the note (if there is any) and inserts that in to a new column called ZDECOMPRESSEDTEXT
- Deletes all the .blob.gz and .blob files (unless you set the –dirty flag when you run it, I prefer to see the resulting files myself)
This should work for any number of notes that are saved. It will also look at the locked notes, recognize that they are not gzipped data, and ignore them. Until more tools nicely support iCloud Notes, this script should be a good alternative to provide you the ability to run queries across the SQLite database as before, but with the plaintext, where it is available.
- Apple’s iCloud Notes need to be gunzipped before you can recover the actual text.
- There are other places data is compressed in this database, more work is needed to piece everything together.
- If you don’t see plaintext, don’t assume there isn’t information there.