生成数据智能

严重的安全性:KeePass“主密码破解”,以及我们可以从中学到什么

日期:

在过去的两周里,我们看到了一系列文章谈论流行的开源密码管理器 KeePass 中被描述为“主密码破解”的方法。

该错误被认为非常重要,足以获得美国政府的官方标识符(它被称为 CVE-2023-32784,如果你想找到它),并且考虑到你的密码管理器的主密码几乎是你整个数字城堡的关键,你就可以理解为什么这个故事引起了很多兴奋。

好消息是,想要利用此错误的攻击者几乎肯定需要已经用恶意软件感染了您的计算机,因此无论如何都能够监视您的击键和正在运行的程序。

换句话说,在 KeePass 的创建者推出更新之前,该错误可以被认为是一个易于管理的风险,更新应该很快就会出现(显然是在 2023 年 XNUMX 月初)。

由于错误的披露者注意 指出:

如果您使用具有强密码的全盘加密并且您的系统[没有恶意软件],您应该没问题。 仅凭这一发现,没有人可以通过互联网远程窃取您的密码。

风险说明

概括地说,该错误归结为难以确保在您完成处理后从内存中清除所有机密数据的痕迹。

我们将在这里忽略如何完全避免在内存中存储秘密数据的问题,即使是短暂的。

在这篇文章中,我们只是想提醒各地的程序员,由具有安全意识的审阅者批准的代码带有诸如“似乎在其自身之后正确清理”之类的注释......

…事实上可能根本没有完全清理,并且潜在的数据泄漏可能不会从代码本身的直接研究中显而易见。

简而言之,CVE-2023-32784 漏洞意味着即使在 KeyPass 程序退出后,KeePass 主密码也可以从系统数据中恢复,因为有关您密码的足够信息(尽管实际上不是原始密码本身,我们将重点关注)稍后)可能会留在系统交换或睡眠文件中,分配的系统内存最终可能会被保存以备后用。

在系统关闭时不使用 BitLocker 加密硬盘的 Windows 计算机上,这会给偷走您笔记本电脑的骗子提供从 USB 或 CD 驱动器启动的机会,甚至恢复您的主密码尽管 KeyPass 程序本身不会将其永久保存到磁盘。

内存中的长期密码泄漏也意味着密码理论上可以从 KeyPass 程序的内存转储中恢复,即使该转储是在您输入密码后很长时间以及 KeePass 后很久才被抓取的它本身不再需要保留它。

显然,您应该假设系统中已有的恶意软件可以通过各种实时侦听技术恢复几乎所有键入的密码,只要它们在您键入时处于活动状态即可。 但是您可能有理由认为,您暴露在危险中的时间仅限于打字的短暂时间,而不是延长到打字后的几分钟、几小时或几天,或者可能更长,包括在您关闭计算机之后。

留下了什么?

因此,我们认为我们应该从高层次上看一下秘密数据是如何以代码中不直接可见的方式留在内存中的。

如果您不是程序员,请不要担心 - 我们会保持简单,并在进行时进行解释。

我们将从在一个简单的 C 程序中查看内存使用和清理开始,该程序通过执行以下操作模拟输入和临时存储密码:

  • 分配一块专用内存 专门用来存放密码的。
  • 插入已知文本字符串 所以如果需要的话我们可以很容易地在内存中找到它。
  • 追加 16 个伪随机 8 位 ASCII 字符 从范围AP。
  • 打印出来 模拟密码缓冲区。
  • 释放内存 希望删除密码缓冲区。
  • 退出中 该程序。

大大简化后,C 代码可能看起来像这样,没有错误检查,使用来自 C 运行时函数的劣质伪随机数 rand(),并忽略任何缓冲区溢出检查(永远不要在实际代码中执行任何操作!):

 // Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);

事实上,我们最终在测试中使用的代码包括如下所示的一些额外的点点滴滴,以便我们可以在使用时转储临时密码缓冲区的全部内容,以查找不需要的或遗留的内容。

请注意,我们故意在调用后转储缓冲区 free(),这在技术上是一个释放后使用的错误,但我们在这里作为一种偷偷摸摸的方式来查看在将我们的缓冲区交回后是否留下了任何重要的东西,这可能会导致现实生活中的危险数据泄漏漏洞。

我们还插入了两个 Waiting for [Enter] 提示进入代码,让我们有机会在程序的关键点创建内存转储,为我们提供原始数据以供稍后搜索,以便查看程序运行时留下的内容。

要进行内存转储,我们将使用 Microsoft 系统内部工具 procdump-ma 选项 (转储所有内存), 这避免了编写我们自己的代码来使用 Windows DbgHelp 系统及其相当复杂 MiniDumpXxxx() 功能.

为了编译 C 代码,我们使用了 Fabrice Bellard 的免费和开源的我们自己的小型简单构建 微型 C 编译器, 适用于 64 位 Windows 源代码和二进制形式 直接来自我们的 GitHub 页面。

文章中图片中所有源代码的可复制粘贴文本显示在页面底部。

这是我们编译并运行测试程序时发生的情况:

C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C 编译器 - 版权所有 (C) 2001-2023 Fabrice Bellard 由 Paul Ducklin 剥离用作学习工具版本 petcc64-0.9.27 [0006] - 生成 64 位仅限 PE -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- ---------------------- 虚拟文件大小部分 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ---------------------- <- unl1.exe(3584 字节)C:UsersduckKEYPASS> unl1.exe 在开始时转储“新”缓冲区 00F51390:90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P...... 00F513A0:73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 stem32cmd。 exe.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F 513F0:42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F4 3C AC 4B 00 00 净值 ExplzV.< .K.. 完整字符串为:unlikelytextJHKNEJJCPOMDJHAN 00F51390: 75 6E 6C 69 6B 65 6C 79 74 65 78 74 4A 48 4B 4E unlikelytextJHKN 00F513A0: 45 4A 4A 43 50 4F 4D 44 4A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.eD 00F513B0 : 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32D r 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F4 3C AC 4B 00 00 净值 ExplzV.<.K..等待 [ENTER] 释放缓冲区...释放后转储缓冲区()00F51390:A0 67 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .g ...... P...... . 00F513A0: 45 4A 4A 43 50 4F 4D 44 4A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.eD 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0:64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F.EFC_4372=1.FPS_00F513F0:42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400:49 4C 45 5F 53 54 52 49 4 E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 4D 00 00 4D AC 4B 00 00 净值 ExplM..MK. 等待 [ENTER] 退出 main()... C:UsersduckKEYPASS>

在这次运行中,我们没有抓取任何进程内存转储,因为我们可以从输出中立即看出这段代码泄漏了数据。

在调用 Windows C 运行时库函数之后 malloc(),我们可以看到我们返回的缓冲区包含程序启动代码遗留下来的环境变量数据,前 16 个字节显然被更改为某种遗留的内存分配标头。

(注意这 16 个字节看起来像两个 8 字节的内存地址, 0xF557900xF50150,分别在我们自己的内存缓冲区之后和之前。)

当密码应该在内存中时,我们可以在缓冲区中清楚地看到整个字符串,正如我们所期望的那样。

但是打完电话后 free(),请注意我们缓冲区的前 16 个字节是如何再次用看起来像附近的内存地址重写的,大概是这样内存分配器可以跟踪内存中可以重复使用的块......

…但是我们“删除”的密码文本的其余部分(最后 12 个随机字符 EJJCPOMDJHAN) 被抛在后面。

我们不仅需要在 C 中管理我们自己的内存分配和取消分配,如果我们想要精确地控制它们,我们还需要确保为数据缓冲区选择正确的系统函数。

例如,通过切换到这段代码,我们可以更好地控制内存中的内容:

通过切换 malloc()free() 使用较低级别的 Windows 分配函数 VirtualAlloc()VirtualFree() 直接,我们得到更好的控制。

然而,我们在速度上付出了代价,因为每次调用 VirtualAlloc() 做更多的工作 malloc(),它通过不断划分和细分预分配的低级内存块来工作。

运用 VirtualAlloc() 重复小块也会占用更多的内存,因为每个块都由 VirtualAlloc() 通常消耗 4KB 内存的倍数(或 2MB,如果您使用所谓的 大内存页), 所以我们上面的 128 字节缓冲区被四舍五入为 4096 字节,浪费了 3968KB 内存块末尾的 4 字节。

但是,如您所见,我们取回的内存会自动清空(设置为零),因此我们看不到之前的内容,这一次当我们尝试执行释放后使用时程序崩溃了把戏,因为 Windows 检测到我们正在尝试查看我们不再拥有的内存:

C:UsersduckKEYPASS> unl2 在开始时转储“新”缓冲区 0000000000EA0000:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ..................... 0000000000EA0010:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ..................... 0000000000EA0020:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .. ............ 0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............... 0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ..................... 0000000000EA0050:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............ 0000000000EA0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................. 0000000000EA0070:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ..................... 0000000000EA0080:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 完整字符串为:unlikelytextIBIPJPPHEOPOIDLL 0000EA75: 6 6E 69C 6 65B 6 79C 74 65 78 74 49 42 49 50 0000000000 unlikelytextIBIP 0010EA4: 50A 50 48 45 4 50F 4 49F 44 4 4C 00C 00 00 00 0000000000 0020 jppheopoidll .... 00AEA00:00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............ 00EA00: 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............... 00EA00:0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .................. . 00EA00: 0000000000 0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............... 00EA00: 0000000000 0080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ..................... 00EA00:0000000000 0000 XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX XNUMX .................. ...等待 [ENTER] 释放缓冲区...在 free() XNUMXEAXNUMX 之后转储缓冲区:[程序在这里终止,因为 Windows 捕获了我们的释放后使用]

因为我们释放的内存需要重新分配 VirtualAlloc() 在它可以再次使用之前,我们可以假设它在被回收之前将被清零。

但是,如果我们想确保它被清空,我们可以调用特殊的 Windows 函数 RtlSecureZeroMemory() 就在释放它之前,以保证 Windows 将首先将零写入我们的缓冲区。

相关功能 RtlZeroMemory(),如果你想知道,它做了类似的事情,但不能保证实际工作,因为如果编译器注意到此后不再使用缓冲区,则允许编译器将其删除为理论上多余的。

如您所见,如果我们想尽量减少存储在内存中的秘密可能会在以后出现的时间,我们需要非常小心地使用正确的 Windows 功能。

在本文中,我们不会研究如何通过将机密锁定到物理 RAM 来防止机密被意外保存到交换文件中。 (暗示: VirtualLock() 它本身实际上还不够。)如果您想了解有关低级别 Windows 内存安全性的更多信息,请在评论中告诉我们,我们将在以后的文章中查看。

使用自动内存管理

避免我们自己分配、管理和释放内存的一种巧妙方法是使用一种编程语言来处理 malloc()free()VirtualAlloc()VirtualFree(),自动。

脚本语言如 Perl的, 蟒蛇, LUA, JavaScript的 和其他人通过在后台跟踪内存使用情况,摆脱了困扰 C 和 C++ 代码的最常见的内存安全错误。

正如我们之前提到的,我们上面写得不好的示例 C 代码现在可以正常工作,但这只是因为它仍然是一个超级简单的程序,具有固定大小的数据结构,我们可以通过检查来验证我们不会覆盖我们的 128-字节缓冲区,并且只有一个执行路径以 malloc() 并以相应的结束 free().

但是,如果我们更新它以允许生成可变长度的密码,或者在生成过程中添加额外的功能,那么我们(或接下来维护代码的任何人)很容易就会出现缓冲区溢出、释放后使用错误或内存不足的情况。永远不会被释放,因此在不再需要它之后很长一段时间内都会留下秘密数据。

在像 Lua 这样的语言中,我们可以让 Lua 运行时环境执行行话中所说的操作 自动垃圾收集,处理从系统获取内存,并在它检测到我们已经停止使用它时返回它。

当我们为我们处理内存分配和取消分配时,我们上面列出的 C 程序变得非常简单:

我们分配内存来保存字符串 s 只需分配字符串 'unlikelytext' 到它。

稍后我们可以明确地向 Lua 暗示我们不再对 s 通过给它赋值 nil (所有 nils 本质上是相同的 Lua 对象),或者停止使用 s 并等待 Lua 检测到不再需要它。

无论哪种方式,所使用的内存 s 最终会自动恢复。

并在附加到文本字符串时防止缓冲区溢出或大小管理不善(Lua 运算符 ..,发音 连接, 本质上是将两个字符串加在一起,比如 + 在 Python 中),每次我们扩展或缩短一个字符串时,Lua 都会神奇地为一个全新的字符串分配空间,而不是修改或替换其现有内存位置中的原始字符串。

由于在文本操作期间分配的中间字符串,这种方法速度较慢,并且导致内存使用峰值高于您在 C 中获得的峰值,但它在缓冲区溢出方面更安全。

但是这种自动字符串管理(行话中称为 不变性,因为字符串永远不会 突变, 或在创建后就地修改)确实会带来新的网络安全问题。

我们在 Windows 上运行上面的 Lua 程序,直到第二次暂停,就在程序退出之前:

C:UsersduckKEYPASS> lua s1.lua 完整的字符串是:unlikelytextHLKONBOJILAGLNLN Waiting for [ENTER] before freeing string... Waiting for [ENTER] before exiting...

这一次,我们进行了进程内存转储,如下所示:

C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Sysinternals 进程转储实用程序 版权所有 (C) 2009-2022 Mark Russinovich 和 Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump 1启动:C:UsersduckKEYPASSlua-s1.dmp [00:00:00] 转储 1 写入:估计转储文件大小为 10 MB。 [00:00:00] 转储 1 完成:在 10 秒内写入 0.1 MB [00:00:01] 达到转储计数。

然后我们运行这个简单的脚本,它读回转储文件,在内存中的任何地方找到已知字符串 unlikelytext 出现,并打印出来,连同它在转储文件中的位置和紧随其后的 ASCII 字符:

即使您以前使用过脚本语言,或者在任何具有所谓 托管字符串,系统会为您跟踪内存分配和释放,并在它认为合适的时候处理它们……

...您可能会惊讶地看到此内存扫描产生的输出:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: unlikelytextALJBNGOAPLLBDEB 006D8B3C: unlikelytextALJBNGOA 006D8B7C: unlikelytextALJBNGO 006D8BFC: unlikelytextALJBNGOAPLLBDEBJ 006D8CBC: unlikelytextALJBN 006 D8D7C:不太可能的文本ALJBNGOAP 006D903C:不太可能的文本ALJBNGOAPL 006D90BC:不太可能的文本ALJBNGOAPLL 006D90FC:不太可能的文本ALJBNGOAPLLB 006D913BC:不太可能的文本ALJBNGOAPLLBD 006D91FC:不太可能的文本ALJBNGOAPLLBD 006 D91C :不太可能的文本ALJBNGOAPLLBDE 006DB923C:不太可能的文本ALJ 006DBB70C:不太可能的文本AL 006DBD8C:不太可能的文本A

你瞧,当时我们抓取了我们的内存转储,即使我们已经完成了字符串 s (并告诉 Lua 我们不再需要它了 s = nil),代码在此过程中创建的所有字符串仍然存在于 RAM 中,尚未恢复或删除。

事实上,如果我们按字符串本身对上述输出进行排序,而不是按照它们在 RAM 中出现的顺序排序,您将能够想象在我们一次将一个字符连接到我们的密码字符串的循环中发生了什么:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | 排序/+10 006DBD0C:不太可能的文本A 006DBB8C:不太可能的文本AL 006DB70C:不太可能的文本ALJ 006D91BC:不太可能的文本ALJB 006D8CBC:不太可能的文本ALJBN 006D90FC:不太可能的文本ALJBNG 006D8B7C:不太可能的文本ALJBNGO 006D8B3C:不太可能的文本ALJBNGOA 006D8D7C:不太可能文本ALJBNGOAP 006D903C:不太可能文本ALJBNGOAPL 006D90BC:不太可能文本ALJBNGOAPLL 006D913C:不太可能文本ALJBNGOAPLLB 006D91FC:不太可能文本ALJBNGOAPLLBD 006D923C:不太可能文本ALJBNGOAPLLBDE 006D8AFC:不太可能文本ALJBNGOAPLLBDEB 006D8BFC : 不太可能文本ALJBNGOAPLLBDEBJ

所有那些临时的、中间的字符串仍然存在,所以即使我们成功地清除了最终的值 s,我们仍然会泄露除最后一个字符之外的所有内容。

事实上,在这种情况下,即使我们故意通过调用特殊的 Lua 函数来强制我们的程序处理所有不需要的数据 collectgarbage() (大多数脚本语言都有类似的东西),那些讨厌的临时字符串中的大部分数据无论如何都停留在 RAM 中,因为我们编译 Lua 来使用旧的自动内存管理 malloc()free().

换句话说,即使在 Lua 本身回收了它的临时内存块以再次使用它们之后,我们也无法控制这些内存块将如何或何时被重新使用,因此它们将在进程中停留多长时间 -等待被嗅出、转储或以其他方式泄露的数据。

进入.NET

但是 KeePass 呢,这是本文的起点?

KeePass 是用 C# 编写的,并使用 .NET 运行时,因此它避免了 C 程序带来的内存管理不善的问题......

…但是 C# 管理它自己的文本字符串,而不是像 Lua 那样,这就提出了一个问题:

即使程序员在完成主密码后避免将整个主密码存储在一个地方,能够访问内存转储的攻击者是否仍然可以找到足够的剩余临时数据来猜测或恢复主密码,即使那些在您输入密码后的几分钟、几小时或几天内,攻击者可以访问您的计算机?

简而言之,是否有可检测到的主密码残留在 RAM 中,即使您希望它们已被删除?

恼人的是,作为 Github 用户 Vdohney 发现,答案(至少对于早于 2.54 的 KeePass 版本)是“是”。

需要明确的是,我们不认为您的实际主密码可以从 KeePass 内存转储中恢复为单个文本字符串,因为作者为主密码输入创建了一个特殊函数,该函数会特意避免存储完整的内容密码放在容易被发现和嗅出的地方。

我们通过将主密码设置为 SIXTEENPASSCHARS,输入它,然后立即、不久和很久之后进行内存转储。

我们使用一个简单的 Lua 脚本搜索转储,该脚本到处寻找密码文本,包括 8 位 ASCII 格式和 16 位 UTF-16(Windows widechar)格式,如下所示:

结果令人鼓舞:

C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp 正在读取转储文件...完成。 正在搜索 SIXTEENPASSCHARS 作为 8 位 ASCII...未找到。 正在搜索 UTF-16 格式的 SIXTEENPASSCHARS... 未找到。

但是 CVE-2023-32784 的发现者 Vdohney 注意到,当您输入主密码时,KeePass 通过构建和显示由 Unicode“blob”字符组成的占位符字符串来为您提供视觉反馈,最多并包括您的长度密码:

在 Windows 上的 widechar 文本字符串中(每个字符由两个字节组成,而不是像 ASCII 中那样每个字节只有一个字节),“blob”字符在 RAM 中被编码为十六进制字节 0xCF 其次是 0x25 (恰好是 ASCII 中的百分号)。

因此,即使 KeePass 在您输入密码本身时非常小心地处理您输入的原始字符,您最终可能会得到剩余的“blob”字符字符串,在重复运行时很容易在内存中检测到,例如 CF25CF25 or CF25CF25CF25...

...而且,如果是这样,您发现的最长的 blob 字符可能会泄露您的密码长度,如果没有别的,这将是一种适度的密码信息泄漏形式。

我们使用以下 Lua 脚本来查找遗留密码占位符字符串的迹象:

输出令人惊讶(我们删除了具有相同 blob 数量的连续行,或者比上一行具有更少的 blob,以节省空间):

C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B:** 00BE64C7:*** [。 . .] 00BE6E8F: **** [. . .] 00BE795F:***** [. . .] 00BE84F7:****** [. . .] 00BE8F37: ******* [ 类似地继续 8 个 blob、9 个 blob 等] [ 直到最后两行,每行恰好 16 个 blob] 00C0503B: ************* *** 00C05077:*************** 00C09337:* 00C09738:* [所有剩余的匹配项都是一个 blob 长] 0123B058:*

在靠近但不断增加的内存地址处,我们发现了一个系统列表,其中包含 3 个 blob,然后是 4 个 blob,依此类推,直到 16 个 blob(密码的长度),然后是许多随机分散的单 blob 字符串实例.

因此,那些占位符“blob”字符串似乎确实会泄漏到内存中并留下来泄漏密码长度,在 KeePass 软件完成您的主密码后很久。

下一步

我们决定进一步挖掘,就像 Vdohney 所做的那样。

我们更改了模式匹配代码,以检测 blob 字符链后跟任何单个 16 位格式的 ASCII 字符(ASCII 字符在 UTF-16 中表示为它们通常的 8 位 ASCII 代码,后跟一个零字节)。

这一次,为了节省空间,我们抑制了与前一个匹配项完全匹配的任何匹配项的输出:

惊喜,惊喜:

C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp
00BE581B: *I
00BE621B: **X
00BE6BD3: ***T
00BE769B: ****E
00BE822B: *****E
00BE8C6B: ******N
00BE974B: *******P
00BEA25B: ********A
00BEAD33: *********S
00BEB81B: **********S
00BEC383: ***********C
00BECEEB: ************H
00BEDA5B: *************A
00BEE623: **************R
00BEF1A3: ***************S
03E97CF2: *N
0AA6F0AF: *W
0D8AF7C8: *X
0F27BAF8: *S

看看我们从 .NET 的托管字符串内存区域中得到了什么!

一组紧密组合的临时“blob 字符串”,显示我们密码中的连续字符,从第二个字符开始。

那些泄漏的字符串之后是广泛分布的单字符匹配项,我们假设这些匹配项是偶然出现的。 (一个 KeePass 转储文件的大小约为 250MB,因此“blob”字符有足够的空间出现,就好像运气好一样。)

即使我们考虑了这四个额外的匹配项,而不是将它们作为可能的不匹配项而丢弃,我们也可以猜测主密码是以下之一:

?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS

显然,这种简单的技术并没有找到密码中的第一个字符,因为第一个“blob 字符串”仅在输入第一个字符后才构建

请注意,这个列表很好而且很短,因为我们过滤掉了不以 ASCII 字符结尾的匹配项。

如果您正在寻找不同范围内的字符,例如中文或韩文字符,您可能会意外命中更多,因为有更多可能的字符可以匹配……

......但我们怀疑你无论如何都会非常接近你的主密码,并且与密码相关的“blob 字符串”似乎在 RAM 中组合在一起,大概是因为它们大约在同一时间由同一部分分配.NET 运行时。

在那里,在一个公认的冗长和散漫的概括中,是一个引人入胜的故事 CVE-2023-32784.

怎么办呢?

  • 如果您是 KeePass 用户,请不要惊慌。 虽然这是一个错误,并且在技术上是一个可利用的漏洞,但想要使用此错误破解您的密码的远程攻击者需要先在您的计算机上植入恶意软件。 这会给他们许多其他方法来直接窃取您的密码,即使这个错误不存在,例如通过在您键入时记录您的击键。 此时,您只需注意即将到来的更新,并在准备就绪时抓住它。
  • 如果您不使用全盘加密,请考虑启用它。 要从您的交换文件或休眠文件(操作系统磁盘文件,用于在重负载或计算机“休眠”时临时保存内存内容)中提取遗留密码,攻击者需要直接访问您的硬盘。 如果您激活了 BitLocker 或其他操作系统的等效项,它们将无法访问您的交换文件、休眠文件或任何其他个人数据,例如文档、电子表格、保存的电子邮件等。
  • 如果您是一名程序员,请随时了解内存管理问题。 不要假设仅仅因为每个 free() 匹配其对应的 malloc() 您的数据安全且管理良好。 有时,您可能需要采取额外的预防措施以避免遗留秘密数据,并且这些预防措施因操作系统而异。
  • 如果您是 QA 测试员或代码审查员,请始终“在幕后”思考。 即使内存管理代码看起来整洁且平衡,也要注意幕后发生的事情(因为最初的程序员可能不知道这样做),并准备好进行一些渗透测试式的工作,例如运行时监控和内存转储以验证安全代码确实按预期运行。

文章代码:UNL1.C

#包括#包括#包括void hexdump(unsigned char* buff, int len) { // 以 16 字节块打印缓冲区 for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +我); // 将 16 个字节显示为十六进制值 for (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // 重复这 16 个字节作为字符 for (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // 获取内存来存储密码,并在缓冲区正式“新建”时显示 // 缓冲区中的内容... char* buff = malloc(128); printf("在 startn 转储'新'缓冲区"); 十六进制转储(浅黄色,128); // 使用伪随机缓冲区地址作为随机种子 srand((unsigned)buff); // 以一些固定的、可搜索的文本开始密码 strcpy(buff,"unlikelytext"); // 附加 16 个伪随机字母,一次一个 for (int i = 1; i <= 16; i++) { // 从 A (65+0) 到 P (65+15) 中选择一个字母 char ch = 65 + (随机数() & 15); // 然后就地修改buff字符串 strncat(buff,&ch,1); } // 完整的密码现在在内存中,所以打印 // 它作为一个字符串,并显示整个缓冲区... printf("Full string was: %sn",buff); 十六进制转储(浅黄色,128); // 现在暂停转储进程 RAM (try: 'procdump -ma') puts("Waiting for [ENTER] to free buffer..."); 获取字符(); // 正式地 free() 内存并再次显示缓冲区 // 以查看是否遗留了任何东西... free(buff); printf("在 free()n 之后转储缓冲区"); 十六进制转储(浅黄色,128); // 暂停再次转储 RAM 以检查差异 puts("Waiting for [ENTER] to exit main()..."); 获取字符(); 返回 0; }

文章代码:UNL2.C

#包括#包括#包括#包括void hexdump(unsigned char* buff, int len) { // 以 16 字节块打印缓冲区 for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +我); // 将 16 个字节显示为十六进制值 for (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // 重复这 16 个字节作为字符 for (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // 获取内存来存储密码,并在正式“新”时显示 // 缓冲区中的内容... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("在 startn 转储'新'缓冲区"); 十六进制转储(浅黄色,128); // 使用伪随机缓冲区地址作为随机种子 srand((unsigned)buff); // 以一些固定的、可搜索的文本开始密码 strcpy(buff,"unlikelytext"); // 附加 16 个伪随机字母,一次一个 for (int i = 1; i <= 16; i++) { // 从 A (65+0) 到 P (65+15) 中选择一个字母 char ch = 65 + (随机数() & 15); // 然后就地修改buff字符串 strncat(buff,&ch,1); } // 完整的密码现在在内存中,所以打印 // 它作为一个字符串,并显示整个缓冲区... printf("Full string was: %sn",buff); 十六进制转储(浅黄色,128); // 现在暂停转储进程 RAM (try: 'procdump -ma') puts("Waiting for [ENTER] to free buffer..."); 获取字符(); // 正式 free() 内存并再次显示缓冲区 // 以查看是否遗留了任何东西... VirtualFree(buff,0,MEM_RELEASE); printf("在 free()n 之后转储缓冲区"); 十六进制转储(浅黄色,128); // 暂停再次转储 RAM 以检查差异 puts("Waiting for [ENTER] to exit main()..."); 获取字符(); 返回 0; }

文章代码:S1.LUA

-- 从一些固定的、可搜索的文本开始 s = 'unlikelytext' -- 从 'A' 到 'P' 添加 16 个随机字符 for i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Full string is:',s,'n') -- 暂停以转储进程 RAM print('Waiting for [ENTER] before freeing string...') io.read() - - 擦除字符串并标记变量未使用 s = nil -- 再次转储 RAM 以查找差异 print('Waiting for [ENTER] before exiting...') io.read()

文章代码:FINDIT.LUA

-- 读取转储文件 local f = io.open(arg[1],'rb'):read('*a') -- 查找后跟一个或多个随机 ASCII 字符的标记文本 local b,e ,m = 0,0,nil while true do -- 查找下一个匹配项并记住偏移量 b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- 当不再匹配时退出matches if not b then break end -- 报告找到的位置和字符串 print(string.format('%08X: %s',b,m)) end

文章代码:SEARCHKNOWN.LUA

io.write('读取转储文件...') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Searching for SIXTEENPASSCHARS as 8-bit ASCII...') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 and 'FOUND' or 'not found','.n') io.write ('正在以 UTF-16 格式搜索 SIXTEENPASSCHARS...') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 and 'FOUND' or 'not found',' .n')

文章中的代码:FINDBLOBS.LUA

-- 读入命令行指定的转储文件 local f = io.open(arg[1],'rb'):read('*a') -- 查找一个或多个密码 blob,后跟任何非 blob -- 请注意,blob 字符 (●) 编码为 Windows widechars -- 作为 litte-endian UTF-16 代码,以十六进制形式显示为 CF 25。 local b,e,m = 0,0,nil while true do -- 我们想要一个或多个 blob,后跟任何非 blob。 -- 我们通过查找显式 CF25 来简化代码 -- 后跟任何仅包含 CF 或 25 的字符串, -- 因此我们将找到 CF25CFCF 或 CF2525CF 以及 CF25CF25。 -- 如果有的话,我们稍后会过滤掉“误报”。 -- 我们需要写 '%%' 而不是 x25 因为 x25 -- 字符(百分号)是 Lua 中的特殊搜索字符! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- 没有更多匹配时退出,如果不是 b 则 break end -- CMD.EXE 无法打印斑点,所以我们将它们转换为星星。 print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) 结束

文章代码:SEARCHKP.LUA

-- 读入命令行指定的转储文件 local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- 现在,我们需要一个或多个 blob (CF25) 后跟代码 -- 对于 A..Z 后跟一个 0 字节以将 ACSCII 转换为 UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- 当没有更多匹配时退出 if not b 然后 break end -- CMD.EXE 无法打印 blob,所以我们将它们转换为星星。 -- 为了节省空间,如果 m ~= p 那么 print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m结束结束

现货图片

最新情报

现货图片

在线答疑

你好呀! 我怎么帮你?