首页 > 系统相关 >逆向原理 | SetWindowsHookEx 原理探究与实验

逆向原理 | SetWindowsHookEx 原理探究与实验

时间:2023-01-17 20:00:11浏览次数:64  
标签:逆向 32 void SetWindowsHookEx hook 线程 64 原理 bit

逆向原理 | SetWindowsHookEx 原理探究与实验

SetWindowsHook其实是在windows逆向中非常重要的一个api与之对应的是UnhookWindowsHookEx,用于卸载钩子。
但是这个api的机制其实是比较复杂的,之前应该没有记录过,这次准备好好记录一下。
同时之后还准备写几篇关于windows上异常处理机制的文章记录一下。
从原理和实验两个部分出发。
author: Mz1


文章目录

原理部分:

  1. SetWindowsHookEx的基本信息和参数
  2. 调用SetWindowsHookEx的时候,操作系统在做什么

实验部分:

  1. hook自己进程中的线程
  2. hook别的进程中的线程
  3. 全局hook的使用

SetWindowsHookEx的基本信息和参数

在msdn上的描述还是比较清晰的:

HHOOK SetWindowsHookExA(
  [in] int       idHook,
  [in] HOOKPROC  lpfn,
  [in] HINSTANCE hmod,
  [in] DWORD     dwThreadId
);

参数1:idHook

idHook表示安装hook的种类,一般来说是宏定义,包括但不限于下方的宏定义,都在msdn可以查到:
image

比较常用的如下:
image

参数2:lpfn

lpfn指向hook使用的过程处理函数。在这个函数的结尾应该调用CallNextHookEx来传递消息,不然进程大概率挂掉。

注意!
如果dwThreadId这个参数(也就是你要hook的线程)是0(0表示全局hook),或者这个线程不属于当前进程,这个lpfn必须指向一个位于dll中的过程处理函数!
否则该hook只能应用于当前进程下的线程。

这是什么原因呢?其实跟操作系统做的事情有关,我们放在下面慢慢说。

参数3:hmod

上面不是说了,在hook其他进程的线程的时候,我们要把过程处理函数放在dll中吗?
这个hmod就是指向了那个dll。
如果是hook当前进程下的线程且过程处理函数位于当前进程中,则直接NULL就行了。

参数4:dwThreadId (最关键)

这应该是这个api中最重要的一个参数,决定了函数执行以后不同的行为。
dwThreadId也就是想要hook的线程id。
如果这个参数为0,就是我们常说的全局hook,将hook应用于当前桌面的所有应用程序。

返回值

返回这个hook的句柄。
如果失败,返回NULL,可以调用GetLastError查看原因。

说到这里,基本的信息就已经解释完毕了,下面是原理和操作系统的行为。

调用SetWindowsHookEx的时候,操作系统在做什么

在msdn的Remark里面有这样一段:

SetWindowsHookEx can be used to inject a DLL into another process. A 32-bit DLL cannot be injected into a 64-bit process, and a 64-bit DLL cannot be injected into a 32-bit process. If an application requires the use of hooks in other processes, it is required that a 32-bit application call SetWindowsHookEx to inject a 32-bit DLL into 32-bit processes, and a 64-bit application call SetWindowsHookEx to inject a 64-bit DLL into 64-bit processes. The 32-bit and 64-bit DLLs must have different names.

上面这段就是说,安装hook的程序和dll,要和目标程序的位数对应上。

Because hooks run in the context of an application, they must match the "bitness" of the application. If a 32-bit application installs a global hook on 64-bit Windows, the 32-bit hook is injected into each 32-bit process (the usual security boundaries apply). In a 64-bit process, the threads are still marked as "hooked." However, because a 32-bit application must run the hook code, the system executes the hook in the hooking app's context; specifically, on the thread that called SetWindowsHookEx. This means that the hooking application must continue to pump messages or it might block the normal functioning of the 64-bit processes.
If a 64-bit application installs a global hook on 64-bit Windows, the 64-bit hook is injected into each 64-bit process, while all 32-bit processes use a callback to the hooking application.

上面这段就是说,你用32位的代码在64位的操作系统上装全局hook的时候,所有32位的应用程序都会被注入这个dll。
但是,上面也说了,注入程序、注入的dll和被注入的程序位数要对应,因此,对于上面32位代码在64位系统上安装全局hook的时候,操作系统仍然会把64位的程序标记为已经被hook,但是!hook的过程处理函数的代码,会在注入程序(32位)的上下文中执行。
64位的全局hook类似。
补充一篇文章:https://blog.csdn.net/xbgprogrammer/article/details/53240535
在这篇文章中的解释比较清楚:
32位dll是不能注入到64位进程中,同理64位dll不能注入到32位进程中。如果32位进程调用SetWindowsHookEx 注入32位dll,其只能注入到32位进程中,虽然不能注入到64位进程,但是64位进程的线程依然被标注为hooked。当64位进程产生需要被hook处理的事件时,系统会在调用SetWindowsHookEx函数的进程(严格的说是线程)中执行hook例程。这要求调用SetWindowsHookEx的线程拥有一个消息泵,否则会阻止64位进程的执行。
据我的推测,要求调用SetWindowsHookEx的线程拥有一个消息泵,是因为64位进程通过windows消息向调用SetWindowsHookEx的线程发送windows消息,通知钩子事件发生。如果这个线程没有处理消息,通信阻塞,64位进程挂起。如果此时安装hook的进程结束掉,64位进程继续执行。

光看肯定是一头雾水的,下面是实验部分。

hook自己进程中的线程

这里我使用的是32位的vc6进行试验
创建一个mfc程序,比较方便。

随便拖一个界面出来:
image

左边的edit用来显示信息(输出)
右边的edit用来打字测试。

这里以键盘消息hook为例子。
先写好代码的框架:

// 全局hook句柄
HHOOK g_hHook = NULL;


// 回调函数先什么都不做
LRESULT CALLBACK lpfn(int code, WPARAM wParam, LPARAM lParam){
	return CallNextHookEx(g_hHook, code, wParam, lParam);
}


// 安装hook
void CHookmyselfDlg::OnButton1() 
{	
	CString str;
	GetDlgItemText(IDC_EDIT1, str);
	SetDlgItemText(IDC_EDIT1, str + "安装hook \r\n");
	
	// 获取当前进程的消息处理线程
	DWORD dwThreadId = GetCurrentThreadId();
	
	// 安装hook
	HHOOK hHook;
	hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)lpfn, NULL, dwThreadId);
	if (hHook != NULL){
		// 安装成功
		g_hHook = hHook;
		GetDlgItemText(IDC_EDIT1, str);
		SetDlgItemText(IDC_EDIT1, str + "安装成功 \r\n");
	}else{
		::MessageBoxA(0,"安装失败", 0,0);
	}
}

// 卸载hook
void CHookmyselfDlg::OnButton2() 
{
	CString str;
	GetDlgItemText(IDC_EDIT1, str);
	SetDlgItemText(IDC_EDIT1, str + "卸载hook \r\n");
	if (UnhookWindowsHookEx(g_hHook) != 0){
		// 成功
		GetDlgItemText(IDC_EDIT1, str);
		SetDlgItemText(IDC_EDIT1, str + "卸载成功 \r\n");
	}else{
		::MessageBoxA(0,"卸载失败", 0,0);
	}
}

image

测试完毕以后可以成功安装hook,修改回调函数的内容,输出按键消息:

// 回调函数
LRESULT CALLBACK lpfn(int code, WPARAM wParam, LPARAM lParam){
	char old[10000] = {0};
	char buff[10000] = {0};
	sprintf(buff, "按下了%x \r\n",wParam);
	GetDlgItemText(g_hWnd, IDC_EDIT1, old, 9000);
	SetDlgItemText(g_hWnd, IDC_EDIT1, strcat(old, buff));
	return CallNextHookEx(g_hHook, code, wParam, lParam);
}

image

至此,我们对自身进程的hook就完成了。

hook别的进程中的线程

我们分别写一个被hook的程序,和一个dll。

被hook的进程,同样用mfc创建,啥都不用做,拖个输入框出,显示一下自己的pid和线程id就行:
image

现在我们需要把hook的回调函数放在dll中了,对dll进行编写:
注意,dll中sethook和unsethook是安装hook的程序使用的,用来安装和卸载hook。

// dll.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"
#include <stdio.h>


HHOOK g_hHook = NULL;
HANDLE g_hModule = NULL;
FILE* g_fp = NULL;


// 写文件函数
void output(char* s){
	fputs(s, g_fp);
	fflush(g_fp);
}

// 回调函数
LRESULT CALLBACK lpfn(int code, WPARAM wParam, LPARAM lParam){
	char buf[255] = {0};
	sprintf(buf, "按下%x \n", wParam);
	output(buf);

	return CallNextHookEx(g_hHook, code, wParam, lParam);
}

// 安装hook的函数
extern "C"
__declspec(dllexport) VOID SetHook(DWORD dwThreadId){
	g_hHook = SetWindowsHookEx(WH_KEYBOARD, lpfn, (HMODULE)g_hModule, dwThreadId);
	if (g_hHook != NULL){
		output("安装hook成功\n");
	}else{
		output("安装失败\n");
	}
}

// 卸载hook的函数
extern "C"
__declspec(dllexport) VOID UnSetHook(){
	if (UnhookWindowsHookEx(g_hHook)!=0){
		output("卸载成功!\n");
	}else{
		output("卸载失败!\n");
	}
}

// 测试用函数
extern "C"
__declspec(dllexport) VOID test(){
	output("test ok \n");
}

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		// 初始化
		g_fp = fopen("C:\\Users\\thinkpad\\Desktop\\1.txt", "a");
		g_hModule = hModule;
		break;
	case DLL_PROCESS_DETACH:
		break;
	case DLL_THREAD_ATTACH:
		break;
	case DLL_THREAD_DETACH:
		break;
	}
    return TRUE;
}

然后编写注入的程序,这里就不用mfc了,直接用控制台方便一点啊:

#include <windows.h>
#include <stdio.h>

int main(){
	int tid;
	void (*pfunctest)();
	void (*pfunc_sethook)(DWORD);
	void (*pfunc_unsethook)();
	// 输入要hook的线程id
	printf("tid: ");
	scanf("%d", &tid);
	
	// 获取一下函数地址什么的
	HMODULE hModule = LoadLibrary("dll.dll");
	pfunctest = (void (__cdecl *)())GetProcAddress(hModule, "test");
	pfunctest();
	pfunc_sethook = (void (__cdecl *)(unsigned long))GetProcAddress(hModule, "SetHook");
	pfunc_unsethook = (void (__cdecl *)(void))GetProcAddress(hModule, "UnSetHook");

	// 启动hook
	pfunc_sethook(tid);
	
	system("pause");

	// 卸载hook
	pfunc_unsethook();
	return 0;
}

至此,完成对指定进程中的线程进行hook:
image

全局hook的使用

最后,我们要尝试的,就是全局hook,只要在上面的基础上将dwThreadId的值设置为0就可以了。
但是!
特别重要的一点,因为64位的程序也会被挂上钩子,要使用消息代理处理(见上面原理部分)
所以将上面的启动hook的程序改成mfc程序,为了兼容之前的dll,直接把调用SetHook时候的dwThreadId改成0就可以了。

// author:Mz1
int tid = 0;   // 全局hook
void (*pfunctest)();
void (*pfunc_sethook)(DWORD);
void (*pfunc_unsethook)();

// 初始化导入函数等信息
void CSetglobalhookDlg::OnButton3() 
{
	HMODULE hModule = LoadLibrary("dll.dll");
	pfunctest = (void (__cdecl *)())GetProcAddress(hModule, "test");
	pfunctest();
	pfunc_sethook = (void (__cdecl *)(unsigned long))GetProcAddress(hModule, "SetHook");
	pfunc_unsethook = (void (__cdecl *)(void))GetProcAddress(hModule, "UnSetHook");	
}
// 安装hook
void CSetglobalhookDlg::OnButton1() 
{
	pfunc_sethook(tid);
}
// 卸载hook
void CSetglobalhookDlg::OnButton2() 
{
	// TODO: Add your control notification handler code here
	pfunc_unsethook();
	
}

做完以后发现自己没加输出,憨憨了,不过不影响。
image

至此,SetWindowsHookEx原理到实践就基本ok啦!!!!!
终于把这个整理清晰了23333

标签:逆向,32,void,SetWindowsHookEx,hook,线程,64,原理,bit
From: https://www.cnblogs.com/Mz1-rc/p/17058120.html

相关文章

  • AI音箱的原理,小爱同学、天猫精灵、siri。
    AI音箱的原理简单的说,音箱工作的时,麦列始终处于拾音状态(对声音进行采样,量化)。进过基本的信号处理(静音检测、降噪等),唤醒模块会判断是否出现唤醒词,是的话就进行更复杂的语音信......
  • Spring Cloud Alibaba——Nacos服务注册原理
    前言再讲Nacos之前,先来讲一下服务注册和发现。我们知道,现在微服务架构是目前开发的一个趋势。服务消费者要去调用多个服务提供者组成的集群。这里需要做到以下几点:1、服......
  • Django Session 原理及配置和使用
    1、Django如何使用session会话1.1)session会话是通过中间件实现的,所以首先需要配置MIDDLEWAREMIDDLEWARE=[......'django.contrib.sessions.middleware.SessionM......
  • Django自定义认证系统原理及源码分析解读
    疑问Django在​​如何自定义用户登录认证系统的时候​​,大家都会里面立马说自定义一个或者多个backend,比如通过账号+密码、邮箱+密码,邮箱+验证码、手机号+短信验证码等等......
  • 【纠错编码原理】1-纠错编码的基本概念:简单入门
    纠错编码的基本概念纠错编码就是在信息序列中加入一些冗余码元,组成一个相关的码元序列——码字,译码时利用码元之间的相关性质类检测错误和纠正错误。不同的纠错编码方法有......
  • Vue3 响应式原理
     响应式原理Vue2使用的是 Object.defineProperty Vue3使用的是Proxy2.0的不足对象只能劫持设置好的数据,新增的数据需要Vue.Set(xxx) 数组只能操作七种方法,修改某......
  • DHCP工作原理及介绍
    DHCP介绍动态主机配置协议DHCP(DynamicHostConfigurationProtocol)是一种用于集中对用户IP地址进行动态管理和配置的技术。即使规模较小的网络,通过DHCP也可以使后续增加......
  • 生成树原理及配置
    STP协议介绍STP(SpanningTreeProtocol)是运行在交换机上的二层破环协议,环路会导致广播风暴、MAC地址表震荡等后果,STP的主要目的就是确保在网络中存在冗余路径时,不会产生环......
  • 【Dubbo3 终极特性】「云原生三中心架构」带你探索 Dubbo3 体系下的配置中心和元数据
    承接上文通过之前的【Dubbo3终极特性】「云原生三中心架构」带你探索Dubbo3体系下的配置中心和元数据中心、注册中心的原理及开发实战(上),让我们对Dubbo3的三中心架构体系......
  • 【Dubbo3 终极特性】「云原生三中心架构」带你探索 Dubbo3 体系下的配置中心和元数据
    承接上文通过之前的【Dubbo3终极特性】「云原生三中心架构」带你探索Dubbo3体系下的配置中心和元数据中心、注册中心的原理及开发实战(上),让我们对Dubbo3的三中心架构体系有......