Windows消息钩取

Hook

Hook,钩子,泛指钓取所需东西而使用的一切工具

“钩子”的概念
假设有一个非常重要的军事设施,其外围设置了3层岗哨以进行保护。外部人员若想进入,需要经过3层岗哨复杂的检查程序(身份确认、随身物品查验、访问事由说明等)。若间谍在通往该军事设施的道路上私设一个岗哨,经过该岗哨的人员未起疑心,通
过时履行同样的检查程序,那么间谍就可以坐享其成,轻松获取(甚至可以操纵)来往该岗哨的所有信息。

消息钩子

Windows操作系统向用户提供GUI,它以事件驱动方式工作。在操作系统重借助键盘,鼠标,选择彩蛋,按钮,以及移动鼠标。改变窗口大小与位置等都是事件(Event)。发生这样的时间后,OS会把事先定义消息发送给相应的应用程序,应用程序分析收到的信息后执行相应动。也就是说,敲击键盘时,消息会从OS移动到应用程序。

常规Windows消息流

  • 发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS messsage queue]。
  • OS判断哪个应用程序中发生了事件,然后从[OS message queue]中
  • 应用程序监视自身的[application message queue],发现新添加的WM_KEYDOWN消息后,调用相应的事件处理程序处理

所谓的“消息钩子

img
HHOOK SetWindowsHookEx(
    int idHook,                        // hook type
    HOOKpROC lpfn,                // hook procedure
    HINSTANCE hMod,                //hook procedure所属的DLL句柄
    DWORD dwThreadId            //需要挂钩的线程ID,为0时表示为全局钩子(Global Hook)
);

hook proceduce是由操作系统调用的回调函数;安装消息钩子时,钩子过程需要存在于某个DLL内部,且该DLL的示例句柄即为hMod。

使用SetWindowsHookEx()设置好钩子后,在某个进程中生成指定消息时,OS就会将相关的DLL文件强制注入(injection)相应进程,然后调用注册的钩子程序。

dwThreadID为0,为全局钩子

通过一个hookmain.exe实现对将要注入的dll控制


//HookMain.cpp
 
#include <stdio.h>
#include <conio.h>
#include <windows.h>
 
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
 
typedef void (*PFN_HOOKSTART)();
typedef void (*PFN_HOOKSTOP)();
 
int main()
{
    HMODULE hDll=NULL;
    PFN_HOOKSTART HookStart=NULL;
    PFN_HOOKSTOP HookStop=NULL;
    char ch=0;
 
    //加载KeyHook.dll
    hDll=LoadLibrary(DEF_DLL_NAME);
 
    //获取导出函数地址
    HookStart=(PFN_HOOKSTART)GetProcAddress(hDll,DEF_HOOKSTART);
    HookStop=(PFN_HOOKSTART)GetProcAddress(hDll,DEF_HOOKSTOP);
 
    //开始钩取
    HookStart();
 
    //等待直到用户输入“q”
    printf("press 'q' to quit!\n");
    while(_getch()!='q');
 
    //终止钩取
    HookStop();
 
    //卸载KeyHook.dll
    FreeLibrary(hDll);
    return 0;
}

先加载KeyHook.dll文件,然后调用HookStart开始钩取

调用HookStop终止钩取

keyhook.dll源码

#include "main.h"
#include <winnt.h>
#include <windef.h>
 
// a sample exported function
#define DEF_PROCESS_NAME "notepad.exe"
HINSTANCE g_hInstance=NULL;
HHOOK g_hHook=NULL;
HWND g_hWnd=NULL;
 
// a sample exported function
//LRESULT是一个数据类型,指的是从窗口程序或者回调函数返回的32位值
//lParam wParam 是宏定义,一般在消息函数中带着两个类型的参数,通常用来存储窗口消息的参数。wParam用来存储小段消息,如标志。lParam 通常用于存储消息所需的对象。
LRESULT CALLBACK KeyboardProc(int nCode,WPARAM wParam,LPARAM lParam)
{
    char szPath[MAX_PATH]={0,};
    char *p=NULL;
 
    if (nCode>=0)
    {
        //0=key press,1=key release
        if (!(lParam&0x80000000))//释放键盘按键时
        {
            GetModuleFileNameA(NULL,szPath,MAX_PATH);
            p=strrchr(szPath,'\\');//查找字符在指定字符串中从左面开始的最后一次出现的位置
 
            //比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序(或下一个“钩子”)
            if (!_stricmp(p+1,DEF_PROCESS_NAME))
                return 1;
        }
 
    }
 
    //若非notepad.exe,则调用CallNextHookEx()函数,将消息传递给应用程序(或下一个“钩子”)。
    return CallNextHookEx(g_hHook,nCode,wParam,lParam);
 
}
 
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            // attach to process
            // return FALSE to fail DLL load
            g_hInstance=hinstDLL;
            break;
 
        case DLL_PROCESS_DETACH:
            // detach from process
            break;
 
        case DLL_THREAD_ATTACH:
            // attach to thread
            break;
 
        case DLL_THREAD_DETACH:
            // detach from thread
            break;
    }
    return TRUE; // succesful
}
#ifdef __cplusplus
extern "C"
{
#endif
 
 
    __declspec(dllexport) void HookStart()
    {
        //钩子类型、回调函数地址、实例句柄、线程ID
        g_hHook=SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,g_hInstance,0);
    }
 
    __declspec(dllexport) void HookStop()
    {
        if(g_hHook)
        {
            UnhookWindowsHookEx(g_hHook);
            g_hHook=NULL;
        }
    }
 
#ifdef __cplusplus
}
#endif

在调用导出函数HookStart 时,SetWindowsHookEx就会将KeyboardProc添加到键盘钩链

KeyboardProc的定义

LRESULT CALLBACK KeyboardProc(
	int code;   //HC_ACTION(0) HCNOREMOVE(3)
    WPARAM wParam,//Virtual-key code
    LPARAM lParam//extra information
);

其中wParam指用户按下的键盘按键的虚拟键值,对键盘这一硬件而> .>言,"A"与’a’虚拟键值是一样的,大小写与lParam的位有关系,使用ToAscii()API函数可以获得实际按下的键盘的ASCII值

安装好键盘钩子后,无论哪个进程,只要发生键盘输入事件,OS就会强制将KeyHook.dll注入相应进程,加载了KeyHook.dll的进程,发生键盘事件后就会首先执行KeyHoook.keyboardProc

KeyboardProc中发生键盘输入事件时就会比较当前进程的名称与notepad进程名称是否相同,相同则返回1,终止KeyboardProc函数,等价于截获并删除信息,这样键盘消息就不会传递到notepad的消息队列

除此之外,当当前进程的名称不是notepad.exe 执行return callNextHookEx语句,消息会被传递到另一个应用程序或钩链的另一个钩子函数

恶意键盘记录器

事实上早期软件恶意键盘器的工作原理与上面的是类似的,只不过构建的钩子链条是不同的,更加复杂

常见的非法用途

  • 在网吧安装好键盘记录器,记录游戏账号密码进行非法买卖,或者清洗游戏币与装备
  • 记录网上银行的账户信息
  • 记录关键服务器的账户密码,窃取机密

比较隐蔽的键盘记录器是硬件版的键盘记录器

内部结构像一个USB存储器,连接在键盘线缆的末端从而设置到PC

其内部有一个flash memory 能够直接接收并存储来自键盘的电子信号

最新产品还支持wifi无线连接,能够更方便地传送键盘输入信息。

虽然安装起来有些不方便,但一旦成功,就很难发现

键盘记录器作者还会使用一些手段对各种安全软件进行绕过

DLL注入

DLL注入指的是向运行中的其他进程强制插入特定的DLL文件。从技术细节来说,DLL注入命令其他进程自行调用LoadLibrary() API,加载(Loading)用户指定的DLL文件。DLL注入与一般DLL加载的区别在于,加载的目标进程是其自身或其他进程。下图描述了DLL注入的概念。

image-20240721150359091

myhack.dll已被强制插入notepad进程(本来notepad并不会加载myhack.dll )。加载到notepad.exe进程中的myhack.dll与已经加载到notepad.exe进程中的DLL(kemel32.dll、user32.dll) —样,拥有访问notepad.exe进程内存的(正当的)权限,这样用户就可以做任何想做的事了(比如:向notepad添加通信功能以实现Messenger、文本网络浏览器等)。
DLL被加载到进程后会自动运行DllMain()函数,用户可以把想执行的代码放到DllMain()函数,每当加载DLL时,添加的代码就会自然而然得到执行。利用该特性可修复程序Bug,或向程序添加新功能。

BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD dwReason.LPVOID lpvReserved)
{
    switch(dwReason)
    {
        case DLL_PROCESS_ATTACH:
            //想执行的代码
            break;
        case DLL_THREAD_ATTACH:
            break;
        case DLL_THREAD_DETACH:
            break;
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

使用DLL注入技术的一些实例

  • 改善功能与修复bug
  • 消息钩取
  • API钩取
  • 监视管理PC用户的应用程序,禁止访问有害网站等
  • 执行恶意代码
  • 游戏破解修改

DLL注入有很多种方法

远程线程注入

DLL注入_远程线程注入_dll注入远程线程创建实验原理5-6行-CSDN博客

这个方法是WINDOWS核心编程提到的方法

  • 线程注入,是通过开启远程线程的方式,将DLL加载到目标宿主进程中的常用方式。

  • 静态链接库:在运行的时候就直接把代码全部加载到程序中,调用方式比如

    #prama comment (lib,"Psapi.lib")
  • 动态链接库:在需要的时候加载,加载方式为:

    ——使用LoadLibrary动态加载DLL

    ——使用GetProcAddress获取DLL中导出函数的指针

    ——最后用FreeLibrary卸载指定的DLL

在Visual Studio编译环境下,DLL又分为三类:

① 非MFC的DLL——使用SDK API进行编程,能被所有语言调用

② MFC规则DLL——使用MFC进行编程,能被所有语言调用

③ MFC扩展DLL——使用MFC进行编程,只能被MFC编写的程序调用

(下面使用第一种)

MFC——Microsoft Foundation Class-Library :微软用C++对API进行的封装,全部封装成了类,方便使用

①使用LoadLibrary加载进所需DLL

HMODULE hMod = LoadLibrary(DLL路径)

②定义导入函数指针

typedef int(*ADD_IMPORT) (int a,int b); // 定义一个指向返回值为int类型的函数指针

③使用GetProcAddress获得函数入口点

ADD_IMPORT add_proc = (ADD_IMPORT) GetProcAddress(hMod,“Add”);

④然后就可以使用了:比如 int result = add_proc(1,2);

关于DLL入口主函数第二个参数ul_reason_for_call,即DLL四种当前的状态:

DLL的编写是按照上文模版

注入的可行性

①kernel32和user32是两个在大部分程序上都会调用的DLL

②同一个DLL,在不同的进程中,不一定被映射(加载)到同一个内存地址

③但是kernel32和user32除外,他们总是被映射到进程的内存首选地址

④因此在所有使用这两个DLL的进程中,这两个DLL的内存地址是相同的

⑤所以在本进程获取的kernel32.dll中的函数的地址,在目标进程中也是一样的

流程:目标进程-传入DLL地址-开启远程线程-加载DLL-实现DLL注入

具体使用函数如下:

OpenProcess // 获取已知进程的句柄

VirtualAllocEx // 在远程进程中申请内存空间

WriteProcessMemory // 向进程中写入东西

GetProcAddress // 取得函数在DLL中的地址

CreateRemoteThreadEx // 创建远程线程——即在其他进程中创建新的线程

CloseHandle // 关闭句柄

利用以上函数即可完成线程的注入

远程注入的核心实现原理是利用了CreateRemoteThread函数,CreateRemoteThread是Windows系统的一个函数,能够在指定的进程上下文中创建一个线程。该函数可以使一个进程在另一个进程中执行任意代码,并返回新线程的句柄。在DLL注入中,我们可以使用该函数在目标进程的上下文中创建一个新线程,从而使我们的DLL代码被加载和运行。该函数的声明如下所示;

HANDLE WINAPI CreateRemoteThread(
  HANDLE                 hProcess,
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  SIZE_T                 dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID                 lpParameter,
  DWORD                  dwCreationFlags,
  LPDWORD                lpThreadId
);
  • hProcess: 目标进程的句柄。
  • lpThreadAttributes: 线程安全描述符,通常为NULL。
  • dwStackSize: 新线程的初始化栈大小,通常为0。
  • lpStartAddress: 线程入口点,指向要在新线程中执行的代码。
  • lpParameter: 传递给线程入口点的参数。
  • dwCreationFlags: 线程创建标志,通常为0。
  • lpThreadId: 如果非NULL,返回新线程的ID号。

在DLL注入中,我们可以使用它来在指定的进程上下文中执行我们的DLL代码,使其被加载和运行。这段代码的实现很容易理解,我们以注入32为DLL为例,代码如下所示;

#include <windows.h>
#include <iostream>
#include <TlHelp32.h>
#include <tchar.h>

// 传入进程名称返回该进程PID
DWORD FindProcessID(LPCTSTR szProcessName)
{
    DWORD dwPID = 0xFFFFFFFF;
    HANDLE hSnapShot = INVALID_HANDLE_VALUE;
    PROCESSENTRY32 pe;
    pe.dwSize = sizeof(PROCESSENTRY32);
    hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
    Process32First(hSnapShot, &pe);
    do
    {
        if (!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile))
        {
            dwPID = pe.th32ProcessID;
            break;
        }
    } while (Process32Next(hSnapShot, &pe));
    CloseHandle(hSnapShot);
    return dwPID;
}

// 远程线程注入
BOOL CreateRemoteThreadInjectDll(DWORD Pid, char* DllName)
{
    HANDLE hProcess = NULL;
    SIZE_T dwSize = 0;
    LPVOID pDllAddr = NULL;
    FARPROC pFuncProcAddr = NULL;

    // 打开注入进程
    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
    if (NULL == hProcess)
    {
        return FALSE;
    }

    // 得到注入文件的完整路径
    dwSize = sizeof(char) + lstrlen(DllName);

    // 在对端申请一块内存
    pDllAddr = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE);
    if (NULL == pDllAddr)
    {
        return FALSE;
    }

    // 将注入文件名写入到内存中
    if (FALSE == WriteProcessMemory(hProcess, pDllAddr, DllName, dwSize, NULL))
    {
        return FALSE;
    }

    // 得到LoadLibraryA()函数的地址
    pFuncProcAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
    if (NULL == pFuncProcAddr)
    {
        return FALSE;
    }

    // 启动线程注入
    HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFuncProcAddr, pDllAddr, 0, NULL);
    if (NULL == hRemoteThread)
    {
        return FALSE;
    }

    // 关闭句柄
    CloseHandle(hProcess);
    return TRUE;
}

int main(int argc, char *argv[])
{
    DWORD pid = FindProcessID("lyshark.exe");
    std::cout << "进程PID: " << pid << std::endl;

    bool flag = CreateRemoteThreadInjectDll(pid, (char *)"d://hook.dll");
    std::cout << "注入状态: " << flag << std::endl;

    return 0;
}

APC注入

APC注入的原理?

APC是一个简称,即“异步过程调用”。APC注入的原理是利用当线程被唤醒时,APC中的注册函数会被执行,并以此去执行我们的DLL加载代码,进而完成DLL注入的目的。在线程下一次被调度的时候,就会执行APC函数,APC有两种形式,由系统产生的APC称为内核模式APC,由应用程序产生的APC被称为用户模式APC。

其实现流程为:

1)当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断。
2)当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。
3)利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。

使用方法:

1.利用快照枚举所有的线程

2.写入远程内存,写入的是Dll的路径

3.插入我们的DLL即可

注册表注入

利用在Windows系统中,当REG中的以下键值中存在有DLL文件路径时,会跟随EXE文件的启动加载这个DLL文件路径中的DLL文件。当如果遇到有多个DLL文件时,需要用逗号或者空格隔开多个DLL文件的路径。

注册表项:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLL

REG注入就是Windows 系统在给EXE文件加载DLL文件的过程中,多加载了一个比如说AppInit_DLL中的DLL文件,这个DLL文件可以是合法的,也可以是病毒文件的DLL,这就要看使用者是怎么利用这个特性了。

img

在系统中每一个进程加载User32.dll时,会收到DLL_PROCESS_ATTACH通知,当User32.dll对其进行处理时,会取得注册表键值HKEY_LOCAL_MACHINE\Software\Microsoft\windowsNT\CurrentVresion\Windows\AppInit_Dlls,并调用LoadLibrary来载入这个字符串中指定的每个DLL。被调用的DLL会在系统调用它们的DllMain函数,并将参数fdwReason的值设为DLL_PROCESS_ATTACH时,对自己进行初始化。所以我们在这个键值中添加我们的Dll路径,即可实现注入。

注册表中默认提供了AppInit_Dlls与LoadAppInit_Dlls两个注册表项

在注册表编辑器中,将要注入的DLL路径字符串写入AppInit_Dlls项目,然后把LoadAppInit_Dlls项目值设置为1,重启后,指定DLL会注入所有运行进程。

工作原理:User32.dll被加载到进程是,会读取AppInit_DLLs注册表项,若有值,则调用LoadLibrary()API加载用户DLL,所以,严格的说,想应DLL并不会被加载到所有进程,而只是加载user32.dll的进程,Windows Xp忽略LoadAppInit.DLLs项。

注入流程:

打开注册表键值如下:
HKEY_LOCAL_MACHINE\SoftWare\MicroSoft\Windows NT\CurrentVersion\Windows\

  1. 在上面的注册表项中操作 AppInit_DLLs 键值,在该键值中添加自己的DLL的全路径加dll名,多个DLL以逗号或者空格分开(因此在文件名中应该尽量不要存在空格),该键值只有第一个dll文件名可以包含路径,后面的都不能包含,因此我们最好将dll放在系统路径 下,这样就可以不用包含路径也能被加载了。

  2. 在该注册表项中添加键值 LoadAppInit_DLLs ,类型为 DWORD,并将其值置为 1 。

ComRes注入

原理

ComRes注入的原理是利用Windows系统中C:\WINDOWS\system32目录下的ComRes.dll这个文件,当待注入EXE如果使用CoCreateInstance()这个API时,COM服务器会加载ComRes.dll到EXE中,利用这个加载过程,我们可以将ComRes.dll替换掉,并在伪造的ComRes.dll,然后利用LoadLibrary()将事先准备好的DLL加载到目标EXE中。

注意事项:

由于直接拷贝comres.dll文件到C:\WINDOWS\system32目录下会引起winows的文件系统保护机制,所以首先需要将C:\WINDOWS\system32\dllcache下的文件替换掉,然后再将其C:\WINDOWS\system32文件替换为我们伪造的文件。

ComRes注入只需伪造与替换就可以完成,编程要求不高,方便使用,但是由于加载了ComRes.dll后,再想替换ComRes.dll文件就不可能了,因此想反复测试ComRes.dll文件就比较麻烦。

劫持进程创建注入

劫持进程创建注入原理是利用Windows系统中的CreateProcess()这个API创建一个进程,并且将第六个参数设置为CREATE_SUSPENDED,进而创建一个挂起状态的进程,利用这个进程状态进行远程线程注入DLL,然后用ResumeThread()函数回复进程。

劫持进程创建注入其实就是远程线程注入的前期加强版,他可以在进程启动前进行注入,由于进程的线程没有启动,这样就可以躲过待注入进程的检测,提高注入的成功率

输入法注入

输入法注入的原理是利用Windows系统中在切换输入法需要输入字符时,系统就会把这个输入法需要的IME文件装在到当前进程中,而由于这个IME文件本质上只是个存放在C:\WINDOWS\system32目录下的特殊的DLL文件,因此我们可以利用这个特性,在IME文件中使用LoadLibrary()函数待注入的DLL文件。

如果想进行输入法注入,需要以下几步:

1、编写IME文件

输入法的IME文件其实就是个显式导出19个特殊函数的DLL文件。

img

如果想编写输入法程序,那么这19个导出函数都需要仔细的研究,但是对于只想实现注入的我们,现在只需要对ImeInquire()有比较深的认识就可以了,ImeInquire()是启动并初始化当前Ime输入法函数。

2、编写装载输入法程序

装载输入法的基本逻辑就是将他们编写的输入法设置为默认输入法,这样只要系统中所有进程都会默认加载他们的恶意输入法程序。黑客们首先需要得到系统当前的默认的输入法,以便恢复时使用。然后需要将IME文件拷贝到C:\WINDOWS\system32目录下,最后将装载成功之后的输入法设置成为默认的输入法。
3、编写卸载输入法

当新建进程不再需要注入时,就需要卸载输入法。卸载输入法时需要先判定系统当前的输入法不是其原有的默认输入法,确认无误之后将系统的默认输入法恢复后,再将恶意输入法卸载即可。

注意:

输入法注入的实现需要对输入法IME文件的生成有所了解,API使用较多,所以实现起来比较困难,单系统存在多个输入法,被注入进程很难判断当前是可信赖输入法还是用于注入的恶意输入法,所以难以阻止,大大提高了注入的几率。

可信进程注入

依赖可信进程注入原理是利用Windows系统中Services.exe这个权限较高的进程,首先先将a.dll远线程注入到Service.exe中,再利用a.dll将b.dll远线程注入的待注入进程中。

img

这里有一个小技巧,当注入到Services.exe里的DLL时,想在做完事情以后悄无声息地将自己释放掉,在Windows中可以利用API函数FreeLibraryAndExitThread(),他可以将自己卸载掉并且退出线程。

消息注入

与windows消息钩取相同