Windows下的注入和Hook
Windows下的注入和Hook
两者的区别不太大,我感觉就是把一个东西的两个方面说了出来,一个注重勾取这个结果,一个注重注入这个行为.
当然其实还是有一定区别的,不是所有注入都是为了勾取某个函数或者修改OEP什么的.或者说适合hook的技术肯定适合用来注入,但是适合注入的技术不一定适合hook,或者说较为麻烦,比如CreateRemoteThread
另外要说的是,两个的实现方法其实很多,就不一一举例了。
| 方法 | 对象 | 位置 | 技术如何 | API |
|---|---|---|---|---|
| 静态 | 文件 | - | X | X |
| 动态 | 进程内存(地址范围:00000000~7FFFFFFF) | 1) IAT 2) 代码 3) EAT |
A) 调试(交互型) B) 注入(独立型) B-1) 独立代码 B-2) DLL 文件 |
A 类:DebugActiveProcess、GetThreadContext、SetThreadContext B-1 类:CreateRemoteThread B-2 类:Registry(AppInit_DLLs)、BHO(仅 IE);SetWindowsHookEx、CreateRemoteThread |
Hook
总结
Hook分类:内联hook,地址hook,异常处理下的hook(int3+veh),不是hook的hook(病毒)
一个hook框架:Windows hook框架Detours踩坑-先知社区
hook要做到 检查参数,检查结果,拦截调用和下发。
要注意 多线程安全,本身Detour函数的多线程安全,inline时要注意长度,保存和恢复(栈平衡,恢复寄存器),返回值,避免重入
方式
消息钩取
Windows是以事件驱动的,这个无需多言.
那么在消息之间的传递之间,我们可以在其中实现钩子.微软也想到了,SetWindowsHookEx这个API可以直接实现消息钩子
HHOOK SetWindowsHookEx(
int idHook, //hook type
HOOKPROC lpfn, //hook procedure,由操作系统调用的回调函数,也就是我们提供的钩子
HINSTANCE hMod, //hook procedure所属dll句柄
DWORD dwThreadId //要挂钩的线程ID,若设置为0代表全局钩子,影响所有进程
)
设置好钩子后,如果某个进程生成指定信息,系统就会把相关dll强行注入相应进程
HOOKPROC Hookproc;
LRESULT Hookproc(
int code,
[in] WPARAM wParam, //指定消息是否由当前进程发送。 如果消息由当前进程发送,则为非零;否则为 NULL。
[in] LPARAM lParam //定义传递给 WH_CALLWNDPROCRET 挂钩过程 HOOKPROC 回调函数的消息参数。
)
{...}
typedef struct tagCWPRETSTRUCT {
LRESULT lResult; //处理由消息值指定的消息的窗口过程的返回值。
LPARAM lParam; //关于消息的附加信息。 确切的含义取决于消息值。
WPARAM wParam; //关于消息的附加信息。 确切的含义取决于消息值。
UINT message; //消息。
HWND hwnd; //处理由消息值指定的消息的窗口的句柄。
} CWPRETSTRUCT, *PCWPRETSTRUCT, *NPCWPRETSTRUCT, *LPCWPRETSTRUCT;
大概了解这个结构就行,要用到查手册即可.
具体示例:
#include "stdio.h"
#include "windows.h"
#define DEF_PROCESS_NAME "notepad.exe"
HINSTANCE g_hInstance = NULL;
HHOOK g_hHook = NULL;
HWND g_hWnd = NULL;
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, '\\');
if( !_stricmp(p + 1, DEF_PROCESS_NAME) )
return 1;
}
}
// 如果不是目标进程,将消息传给应用或者下一个钩子
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
#ifdef __cplusplus
extern "C" {
#endif
__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
消息钩取大概有这些函数(里面有些待实现回调函数,可以利用,比如键盘监视):
- CallMsgFilter
- CallNextHookEx
- CallWndProc
- CallWndRetProc
- CBTProc
- DebugProc
- ForegroundIdleProc
- GetMsgProc
- JournalPlaybackProc
- JournalRecordProc
- KeyboardProc
- LowLevelKeyboardProc
- LowLevelMouseProc
- MessageProc
- MouseProc
- SetWindowsHookEx
- ShellProc
- SysMsgProc
- UnhookWindowsHookEx
一种反消息挂钩的简单方式:Windows 反注入(一) - 哔哩哔哩
调试钩取
调试进程经过注册后,每当被调试者发生调试事件(DebugEvent)时,OS就会暂停其运行并向调试器报告相应事件。调试器对相应事件做适当处理后,使被调试者继续运行。
一般的异常(Exception)也属于调试事件。 若相应进程处于非调试,调试事件会在其自身的异常处理或OS的异常处理机制中被处理掉。 调试器无法处理或不关心的调试事件最终由OS处理。
各种事件主要是断点异常,也就是EXCEPTION_BREAKPOINT,这个也必须由调试器处理.
既然如此我们只要:
- 对想钩取的进程进行附加操作,使之成为被调试者;
- “钩子”:将API起始地址的第一个字节修改为OxCC;
- 调用相应API时,控制权转移到调试器;
- 执行需要的操作(操作参数、返回值等);
- 脱钩:将OxCC恢复原值(为了正常运行API);
- 运行相应API(无OxCC的正常状态);
- “钩子”:再次修改为OxCC(为了继续钩取);
- 控制权返还被调试者。
int main(int argc, char* argv[]){
DWORDdwPID;
// 附加
dwPID =atoi(argv[1]);
if(!DebugActiveProcess(dwPID)){...} //另一种启动调试的方法是使用CreateProcess()API,可以直接以调试模式启动相关进程
//调试器循环
DebugLoop();
return 0;
}
void DebugLoop()
{
DEBUG_EVENT de; // 去msdn看看,这里主要是dwDebugEventCode
DWORD dwContinueStatus;
// 等待debuggee事件
while( WaitForDebugEvent(&de, INFINITE) )
{
dwContinueStatus = DBG_CONTINUE;
//生成或者附加的时候
if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
OnCreateProcessDebugEvent(&de); //在这里加上0xcc
}
//异常事件
else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
{
if( OnExceptionDebugEvent(&de) )
continue;
}
//debugee终止事件的时候
else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
break;
}
//再次运行debuggee
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO)); //g_cpdi是CREATE_PROCESS_DEBUG_INFO结构体(出处:MSDN)变量。
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL); //备份
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL); // 写入
return TRUE;
}
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
CONTEXT ctx;
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
// 确认是断点异常(int 3)时
if( EXCEPTION_BREAKPOINT == per->ExceptionCode )
{
// 确认是WriteFile
if( g_pfWriteFile == per->ExceptionAddress )
{
// #1. Unhook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
// #2. 获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);
// #3. 获取WriteFile的参数
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);
// #4. 缓冲区
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);
// #5. 复制
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);
// #6. 转化
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}
printf("\n### converted string ###\n%s\n", lpBuffer);
// #7. 修改
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
// #8. 释放
free(lpBuffer);
// 修改eip会首地址
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);
// #10. Debuggee继续运行
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0); //调用后可以进程会放弃自己的当前时间片,如果没有会导致进程继续修改
// #11. API Hook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);
return TRUE;
}
}
return FALSE;
}
通过CREATE_PROCESS_DEBUG_INFO结构体的hProcess成员(被调试进程的句柄),可以钩取WriteFile()API(不使用调试方法时,可以使用OpenProcess()API获取相应进程的句柄)。调试方法中,钩取的方法非常简单。
另外自XP起可以调用DebugSetProcessKillOnExit()来做到调试器退出而无需销毁被调试者,但是需要脱钩,否则断点会留在那里.
SetThreadContext
正在执行的线程可以用SuspendThread暂停执行,我们可以把保存下来的上下文的eip修改到我们指定的shellcode位置,然后用ResumeThread恢复
hook位置
IAT钩取
通过修改IAT为自己的函数来实现,在自己的钩子函数实现要做的后,再跳转到相应函数地址执行.
API代码修改钩取
修改开头字节为跳转字节跳转到钩子函数.这个比IAT的限制更少,而且靠这个就能做到全局hook,就可以搞些事情,比如全局API钩取
要实现全局hook,要做到在当前的进程和未来新创建的进程都hook.
先讲几个注意点:
- 不建议hook pid小于100的进程,容易出错.
- 可以在注入的dll中写一个共享内存节区,方便所有进程都知道一些需要共享的内容
- 可以把dll放在所有进程都知道的地方,比如
%SYSTEM%
首先要在系统的快照里过一遍所有进程.
在windows中启动运行进程的API其内部调用的是CreateProcess,而各种版本的是CreateProcess的更底层API是ntdll.ZwResumeThread(这个是逆向工程核心原理的,里面说微软没有开源这个函数,有可能会过时了,要注意)
这样我们就可以实现Rootkit(隐藏进程,只需要hook所有使用查找进程的API)
热补丁技术
热补丁技术相比API修改更稳定和高效(因为API修改要不断脱钩/挂钩,而且直接修改)
微软在制作系统库时,许多API(不是所有,要注意API是否适用)的初始代码位置(跳转到的位置)都被加上MOV EDI,EDI两个字节和上方(跳转到的位置前5个字节)5个字节则是NOP,共七个字节无意义指令,方便未来打热补丁.
“热补丁”由API钩取组成,在进程处于运行状态时临时更改进程内存中的库文件(重启系统时,修改的目标库文件会被完全取代)。
如何实现,就是在两个字节的位置构造一个短跳转,跳转到前5个字节,在5个字节处构造长跳转,跳转到目标钩子,而调用API只要采取+2的方式即可.
注入
DLL注入
总结
这个用处有很多,比如通过注入修bug,修改和扩展程序功能,因为用LoadLibrary加载DLL后DllMain就会被调用.
还可以监管其他程序和写恶意代码.
有三种方式:
- 创建远程线程
CreateRemoteThread - 利用注册表
AppInit_DLLs值 - 消息钩取
SetWindowsHookEx
CreateRemoteThread
//hack.dll
HMODULE g_hMod = NULL;
DWORD WINAPI ThreadProc(LPVOID lParam) { // 使用 CreateThread、CreateRemoteThread 或 CreateRemoteThreadEx 函数的 lpParameter 参数传递给函数的线程数据。
return 0; // 返回值指示此函数的成功或失败。返回值不应设置为 STILL_ACTIVE (259),如 GetExitCodeThread 中所述。GetExitCodeThread 函数仅在线程终止后返回由应用程序定义的有效错误代码。 因此,应用程序不应使用 STILL_ACTIVE (259) 作为错误代码。 如果线程返回 STILL_ACTIVE (259) 作为错误代码,则测试此值的应用程序可能会将其解释为表示线程仍在运行,并在线程终止后继续测试线程完成情况,这可能会使应用程序进入无限循环。 为避免此问题,调用方应仅在确认线程退出后调用 GetExitCodeThread 函数。 使用等待持续时间为零的 WaitForSingleObject 函数来确定线程是否已退出。
}
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // DLL 模块的句柄
DWORD fdwReason, // 调用原因
LPVOID lpvReserved // 保存位置
) {
HANDLE hTread = NULL;
g_hMod = (HMODULE)hinstDLL;
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
OutputDebugString(L"Fuck you, MicroSoft!");
hTread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
// 指向 SECURITY_ATTRIBUTES 结构的指针,该结构指定新线程的安全描述符,并确定子进程是否可以继承返回的句柄。 如果 lpThreadAttributes 为 NULL,则线程将获取默认的安全描述符,并且无法继承句柄。
// 堆栈的初始大小(以字节为单位)。 系统将此值舍入到最近的页面。 如果此参数为零,则新线程使用可执行文件的默认大小。
// 指向由线程执行的应用程序定义函数的指针。 此指针表示线程的起始地址。 有关线程函数的详细信息,请参阅 ThreadProc。
// 指向要传递给线程的变量的指针。
// 控制线程创建的标志。
//邪恶dll需要自行创建一个线程来执行自己的邪恶计划.
CloseHandle(hTread);
break;
default:
break;
}
return TRUE;
}
//createremotethread.cpp
#include <Windows.h>
#include <tchar.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;
// 用PID获取目标进程句柄
if (!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID))) {
_tprintf(L"OpenProcess %d failed: %d", dwPID, GetLastError());
return FALSE;
} //通过pid获得程序句柄
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);//在目标程序申请一段内存
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
//在目标程序目标地址写入
hMod = GetModuleHandle(L"kernel32.dll");// 如果开启了ASLR,需要先查找目标进程中的kernel32.dll的基址,再加上本进程入口与基址的偏移获取目标进程loadlibrary的地址。
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryW");
hThread = CreateRemoteThread(
hProcess, //目标进程
NULL,
0,
pThreadProc,
pRemoteBuf,
0,
NULL
);//和CreateThread很像
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
注意在进行调试操作,用这些调试API前,要调整进程权限
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;
}
//打开当前进程的访问令牌(通过 OpenProcessToken)
//查找指定特权名称对应的本地唯一标识符(LUID,通过 LookupPrivilegeValue)
//根据参数 bEnablePrivilege 的值,启用或禁用该特权(通过 AdjustTokenPrivileges)
//SE_DEBUG_NAME 特权允许调试任何进程
//SE_SHUTDOWN_NAME 特权允许关闭系统
//SE_BACKUP_NAME 特权允许备份文件
64位下对系统服务不能生效(会话0),普通进程可以,当然这个是逆向工程核心原理的,有可能过时了.
在vista新增了API,CreateRemoteThread改为直接调用这些API,kernelbase!CreateRemoteThreadEx与ntdll!ZwCreateThreadEx.书中描述,kernelbase是一个新加的库,作为一个封装kernel32的库,所以问题不出在这里,核心在于一个微软未公开的函数ZwCreateThreadEx,如果调用它就不会出现DLL注入失败的问题.因为一个参数CreateSuspended,实际上就是检查了是否是同一个会话,因为用了CsrClientCallServer来登记失败了。
从WindowsXP开始,
CreateRemoteThreadAPI内部的实现算法采用了挂起模式, 即先创建出线程,再使用“恢复运行”方法继续执行(CreateSuspended=1)。较该方法中使用的参数与图43-13中的参数可以发现,它们的不同在于第七个参数 (CreateSuspended)。直接调用ZwCreateThreadExO成功注人DLL时,CreateSuspended参数值为 FALSE(O),而在CreateRemoteThreadOAPI内部调用ZwCreateThreadExO时,该CreateSuspended参 数值为TRUE(1)。这就是DLL注入失败的原因。也就是说,在API内部创建远程线程时采用了挂起模式,若远程进程属于会话0,则不会“恢复运行”,而是直接返回错误。
ntdll!NtCreateThreadExAPI是一个尚未公开的API,所以微软不建议直接调用它,否则将导致系统稳定性失去保障。就我的测试结果来看,调用它之后工作非常正 常,但微软可能在以后某个时候修改它。在某个项目中使用该方法时,一定要注意这点。
目前看下来NTCreateThreadEx依旧可行
RtlCreateUserThread
这个函数类似于CreateRemoteThread,最后都是用NtCreateThreadEx来创建线程实体。不同的是,它不需要经过CSRSS的验证,但是他必须调用ExitThread或RtlExitUserThread来结束自己。
AppInit_DLLs
微软在注册表默认提供了AppInit_DLLs和LoadAppInit_DLLs两个注册表
把要注入DLL的路径写入AppInit_DLLs,再在另一个把项目值写为1,重启时就能注入了.
上述方法的工作原理是,User32.dll 被加载到进程时,会读取AppInit_DLLs注册表项,若有值,则调用LoadLibrary()加载用户DLL。所以,严格地说,相应 DLL 并不会被加载到所有进程,而只是加载至加载user32.dll的进程。请注意,WindowsXP会忽略LoadAppInit_DLLs注册表项。
SetWindowsHookEx
就是上面消息钩取的用法
APC注入
线程在从等待状态中(必须是调用特定函数后才能进入,不一一举例,因为用处不大,条件苛刻)苏醒的时候,会检查有没有APC函数交付给自己,我们就可以用QueueUserAPC来在对方的APC队列中添加函数,实现注入。
卸载DLL
这个方法只能卸载我们自己主动加载的dll,实际上就是CreateRemoteThread,然后把LoadLibrary改为FreeLibrary
讲几个可以用到的api:
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
BOOL Process32Next(
[in] HANDLE hSnapshot,
[out] LPPROCESSENTRY32 lppe
);
CreateToolhelp32Snapshot 函数 (tlhelp32.h) - Win32 apps | Microsoft Learn
Process32First 函数 (tlhelp32.h) - Win32 apps | Microsoft Learn
Process32Next 函数 (tlhelp32.h) - Win32 apps | Microsoft Learn
用来获取当前进程内存快照,配合Process32Frist和Process32Next可以以此来比对进程名字获取pid
手工修改注入DLL
通过修改导入表来做到的
注意绑定导入表,这个修改时需要调整
可以通过这个做到永久破解,只需要备着dll就行了.
代码注入
实际上,依旧是CreateRemoteThread,但是由于不能用dll,所以要我们自己搞数据,就是在对方进程开辟内存,然后把地址同回调函数一同传入,新建线程以实现.
当然我们也可以用调试API。
这里我们就可以写shellcode了.shellcode相比C语言编写的优势在于,我们可以手动在里面添加数据,构造一些数据会方便点(字符串).虽然C语言也可以用内联汇编就是了.
说一个汇编技巧,用来解决如何把字符串压栈:
call next ;next只是用来方便说明
db xxxxxx ;假设这里是数据
next:
xxx
这里call就会把esp压栈,然后jmp指定地址,一个小技巧
有趣的方式
输入法注入,利用微软给输入法规范的dll接口来实现注入。
SPI网络过滤器注入
ShimEngine注入
防范措施
详情看加密与解密的第12章 注入技术

浙公网安备 33010602011771号