老树开新花:DLL劫持漏洞新玩法
DLL劫持漏洞已经是一个老生常谈,毫无新鲜感的话题了。DLL劫持技术也已经是黑客们杀人越货,打家劫舍必备的武器。那么,随着Win10的诞生,微软是否已经修复了此漏洞?同时在当前的安全环境下,DLL劫持漏洞是否又有新的利用方式和价值?
本文将对DLL劫持漏洞的原理,漏洞实现,漏洞挖掘,漏洞利用,漏洞防御等方面进行分析。
0×01 DLL劫持漏洞原理
在Windows系统中,为了节省内存和实现代码重用,微软在Windows操作系统中实现了一种共享函数库的方式。这就是DLL(Dynamic Link Library),即动态链接库,这种库包含了可由多个程序同时使用的代码和数据。
每个DLL都有一个入口函数(DLLMain),系统在特定环境下会调用DLLMain。在下面的事件发生时就会调用dll的入口函数:
1.进程装载DLL。
2.进程卸载DLL。
3.DLL在被装载之后创建了新线程。
4.DLL在被装载之后一个线程被终止了。
另外,每个DLL文件中都包含有一个导出函数表也叫输出表(存在于PE的.edata节中)。使用一些PE文件查看工具如LoadPE,就可以查看导出函数的符号名即函数名称和函数在导出函数表中的标识号。
应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接(load-time dynamic linking)也叫静态调用和显式链接(run-time dynamic linking)也叫动态调用。隐式链接方式一般用于开发和调试,而显式链接方式就是我们常见的使用LoadLibary或者LoadLibraryEx函数(注:涉及到模块加载的函数有很多)来加载DLL去调用相应的导出函数。调用LoadLibrary或者LoadLibraryEx函数时可以使用DLL的相对路径也可以使用绝对路径,但是很多情况下,开发人员都是使用了相对路径来进行DLL的加载。那么,在这种情况下,Windows系统会按照特定的顺序去搜索一些目录,来确定DLL的完整路径。关于动态链接库的搜索顺序的更多详细资料请参阅MSDN。根据MSDN文档的约定,在使用了DLL的相对路径
调用LoadLibrary函数时,系统会依次从下面几个位置去查找所需要调用的DLL文件。
1.程序所在目录。
2.加载 DLL 时所在的当前目录。
3.系统目录即 SYSTEM32 目录。
4.16位系统目录即 SYSTEM 目录。
5.Windows目录。
6.PATH环境变量中列出的目录
微软为了防止DLL劫持漏洞的产生,在XP SP2之后,添加了一个SafeDllSearchMode的注册表属性。注册表路径如下:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session
Manager\SafeDllSearchMode
当SafeDllSearchMode的值设置为1,即安全DLL搜索模式开启时,查找DLL的目录顺序如下:
1.程序所在目录
2.系统目录即 SYSTEM32 目录。
3.16位系统目录即 SYSTEM 目录。
4.Windows目录。
5.加载 DLL 时所在的当前目录。
6.PATH环境变量中列出的目录。
但是经过笔者测试发现,XP系统之后发布的Windows操作系统中,默认情况下并未开启安全DLL搜索模式。但是这是否意味着DLL劫持漏洞就可以轻易被利用呢?微软为了更进一步的防御系统的DLL被劫持,将一些容易被劫持的系统DLL写进了一个注册表项中,那么凡是此项下的DLL文件就会被禁止从EXE自身所在的目录下调用,而只能从系统目录即SYSTEM32目录下调用。注册表路径如下:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
下面是不同的Windows系统中默认情况下的KnownDLLs注册表项的截图。
Win2k3:
Win7:
Win8.1:
Win2K8:
Win10:
通过上面的截图我们可以发现,以前经常使用的一些劫持DLL已经被加入了KnownDLLs注册表项,这就意味着使用诸如usp10.dll,lpk.dll,ws2_32.dll去进行DLL劫持已经失效了。
Windows操作系统通过“DLL路径搜索目录顺序”和“KnownDLLs注册表项”的机制来确定应用程序所要调用的DLL的路径,之后,应用程序就将DLL载入了自己的内存空间,执行相应的函数功能。
不过,微软又莫名其妙的允许用户在上述注册表路径中添加“ExcludeFromKnownDlls”注册表项,排除一些被“KnownDLLs注册表项”机制保护的DLL。也就是说,只要在“ExcludeFromKnownDlls”注册表项中添加你想劫持的DLL名称就可以对该DLL进行劫持,不过修改之后需要重新启动电脑才能生效。
在上述描述加载DLL的整个过程中,DLL劫持漏洞就是在系统进行安装“DLL路径搜索目录顺序”搜索DLL的时候发生的。无论安全DLL搜索模式是否开启,系统总是首先会从程序所在目录加载DLL,如果没有找到就按照上面的顺序依次进行搜索。那么,利用这个特性,攻击者就可以伪造一个相同名称,相同导出函数表的一个“假”DLL,并将每个导出函数转向到“真”DLL。将这个“假”DLL放到程序的目录下,当程序调用DLL中的函数时就会首先加载“假”DLL,在“假”DLL中攻击者已经加入了恶意代码,这时这些恶意代码就会被执行,之后,“假”DLL再将DLL调用流程转向“真”DLL,以免影响程序的正常执行。
0×02 如何编写一个劫持的DLL?
在挖掘DLL劫持漏洞之前,需要明白如何通过编程编写一个用于劫持的DLL。
编写一个用于劫持指定DLL的DLL文件,需要两个步骤:
1.查看被劫持的DLL的导出函数表。
2.编程实现劫持DLL向原DLL的导出函数的转发,并加入你的“恶意代码”。
使用DLL_Hijacker.py脚本可以一键生成劫持指定DLL的CPP源码文件。对这个CPP文件进行编译就生成了相应的劫持DLL文件。
0×03 劫持Windows系统的DLL
要分析一个应用程序是否存在劫持系统DLL的漏洞,需要这么几个步骤:
1.启动应用程序
2.使用Process Explorer等类似软件查看该应用程序启动后加载的动态链接库。
3.从该应用程序已经加载的DLL列表中,查找在上述“KnownDLLs注册表项”中不存在的DLL。
4.编写第三步中获取到的DLL的劫持DLL。
5.将编写好的劫持DLL放到该应用程序目录下,重新启动该应用程序,检测是否劫持成功。
接下来,以微信PC客户端为例,测试系统为Win8.1:
运行微信PC客户端后,使用Process Explorer软件查看已加载的DLL如下:
与Win8.1的“KnownDLLs注册表项”进行对比:
以msls31.dll为例,使用DLL_Hijacker.py生成源码CPP文件,编译后,将生成好的msls31.dll放到微信PC客户端的目录下:
启动微信PC客户端,可以发现劫持DLL中的“恶意代码”已经执行了。从这个消息框的图标也能看出来,微信PC客户端进程已经加载了这个被用于劫持msls31.dll的DLL文件。点击确定之后,微信主窗口就会加载显示,并不影响原应用程序的执行。
在实际的测试当中,并不是每一个被宿主进程加载但不在“KnownDLLs注册表项”中的DLL能够成功劫持,如ntdll.dll等系统核心dll已经被微软做了保护。其他的dll可能是宿主进程在调用LoadLibrary函数时使用了“绝对路径”,另外,个别系统内置的DLL文件的导出表中会有额外的附加数据,要对这类DLL进行劫持,必须对这些额外的附加数据也做处理才能成功劫持,最常见的情况是DLL能够成功劫持,但是会影响原应用程序的正常执行。
为了实现自动化测试DLL劫持漏洞,我编写了一个PowerShell脚本,可以检测出宿主进程在启动后加载的DLL模块,并分析出在“KnownDLLs注册表项”中不存在的已加载的DLL,当然,这个脚本只能粗略的分析出哪些DLL可能存在劫持漏洞,要实现完全自动精准的分析工具,只需要把DLL_Hijacker.py的功能和DLLHijack_Detecter.ps1的功能结合在一起即可。
0×04 劫持应用程序的DLL
和劫持应用程序的DLL相比,劫持系统本身的DLL通用性更强,例如,笔者测试发现msimg32.dll依旧可以劫持大多数应用程序。这个DLL已经多次被黑客用于DLL劫持执行恶意代码。但是劫持应用程序的DLL比劫持系统本身的DLL隐蔽性更好,安全软件难以判断这种“劫持行为”。
劫持应用程序的DLL的步骤与劫持系统本身的DLL类似,这里就不再赘述,只是要注意有些DLL是在功能执行中动态加载的并非启动了应用程序时就会加载。
这里依旧使用微信PC客户端为例,观察微信安装目录的DLL:
根据个人经验结合应用程序的一些功能和DLL文件名可以判断出部分DLL的功能,例如:
PrScrn.dll是提供截图功能的DLL,查看其导出函数只有一个PrScrn且不需要参数就可以调用,使用如下命令可以验证:
Rundll32.exe [微信安装目录]\PrScrn.dll,PrScrn
接下来,以PrScrn.dll为例,进行DLL劫持。使用DLL_Hijacker.py生成DLL源码后对下面的代码进行修改,例如将PrScrn.dll修改为PrScrn_Origial.dll。
编译后,将原来的PrScrn.dll改名为PrScrn_Origial.dll并将生成的PrScrn.dll放到微信安装目录。
登录微信,点击截图的图标,就可以发现弹框了。
对于类似于PrScrn.dll这样导出函数无参数的DLL还可以使用常规的一个DLL动态调用另外一个DLL的方式来劫持,主要代码如下:
#include "stdafx.h"
extern "C" __declspec(dllexport) void PrScrn();
BOOL APIENTRY DllMain(HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
//MessageBox(NULL, "DLL Hijack! by DLLHijacker!", ":)", 0);
return TRUE;
}
void PrScrn()
{
MessageBox(NULL, "DLL Hijack! by DLLHijacker!", ":)", 0); //your evil code
HINSTANCE hDllInst = LoadLibrary("PrScrn_Origial.dll");
if (hDllInst)
{
typedef DWORD(WINAPI *EXPFUNC)();
EXPFUNC exportFunc = NULL;
exportFunc = (EXPFUNC)GetProcAddress(hDllInst, "PrScrn");
if (exportFunc)
{
exportFunc();
}
FreeLibrary(hDllInst);
}
return;
}
0×04 DLL劫持漏洞的防御
对于DLL劫持漏洞产生的原因,并不能单一的归咎于微软,只能说这是微软的一个“设计缺陷”,要从根本上防御DLL劫持漏洞,除了微软提供的“安全DLL搜索模式”和“KnownDLLs注册表项”机制保护DLL外,开发人员必须要做更多来保护应用程序自身。开发过程中,调用LoadLibrary,LoadLibraryEx等会进行模块加载操作的函数时,使用模块的物理路径作为参数。在程序调用DLL时使用“白名单”+ “签名”进行DLL的验证。或者在开发应用程序时,在代码开头调用SetDllDirectory函数,把当前目录从DLL的搜索顺序列表中删除,不过这个API只能在打了KB2533623补丁的Windows7,2008上使用。-_-!
不过即使使用了这些防御措施,DLL劫持漏洞依旧可能会存在,更何况目前很多厂商对于DLL劫持漏洞都是持“忽略”的态度。
0×05 总结
从DLL劫持漏洞产生的原理可以分析出,由于微软的这个“设计缺陷”以及开发人员的不规范编码,使得此漏洞可以在很多情况下都会产生,其危害程度根据不同类型的劫持方式也有不同,最典型危害最直接的莫过于存在“文件后缀关联”的应用程序的DLL劫持,只要将相关联的后缀文件和制作好的劫持DLL放在同一目录,打开关联后缀的文件时应用程序就会加载制作好的劫持DLL。只要执行了攻击者的恶意代码,那么攻击者就有权限做很多事情。
另外,在挖掘此类漏洞时也要注意到不存在劫持漏洞的DLL如果调用了一个或多个其他DLL,那么依旧有可能会产生DLL劫持漏洞。如果宿主进程已经取得了UAC权限,那么DLL劫持也是另外一种获取UAC权限的方式,同时,由于DLL劫持漏洞的隐蔽性,安全软件难以捕捉检测,很多恶意软件使用此漏洞作为后门的通道或者自启动技术。在此次测试中,笔者发现国内很多用户使用率较高的应用程序均存在DLL劫持漏洞。由此看来,DLL劫持漏洞本身不算高危漏洞但是如果将此漏洞与其他一些攻击技术进行捆绑组合,危害程度将会大幅提升。