Photo Vault app still pwnable in 2019? An adventure in iOS RE

It’s been a while since I posted anything, and I suppose that’s a natural part of having a blog. I decided not to force myself to procure content and instead wait until I had something I really wanted to write about. And so here we are! In this article I’m going to talk about a process brand new to me until a few days ago. This has been an absolute blast to learn about, although I will admit it was frustrating at times.

This article focuses more on the outcome of my research, without dwelling too much on exactly how I got there. I am however planning a follow-up post with a whole pile of lessons learned as I think there are a lot of gotchas and overall frustrations that could very possibly be skipped.

Why target this app specifically?

com.enchantedcloud.photovault or “Private Photo Vault” (hereafter PPV) has been the subject of security research before. In November 2015, a detailed breakdown was published by Michael Allen at IOActive and he found that the app didn’t actually encrypt anything! It’s security amounted to blocking users from seeing any media inside until the passcode had been entered and this was extremely easy to defeat. I figured revisiting this same app in 2019 could be fun/interesting just to see how far it has or hasn’t come since then.

Key Takeaways

Whether you consider this app secure or not depends on what kind of access you’ve got to various extraction methods. For examiners with filesystem type extractions (GrayKey / Cellebrite CAS / jailbroken devices), the security of PPV is trivial to defeat and I will demonstrate how below. For examiners obtaining logical type extractions (iTunes backup, UFED 4PC, Magnet ACQUIRE, etc.) decryption will be more challenging and further reversing work will be required. I do believe it is possible though.

PPV uses RNCryptor, an encryption library with implementations available in ObjectiveC, C#, JS etc. RNCryptor is open source and we can absolutely use that to our advantage. One thing RNCryptor doesn’t manage is key storage, and the developer of PPV has apparently decided to rely on the security of the iOS Keychain to store, well, everything we need to perform decryption.

The master key is stored in the keychain under “ppv_DateHash”. The plaintext PIN, which is a maximum 4 digits, is also stored in the keychain as “ppv_uuidHash1”.

Each encrypted media file (found with its original in the app’s sandbox at /Library/PPV_Pics/) is essentially a container. The first two bytes can be safely ignored, the next 16 bytes are the IV (Initialization Vector), and the remaining bytes are the cipher text with the exception of the last 32 bytes which are related to HMAC and can safely be ignored.

Once generated, the master encryption key never changes even if you change your PIN. This might seem like a poor design choice, but it’s actually how your iPhone works too and it can be quite secure as long as the master key is well protected. Secure Enclave makes sure that this key never sees the light of day but this is not true for keychain data.

Basic Outline of the Process / Tools Used

  • Locate and jailbreak test iOS device (I used Electra root for my test device, an iPhone 6S running iOS 11.2.1).
  • Installed PPV (target app) by sideloading with Cydia Impactor (app store works too).
  • Setup access over USB with ITNL (iTunnel) and obtained root access to device via SSH.
SSH tunnel over USB thanks to itnl.
  • Installed and verified operation of frida-server on the device – I did this using Sileo but should be doable via Cydia as well.
  • Used frida-ios-dump by AloneMonkey to obtain decrypted binary of the target app (recommend Python 3.7)
  • Conducted static analysis of decrypted binary using Hopper . I had great success with searching for a value from the plist I believed to be associated to crypto. This app is not free but the trial is fully functional for 15 minutes – make sure you hurry! 🙂
Static analysis using Hopper – this class looks like it might be of use!
  • With my newly discovered knowledge I fired up Frida with this little gem: ObjC Method Observer, an awesome codeshare script by mrmacete (@bezjaje) to snoop on iOS method invocations of a specific class on a live device. (I targetted LSLCrypt and RNCryptor classes on PPV)
Note the test passcode of 1234 at the end of the giant SHA256 string.
  • Switched back and forth between Hopper and Frida console until I established a good idea of what was going on. The biggest breakthrough here was that the encryption key doesn’t change when you change the passcode, and that it is stored in keychain.plist
PIN change does not affect our encryption key, which conveniently gets stored in this device’s keychain.plist
  • Studied the RNCryptor-objc github repo to develop an understanding of how this AES wrapper works.
  • Develop PoC in C# using the amazing LINQpad to decrypt media in PPV_Photos given the keychain.plist

Decryption PoC

This script is C# and was written in/for Linqpad, but could be adapted to a Visual Studio project very easily. It uses only native libraries. You will need to plugin your AES Key as base64 in the “USER CONFIGURATION REQUIRED” section 😀 ! I call this a PoC because it does zero error checking and may or may not work for you without tweaking.

I might throw together a GUI app to do this more easily if people would use it. DM me on Twitter or Discord and let me know if that sounds interesting/useful.

void Main()
{
	// USER CONFIGURATION REQUIRED --------------------------------->
	
		// The input directory should point to the PPV sandbox where all the encrypted media resides
		var pathToEncryptedFiles = @"c:\ppvtest\335CE0B0-..-B521433DD5D2\Library\PPV_Pics";
		
		// Where to spit out the decrypted media
		var decryptFilesTo = @"c:\ppvtest\out\";
	
		// from keychain.plist -- genp with key "ppv_dateHash"
		var aesKeyb64 = "mUAf0A6QF+DOoo...7tbZuqw2ImuRAkql0mY0zM=";

	// END USER CONFIGURATION REQUIRED !!!
	
	Directory.CreateDirectory(decryptFilesTo);

	
	// Convert to byte[] from base64 string
	var aesKey = Convert.FromBase64String(aesKeyb64);
	
	// Iterate encrypted files in the PPV_Pics folder.
	foreach (var item in Directory.GetFiles(pathToEncryptedFiles))
	{
		var inputData = File.ReadAllBytes(item);
		// The IV is located at offset 0x2 and is 16 bytes long.
		var iv = inputData.Skip(2).Take(16).ToArray();
		
		// Our header is 18 bytes (0x0 for version, 0x1 for options, and 0x2 for 16 bytes IV)
		var headerLength = 18;
		
		// The cipher text is the rest, minus 32 which is used for HMAC stuff.
		var cipherText = inputData.Skip(headerLength).Take(inputData.Length - headerLength - 32).ToArray();
		
		File.WriteAllBytes(decryptFilesTo + new FileInfo(item).Name, decryptAesCbcPkcs7(cipherText, aesKey, iv));
	}
}

// Borrowed from Rob Napier's RNCryptor-cs
// https://github.com/RNCryptor/RNCryptor-cs
private byte[] decryptAesCbcPkcs7(byte[] encrypted, byte[] key, byte[] iv)
{
	var aes = Aes.Create();
	aes.Mode = CipherMode.CBC;
	aes.Padding = PaddingMode.PKCS7;
	var decryptor = aes.CreateDecryptor(key, iv);


	byte[] plainBytes;
	using (MemoryStream msDecrypt = new MemoryStream())
	{
		using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Write))
		{
			csDecrypt.Write(encrypted, 0, encrypted.Length);
			csDecrypt.FlushFinalBlock();
			plainBytes = msDecrypt.ToArray();
		}
	}

	return plainBytes;
}

Acknowledgements

I’d like to thank the following people for their assistance on this research project:

  • Braden Thomas (@drspringfield) at Grayshift for his always spot-on advice and extensive depth of knowledge on all things iOS.
  • Ivan Rodriguez (@ivRodriguezCA) for his excellent blog and great advice.
  • @karate on DFIR Discord (Magnus RC3 Sweden) (@may_pol17) for his excellent guidance and urging to get Frida working.
  • Or Begam (@shloophen) from Cellebrite for reviewing my decryption PoC and spotting that final bug, connecting me with Ivan Rodriguez and generally being awesome.

1 thought on “Photo Vault app still pwnable in 2019? An adventure in iOS RE”

Leave a Reply

Your email address will not be published. Required fields are marked *