《C++黑客编程解密》01
黑客编程入门
windows就是一个大的死循环
开发的三种方式:
- SDK开发(C语言调用api)
- MFC(对于api的封装)
- 托管式开发(常见于 C# + .NET,C++也可以做)
消息来源:
- 操作系统产生
- 用户触发事件产生
- 由消息产生的消息
消息常见分类:
- 预定义消息
- 窗口消息 WM_
- 设备消息 DBT_
- 按钮消息 BM_
- 。。。
- 自定义消息 (以 WM_USER + 1 开始作为自己的编码)
消息的结构 MSG:
- 窗口句柄
- 消息类型
- 高位参数
- 低位参数
- 事件
- 包含xy位置的结构体
/*
* Message structure
*/
typedef struct tagMSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;
SDK 实现流程:
- WinMain
- MSG结构体用于处理消息
- 注册窗口类
- 创建窗口
- 显示窗口
- 刷新窗口
- 消息循环
消息机制
Windows下窗口应用都是基于消息机制的,os与应用程序间、应用于应用之间,大部分都是通过消息机制进行通信。
DOS程序执行:
单任务操作系统,顺序执行。 Command.com -> DOS 程序 -> Command.com

Windows 程序执行流程:

这是简化图,实际上Windows内部比这更复杂。现在主要关注“主程序”和“窗口过程”,二者间有“系统程序模块”。
主程序用于注册窗口类、获取消息、分发消息。使用 RegisterClassEx() 注册窗口类,这个类中包含窗口过程的地址;再通过 GetMessage() 获取消息;再用 DispatchMessage() 分发消息。消息分发后没有直接调用“窗口过程”,而是由系统模块查找指定的窗口类,通过窗口类再找到窗口过程的地址,再将消息送给窗口过程进行处理。
窗口过程定义了需要处理的消息,根据不同消息不同操作,还可以交给系统的默认系统过程处理。
消息队列是属于线程的,是windows系统为线程创建并维护的一个队列,用于存放各类消息。 系统自身维护一个系统消息队列,然后还为每个GUI线程线程维护一个线程专门消息队列。每个线程默认没有消息队列,只有在线程第一次调用GDI函数时才为其创建消息队列。一个应用程序可以有多个线程,但只能有一个UI线程,默认为主线程,其他子线程是无法操作UI并创建UI元素的。
一个Win32程序
WinMain 中流程:注册一个窗口类,创建一个窗口并显示,然后不停的获取属于自己的消息并分发给自己的窗口过程,直到收到 WM_QUIT 后退出。
- 创建窗口
WNDCLASS类 - 注册窗口
RegisterClass(&WndClass) - 创建窗口
CreateWindow() - 显示窗口
ShowWindow(hwnd, nCmdShow) - 更新窗口
UpdateWindow(hwnd) - 获取消息
GetMessage(&msg, NULL, 0, 0)) - 翻译消息
TranslateMessage(&msg) - 分发消息
DispatchMessage(&msg) - 窗口过程
windows 桌面程序示例
#include <Windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(
HINSTANCE hInstance, // 本程序的在内存的起始地址,可通过 GetModuleHandle() 获得
HINSTANCE hPrevInstance, // win16 遗留,现在不再用
PSTR szCmdLine, // 启动参数
int nCmdShow) // 启动方式,最大化、最小化、隐藏 等。
{
static TCHAR szAppName[] = TEXT("HelloWin");
WNDCLASS wndclass;
ZeroMemory(&wndclass, sizeof(wndclass));
HWND hwnd;
MSG msg;
// 1. 创建窗口类
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1; // 背景色句柄
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); // 鼠标句柄
wndclass.hIcon = LoadIcon(NULL, IDI_QUESTION); // 图标句柄
wndclass.lpszMenuName = NULL;
wndclass.lpfnWndProc = WndProc; // 设置窗口过程地址
wndclass.lpszClassName = szAppName; // 窗口类类名
// 2. 注册窗口类
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("regest windwos class failed!"), szAppName, MB_ICONERROR);
return 0;
}
// 3. 创建窗口
hwnd = CreateWindow(szAppName, // 窗口类名称
TEXT("The Hello Program"), // 标题
WS_OVERLAPPEDWINDOW, // 风格,窗口格式。此处使用的是常见的通过位运算符合的类型
CW_USEDEFAULT, // 初始x坐标,相对屏幕左上角
CW_USEDEFAULT, // 初始y坐标,使用的是默认值
CW_USEDEFAULT, // 初始x方向尺寸,窗口初始宽度
CW_USEDEFAULT, // 初始y方向尺寸
NULL, // 父窗口句柄,子窗口总是在父窗口前
NULL, // 窗口菜单句柄
hInstance, // 程序实例句柄
NULL); // 创建参数
// 4. 显示窗口,第二个参数决定窗口在屏幕中的初始显示形式
// 正常 SW_SHOWNORMAL
// 最小化 SW_SHOWMAXIMIZED
// 最大化 SW_SHOWMINNOACTIVE
ShowWindow(hwnd, nCmdShow);
// 5. 指示窗口对自身进行重绘,向窗口过程发送一条 WM_PAINT 消息
UpdateWindow(hwnd);
// 6. 获取消息
// win为每个程序维护了一个消息队列,输入事件发生后win会将这些事件转化为“消息”
// 从消息队列中检索,当message字段不为 WM_QUIT 时返回非0值,否则返回0
/*
* BOOL GetMessage (
* LPMSG lpMsg. // message information
* HWND hWnd, // handle to window
* UINT, wMsgFilterMin, // first message
* UINT, wMsgFilterMax // last message
* );
*
* 类似函数:PeekMessage(),判断消息队列中是否有消息,若没有则主动让出cpu时间。
*/
while (GetMessage(&msg,
NULL,
0,
0))
{
// 7. 翻译一些键盘消息
// 将虚拟键码转换为字符消息
// 就是将 WM_KEYDOWN WM_KEYUP 转换为 WM_CHAR
// 将 WM_SYSKEYDOWN WM_SYSKEYUP 转换为 WM_SYSCHAR
TranslateMessage(&msg);
// 8. 将消息发送给窗口过程
DispatchMessage(&msg);
}
return msg.wParam;//参数通常为 0
}
/************************* 窗口过程 *************************/
// win prock :处理消息的函数。这4个参数与Message结构体的前4个参数一一对应
LRESULT CALLBACK WndProc(
HWND hwnd, // 接受消息的窗口的句柄
UINT message, // 标识消息的数字,WINUSER.H中定义各种以WM开头的标识符
WPARAM wParam,
LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (message)
{
// 调用 CreateWindow 函数创建窗口时生成此消息
case WM_CREATE:
{
MessageBox(hwnd, TEXT("创建成功"), TEXT("title"), MB_DEFBUTTON1);
return 0;
}
/*
* 当窗口的客户区的部分或全部“无效”且需要“更新”时,应用得到此通知,意味着窗口需要重绘。
* 何时客户区无效?
* 首次创建时整个客户区都是无效的,因为此时应用尚未在该窗口上绘制任何东西。第一条WM_PAINT 消息通常在UpdateWindow时出现。
* 调整窗口大小时客户区也会变得无效,此后窗口过程接收到一条 WM_PAINT 消息。
* 最小化最大化、拖动窗口发生遮盖时都会引起无效。
*/
case WM_PAINT://DefWindowProc 中的默认处理就是简单调用 BeginPaint EndPaint
{
// 表明窗口绘画开始,第二个参数是指向PAINTSTRUCT结构的指针
// 返回一个设备环境句柄,指物理输出设备机器驱动。使用它只能在客户区内绘制
hdc = BeginPaint(hwnd, &ps);
// 获取窗口客户区的尺寸,设置rect结构体中的 left top right bottom,left top总为0,right bottom为像素个数
GetClientRect(hwnd, &rect);
// 显示一个文本字符串
DrawText(hdc,
TEXT("Hello, win32!"),
-1, // 表示文本字符串以0结尾
&rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER);//规定格式
// 结束窗口绘画,释放 hdc
EndPaint(hwnd, &ps);
return 0;
}
case WM_DESTROY:
{
// 将“退出”消息 WM_QUIT 插入消息队列,用于退出消息循环
PostQuitMessage(0);
return 0;
}
case WM_CLOSE:
{
if (IDYES == MessageBox(hwnd, TEXT("是否退出程序?"), TEXT("退出"), MB_YESNO))
{
DestroyWindow(hwnd);
PostQuitMessage(0);
}
}
default:
break;
}
// 执行默认消息处理,不进行处理的消息都必须传给此函数。必须要有,否则结束程序等功能无法进行
return DefWindowProc(hwnd, message, wParam, lParam);
}
windows 消息循环详解:
- 创建完 win32 程序后,当用户对鼠标、键盘操作应用时,由于os监控IO设备,故事件首先转化为消息,由windows 捕获,存放于系统消息队列
- os直到消息改由哪个应用处理,然后拷贝到相应的程序消息队列,同时从系统消息队列中删除。
- 应用中的消息循环不断执行,从 GetMessage() 从应用消息队列中查获消息,返回一个正值并获得消息。队列为空时程序阻塞。
- 取出消息后使用 TranslateMessage() 处理虚拟键盘信息。
- 调用 DispatchMessage(),此函数将消息再给 windows 系统,os找到目标窗口并分发给该窗口,调用消息对应的窗口过程函数。
- 消息处理完后,窗口过程返回,消息循环继续,Windows系统继续监控消息。
模拟鼠标键盘按键操作
可以使用 SendMessage() PostMessage() 外,还可以通过 keybd_event() mouse_event() 这两个专门的模拟操作。
基于发送消息的模拟


模拟鼠标和键盘按键消息时最好使用 PostMessage() 而不是 SendMessage()。
void fun()
{
HWND hWnd = FindWindow(L"MozillaCompositorWindowClass", NULL);
if (hWnd == NULL)
{
printf("dont find\n");
return 0;
}
PostMessage(hWnd, WM_KEYDOWN, VK_F5, 1);
PostMessage(hWnd, WM_KEYUP, VK_F5, 1);
}
通过API函数模拟鼠标键盘操作
Windows 下大多数消息都可以直接使用对应的等价API函数,不必直接通过发送消息。如 WM_GETTEXT 消息对应 GetWindowText() ,都用于获取文本内容。
WINUSERAPI
VOID
WINAPI
keybd_event(
_In_ BYTE bVk, // virtual-key code 详情可见:https://docs.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
_In_ BYTE bScan, // hardware scan code
_In_ DWORD dwFlags, // function options
_In_ ULONG_PTR dwExtraInfo); // additional keystroke data
WINUSERAPI
VOID
WINAPI
mouse_event(
_In_ DWORD dwFlags, // motion and click options
_In_ DWORD dx, // horizontal position or change
_In_ DWORD dy, // vertical position or change
_In_ DWORD dwData, // wheel movement
_In_ ULONG_PTR dwExtraInfo); // application-defined information
WINUSERAPI
BOOL
WINAPI
ClientToScreen( // 获得窗口在屏幕中的位置
_In_ HWND hWnd,
_Inout_ LPPOINT lpPoint);
WINUSERAPI
BOOL
WINAPI
SetCursorPos( // 鼠标移动到指定位置
_In_ int X,
_In_ int Y);
在给应用使用这种函数传递消息时必须将其置于激活状态,搭配 SetForegroundWindow 使用
WINUSERAPI
BOOL
WINAPI
SetForegroundWindow(
_In_ HWND hWnd
);
示例:
鼠标和键盘按键事件
#include <windows.h>
#include <iostream>
int main()
{
//启动记事本
WinExec("notepad.exe", SW_SHOW);
system("pause");
// 找到记事本
HWND hwnd = FindWindow(TEXT("Notepad"), NULL);
if (hwnd == NULL)
{
printf("not find Notepad");
return 0;
}
SetForegroundWindow(hwnd);
//键盘事件
keybd_event(0x42, 0, 0, 0);
Sleep(300);
keybd_event(0x43, 0, 0, 0);
Sleep(300);
keybd_event(0x44, 0, 0, 0);
//鼠标事件
POINT pt = { 0 };
ClientToScreen(hwnd, &pt); // 获得窗口在屏幕的坐标
SetCursorPos(pt.x + 20, pt.y + 20); // 移动指针到指定位置
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0); // 鼠标右键按下
Sleep(300);
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0); // 鼠标右键释放
keybd_event(0x41, 0, 0, 0); // 键盘A键,全选
PostMessage(hwnd, WM_CLOSE, 0, 0); // 关闭
std::cout << "main 函数结束\n";
}
鼠标和键盘事件在很多地方都能用到,如:病毒单击AV的警告提示;外挂进行快速单击。。。
但有些游戏过滤了PostMessage发送的消息;有些hook了 keydb_event 和 mouse_event函数;有些使用DX响应鼠标和键盘。。。
通过消息实现进程间通信
Windows下进程通信手段有:
使用消息机制通信有一定限制性。没有窗口的应用没有消息驱动,无法通过消息进行通信。以下两种方法:
- 通过自定义信息进行进程通信
- 通过
WM_COPYDATA通信
通过自定义信息进行进程通信
消息分两种:
- 系统定义,从 0 到 0x3ff
- 用户自定义,从0x400开始系统没有定义,系统提供了一个宏 WM_USER。自定义消息时在这个宏基础上加值即可
软件一:
发送自定义消息
application01
#include <Windows.h>
#include <stdio.h>
#define WM_UMSG WM_USER + 1
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR szCmdLine,
int nCmdShow)
{
HWND hwnd = FindWindow(L"MyWndClass", NULL);
if (hwnd == NULL)
{
MessageBox(NULL, L"dont find window\n", L"xxx", MB_OK);
return 0;
}
PostMessage(hwnd, WM_UMSG, 1, 1);
return 0;
}
软件二:
接受消息,将两个参数相加
application02
#include <Windows.h>
#include <stdio.h>
#include <string.h>
#define WM_UMSG WM_USER + 1
LRESULT CALLBACK WnProc(
HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam)
{
HDC hdc;
PAINTSTRUCT ps;
RECT rect;
switch (msg)
{
case WM_UMSG:
{
wchar_t wstr[10] = { 0 };
swprintf_s(wstr, L" %d ", (int)(wparam + lparam));
MessageBox(NULL, (LPWSTR)wstr, L"结果", MB_OK);
break;
}
case WM_CLOSE:
{
DestroyWindow(hwnd);
PostQuitMessage(0);
}
default:
break;
}
return DefWindowProc(hwnd, msg, wparam, lparam);
}
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
PSTR szCmdLine,
int nCmdShow)
{
WNDCLASS wndclass;
ZeroMemory(&wndclass, sizeof(WNDCLASS));
wndclass.lpfnWndProc = WnProc;
wndclass.lpszClassName = L"MyWndClass";
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("regest windwos class failed!"), L"failed", MB_ICONERROR);
return 0;
}
HWND hwnd = CreateWindow(
wndclass.lpszClassName,
L"windows name",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL
);
ShowWindow(hwnd, SW_NORMAL);
UpdateWindow(hwnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
通过 WM_COPYDATA 通信
自定义消息传递的数据过于简单,通过 WM_COPYDATA 消息进行进程间通信会更加灵活。但由于 SendMessage() 的阻塞机制,传递数据时也不宜过多。必须用 SendMessage() 发送,不能使用 PostMessage() 发送。
SendMessage(
hWnd, // handle to destination window
WM_COPYDATA, // message to send
wParam, // handle to window (HWND) 发送消息的窗口句柄,可省略
lParam // data (PCOPYDATASTRUCT) COPYDATASTRUCT 结构体指针
);
typedef struct tagCOPYDATASTRUCT {
ULONG_PTR dwData; // 自定义数据
DWORD cbDATA; // 指向数据的大小
PVOID lpData; // 指向数据的指针
} COPYDATASTRUCT, *PCOPYDATASTRUCT;
PostMessage 与 SendMessage 区别
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
BOOL PostMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
参数相同,返回值不同
LRESULT 表示的是消息被处理后的返回值,BOOL 表示的是消息是不是 Post 成功。
PostMessage 是异步的,SendMessage 是同步的。PostMessage会将消息压入窗口所在线程的消息队列,然后立即返回;而SendMessage则不经过消息队列,SendMessage可认为是直接调用了该窗口的窗口过程,因此在我们需要获得消息处理后的返回值的时候,就要用到SendMessage。
在多线程应用中,PostMessage的用法还是一样,但SendMessage则不同了。如果在线程A中向线程B所创建的一个窗口hWndB发送消息SendMessage(hWndB,WM_MSG,0,0),那么系统将会立即将执行权从线程A切换到线程B,然后在线程B中调用hWndB的窗口过程来处理消息,并且在处理完该消息后,执行权仍然在B手中!这个时候,线程A则暂停在SendMessage处,等待下次线程A获得执行权后才继续执行,并且仍然可以获得消息处理的结果(返回值)。一般,为了避免死锁,在B中对WM_MSG做出处理之前,要加上:
if(InSendMessage())
{
RelpyMessage(lResult);
}
即,如果该消息来自另一个线程,则立即 ReplyMessage,LResult就是返回值。如果在同一线程内 InSendMessage 返回 False。
一个关于消息的例子:
关于消息的例子
#include <Windows.h>
#include <iostream>
#include <stdio.h>
#include <string>
void OnExec(LPSTR cmd)
{
WinExec(cmd, SW_SHOW);
//BOOL success = CreateProcess(cmd, cmd, NULL, NULL, TRUE, 0, NULL, NULL, NULL, NULL);
}
void OnEditWnd()
{
HWND hWnd = FindWindow(
NULL, // class name
L"无标题 - 记事本" // window name
);
if (hWnd == NULL)
{
printf("not find \"无标题 - 记事本\"");
return;
}
SendMessage(
hWnd, // handle to destination window
WM_SETTEXT, // message
0, // first parameter
(LPARAM)TEXT("更改后的名称") // second parameter
);
}
void OnGetWnd()
{
HWND hWnd = FindWindow(TEXT("Notepad"), NULL);
if (hWnd == NULL)
{
printf("not find Notepad");
return;
}
char CaptionText[100] = { 0 };
SendMessage(hWnd, WM_GETTEXT, 100, (LPARAM)CaptionText);
//printf("get caption : %s\n", CaptionText);
MessageBox(NULL, (LPCWSTR)CaptionText, (LPCWSTR)"", MB_OK);
}
void OnClose()
{
HWND hWnd = FindWindow(L"Notepad", NULL);
if (hWnd == NULL)
{
printf("not find Notepad\n");
return;
}
PostMessage(hWnd, WM_CLOSE, NULL, NULL);
}
int main()
{
OnExec((LPSTR)"notepad.exe");
OnGetWnd();
OnEditWnd();
OnGetWnd();
system("pause");
OnClose();
std::cout << "main 函数结束\n";
}
辅助工具
Spy++
有些软件窗口标题会根据不同情况进行改变,但是类名通常不会变。Spy++ 这个工具用于显示系统进程、窗口之间的关系,提供窗口各种信息,对指定窗口进行消息监控。
路径:visual studio 菜单 -> 工具 -> Spy++
有些软件有反 Spy++ 功能,通过 FindWindow() 函数找到Spy++窗口。使用“隐藏Spy++”可以有效避免反检测。

选中pdf阅读器,得到句柄和类名 。每次生成进程会获得不同的句柄,单类名不会变。

Error Lookup 工具使用


在程序中出错后通过 GetLastError()得到错误码,通过此工具获得错误详细信息。
调试工具
编辑好代码后按 F10 进入调试状态

如果没有需要的窗口可以通过菜单项“调试”添加上

4中程序运行方式:
- Step Into 单步步入:会进入函数内部
- Step Over 单步步过:不会进入调用的函数
- Step Out :执行到函数返回处
- Run to Cursor :执行到光标处
三个调试命令:
- F9 光标处设置断点
- F5 进入调试状态
- F7 结束调试状态下的程序

浙公网安备 33010602011771号