学习笔记之-window hook (x86 和 64 版本) 初探

缘由:本来想做个微信的机器人,发现wev方法已经歇菜了,微信貌似不让web版好好工作了,因此就顺带看了一下hook技术,调试了一个网上现成的代码。竟然调试成功,并且有了一点儿体会。

为了简单起见,介绍部分,主要来自各味前辈的总结。
文章主要参考
HOOK API入门之Hook自己程序的MessageBoxW(简单入门)
本文的目的是对其中的部分代码进行更小白的解释

Hook的本意是钩子,意思比较形象,就是对应用程序勾一下,干点儿别的。
下面进入我的个人理解的介绍:

hook的个人入门级理解##

1、hook的个人目的和用途
在现有程序中插一脚,瞄一眼,在不影响现有程序运行的情况下,干点儿事情,善意的或者恶意的。
2、实现思路
截获运行流程,令其转向,然后再返回。
3、怎么截获
原则上讲,水平高从程序的任何地方截获都可以,只要来的时候不漏痕迹就行了。然而,这是理论上而已,可行的方案是从调用入口截获,这样符合模块操作的风格,对原来的程序流程的影响容易做到最小。因此,入门级的截获就是截获已知函数。
4、截获原则
函数类型和参数等等等要和原来的函数一模一样。

具体实现思路

参考引文,实现对自己message box 的hook

很多问题参考原文即可。
下面整理一下具体过程的流程
1、定义自己的函数
2、获取自己的函数地址
3、获取Message box 在 User32.dll 中的地址
4、用自己的函数地址替换原函数地址
就上面的四个步骤,具体实现的时候,还有一些技巧,例如怎么替换,替换后的具体行为是什么等等。

代码和解释

原文是基于64位的win7环境,没有给出具体的32位做法,本文将给出两种环境下的代码。
64位:
1、准备工作

char szOldAPI[12] = {
    
    };
// mov         rax,13F371A3Ch
#ifdef  _M_X64
char NewCode[12] = {
    
     0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x50,0xC3 };
#endif

定义存放函数指针附近附近的数据,实际上是存贮机器码(这个如果不懂,先不管吧)。这个机器码就是调用关系,或者理解成汇编语言就OK了。

szOldAPI: 是存贮原函数的地址附近的数据,这个用来恢复原函数的调用关系用。
NewCode:是我们新的函数地址附近的数据,用来替换原函数的数据。
第一个字节和第二个字节是 汇编代码 mov rax xxxxxxx,如图示,后面的数字就是函数地址。将函数地址里的值放入到rax寄存器中。
下面的50C3就是最后的两个字节,汇编代码就是 push rax, 将rax 寄存器压入堆栈,然后返回。
在这里插入图片描述
篮筐内的数据时替换后我们的函数地址。上面解释未必准确,但是方向应该是对的。因为我对汇编并不熟悉。

为了对比,下面列出没有替换时的原函数地址附近的数据情况
在这里插入图片描述
注意首地址是一样的,我们替换了最前面的12个字节,完成了向我们函数的跳转。
注:moe rax指令是完成一种系统调用,具体参考
汇编语言基础:寄存器和系统调用
2、替换操作

	DWORD dwPid = ::GetCurrentProcessId();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);
	//获取原API入口地址
	HMODULE hmod = ::LoadLibrary(_T("User32.dll"));
	OldMsgBoxW = (MsgBoxW)::GetProcAddress(hmod, "MessageBoxW");
	pfOldMsgBoxW = (FARPROC)OldMsgBoxW;//注意这里有个转换,32位好像不用。

	if (pfOldMsgBoxW == NULL)
	{
    
    
		MessageBox(NULL, _T("获取原API入口地址出错"), _T("error!"), 0);
		return;
	}
#ifdef _M_X64
	DWORD64 dwJmpAddr = 0;
	dwJmpAddr = (DWORD64)MyMessageBoxW; //存下我们自己的函数的地址
	memcpy(NewCode + 2, &dwJmpAddr, 8); //把地址写到szNewAPI中间的8个0字节处
	ReadProcessMemory((void*)-1, pfOldMsgBoxW, szOldAPI, FUN_ADD_LEN, NULL); //读出原来的前FUN_ADD_LEN个字节
	//ReadProcessMemory(hProcess, pfOldMsgBoxW, OldCode, 12, NULL); //读出原来的前12个字节
#endif

上面是准备工作,还没有进行hook操作。
注意:12个字节长度的原因是因为利用上面的汇编语句,这个长度是最短的长度,并不是必须的,可以再长一些,只要记住被替换的部分,可以恢复即可。

3、hook 操作代码

	ASSERT(hProcess != NULL);
	//修改API函数入口前5个字节为jmp xxxxxx
	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, PAGE_READWRITE, &dwOldProtect);
	WriteProcessMemory(hProcess, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, 0);
	//WriteProcessMemory((void*)-1, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, NULL); //写入我们处理后的FUN_ADD_LEN个字节 

	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, dwOldProtect, &dwTemp);

替换操作的代码是

WriteProcessMemory(hProcess, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, 0);

FUN_ADD_LEN 的值是12, 32位同。

执行完上述代码之后,调用关系就已经被修改了。
修改之前,的内存内容
在这里插入图片描述
修改之后

请读者自行比较前12个字节的变化,一起获得感性认识。

至此,原理以及解释已经结束。

下面介绍一下32位的做法
原理一样,只不过汇编语言不同。这里用了jmp 汇编,该汇编代码是无条件跳转命令,后面数字是地址偏移量,而不是绝对地址,因此后面的数值要进行计算。

这个偏移量的计算如下(直接引用参考网友的注释)

#ifdef _M_IX86
	DWORD32 dwJmpAddr = 0;
	dwJmpAddr = (DWORD32)MyMessageBoxW; //存下我们自己的函数的地址
	dwJmpAddr = (DWORD32)MyMessageBoxW - (DWORD32)pfOldMsgBoxW-5;//-5 
	memcpy(NewCode + 1, &dwJmpAddr, 4); //把地址写到szNewAPI中间的0字节处

	//or the following codes , the both work.
	//_asm
	//{
    
    
	//	lea eax, MyMessageBoxW //获取我们的MyMessageBoxW函数地址
	//	mov ebx, pfOldMsgBoxW  //原系统API函数地址
	//	sub eax, ebx			 //int nAddr= UserFunAddr – SysFunAddr
	//	sub eax, 5			 //nAddr=nAddr-5, 
	//	mov dword ptr[NewCode + 1], eax //将算出的地址nAddr保存到NewCode后面4个字节
	//				  //注:一个函数地址占4个字节
	//}

	ReadProcessMemory((void*)-1, pfOldMsgBoxW, szOldAPI, FUN_ADD_LEN, NULL); //读出
#endif

说明:参考网友使用的是汇编代码,后面都有注释,笔者根据汇编码改成了非汇编的代码,经测试二者工作都正常。(这是显然的,因为结果相同的。)

完整代码和参考界面

下面是完整的调试过的代码,读者可以拷贝使用

// MFCHookTestDlg.cpp: 实现文件
//

#include "pch.h"
#include "framework.h"
#include "MFCHookTest.h"
#include "MFCHookTestDlg.h"
#include "afxdialogex.h"
#include <mswsockdef.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#endif
#ifdef  _M_IX86
#define  FUN_ADD_LEN 5
#endif
#ifdef  _M_X64
#define  FUN_ADD_LEN 12
#endif


typedef int (WINAPI* MsgBoxW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
MsgBoxW OldMsgBoxW = NULL;//指向原函数的指针
FARPROC pfOldMsgBoxW;  //指向函数的远指针

char szOldAPI[12] = {
    
    };
//存放原来API函数在内存中的前12个字节
//jp ....
#ifdef  _M_IX86
char NewCode[12] = {
    
     0xe9,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x50,0xC3 };
#endif

// mov         rax,13F371A3Ch
#ifdef  _M_X64
char NewCode[12] = {
    
     0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x50,0xC3 };
#endif

//存放跳转指令,中间的8个字节是0,因为现在还没有存放目标地址 

HANDLE hProcess = NULL;//本程序进程句柄
HINSTANCE hInst = NULL;//API所在的dll文件句柄

void HookOn();
void HookOff();
void GetApiEntrance();
int WINAPI MyMessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType)
{
    
    
	TRACE(lpText);
	HookOff();//调用原函数之前,记得先恢复HOOK呀,不然是调用不到的
			  //如果不恢复HOOK,就调用原函数,会造成死循环
			  //毕竟调用的还是我们的函数,从而造成堆栈溢出,程序崩溃。

	int nRet = ::MessageBoxW(hWnd, _T("哈哈,MessageBoxW被HOOK了"), lpCaption, uType);

	HookOn();//调用完原函数后,记得继续开启HOOK,不然下次会HOOK不到。 

	return nRet;
}



// 用于应用程序“关于”菜单项的 CAboutDlg 对话框

class CAboutDlg : public CDialogEx
{
    
    
public:
	CAboutDlg();

// 对话框数据
#ifdef AFX_DESIGN_TIME
	enum {
    
     IDD = IDD_ABOUTBOX };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 支持

// 实现
protected:
	DECLARE_MESSAGE_MAP()
};

CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
    
    
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
    
    
	CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()


// CMFCHookTestDlg 对话框



CMFCHookTestDlg::CMFCHookTestDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_MFCHOOKTEST_DIALOG, pParent)
{
    
    
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CMFCHookTestDlg::DoDataExchange(CDataExchange* pDX)
{
    
    
	CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CMFCHookTestDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDC_BEGIN_HOOK, &CMFCHookTestDlg::OnBnClickedBeginHook)
	ON_BN_CLICKED(IDC_CALL_MSGBOX, &CMFCHookTestDlg::OnBnClickedCallMsgbox)
	ON_BN_CLICKED(IDC_END_HOOK, &CMFCHookTestDlg::OnBnClickedEndHook)
END_MESSAGE_MAP()


// CMFCHookTestDlg 消息处理程序

BOOL CMFCHookTestDlg::OnInitDialog()
{
    
    
	CDialogEx::OnInitDialog();

	// 将“关于...”菜单项添加到系统菜单中。

	// IDM_ABOUTBOX 必须在系统命令范围内。
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != nullptr)
	{
    
    
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
    
    
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 设置此对话框的图标。  当应用程序主窗口不是对话框时,框架将自动
	//  执行此操作
	SetIcon(m_hIcon, TRUE);			// 设置大图标
	SetIcon(m_hIcon, FALSE);		// 设置小图标

	// TODO: 在此添加额外的初始化代码
	GetApiEntrance();
	return TRUE;  // 除非将焦点设置到控件,否则返回 TRUE
}

void CMFCHookTestDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
    
    
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
    
    
		CAboutDlg dlgAbout;
		dlgAbout.DoModal();
	}
	else
	{
    
    
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

// 如果向对话框添加最小化按钮,则需要下面的代码
//  来绘制该图标。  对于使用文档/视图模型的 MFC 应用程序,
//  这将由框架自动完成。

void CMFCHookTestDlg::OnPaint()
{
    
    
	if (IsIconic())
	{
    
    
		CPaintDC dc(this); // 用于绘制的设备上下文

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// 使图标在工作区矩形中居中
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// 绘制图标
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
    
    
		CDialogEx::OnPaint();
	}
}

//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CMFCHookTestDlg::OnQueryDragIcon()
{
    
    
	return static_cast<HCURSOR>(m_hIcon);
}



void CMFCHookTestDlg::OnBnClickedBeginHook()
{
    
    

	
	HookOn();

	SetDlgItemText(IDC_STATIC_INFO, _T("Hook已启动"));

}

//开启钩子的函数
void HookOn()
{
    
    
	ASSERT(hProcess != NULL);

	DWORD dwTemp = 0;
	DWORD dwOldProtect;

	//修改API函数入口前5个字节为jmp xxxxxx
	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, PAGE_READWRITE, &dwOldProtect);
	WriteProcessMemory(hProcess, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, 0);
	//WriteProcessMemory((void*)-1, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, NULL); //写入我们处理后的FUN_ADD_LEN个字节 

	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, dwOldProtect, &dwTemp);

}
//关闭钩子的函数
void HookOff()
{
    
    
	ASSERT(hProcess != NULL);

	DWORD dwTemp = 0;
	DWORD dwOldProtect;

	//恢复API函数入口前5个字节
	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, PAGE_READWRITE, &dwOldProtect);
	WriteProcessMemory(hProcess, pfOldMsgBoxW, szOldAPI, FUN_ADD_LEN, 0);
	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, dwOldProtect, &dwTemp);
}


//获取API函数入口前5个字节
//旧入口前5个字节保存在前面定义的字节数组BYTE OldCode[5]
//新入口前5个字节保存在前面定义的字节数组BYTE NewCode[5]
void GetApiEntrance()
{
    
    

	DWORD dwPid = ::GetCurrentProcessId();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);


	//获取原API入口地址
	HMODULE hmod = ::LoadLibrary(_T("User32.dll"));
	OldMsgBoxW = (MsgBoxW)::GetProcAddress(hmod, "MessageBoxW");
	pfOldMsgBoxW = (FARPROC)OldMsgBoxW;

	if (pfOldMsgBoxW == NULL)
	{
    
    
		MessageBox(NULL, _T("获取原API入口地址出错"), _T("error!"), 0);
		return;
	}

	 将原API的入口前5个字节代码保存到OldCode[]
	//_asm
	//{
    
    
	//	lea edi, OldCode		//获取OldCode数组的地址,放到edi
	//	mov esi, pfOldMsgBoxW //获取原API入口地址,放到esi
	//	cld	  //方向标志位,为以下两条指令做准备
	//	movsd //复制原API入口前0-3个字节到OldCode数组
	//	movsd //复制原API入口第4-7个字节到OldCode数组
	//	movsd //复制原API入口第8-11个字节到OldCode数组
	//}

	//ReadProcessMemory((void*)-1, pfOldMsgBoxW, OldCode, FUN_ADD_LEN, NULL); //读出原来的前5个字节
	//WriteProcessMemory((void*)-1, pfOldMsgBoxW, szNewAPI, FUN_ADD_LEN, NULL); //写入我们处理后的5个字节 

#ifdef _M_IX86
	DWORD32 dwJmpAddr = 0;
	dwJmpAddr = (DWORD32)MyMessageBoxW; //存下我们自己的函数的地址
	dwJmpAddr = (DWORD32)MyMessageBoxW - (DWORD32)pfOldMsgBoxW-5;//-5 
	memcpy(NewCode + 1, &dwJmpAddr, 4); //把地址写到szNewAPI中间的8个0字节处

	//or the following coded , both work.
	//_asm
	//{
    
    
	//	lea eax, MyMessageBoxW //获取我们的MyMessageBoxW函数地址
	//	mov ebx, pfOldMsgBoxW  //原系统API函数地址
	//	sub eax, ebx			 //int nAddr= UserFunAddr – SysFunAddr
	//	sub eax, 5			 //nAddr=nAddr-5, 
	//	mov dword ptr[NewCode + 1], eax //将算出的地址nAddr保存到NewCode后面4个字节
	//				  //注:一个函数地址占4个字节
	//}

	ReadProcessMemory((void*)-1, pfOldMsgBoxW, szOldAPI, FUN_ADD_LEN, NULL); //读出原来的前FUN_ADD_LEN个字节
	//ReadProcessMemory(hProcess, pfOldMsgBoxW, OldCode, 12, NULL); //读出原来的前12个字节
#endif
#ifdef _M_X64
	DWORD64 dwJmpAddr = 0;
	dwJmpAddr = (DWORD64)MyMessageBoxW; //存下我们自己的函数的地址
	memcpy(NewCode + 2, &dwJmpAddr, 8); //把地址写到szNewAPI中间的8个0字节处
	ReadProcessMemory((void*)-1, pfOldMsgBoxW, szOldAPI, FUN_ADD_LEN, NULL); //读出原来的前FUN_ADD_LEN个字节
	//ReadProcessMemory(hProcess, pfOldMsgBoxW, OldCode, 12, NULL); //读出原来的前12个字节
#endif



	//NewCode[0] = 0xe9;//实际上0xe9就相当于jmp指令

	//获取MyMessageBoxW的相对地址,为Jmp做准备
	//int nAddr= UserFunAddr – SysFunAddr - (我们定制的这条指令的大小);
	//Jmp nAddr;
	//(我们定制的这条指令的大小), 这里是5,5个字节嘛
	//BYTE NewCode[12];

	//填充完毕,现在NewCode[]里的指令相当于Jmp MyMessageBoxW
	//既然已经获取到了Jmp MyMessageBoxW
	//现在该是将Jmp MyMessageBoxW写入原API入口前5个字节的时候了
	//知道为什么是5个字节吗?
	//Jmp指令相当于0xe9,占一个字节的内存空间
	//MyMessageBoxW是一个地址,其实是一个整数,占4个字节的内存空间
	//int n=0x123;   n占4个字节和MyMessageBoxW占4个字节是一样的
	//1+4=5,知道为什么是5个字节了吧
}



void CMFCHookTestDlg::OnBnClickedCallMsgbox()
{
    
    
	::MessageBoxW(m_hWnd, _T("这是正常的MessageBoxW"), _T("Hello"), 0);
}


void CMFCHookTestDlg::OnBnClickedEndHook()
{
    
    
	HookOff();
	SetDlgItemText(IDC_STATIC_INFO, _T("Hook未启动"));
}

参考界面:
在这里插入图片描述

VS2017 社区版调试通过。

说明:其中核心代码都是来自网友,笔者只不过是将64位和32位合并而已,并保留了原作的注释(也有本人注释),向原作表示感谢。

成文比较仓促,汇编解释可能不太严谨,有问题请谅解,但是代码已经测试成功。

马拉孙2020-12-29
于泛五道口地区

新年将至,预祝大家新年快乐。

猜你喜欢

转载自blog.csdn.net/Uman/article/details/111880672
今日推荐