当我检查软件安全性时,要检查的要点之一是使用动态库。 诸如劫持DLL(“ dll欺骗”或“ dll拦截”)之类的攻击非常罕见。 这很可能是因为Windows开发人员正在添加安全机制来防止攻击,而软件开发人员在安全性方面更加谨慎。 但是更有趣的是目标软件容易受到攻击的情况。
简要描述攻击,劫持DLL造成了一种情况,其中某些可执行文件试图加载dll,但是攻击者干预了此过程,而不是预期的库,而是使用从攻击者获得的有效负载加载了特别准备的dll。 结果,来自dll的代码将以已启动应用程序的权限执行,因此通常会选择具有较高权限的应用程序作为目标。
为了正确加载该库,必须满足许多条件:可执行文件的位大小和该库必须匹配,并且如果在应用程序启动时加载了该库,则dll必须导出该应用程序期望导入的所有功能。 通常,一次导入是不够的-非常希望应用程序在加载dll之后继续其工作。 为此,准备好的库的功能必须与原始库的功能相同。 最简单的方法是将函数调用从一个库传递到另一个库。 这些是称为代理dll的dll。

削减部分将包括创建代码库和实用程序两种形式的创建此类库的选项。
小理论回顾
库通常是使用LoadLibrary函数加载的,库名会传递到该函数中。 如果通过名称而不是名称传递完整路径,则应用程序将尝试加载指定的库。 例如,调用LoadLibrary(“ C:\ Windows \ system32 \ version.dll”)将加载指定的dll。 或者,如果该库不存在,则将不会加载该库。
有点乏味如果某些dll已经加载到应用程序中,则不会再次加载。 鉴于几乎在所有exe文件的开头都加载了version.dll,实际上,上述调用实际上不会加载任何东西。 但是我们仍然考虑一般情况,将示例视为对某些抽象库的调用。
如果编写LoadLibrary(“ version.dll”),则完全是另一回事。 在正常情况下,结果将与之前的情况完全相同-C:\ Windows \ system32 \ version.dll将被加载,但并非如此简单。
首先,将搜索一个库,其
顺序如下:
- 可执行文件夹
- 文件夹C:\ Windows \ System32
- 文件夹C:\ Windows \ System
- 文件夹C:\ Windows
- 文件夹设置为当前应用程序
- PATH环境变量中的文件夹
一些乏味在64位系统上启动32位应用程序时,所有对C:\ Windows \ system32的调用都将转发到C:\ Windows \ SysWOW64。 这只是出于描述的准确性,从攻击者的角度来看,区别并不是特别重要。
运行exe文件时,操作系统将从文件导入部分加载所有库。 从一般意义上讲,我们可以假设OS强制文件调用LoadLibrary,并传递在import节中编写的所有库名。 由于在99.9%的情况下是名称而不是路径,因此当应用程序启动时,将在系统中搜索所有已加载的库。
从dll搜索位置的列表中,有两点对我们非常重要-1和6。如果将version.dll放在启动文件的同一文件夹中,则将加载已加载的文件而不是系统文件。 这种情况几乎从未遇到过,因为如果有机会放置库,那么很可能可以替换可执行文件本身。 但是,这种情况还是可能的。 例如,如果可执行文件位于可写文件夹中并且是具有自动启动功能的服务,则在服务本身运行时无法对其进行更改。 或者在启动之前通过校验和从外部检查启动的文件,然后仍然不能选择替换文件。 但是将库放在旁边将是非常真实的。
您可能无法在可执行文件旁边创建文件,但是可以创建文件夹。 在这种情况下,WinSxS重定向机制(也称为“ DotLocal”)可能会起作用。
简要介绍DotLocal文件的清单可能包含对特定版本库的依赖。 在这种情况下,启动可执行文件(例如,将其命名为application.exe)时,操作系统将检查与文件本身相同的文件夹中是否存在名为application.exe.local的文件夹。 此文件夹应具有复杂名称的子文件夹,如amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b,其中已经有comctl32.dll库。 库名称和文件夹名称信息应在清单中指出,这只是遇到的第一个过程的示例。 如果没有文件夹或文件,则该库将从C:\ Windows \ WinSxS中获取。 在示例中,C:\ Windows \ WinSxS \ amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.9600.19291_none_6248a9f3ecb5e89b \ comctl32.dll。
但这更多的是例外,而不是规则。 但是,当dll搜索达到列表中第6个数字时,情况确实很真实。 如果应用程序尝试加载不在系统上或文件旁边的dll,则所有搜索将最多上升6点,这可能是可写文件夹。
例如,典型的Python安装最常出现在C:\ Python(或close)文件夹中。 python安装程序本身建议将其文件夹添加到PATH系统变量。 因此,我们为发起攻击提供了一个很好的跳板-该文件夹可被所有用户写入,并且任何尝试加载不存在的库的尝试都会从PATH进入路径搜索。
现在理论已经完成,请考虑有效负载的创建-代理库本身。
第一种选择。 诚实的代理库
让我们从一个相对简单的例子开始-我们将创建一个诚实的代理库。 在这种情况下,诚实意味着将显式注册dll中的所有函数,并且将为每个函数编写一个与原始库具有相同名称的函数调用。 使用这样的库对于被调用的代码将是完全透明的:如果它调用某些函数,则它将收到正确的答案,结果以及应并行执行的所有操作。
这是指向version.dll库的完成的示例(
github )的链接。
代码重点:
- 诚实地描述了原始库导出表中的所有函数原型。
- 原始库已加载,所有对我们函数的调用都被扔进了库中。
方便地 ,该应用程序可以继续正常运行,而不会遇到任何“特殊影响”。
不方便的是,我必须为每个函数编写一堆统一的代码,而且还要仔细检查原型的一致性。
第二种选择。 简化代码编写
当处理像version.dll这样的库时,其中的导入表很小,只有17个函数,原型很简单,那么诚实的代理库是一个不错的选择。

但是,如果使用该库的代理服务器(例如bcrypt),则一切都会更加复杂。 这是她的导入表:

57个功能! 以下是几个原型示例:


我们只能说没有什么是不可能的,但是为这样的库创建一个诚实的代理并不是一件令人愉快的事情。
如果您对函数作弊,则可以简化代码。 我们将库中的所有函数声明为__declspec(裸露),在正文中,我们将使用仅使原始库中的函数成为jmp的汇编代码。 这将使我们不必使用冗长的原型,而可以在没有视图参数的情况下在任何地方放置简单的公告:
无效foo()
当应用程序调用我们的函数时,代理库将不会对寄存器和堆栈执行任何操作,从而使原始函数能够按需完成所有工作。
使用此方法的version.dll库的示例(
github )。
重点:
- 原始库已加载,所有对我们函数的调用均被抛入其中。 函数体和加载都包装在宏中。
借助宏,可以
方便 ,正确地运行应用程序,甚至可以轻松描述甚至很多功能。
不便之处是x64中出现了意外的情况。 Visual Studio(如果我没有记错的话,自2012年以来一直存在)禁止在64位代码中使用裸露和asm插入。 从头开始编写代理时,每个函数都必须验证它是否在def文件中描述,是否已加载原始文件以及函数的主体是否已描述。
第三种选择。 我们一般丢掉身体
使用裸露建议另一种选择。 您可以创建一个导入表,该表的所有功能都将引用一行真实的代码:
无效nop(){}
这样的库将由应用程序加载,但将不起作用。 调用任何函数时,堆栈很可能会被撕裂或发生其他问题。 但这并不总是不好的-例如,如果dll注入的目标只是简单地使用必要的权限运行代码,则足以执行DllMain代理库中的有效负载并立即安静地终止应用程序。 在这种情况下,将不会真正调用函数,也不会出现错误。
在
github上的一个例子,再次是version.dll。
代码重点:
方便地,这样的代理库只编写了几分钟。
被调用的应用程序停止工作
是很不方便的。
第四个选项。 取现成的工具
编写dll很好,但并不总是很方便,而且也不是很快,因此您应该考虑使用自动选项。
您可以沿用旧病毒的路径-获取我们要创建其代理的库,在其中创建代码的可执行部分,在其中写下有效负载,并将入口点更改为该部分。 这不是最简单的方法,因为您可能会意外破坏某些内容,因此必须编写汇编程序,记住PE文件的设备。 这不是我们的方式。
要操作dll劫持,我们将添加另一个dll劫持。

这是相对容易做到的。 我们复制要制作其代理的库,然后将具有任意功能的dll添加到此副本的导入表中。 现在下载将沿着链进行-在可执行文件的开始处,将加载代理dll,这将加载指定的库本身。
“嘿,您将加载一个库替换为另一个。 有什么意义? 都是一样的,必须对dll进行编码! 一切都正确,但是仍然有一种感觉。 现在,对具有有效负载的库的需求将减少。 您可以指定任何名称,主要是仅导出一个可以具有任何原型的函数。 在导入表中输入库和函数的主要名称。
具有有效负载的库可以在任何情况下都可以使用。
您可以使用许多PE编辑器(例如CFF Explorer或pe-bear)修改导入表。 对于我自己,我在C#中编写了一个小实用程序,该程序可以纠正表而没有不必要的手势。
github上的源代码,
发布版本中的 binar。
结论
在本文中,我尝试介绍了自己创建代理dll的基本方法。 剩下的只是告诉如何捍卫。
通用建议不多:
- 不要在用户可写的文件夹中存储可执行文件,尤其是那些具有较高权限的可执行文件。
- 最好在执行LoadLibrary之前先找到并验证该库的存在。
- 查看操作系统中可用的现有保护方法。 例如,在Windows 10中,可以设置PreferSystem32标志,以便 dll搜索不是从可执行文件文件夹开始,而是以system32开始。
感谢您的关注,我很高兴听到问题,建议,意见和评论。
UPD:根据评论员的建议,我提醒您,您需要仔细选择图书馆。 如果该库包含在KnownDlls列表中,或者名称类似于MinWin(ApiSetSchema,api-ms-win-core-console-l1-1-0.dll-仅此而已),则由于处理功能,很可能将无法拦截该库操作系统中的此类dll。