Win32开发(二)对话框

ʕ •̀ o •́ ʔ

1 子窗口处理

在 Windows 中,不管是按钮还是菜单等组件都被操作系统认为是一个窗口。每个窗口都有对应的窗口类,只不过操作系统为我们预定义好了很多常用的窗口类,并且已经注册好了(窗口回调函数这些都是预定义好的)。

1.1 创建子窗口

注意:按钮这些都是窗口,操作系统已经为我们预定义好了很多窗口,如果我们使用Button等窗口的话,使用时直接调用CreateWindowExW创建窗口即可,因为操作系统为我们把这些窗口类设计并注册好了

1
2
3
4
5
6
7
8
9
10
hwndPushButton = CreateWindow ( 				
TEXT("Button"),
TEXT("普通按钮"),
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_DEFPUSHBUTTON,
10, 10,
80, 20,
hwnd,
(HMENU)1001, //当前窗口是子窗口时,表示当前窗口编号ID,在WM_COMMAND时使用
hAppInstance,
NULL);

我们都知道,窗口类里面包含窗口回调函数等成员,当我们使用操作系统提供的窗口时,如果我们想要获取这个窗口关于窗口类的信息,可以调用 GetClassNameW,该函数根据提供的窗口句柄返回窗口类名:

1
2
3
4
5
int GetClassNameW(
[in] HWND hWnd, // 想要获取窗口类名的窗口句柄
[out] LPWSTR lpClassName, // 用来接收窗口类名字符串的缓冲区
[in] int nMaxCount // 缓冲区的长度
);

然后根据得到的窗口类名获取窗口类信息:

1
2
3
4
5
BOOL GetClassInfoExW(
[in, optional] HINSTANCE hInstance, // 窗口所在模块基地址,wWinMain参数一的值
[in] LPCWSTR lpszClass, // 窗口类名
[out] LPWNDCLASSEXW lpwcx // 指向返回的窗口类结构
);

总结:

1、按钮是一种特殊的窗体,并不需要提供单独的窗口回调函数。

2、当按钮有事件产生时,会给父窗口消息处理程序发送一个 WM_COMMAND 消息。

1.2 子窗口消息处理

如果我们使用操作系统提供的预定义好的子窗口,那么我们是无法直接操作子窗口的窗口处理回调函数的。

但是操作系统会将这类预定义好的子窗口的消息,将其封装成一个 WM_COMMAND 消息,然后转发给该子窗口所属的父窗口。此时 WM_COMMAND 消息的 LOWORD(wParam) 表示子窗口的 ID

2.png

所以我们只需要在父窗口中对 WM_COMMAND 类型消息处理就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
)
{
switch(uMsg)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}

case WM_COMMAND:
{
switch(LOWORD(wParam))
{
case 1001:
MessageBox(hwnd,"Hello Button 1","Demo",MB_OK);
break;
default:
return DefWindowProc(hwnd,uMsg,wParam,lParam);
}
}
default:
return DefWindowProc(hwnd,uMsg,wParam,lParam);
}// end of switch(uMsg)
return 0;
}

1.3 OD定位子窗口消息

一、定位 WinMain 入口函数

在OD中选项-调试设置-事件,设置让程序断在OEP处(WinMainCRTStartup函数)。

由于 WinMain 程序有四个参数,且参数一是模块 ImageBase,如下图函数 GetMoudleHandleA 返回值 eax 直接被压栈,这里猜测地址 0x00401000 就是 WinMain 函数,回车跟进去。

3.png

二、定位主窗口的窗口处理回调函数

主窗口的回调函数是注册在窗口类 WNDCLASS 的第二个成员,且窗口类是由函数 RegisterClass 注册的,所以只要找到该函数就可以了。

如下图,在 0x0040104C 处压栈的 edx 即为 WNDCLASS 结构体的地址。先让程序断在0x0040104D 记录下此时 edx 指向的地址,然后让程序执行到 0x00401071 时观察 edx 指向的地址数据第二个成员地址为 0x00401110 即为主窗口回调函数的地址。

4.png

三、条件断点下断子窗口的消息

进入到主窗口回调函数,如下图。

5.png

回调函数的原型如下:

1
2
3
4
5
6
LRESULT CALLBACK WindowProc(  			
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
);

由上图可以看到,回调函数使用 esp 寻址,此时堆栈情况如下:

6.png

则需要在函数入口 0x00401110 处下一个条件断点,[esp+8]==WM_COMMAND

7.png

然后去随意点击一个子窗口按钮,发现此时程序被断下来了,此时 EIP == 0x00401110

8.png

但是如何精准的断在我们想要的子窗口上呢?

点击快捷 W 按钮打开当前的窗口,可以看到有 3 个 “Button” 类型的按钮窗口,所以此时在条件断点上再加一个判断条件即可,如我想要断在复选框上,其中一种方法就是在主窗口的回调函数修改判断条件为 [esp+8]==WM_COMMAND && [esp+0xC]==0x3EA

9.png

10.png

此时再去点复选框,消息就会被断下来了。

2 对话框

2.1 对话框知识

  1. 大多数应用程序都使用对话框提示输入需要用户输入的菜单项的其他信息。 使用对话框是应用程序检索输入的唯一建议方法。

  2. 有两种类型的对话框:有模式和无模式。区别:有模式的必须将该窗口关闭后,父窗口才能进行操作。可以参考:模式对话框与非模式对话框的区别

  3. 对话框模板是对话框及其包含的控件的二进制说明。 对话框模版可以从应用程序的可执行文件资源加载,也可以在应用程序运行时在内存中创建。应用程序使用 DialogBoxDialogBoxIndirect 函数创建模式对话框。 DialogBox 需要包含对话框模板的资源的名称或标识符; DialogBoxIndirect 需要包含对话框模板的内存对象的句柄。

  4. 每个对话框都需要对应的窗口处理回调函数,由开发人员提供。

  5. 应用程序通常使用 DialogBoxCreateDialog 函数创建对话框。 DialogBox 创建模式对话框; CreateDialog 创建无模式对话框。 这两个函数从应用程序的可执行文件加载对话框模板,并创建与模板规范匹配的弹出窗口。

  6. 对话框上的所有控件(子窗口)的消息,会发送到对话框的回调函数。子对话框的消息也可以发送到父对话框去处理(通知事件)。父窗口销毁时,子窗口会跟着被销毁。

  7. 应用程序使用 MessageBoxMessageBoxEx 函数创建的消息框也是对话框。

  8. 应用程序使用 EndDialog 函数销毁模式对话框。

    1
    2
    3
    4
    BOOL EndDialog(
    [in] HWND hDlg, //对话框句柄
    [in] INT_PTR nResult //该值将传给DialogBox,让DialogBox返回该值
    );
  9. 对话框中的每个控件必须具有唯一标识符才能将其与其他控件区分开来。 控件通过 WM_COMMAND 消息将信息发送到对话框过程,因此控制标识符对于确定发送指定消息的控件至关重要。 此规则的唯一例外是静态控件的控制标识符。 静态控件不需要唯一标识符,因为它们不发送 任何WM_COMMAND 消息。

2.2 创建对话框

创建对话框,不需要 CreateWindowExW 创建主窗口,简单分为以下三步:

  1. 创建资源文件,对话窗口、控件等(控件就是子窗口)。
  2. 定义 Dialog 窗口回调函数。
  3. DialogBox创建窗口。
  • 模版:由编译器资源提供的模版
  • 从资源创建对话框

使用资源模版创建对话框仅需要 2 步:

  1. 创建窗口
  2. 提供消息处理函数

函数 DialogBox :从对话框模板资源创建模式对话框(创建对话框使用DialogBox函数,而创建窗口使用CreateWindow)。定义如下:

1
2
3
4
5
6
INT_PTR DialogBox(					
HINSTANCE hInstance, //包含对话框模板的模块的句柄。如果此参数为 NULL,则使用当前的可执行文件。
LPCTSTR lpTemplate, //对话框句柄值,该值为指针或整数值。
HWND hWndParent, //父窗口句柄
DLGPROC lpDialogFunc //该窗口处理函数(回调函数)
);

lpTemplate :指针指向对话框模板名称,整数值则为资源标识符(也是句柄值),则其高位字必须为零,其低位字必须包含该标识符。使用 MAKEINTRESOURCE 宏来将整数值转换为指针。

返回值:该函数的返回值在 EndDialog 函数中指定,一般在 EndDialog(hwndDlg, 0) 中指定返回 0,如果要返回一个错误,可以返回-1。

一、VS2019 新建对话框:

  1. 新建对话框资源:选中项目右键-添加-资源-Dialog-新建。这时候就会多出了 resource.rc 资源文件和 resource.h 头文件。如果要再建立第二个对话框:在“资源文件”上右键-添加-资源-Dialog即可。

  2. 编辑窗口属性:对着窗口右键-属性即可。

  3. 在窗口添加控件:视图-工具箱-直接将需要的空间拖上去窗口即可。控件复制可以按住 Ctrl 拖拉,或者 Ctrl+C/V

  4. 控件对齐:按住 Ctrl 选择需要对齐的控件,右键即可选择。

  5. 显示最小化、最大化:对话框属性-行为-应用程序窗口(True)。

  6. 在对话框添加菜单:

    • 在资源右键-添加-菜单-属性修改名称
    • 在对话框属性-杂项-选择菜单。

    给菜单添加行为:就是添加自窗口事件处理函数。

  7. 修改对话框DPI:选中项目右键-属性-清单工具-输入和输出-DPI识别功能-高DPI识别。

  8. EDIT控件输出多行:必须选择属性:Auto Hscroll->False、MultiLine->True、Want Return ->True(If not \r\n)。

2.3 对话框回调函数

对话框过程