Windows DLL 注入技术

Windows DLL 注入技术

本文主要介绍四种常见的 Windows DLL 注入技术。

分别为全局钩子、远线程钩子、突破 SESSION 0 隔离的远线程注入和 APC 注入。

全局钩子注入

Windows 中大部分应用是基于 Windows 的消息机制,Windows提供截获这些消息的钩子函数。

根据钩子作用的不同范围,钩子可以被分为全局和局部钩子。局部钩子是针对某个线程的,全局钩子是只要有使用消息机制的应用。接下来我们主要来看下利用SetWindowsHookEx实现全局钩子。

SetWindowsHookEx函数介绍

HHOOK SetWindowsHookEx(
  int       idHook, // 要安装的钩子程序的类型
  HOOKPROC  lpfn, // 指向钩子函数的指针
  HINSTANCE hmod, // 指向钩子过程的DLL的句柄
  DWORD     dwThreadId // 与钩子进程关联的线程标识符
);
// 具体详细介绍可以查阅msdn文档,里面有更为详细的介绍

实现过程

首先我们需要创建一个windowsHookDll DLL,这个DLL在安装全局钩子后只要系统中的其他进程接收到可以发出钩子的消息,这个DLL就会被加载到此进程的地址空间中。这样便实现了Dll注入。

具体实现代码如下:

#include "Hook.h"
#include <tchar.h>

extern HMODULE g_hDllModule;
// 共享内存
#pragma data_seg("mydata") 
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:mydata,RWS")


// 钩子回调函数
LRESULT GetMsgProc(
    int code,
    WPARAM wParam,
    LPARAM lParam)
{
    return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}

// 注册钩子
BOOL SetHook()
{
    g_hHook = SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hDllModule, 0); //使用WH_GETMESSAGE 确保DLL能够注入所有的进程
    if (g_hHook)
    {
        return TRUE;
    }
    return FALSE;
}

// 卸载钩子
BOOL UnSetHook()
{
    if (g_hHook)
    {
        UnhookWindowsHookEx(g_hHook);
    }
    return TRUE;
}

试验结果

新建TestHook.exe程序,首先加载windowsHookDll.dll导出函数SetHook并调用。 在注册成功后用ProcessExplorer.exe察看explorer.exe中的加载情况。

[图片上传失败...(image-276fcd-1605431961554)]

如果想要卸载可调用函数UnSetHook,执行完成后再观察explorer.exe是否加载了windowsHookDll.dll。

远线程注入

远线程注入是指一个进程在另一个进程中创建线程的技术。主要是利用LoadLibrary在所有进程空间中的地址是一样,进程在另一个进程中创建线程时传入LoadLibrary的地址和我们要注入的DLL的路径,这样在另一个进程中就能通过LoadLibray加载DLL到进程空间中。

说起来简单,但是实现起来却有几大难点需要注意的。

  • 如何将DLL路径传给另一个进程,因为进程之间是相互隔离的,所以我们要在我们要注入的进程中申请内存。
  • 需要对打开的进程进行提权,这就要求我们的注入程序要有管理员以上的权限。

函数介绍

HANDLE OpenProcess(
  DWORD dwDesiredAccess, // 进程访问权限
  BOOL  bInheritHandle, // 子进程是否继承此句柄
  DWORD dwProcessId // 要打开的进程id
);

// 分配指定进程的内存
LPVOID VirtualAllocEx(
  HANDLE hProcess, // 进程句柄
  LPVOID lpAddress, // 要分配内存起始地址,为NULL则自动分配
  SIZE_T dwSize, // 要分配的内存大小
  DWORD  flAllocationType,// 内存分配类型
  DWORD  flProtect // 内存保护(读写)
);

// 写入内存数据
BOOL WriteProcessMemory(
  HANDLE  hProcess, // 进程句柄
  LPVOID  lpBaseAddress, // 目标进程缓冲区地址
  LPCVOID lpBuffer, // 要写入数据的地址
  SIZE_T  nSize, // 数据大小
  SIZE_T  *lpNumberOfBytesWritten // 写入数据的返回大小, 为NULL则忽略此参数
);

// 在另一个进程中创建线程
HANDLE CreateRemoteThread(
  HANDLE                 hProcess, // 进程句柄
  LPSECURITY_ATTRIBUTES  lpThreadAttributes, // 线程安全描述符
  SIZE_T                 dwStackSize, // 堆栈的初始大小
  LPTHREAD_START_ROUTINE lpStartAddress, // 函数地址(存在于另一个进程中)
  LPVOID                 lpParameter, // 函数的参数
  DWORD                  dwCreationFlags, // 线程创建标志
  LPDWORD                lpThreadId // 线程标识符
);

// 详细信息可参阅msdn

代码实现


/*dwProcessId: 目标进程的pid
**pszDllFileName: 要注入DLL的路径
*/
BOOL CreateRemoteThreadInjectDll(DWORD dwProcessId, char *pszDllFileName)
{
    HANDLE hProcess = NULL;
    SIZE_T dwSize = 0;
    LPVOID pDllAddr = NULL;
    FARPROC pFuncProcAddr = NULL;

    // 打开注入进程,获取进程句柄
    hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (NULL == hProcess)
    {
        EP_ShowError("OpenProcess");
        return FALSE;
    }
    // 在注入进程中申请内存
    dwSize = 1 + ::lstrlenA(pszDllFileName);
    pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
    if (NULL == pDllAddr)
    {
        EP_ShowError("VirtualAllocEx");
        return FALSE;
    }
    // 向申请的内存中写入数据
    if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
    {
        EP_ShowError("WriteProcessMemory");
        return FALSE;
    }
    // 获取LoadLibraryA函数地址
    pFuncProcAddr = ::GetProcAddress(::GetModuleHandleA("kernel32.dll"), "LoadLibraryA");
    if (NULL == pFuncProcAddr)
    {
        EP_ShowError("GetProcAddress_LoadLibraryA");
        return FALSE;
    }
    // 使用 CreateRemoteThread 创建远线程, 实现 DLL 注入
    HANDLE hRemoteThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL);
    if (NULL == hRemoteThread)
    {
        EP_ShowError("CreateRemoteThread");
        return FALSE;
    }
    // 关闭句柄
    ::CloseHandle(hProcess);

    return TRUE;
}

远线程注册进阶 突破SESSION 0隔离

在系统普通进程中可以使用远线程注入DLL,但如果要注入系统服务则不行,因为有SESSION 0隔离。想要突破隔离需要使用ZwCreateThreadEx函数。这也是和远线程注入的区别。

ZwCreateThreadEx 是未公开的函数,在ntdll.dll中,它的函数声明如下:

#ifdef _WIN64
    typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown);
#else
    typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown);
#endif

具体实现代码:

// 使用 ZwCreateThreadEx 实现远线程注入
BOOL ZwCreateThreadExInjectDll(DWORD dwProcessId, char *pszDllFileName)
{
    HANDLE hProcess = NULL;
    SIZE_T dwSize = 0;
    LPVOID pDllAddr = NULL;
    FARPROC pFuncProcAddr = NULL;
    HANDLE hRemoteThread = NULL;
    DWORD dwStatus = 0;

    // 打开注入进程,获取进程句柄
    hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (NULL == hProcess)
    {
        EP_ShowError("OpenProcess");
        return FALSE;
    }
    // 在注入进程中申请内存
    dwSize = 1 + ::lstrlenA(pszDllFileName);
    pDllAddr = ::VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
    if (NULL == pDllAddr)
    {
        EP_ShowError("VirtualAllocEx");
        return FALSE;
    }
    // 向申请的内存中写入数据
    if (FALSE == ::WriteProcessMemory(hProcess, pDllAddr, pszDllFileName, dwSize, NULL))
    {
        EP_ShowError("WriteProcessMemory");
        return FALSE;
    }
    // 加载 ntdll.dll
    HMODULE hNtdllDll = ::LoadLibraryA("ntdll.dll");
    if (NULL == hNtdllDll)
    {
        EP_ShowError("LoadLirbary");
        return FALSE;
    }
    // 获取LoadLibraryA函数地址
    pFuncProcAddr = ::GetProcAddress(::GetModuleHandleA("Kernel32.dll"), "LoadLibraryA");
    if (NULL == pFuncProcAddr)
    {
        EP_ShowError("GetProcAddress_LoadLibraryA");
        return FALSE;
    }
    // 获取ZwCreateThread函数地址
#ifdef _WIN64
    typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        ULONG CreateThreadFlags,
        SIZE_T ZeroBits,
        SIZE_T StackSize,
        SIZE_T MaximumStackSize,
        LPVOID pUnkown);
#else
    typedef DWORD(WINAPI *typedef_ZwCreateThreadEx)(
        PHANDLE ThreadHandle,
        ACCESS_MASK DesiredAccess,
        LPVOID ObjectAttributes,
        HANDLE ProcessHandle,
        LPTHREAD_START_ROUTINE lpStartAddress,
        LPVOID lpParameter,
        BOOL CreateSuspended,
        DWORD dwStackSize,
        DWORD dw1,
        DWORD dw2,
        LPVOID pUnkown);
#endif
    typedef_ZwCreateThreadEx ZwCreateThreadEx = (typedef_ZwCreateThreadEx)::GetProcAddress(hNtdllDll, "ZwCreateThreadEx");
    if (NULL == ZwCreateThreadEx)
    {
        EP_ShowError("GetProcAddress_ZwCreateThread");
        return FALSE;
    }
    // 使用 ZwCreateThreadEx 创建远线程, 实现 DLL 注入
    dwStatus = ZwCreateThreadEx(&hRemoteThread, PROCESS_ALL_ACCESS, NULL, hProcess, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, 0, 0, 0, NULL);
    if (NULL == hRemoteThread)
    {
        EP_ShowError("ZwCreateThreadEx");
        return FALSE;
    }
    // 关闭句柄
    ::CloseHandle(hProcess);
    ::FreeLibrary(hNtdllDll);

    return TRUE;
}

试验结果

同远线程注入一致,只是在服务程序中不能弹窗。

APC注入

APC(Asynchronous Procedure Call)为异步过程调用,APC注入是指利用线程本身的APC队列进行DLL注入。

函数介绍

// 将异步调用函数添加到指定线程的APC队列中
DWORD QueueUserAPC(
  PAPCFUNC  pfnAPC, // 函数指针
  HANDLE    hThread, // 线程句柄
  ULONG_PTR dwData // 函数参数
);

// 详细信息可参阅msdn

实现原理

APC队列中的函数需要等待线程挂起时才会被执行,所以要保证我们注入的程序能被执行需要将我们的函数插入到进程的所有线程中。具体代码实现如下:

// apc.h
#include <Windows.h>
#include <TlHelp32.h>


// 根据进程名称获取PID
DWORD GetProcessIdByProcessName(char *pszProcessName);

// 根据PID获取所有的相应线程ID
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD **ppThreadId, DWORD *dwThreadIdLength);

// APC注入
BOOL ApcInjectDll(char *pszProcessName, char *pszDllName);


//apc.cpp
#include "APC.h"


void ShowError(char *pszText)
{
    char szErr[MAX_PATH] = { 0 };
    ::wsprintf(szErr, "%s Error[%d]\n", pszText);
    ::MessageBox(NULL, szErr, "ERROR", MB_OK | MB_ICONERROR);
}


// 根据进程名称获取PID
DWORD GetProcessIdByProcessName(char *pszProcessName)
{
    DWORD dwProcessId = 0;
    PROCESSENTRY32 pe32 = { 0 };
    HANDLE hSnapshot = NULL;
    BOOL bRet = FALSE;
    ::RtlZeroMemory(&pe32, sizeof(pe32));
    pe32.dwSize = sizeof(pe32);

    // 获取进程快照
    hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (NULL == hSnapshot)
    {
        ShowError("CreateToolhelp32Snapshot");
        return dwProcessId;
    }

    // 获取第一条进程快照信息
    bRet = ::Process32First(hSnapshot, &pe32);
    while (bRet)
    {
        // 获取快照信息
        if (0 == ::lstrcmpi(pe32.szExeFile, pszProcessName))
        {
            dwProcessId = pe32.th32ProcessID;
            break;
        }

        // 遍历下一个进程快照信息
        bRet = ::Process32Next(hSnapshot, &pe32);
    }

    return dwProcessId;
}


// 根据PID获取所有的相应线程ID
BOOL GetAllThreadIdByProcessId(DWORD dwProcessId, DWORD **ppThreadId, DWORD *pdwThreadIdLength)
{
    DWORD *pThreadId = NULL;
    DWORD dwThreadIdLength = 0;
    DWORD dwBufferLength = 1000;
    THREADENTRY32 te32 = { 0 };
    HANDLE hSnapshot = NULL;
    BOOL bRet = TRUE;

    do
    {
        // 申请内存
        pThreadId = new DWORD[dwBufferLength];
        if (NULL == pThreadId)
        {
            ShowError("new");
            bRet = FALSE;
            break;
        }
        ::RtlZeroMemory(pThreadId, (dwBufferLength * sizeof(DWORD)));

        // 获取线程快照
        ::RtlZeroMemory(&te32, sizeof(te32));
        te32.dwSize = sizeof(te32);
        hSnapshot = ::CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
        if (NULL == hSnapshot)
        {
            ShowError("CreateToolhelp32Snapshot");
            bRet = FALSE;
            break;
        }

        // 获取第一条线程快照信息
        bRet = ::Thread32First(hSnapshot, &te32);
        while (bRet)
        {
            // 获取进程对应的线程ID
            if (te32.th32OwnerProcessID == dwProcessId)
            {
                pThreadId[dwThreadIdLength] = te32.th32ThreadID;
                dwThreadIdLength++;
            }

            // 遍历下一个线程快照信息
            bRet = ::Thread32Next(hSnapshot, &te32);
        }

        // 返回
        *ppThreadId = pThreadId;
        *pdwThreadIdLength = dwThreadIdLength;
        bRet = TRUE;

    } while (FALSE);

    if (FALSE == bRet)
    {
        if (pThreadId)
        {
            delete[]pThreadId;
            pThreadId = NULL;
        }
    }

    return bRet;
}


// APC注入
BOOL ApcInjectDll(char *pszProcessName, char *pszDllName)
{
    BOOL bRet = FALSE;
    DWORD dwProcessId = 0;
    DWORD *pThreadId = NULL;
    DWORD dwThreadIdLength = 0;
    HANDLE hProcess = NULL, hThread = NULL;
    PVOID pBaseAddress = NULL;
    PVOID pLoadLibraryAFunc = NULL;
    SIZE_T dwRet = 0, dwDllPathLen = 1 + ::lstrlen(pszDllName);
    DWORD i = 0;

    do
    {
        // 根据进程名称获取PID
        dwProcessId = GetProcessIdByProcessName(pszProcessName);
        if (0 >= dwProcessId)
        {
            bRet = FALSE;
            break;
        }

        // 根据PID获取所有的相应线程ID
        bRet = GetAllThreadIdByProcessId(dwProcessId, &pThreadId, &dwThreadIdLength);
        if (FALSE == bRet)
        {
            bRet = FALSE;
            break;
        }

        // 打开注入进程
        hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
        if (NULL == hProcess)
        {
            ShowError("OpenProcess");
            bRet = FALSE;
            break;
        }

        // 在注入进程空间申请内存
        pBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (NULL == pBaseAddress)
        {
            ShowError("VirtualAllocEx");
            bRet = FALSE;
            break;
        }
        // 向申请的空间中写入DLL路径数据 
        ::WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet);
        if (dwRet != dwDllPathLen)
        {
            ShowError("WriteProcessMemory");
            bRet = FALSE;
            break;
        }

        // 获取 LoadLibrary 地址
        pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"), "LoadLibraryA");
        if (NULL == pLoadLibraryAFunc)
        {
            ShowError("GetProcessAddress");
            bRet = FALSE;
            break;
        }

        // 遍历线程, 插入APC
        i = dwThreadIdLength;
        for (; i > 0; --i)
        {
            // 打开线程
            hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
            if (hThread)
            {
                // 插入APC
                ::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pBaseAddress);
                // 关闭线程句柄
                ::CloseHandle(hThread);
                hThread = NULL;
            }
        }

        bRet = TRUE;

    } while (FALSE);

    // 释放内存
    if (hProcess)
    {
        ::CloseHandle(hProcess);
        hProcess = NULL;
    }
    if (pThreadId)
    {
        delete[]pThreadId;
        pThreadId = NULL;
    }

    return bRet;
}

总结

Windows注入技术可以方便我们对目标进程作修改,但是也可能使目标进程崩溃,在使用的时候需要小心谨慎。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • 这篇文章主要介绍了Windows x86/ x64 Ring3层注入Dll总结的相关资料,需要的朋友可以参考下 0...
    03ngnntds阅读 294评论 0 0
  • 参考文献 逆向工程核心原理 Windows消息勾取 消息钩子 Windows操作系统向用户提供GUI,它以事件驱动...
    SamiraG阅读 350评论 0 0
  • DLL注入是把指定的DLL加载到另一个进程的内存空间中去。 DLL注入技术:1、通过远程线程注入 ...
    kotw_zjc阅读 601评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,518评论 16 22
  • 创业是很多人的梦想,多少人为了理想和不甘选择了创业来实现自我价值,我就是其中一个。 创业后,我由女人变成了超人,什...
    亦宝宝阅读 1,805评论 4 1