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.只有调用了User32.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(
  _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;
    HANDLE                  hSnapshot = INVALID_HANDLE_VALUE;
    HANDLE                  hProcess = NULL;
    MODULEENTRY32           me = { sizeof(me), };
    LPTHREAD_START_ROUTINE  pThreadProc = NULL;
    HMODULE                 hMod = NULL;
    TCHAR                   szProcName[MAX_PATH] = { 0, };
    if (INVALID_HANDLE_VALUE == (hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID)))
    {
        _tprintf(L"EjectDll() : CreateToolhelp32Snapshot(%d) failed!!! [%d]\n",
            dwPID, GetLastError());
        goto EJECTDLL_EXIT;
    }
    bMore = Module32First(hSnapshot, &me);
    for (; bMore; bMore = Module32Next(hSnapshot, &me))//查找模块句柄
    {
        if (!_tcsicmp(me.szModule, szDllPath) ||
            !_tcsicmp(me.szExePath, szDllPath))
        {
            bFound = TRUE;
            break;
        }
    }
    if (!bFound)
    {
        _tprintf(L"EjectDll() : There is not %s module in process(%d) memory!!!\n",
            szDllPath, dwPID);
        goto EJECTDLL_EXIT;
    }
    if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
    {
        _tprintf(L"EjectDll() : OpenProcess(%d) failed!!! [%d]\n",
            dwPID, GetLastError());
        goto EJECTDLL_EXIT;
    }
    hMod = GetModuleHandle(L"kernel32.dll");
    if (hMod == NULL)
    {
        _tprintf(L"EjectDll() : GetModuleHandle(\"kernel32.dll\") failed!!! [%d]\n",
            GetLastError());
        goto EJECTDLL_EXIT;
    }
    pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "FreeLibrary");
    if (pThreadProc == NULL)
    {
        _tprintf(L"EjectDll() : GetProcAddress(\"FreeLibrary\") failed!!! [%d]\n",
            GetLastError());
        goto EJECTDLL_EXIT;
    }
    if (!CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL))
    {
        _tprintf(L"EjectDll() : MyCreateRemoteThread() failed!!!\n");
        goto EJECTDLL_EXIT;
    }
    bRet = TRUE;
EJECTDLL_EXIT:
    if (hProcess)
        CloseHandle(hProcess);
    if (hSnapshot != INVALID_HANDLE_VALUE)
        CloseHandle(hSnapshot);
    return bRet;
}
  
int main()
{
    //InjectDll(6836, L"C:\\a.dll");
    EjectDll(6836, L"C:\\a.dll");
    return 0;
}

(三)、使用SetWindowsHookEx函数对应用程序挂钩(HOOK)迫使程序加载dll

  消息钩子:Windows操作系统为用户提供了GUI(Graphic User Interface,图形用户界面),它以事件驱动方式工作。在操作系统中借助键盘、鼠标、选择菜单、按钮、移动鼠标、改变窗口大小与位置等都是事件。发生这样的事件时,操作系统会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后会执行相应的动作。也就是说,在敲击键盘时,消息会从操作系统移动到应用程序。所谓的消息钩子就是在此期间偷看这些信息。以键盘输入事件为例,消息的流向如下:
  1.发生键盘输入时,WM_KEYDOWN消息被添加到操作系统的消息队列中;
  2.操作系统判断这个消息产生于哪个应用程序,并将这个消息从消息队列中取出,添加到相应的应用程序的消息队列中;
  3.应用程序从自己的消息队列中取出WM_KEYDOWN消息并调用相应的处理程序。
  当我们的钩子程序启用后,操作系统在将消息发送给用用程序前会先发送给每一个注册了相应钩子类型的钩子函数。钩子函数可以对这一消息做出想要的处理(修改、拦截等等)。多个消息钩子将按照安装钩子的先后顺序被调用,这些消息钩子在一起组成了"钩链"。消息在钩链之间传递时任一钩子函数拦截了消息,接下来的钩子函数(包括应用程序)将都不再收到该消息。
  像这样的消息钩子功能是Windows提供的最基本的功能,MS Visual Studio中提供的SPY++就是利用了这一功能来实现的,SPY++是一个十分强大的消息钩取程序,它能够查看操作系统中来往的所有消息。
  消息钩子是使用SetWindowsHookEx来实现的。函数的原型如下:

HHOOK WINAPI SetWindowsHookEx(
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);

  idHook参数是消息钩子的类型,可以选择的类型在MSDN中可以查看到相应的宏定义。比如我们想对所有的键盘消息做挂钩,其取值将是WH_KEYBOARD,WH_KEYBOARD这个宏的值是2。
  lpfn参数是钩子函数的起始地址,注意:不同的消息钩子类型的钩子函数原型是不一样的,因为不同类型的消息需要的参数是不同的,具体的钩子函数原型需要查看MSDN来获得。注意:钩子函数可以在结束前任意位置调用CallNextHookEx函数来执行钩链的其他钩子函数。当然,如果不调用这个函数,钩链上的后续钩子函数将不会被执行。
  hMod参数是钩子函数所在的模块的模块句柄。
  dwThreadId参数用来指示要对哪一个进程/线程安装消息钩子。如果这个参数为0,安装的消息钩子称为“全局钩子”,此时将对所有的进程(当前的进程以及以后要运行的所有进程)下这个消息钩子。注意:有的类型的钩子只能是全局钩子。
  注意:钩子函数应当放在一个dll中,并且在你的进程中LoadLibrary这个dll。然后再调用SetWindowsHookEx函数对相应类型的消息安装钩子。
  当SetWindowsHookEx函数调用成功后,当某个进程生成这一类型的消息时,操作系统会判断这个进程是否被安装了钩子,如果安装了钩子,操作系统会将相关的dll文件强行注入到这个进程中并将该dll的锁计数器递增1。然后再调用安装的钩子函数。整个注入过程非常方便,用户几乎不需要做什么。
  当用户不需要再进行消息钩取时只需调用UnhookWindowsHookEx即可解除安装的消息钩子,函数的原型如下:

BOOL WINAPI UnhookWindowsHookEx(
  _In_ HHOOK hhk
);

  hhk参数是之前调用SetWindowsHookEx函数返回的HHOOK变量。这个函数调用成功后会使被注入过dll的锁计数器递减1,当锁计数器减到0时系统会卸载被注入的dll。

  这种类型的dll注入的优点是注入简单,缺点是只能对windows消息进行Hook并注入dll,而且注入dll可能不是立即被注入,因为这需要相应类型的事件发生。其次是它不能进行其他API的Hook,如果想对其它的函数进行Hook,你需要再在被注入的dll中添加用于API Hook的代码。
  接下来将给出这一dll注入方案的示例程序的代码,代码包含两部分,一部分是dll的源文件,另一部分是控制台程序的源代码。该程序的功能是屏蔽所有notepad.exe(Windows附带的记事本程序)的按键消息,该程序在Windows xp下测试通过。

#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#pragma warning(disable : 4996)
HHOOK ghHook = NULL;
HINSTANCE ghInstance = NULL;
LRESULT CALLBACK KeyboardProc(
    _In_ int    code,
    _In_ WPARAM wParam,
    _In_ LPARAM lParam
)
{
    TCHAR szPath[MAX_PATH] = {0,};
    TCHAR sProcessName[MAX_PATH] = {0,};
    if (code == 0 && !(lParam & 0x80000000))//如果是释放按键
    {
        GetModuleFileName(NULL, szPath, MAX_PATH);
        _wsplitpath(szPath, NULL, NULL, sProcessName, NULL);
        if (0==_wcsicmp(sProcessName, L"notepad"))//如果进程名是notepad
        {
            return 1;//删除消息,不再往下传递
        }
    }
    return CallNextHookEx(ghHook, code, wParam, lParam);//继续传递消息
}
  
BOOL APIENTRY DllMain(
    HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        ghInstance = hModule;//获得本实例的模块句柄
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
  
extern "C"
{
    __declspec(dllexport) void HookStart()
    {
        ghHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, ghInstance, 0);
    }
    __declspec(dllexport) void HookStop()
    {
        if (ghHook)
        {
            UnhookWindowsHookEx(ghHook);
            ghHook = NULL;
        }
    }
}
#include <stdio.h>
#include <conio.h>
#include <tchar.h>
#include <windows.h>
typedef  void(*PFNHOOKSTART)();
typedef  void(*PFNHOOKSTOP)();
int main()
{
    HMODULE Hmod = LoadLibraryA("hookdll.dll");
    PFNHOOKSTART pHookStart = (PFNHOOKSTART)GetProcAddress(Hmod, "HookStart");
    PFNHOOKSTOP pHookStop = (PFNHOOKSTOP)GetProcAddress(Hmod, "HookStop");
    pHookStart();
    printf("print 'q' to quite!\n");
    while (_getch() != 'q');
    pHookStop();
    FreeLibrary(Hmod);
    return 0;
}

(四)、替换应用程序一定会使用的dll

  这种方法通常被编写恶意代码的人员用来编写木马,因此又被称为使用木马dll来注入dll。通常我们应当首先确认目标进程一定会载入的dll,然后替换掉它。举个例子:比如我们知道目标进程一定会载入Xyz.dll,那么我们可以创建自己的dll并与它起同样的名字。当然,我们必须将原先被替换掉的Xyz.dll改成别的名字,比如改成Xyz_1.dll。
  注意:在我们编写的Xyz.dll(将被注入的dll)内部,我们要导出原来的Xyz.dll所导出的所有符号。这一点很容易实现,可以用dll的函数转发器实现(转发到Xyz_1.dll的相同函数),这样一来我们只需要对需要HOOK(挂钩)的函数编写挂钩代码即可,这一过程我们仅仅是多了一些重复工作。看起来这个方法是完美的,并且很多木马程序经常这么干,但是它存在一个很严重的问题:如果被替换的dll后来由于程序升级导致替换的dll添加了新的导出函数,而被注入的dll并未及时添加这些新增导出函数的转发器(或者Hook程序),这将导致使用了新的导出函数的程序不能正常运行。另外,请不要随意的替换系统的dll,因为在dll注入一般应当只注入到目标进程即可,而注入到别的进程之后将带来很大的安全隐患。

(五)、把dll作为调试器来注入

  使用过OD(OllyDbg)的人员可能会为OD的强大功能感到惊叹。因为OD可以调试一个程序并任意的修改被调试的程序。OD的工作原理是向目标进程使用了调试功能。调试器可以在被调试进程中执行很多特殊操作,操作系统载入一个被调试程序的时候,会在被调试的主线程尚未开始执行任何代码前,自动通知调试器(用来调试被调试进程的进程),这时调试器可以将一些代码注入到被调试进程的地址空间中,保存被调试进程的CONTEXT结构,修改EIP指向我们注入的代码的起始位置执行这些代码。最后再让被调试的进程恢复原来的CONTEXT,继续执行。整个过程对被调试的进程而言好像没发生任何事情。
  这种注入方式需要对调试功能有所研究,并且能够对进程的CONTEXT进行操作,最后还需要对不同的CPU平台进行量身操作。此外,我们可能还需要手工编写一些汇编指令来让被调试的程序执行。这对编写人员的能力要求较高。最后,这种方法在调试器终止后,Windows会自动终止被调试的程序。不过调试器可以通过调用DebugSetProcessKillOnExit函数并传入FALSE,来改变Windows的默认行为。然后调试器就可以调用DebugActiveProcessStop函数来终止调试了。
  为什么要在主线程尚未开始执行任何代码前执行代码注入呢?因为这个时候注入最安全,其实你可以在任何时候对被调试的程序下断点并进行以上注入操作,但是为了保证被调试程序的稳定运行你可能需要做更多的工作。

(六)、用CreateProcess对子进程注入dll

  这个方法与把dll作为调试器来注入方法有许多相似之处,同样也具有较大的难度。这里要求目标进程是注入者进程的子进程。当使用CreateProcess函数来创建一个子进程时,可以选择创建后立即挂起该进程。这样,创建的子进程并不会开始执行且EIP指向ntdll.dll的RtlUserThreadStart函数的开始位置(在win10上EIP=0X76F9BA60),此时的子进程处于挂起状态。因此,我们可以有目的地修改EIP的值让其从另一个位置继续执行,但随意的修改EIP的值往往使创建的子程序崩溃。为了让创建的子进程载入dll必须调用LoadLibrary函数。在使用CreatRemoteProcess方法中也介绍了一点:必须在目标进程(这里指子进程)中写入载入的dll的完整路径。因此我们在修改EIP指向我们的代码之前需要将一部分代码注入到目标进程中。其中被注入的代码至少应包括如下操作:将dll路径首地址压栈;调用LoadLibrary函数;跳转回原先EIP位置,让程序继续执行,好像什么都没发生过。
  但是,为了程序的稳定运行,这样做还不够。注入的代码应该在执行后能恢复执行前的所有状态。因此为了注入dll需要向目标进程注入较为安全的代码应该包含如下操作
    1.保存所有寄存器的值;
    2.将dll路径首地址压栈;
    3.调用LoadLibrary函数;
    4.恢复所有寄存器的值;
    5.跳转到原先EIP位置,让程序继续执行,好像什么都没发生。
  该方法有如下优点:在程序未开始执行前执行了dll注入,一般比较难以被发现。几乎可以对所有的程序进行注入。
  该方法同样具有缺点:首先需要严谨的设计注入的代码,并根据不同的cpu平台进行设计。其次就是目标进程要是注入着创建的子进程。
  接下来将给出一段示例代码,该程序以控制台方式运行。并在Windows 10和Windows xp上测试通过。(这段代码参考自看雪论坛的IamHuskar,这里表示感谢!)

#include <windows.h>
#include <stdio.h>
#pragma warning(disable : 4996) 
  
//在子进程创建挂起时注入dll
//hProcess      被创建时挂起的进程句柄
//hThread       进程中被挂起的线程句柄
//szDllPath     被注入的dll的完整路径
BOOL StartHook(HANDLE hProcess, HANDLE hThread, TCHAR *szDllPath)
{
    BYTE ShellCode[30 + MAX_PATH * sizeof(TCHAR)] =
    {
        0x60,               //pushad
        0x9c,               //pushfd
        0x68,0xaa,0xbb,0xcc,0xdd,   //push xxxxxxxx(xxxxxxxx的偏移为3)
        0xff,0x15,0xdd,0xcc,0xbb,0xaa,  //call [addr]([addr]的偏移为9)
        0x9d,               //popfd
        0x61,               //popad
        0xff,0x25,0xaa,0xbb,0xcc,0xdd,  //jmp [eip]([eip]的偏移为17)
        0xaa,0xaa,0xaa,0xaa,        //保存loadlibraryW函数的地址(偏移为21)
        0xaa,0xaa,0xaa,0xaa,        //保存创建进程时被挂起的线程EIP(偏移为25)
        0,              //保存dll路径字符串(偏移为29)
    };
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_ALL;
    if (!GetThreadContext(hThread, &ctx))
    {
        printf("GetThreadContext() ErrorCode:[0x%08x]\n", GetLastError());
        return FALSE;
    }
    //在目标进程内存空间调拨一块可执行的内存
    LPVOID LpAddr = VirtualAllocEx(hProcess, NULL, 30 + MAX_PATH * sizeof(TCHAR), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (LpAddr == NULL)
    {
        printf("VirtualAllocEx() ErrorCode:[0x%08x]\n", GetLastError());
        return FALSE;
    }
    //获得LoadLibraryW函数的地址
    DWORD LoadDllAAddr = (DWORD)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
    if (LoadDllAAddr == NULL)
    {
        printf("GetProcAddress() ErrorCode:[0x%08x]\n", GetLastError());
        return FALSE;
    }
    printf("原始EIP=0x%08x\n", ctx.Eip);
    //写入dllpath
    memcpy((char*)(ShellCode + 29), szDllPath, MAX_PATH);
    //写入push xxxxxxxx
    *(DWORD*)(ShellCode + 3) = (DWORD)LpAddr + 29;
    //写入loadlibraryA地址
    *(DWORD*)(ShellCode + 21) = LoadDllAAddr;
    //写入call [addr]的[addr]
    *(DWORD*)(ShellCode + 9) = (DWORD)LpAddr + 21;
    //写入原始eip
    *(DWORD*)(ShellCode + 25) = ctx.Eip;
    //写入jmp [eip]的[eip]
    *(DWORD*)(ShellCode + 17) = (DWORD)LpAddr + 25;
    //把shellcode写入目标进程
    if (!WriteProcessMemory(hProcess, LpAddr, ShellCode, 30 + MAX_PATH * sizeof(TCHAR), NULL))
    {
        printf("WriteProcessMemory() ErrorCode:[0x%08x]\n", GetLastError());
        return FALSE;
    }
    //修改目标进程的EIP,执行被注入的代码
    ctx.Eip = (DWORD)LpAddr;
    if (!SetThreadContext(hThread, &ctx))
    {
        printf("SetThreadContext() ErrorCode:[0x%08x]\n", GetLastError());
        return FALSE;
    }
    printf("修改后EIP=0x%08x\n", ctx.Eip);
    return TRUE;
};
  
int main()
{
    STARTUPINFO sti;
    PROCESS_INFORMATION proci;
    memset(&sti, 0, sizeof(STARTUPINFO));
    memset(&proci, 0, sizeof(PROCESS_INFORMATION));
    sti.cb = sizeof(STARTUPINFO);
    wchar_t ExeName[MAX_PATH] = L"C:\\aimprocess.exe";//子进程的名字及启动参数
    wchar_t DllName[MAX_PATH] = L"C:\\hookdll2.dll";//被注入的dll的完整路径
    if (CreateProcess(NULL, ExeName, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sti, &proci) ==NULL)
    {
        printf("CreateProcess() ErrorCode:[0x%08x]\n", GetLastError());
        getchar();
        return 0;
    }
    if (!StartHook(proci.hProcess, proci.hThread, DllName))
    {
        TerminateProcess(proci.hProcess, 0);
        printf("Terminated Process\n");
        getchar();
        return 0;
    }
    ResumeThread(proci.hThread);
    CloseHandle(proci.hProcess);
    CloseHandle(proci.hThread);
    return 0;
}

  现在对以上代码做分析,程序首先调用CreateProcess函数来创建一个挂起的进程。创建成功后,prosic结构体保存了子进程的进程句柄和主线程的线程句柄。接下来调用StartHook函数进行代码注入。
  现在我们来详细地分析StartHook函数,首先它创建了一段ShellCode,ShellCode的内容将被会复制到目标进程的空间中。但是当前的ShellCode还不能正常工作。因为它的很多数据要依靠放入目标进程的地址来决定。ShellCode实际上是一段汇编代码后面附带了执行这段代码所需的变量或数据。所有的汇编代码已在注释当中进行标注。ShellCode数组的长度由汇编代码长度和变量的长度的总和。
  接下来的工作是修复ShellCode中部分汇编指令引用的地址,这些地址要以目标进程写入的地址作为基础偏移量。那么我们首先应该用VirtualAllocEx在目标进程的空间中调拨一块可执行的物理内存用来保存ShellCode代码。当然,LoadLibraryW函数的地址还是要从本进程中获得。当对ShellCode数据修改完毕后,就可以将ShellCode通过WriteProcessMemory函数将ShellCode复制到目标进程中。接下来需要修改目标进程的EIP指针来使主线程从ShellCode的开始处。最后,恢复目标进程,让其继续运行即可。
  通过以上分析,对上述代码的执行步骤做如下总结
    1.创建一个挂起的子进程作为目标进程;
    2.准备一份预先设计好的ShellCode(应具有上面所述的基本功能);
    3.用VirtualAllocEx在目标进程中调拨一块可执行的内存;
    4.以分配的内存为基准修复ShellCode的汇编代码引用的地址和数据;
    5.用WriteProcessMemory函数将修复完毕的ShellCode复制目标进程在第3步分配的内存中;
    6.修改目标进程的主线程的EIP指向第3步分配的内存的首地址;
    7.恢复目标进程的主线程。
  此方法的难点是设计好ShellCode代码,这需要编写者具有较高的汇编和分析设计能力。


 

作者:J坚持C
欢迎任何形式的转载,但请务必注明出处。
限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

 

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Ring3下注入DLL的另类方法,能过杀软和游戏NP(源码) 注入DLL是做全局钩子或者拦截类软件都有可能用到的技术,如果做外挂的话我们也有 可能需要注入一个DLL到游戏进程中去干点什么“坏事”。 但我们知道现在要注入DLL是越 来越难了。场景1:制作火星文输入法外挂,原理是利用API HOOK拦截并修改输入法相关函 数,需要注入一个DLL到所有进程中,但是后来发现,在开启了瑞星的帐号保险箱后,用户 将不能在QQ中输入火星文。原因是瑞星保护了QQ进程,禁止对其注入DLL,解决方法是提示 用户关闭帐号保险箱 -_-| 确实是很降低用户体验的一个不是办法的办法。场景2:制作某 游戏外挂,需要注入一个DLL到游戏进程中去直接调用游戏函数完成某一功能。结果发现该 游戏有NP保护,OpenProcess打不开,创建远程线程也不行,试用其它方法也一一失败。遇 到上面的情况,高手们自然是转到Ring0下面去,使用驱动之类的办法来对付啦,不过吾等 菜鸟可就是酒井没法子了 -_-| 不过也别太灰心,凡事总会有办法的。我想我们需要一种持久的、稳定的、不容易被安 全软件屏蔽的DLL注入方法,后来发现,输入法程序就是能完成这一任务的理想人选。输入 法程序程序到底是什么?它没有自己的进程,并且在系统还没有登录时就已被加载(在欢迎 界面你也可以调出输入法),它可以在游戏中打开,也可以在控制台程序中打开,还可以在 瑞星保护下的QQ中打开,在杀软中也可以打开,这不就是我们要找的特性吗。那么,输入法 到底是什么呢?根据Windows的规定,输入法其实就是一个DLL,不过它是一个特殊的DLL, 它必须具有标准输入法程序所规定的那些接口,输入法是由输入法管理器(imm32.dll)控 制的,输入法管理器又是由user32.dll控制的。输入法在系统目录是以IME为扩展名的文件 ,当在应用程序中激活某个输入法时,输入法管理器就会在那个应用程序的进程中加载对应 的IME文件,注意,加载IME文件跟加载普通的DLL并没有本质区别,所以,可以认为,输入 法其实就是注入到应用程序中的一个DLL文件,并且,这种“注入”是不会被杀软和游戏NP 拦截的(至少目前是)。现在,我们已经有了一个注入DLL的另类方法,那就是利用输入法 。具体流程是这样,首先制作一个标准输入法文件,但是这个输入法并不完成文字输入工作 ,它的唯一任务就是用来注入DLL,所以称为“服务输入法”,然后,制作一个控制程序, 来控制服务输入法,当然最后还需要一个用于注入的目标DLL,这样一共就有3个文件。开始 工作后,控制程序首先将服务输入法安装到系统中,然后传递几个参数给服务输入法,参数 中包括了需要注入DLL文件的名称和路径,然后,控制程序将服务输入法设置为系统的默 认输入法,这样新的程序一打开,服务输入法就会注入那个程序。当然,在服务输入法安装 之前打开的程序不会被注入,这时需要向系统中的所有窗口POST一条 WM_INPUTLANGCHANGEREQUEST消息,该消息可以在指定窗口中后台激活服务输入法,这样, 系统中所有拥有窗口的进程就都被我们的服务输入法注入了。服务输入法注入程序之后,就 会根据控制程序传递过来的参数加载目标DLL,这样目标DLL也就随着服务输入法一同注入到 目标程序中了。注意服务输入法是控制程序用WM_INPUTLANGCHANGEREQUEST消息在所有窗口 中自动激活的,如果某个窗口自动激活失败,你就需要在那个窗口中手工切换到服务输入法 ,这样才能注入进去了。至于注入以后,你就可以在窗口中切换到别的输入法,这并不会影 响已经注入进去的DLL。我将这一套功能制作成一个完整的示例,你可以在以下地址下载: http://www.pen88.com/download/imehook.rar 压缩包中的第6个和第8个文件夹演示了此 功能并包含所有源代码。其中文件imedllhost09.dll就是服务输入法,运行时会被安装到系 统中,控制程序退出时会自动卸载该输入法,这样用户就不太容易察觉,你还可以重新编译 该输入法,将名称改为“中文(中国)”,这样隐蔽性更好。文件hxwdllwx.dll是演示用的 目标DLL,你可以替换成自己的DLL,然后那个exe文件就是控制程序了。输入法 imedllhost09.dll在运行时会被复制到系统目录并更名为imedllhost09.ime,它导出了2个 函数用于控制。在VB中的声明为: Public Declare Function IMESetPubString Lib "imedllhost09.ime" (ByVal RunDLLStr As String, ByVal UnloadDll As Long, ByVal loadNextIme As Long, ByVal DllData1 As Long, ByVal DllData2 As Long, ByVal DllData3 As Long) As Long Public Declare Function IMEClearPubString Lib "imedllhost09.ime" () As Long 其中IMESetPubString用于向输入法传递要注入DLL等参数。RunDLLStr,要注入DLL命令 和完整路径。UnloadDll,当输入法退出时,是否同时卸载目标DLL 0-是,1-否。 loadNextIme,当切换至该服务输入法时,是否直接切换到下一个输入法(这样服务输入法 就好像被跳过了,可最小限度影响用户的输入法顺序) 0-否,1-是。DllData1,DllData2 ,DllData3是传递给目标DLL的回调函数(函数名称必须为RunDllHostCallBack)的参数, 你可以在目标DLL中导出一个函数,名称为RunDllHostCallBack,这样当输入法注入时会调 用目标DLL的该回调函数并向其传递这3个参数。函数原型为(VC): DWORD RunDllHostCallBack(DWORD calldata1, DWORD calldata2,DWORD calldata3); IMEClearPubString函数用于清除输入法的配置,清除后,输入法将停止在新的程序中注入 目标DLL,但已注入DLL不会卸载。 好了,利用输入法来注入DLL基本上就是这样了,详细的用法大家可以看压缩包中的第8个文 件夹,其中服务输入法是VC写的,控制程序是VB的,代码都是有注释的。测试发现该方法能 过目前所有杀软,也能注入冰刃。当然缺点还是有的,就是目标程序如果不接受输入法那就 没办法了,但是现在一般的游戏都不会禁止玩家在里面打字吧,而且杀软也不能禁止用户输 入汉字吧,哈哈,所以通用性应该还是蛮好的。 最后,我再介绍另一个注入DLL的方法,估计也很少被用到。是利用一个未公开函数 RegisterUserApiHook,可以在网上搜索关键词“RegisterUserApiHook”,查到有人在 Windows 2003下测试成功,但是我在Windows XP测试却失败。后来终于找到了失效的原因。 RegisterUserApiHook函数可以在系统中注册一个全局钩子,你需要在钩子中指定一个DLL和 一个回调函数,然后,所有加载了user32.dll的程序就都会在启动时加载你指定的这个DLL 。用这个函数来注入DLL也是很不错的。但是测试发现它的注入能力似乎赶不上上面提到的 利用输入法来注入的办法,可以注入一般的程序和某些安全程序,但是对冰刃无效。而且它 有一个限制,就是系统中只能同时存在一个这样的钩子。实际上这个钩子平时是被系统中的 Themes服务占用了,Themes服务正是利用这个钩子HOOK了绘制窗口的相关API,所以才让所 有程序窗口变成XP主题样式的。所以我们要用这个钩子的话,必须先关闭Themes服务,这样 在XP下也可以用了,但是这样系统就变成Windows 2000的样式了 -_-| RegisterUserApiHook函数的VB声明如下: Public Declare Function RegisterUserApiHookXP Lib "user32" Alias "RegisterUserApiHook" (ByVal hInstance As Long, ByVal fnUserApis As Long) As Long Public Declare Function RegisterUserApiHook2003 Lib "user32" Alias "RegisterUserApiHook" (pRegInfo As HookAPIRegInfo2003) As Long 可以看到,在XP和2003下这个函数的参数是不一样的。关于此函数的示例代码,请参见压缩 包中的第5个文件夹。 最后的最后,再介绍一个未公开函数InitializeLpkHooks,这个函数在网上能找到的资料更 少,只有一个声明而已。但是它名称中最后那个“Hooks”误导了我,我以为又是一个可以 用来注入DLL的不错函数,用OD反出来一看,原来只是个局部HOOK而已。虽然没太大用,还 是一并写上吧,也许谁用得着呢。InitializeLpkHooks顾名思义就是HOOK LPK的,Windows 有个lpk.dll,就是支持多语言包的那么个功能。测试发现好多程序在TextOut之前似乎是要 调用lpk.dll里面的相关函数的,可能是支持多语言的程序就需要用这个来判断到底要显示 那种语言吧。而InitializeLpkHooks,就是用来HOOK lpk.dll里面的4个函数的,这4个函数 是LpkTabbedTextOut,LpkPSMTextOut,LpkDrawTextEx,LpkEditControl。我们先打开VB, 在窗体中加入以下代码吧: Private Sub Form_Load() DLLhwnd = LoadLibrary("lpk.dll") '加载DLL DLLFunDre = GetProcAddress(DLLhwnd, "LpkDrawTextEx") '获取回调函数地址 LpkHooksInfo.lpHookProc_LpkTabbedTextOut = 0 LpkHooksInfo.lpHookProc_LpkPSMTextOut = 0 LpkHooksInfo.lpHookProc_LpkDrawTextEx = GetLocalProcAdress(AddressOf HookProc1) '设置要HOOK的LPK函数 LpkHooksInfo.lpHookProc_LpkEditControl = 0 InitializeLpkHooks LpkHooksInfo End Sub Private Sub Form_Unload(Cancel As Integer) LpkHooksInfo.lpHookProc_LpkTabbedTextOut = 0 LpkHooksInfo.lpHookProc_LpkPSMTextOut = 0 LpkHooksInfo.lpHookProc_LpkDrawTextEx = DLLFunDre LpkHooksInfo.lpHookProc_LpkEditControl = 0 InitializeLpkHooks LpkHooksInfo FreeLibrary DLLhwnd End Sub 然后新建一个模块,在模块中加入以下代码: Public Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long Public Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, ByVal lpProcName As String) As Long Public Declare Function FreeLibrary Lib "kernel32" (ByVal hLibModule As Long) As Long ' ----------------未公开函数-------------------------------------- Public Declare Sub InitializeLpkHooks Lib "user32" (lpProcType As Any) Type LpkHooksSetting lpHookProc_LpkTabbedTextOut As Long lpHookProc_LpkPSMTextOut As Long lpHookProc_LpkDrawTextEx As Long lpHookProc_LpkEditControl As Long End Type ' ------------------------------- Public DLLhwnd As Long, DLLFunDre As Long Public LpkHooksInfo As LpkHooksSetting Public Function GetLocalProcAdress(ByVal lpProc As Long) As Long GetLocalProcAdress = lpProc End Function Function HookProc1(ByVal a1 As Long, ByVal a2 As Long, ByVal a3 As Long, ByVal a4 As Long, ByVal a5 As Long, ByVal a6 As Long, ByVal a7 As Long, ByVal a8 As Long, ByVal a9 As Long, ByVal a10 As Long) As Long HookProc1 = 0 End Function 运行一下看看,是不是窗体中标题栏和按钮上的文字都没有了,因为我们把函数 LpkDrawTextEx替换成自己的函数HookProc1了。这个函数有10个参数,其中几个好像是字符 串指针,似乎可以用来截获窗体要显示的文字,然后改成另一种语言的文字,我猜想,也许 就是这个用途吧。哈哈,纯属猜测。以上就是函数InitializeLpkHooks的用法了。 以上就是全部。 本文所有示例代码的下载地址是: http://www.pen88.com/download/imehook.rar 我的QQ511795070,欢迎交流。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值