不雅暴露:你的秘密正在泄露
硬编码的加密密钥?在我商业购买的闭源软件中?这比你想象的要常见得多。就像,真的非常常见。
这篇博客文章详细讲述了一个真实的加密密钥发现、DLL修改、密码恢复和软件平台安全风险的故事。请注意,所有对具体目标软件的引用都已擦除,所有加密敏感材料都已为讲述这个故事而进行了模拟,希望能避免惹恼太多人。
现在,事不宜迟,开始我们的故事…
那不是哈希…
几年前,我正在查看Teams通知,看到以下消息发送给所有测试者:
“哈希挑战 - 第一步,可能是识别哈希类型。使用了哈希生成器,似乎没有结果匹配。用户名是dbadmin,哈希是I3bnJsdcK4qwstvVaekB5CzcT7ESjmR/xpB8IKNtMFc=。任何建议都将非常棒” - Jordan Drysdale, 2022-07-25
继续我作为BHIS最“容易被技术问题吸引”的小怪物的趋势,我决定至少给那个“哈希”值一个快速的嗅探测试,看看是否能给我的好朋友Jordan Drysdale一些有用的东西。乍一看,很明显哈希值至少是base64编码的,所以我使用Python对值进行base64解码,然后将其转换为十六进制字符串,最后使用hashid Linux实用程序来确定值可能是什么哈希类型(如果有的话)。从hashid命令输出中,看起来哈希可能是类似SHA-256的东西。
可能哈希值的转换
我把它扔回给Jordan,以防它有用,但这种情况的某些方面让我怀疑:这真的是哈希吗?Jordan在哪里找到这个值的?此时,我完全投入了,并与他通了个电话,他给我看了一个类似于下面截图的配置文件。
CryptKeeper配置文件
正如这篇博客文章引言中所暗示的,这里的“CryptKeeper”是一个名为“[已编辑]”的软件的替身,该软件被[已编辑]组织用来帮助他们管理他们的[已编辑]。你或许能想象我多么希望前面的句子没有被编辑,但唉,这不是我们发现自己所处的情况。只要知道,尽管这个例子可能看起来是人为设计的,但它非常接近我们在那次命运攸关的 engagement 中在野外发现的情况。
经过讨论,Jordan和我最终得出了一个简单的结论:那个密码值没有被哈希,而是被加密了。这开始变得有趣了。
偷看一眼
此时,Jordan打开了一个名为dnSpy的工具,并打开了我们找到的配置文件附近的一个DLL文件。实际上,这导致加载了一堆程序集,既有来自第三方软件的,也有来自.NET框架本身的。经过一些粗略的浏览,Jordan最终搜索了单词“crypt”。这返回了几个结果,其中许多看起来特别诱人。
CryptKeeper搜索结果
正如我们所看到的,似乎有加密和解密方法,以及三个获取似乎是AES加密的密钥、盐和IV值的方法。考虑到这一点,我们打开了来自CryptKeeper.Security命名空间的RadicalRijndael类。
和兄弟们一起破解一个冷的
好吧,现在我们这里有什么?两个用于加密和解密的公共方法,它们自己在调用额外的辅助函数之前检索似乎是静态密钥、盐和IV值。我们滚动到类的底部,观察负责获取加密材料的函数,结果发现是简单地返回字节数组作为字符串的方法。
密钥检索方法
Jordan把这三个值发给我,我把它们扔进Python shell,看看我们在处理什么。经过一些轻微的处理,我们发现所有三个值都是人类可读的ASCII字符串。
解码的加密秘密材料
直接从源头来
随着我们对受影响软件的理解,以及必要的加密材料被烘焙到编译的DLL中的知识,我们现在需要解密我们加密的密码值。在经过几次使用各种不同工具的天真解密尝试失败后,我们有了一个如何进行的想法。通过将DLL加载到内存中,你可以然后使用一种称为“反射程序集”的方法访问该DLL中的资源。
反射程序集入门
当一个类是公共的,并且理想情况下当一个方法是静态的(允许直接执行而不需要关联类的实例)时,你可以运行类似以下PowerShell命令来加载和直接运行.NET方法。
|
|
或者,在更新版本的PowerShell中,你可以使用新的using命令。
|
|
这在具有限制性应用程序控制机制但不限制PowerShell访问的环境中加载和运行程序集非常有效,但它要求目标类是公共的,目标方法既是公共的又是静态的。从CryptKeeper中的加密例程中,我们有一个非公共的内部类和公共但非静态的方法。
如果一个类是公共的但一个方法不是静态的,那根本不是大问题。以CryptKeeper DLL中的简单公共Greeter类为例:
公共类,公共非静态方法
所以只是为了说明这一点,以下截图显示我们对Greeter类型有可见性,但对名为Hello的方法没有可见性。
对类可见,对方法不可见
因为Hello方法不是静态的,但是公共的,我们可以通过Greeter类的实例访问该方法。为此,我们可以运行[Greeter]::New()或New-Object Greeter。
创建Greeter类的实例
如果我们然后将这两个命令中的任何一个的结果保存到PowerShell变量中,我们可以然后通过创建的对象调用Hello方法。
成功的非静态方法调用
好吧,很酷。这是问题的一半解决了。然而,当我们尝试访问内部RadicalRijndael类时,我们被阻止了。
对内部类不可见
现在,我们只需要弄清楚如何获得对RadicalRijndael类的访问权限。
拿走并烘焙
我想到的第一种获得对我们目标类访问权限的方法是将DLL外传到一台安装了dnSpy和.NET构建工具的机器上。从那里,可以在dnSpy界面中右键单击类名并将其修改为我们喜欢的样子。
C#类修改菜单条目
在这种情况下,我们需要做的就是将类从“internal”改为“public”,然后单击“Compile”按钮。
类修改和重新编译
随着类重新编译,我们可以然后将更改保存到新模块。在这种情况下,为了保留原始文件,我认为最好创建一个新的DLL文件。
将模块另存为新DLL
最后,我们可以用PowerShell反射程序集加载修改后的DLL,然后访问新公开的类来解密我们的密码,如下所示。
使用修改后的DLL成功解密密码
非菜单项
现在,虽然我喜欢外传、修改和反射程序集技术,但自我们最初发现以来的几年里,有些事情一直困扰着我:我们是否可能绕过.NET可见性检查并直接访问私有或内部代码?如果我们能够做到这一点,我们可以在客户端环境中利用加密代码而无需任何外传,并附加好处是降低在“传统”方式中完成此操作所需的知识门槛和软件设置。
带着这个困扰的问题,我开始了我的搜索。回顾我们已经知道的内容,以下截图显示了一个传统的反射程序集DLL加载,随后尝试访问我们的公共Greeter和内部RadicalRijndael类,分别成功和失败。
类型可见性
在研究时,我找到了对Import-Module和Add-Type PowerShell cmdlet的引用,但它们似乎都没有提供对内部或私有类型的可见性,所以我查看了加载程序集的字段,最终找到了一个名为GetType的方法,有几个重载。
程序集GetType函数重载
通读Microsoft GetType文档,我观察了下面显示的重载的文档。
Microsoft GetType文档摘录
也许我们可以使用GetType(String)来使用绝对名称获取我们DLL中类的句柄…
|
|
是的,绝对可以!
使用GetType对两个类可见
然而,在尝试创建对象实例时,我们仍然遇到问题。如下所示,我们可以使用New-Object cmdlet创建Greeter的实例,但不能创建RadicalRijndael的实例。
仍然找不到RadicalRijndael类型
所以,让我们深入研究加载到$radical变量中的类型,寻找任何带有“constructor”一词的东西。最终,我们找到了GetConstructors()方法,文档在这里。
GetConstructors方法文档摘录
请注意,还有一个GetConstructor方法来搜索和获取特定的构造函数,但我无法很好地掌握它。相反,让我们看看RadicalRijndael类的构造函数。由于我们在dnSpy中没有看到任何显式构造函数,我们应该期望只看到默认构造函数,而确实如此。
RadicalRijndael默认构造函数
由于GetConstructors方法返回一个列表,我们可以通过将结果的第零个元素保存到PowerShell变量中来获取默认构造函数。从那里,深入研究构造函数对象,我们遇到了Invoke方法,它有几个重载。
将默认构造函数分配给变量,Invoke方法重载摘要
我们只有一个构造函数,并且我们知道它不接受任何参数,所以我们这里应该不需要做任何太花哨的事情。我花了一些时间查找Invoke方法的文档,但没有找到太多。最终,心血来潮,我尝试用$null的参数值运行Invoke方法,然后你瞧…
成功的构造函数调用
哇。看那个,我们我们的对象就在那里!将结果捕获到变量中,我们可以使用Get-Member cmdlet并过滤成员类型为“Method”,我们可以看到我们的公共Encrypt和Decrypt方法!
公共方法可见性
这意味着我们可以在不首先修改DLL的情况下解密我们的密码。
使用未修改的DLL成功解密密码
好的,所以从头开始,我们最终得到以下PowerShell命令序列。
|
|
我们得到解密后的密码作为结果。
全部整合
进一步工作和结论
展望这种方法可能带我们去的地方,我设想(并且诚实地希望为这篇博客文章工作)一个像Snaffler这样的工具的扩展,以促进发现可能值得注意的加密库等的引用。Snaffler存储库有一个名为“UltraSnaffler”的替代工具,但我在时间用完之前无法编译它,也无法想出一个好的方法来查看DLL文件,而不会在时间和资源上令人望而却步。
你可能认为这种事情是一个已解决的问题:不要像这样在代码中存储秘密。而且确实是。但从“世界有一个答案”到“每个开发团队都正确实施它”还有很长的路要走。谁知道你可能会在源代码或编译的DLL中发现什么,由于不良实践、宽松的遗留代码卫生,甚至是善意的向后兼容性努力而被保留下来?绝对值得一看。我们当然会的。