Skip to main content

Sensitive Data Disclosure

iOS apps routinely store information in SQLite databases, or Properly List (PLIST) files. This could include sensitive information such as credentials, session information, encryption keys, etc. It is always worthwhile to review every file in the both the Bundle and Data directories to see what you can find.

To review the information in SQLite databases, you will use the sqlite3 command which should be on your device. PLIST files can be either XML or binary, but the plutil command will convert them if necessary.

Some other files will be binary, so you will need to use a tool such as the strings command to review them.

Property List (plist)

Search for all PLIST files in both the Bundle and Data directories on the device.

# find ${BUNDLEDIR} -type f -exec grep -ali plist {} \;
# find ${DATADIR} -type f -exec grep -ali plist {} \;

To review a specific PLIST file:

# plutil filename.plist

NSKeyedArchiver Data

In many cases during runtime the application will store data in a PLIST file using an Apple API called NSKeyedArchiver. This API is used to serialize data which is then stored in the PLIST file as hex. If you encounter this type of data, you can easily decode it:

. . .
LastUserIdentity = <62706c69 73743030 d4010203 04050607 0a582476
65727369 6f6e5924 61726368 69766572 5424746f 7058246f 626a6563
74731200 0186a05f 100f4e53 4b657965 64417263 68697665 72d10809
5f10104c 61737455 73657249 64656e74 69747980 01a50b0c 13141555
246e756c 6cd30d0e 0f101112 59757365 7249644b 6579586f 72674964
4b657956 24636c61 73738002 80038004 5f100f30 30357130 30303030
30364f42 7a535f10 12303044 71303030 30303030 45416943 454157d2
16171819 5a24636c 6173736e 616d6558 24636c61 73736573 5f101553
46557365 72416363 6f756e74 4964656e 74697479 a21a1b5f 10155346
55736572 4163636f 756e7449 64656e74 69747958 4e534f62 6a656374
00080011 001a0024 00290032 00370049 004c005f 00610067 006d0074
007e0087 008e0090 00920094 00a600bb 00c000cb 00d400ec 00ef0107
00000000 00000201 00000000 0000001c 00000000 00000000 00000000
00000110>;
. . .

Simply copy out the hex data between the < & the > and pipe it into a tool that can reverse the hex -- such as xxd.

echo “{ 62706c69 . . . 00000110 }” | xxd -r -p

bplist00?
X$versionY$archiverT$topX$objects??_NSKeyedArchiver _LastUserIdentity??

YuserIdKeyXorgIdKeyV$class???_005q0000006OBzS_00Dq0000000EAiCEAWZ
$classnameX$classes_SFUserAccountIdentity SFUserAccountIdentityXNSObjec$)
27IL_agmt~????????????

While this is not especially helpful, we can see from the beginning of the output bplist00? that it is another property list file embedded into a property list file. So, we can further extend our command to automatically decode the binary PLIST data using plutil.

echo “{ 62706c69 . . . 00000110 }” | xxd -r -p | plutil -p -

{
"$archiver" => "NSKeyedArchiver"
"$objects" => [
0 => "$null"
1 => {
"$class" => <CFKeyedArchiverUID 0x7fefc1e08ab0 [0x7fff8b4988c0]>{value = 4}
"orgIdKey" => <CFKeyedArchiverUID 0x7fefc1e08a90 [0x7fff8b4988c0]>{value = 3}
"userIdKey" => <CFKeyedArchiverUID 0x7fefc1e08a70 [0x7fff8b4988c0]>{value = 2}
}
2 => "005q0000006OBzS"
3 => "00Dq0000000EAiCEAW"
4 => {
"$classes" => [
0 => "SFUserAccountIdentity"
1 => "NSObject"
]
"$classname" => "SFUserAccountIdentity"
}
]
"$top" => {
"LastUserIdentity" => <CFKeyedArchiverUID 0x7fefc1e089b0 [0x7fff8b4988c0]>{value = 1}
}
"$version" => 100000
}

This data is much better for us to review for sensitive information.

As the name NSKeyedArchiver implies, the data is listed as a key value pair. For instance, the data below is an entry for the "orgIdKey".

"orgIdKey" => <CFKeyedArchiverUID 0x7fefc1e08a90 [0x7fff8b4988c0]>{value = 3}

It is linked to the value listed as "3" in the data block. We can see that the value of 3 is:

3 => \"00Dq0000000EAiCEAW\"

So, in this case the orgIdKey is 00Dq0000000EAiCEAW.

Decoding this data can quickly become tedious, but it will frequently reveal username's, passwords, tokens, keys, etc.

SQLite Databases

Check the Data directory for all SQLite databases. Try to open every one of them and review the information. Many times, Cache files with HTTP request/response are stored in these. Session ID's and/or credentials could also be here:

# find $APP_DIR -type f -exec grep -ali sqlite {} \;
# find $APP_DIR -type f -exec grep -ali data {} \;
# find $DATA_DIR -type f -exec grep -ali sqlite {} \;
# find $DATA_DIR -type f -exec grep -ali data {} \;

To open an SQLite database for review:

# sqlite3 filename.db

To list the tables in the database:

> .tables

To review the data in a table:

> select * from tablename;

To exit the database:

> .exit

Recover Deleted Database Records

It is also a good idea to check the SQLite database for "deleted" records. SQLite is configured to simply mark data as deleted, without actually removing it from the database file. Using some basic forensics tools, you can "recover" these records and review them. The main tool is the SQLite Parser.

% python sqlparse.py -f filename.db -r -o report.txt

Review the report.txt file for deleted/recovered records.

SQLCipher Databases

SQLCipher is an open-source library that provides transparent, secure 256-bit AES encryption for SQLite databases. Essentially, it is a specialized build of SQLite to perform on-the-fly database encryption. It uses the standard SQLite APIs to manipulate tables using SQL.

To interact with a SQLCipher encrypted database, you will need the "sqlcipher" command, or an SQLite utility capable of interacting with the encrypted database.


Note: To migrate from SQLCipher 3.x to 4.x, you will need to run these commands in the sqlcipher session:

PRAGMA key = '<key material>';
PRAGMA cipher_migrate;

Setting Encryption Key

The encryption key is typically set as a PRAGMA key in the database. There are three ways this key can be set:

  • passphrase
  • raw key data
  • raw key data with a salt

When using a passphrase, the passphrase is converted to a key using the PBKDF2 key derivation process. Raw key data does not use key derivation.

  • If the raw key data is 64-character hex string, then the key is not salted.

  • If the raw key data contains a salt, then the overall length of the key is 96 characters. The first 64 characters are the key, the remaining 32 characters are the salt.

Passphrase Example:

sqlite> PRAGMA key = 'passphrase';

Raw Key Data (No Salt) Example:

sqlite> PRAGMA key = "x'281B2DC1044B55761594552D903D5FA5B8E5604C2237EFCB16BF65D732E5D2AB'";

Raw Key Data Example with Salt Example:

PRAGMA key = "x'276381B240FADDC1044B535572A67C61594552D92AF03D5FA5B8E560681C4C2E38C237EC34FCB16CE9BF65D732E5D2AB'";

Recover the Key

To open the encrypted database, you will obviously need the key that was used when the database was encrypted or re-keyed. For mobile apps, this can be changed anytime that the app is initialized, or when the user logs in to the app. So, it is important that you copy over the encrypted database and recover the key in the same session.

Typically, there are two ways to recover the raw key; pull it from memory or use Frida to get it at runtime. Pulling the key from memory is the easiest method. Using Frida scripts will need to be custom written per app to ensure you are tracing the correct function.

Dump Memory

Dump the running process memory using any of the common methods. In the example below, I used fridump to output the data into a "strings.txt" file. If the database is using a raw key, it will start with an x' followed by the hex string of the key. So, just search for that in the strings.txt file.

% grep "x\'" strings.txt 
%x'8)D
'H'x'
.) (x'6'j'-(W)
x'276381b240faddc1044b535572a67c61594552d92af03d5fa5b8e560681c4c2e38c237ec34fcb16ce9bf65d732e5d2ab'!
x'276381b240faddc1044b535572a67c61594552d92af03d5fa5b8e560681c4c2e38c237ec34fcb16ce9bf65d732e5d2ab'

In this example, we find two locations that contain the raw key. Also, since this key is 96 characters, we know that there is a salt used for the encryption.

For mobile apps, a "raw key" is typically used since that key can be created easily without needing to store a passphrase in the code or configuration files. With a raw key, it is just a matter of obtaining 64 or 96 characters of hex data from a random number generator.

Binary Disassembly

If the database used a passphrase for the encryption key, then you will need to disassemble the executable to recover the passphrase.

Open the Encrypted Database

Open the encrypted database - using the ATTACH SQL method. In this method, open a new SQLite3 database (file does not need to exist), then attach the encrypted database to it.

% sqlcipher test.db
SQLCipher version 3.27.2 2019-02-25 16:06:06
Enter ".help" for usage hints.
sqlite> ATTACH DATABASE 'encrypted.db' AS db2 KEY "x'276381B240FADDC1044B535572A67C61594552D92AF03D5FA5B8E560681C4C2E38C237EC34FCB16CE9BF65D732E5D2AB'";
sqlite>
sqlite> --if you have sqlcipher 4 but the DB used sqlcipher 3, run this:
sqlite> PRAGMA cipher_migrate;
sqlite>
sqlite> .tables
db2.TableName1 db2.TableName3 db2.TableName5
db2.TableName2 db2.TableName4
sqlite>

At this point, use the SELECT statement to review the information that is stored in each table.

sqlite> SELECT * FROM db2.TableName1;
sqlite> SELECT * FROM db2.TableName2;

Decrypt the Database

Once you know the key works, the best thing to do is to decrypt the database, and export it to an unencrypted database, which will make its interaction much easier. In the example below we will use the sqlcipher command to open the encrypted database, then attach a new database to it (named unencrypted.db), then export the tables to the new, unencrypted database. Following this, we will be able to easily interact with the new "unencrypted.db" with just the standard sqlite3 commands.

% sqlcipher encrypted.db
SQLCipher version 3.27.2 2019-02-25 16:06:06
Enter ".help" for usage hints.
sqlite> PRAGMA key = "x'276381B240FADDC1044B535572A67C61594552D92AF03D5FA5B8E560681C4C2E38C237EC34FCB16CE9BF65D732E5D2AB'";
sqlite>
sqlite> --if you have sqlcipher 4 but the DB used sqlcipher 3, run this:
sqlite> PRAGMA cipher_migrate;
sqlite>
sqlite> ATTACH DATABASE 'unencrypted.db' AS plaintext KEY '';
sqlite> SELECT sqlcipher_export('plaintext');

sqlite> .exit

Now, basic SQLite interaction is available without requiring the key.

% sqlite3 unencrypted.db 
SQLite version 3.24.0 2018-06-04 14:10:15
Enter ".help" for usage hints.
sqlite> .tables
TableName1 TableName2 TableName3 TableName4 TableName5
sqlite> select * from TableName3;
4437587|Test Test Test

Type GETINFO for details|outbound|unicast|delivered|2019-10-23T17:45:19.657553|855807||||852765|858148
sqlite>
sqlite> .exit