作者:冀云
出版社:信息技术第一出版分社
格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT
C++ 黑客编程揭秘与防范(第2版)试读:
前言
备受关注的黑客到底是什么“黑客”已经成为一个热门的话题,“黑客事件”更是已经受到各大媒体的关注,甚至很多媒体对黑客事件进行不负责任的报道与炒作。从传统媒体到互联网媒体,从平面媒体到多元化的媒体平台,都在报导黑客事件,报道新爆发的蠕虫、病毒等一系列相关事件。在各种媒体的炒作下,越来越多的年轻人都在追逐“当黑客的潮流”,很多热爱计算机的年轻人都追求自己能成为黑客。很多人都期望自己能一蹴而就成为一名顶级的黑客。很多人读着“头号电脑骇客”凯文米特尼克的传奇故事,在波涛汹涌的心情下,幻想着自己也能成为网络上出神入化的黑客风云人物。为此,很多“追黑”的网友深入各大黑客网站下载各种黑客工具来进行入侵、破坏(在用别人工具的同时,说不定也会遭到一些不怀好意的黑客网站的暗算,比如自己用的黑客工具本身就被放置了后门),从而满足自己追求成为黑客的心理。
在追求成为黑客的过程中真地学到了多少知识,自身的技术水平距离真正的黑客到底还有多远,这是很多只会使用工具的黑客应该冷静下来认真思考的问题。他们应该思考自己是否了解 TCP、UDP、ICMP 等 TCP/IP 中常用协议的结构与协议原理,是否知道ARP欺骗的原理,是否能独立开发并完成一个后门等。
什么是黑客?百度百科里黑客的含义如下(摘自百度百科,且有改动):“热衷研究、撰写程序的专才,精通各种计算机语言和系统,且必须具备乐于追根究底、穷究问题的特质。‘黑客’一词是由英语 Hacker 音译出来的,是指专门研究、发现计算机和网络漏洞的计算机爱好者。早期在美国的电脑界是带有褒义的。”
看到上面百度百科给出的黑客含义后,是不是很多只会使用工具的所谓的黑客能明白一个道理,即黑客是要会编写程序的?
再看一下百度百科里对只会使用工具的黑客的解释(摘自百度百科):“脚本小子,英文 script kiddie 或 script boy。脚本小子指的是用别人写的程序的人。脚本小子是一个贬义词,用来描述以黑客自居并沾沾自喜的初学者。”
那些自以为是的工具黑客只不过是一个“脚本小子”,还是一个被大家所“鄙视”的“小子”。是不是心里觉得不是很舒服了?是不是觉得自己应该提高了?如果是的话,那么就请抛开以前当工具黑客的想法,开始编写黑客工具吧!心态影响成长
新手可能会问:编写自己的黑客工具是不是很难,是不是要懂编程语言,要懂哪种编程语言呢?笔者的回答是肯定的。抛开用工具当黑客的想法,其实是让大家抛开浮躁的想法,认真地学一些真正的技术,哪怕只是一些入门的基础。想做黑客就要有创新、研发的精神,如果只是做一个只会用软件的应用级的计算机使用者,那么必定永远达不到黑客级的水平,因为工具人人都会用,只是比别人多知道几个工具而已。抛开浮躁,静下心来从头开始学习基础,为将来的成长做好足够的准备。攻防的广义性
黑客做得最多的就是“入侵”,这里所说的入侵不是一个狭义上的入侵,因为它不单单针对网络、系统的入侵。这里说的是一个广义上的入侵,“入侵”一词是指“在非授权的情况,试图存取信息、处理信息或破坏系统以使系统不可靠、不可用的故意行为。”由此可以看出,入侵并非单指网络或系统。这里说的“入侵”包括两个方面,一个是针对网络(系统)方面的入侵,另一个是针对软件的入侵。网络的入侵是通常意义上的入侵,而软件的入侵通常就是人们说的软件破解(包括漏洞挖掘等内容)。无论是侵入别人系统,还是破解某款软件,都是在非授权的情况下得到相应的权限,比如系统权限或者软件的使用权限。本书讲点什么
本书针对“网络入侵”和“软件入侵”两方面来介绍黑客编程,从攻防两个角度来学习黑客编程的知识,通过一系列知识体系完成“黑客编程”的养成计划。
本书会介绍大量的基础知识,这些基础知识看起来与普通的应用程序编程没有什么差别。其实,所谓“黑客编程”(也称为“安全编程”),“是指采用常规的编程技术,编写网络安全、黑客攻防类的程序、工具”。因此,普通的编程技术与黑客编程技术并没有本质的差别,只是开发的层面不同。普通的编程注重的是客户的需求,而黑客编程注重的则是攻与防。
黑客编程有其两面性,按照攻防角度可以分为“攻击类入侵编程”和“防范类安全编程”。结合上面提到的“网络”和“软件”两方面来说,常见的“网络攻击”程序有扫描器、嗅探器、后门等;常见的“软件攻击”程序有查壳器、动态调试器、静态分析器、补丁等(这些工具是一些调试工具和逆向分析工具,因为软件破解、漏洞挖掘等会用到这些调试工具,因此称其为“软件攻击”工具)。常见的“网络(系统)防范”程序有“杀毒软件”、“防火墙”、“主动防御系统”等;常见的“软件防范”程序有“壳”、“加密狗”、“电子令牌”等。
根据前面提到的攻防两方面的内容,本书会涉及扫描器的开发、后门的开发、应用层抓包器的开发等黑客攻防方面的相关内容。本书还会讲解关于软件方面的知识,主要涉及PE结构、加壳、脱壳、逆向分析等一系列相关知识。由于技术的两面性,希望读者有一个良性的学习心态。读者能从本书中得到什么
通过本书,读者能学到Windows下基于消息的软件开发、基于Winsock的网络应用程序的开发、软件逆向分析和调试知识等一系列的编程、调试及安全知识。在学习的过程中,读者应该大量阅读和参考其他相关资料,并且一定要亲自动手进行编程。编程绝对不是靠看书能够学会的!
通过本书的指导,再加上自身实践和练习,读者可以具备基本的 Windows 下的应用程序开发、网络程序开发的能力,基本的系统底层开发能力。除了相关开发能力外,读者还能具备初级的病毒分析能力、软件保护等相关的安全知识。如何无障碍阅读此书
阅读本书的读者最好具有C和C++编程的基础知识,有其他编程语言基础知识的读者也可以无障碍阅读。无编程知识的读者阅读本书的同时,只要学习了本书中涉及的相关基础知识,同样可以阅读本书。
本书涉及范围较多,知识面比较杂,但是本书属于入门级读物,专门为新手准备,只要读者具备一定的基础知识,即可顺利进行阅读。在阅读本书的基础上,读者可以接着学习更深层次的知识,希望本书能帮助读者提高自身的能力。
建议:请读者深入学习操作系统原理、数据结构、编译原理、计算机体系结构等重要的计算机基础知识。免责
本书属于入门级图书,无法保证读者成为黑客。作者本人也不是黑客,但是至少要有一个成为黑客的想法和成为黑客的动力。因此,如果本书没能达到读者所期待的目标,那么也请恕笔者无奈,笔者只是带领读者入门。
本书中内容主要用于教学,指导新手如何入门、如何学习编程知识,从编程的过程中了解黑客编程的基础知识。请勿使用自己的知识做出有碍公德之事,在准备通过技术手段进行蓄意破坏时,请想想无数“高手”的下场。读者如若作奸犯科,与作者本人和出版社无任何关系,请读者自觉遵守国家法律。
由于作者水平有限,书中难免会有差错,敬请谅解。中肯取代无礼,客观代替谩骂。
编辑联系邮箱:zhangtao@ptpress.com.cn。第1章黑客编程入门
读者是否曾经用别人开发的工具尝试“入侵”,是否希望开发出自己的黑器?本章将介绍Win32开发平台的开发基础,带领读者进入Windows编程的大门。
Windows是一个庞大而复杂的操作系统,它提供了丰富而强大的功能,不但操作灵活方便,而且有众多的应用软件对其进行支持。Windows 因有众多软件的支持,长期雄霸于 PC系统。之所以有众多软件的支持,是因为Windows提供了良好的应用程序开发平台(接口)、完整的开发文档和各种优秀的开发环境。对于一个程序员来说,除了要掌握基本的开发语言以外,还要掌握具体的开发环境和系统平台的相关知识。在掌握编程语言和开发环境等知识后,还要掌握调试技术以及各种调试分析工具。同样,Windows操作系统提供了良好的调试接口,并且有非常多的调试工具。
本章主要介绍Windows的消息机制,Windows下的开发工具、辅助工具,还有调试工具。本章的目的在于对Windows消息机制进行回顾,它是Windows开发的基础,方便后续章节内容的讲解。本章对于 Windows 编程的一些基本概念不会进行过多的介绍。除了对消息机制进行回顾外,本章还要介绍集成在Visual C++(VC6)中的调试工具和其他一些开发辅助工具。1.1初识Windows消息
大部分 Windows 应用程序都是基于消息机制的(命令行下的程序并不基于消息机制),熟悉 Windows 操作系统的消息机制是掌握 Windows 操作系统下编程的基础。本节将带领读者认识和熟悉Windows的消息机制。1.1.1 对消息的演示测试
在真正学习和认识消息之前,先来完成一个简单的任务,看看消息能完成什么样的工作。首先写一个简单的程序,通过编写的程序发送消息来关闭记事本的进程、获取窗口的标题和设置窗口的标题。
程序的具体代码如下:
void CMsgTestDlg::OnClose()
{
//TODO:Add your control notification handler code here
HWND hWnd = ::FindWindow("Notepad", NULL);
if ( hWnd == NULL )
{
AfxMessageBox("没有找到记事本");
return ;
}
::SendMessage(hWnd, WM_CLOSE, NULL, NULL);
}
void CMsgTestDlg::OnExec()
{
//TODO:Add your control notification handler code here
WinExec("notepad.exe", SW_SHOW);
}
void CMsgTestDlg::OnEditWnd()
{
//TODO: Add your control notification handler code here
HWND hWnd = ::FindWindow(NULL, "无标题 - 记事本");
if ( hWnd == NULL )
{
AfxMessageBox("没有找到记事本");
return ;
}
char *pCaptionText = "消息测试";
::SendMessage(hWnd, WM_SETTEXT, (WPARAM)0, (LPARAM)pCaptionText);
}
void CMsgTestDlg::OnGetWnd()
{
//TODO: Add your control notification handler code here
HWND hWnd = ::FindWindow("Notepad", NULL);
if ( hWnd == NULL )
{
AfxMessageBox("没有找到记事本");
return ;
}
char pCaptionText[MAXBYTE] = { 0 };
::SendMessage(hWnd, WM_GETTEXT, (WPARAM)MAXBYTE, (LPARAM)pCaptionText);
AfxMessageBox(pCaptionText);
}
编写的代码中有4个函数:第1个函数OnClose()是用来关闭记事本程序的;第2个函数OnExec()是用来打开记事本程序的,主要是测试其他3个函数时可以方便地打开记事本程序;第3个函数OnEditWnd()是用来修改记事本标题的;第4个函数OnGetWnd()是用来获取当前记事本标题的。程序的界面如图1-1所示。图1-1 消息测试窗口
简单测试一下这个程序。首先单击“打开记事本程序”按钮,出现记事本的窗口(表示记事本程序被打开了);接着单击“修改记事本标题”按钮,可以发现记事本程序的窗口标题改变了;再单击“获取记事本标题”按钮,弹出记事本程序窗口标题的一个对话框;最后单击“关闭记事本程序”按钮,记事本程序被关闭。1.1.2 对MsgTest代码的解释
上面的代码中要学习的API函数有两个,分别是FindWindow()和SendMessage()。下面看一下它们在MSDN中的定义。
FindWindow()函数的定义如下:
HWND FindWindow(
LPCTSTR lpClassName, // class name
LPCTSTR lpWindowName // window name
);
FindWindow()函数的功能是,通过指定的窗口类名( lpClassName )或窗口标题(lpWindowName)查找匹配的窗口并返回最上层的窗口句柄。简单理解就是,通过指定的窗口名(窗口名相对于窗口类来说要直观些,因此往往使用的是窗口名)返回窗口句柄。FindWindow()函数有2个参数,分别是lpClassName和lpWindowName。通过前面的描述,该函数通常使用的是第2个参数lpWindowName,该参数是指定窗口的名称。在例子代码中,为程序指定的窗口名是“无标题—记事本”。“无标题—记事本”是记事本程序打开后的默认窗口标题,当 FindWindow()找到该窗口时,会返回它的窗口句柄。例子代码中也使用了lpClassName(窗口类名),在窗口的名称会改变的情况下,只能通过窗口类名来获取窗口的句柄了。FindWindow()函数返回的窗口句柄是为了给SendMessage()函数来使用的。
SendMessage()函数的定义如下:
LRESULT SendMessage(
HWND hWnd, // handle to destination window
UINT Msg, // message
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
该函数的作用是根据指定窗口句柄将消息发送给指定的窗口。该函数有3个参数,第1个参数hWnd是要接收消息的窗口的窗口句柄,第2个参数Msg是要发送消息的消息类型,第3个参数wParam和第4个参数lParam是消息的两个附加参数。第1个参数hWnd在前面已经介绍过了,该参数通过FindWindow获取。
程序的代码中,SendMessage()函数的第2个参数分别使用的是WM_CLOSE消息、WM_SETTEXT消息和WM_GETTEXT消息。下面来看这3个消息的具体含义。
WM_CLOSE:将WM_CLOSE消息发送后,接收到该消息的窗口或应用程序将要关闭。WM_CLOSE消息没有需要的附加参数,因此wParam和lParam两个参数都为NULL。
WM_SETTEXT:应用程序发送WM_SETTEXT消息对窗口的文本进行设置。该消息需要附加参数,wParam参数未被使用,必须指定为0值,lParam参数是一个指向以NULL为结尾的字符串的指针。
WM_GETTEXT:应用程序发送WM_GETTEXT消息,将对应窗口的文本复制到调用者的缓冲区中。该消息也需要附加参数,wParam 参数指定要复制的字符数数量,lParam 是接收文本的缓冲区。
例子代码在VC6下进行编译连接,生成可执行文件后,可以通过按钮的提示进行测试,以便感性认识消息的作用。1.1.3 如何获取窗口的类名称
编写程序调用FindWindow()函数的时候,通常会使用其第2个参数,也就是窗口的标题。但是有些软件的窗口标题会根据不同的情况进行改变,那么程序中就不能在FindWindow()函数中直接通过窗口的标题来获得窗口的句柄了。而窗口的类名通常是不会变的,因此编程时可以指定窗口类名来调用FindWindow()函数以便获取窗口句柄。那么,如何能获取到窗口的类名称呢?这就是将要介绍的第1个开发辅助工具——Spy++。
Spy++是微软 Visual Studio 中提供的一个非常实用的小工具,它可以显示系统的进程、窗口等之间的关系,可以提供窗口的各种信息,可以对系统指定的窗口进行消息的监控等。它的功能非常多,这里演示如何用它来获取窗口的类名称。
打开开始菜单,在 Visual Studio 的菜单路径下找到 Spy++,打开 Spy++窗口,如图 1-2所示。
选择工具栏中的“Find Window”按钮,如图1-3所示。图1-2 Microsoft Spy++窗口图1-3 Find Window按钮
单击“Find Window”按钮,出现如图1-4 所示的窗口。图1-4 Find Window窗口
在图1-4 中,用鼠标左键单击“Finder Tool”后面的图标,然后拖曳到指定的窗口上,会显示出“Handle”(窗口句柄)、“Caption”(窗口标题)和“Class”(窗口类名),其中“Class”是编程时要使用的类名称。“Hide Spy++”是一个比较实用的功能,它用来隐藏Spy++主窗口界面。选中该复选框后,拖曳“Finder Tool”后的图标时,图1-2所示为窗口将被隐藏。这个功能的实用之处在于,有些应用软件有反Spy++的功能,隐藏Spy++主窗口有助于避免被反Spy++的软件检测到。为什么隐藏Spy++的“Find Window”窗口会有反检测的功能,反检测的原理是什么?原理很简单,目标程序也是通过调用FindWindow()函数来查找Spy++窗口的,如果有该窗口,就进行一些相应的处理。
注:通过Spy++找到的窗口句柄是不能在编程中使用的,每次打开窗口时,窗口的句柄都会改变。
将“Finder Tool”后的图标拖曳到记事本的标题处,Spy++的Find Window窗口显示的内容如图1-5所示。图1-5 获取到信息的Find Window窗口
从图1-5中可以得到记事本程序的标题和类名称。当编写程序调用FindWindow()函数,不能通过程序的标题文本得到窗口的句柄时,可以通过窗口类名称得到窗口的句柄。1.2Windows消息机制的处理
SendMessage()将指定的消息发送给指定的窗口,窗口接收到消息也有相应的行为发生。那么窗口接收到消息后的一系列行为是如何发生的?下面通过熟悉 Windows 的消息机制来理解消息处理背后的秘密。1.2.1 DOS程序与Windows程序执行流程对比
Windows下的窗口应用程序都是基于消息机制的,操作系统与应用程序之间、应用程序与应用程序之间,大部分都是通过消息机制进行通信、交互的。要实际掌握Windows应用程序内部对消息的处理,必须分析实际的源代码。在编写一个基于消息的Windows应用程序前,先来比较DOS程序和Windows程序在执行时的流程。
1.DOS程序执行流程
在DOS下将编写完的程序进行执行,在执行时有明显的流程。比如用C语言编写程序后,程序执行时的大致流程如图1-6所示。图1-6 传统DOS程序执行流程
在图1-6中可以看出,DOS程序的流程是按照代码的顺序和流程依次执行。大致步骤为:DOS程序从main()主函数开始执行(其实程序真正的入口并不是main函数);执行的过程中按照代码编写流程依次调用各个子程序;在执行的过程中会等待用户的输入等操作;当各个子程序执行完成后,最终会返回main()主函数,执行主函数的return语句后,程序退出(其实程序真正的出口也并不是main函数的return语句)。
2.Windows程序执行流程
DOS程序的执行流程比较简单,但是Windows应用程序的执行流程就比较复杂了。DOS是单任务的操作系统。在DOS中,通过输入命令,DOS操作系统会将控制权由Command.com转交给DOS程序从而执行。而Windows是多任务的操作系统,在Windows下同时会运行若干个应用程序,那么Windows就无法把控制权完全交给一个应用程序。Windows下的应用程序是如何工作的?首先看一下Windows应用程序内部的大致结构图,如图1-7所示。
图1-7可能看起来比较复杂,其实Windows应用程序的内部结构比该示意图更复杂。在实际开发Windows应用程序时,需要关注的部分主要是“主程序”和“窗口过程”两部分。但是从图1-7来看,主程序和窗口过程没有直接的调用关系,而在主程序和窗口过程之间有一个“系统程序模块”。“主程序”的功能是用来注册窗口类、获取消息和分发消息。而“窗口过程”中定义了需要处理的消息,会根据不同的消息执行不同的动作,而不需要程序处理的消息则会交给默认的系统过程进行处理。
在“主程序”中,RegisterClassEx()函数会注册一个窗口类,窗口类中的字段中包含了“窗口过程”的地址信息,也就是把“窗口类”的信息(包括“窗口过程的地址信息”)告诉操作系统。然后“主程序”不断通过调用 GetMessage()函数获取消息,再交由 DispatchMessge()函数来分发消息。消息分发后并没有直接调用“窗口过程”让其处理消息,而是由系统模块查找该窗口指定的窗口类,通过窗口类再找到窗口过程的地址,最后将消息送给该窗口过程,由窗口过程处理消息。图1-7 Windows 应用程序执行原理图1.2.2 一个简单的Windows应用程序
相对一个简单的DOS程序来说一个简单的Windows应用程序,要很长。下面的例子中只实现一个特别简单的程序,这个程序在桌面上显示一个简单的窗口,它没有菜单栏、工具栏、状态栏,只是在窗口中输出一段简单的字符串。虽然程序如此简单,但是也要编写 100行左右的代码。考虑到初学的读者,这里将一部分一部分地逐步介绍代码中的细节,以减少代码的长度,从而方便初学者的学习。
1.Windows窗口应用程序的主函数——WinMain()
在DOS时代,在Windows下的命令行的程序,要使用C语言编写代码的时候都是从main()函数开始的。而在Windows下编写有窗口的程序时,要用C语言编写窗口程序就不再从main()函数开始了,取而代之的是WinMain()函数。
既然Windows应用程序的主函数是WinMain(),那么就从了解WinMain()函数的定义开始学习Windows应用程序的开发。WinMain()函数的定义如下:
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // command line
int nCmdShow // show state
);
该函数的定义取自MSDN中,在看到WinMain()函数的定义后,很直观地会发现WinMain函数的参数比main()函数的参数变多了。从参数个数上来说,WinMain()函数接收的信息更多了。下面来看每个参数的含义。
hInstance是应用程序的实例句柄。保存在磁盘上的程序文件是静态的,当被加载到内存中时,被分配了CPU、内存等进程所需的资源。这样,一个静态的程序被实例化为一个有各种执行资源的进程了。句柄的概念随上下文的不同而不同,句柄是操作某个资源的“把手”。当需要对某个实例化进程操作时,需要借助该实例句柄进行操作。这里的实例句柄是程序装入内存后的起始地址。实例句柄的值也可以通过GetModuleHandle()参数来获得(注意系统中没有GetInstanceHandle()函数,不要误以为是hInstance就会有GetInstanceXXX()类的函数)。
hPrevInstance是同一个文件创建的上一个实例的实例句柄。这个参数是Win16平台下的遗留物,在Win32下已经不再使用了。
lpCmdLine 是主函数的参数,用于在程序启动时给进程传递参数。比如在“开始”菜单的“运行”中输入“notepad c:\boot.ini”,这样就通过记事本打开了C 盘下的boot.ini 文件。C:\Boot.ini文件是通过lpCmdLine传递给notepad.exe程序的。
nCmdShow是进程显示的方式,可以是最大化显示、最小化显示,或者是隐藏等显示方式(如果是启动木马程序的话,启动方式当然要由自己进行控制)。
主函数的参数都介绍完了。编写Windows的窗口程序,需要主函数中应该完成哪些操作是下面要讨论的内容。
2.WinMain()函数中的流程
编写Windows下的窗口程序,在WinMain()主函数中主要完成的任务是注册一个窗口类,创建一个窗口并显示创建的窗口,然后不停地获取属于自己的消息并分发给自己的窗口过程,直到收到WM_QUIT消息后退出消息循环结束进程。这是主函数中程序的执行脉络,程序中将注册窗口类、创建窗口的操作封装为自定义函数。
代码如下:
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG Msg;
BOOL bRet;
//注册窗口类
MyRegisterClass(hInstance);
//创建窗口并显示窗口
if ( !InitInstance(hInstance, SW_SHOWNORMAL) )
{
return FALSE;
}
//消息循环
//获取属于自己的消息并进行分发
while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 )
{
if ( bRet==1 )
{
//handle the error and possibly exit
break;
}
else
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
}
return Msg.wParam;
}
在代码中,MyRegisterClass()和InitInstance()是两个自定义的函数,分别用来注册窗口类,创建窗口并显示更新创建的窗口。后面的消息循环部分用来获得消息并进行消息分发。它的流程如图1-7所示的“主程序”部分。
代码中主要是3个函数,分别是GetMessage()、TranslateMessage()和DispatchMessage()。这3个函数是Windows提供的API函数,它们的定义如下。
BOOL GetMessage(
LPMSG lpMsg, // message information
HWND hWnd, // handle to window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax // last message
);
该函数用来获取属于自己的消息,并填充 MSG 结构体。有一个类似于 GetMessage()的函数是 PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动让出CPU时间给其他进程。关于PeekMessage()函数的使用,请参考MSDN。
BOOL TranslateMessage(CONST MSG *lpMsg);
该函数是用来处理键盘消息的。它将虚拟码消息转换为字符消息,也就是将WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息。
LRESULT DispatchMessage(CONST MSG *lpmsg);
该函数是将消息分发到窗口过程中。
3.注册窗口类的自定义函数
在 WinMain()函数中,首先调用了 MyRegisterClass()这个自定义函数,需要传递进程的实例句柄 hInstance 作为参数。该函数完成窗口类的注册,分为两步:第一步是填充WNDCLASSEX 结构体,第二步是调用 RegisterClassEx()函数进行注册。该函数相对简单,稍微复杂的是WNDCLASSEX结构体的成员较多。
代码如下:
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX WndCls;
//填充结构体为0
ZeroMemory(&WndCls, sizeof(WNDCLASSEX));
//cbSize是结构体大小
WndCls.cbSize = sizeof(WNDCLASSEX);
//lpfnWndProc 是窗口过程地址
WndCls.lpfnWndProc = WindowProc;
//hInstance 是实例句柄
WndCls.hInstance = hInstance;
//lpszClassName 是窗口类类名
WndCls.lpszClassName = CLASSNAME;
//style是窗口类风格
WndCls.style = CS_HREDRAW | CS_VREDRAW;
//hbrBackground 是窗口类背景色
WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1;
//hCursor 是鼠标句柄
WndCls.hCursor = LoadCursor(NULL, IDC_ARROW);
//hIcon是图标句柄
WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION);
//其他
WndCls.cbClsExtra = 0;
WndCls.cbWndExtra = 0;
return RegisterClassEx(&WndCls);
}
在代码中,WNDCLASSEX结构体的成员都介绍了。WNDCLASSEX中最重要的字段是lpfnWndProc,它将保存的是窗口过程的地址。窗口过程是对各种消息进程处理的“汇集地”,也是编写Windows应用程序的重点部分。代码中的函数都比较简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()3个函数。由于这3个函数使用简单,通过代码就可以进行理解,这里不做过多介绍。
注册窗口类的重点是在后面的代码中可以根据该窗口类创建该种类型的窗口。代码中,在定义窗口类时指定了背景色、鼠标指针、窗口图标等,那么使用该窗口类创建的窗口都具有相同的窗口类型。
4.创建主窗口并显示更新
注册窗口类后,根据该窗口类创建具体的主窗口并显示和更新窗口。
代码如下:
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd=NULL;
//创建窗口
hWnd = CreateWindowEx(WS_EX_CLIENTEDGE,
CLASSNAME,
"MyFirstWindow",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
if ( NULL == hWnd )
{
return FALSE;
}
//显示窗口
ShowWindow(hWnd, nCmdShow);
//更新窗口
UpdateWindow(hWnd);
return TRUE;
}
在调用该函数时,需要给该函数传递实例句柄和窗口显示方式两个参数。这两个参数的第1个参数通过WinMain()函数的参数hInstance指定,第2个参数可以通过WinMain()函数的第3个参数指定,也可以进行自定义指定。程序中的调用代码如下:
InitInstance(hInstance, SW_SHOWNORMAL);
在创建主窗口时调用了CreateWindowEx()函数,先来看看它的函数原型:
HWND CreateWindowEx(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // registered class name
LPCTSTR lpWindowName, // window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // menu handle or child identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // window-creation data
);
CreateWindowEx()中的第2个参数是lpClassName,由注释可以知道是已经注册的类名。这个已经注册的类名就是WNDCLASSEX结构体的lpszClassName字段。
5.处理消息的窗口过程
按照如图1-7所示的流程,主函数的部分已经都实现完成了。接下来看程序中关键的部分——窗口过程。从主函数中看出,在主函数中没有任何地方直接调用窗口过程,只是在注册窗口类时指定了窗口过程的地址。那么窗口类是由谁进行调用的呢?答案是由操作系统进行调用的。原因有二,首先窗口过程的地址是由系统维护的,注册窗口类时是向系统进行注册。其次是除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,比如前面的例子中调用SendMessage()函数发送消息后,需要系统调用目标程序的窗口过程来完成相应的动作。如果窗口过程由自己调用,那么窗口就要自己维护窗口类的信息,进程间消息的通信会非常繁琐,也会无形中增加系统的开销。
窗口过程的代码如下:
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hDC;
RECT rt;
char *pszDrawText = "Hello Windows Program.";
switch (uMsg)
{
case WM_PAINT:
{
hDC=BeginPaint(hwnd,&ps);
GetClientRect(hwnd, &rt);
DrawTextA(hDC,
pszDrawText, strlen(pszDrawText),&rt,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
EndPaint(hwnd, &ps);
break;
}
case WM_CLOSE:
{
if ( IDYES==MessageBox(hwnd,
"是否退出程序", "MyFirstWin", MB_YESNO) )
{
DestroyWindow(hwnd);
PostQuitMessage(0);
}
break;
}
default:
{
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
return 0;
}
在WinMain()函数中,通过调用RegisterClassEx()函数进行了窗口类的注册,通过调用CreateWindowEx()函数创建了窗口,并且GetMessage()函数不停地获取消息,但是在主函数中没有对被创建的窗口做任何处理。那是因为真正对窗口行为的处理全部放在了窗口过程中。当WinMain()函数中的消息循环得到消息以后,通过调用DispatchMessage()函数将消息派发(不是由 DispatchMessage()函数直接派发)给了窗口过程,从而由窗口过程对消息进行处理。
窗口过程的定义是按照MSDN上给出的形式进行定义的,MSDN上的定义形式如下:
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
WindowProc是窗口过程的函数名,这个函数名可以随意改变,但是该窗口过程的函数名必须与WNDCLASSEX结构体中lpfnWndProc的成员变量的值一致。函数的第1个参数hwnd是窗口的句柄,第2个参数uMsg是消息值,第3个和第4个参数是对于消息值的附加参数。这四个参数的类型与SendMessage()函数的参数相对应。
上面 WindowProc()窗口过程中只对两个消息进行了处理,分别是 WM_PAINT 和WM_CLOSE。这里为了演示因此简单处理了两个消息。Windows 中有上千种消息,那么多的消息不可能全部都由程序员自己去处理,程序员只处理一些程序中需要的消息,其余的消息就交给了DefWindowProc()函数进行处理。DefWindowProc()函数实际上是将消息传递给了操作系统,由操作系统来处理程序中没有处理的消息。比如,在调用CreateWindow()函数时,系统会发送消息 WM_CREATE 给窗口过程,但是这个消息可能对程序的功能并不需要进行特殊的处理,因此直接交由DefWindowProc()函数让系统进行处理。
DefWindowProc()函数的定义如下:
LRESULT DefWindowProc(
HWND hWnd, // handle to window
UINT Msg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
该函数的 4 个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数就可以完成该函数的调用。在switch分支结构中的default位置直接调用DefWindowProc()函数就可以了。
WM_CLOSE 消息是关闭窗口时发出的消息,在这个消息中需要调用 DestoryWindow()函数来销毁窗口,并且调用PostQuitMessage()来退出消息循环,使程序退出。对于WM_PAINT消息,这里不进行介绍,涉及的几个API函数可以参考MSDN进行了解。
有的资料在介绍消息循环时会给出一个建议,就是把需要经常处理的消息放到程序靠上的位置,而将不经常处理的消息放到程序靠下的位置,从而提高程序的效率。其实,在窗口过程中往往会使用 switch 结构对消息进行判断,而 switch 结构在编译器进行编译后会进行优化处理,从而提高程序的运行效率。关于 switch 结构的优化,将在其他章节进行介绍。1.3模拟鼠标键盘按键的操作
鼠标和键盘的操作也会被转换为相应的系统消息,窗口过程中在接收到鼠标或键盘消息后会进行相应的处理。通过前面的内容了解到,可以通过 SendMessage()和 PostMessage()发送消息到指定的窗口过程中,那么使用这两个函数来发送鼠标和键盘的相关消息就可以进行鼠标和键盘的模拟操作。除了 SendMessage()和 PostMessage()外,还可以通过 keybd_event()和mouse_event()两个专用的函数进行鼠标和键盘按键的模拟操作。关于鼠标和键盘按键的模拟的用处就不多说了,想必读者都是知道的。1.3.1 基于发送消息的模拟
通过前面的知识已经明白,Windows的应用程序是基于消息机制的,对于鼠标和键盘的操作也会被系统转化为相应的消息。首先来学习如何通过发送消息进行鼠标和键盘的模拟操作。
1.鼠标、键盘按键常用的消息
无论是鼠标指针(或光标)的移动、单击,还是键盘的按键,通常在Windows应用程序中都会转换成相应的消息。在操作鼠标时,使用最多的是移动鼠标和单击鼠标键。比如,在教新手使用计算机时会告诉他,将鼠标指针(或光标)移动到“我的电脑”上,然后单击鼠标右键,在弹出的快捷菜单中用鼠标左键单击选择“属性”对话框。当移动鼠标光标的时候,系统中对应的消息是 WM_MOUSEMOVE 消息,按下鼠标左键时的对应的消息是WM_LBUTTONDOWN,释放鼠标左键时,对应的消息是 WM_LBUTTONUP。在系统中,鼠标的消息有很多。在MSDN中查询到的鼠标消息如图1-8所示。图1-8 鼠标相关消息
同样,在系统中也定义了键盘的按下与抬起的消息。键盘按下的消息是 WM_KEY DOWN,与之对应的键盘抬起的消息是WM_KEYUP。除了这两个消息外,还有一个消息是比较常用的,这个消息在前面介绍消息循环时介绍过,就是 WM_CHAR 消息。键盘的消息相对于鼠标要少很多,在MSDN中查询到的键盘消息如图1-9所示。图1-9 键盘相关消息
2.PostMessage()函数对键盘按键的模拟
通过前面的知识已经知道,PostMessage()和SendMessage()这两个函数可以对指定的窗口发送消息。既然鼠标和键盘按键的操作被系统转换为相应的消息,那么就可以使用PostMessage()和SendMessage()通过按鼠标和键盘按键发送的消息来模拟它们的操作。对于模拟键盘按键消息,最好使用 PostMessage()而不要使用 SendMessage()。在很多情况下, SendMessage()是不会成功的。
现在编写一个简单的通过 PostMessage()函数模拟键盘发送(按 F5 键发送)的信息来刷新网页的小工具。首先打开VC6.0,创建一个对话框的工程,按照图1-10所示设置界面。图1-10 模拟键盘刷新网页界面布局
按照如图1-10所示的界面进行布局,然后为“开始”按钮设置控件变量。这个小程序在“IE浏览器标题”处输入要刷新的页面的标题,在“刷新频率”处输入一个刷新的时间间隔,单位是秒。
当了解程序的功能并且将程序的界面布置好以后,就可以开始编写程序的代码了。程序的代码分为两部分,第一部分是程序要处理“开始”按钮的事件,第二部分是要按照指定的时间间隔对指定的浏览器发送按F5键的消息来刷新网页。
首先来编写响应“开始”按钮事件的代码,双击“开始”按钮来编写它的响应事件。代码如下:
void CKeyBoardDlg::OnBtnStart()
{
//TODO: Add your control notification handler code here
CString strBtn;
int nInterval = 0;
//获取输入的浏览器标题
GetDlgItemText(IDC_EDIT_CAPTION, m_StrCaption);
//获取输入的刷新频率
nInterval = GetDlgItemInt(IDC_EDIT_INTERVAL, FALSE, TRUE);
//判断输入的值是否非法
if ( m_StrCaption ==""|| nInterval == 0 )
{
return;
}
//获取按钮的标题
m_Start.GetWindowText(strBtn);
if ( strBtn == "开始" )
{
//设置定时器
SetTimer(1, nInterval * 1000, NULL);
m_Start.SetWindowText("停止");
GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(FALSE);
GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(FALSE);
}
else
{
//结束定时器
KillTimer(1);
m_Start.SetWindowText("开始");
GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(TRUE);
GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(TRUE);
}
}
在代码中,首先判断按钮的文本,如果是“开始”,则通过SetTimer()函数设置一个定时器;如果按钮的文本不是“开始”,则通过KillTimer()函数关闭定时器。
这里的SetTimer()和KillTimer()是MFC中CWnd类的两个成员函数,不是API函数。很多 MFC 中的类成员函数和 API 函数的写法是一样的,但是它们还是有区别的。比较一下SetTimer()在MFC中的定义和API函数的定义的差别。
MFC中的定义如下:
UINT SetTimer(
UINT nIDEvent,
UINT nElapse,
void (CALLBACK EXPORT* lpfnTimer)(
HWND, UINT, UINT, DWORD) );
API函数的定义如下:
UINT_PTR SetTimer(
HWND hWnd, // handle to window
UINT_PTR nIDEvent, // timer identifier
UINT uElapse, // time-out value
TIMERPROC lpTimerFunc // timer procedure
);
从定义中可以看出,MFC中SetTimer()函数的定义比API中SetTimer()函数的定义少了一个参数,即HWND 的窗口句柄的参数。在MFC 中,窗口相关的成员函数都不需要指定窗口句柄,在MFC的内部已经维护了一个m_hWnd的句柄变量。
在按钮事件中添加定时器,那么定时器会按照指定的时间间隔进行相应的处理。定时器部分的代码如下:
void CKeyBoardDlg::OnTimer(UINT nIDEvent)
{
//TODO: Add your message handler code here and/or call default
HWND hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));
//发送键盘按下
::PostMessage(hWnd, WM_KEYDOWN, VK_F5, 1);
Sleep(50);
//发送键盘抬起
::PostMessage(hWnd, WM_KEYUP, VK_F5, 1);
CDialog::OnTimer(nIDEvent);
}
关于定时器的处理非常简单,通过 FindWindow()函数得到要刷新窗口的句柄,然后发送WM_KEYDOWN和WM_KEYUP消息来模拟键盘按键即可。其实在模拟的过程中,可以省去WM_KEYUP消息的发送,但是为了模拟效果更接近真实性,建议在模拟时将消息成对发送。
将写好的程序编译连接后运行起来看效果,在“IE浏览器标题”处输入浏览器的标题,这个标题可以通过Spy++获得,然后在“刷新频率”处输入1。然后单击“开始”按钮,观察浏览器每个1秒进行
试读结束[说明:试读内容隐藏了图片]