DLL搜索路径和DLL劫持
环境:XP SP3 VS2005
作者:magictong
为什么要把DLL搜索路径(DLL ORDER)和DLL劫持(DLL Hajack)拿到一起讲呢?呵呵,其实没啥深意,仅仅是二者有因果关系而已。可以讲正是因为Windows系统下面DLL的搜索路径存在的漏洞才有了后来的一段时间的DLL劫持大肆流行。
最近(其实不是最近,哈,是以前分析过,断断续续的……)简单分析了一个DLL劫持下载者的行为,感觉有必要写点东西说明一下。其实DLL劫持是比较好预防的,从编程规范上我们可以进行规避(后面会专门讲到),从实时防护的角度来讲我们也可以想出一些办法进行拦截。新的DLL劫持者基本都是通过当前路径来入侵,一些老的DLL劫持者一般都是通过exe的安装目录来入侵的,为什么会这样,后面还会讲到。
要搞清DLL劫持的原理,首先要搞清DLL搜索路径,到哪去搞清?当然是问微软啦!MSDN上面有一篇专门讲DLL搜索顺序的文章(Dynamic-Link Library Search Order http://msdn.microsoft.com/en-us/library/ms682586%28VS.85%29.aspx),虽然是英文的但是并不复杂讲得很清楚,大家如果对这块兴趣大的话可以仔细研读下,我就不翻译了。
Dynamic-Link Library Search Order里面主要讲到一个安全DLL搜索模式的问题,大家可以通过下面的表格来看一下不同系统对安全DLL搜索模式的支持情况(下表中用SDS代表安全DLL搜索模式):
系统 |
Win2k |
Win2kSP4 |
XP |
XPSP2 |
XPSP3 |
是否支持SDS |
不支持 |
支持 |
支持 |
支持 |
支持 |
SDS是否默认开启 |
不适用 |
否 |
否 |
是 |
是 |
SDS是否可以通过注册表开启 |
不适用 |
是 |
是 |
不适用 |
不适用 |
是否支持SetDllDirectory |
不支持 |
不支持 |
不支持 |
支持 |
支持 |
注:在vista和win7下没有做过实验,有兴趣的可以自己做做实验。
注:上面说到通过注册表开启是指将HKLM\System\CurrentControlSet\Control\Session Manager键值下的属性SafeDllSearchMode的值设置为1(如果没有SafeDllSearchMode就自己手动创建)。
在安全DLL搜索模式开启的情况下,搜索顺序是:
1、应用程序EXE所在的路径。
2、系统目录。
3、16位系统目录
4、Windows目录
5、当前目录
6、PATH环境变量指定的目录
如果安全DLL搜索模式不支持或者被禁用,那么搜索顺序是:
1、应用程序EXE所在的路径。
2、当前目录
3、系统目录。
4、16位系统目录
5、Windows目录
6、PATH环境变量指定的目录
说了这么多?我们怎么校验自己的系统的DLL的搜索顺序呢?其实是很简单的,我们首先构造两个简单的程序,一个DLL程序一个EXE程序,代码很简单,如下:
DLL程序:
- <span style="font-size:16px;">BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
- {
- switch(ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- char szDllPath[MAX_PATH] = {0};
- GetModuleFileNameA(hModule, szDllPath, MAX_PATH);
- cout << "DLL PATH: " << szDllPath << endl;
- break;
- }
- case DLL_THREAD_ATTACH:
- break;
- case DLL_THREAD_DETACH:
- break;
- case DLL_PROCESS_DETACH:
- break;
- default:
- break;
- }
- return TRUE;
- }</span>
EXE程序:
- <span style="font-size:16px;">int _tmain(int argc, _TCHAR* argv[])
- {
- HMODULE hDll = LoadLibrary(TEXT("DLLHijackSO.dll"));
- if (!hDll)
- {
- cout << "DLLHijackSO.dll Load Faild!" << endl;
- }
- return 0;
- }</span>
够简单了吧,思路也很明确,在DLL的DLLMAIN里面会打印出DLL所在的路径,在EXE程序里面通过DLL的名字(不是全路径)去加载这个DLL,如果加载失败会打印出一条加载失败信息。然后根据上面提到的6个地方,分别放一个DLL程序编译出来的DLL(我起的名字是DLLHijackSO.dll),EXE编译出来的DLLHijackApp.exe是放在H:\Prj_N\DLLHijackSO\Release里面的,然后把cmd启动,cmd启动之后,它的当前路径一般都是设置的用户目录,譬如我的机器上面就是C:\Documents and Settings\magictong,通过CD命令对当前文件夹的切换,当前路径也随之改变。实验的基本过程,因为整个系统放置了6个DLLHijackSO.dll,每运行DLLHijackApp.exe一次,如果成功加载DLL,那么就把加载的那个DLL删除,持续进行,直到加载失败。好,实验开始……
我的系统的六个位置(其中最后一个PATH变量指定的路径,你选取一个就可以了):
H:\Prj_N\DLLHijackSO\Release
C:\WINDOWS\system32
C:\WINDOWS\system
C:\WINDOWS
C:\Documents and Settings\magictong
C:\Python25
实验结果如图:
根据结果,我想已经很明确了,我的系统是启用了安全DLL搜索模式的,因为我的系统是XPSP3。另外就是关于当前路径的问题,其实当前路径是可以由进程的父进程来设置的,大家可以去看CreateProcess里面的参数,有一项是设置当前路径的,也就是为什么CMD启动你的进程的时候,当前路径会在“你想不到的地方”,explorer启动进程则是把当前路径设置为应用程序所在的路径(当前路径可以通过GetCurrentDirectory这个API来获取)。
我想如果DLL搜索路径搞清楚了,DLL劫持的原理就很简单了。个人觉得一两句话就可以说清楚:在进行DLL加载时,如果不是绝对路劲,系统会按照DLL的搜索路径依次进行目标DLL的搜索直到找到目标DLL或者加载失败,如果你在真实的DLL被找到之前的路径放入你的劫持(同名)DLL,那么应用程序将先加载到你的DLL,这样就是DLL劫持的过程。
原理虽然简单,你的劫持DLL的选取和编写则要有些技巧,不是所有的DLL都可以被劫持的,有些DLL是受系统保护的,譬如user32.dll等,这些是不能劫持的。在一些老的DLL劫持病毒里面一般是选取usp10.dll,lpk.dll等,原因很简单,一般的应用程序都会加载它们,而且没有被系统保护(好不好用,谁用谁知道,我反正没用过)。
首先简单总结下DLL劫持和DLL注入的区别:
|
DLL劫持 |
DLL注入 |
主动性 |
被动,等待目标运行 |
主动,目标必须已经在运行 |
是否需要跳板 |
不需要 |
需要,由第三方来帮助注入 |
是否容易拦截 |
不容易 |
容易 |
是否容易免疫 |
容易 |
不容易 |
DLL的编写要求 |
伪造真实DLL一样的导出函数表 |
可以按自己的流程写导出函数 |
下面讲一下新老两种DLL劫持的攻击原理和防御方案:
之前提到过一种老的DLL劫持的利用,劫持usp10.dll,lpk.dll等等。这些DLL的实际目录在system32下,病毒利用DLL的搜索排名第一的是应用程序自身所在的目录,释放同名的劫持DLL到应用程序目录下,这样应用程序启动时就会先加载了劫持DLL,达到不可告人目的。应用程序加载了劫持DLL之后又有两种后续的攻击方案,一种是转发调用到正常的DLL,使应用程序毫无觉察,同时秘密在后台下载更多的下载器木马等。另一种就是直接破坏造成程序无法运行,这种主要用于干掉杀软等安全软件。
通用免疫方案:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs]在此注册表项下定义一个“已知DLL名称”,那么凡是此项下的DLL文件就会被禁止从EXE自身目录下调用,而只能从系统目录,也就是system32目录下调用。根据这个方案是不是可以写一个简单的DLL劫持免疫器了呢?当然对于一个安全软件来说,这个地方也是要保护的地方之一。下图是我机器上面该键值下面的一些DLL名称:
之前看过讲微点的一篇文章,讲的是对这种老的DLL劫持实行的一种拦截方案:如果发现一个系统下的敏感DLL被加载,则通过堆栈回溯找到该敏感DLL的加载者的地址,如果是在一个同名的DLL里面则认为是被DLL劫持了,报告发现病毒。从对抗的角度来讲,这种方案很容被过掉,只要修改栈帧指针可能就发现不了了。
新的DLL劫持的攻击原理和防御方案:
很多应用程序都是支持插件和扩展的,尤其是一些播放器软件,支持解码插件,第一次安装的时候可能只装了常见的一些音视频解码插件,在遇到一些特殊的音视频格式时就需要实时去网络上拖取一个解码插件下来进行解码操作,当然软件会首先尝试加载这个解码插件(通常是一个DLL),这个时候一些设计有缺陷的产品(譬如不是通过绝对路径加载插件)在加载DLL时就会搜索上面提到过的各个路径。一般这种情况下,恶意攻击者会在网络上提供一些用户感兴趣名字的视频图片神马的,用户下载压缩包解压后,其实压缩文件中包含着两个以上的文件,用户很难发现,解压后,劫持DLL和视频或者图片文件是放在同一个目录的,当然劫持DLL文件的属性是系统隐藏,然后用户高高兴兴的去双击那个视频或者图片文件,杯具发生了……这实际上是利用的DLL搜索时会搜索当前目录这个特点来进行DLL劫持的,为什么当前目录是视频图片文件存放的目录呢?
这个可以做个实验,与文件关联有关,在注册表里面注册一个._magic后缀的文件类型,打开这种文件的应用程序是c盘下的DLLHijackApp.exe(文件关联这块有兴趣的自己可以查资料,因为与本文关系不大就不细讲了,见下图),DLLHijackApp.exe的作用就是弹出一个MessageBox打出当前目录,如果直接双击运行DLLHijackApp.exe,当前目录就是c盘,如果在桌面上新建一个x._magic文件再双击运行,打印出的当前目录则是桌面目录(也就是x._magic文件所在的目录,见下面的图)。
直接双击运行C盘下的DLLHijackApp.exe,当前目录:
双击打开桌面上的x._magic的文件,当前目录:
现在大家应该明白为什么当前目录是文件所在的目录了吧。病毒正是利用这一点,把劫持DLL和音视频,图片文件捆绑在一起下载,达到入侵的目的。
防御方案:
暂无通用的防御方案,因为劫持的都是一些第三方的DLL,暂时只能通过下载保护之类的途径进行保护(这类攻击最终还是会转去下载更多的盗号木马或者下载器之类的,然后进行一些盗号、破坏等等的事情)。
讲了这么多,来看一个DLL劫持的实例,是简单写的一个说明原理的小例子:
劫持DLL要保证应用程序运行正常,不被用户发现,除了和原来的DLL有相同的名字之外还需要导出和原DLL一样的函数。我们现在已经有了一个名字是DLLHijackSO.dll的DLL,他导出一个Add函数,这个函数原型是int Add(int a, int b),很简单吧。假设这个DLL是一个系统DLL,是放在system32目录下。
原DLL的代码如下(Add函数通过def文件导出):
- <span style="font-size:16px;">// DLLHijackSO.cpp : Defines the entry point for the DLL application.
- //
- #include "stdafx.h"
- #include <iostream>
- using namespace std;
- BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
- {
- switch(ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- char szDllPath[MAX_PATH] = {0};
- GetModuleFileNameA(hModule, szDllPath, MAX_PATH);
- cout << "ORC DLL - DLL PATH: " << szDllPath << endl;
- break;
- }
- case DLL_THREAD_ATTACH:
- break;
- case DLL_THREAD_DETACH:
- break;
- case DLL_PROCESS_DETACH:
- break;
- default:
- break;
- }
- return TRUE;
- }
- int Add(int a, int b)
- {
- return a + b;
- }</span>
我们的应用程序的代码如下(名字是DLLHijackApp.exe,加载这个DLL,然后调用它的Add函数):
- <span style="font-size:16px;">// DLLHijackApp.cpp : Defines the entry point for the console application.
- //
- #include "stdafx.h"
- #include <Windows.h>
- #include <iostream>
- using namespace std;
- int _tmain(int argc, _TCHAR* argv[])
- {
- SetDllDirectoryW(TEXT(""));
- HMODULE hDll = LoadLibrary(TEXT("DLLHijackSO.dll"));
- if (!hDll)
- {
- cout << "DLLHijackSO.dll Load Faild!" << endl;
- }
- else
- {
- typedef int (*PFUNADD)(int , int );
- cout << "App - Add(int a, int b)" << endl;
- HMODULE hMod = LoadLibrary(TEXT("DLLHijackSO.dll"));
- if (hMod)
- {
- PFUNADD pfnAdd = (PFUNADD)GetProcAddress(hMod, "Add");
- cout << "App 1022 + 1022 = " << pfnAdd(1022, 1022) << endl;
- }
- }
- if (hDll)
- {
- FreeLibrary(hDll);
- hDll = NULL;
- }
- char szCurrentDir[MAX_PATH] = {0};
- GetCurrentDirectoryA(MAX_PATH, szCurrentDir);
- MessageBoxA(NULL, szCurrentDir, "当前目录", MB_OK);
- cout << "CUR PATH: " << szCurrentDir << endl;
- return 0;
- }</span>
我们先测试一下,把DLLHijackSO.dll放入system32下,然后应用程序DLLHijackApp.exe放在任意位置,运行结果如下:嗯,是没有问题的。
现在我们写劫持DLL,其实也很简单,它编译出来的DLL名字也是DLLHijackSO.dll,也通过def文件导出了Add函数:
- <span style="font-size:16px;">// DLLHijackHijack.cpp : Defines the entry point for the DLL application.
- //
- #include "stdafx.h"
- #include <iostream>
- #include <tchar.h>
- using namespace std;
- namespace DLLHijackName
- {
- HMODULE m_hModule = NULL; //原始模块句柄
- // 加载原始模块
- inline BOOL WINAPI Load()
- {
- TCHAR tzPath[MAX_PATH] = {0};
- GetSystemDirectory(tzPath, MAX_PATH);
- _tcsncat_s(tzPath, MAX_PATH, TEXT("\\DLLHijackSO.dll"), _TRUNCATE);
- m_hModule = LoadLibrary(tzPath);
- if (!m_hModule)
- {
- cout << "无法加载DLLHijackSO.dll,程序无法正常运行。" << endl;
- }
- return (m_hModule != NULL);
- }
- // 释放原始模块
- inline VOID WINAPI Free()
- {
- if (m_hModule)
- FreeLibrary(m_hModule);
- }
- // 获取原始函数地址
- FARPROC WINAPI GetOrgAddress(PCSTR pszProcName)
- {
- FARPROC fpAddress;
- if (m_hModule == NULL)
- {
- if (Load() == FALSE)
- ExitProcess(-1);
- }
- fpAddress = GetProcAddress(m_hModule, pszProcName);
- if (!fpAddress)
- {
- cout << "无法找到函数,程序无法正常运行。" << endl;
- ExitProcess(-2);
- }
- return fpAddress;
- }
- }
- BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
- {
- switch(ul_reason_for_call)
- {
- case DLL_PROCESS_ATTACH:
- {
- char szDllPath[MAX_PATH] = {0};
- GetModuleFileNameA(hModule, szDllPath, MAX_PATH);
- cout << "DLL Hijack - DLL PATH: " << szDllPath << endl;
- break;
- }
- case DLL_THREAD_ATTACH:
- break;
- case DLL_THREAD_DETACH:
- break;
- case DLL_PROCESS_DETACH:
- DLLHijackName::Free();
- break;
- default:
- break;
- }
- return TRUE;
- }
- typedef int (*PFUNADD)(int , int );
- int Add(int a, int b)
- {
- cout << "DLL Hijack - Add(int a, int b)" << endl;
- PFUNADD pfnAdd = (PFUNADD)DLLHijackName::GetOrgAddress("Add");
- if (pfnAdd)
- {
- return pfnAdd(a, b);
- }
- return 0;
- }</span>
好,现在我们把这个劫持的DLLHijackSO.dll放在DLLHijackApp.exe同目录下,运行:
劫持成功!
当然,我们的重点还是要放在避免我们编写的软件被DLL劫持,一般有以下一些针对DLL劫持的安全编码的规范(其实大家也应该可以从上述的DLL劫持的原理自己总结出来)::
1)调用LoadLibrary,LoadLibraryEx,CreateProcess,ShellExecute等等会进行模块加载操作的函数时,指明模块的完整(全)路径,禁止使用相对路径(这样基本就可以防死上面所讲的第二种DLL劫持了)。
2)在应用程序的开头调用SetDllDirectory(TEXT(""));从而将当前目录从DLL的搜索列表中删除,也就是搜索时不搜索当前目录。
3)打上最新的系统补丁,确保安全DLL搜索模式是开启状态。
4)对于安全软件来讲要确保用户的机器上面的KnownDLLs下是完整的。
5)DLL的重定向等需要注意的问题。