免责声明
⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权!
硬编码密钥(在SQLite中)和加密算法(在AesFormula.js文件中)信息泄露导致真实凭据被泄露
一、案例研究
本节案例研究将讨论我们的一项测试结果,涉及基于Electron的应用程序中的客户端-服务器模型。
二、情况描述
在首次运行相关桌面应用程序时,我们注意到该应用程序具有记住输入凭据的功能。这似乎是合理的,因为应用程序提供的默认凭据相当复杂且难以记住。
(为了维护程序所有者的隐私,在本案例研究中我们不会使用真实的图像和代码。然而,为了便于讨论,我们将尝试绘制示意图并重写代码,以大致代表我们提供的解释内容)。
然而,在原生应用程序的概念中,有一个我们需要理解的原则。当我们使用应用程序提供的“记住我”功能时,本质上该应用程序会将我们的凭据存储在当前操作系统的本地环境中。尽管存储方法和形式有所不同,但为了清晰地理解流程,这一原则必须被牢牢遵守。
那么,这些数据存储在哪里呢?从技术角度来看,第三方应用程序数据的存储位置因操作系统而异。
【例如】,macOS上的应用程序数据通常存储在用户“Library”目录中的“Application Support”文件夹中。而在Windows上,第三方应用程序数据通常存储在用户“AppData”目录中的一个隐藏文件夹中。在Linux系统中,应用程序数据通常存储在用户目录中的“.config”文件夹中。
由于此次测试是在适用于macOS的应用程序上进行的,因此接下来的说明将重点关注macOS的相关细节。
三、 信息收集
注意:为了分析该应用程序的行为,我们需要尝试先登录,并勾选“记住我的密钥”选项。
3.1 检查 Application Support 和 Preferences 目录
在macOS环境中的初步实验中,我们检查了应用程序的目录,这些目录通常位于“Application Support”和“Preferences”中。
需要说明的是,“Application Support”目录主要用于macOS应用程序存储运行应用程序所需的数据,或者至少是与用户相关的应用程序数据。这些数据可能包括配置文件、本地数据库、缓存,或其他用于确保应用程序平稳运行的文件。例如,一个数据处理应用程序可能会在此存储文档模板,而一个游戏应用程序可能会在此存储用户的游戏进度数据。
另一方面,“Preferences”目录主要用于存储用户自定义的应用程序配置文件。它包括用户偏好设置,例如显示设置、语言偏好、键盘设置等。在这种情况下,用户设置通常“存储”在.plist文件中。
那么我们该如何操作呢❓ 在这种情况下,我们使用了一种相当传统的方法,即利用 grep 命令并结合针对我们用于登录应用程序的用户名的特定搜索。
以下是我们使用的命令:
grep -r username_of_the_apphere /Users/username/Library/Application\ Support/target_app/
以下是对该命令的简单解释:
-
grep
:这是一个用于匹配给定输入中指定字符串模式的命令。 -
-r
:这是一个选项,用于递归地搜索目录及其子目录中的内容。 -
username_of_the_apphere
:这是我们要搜索的字符串,即我们在应用程序中使用的用户名。 -
/Users/username/Library/Application Support/target_app/
:这是我们想要执行搜索的目录。
在这种情况下,grep 将在目录 /Users/username/Library/Application\ Support/target_app/ 及其子目录中的所有文件内搜索该字符串。
我们同样对首选项目录下的 “target_app” 文件进行了相同的操作。
grep -r username_of_the_apphere /Users/username/Library/Preferences/com.electron.target-app.plist
那么结果如何❓ 简而言之,通过这次分析,我们没有发现任何与用户名相关的数据。随后,我们尝试转向另一个位置进行检查。
可能有人会问,为什么我们如此确信该用户名是以明文(未保护的)形式存储的?实际上,我们并不确定该用户名的存储状态,但尝试搜索并无妨。
3.2 检查隐藏目录(点目录)的可能性
我们需要注意的一点是,在 macOS(以及通用的 Unix 系统)中,有一些目录对用户是隐藏的。这些目录通常以一个点(.)开头,位于 /Users/username 路径下。
在实际应用中,第三方应用程序有时会利用这些类型的目录来存储与应用程序相关的信息,而这些信息从技术角度讲并不需要用户访问。
注意
:有时也会有应用程序将其数据存放在 /Users/Username/.local/share 目录中。
基于这种情况,我们决定检查 /Users/Username 路径,看看是否存在这类目录。长话短说,我们找到了一个肯定的结果,也就是目标应用(target_app)确实拥有这样一个目录。
当我们首次发现.data.db
文件的存在时,我们就已经相当确信该文件可能包含我们正在寻找的凭据。然而,为了确认这一点,我们按照之前提到的方法使用grep进行了传统搜索。果然,结果如我们所预料,这个用户名确实存在于相关的.db文件中。
3.3 使用 DB Browser for SQLite 打开 .db 文件
通常,一个应用程序(无论是移动端还是桌面端)都会使用轻量级的数据库管理系统(DBMS)在本地计算机上进行存储。
一种常见的选择是 SQLite
。
在确认该文件确实是 SQLite 格式后,我们使用了一个名为 DB Browser for SQLite 的简单工具将其打开(尽管也可以使用其他工具)。
长话短说,我们找到了我们想要的内容,也就是存储用户名、URL 和密钥的列。
然而,在进一步检查我们使用的值后,发现密钥的值与预期的值不符。从它的格式来看,我们认为这是一种AES加密格式,这是常用的数据安全算法。
Value: eLy2xThk+y7Rki4J13zvGVxrnqZbrOtznvNbhOKIgWo=
再次需要指出的是,为了简化讨论,同时隐藏任何加密值和代码,我们已经用其他样本替换了它们(是的,这是从ChatGPT重制的)。
3.4 寻找加密算法
根据我们描述的情况,我们继续测试,探索用于“保护”密钥值的潜在算法和公式。简而言之,考虑到target_app应用程序是基于Electron构建的,我们尝试提取包内容中的app.asar文件。
请注意:
要了解如何检测基于Electron的应用程序以及提取过程,可以参考前几篇文章。
成功提取后,我们可以继续搜索应用程序使用的算法和公式,以保护前面提到的密钥值。
3.5 找到加密算法
在仔细检查提取目录中的每个文件后,我们发现了一个引起我们注意的文件,即AesFormula.js
:
3.6 理解加密流程
在检查AesFormula.js文件中的代码流程时,我们发现了一个负责动态生成加密密钥和初始化向量(IV)的代码段。该代码段包含两个函数:generateEncryptionKey和generateIV。
在generateEncryptionKey函数中,使用crypto.randomBytes方法生成一个随机的密钥组件,长度由KEY_LENGTH指定。随后,使用generatePBKDF2Salt函数生成一个盐值(SALT)。
这些随机密钥组件和默认密钥通过按位异或(XOR)操作结合,生成一个唯一的密钥。这个生成的密钥与生成的SALT一起,通过PBKDF2算法进行密钥派生,最终得到一个派生密钥(pbkdfKey)。最后,从这个派生密钥中选取前部分(等于KEY_LENGTH字节)作为AES加密的加密密钥。
类似地,在generateIV函数中,使用crypto.randomBytes方法生成一个随机的初始化向量(IV),其长度由IV_LENGTH指定。这个IV将在AES加密过程中作为唯一的初始化向量使用。
这些动态生成过程确保每次加密操作使用不同的密钥和初始化向量(IV),从而增强了加密机制的安全性。
从技术上讲,AesFormula通过为每次加密操作动态生成加密密钥和IV,提供了安全的加密功能。
为了验证我们的假设,我们尝试多次登录和登出,并观察SQLite中密钥的变化。
那么,结果如何?结果是积极的。在三次有效的登录尝试
中,我们发现SQLite中的密钥值确实发生了变化。
以下是相同值的三次不同加密结果:
接下来是什么呢?当然是要找到由这段代码生成的salt、key和IV的值。至于过程,我们继续研究现有代码的流程,直到最终遇到了以下这段代码:
这段代码显示,所有密钥生成过程的结果将存储在一个名为“encryptionMaterial”的部分中。
3.7 找到“Key”存储位置
在这种情况下,我们需要查找存储在encryptionMaterial中的值。长话短说,在尝试分析SQLite数据库的内容后,我们最终发现encryptionMaterial的值硬编码存储在同一张表中,即t_user表的some_secret_name列中。
四、解密密钥值
我们已经得到了密钥的加密值,并且我们也有用来加密的密钥值(iv、keyComponentBuf和salt),接下来我们将尝试解密现有的密钥值。
要执行此解密操作,最简单的方法之一是利用程序本身(在本例中,是AesFormula.js文件中可用的函数)。
以下是一个简单的代码,可以用来解密密钥值:
const AesFormula = require('./AesFormula');const encryptedValue = 'VQwSxZiICNhGjzLpcLSz1CDP11bEib26LFw+4av6VmE=';
const encryptionMaterial = {iv: Buffer.from([0xb0, 0xe3, 0x88, 0x26, 0x7c, 0xe3, 0xc3, 0x91, 0x5d, 0x95, 0xec, 0x72, 0xa7, 0x67, 0xd5, 0x20]),keyComponentBuf: Buffer.from([0xb8, 0x02, 0x63, 0x02, 0xe6, 0xcf, 0x1e, 0x61, 0xa9, 0x56, 0x1f, 0x49, 0xb1, 0x74, 0x77, 0x23, 0xb0, 0x59, 0xca, 0xa3, 0xa2, 0x8b, 0x34, 0x71, 0x23, 0x6c, 0xa5, 0x4d, 0x34, 0x85, 0x78, 0xeb]),pbkdf2SaltBuf: Buffer.from([0x4e, 0xc4, 0x6f, 0x21, 0x4b, 0x55, 0xc7, 0x58, 0x38, 0x9d, 0x6e, 0x31, 0xc0, 0x37, 0xaa, 0x68])
};const decryptValue = async () => {try {const decryptedResult = await AesFormula.decryptWithAES(encryptedValue, encryptionMaterial);console.log('Decrypted value:', decryptedResult);} catch (error) {console.error('Error while decrypting:', error);}
};decryptValue();
我们需要做的就是将来自encryptionMaterial的值(iv、keyComponentBuf和salt)输入到其中。将这段代码保存(例如命名为:decrypt.js)。然后,将AesFormula.js文件放在与此代码相同的目录中。
要执行这段代码,我们需要借助node.js。
以下是运行程序后的最终结果:
总之,PoC(概念验证)过程包括:
1、从data.db文件中的t_user表的key列获取key值。
2、从data.db文件中的t_user表的some_secret_name列获取encryptionMaterial值。
3、将key值复制到我们之前创建的decrypt.js代码中。
4、将encryptionMaterial值(逐个为iv、keyComponentBuf和salt)复制到我们创建的decrypt.js代码中。
5、将AesFormula.js文件放置在与decrypt.js文件相同的目录中。
6、使用node.js运行decrypt.js代码,命令为node decrypt.js。
五、其他信息
5.1 观点
从这个案例中,我们也可以认识到,应用程序实施的加密相当稳健,例如使用动态密钥,这无疑使得攻击者猜测变得困难。然而,关键问题最终集中在密钥公式和密钥本身的存储方式。
这个问题的执行可能引发一个问题:“获得本地访问权限的攻击者是否能够直接通过启用‘记住我的密钥’功能的target_app登录?”
本质上,这是一个有效的问题。然而,我们需要更深入地思考,因为攻击者在本地目标上的存在通常是有时间限制的。攻击者要想探索target_app中的每一条数据,无疑会受到限制,并且对于攻击者来说并不理想。
因此,通过利用本报告中描述的问题,攻击者将不再受时间限制,从用户的target_app中窃取数据。因为攻击者可以快速执行窃取操作,通过访问存储在SQLite数据库中的硬编码用户名、URL、key和encryptionMaterial。随后,攻击者可以从本地目标之外的各种位置进行探索。
尽管关于某些程序中这个问题的有效性的争论仍然存在,但在红队演习中无疑是有益的。
5.2 SafeStorage模块
需要注意的是,Electron框架提供了一个名为SafeStorage[1]的模块,用于保护存储在磁盘上的数据,以免被其他应用程序或具有完全磁盘访问权限的用户访问。从技术上讲,Electron中的safeStorage利用操作系统本地的安全存储API,以加密形式存储数据(因此,这可能是一个不错的替代方案)。
References
[1] SafeStorage: https://www.electronjs.org/docs/latest/api/safe-storage
原创 白帽子左一