消息断点
1.快捷创建窗口
win32程序创建窗口需要:定义窗口类、注册窗口类、创建窗口、写回调函数来处理消息、添加消息循环来接收消息等等;
还有另外一种创建窗口的方式:利用资源文件;
先创建一个空的win32项目;
在项目中添加资源文件:
File -> New -> Resource Script
创建成功后会新增2个文件:xxx.rc 和 resource.h;这两个文件一般不要手动去改,是vc6自动维护的;
resource.h添加到了工程目录中,只不过在vc6中看不到,还需要手动添加vc6中;
右键选中项目工程 -》Add Files to project -》选择resource.h;
1)新增对话框窗口
打开.rc文件 -》右键选中rc中的文件夹选InsertResource -》选对话框;
可以看到生成了一个对话框窗口;
右键点击窗口 -》properties可以对该窗口属性做修改;
打开resource.h,可以看到自动生成了一个该窗口id的宏;
2)画对话框
新增了一个对话框窗口,还需要在winmain中画出该窗口才行;
画普通窗口时,是调用createwindow函数;
这里调用的是 DialogBox函数
INT_PTR DialogBox( HINSTANCE hInstance, // handle to module 这个是winmain的hinstance,也就是程序的ImageBase LPCTSTR lpTemplate, // dialog box template resource.h中生成的窗口id,需要转成char*类型; HWND hWndParent, // handle to owner window 父窗口句柄,NULL DLGPROC lpDialogFunc // dialog box procedure 对话框的回调函数 );
关于参数LPCTSTR lpTemplate;
这里需要的是字符串,而resource.h生成的是数字类型;
因此需要强转成char*;也可以用函数宏来将数字转为字符串:MAKEINTRESOURCE(DLG_FIRST);
类似的有通过函数名或函数导出序号找函数地址的:GetProcAddress;
有些函数没有通过名字导出,只有序号,因此GetProcAddress可以传入char*类型的序号或者函数名,在函数内部判断到底是字符串还是数字然后分别处理;
还需要定义回调函数;
回调函数和普通窗口不同,需要处理的消息返回TRUE,不需要处理的返回FALSE,而不是用default来交给系统来处理;
回调函数:
BOOL CALLBACK DialogProc( HWND hwndDlg, // handle to dialog box UINT uMsg, // message WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ) { switch(uMsg) { case WM_INITDIALOG : MessageBox(NULL,TEXT("WM_INITDIALOG"),TEXT("INIT"),MB_OK); return TRUE ; case WM_COMMAND : switch (LOWORD (wParam)) { case IDC_BUTTON_OK : MessageBox(NULL,TEXT("IDC_BUTTON_OK"),TEXT("OK"),MB_OK); return TRUE; case IDC_BUTTON_OUT: MessageBox(NULL,TEXT("IDC_BUTTON_OUT"),TEXT("OUT"),MB_OK); EndDialog(hwndDlg, 0); return TRUE; } break ; } return FALSE ; }
3)拖控件
可以在控件栏点击控件,拖道对话框中,来给对话框添加控件;也可以直接双击控件来添加;
可以用vc6下方的工具栏调整选中的控件的格式:
4)获取控件中的内容
例如获取文本框的内容需要两步:
1】获取文本框句柄
参数:
父窗口局部 ->这里是对话框句柄;
控件id ->自动生成的,在resource.h中可以看到
HWND hEditUser = GetDlgItem(hDlg,IDC_EDIT_USER);
2】通过文本框句柄获取文本框内容
不同的控件获取内容的函数可能不同;
参数:
控件句柄 ->在上一步中获取
缓冲区 ->获取的窗口内容在缓冲区中存放
缓冲区大小
TCHAR szUserBuff[0x50]; //定义一个缓冲区 GetWindowText(hEditUser,szUserBuff,0x50); //将获取的内容放缓冲区
5)登录验证
例如:点击登陆按钮时验证用户名和密码,如果是“admin”和“123”时弹ok,否则弹error;
思路:
在登陆按钮点击事件中获取用户名和密码文本框的内容;
按钮点击事件在WM_COMMAND消息中处理;
WM_COMMAND消息处理里面的switch中找到登陆按钮对应的case即可;
获取两个文本框的内容;
与字符串“admin”和“123”比较,根据结果弹不同的框;
代码:
#include "stdio.h" #include "windows.h" #include "resource.h" BOOL CALLBACK DialogProc( HWND hwndDlg, // handle to dialog box UINT uMsg, // message WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); int CALLBACK WinMain( //CALLBACK表示__stdcall HINSTANCE hInstance, //相当于imagebase HINSTANCE hPrevInstance, //永远都是NULL不用管 LPSTR lpCmdLine, //命令行启动程序时后面接的参数用这个来读 int nCmdShow //以什么方式显示:最大化、最小化、隐藏 ){ //添加一个对话框窗口 DialogBox( hInstance, MAKEINTRESOURCE(DLG_FIRST), NULL, DialogProc ); return 0; } //回调函数 BOOL CALLBACK DialogProc( HWND hwndDlg, // handle to dialog box UINT uMsg, // message WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ) { HWND t_userName; HWND t_pwd; switch(uMsg) { case WM_INITDIALOG : //MessageBox(NULL,TEXT("WM_INITDIALOG"),TEXT("INIT"),MB_OK); return TRUE ; //用rc创建的窗口回调函数不需要处理默认消息,处理的return TRUE,不处理的return FALSE case WM_COMMAND : //处理按钮消息 switch (LOWORD (wParam)) { case BTN_OK : //MessageBox(NULL,TEXT("BTN_OK"),TEXT("OK"),MB_OK); //获取文本框句柄 t_userName = GetDlgItem(hwndDlg, TXT_NAME); t_pwd = GetDlgItem(hwndDlg, TXT_PWD); //获取文本框内容 TCHAR userBuf[0x50]; TCHAR pwdBuf[0x50]; GetWindowText(t_userName, userBuf, 0x50); GetWindowText(t_pwd, pwdBuf, 0x50); if(!lstrcmp(userBuf,TEXT("admin")) && !lstrcmp(pwdBuf, TEXT("123"))){ MessageBox(NULL,TEXT("ok"), TEXT("验证"), MB_OK); }else{ MessageBox(NULL,TEXT("err"), TEXT("验证"), MB_OK); } return TRUE; case BTN_OUT: MessageBox(NULL,TEXT("BTN_OUT"),TEXT("OUT"),MB_OK); EndDialog(hwndDlg, 0); //店家按钮时关闭对话框 return TRUE; } break ; } return FALSE ; }
踩坑:
使用文本框句柄时:
编译时提示:“error C2361: initialization of 't_userName' is skipped by 'default' label”。
原因:
C++约定,在块语句中,对象的作用域从对象的声明语句开始直到块语句的结束,也就是说default标号后的语句是可以使用对象a的。如果程序执行时从switch处跳到default处,就会导致对象a没有被正确地初始化。确保对象的初始化可是C++的重要设计哲学,所以编译器会很严格地检查这种违例情况,像上述的示例代码中default语句后面并没有使用a,但考虑到以后代码的改动可能无意中使用,所以一样被封杀。
解决:将文本框句柄定义提到switch上面;
2.利用消息断点分析程序
在分析win32程序时可以用以下几步:
找入口函数 ->GetModuleHandleA后面的call,有4个参数;
找消息处理函数 ->RegisterClass函数注册WNDCLASS,WNDCLASS结构中包含消息处理函数的地址;
过滤消息 ->下条件断点,消息类型一般在wparam中,也就是[esp+8]==特定消息类型;
但有时候当程序很复杂时,难以找到消息处理函数,需要一些技巧;
可以利用消息断点来分析程序;
消息断点的本质就是条件断点;
例如:利用消息断点分析登陆的用户名和密码
用od带卡程序,按F9运行程序,直到窗口出现;
点od上放的W查看子窗口信息;
如果看不到,可以右键-》Actuallize
可以看到各个子窗口的信息;
.rc创建的窗口系统会自动给每个组件窗口生成消息处理函数,ClsProc就是该组件消息处理函数的地址;
组件的消息处理函数最终会调用我们自己写的消息处理函数;
也就是说登陆按钮的消息处理函数最终会调用我们写的消息处理函数;
利用这一点在登陆按钮的消息处理函数中下断点,然后从按钮的消息处理函数追踪到程序的消息处理函数;
在登陆按钮的ClsProc处按右键-》选消息条件断点;
消息条件断点本质上就是条件断点,只不过是od帮我们下;
然后选消息类型:
这里选鼠标左键抬起事件而不是wm_command;
虽然按钮最终传递到程序的消息处理函数时是WM_COMMAND消息,但这里是按钮自己的消息处理事件,而不是程序的;
这时会在系统的消息处理函数中下一个条件断点;
可以看到所有的按钮组件处都被下了断点,因为系统提供的消息处理函数对相同的组件都是用的同一个;
点击od上方的B查看断点窗口;
右键点击下的消息断点-》Edit condition,可以看到实际上消息断点就是一个条件断点;
按钮的消息处理函数最终会调用程序的消息处理函数;
接下来要找到程序的消息处理函数,也就是我们自己写的回调函数;
但并不知道程序的回调函数在哪里被调用的;
利用内存访问断点:
因为程序中的回调函数是在pe结构的代码段中;
可以下内存访问断点来找回调函数;
系统提供的消息处理函数在dll中,而不再程序的pe结构中;
当程序执行时跳到pe结构的代码段时,说明此时可能调用了回调函数;
点击登陆按钮,因为下了断点,程序会被断下来;
点击od上方的M,查看程序的内存窗口,在代码段处下内存访问断点;
这样当点击登陆后,如果消息处理函数访问了程序的代码段就会被断下来;
接着按F9让程序继续运行,程序会在访问代码段时停下来;
还需要确定访问代码段的消息是否是登录按钮的点击事件发出的;因为还有其它消息同样会访问代码段;
分析:
按钮传到回调函数时的消息是WM_COMMAND,对应0x111;
如果是调用的回调函数,那么第二个参数就是消息类型,如果值是0x111说明找到了;
而第二个参数在[esp+8]处,因为在调用函数时栈顶为函数返回地址,[esp+4]为第一个参数,[esp+8]为第二个参数;
观察堆栈:不是0x111,因此不是要找的函数;
按F8执行,因为下了内存访问断点,此时每一步都会被断下,执行到脱离代码段后,按F9,使程序再次被断下;
重复上面的操作放过不需要的消息,直到找到wm_command消息;
此时说明找到了回调函数;
在回调函数处下条件断点,条件为消息类型是WM_COMMAND,即[esp+8]==WM_COMMAND;
然后删除其它断点,运行程序,点击登陆按钮,在断点处单步运行分析;
可以看到,用户名和密码分别是“admin”,“123”,当正确时弹出ok;
在程序中试验:
成功;