《C++黑客编程解密》01

黑客编程入门

windows就是一个大的死循环

开发的三种方式:

  1. SDK开发(C语言调用api)
  2. MFC(对于api的封装)
  3. 托管式开发(常见于  C# + .NET,C++也可以做)

消息来源:

  1. 操作系统产生
  2. 用户触发事件产生
  3. 由消息产生的消息

消息常见分类:

  1. 预定义消息
    1. 窗口消息 WM_
    2. 设备消息 DBT_
    3. 按钮消息 BM_
    4. 。。。
  2. 自定义消息 (以 WM_USER + 1 开始作为自己的编码)

消息的结构 MSG

  1. 窗口句柄
  2. 消息类型
  3. 高位参数
  4. 低位参数
  5. 事件
  6. 包含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 实现流程:

  1. WinMain
  2. MSG结构体用于处理消息
  3. 注册窗口类
  4. 创建窗口
  5. 显示窗口
  6. 刷新窗口
  7. 消息循环

消息机制

Windows下窗口应用都是基于消息机制的,os与应用程序间、应用于应用之间,大部分都是通过消息机制进行通信。

DOS程序执行:

单任务操作系统,顺序执行。 Command.com -> DOS 程序 -> Command.com

Windows 程序执行流程:

这是简化图,实际上Windows内部比这更复杂。现在主要关注“主程序”和“窗口过程”,二者间有“系统程序模块”。

主程序用于注册窗口类、获取消息、分发消息。使用 RegisterClassEx() 注册窗口类,这个类中包含窗口过程的地址;再通过 GetMessage() 获取消息;再用 DispatchMessage() 分发消息。消息分发后没有直接调用“窗口过程”,而是由系统模块查找指定的窗口类,通过窗口类再找到窗口过程的地址,再将消息送给窗口过程进行处理。

窗口过程定义了需要处理的消息,根据不同消息不同操作,还可以交给系统的默认系统过程处理。

消息队列是属于线程的,是windows系统为线程创建并维护的一个队列,用于存放各类消息。 系统自身维护一个系统消息队列,然后还为每个GUI线程线程维护一个线程专门消息队列。每个线程默认没有消息队列,只有在线程第一次调用GDI函数时才为其创建消息队列。一个应用程序可以有多个线程,但只能有一个UI线程,默认为主线程,其他子线程是无法操作UI并创建UI元素的。

一个Win32程序

WinMain 中流程:注册一个窗口类,创建一个窗口并显示,然后不停的获取属于自己的消息并分发给自己的窗口过程,直到收到 WM_QUIT 后退出。

  1. 创建窗口 WNDCLASS
  2. 注册窗口 RegisterClass(&WndClass)
  3. 创建窗口 CreateWindow()
  4. 显示窗口 ShowWindow(hwnd, nCmdShow)
  5. 更新窗口 UpdateWindow(hwnd)
  6. 获取消息 GetMessage(&msg, NULL, 0, 0))
  7. 翻译消息 TranslateMessage(&msg)
  8. 分发消息 DispatchMessage(&msg)
  9. 窗口过程
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 消息循环详解:

  1. 创建完 win32 程序后,当用户对鼠标、键盘操作应用时,由于os监控IO设备,故事件首先转化为消息,由windows 捕获,存放于系统消息队列
  2. os直到消息改由哪个应用处理,然后拷贝到相应的程序消息队列,同时从系统消息队列中删除。
  3. 应用中的消息循环不断执行,从 GetMessage() 从应用消息队列中查获消息,返回一个正值并获得消息。队列为空时程序阻塞。
  4. 取出消息后使用 TranslateMessage() 处理虚拟键盘信息。
  5. 调用 DispatchMessage(),此函数将消息再给 windows 系统,os找到目标窗口并分发给该窗口,调用消息对应的窗口过程函数。
  6. 消息处理完后,窗口过程返回,消息循环继续,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下进程通信手段有:

  1. 管道
  2. 内存共享
  3. 剪贴板
  4. COM
  5. 数据复制
  6. DDE
  7. 文件映射
  8. Mailslots
  9. 管道
  10. RPC
  11. Windows 套接字
  12. 。。。

使用消息机制通信有一定限制性。没有窗口的应用没有消息驱动,无法通过消息进行通信。以下两种方法:

  1. 通过自定义信息进行进程通信
  2. 通过 WM_COPYDATA 通信

通过自定义信息进行进程通信

消息分两种:

  1. 系统定义,从 0 到 0x3ff
  2. 用户自定义,从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中程序运行方式:

  1. Step Into 单步步入:会进入函数内部
  2. Step Over 单步步过:不会进入调用的函数
  3. Step Out :执行到函数返回处
  4. Run to Cursor :执行到光标处

三个调试命令:

  1. F9 光标处设置断点
  2. F5 进入调试状态
  3. F7 结束调试状态下的程序

 

posted @ 2022-07-28 23:18  某某人8265  阅读(395)  评论(0)    收藏  举报