通过DLL重定向实现API劫持

本篇paper译自exp-db-papers中API Interception via DLL Redirection。
https://github.com/offensive-security/exploit-database-papers

通过DLL重定向实现API劫持

在Windows系统中,所有的应用程序都必须经由API函数来实现和内核的通信;同样地,尽管在最简单的Windows应用程序中这些函数也十分危险。因此,拦截、监视以及修改一个应用程序API调用的技术通常叫做API Hooking,非常有效的给予其所调用进程的完全控制。从多方面考虑这将非常有用,包括调试、逆向工程以及hacking。

实际上有多种方法可以用于实现我们的目的,被文章仅仅实验DLL重定向。选择这一方法出于多种原因考虑:

  1. 实现相对简单。
  2. 允许我们去观察、修改传递给API函数的参数,修改函数的返回值以及运行其他我们想要执行的代码。
  3. 其他的方法往往需要将代码插入到目标进程或者从一个外部应用程序中执行代码,DLL重定向则仅仅需要拥有向目标应用程序工作目录的写权限。
  4. 我们无需修改目标(磁盘或内存中)或者任意系统文件即可拦截任意API调用。

工具和预备知识

本文将使用下列软件。你当然也可以选择自己偏好的任意工具,然而,请记住它们的具体使用方法和实现:

  • Visual C++ - 用来编译DLL文件
  • OllyDbg - 用来实验目标应用程序和外部模块
  • DumpbinGUI - 用来获取目标DLL导出的函数列表
  • Linkout.pl - 用于自动处理一些体力活(需要ActivePerl)

假定读者有牢固的Win32 C/C++编程、汇编语言以及以上所提到应用程序使用的基础(当然不包括linkout.pl)。对于API Hooking其他的方法有一个基础的理解也很有帮助。

DLL重定向是什么

可执行程序的导入API函数源于DLL文件,DLL重定向允许我们去告知一个程序所需要加载的DLL被放在另一个目录,而不在使用原始的那些。通过这种方法我们可以创建一个与原始文件同名的DLL,导出和原始DLL相同的函数,但是每个函数都可以包含我们想放置的代码。DLL重定向有两种方法,第一种方法有时被称为”dot local”重定向:

应用程序可能依赖于一个特定的DLL版本,当其他应用程序安装了旧或新的相同DLL时,该应用程序可能会运行失败。有两种方法来确保应用程序使用了正确的DLL:DLL重定向以及端对端组件。开发者和管理员应该对存在的应用程序使用DLL重定向技术,因为他无需修改应用程序。

换句话说,.local DLL重定向为开发者提供了强制一个应用程序使用特殊的不同版本DLL的能力。例如,oldapp.exe仅仅在一个旧版本的user32.dll上正常运行,你可以告诉它在自己的工作目录中加载旧版本的user32.dll来工作,而不是将这个旧版本的user32.dll放置在system32目录。其他的应用程序仍然会加载system32下的.local文件(该文件是个简单的空文件,名字是在目标应用程序的名字后加一个.local扩展;这里就是oldapp.exe.local),这个.local文件连同旧版本user32.dll都要放在oldapp.exe所在的目录下。

然而,仍然存在一些限制。最重要的,根据MSDN的说法,具体的DLL文件(称为Known DLL’s)在Windows XP中不能够重定向(Win2k没有此限制)。Known DLL’s的列表可以在KHEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs中找到;其中也包含了广为人知的kernel32.dll,user32.dll,gdi32.dll。然而以我个人经验来看,这并不是真的——似乎在Windows XP中,一个应用程序要么可以重定向所有的DLL,要么都不行。同样地,如果目标是一个Windows XP上运行的应用程序,.local重定向这种方法并不值得信赖,这种方法应该仅仅用于Windows 2000。

第二种方法,也就是我们即将要使用的方法,使用了manifest文件来达成同样的结果。manifest文件和.local文件使用相同的命名转换方式(例如,oldapp.exe.manifest),但却并不为空。他们必须包含具体的XML格式信息。此外,manifest文件也仅在Windows XP和Vista系统上被支持(Win2K不行),但相对于.local重定向来说它们更为可靠,并允许我们重定向任何DLL文件。(注意:我仅在Windows XP上验证过;Windows Vista上可能会有些限制和改动)

如何使用DLL重定向

上面提及的任一方法做重定向都相当简单,像我们后面即将看到的那样,完整的DLL重定向实现比这复杂一些。现在,我们来看一些基础:获取程序来加载当前工作目录下的DLL文件。

程序仅在被通知需要重定向时才会使用DLL重定向,通知的方法也非常简单。对.local重定向来说,program_name.exe.local文件的创建会引起应用程序在系统目录中查找DLL文件之前优先在当前工作目录中查找。这非常简单,但也正如上文提及的,在现代系统上并不可靠。

manifest文件则相对复杂,有一些必要的XML信息必须要保存在manifest文件中。下面是一个manifest文件的样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="6.0.0.0"
processorArchitecture="x86"
name="redirector"
type="win32"
/>
<description>DLL Redirection</description>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="X86"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
<file
name="user32.dll"
/>
</assembly>

我们放弃在manifest文件格式上的讨论,这和本文的主题并不直接相关(更多信息参考http://windowssdk.msdn.microsoft.com/en-gb/library/ms766454.aspx)。然而,注意``一节,这一节中我们声明了名字属性,设置它和user32.dll相同。这告诉应用程序manifest文件中指定的user32.dll需要在当前目录下加载。一旦该文件被创建,用和.local文件同样的命名方法保存名字(program_name.exe.manifest),同时也要把它放在目标程序所在的目录下。

创建Stub DLL

我们已经清楚了DLL重定向如何工作,但是像大多数事情那样,实现上还有一点点复杂:当我们重定向DLL时,我们需要让所有的函数都能够查找到。比如说我们想要拦截IE的所有MessageBox调用。MessageBox在user32.dll中,所以我们需要创建一个叫user32.dll且包含MessageBox函数的文件,同时创建一个iexplore.exe.manifest文件并把二者都放在iexplore.exe所在目录下。现在当IE导入API函数时,会加载我们的user32.dll,此后当IE调用MessageBox时,就会执行我们的函数。

问题在于MessageBox并不是IE从user32.dll导入的唯一函数。IE有着几百个从user32.dll中导入的函数,如果其中的一个找不到那么IE就会加载失败。另一方面我们也不想要重写所有的user32 DLL函数,我们可以简单的把其他的函数都转向原始user32.dll文件中的函数。

首先,我们需要用dumpbinGUI工具来查看user32.dll导出的所有函数(右击user32.dll,找到dumpbinGUI->EXPORTS)。你可能会看到类似这样的东西:

这里我没用这个花哨的GUI,直接dumpbin就行了:

现在选择所有列出的函数(ActivateKeyboardLayout开始到wvsprintfW截止),拷贝到一个user32.txt文件中,稍后我们会使用。所有的这些函数都需要从我们的DLL中导出并且定向到原始user32.dll中一致的函数。可以使用链接器导向语法来实现:

1
#pragma comment(linker, "/export:MessageBox=user33.MessageBox")

该语句使得链接器增加一个可导出的MessageBox函数到我们的DLL导出表中,并且这个导出的函数简单的被定位到user33.dll中的MessageBox函数。注意到使用了user33这个名字而不是user32。这是因为如果我们将其命名为user32.dll,我们就需要指定成user32.MessageBox,这会递归的指向本体。因此,我们拷贝原始user32.dll文件到IE所在目录下并重命名为user33.dll来防止递归。

User32不包含任何内容,除了一些按顺序的DLL导出函数。这些函数需要一些额外信息。一个函数的序号代表了出现在DLL文件中的位置;换句话说,一个序号为243的函数是DLL导出的第243个函数。为了保证我们的DLL按顺序导出函数,使得函数指向原始DLL中的正确序号,我们用这样的语法:

1
#pragma comment(linker,"/export:ord243=shlwapi32.#243,@243,NONAME")

他告诉链接器导出一个序号为243的ord243函数,指向了shlwapi32中的第243个,这里没有包含导出的名称。这里唯一的差别在于”@243”表示链接器导出函数使用的是243序列值,而”NONAME”则告诉链接器不要通过函数名称进行导出。ord243是个随机名称,当不使用名字导出时,我们写啥名字其实都不影响。

简明扼要的说,我们现在有3个文件,多放在iexplore.exe所在目录中:

  1. iexplore.exe.manifest - 指定user32.dll需要从当前目录中加载。
  2. user32.dll - 我们自己的DLL文件,内部函数指向了原始的user32.dll文件。
  3. user33.dll - 原始user32.dll文件副本,改了名称。

开始我们的表演

我们已经清楚了需要干些什么,也清楚该如何做,是时候展现真正的技术了。user32.dll有一大堆的函数,如果手工一个一个创建链接器语句实在是浪费生命。因此,我们使用一个脚本来完成这项工作,该脚本需要用到上文提到的user32.txt,它就是linkout.pl。脚本使用方法非常简单:仅需要指定保存函数列表的文本文件(user32.txt)、想要导向的DLL名称(user33)以及输出文件名(缺省使用out.txt)作为参数。

linkout.pl创建的out.txt文件长成这样:

现在我们需要的链接器导向语句全部生成好了,拷贝它们到DLL项目的CPP源文件中,编译成user32.dll。打开VS IDE创建一个新的Win32 C++ DLL Project,命名为user32。你可以删除user32.cpp中的所有预生成代码(除了#include "stdafx.h"),粘贴out.txt中所有代码到user32.cpp中,编译整个项目。

拷贝生成的user32.dll到IE所在目录,同时也拷贝原始的user32.dll文件(记得改成user33.dll)。最后,使用上文提供的模板创建一个iexplore.exe.manifest文件(IE识别.local重定向,所以你如果信任的话也可以创建一个.local文件)。启动IE,它应该会运行良好。为了测试IE加载的确实是我们的新user32.dll而不是system32下的,可以简单的重命名user33.dll成user34.dll,然后再次运行IE。此时IE应该会因下列错误而失败:

这确定了我们的DLL,使用的GetShellWindow函数是user33.GetShellWindow。

修改函数

到此我们的DLL除了转到user33.dll以外什么也没做。我们的终极目标是修改一些API调用。让我们看看还需要哪些额外的步骤去拦截修改API函数,以使用windows计算器为例。注意到计算器不识别.local重定向,所以必须要使用manifest文件。

创建一个calc.exe的拷贝,使用OD打开。因为我们已经有了一个user32 DLL,让我们找找在user32.dll中被调用到的API函数。第一个遇到的调用是GetProcessDefaultLayout。

再次打开user32 DLL工程,注释掉GetProcessDefaultLayout一行。增加下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
__declspec(naked) void myGetProcessDefaultLayout(void)
{
HINSTANCE handle;
FARPROC function;
DWORD retaddr;
__asm{
pop retaddr
}
handle = LoadLibraryA("user33.dll");
if(!handle){
MessageBoxA(NULL, "Failed to load user33.dll!", "Error", MB_OK | MB_ICONERROR);
ExitProcess(0);
}
function = GetProcAddress(handle, "GetProcessDefaultLayout");
if(!function){
MessageBoxA(NULL, "Failed to load GetProcessDefaultLayout!","Error",MB_OK | MB_ICONERROR);
ExitProcess(0);
}
MessageBoxA(NULL, "GetProcessDefaultLayout called!","Hooked!",MB_OK);
__asm{
call far dword ptr function
push retaddr
retn
}
}

在函数中,我们先pop出栈上的返回地址到retaddr变量中;我们可以通过LoadLibrary和GetProcAddress来找到真正的GetProcessDefaultLayout地址。此后,我们创建一个消息框,提供我们以虚拟的确认以表示我们成功的拦截了API调用。最后,我们调用真实的GetProcessDefaultLayout函数(call far dword ptr function),将返回地址压入到栈上并返回。我们也可以修改传递给GetProcessDefaultLayout的参数以及它的返回值。

为了导出我们的新函数,我们需要一个DEF文件。DEF文件可以用于创建一个导出函数的列表,并且可以定义导出的函数名。在工程目录下创建一个user32.def文件并写入下面的内容:

1
2
3
4
LIBRARY user32.dll
EXPORTS
GetProcessDefaultLayout=myGetProcessDefaultLayout

这回告诉链接器去导出myGetProcessDefaultLayout函数作为GetProcessDefaultLayout。把DEF文件加入到项目中(项目属性->配置属性->链接器->输入->模块定义文件)并编译新的user32项目。

拷贝创建的user32.dll,user33.dll和calc.exe.manifest到calc.exe所在的目录。运行计算器,你会看到这样一个消息框:

DLL重定向很有用,然而,试图拦截一些关键DLL如user32,kernel32中的函数调用会导致应用程序的不稳定。尽管所有的函数都转向了原始的函数,但也可能引起一些非期望结果的情形。其他的方法,特别是IAT补丁,允许你仅仅重定向单一调用,这无疑降低了风险。结合DLL重定向和IAT补丁,你可以实现最佳的效果:重定向关键DLL中的单一函数,与此同时仍然消除了外部应用程序的需求或项目标应用程序IAT做DLL注入。

例如,你可以在应用程序执行体中找到一个早期调用的函数,放置在相关的隐藏DLL中,执行DLL重定向来拦截调用。此后你可以在传递控制权给应用程序之前,实现IAT补丁所需要的代码。

结论

DLL重定向在Windows中可以是一个很有用的工具用于控制用户空间应用程序。它允许你控制任意Windows平台下可用的API函数,因此就允许你控制已存在的程序代码(或插入新的代码到进程)而无需修改应用程序代码本身(在磁盘或内存中)。这种能力也带来了安全隐患,对于用户和软件公司来说,它可能被用于危害用户的系统,也可以绕过一些审计保护技术(time-trials,CRC校验等)。

文章目录
  1. 1. 通过DLL重定向实现API劫持
    1. 1.1. 工具和预备知识
    2. 1.2. DLL重定向是什么
    3. 1.3. 如何使用DLL重定向
    4. 1.4. 创建Stub DLL
    5. 1.5. 开始我们的表演
    6. 1.6. 修改函数
    7. 1.7. 结论
,