基本概念
Windows的应用程序在加载进程所需的动态库时,会根据导入表一一加载。但是,我们的导入表中只保存有动态库的名称,系统如何知道这个动态库的完整路径并将其加载呢?
其实,在加载动态库时,系统会按照一定的顺序搜索各个目录来寻找指定的Dll文件。一般搜索顺序为:
应用程序所在目录-->系统目录-->16位系统目录-->Windows目录-->运行程序的当前目录-->PATH环境变量。
这还与注册表项HKLM\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode有关,如果键值为1,则执行上述的搜索顺序。如果键值为0,则执行如下的搜索顺序:
应用程序所在目录-->运行程序的当前目录-->系统目录-->16位系统目录->Windows目录-->PATH环境变量。
注入原理
上面说到,系统在加载所需的动态库时,第一个搜索的目录总是我们应用程序所在的目录,而应用程序常用的动态库Kernel32.dll、User32.dll等动态库是保存在Windows系统目录下的,搜索顺序都是比较靠后的,那么我们是否可以在当前目录下伪造一个User32.dll,提供同样的导出表,将每个导出函数转发到真正的User32.dll动态库里,同时可以实现我们自定义的操作,让系统误以为自己实际上已经加载了真正的User32.dll呢?
这确实是行得通的。因为我们在系统加载真正的动态库之前阻止了他,就好像我们将Dll劫持了一样,所以这种注入方式又被称为Dll劫持注入。
函数转发
在劫持了真正的Dll文件后,系统就会使用我们自定义Dll文件的导出表,这是如果不做调整,我们的Dll就会"露馅"。所以,在劫持了真正的Dll文件后,我们同时要保证Dll文件内的功能也要"劫持"到,这里我们使用的是函数转发机制。
#pragma comment(linker,"/EXPORT:MyMessageBox=User32.MessageBoxA")
这段代码告诉编译器,系统在调用我们的Dll中的MyMessageBox时,会自动转发到User32.Dll中的MessageBoxA函数去执行。这就好比我们绑架了User32,然后我们假装告诉操作系统我们就是User32,这时候系统告诉我实现一下MessageBoxA函数,我们就可以把这个活甩给真正的User32,让它来完成,然后我们把完成的结果再返回给系统。以上就是函数转发机制的实现原理。
函数调用
除了函数转发外,还有一种比较常用的方法,那就是函数调用。
VOID _declspec(naked) MyMessageBox()
{
LPVOID MessageBoxAddress = NULL;
HMODULE ModuleHandle = NULL;
//加载真正的动态库
ModuleHandle = LoadLibrary(_T("User32.dll"));
if (ModuleHandle != NULL)
{
//获得导出函数的地址
MessageBoxAddress = GetProcAddress(ModuleHandle, "MessageBoxA");
if (MessageBoxAddress)
{
__asm
{
//跳转到真正的函数地址处
jmp MessageBoxAddress
}
}
//释放动态库
FreeLibrary(ModuleHandle);
}
}
函数调用主要是在我们的函数内部,又手动加载了真正的动态库,获得了动态库的导出函数,一旦我们的函数被系统调用,我们再在函数内调用真正的函数,在实现了我们自定义操作的同时,同时保证了功能的完整性。
如果说函数转发是"胁迫"真正的Dll帮我们实现功能,那么函数调用就是让Dll告诉我们实现的方法,我们自己自己来实现。
局限性
(1)Windows7以上,系统没有了SafeDllSearchMode 这一选项,而是采用新的注册表项\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs,凡是此项下的DLL文件就会被禁止从应用程序自身所在的目录下调用,而只能从系统目录即SYSTEM32目录下调用。
(2)有些Dll文件在加载时需要检查签名等选项,如果是我们伪造的Dll,检查时就会出错,这也就导致了Dll劫持的失败。