DLL注入浅析(上)

不少人在自学DLL注入的时候十分痛苦,笔者也是其中一位,网上的资料少之又少,或者不够详尽,或是满篇代码,无法理解。笔者打算用两篇文章来简单阐述下DLL注入原理,起一个抛砖引玉的作用。

DLL注入浅析(上):我会讲到DLL的基本原理,挂钩函数的安装,最后写一个键盘钩子。

DLL注入浅析(下):介绍广为流传的CreateRemoteThread方法,达到注入DLL的目的。

 

 

1. 什么是DLL

2. DLL的入口函数

3. 显示载入和卸载DLL

4. 使用Windows挂钩

 

 

 

1.什么是DLL

DLL通俗来讲只是一组源代码模块,每个模块包含一些可供应用程序或其他DLL调用的函数,因此在DLL中,通常是没有用来处理消息循环或穿件窗口的代码。在所有源文件编译完成后,链接器会像链接应用程序的可执行文件那样,对它们进行链接。

在应用程序(或其他DLL)调用一个DLL中的函数之前,必须将该DLL的文件映像映射到调用进程的地址空间中,我们可以使用两种方法来达到这种目地:

隐式载入和显式载入。我这里仅介绍显式载入,这在DLL注入中应用最为广泛。

 

使用过windows的朋友一定都见过Kernel32.dll,这是windows的内核dll,包含的函数用来管理内存,进程以及线程。但是为什么微软要把这些函数放在kernel32.dll这个DLL中呢?因为DLL的优势非常明显。

1.DLL可以自由的扩展功能,而无需对应用程序进行操作。

2.DLL使项目更加清晰便于管理。

3.DLL只需要被载入内存一次就可以供其他应用程序使用,节省了内存。

4.DLL促进了应用程序本地化

5.DLL有助于解决平台差异

6.DLL可用于特殊目地,比如我们将要使用的SetWindowsHookEx这类挂钩函数。

 

 

 

 

2.DLL的入口函数

  DLL有一个入口函数:

042516_0634_DLL1.png

 

(注意这个入口函数的拼写,很容易错写为DLLMain)

参数hinstDLL是这个DLL的句柄。这个值表示一个虚拟内存地址,DLL的文件映射就被映射到进程地址空间的这个位置。通常我们会把它保存在一个全局变量中。
参数fdwReason表示系统调用入口函数的原因,有四个值:
DLL_PROCESS_ATTACH
当系统第一次将一个DLL映射到进程的地址空间中时,会调用DllMain函数 ,并在fdwReason参数中传入DLL_PROCESS_ATTACH。
DLL_PROCESS_DETACH
当系统将一个DLL从进程的地址空间中撤销映射时,会调用DLL的DllMain函数,并在fdwReason参数中传入DLL_PROCESS_DETACH
DLL_THREAD_ATTACH
当进程创建一个线程的时候,系统会检查当前映射到该进程的地址空间中的所有DLL文件映射,并用DLL_PROCESS_DETACH来调用每个DLL的DllMain函数。
DLL_THREAD_DETACH
如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。

为了节省篇幅,这四个值的更为详细的内容这里不累赘,附上MSDN地址,各位自己研究
https://msdn.microsoft.com/zh-cn/windows/ms682583%28v=vs.100%29
参数lpvReserved保留,没有太多用得上的地方,详情仍见MSDN。

 

 

 

 

3.显式载入和卸载DLL

任何时候,进程中的一个线程可以调用下面这两个函数来将一个DLL映射到进程的地址空间:

LoadLibrary ()

042516_0634_DLL3.png

 

参数lpFileName是要载入的DLL名。
LoadLibraryEx ()

 
 
 
 
 
 

042516_0634_DLL2.png

参数hFile保留,填为NULL
参数 dwFlags指定了载入DLL的动作,如果为NULL,将与LoadLibrary没有区别,具体参数请查阅MSDN
https://msdn.microsoft.com/en-us/library/windows/desktop/ms684179%28v=vs.85%29.aspx

 
卸载DLL使用FreeLibrary(Ex)函数

 

 

一旦显示的载入了一个DLL模版,线程必须使用下面函数老得到它想要引用的函数:

042516_0634_DLL4.png

参数hModule指定了DLL的句柄,它是之前调用LoadLibrary(Ex),LoadPackagedLibrary或者GetModuleHandle的返回值。
参数lpProcname是你要引用的函数或者变量的名字。

 

 

 

4.使用Windows挂钩

 

首先介绍SetWindowsHookEx()函数:

042516_0634_DLL5.png

参数idHook是钩子类型
参数lpfn是钩取过程
参数hMod是钩取过程所属的DLL句柄,一般来说就是该DLL的句柄
参数dwThreadId是要挂钩的线程ID,如果是0,就将安装一个全局钩子

 
 
 
 
 
 
 
 
有了SetWindowsHookEx()函数,我们就可以把KeyboardProc()添加到键盘钩链

例如:

SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, threadId);

 

我们再来看一下KeyboardProc()函数:

042516_0634_DLL6.png

如果code小于0,则必须让KeyboardProc返回CallNextHookEx()
如果code等于0,则wParam和lParam包含按键消息
如果code等于3,wParam和lParam包含按键消息,并且按键消息不能从消息队列中移除
参数wParam代表一个虚拟值,对应每一个键盘按键
参数lParam 是一个长为32位的值:
其0 – 15位 代表按下键盘次数
其16 – 23位 代表扫描码(详细内容自行查阅资料)
其 24 位 如果是1,代表按键是扩展按键。如果是0,代表是小键盘数字按键。
其 25 – 28位 保留
其 29 位 如果是1,代表ALT键被按下
其 30 位 在消息发送前键被按下则为1,键松开则为0
其 31 位 有键被按下则为1,否则为0

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
安装好键盘钩子后,只要指定进程发生键盘输入事件,OS就会强制将我们的这个DLL注入到相应进程,键盘事件将由KeyboardProc接管。

我们将这个dll命名为KeyHook.dll

 

这个DLL代码如下:

 

// KeyHook.cpp : 定义 DLL 应用程序的导出函数。

//

 

#include "stdafx.h"

#include <stdio.h>

#include <Windows.h>

#include <iostream>

#include <string>

 

using namespace std;

 

#define DEF_PROCESS_NAME "notepad.exe"

 

 

 

 

HINSTANCEg_hInstance = NULL;

HHOOKg_hHook = NULL;

HWNDg_hWnd = NULL;

 

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)

{

switch (dwReason) //通常使用switch分别处理每种情况

{

case DLL_PROCESS_ATTACH:

//将DLL句柄传递给g_hInstance

g_hInstance =hinstDLL;

break;

case DLL_PROCESS_DETACH:

break;

}

return TRUE;

}

 

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)

{

if (!(lParam & 0x80000000))/与16进制80000000做与运算是验证最高位是否为1,最高位为1代表键被按下

 

{

MessageBoxA(NULL, "I captured your input", "Hook", MB_OK);

 

//如果按下了键,则return 1,终止KeyboardProc函数,这意味着截获且删除信息。

return 1;

 

}

 

 

return CallNextHookEx(g_hHook, nCode, wParam, lParam);

}

 

#ifdef __cplusplus

extern "C" {

#endif // __cplusplus

__declspec(dllexport) voidHookStart()

{

HWND hDll = NULL;

DWORD threadId = 0;

hDll = FindWindow(NULL,TEXT("无标题 - 记事本")); //使用FindWindow函数找到记事本的句柄

threadId =GetWindowThreadProcessId(hDll, NULL); //找到记事本的线程ID

if (threadId ==0) printf("SetHookfailed!!!\n");

else

{

printf("SetHook succeed!!!\n");

g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstance, threadId); //为记事本安装钩子

}

}

__declspec(dllexport) voidHookStop()

{

if (g_hHook)

{

UnhookWindowsHookEx(g_hHook);   //卸载钩子

g_hHook =NULL;

}

}

#ifdef __cplusplus

}

#endif

 

 

 

 

几点补充:

__declspec(dllexport)修饰符说明了接下来的函数或者变量是要导出DLL的函数或者变量,也就是我们在应用程序中载入DLL后,可以使用这些函数和变量,因为它们被导出了。

 

extern “C”  C++编译器通常会对函数名和变量名进行改编,当我们打算在可执行文件里面调用这个函数时,链接器会发现可执行文件引用了一个不存在的函数并报错。用extern “C” 告诉编译器不要对函数名或者变量名进行改编。

 

至于#ifdef  #endif的用法,各位自行查找资料。

 

下面简述下这段代码的流程:

当DLL被载入时,首先执行DllMain,将DLL句柄传给了g_hInstance。

当 应用程序调用HookStart时,执行DLL中HookStart里面的代码,首先获取空白记事本的句柄,通过句柄再得到线程ID,再使用 SetwindowsHookEx为该线程安装钩子,调用KeyboradProc函数,KeyboardProc函数检测到记事本发生键盘输入事件后将 拦截下键盘消息,并弹出对话框(注意,当在记事本内双击鼠标时,也会使lParam的最高位被设置为1)。当应用程序调用HookStop时,卸载钩子, 结束。

 

 

 

 

 

我们已经写好了DLL,接下来就要写调用该DLL的程序

 

这个程序很好实现:
1.载入DLL
2.调用HookStart
3.调用HookStop
4.结束

 

// HookMain.cpp : 定义控制台应用程序的入口点。

//

 

#include "stdafx.h"

#include <stdio.h>

#include <Windows.h>

 

#define DEF_DLL_NAME "KeyHook.dll"

#define DEF_HOOKSTART "HookStart"

#define DEF_HOOKSTOP "HookStop"

 

//定义两个空的函数指针数据类型,具体原因下面会解释,不懂typedef语法的请自行查阅相关资料

typedef void(*PFN_HOOKSTART)();

typedef void(*PFN_HOOKSTOP)();

 

int main()

{

HMODULE hDll= NULL;  //DLL的地址

PFN_HOOKSTART LetStart = NULL; //申明两个函数指针

PFN_HOOKSTOP LetStop = NULL;

char ch = 0;

 

hDll =LoadLibraryA(DEF_DLL_NAME);  //使用LodLibrary载入DLL,取得句柄

 

//使用GetProcAddress得到DLL导出的函数HookStart和HookStop

LetStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);

LetStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);

 

LetStart();  //执行DLL中的HookStart

 

printf("press 'q' to quit!\n");

while (getchar()!= 'q');

 

LetStop();  //执行DLL中的HookStop

 

FreeLibrary(hDll);  //卸载DLL

}

 

 

这段代码很容易理解,唯一需要注意的是,在能够使用GetProcAddress返回的函数指针来调用函数之前,我们需要将它转型为与函数的签名(结构)相匹配的正确类型。

 

例如若想调用kernel32.dll中的GetNativeSystemInfo函数。我们查看下这个函数结构
7

那我们就该定义一个这样的类型,与函数签名(结构)相符合,仔细观察这个定义

typedef void (WINAPI *PGNSI)(LPSYSTEM_INFO);

然后申明一个函数指针

PGNSI pGNSI;

再使用GetProcAddress得到函数地址,记住要用(PGNSI)转换类型

pGNSI = (PGNSI) GetProcAddress(
      GetModuleHandle(TEXT("kernel32.dll")), 
      "GetNativeSystemInfo");

在这里我们的HookStart和HookStop同理,读者可以自行比较下KeyHook中函数签名和HookMain中定义的类型是否相符。

最后提醒一点:32位DLL和程序只能注入同样32位程序,同理,64位DLL和程序也只能注入64位程序,否则会发生注入失败或者其他意想不到的结果。

我们运行HookMain.exe,结果十分满意

8

 



发表评论

发表评论

*

沙发空缺中,还不快抢~