Windows系统的dll注⼊
⼀、什么是dll注⼊
  在Windows操作系统中,运⾏的每⼀个进程都⽣活在⾃⼰的程序空间中(保护模式),每⼀个进程都认为⾃⼰拥有整个机器的控制权,每个进程都认为⾃⼰拥有计算机的整个内存空间,这些假象都是操作系统创造的(操作系统控制CPU使得CPU启⽤保护模式)。理论上⽽⾔,运⾏在操作系统上的每⼀个进程之间都是互不⼲扰的,即每个进程都会拥有独⽴的地址空间。⽐如说进程B修改了地址为
0x4000000的数据,那么进程C的地址为0x4000000处的数据并未随着B的修改⽽发⽣改变,并且进程C可能并不拥有地址为
0x4000000的内存(操作系统可能没有为进程C映射这块内存)。因此,如果某进程有⼀个缺陷覆盖了随机地址处的内存(这可能导致程序运⾏出现问题),那么这个缺陷并不会影响到其他进程所使⽤的内存。
  也正是由于进程的地址空间是独⽴的(保护模式),因此我们很难编写能够与其它进程通信或控制其它进程的应⽤程序。
  所谓的dll注⼊即是让程序A强⾏加载程序B给定的a.dll,并执⾏程序B给定的a.dll⾥⾯的代码。注意,程序B所给定的a.dll原先并不会被程序A主动加载,但是当程序B通过某种⼿段让程序A“加载”a.dll后,程序
A将会执⾏a.dll⾥的代码,此时,a.dll就进⼊了程序A的地址空间,⽽a.dll模块的程序逻辑由程序B的开发者设计,因此程序B的开发者可以对程序A为所欲为。
⼆、什么时候需要dll注⼊
  应⽤程序⼀般会在以下情况使⽤dll注⼊技术来完成某些功能:
    1.为⽬标进程添加新的“实⽤”功能;
    2.需要⼀些⼿段来辅助调试被注⼊dll的进程;
    3.为⽬标进程安装钩⼦程序(API Hook);
三、dll注⼊的⽅法
  ⼀般情况下有如下dll注⼊⽅法:   
    1.修改注册表来注⼊dll;
    2.使⽤CreateRemoteThread函数对运⾏中的进程注⼊dll;
    3.使⽤SetWindowsHookEx函数对应⽤程序挂钩(HOOK)迫使程序加载dll;
    4.替换应⽤程序⼀定会使⽤的dll;
    5.把dll作为调试器来注⼊;
    6.⽤CreateProcess对⼦进程注⼊dll
    7.修改被注⼊进程的exe的导⼊地址表。
  接下来将详细介绍如何使⽤这⼏种⽅式完成dll注⼊。
四、注⼊⽅法详解
(⼀)、修改注册表
  如果使⽤过Windows,那么对注册表应该不会陌⽣。整个系统的配置都保存在注册表中,我们可以通过修改其中的设置来改变系统的⾏为。
  ⾸先打开注册表并定位到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows项,如下图所⽰,他显⽰了该注册表项中的条⽬。
  AppInit_DLLs键的值可以是⼀个dll的⽂件名或⼀组dll的⽂件名(通过逗号或空格来分隔),由于空格是⽤来分隔⽂件名的,因此dll⽂件名不能含有空格。第⼀个dll的⽂件名可以包含路径,但其他的dll包含的路径将被忽略。
  LoadAppInit_DLLs键的值表⽰AppInit_DLLs键是否有效,为了让AppInit_DLLs键的值有效,需要将LoadAppInit_DLLs的值设置为1。
  这两个键值设定后,当应⽤程序启动并加载User32.dll时,会获得上述注册表键的值,并调⽤LoadLibrary来调⽤这些字符串中指定的每⼀个dll。这时每个被载⼊的dll可以完成相应的初始化⼯作。但是需要注意的是,由于被注⼊的dll是在进程⽣命期的早期被载⼊的,因此这些dll在调⽤函数时应慎重。调⽤Kernel32.dll中的函数应该没有问题,因为Kernel32.dll是在User32.dll载⼊前已被加载。但是调⽤其他的dll中的函数时应当注意,因为进程可能还未载⼊相应的dll,严重时可能会导致蓝屏。
  这种⽅法很简单,只需要在注册表中修改两个键的值即可,但是有如下缺点:
1.只有调⽤了User3
2.dll的进程才会发⽣这种dll注⼊。也就是说某些CUI程序(控制台应⽤程序)可能⽆法完成dll注⼊,⽐如将dll注⼊到编译器或链接器中是不可⾏的。
    2.该⽅法会使得所有的调⽤了User32.dll的程序都被注⼊指定的dll,如果你仅仅想对某些程序注⼊dll,这样很多进程将成为⽆辜的被注⼊着,并且其他程序你可能并不了解,盲⽬的注⼊会使得其他程序发⽣崩溃的可能性增⼤。
    3.这种注⼊会使得在应⽤程序的整个⽣命周期内被注⼊的dll都不会被卸载。注⼊dll的原则是值在需要的时间才注⼊我们的dll,并在不需要时及时卸载。
(⼆)、使⽤CreateRemoteThread函数对运⾏中的进程注⼊dll
  这种⽅法具有最⾼的灵活性,同时它要求掌握的知识也很多。从根本上说,dll注⼊技术要求⽬标进程中的⼀个线程调⽤LoadLibrary函数来载⼊我们想要注⼊的dll,由于我们不能轻易的控制别⼈进程中的线程,因此这种⽅法要求我们在⽬标进程中创建⼀个线程并在线程中执⾏LoadLibrary函数加载我们要注⼊的dll。幸运的是Windows为我们提供了CreateRemoteThread函数,它使得在另⼀个进程中创建⼀个线程变得⾮常容易。CreateRemoteThread函数的原型如下:
HANDLE WINAPI CreateRemoteThread(
_In_  HANDLE                hProcess,
_In_  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
_In_  SIZE_T                dwStackSize,
_In_  LPTHREAD_START_ROUTINE lpStartAddress,
_In_  LPVOID                lpParameter,
_In_  DWORD                  dwCreationFlags,
_Out_ LPDWORD                lpThreadId
);
  该函数与CreateThread仅仅只多出第⼀个参数hProcess,hProcess表⽰创建的新线程属于哪⼀个进程。
  参数lpStartAddress表⽰线程函数的起始地址,注意这个地址在⽬标进程的地址空间中。
  现在问题来了,我们如何调⽤让创建的线程执⾏LoadLibrary函数来加载我们要注⼊的dll呢?答案很简单:只需要创建的线程的线程函数地址是LoadLibrary函数的起始地址即可。我们都知道,每⼀个线程创建时应该指定⼀个参数只有4个字节,返回值也只是4个字节的函数即可(从汇编的⾓度看确实如此,只要保证调⽤前后栈平衡即可),⽽LoadLibrary函数就满⾜这些条件。LoadLibrary函数的原型如下:
HMODULE WINAPI LoadLibrary(
_In_ LPCTSTR lpFileName
);
  可以发现LoadLibrary函数完全满⾜上述条件,LoadLibrary的参数是dll路径的起始地址,这个参数也就是CreateRemoteThread函数的lpParameter参数。但是参数指向的地址应该是⽬标进程的地址,并且该地址处应保存被加载dll的路径字符串。但是⼀开始我们并不知道⽬标进程是否存在这样⼀个地址并且这个地址恰好保存了我们的dll的完整路径。解决这⼀问题的最保险的办法是使⽤VirtualAllocEx函数在⽬标进程中开辟⼀块内存存放我们的dll的路径。VirtualAllocEx函数的原型如下:
LPVOID WINAPI VirtualAllocEx(
_In_    HANDLE hProcess,
_In_opt_ LPVOID lpAddress,
_In_    SIZE_T dwSize,
_In_    DWORD  flAllocationType,
_In_    DWORD  flProtect
);
  VirtualAllocEx函数允许我们在⽬标进程中开辟⼀块指定⼤⼩(以字节为单位)的内存,并返回这块内存的起始地址。之后就可以⽤WriteProcessMemory函数将dll⽂件路径的数据复制到⽬标进程中。WriteProcessMemory函数的原型如下:
BOOL WINAPI WriteProcessMemory(
createprocessa_In_  HANDLE  hProcess,
_In_  LPVOID  lpBaseAddress,
_In_  LPCVOID lpBuffer,
_In_  SIZE_T  nSize,
_Out_ SIZE_T  *lpNumberOfBytesWritten
);
  在开始注⼊前,还需要确认⼀件事,就是⽬标进程使⽤的字符编码⽅式。因为我们所调⽤的LoadLibrary函数在底层实际调⽤有两种可能:
  如果⽬标程序使⽤的是ANSI编码⽅式,LoadLibrary实际调⽤的是LoadLibraryA,其参数字符串应当是ANSI编码;
  如果⽬标程序使⽤的是Unicode编码⽅式,LoadLibrary实际调⽤的是LoadLibraryW,其参数字符串应当是Unicode编码。
  这使得注⼊过程变得很⿇烦,为了减少复杂性,不妨直接使⽤LoadLibraryA或LoadLibraryW⽽不是⽤LoadLibrary函数来避免这⼀⿇烦。另外,即使使⽤的是LoadLibraryA,LoadLibraryA也会将传⼊的ANSI编码的字符串参数转换成Unicode编码后再调⽤LoadLibraryW。综上,不妨⼀致使⽤LoadLibraryW函数,并且字符串⽤Unicode编码即可。
  最后,我们可能会为获得⽬标进程中LoadLibraryW函数的起始地址⽽头疼,但其实这个问题也很简单,因为⽬标进程中函数LoadLibraryW的起始地址和我们的进程中的LoadLibraryW函数的起始地址是⼀样的。因此我们只需要⽤GetProcAddress即可获得LoadLibraryW函数的起始地址。
  经过以上漫长的分析,我们对CreateRemoteThread注⼊⽅法的原理有了较为清晰的理解,接下来我们就需要总结⼀下我们必须采取的步骤:
    (1).⽤VirtualAllocEx函数在⽬标进程的地址空间中分配⼀块⾜够⼤的内存⽤于保存被注⼊的dll的路径。
    (2).⽤WriteProcessMemory函数把本进程中保存dll路径的内存中的数据拷贝到第(1)步得到的⽬标进程的内存中。
    (3).⽤GetProcAddress函数获得LoadLibraryW函数的起始地址。LoadLibraryW函数位于Kernel32.dll中。
    (4).⽤CreateRemoteThread函数让⽬标进程执⾏LoadLibraryW来加载被注⼊的dll。函数结束将返回载⼊dll后的模块句柄。
    (5).⽤VirtualFreeEx释放第(1)步开辟的内存。
  在需要卸载dll时我们可以在上述第(5)步的基础上继续执⾏以下步骤:
    (6).⽤GetProcAddress函数获得FreeLibrary函数的起始地址。FreeLibrary函数位于Kernel32.dll中。
    (7).⽤CreateRemoteThread函数让⽬标进程执⾏FreeLibrary来卸载被注⼊的dll。(其参数是第(4)步返回的模块句柄)。
  如果不在上述步骤基础上执⾏操作,卸载dll时你需要这么做:
    (1).获得被注⼊的dll在⽬标进程的模块句柄。
    (2).重复上述步骤的第(6)、(7)两步。
  接下来给出编写的参考代码,该程序以控制台应⽤程序⽅式运⾏,并在Windows 10上测试通过。
#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"
#include "io.h"
#include "tchar.h"
//判断某模块(dll)是否在相应的进程中
//dwPID  进程的PID
//szDllPath  查询的dll的完整路径
BOOL CheckDllInProcess(DWORD dwPID, LPCTSTR szDllPath)
{
BOOL                    bMore = FALSE;
HANDLE                  hSnapshot = INVALID_HANDLE_VALUE;
MODULEENTRY32          me = { sizeof(me), };
if (INVALID_HANDLE_VALUE ==
(hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)))//获得进程的快照
{
_tprintf(L"CheckDllInProcess() : CreateToolhelp32Snapshot(%d) failed [%d]\n",
dwPID, GetLastError());
return FALSE;
}
bMore = Module32First(hSnapshot, &me);//遍历进程内得的所有模块
for (; bMore; bMore = Module32Next(hSnapshot, &me))
{
if (!_tcsicmp(me.szModule, szDllPath) || !_tcsicmp(me.szExePath, szDllPath))//模块名或含路径的名相符
{
CloseHandle(hSnapshot);
return TRUE;
}
}
CloseHandle(hSnapshot);
return FALSE;
}
//向指定的进程注⼊相应的模块
//向指定的进程注⼊相应的模块
//dwPID  ⽬标进程的PID
//szDllPath  被注⼊的dll的完整路径
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE                  hProcess = NULL;//保存⽬标进程的句柄
LPVOID                  pRemoteBuf = NULL;//⽬标进程开辟的内存的起始地址
DWORD                  dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);//开辟的内存的⼤⼩
LPTHREAD_START_ROUTINE  pThreadProc = NULL;//loadLibreayW函数的起始地址
HMODULE                hMod = NULL;//kernel32.dll模块的句柄
BOOL                    bRet = FALSE;
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))//打开⽬标进程,获得句柄
{
_tprintf(L"InjectDll() : OpenProcess(%d) failed [%d]\n",
dwPID, GetLastError());
goto INJECTDLL_EXIT;
}
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize,
MEM_COMMIT, PAGE_READWRITE);//在⽬标进程空间开辟⼀块内存
if (pRemoteBuf == NULL)
{
_tprintf(L"InjectDll() : VirtualAllocEx() failed [%d]\n",
GetLastError());
goto INJECTDLL_EXIT;
}
if (!WriteProcessMemory(hProcess, pRemoteBuf,
(LPVOID)szDllPath, dwBufSize, NULL))//向开辟的内存复制dll的路径
{
_tprintf(L"InjectDll() : WriteProcessMemory() failed [%d]\n",
GetLastError());
goto INJECTDLL_EXIT;
}
hMod = GetModuleHandle(L"kernel32.dll");//获得本进程kernel32.dll的模块句柄
if (hMod == NULL)
{
_tprintf(L"InjectDll() : GetModuleHandle(\"kernel32.dll\") failed [%d]\n",
GetLastError());
goto INJECTDLL_EXIT;
}
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");//获得LoadLibraryW函数的起始地址 if (pThreadProc == NULL)
{
_tprintf(L"InjectDll() : GetProcAddress(\"LoadLibraryW\") failed [%d]\n",
GetLastError());
goto INJECTDLL_EXIT;
}
if (!CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL))//执⾏远程线程
{
_tprintf(L"InjectDll() : MyCreateRemoteThread() failed\n");
goto INJECTDLL_EXIT;
}
INJECTDLL_EXIT:
bRet = CheckDllInProcess(dwPID, szDllPath);//确认结果
if (pRemoteBuf)
VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE);
if (hProcess)
CloseHandle(hProcess);
return bRet;
}
//让指定的进程卸载相应的模块
//dwPID  ⽬标进程的PID
//szDllPath  被注⼊的dll的完整路径,注意:路径不要⽤“/”来代替“\\”
BOOL EjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
BOOL                    bMore = FALSE, bFound = FALSE, bRet = FALSE;

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。