缘由:本来想做个微信的机器人,发现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
于泛五道口地区
新年将至,预祝大家新年快乐。