文章目录
21. windows消息钩取
偷看或操作信息的行为就是钩取(hooking),其中最基本的是消息钩取(message hooking)。
windows提供GUI,它以事件驱动(event driven)方式工作。
21.2 消息钩子
举例:
- 敲击键盘,
WM_KEYDOWN
被添加到os message queue; - os从中取出消息,添加到对应应用程序的message queue;
- 应用,比如记事本,监视自己的队列,发现新添加的
WM_KEYDOWN
,于是调用相应的事件处理程序。
可以看出,os与app之间有一条钩链(hook chain),如果在中间设置消息钩子,就能进行“中间人攻击”了。
如果同时设置多个钩子,按照顺序调用,这条链条叫做钩链。
Visual Studio提供了SPY++,这个消息钩取程序可以查看os中来往的所有消息。
21.3 SetWindowsHookEx
winuser.h
的这个api可以实现消息钩子。
HHOOK SetWindowsHookEx(
int idHook, // hook type
HOOKPROC lpfn, // hook procedure
HINSTANCE hMod, // handle to application instance
DWORD dwThreadId // thread identifier);
);
HOOKPROC
钩子过程是由系统调用的回调函数。安装钩子时,钩子过程需要存在于某个dll内部,且该dll的示例句柄(instance handle)即hMod
.
若dwThreadId
为0,则钩子为Global Hook
,影响所有进程。
设置好后,某个进程生成指定消息时,os会将相关dll注入相应进程,调用钩子过程。
GetProcAddress(), LoadLibrary()
位于libloaderapi.h
,
用以下代码生成dll,项目名KeyHook
。
//KeyHook.cpp
#include "stdio.h"
#include "windows.h"
//定义目标进程名为notepad.exe
#define DEF_PROCESS_NAME "notepad.exe"
//定义全局变量
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
//DllMain()函数在DLL被加载到进程后会自动执行
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved) {
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
g_hInstance = hinstDLL;
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
//
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
char szPath[MAX_PATH] = { 0, };
char *p = NULL;
if (nCode >= 0) {
//释放键盘按键时,bit 31 : 0 => press, 1 => release
if (!(lParam & 0x80000000)) {
GetModuleFileNameA(NULL, szPath, MAX_PATH);
p = strrchr(szPath, '\\');
//比较当前进程名称,若为notepad.exe,则消息不会传递给应用程序或下一个钩子函数
//_stricmp()函数用于比较字符串,i表示不区分大小写,若两个值相等则返回0
if (!_stricmp(p + 1, DEF_PROCESS_NAME)) {
return 1;
}
}
}
//比较当前进程名称,若非notepad.exe,则消息传递给应用程序或下一个钩子函数
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
//在C++中调用C的库文件,用extern "C"告知编译器,因为C++支持函数重载而C不支持,两者的编译规则不同
#ifdef __cplusplus
extern "C" {
#endif
//__declspec,针对编译器的关键字,用于指出导出函数
//当调用导出函数HookStart()时,SetWindowsHookEx()函数就会将KeyboardProc()添加到键盘钩链
__declspec(dllexport) void HookStart()
{
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
同一解决方案下新建项目KeyMain
,
//keyhook.h
#pragma once
#ifndef _KEYHOOK_H
#define _KEYHOOK_H
#ifdef __cplusplus
extern "C" {
#endif
void HookStart();
void HookStop();
#ifdef __cplusplus
}
#endif
#endif
//HookMain
#include "stdio.h"
#include "windows.h"
//Console Input/Output,定义了通过控制台进行数据输入和数据输出的函数
//主要是一些用户通过按键盘产生的对应操作,比如getch()函数等等
#include "conio.h"
#include "keyhook.h"
//定义一些常量
#define DEF_DLL_NAME "KeyHook.dll"
#define DEF_HOOKSTART "HookStart"
#define DEF_HOOKSTOP "HookStop"
//定义两个参数为空、返回值为void的函数指针
typedef void(*PFN_HOOKSTART)();
typedef void(*PFN_HOOKSTOP)();
void main() {
//定义及初始化句柄变量和函数指针
HMODULE hDll = NULL;
PFN_HOOKSTART HookStart = NULL;
PFN_HOOKSTOP HookStop = NULL;
//加载KeyHook.dll
hDll = LoadLibraryA(DEF_DLL_NAME);
//若加载不成功,则输出错误信息
if (hDll == NULL) {
printf("[-]无法加载%s [%d]\n", DEF_DLL_NAME, GetLastError());
return;
}
//获取导出函数地址
HookStart = (PFN_HOOKSTART)GetProcAddress(hDll, DEF_HOOKSTART);
HookStop = (PFN_HOOKSTOP)GetProcAddress(hDll, DEF_HOOKSTOP);
//开始钩取
HookStart();
//直至用户输入“q”退出钩取
printf("[*]等待输入 'q' 来停止钩取...\n");
while (_getch() != 'q');
//终止钩取
HookStop();
//卸载KeyHook.dll
FreeLibrary(hDll);
}
win10 和 记事本都是64位的情况下,编译前要把解决平台方案改为x64!!!!!!!!!
监测工具使用Process Explorer.
运行后,任何进程发生键盘输入,OS就会将KeyHook.dll注入进程,并调用KeyboardProc()
,将当前进程名称与notepad.exe
比较,若相同则返回1,终止KeyboardProc()
函数,这意味着截获并删除消息;若不是notepad.exe
,则消息被传递给下一个钩子函数。
21.5.1 调试
查找核心代码的几种方法:
- 逐行跟踪(略)
- 检索api
- 检索字符串
这里使用检索字符串方法调试KeyMain.exe。
search for - all referrenced text strings
,找到主函数,调试即可。注意根据SetWindowsHookEx()
参数锁定钩子过程函数的值。
按照以下步骤调试notepad.exe进程中的KeyHook.dll:
debugging options - events - break on new module(dll)
,有新的dll装入时会暂停调试,弹出executable modules
窗口;- 进入keyhook.dll的ep;
- 在钩子函数地址处下断点;
23. dll注入
dll注入的原理就是从外部促使目标进程调用LoadLibrary()
,使用LoadLibrary()
api加载某个dll时,dll的DllMain()
会被调用执行。被注入的dll拥有目标进程的内存访问权限。
用途:消息钩取,api钩取,恶意代码,管理员也可以用来监视用户的程序。
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved) {
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
//code
break;
case DLL_THREAD_DETACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
上面的消息钩取与常规的dll注入唯一的区别是,os会直接将已经注册的钩取dll注入目标进程。
dll注入主要有3种方法:
CreateRemoteThread() API
- 使用注册表,涉及
AppInit_DLLs
值 - 消息钩取,
SetWindowsHookEx() API
CreateRemoteThread()
DebugView工具用来捕获并显示进程输出的所有调试字符串,与ProcessExplorer同一开发者,微软官网Sysinternal部分可下载。
//myhack.cpp
#include "windows.h"
#include "tchar.h"
#pragma comment(lib,"urlmon.lib")
#define DEF_URL (L"http://www.naver.com/index.html")
#define DEF_FILE_NAME (L"index.html")
HMODULE g_hMod = NULL;
DWORD WINAPI ThreadProc(LPVOID lParam)
{
TCHAR szPath[_MAX_PATH] = {0,};
if( !GetModuleFileName(g_hMod,szPath,MAX_PATH))
{
return FALSE;
}
TCHAR *p = _tcsrchr(szPath,'\\');
if(!p)
{
return FALSE;
}
_tcscpy_s(p+1,_MAX_PATH,DEF_FILE_NAME);
URLDownloadToFile(NULL,DEF_URL,szPath,0,NULL);
return 0;
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReversed)
{
HANDLE hThread = NULL;
g_hMod = (HMODULE)hinstDLL;
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
OutputDebugString(L"myhack.dll injection!!!");
hThread = CreateThread(NULL,0,ThreadProc,NULL,0,NULL);
CloseHandle(hThread);
break;
}
return TRUE;
}
dll被加载时,会输出调试字符串"myhack.dll injection!!!",然后创建线程调用ThreadProc()
,该函数通过URLDownloadToFile() api
来下载网页。
所以,myhack.dll注入notepad.exe时会下载网页。
//InjectDll.cpp
#include "windows.h"
#include "tchar.h"
#include "stdio.h"
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL,hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
//1. get the handle of notepad.exe
if(!(hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwPID)))
{
_tprintf(L"OpenProcess(%d) failed!!![%d]\n",dwPID,GetLastError());
return FALSE;
}
//2. alloc dwBufSize bytes memory in notepad.exe,
pRemoteBuf = VirtualAllocEx(hProcess,NULL,dwBufSize,MEM_COMMIT,PAGE_READWRITE);
if (pRemoteBuf == NULL)
{
_tprintf(L"VirtualAllocEx() failed!!![%d]\n", GetLastError());
return FALSE;
}
//3. write the path of myhack.dll into the memory allocated
if (!WriteProcessMemory(hProcess, pRemoteBuf, (LPCVOID)szDllPath, dwBufSize, NULL))
{
_tprintf(L"WriteProcessMemory() failed!!![%d]\n", GetLastError());
return FALSE;
}
//4. get the address of LoadLibraryW() api from module
hMod = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod,"LoadLibraryW");
if (hMod == NULL)
{
_tprintf(L"GetModuleHandle() failed!!![%d]\n", GetLastError());
return FALSE;
}
if (pThreadProc == NULL)
{
_tprintf(L"GetProcAddress() failed!!![%d]\n", GetLastError());
return FALSE;
}
//5. run the thread in notepad.exe
hThread = CreateRemoteThread(hProcess,NULL,0,pThreadProc,pRemoteBuf,0,NULL);
WaitForSingleObject(hThread,INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
int main(int argc,char *argv[])
{
if(argc != 3)
{
//_tprintf(L"USAGE: %ls pid dll_path\n", (LPCTSTR)argv[0]);
printf("USAGE: %s pid dll_path\n", argv[0]);
return 1;
}
//if(InjectDll((DWORD)_tstol((const wchar_t*)argv[1]), (LPCTSTR)"myhack.dll") )
if (InjectDll((DWORD)atoi(argv[1]), (LPCTSTR)argv[2]))
{
//_tprintf(L"InjectDll(\"%ls\") successfully!!!\n",argv[2]);
printf("InjectDll(\"%s\") successfully!!!\n", argv[2]);
}
else
{
//_tprintf(L"InjectDll(\"%ls\") failed!!!\n",argv[2]);
printf("InjectDll(\"%s\") failed!!!\n", argv[2]);
}
return 0;
}
上面InjectDll()
的步骤:
- 获取目标进程的句柄;
- 在目标进程里开辟缓冲区;
- 往缓冲区写入dll路径;
- 获得进程中
kernel32.dll
的LoadLibraryW() api
的地址; - 运行远程线程。
目标其实就是让目标进程调用LoadLibrary()
加载dll。
windows中,kernel32.dll
在每个进程中的加载地址都是相同的。如果不同,上面的代码运行时会有内存引用错误。
windows提供了调试api,用于访问其它进程的内存空间,常用的有:VirtualAllocEx(), VirtualFreeEx(), WriteProcessMemory(), ReadProcessMemory().
第5步中,CreateRemoteThread()
原型如下:
HANDLE
WINAPI
CreateRemoteThread(
_In_ HANDLE hProcess,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
除了第一个参数,其余与CreateThread()
相同。
AppInit_DLLs
windows注册表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows
路径下提供了AppInit_DLLs
和LoadInit_DLLs
两个注册表项。
把dll路径字符串写入AppInit_DLLs
,然后把LoadInit_DLLs
设为1,重启后,某进程加载user32.dll
时,会读取AppInit_DLLs
,若有值,就调用LoadLibrary()
加载dll。
windows xp会忽略
LoadInit_DLLs
// myhack2.cpp
#include "windows.h"
#include "tchar.h"
#define DEF_CMD L"c:\\Program Files\\Internet Explorer\\iexplore.exe"
#define DEF_ADDR L"http://www.naver.com"
#define DEF_DST_PROC L"notepad.exe"
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
TCHAR szCmd[MAX_PATH] = {0,};
TCHAR szPath[MAX_PATH] = {0,};
TCHAR *p = NULL;
STARTUPINFO si = {0,};
PROCESS_INFORMATION pi = {0,};
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
switch( fdwReason )
{
case DLL_PROCESS_ATTACH :
if( !GetModuleFileName( NULL, szPath, MAX_PATH ) )
break;
if( !(p = _tcsrchr(szPath, '\\')) )
break;
if( _tcsicmp(p+1, DEF_DST_PROC) )
break;
wsprintf(szCmd, L"%s %s", DEF_CMD, DEF_ADDR);
if( !CreateProcess(NULL, (LPTSTR)(LPCTSTR)szCmd,
NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS,
NULL, NULL, &si, &pi) )
break;
if( pi.hProcess != NULL )
CloseHandle(pi.hProcess);
break;
}
return TRUE;
}
若加载该dll的进程为记事本,则以隐藏模式运行ie。
SetWindowsHookEx
见21章。
24. dll卸载
原理
CreateRemoteThread()
是让目标进程调用LoadLibrary()
,dll卸载则是让目标进程调用FreeLibrary()
。
CreateRemoteThread()
的lpStartAddress
为FreeLibrary()
地址,lpParameter
为dll句柄即可。
每个windows内核对象(kernel object)都拥有一个引用计数(reference count),
LoadLibrary()
则加一,FreeLibrary()
则减一。
实现
// EjectDll.exe
#include "windows.h"
#include "tlhelp32.h"
#include "tchar.h"
#define DEF_PROC_NAME (L"notepad.exe")
#define DEF_DLL_NAME (L"myhack.dll")
DWORD FindProcessID(LPCTSTR szProcessName)
{
DWORD dwPID = 0xFFFFFFFF;
HANDLE hSnapShot = INVALID_HANDLE_VALUE;
PROCESSENTRY32 pe;
// Get the snapshot of the system
pe.dwSize = sizeof( PROCESSENTRY32 );
hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );
// find process
Process32First(hSnapShot, &pe);
do
{
if(!_tcsicmp(szProcessName, (LPCTSTR)pe.szExeFile))
{
dwPID = pe.th32ProcessID;
break;
}
}
while(Process32Next(hSnapShot, &pe));
CloseHandle(hSnapShot);
return dwPID;
}
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;
if( !OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken) )
{
_tprintf(L"OpenProcessToken error: %u\n", GetLastError());
return FALSE;
}
if( !LookupPrivilegeValue(NULL, // lookup privilege on local system
lpszPrivilege, // privilege to lookup
&luid) ) // receives LUID of privilege
{
_tprintf(L"LookupPrivilegeValue error: %u\n", GetLastError() );
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if( bEnablePrivilege )
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;
// Enable the privilege or disable all privileges.
if( !AdjustTokenPrivileges(hToken,
FALSE,
&tp,
sizeof(TOKEN_PRIVILEGES),
(PTOKEN_PRIVILEGES) NULL,
(PDWORD) NULL) )
{
_tprintf(L"AdjustTokenPrivileges error: %u\n", GetLastError() );
return FALSE;
}
if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
{
_tprintf(L"The token does not have the specified privilege. \n");
return FALSE;
}
return TRUE;
}
BOOL EjectDll(DWORD dwPID, LPCTSTR szDllName)
{
BOOL bMore = FALSE, bFound = FALSE;
HANDLE hSnapshot, hProcess, hThread;
HMODULE hModule = NULL;
MODULEENTRY32 me = { sizeof(me) };
LPTHREAD_START_ROUTINE pThreadProc;
// dwPID = notepad PID
// use TH32CS_SNAPMODULE to get the name of DLL loaded in notepad
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
bMore = Module32First(hSnapshot, &me);
for( ; bMore ; bMore = Module32Next(hSnapshot, &me) )
{
if( !_tcsicmp((LPCTSTR)me.szModule, szDllName) ||
!_tcsicmp((LPCTSTR)me.szExePath, szDllName) )
{
bFound = TRUE;
break;
}
}
if( !bFound )
{
CloseHandle(hSnapshot);
return FALSE;
}
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) )
{
_tprintf(L"OpenProcess(%d) failed!!! [%d]\n", dwPID, GetLastError());
return FALSE;
}
//Get the address of FreeLibrary() api
hModule = GetModuleHandle(L"kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");
hThread = CreateRemoteThread(hProcess, NULL, 0,
pThreadProc, me.modBaseAddr,
0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
CloseHandle(hSnapshot);
return TRUE;
}
int main(int argc, char* argv[])
{
DWORD dwPID = 0xFFFFFFFF;
// find process
dwPID = FindProcessID(DEF_PROC_NAME);
if( dwPID == 0xFFFFFFFF )
{
_tprintf(L"There is no <%s> process!\n", DEF_PROC_NAME);
return 1;
}
_tprintf(L"PID of \"%s\" is %d\n", DEF_PROC_NAME, dwPID);
// change privilege
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
return 1;
// eject dll
if( EjectDll(dwPID, DEF_DLL_NAME) )
_tprintf(L"EjectDll(%d, \"%s\") success!!!\n", dwPID, DEF_DLL_NAME);
else
_tprintf(L"EjectDll(%d, \"%s\") failed!!!\n", dwPID, DEF_DLL_NAME);
return 0;
}
之前的keyhook注入成功后,运行该程序,keyhook.dll
就会卸载。
typedef struct tagMODULEENTRY32W
{
DWORD dwSize;
DWORD th32ModuleID; // This module
DWORD th32ProcessID; // owning process
DWORD GlblcntUsage; // Global usage count on the module
DWORD ProccntUsage; // Module usage count in th32ProcessID's context
BYTE * modBaseAddr; // Base address of module in th32ProcessID's context
DWORD modBaseSize; // Size in bytes of module starting at modBaseAddr
HMODULE hModule; // The hModule of this module in th32ProcessID's context
WCHAR szModule[MAX_MODULE_NAME32 + 1]; //dll name
WCHAR szExePath[MAX_PATH];
} MODULEENTRY32W;
typedef MODULEENTRY32W * PMODULEENTRY32W;
typedef MODULEENTRY32W * LPMODULEENTRY32W;
根据InjectDll.cpp
部分所说,FreeLibrary()
地址在所有进程中都是一样的。
FreeLibrary()
与ThreadProc()
都只有一个参数,所以能把CreateRemoteThread()
的线程函数参数换成FreeLibrary()
。
WINBASEAPI
BOOL
WINAPI
FreeLibrary(
_In_ HMODULE hLibModule
);