WebBrowser无法显示招商银行密码输入控件的问题
本文由CharlesSimonyi发表于CSDN博客:http://blog.csdn.net/charlessimonyi/article/details/30479131转载请注明出处
之前就看到CSDN论坛上有人提问,自己写的程序中的WebBrowser打开招商银行的登录页面后(https://pbnj.ebank.cmbchina.com/CmbBank_GenShell/UI/GenShellPC/Login/Login.aspx),无法显示密码输入控件,但是在IE中可以正常显示。
后来又在猪八戒威客网上看到有人提这个问题,并且还悬赏了570人民币。重赏之下必有勇夫,这不,为了买一块新硬盘,我就一头扎进了这个问题里。
不卖关子,先把解决方法告诉大家,相信有不少人在头疼这个问题。先把你手头的问题解决了,如果有兴趣再继续往下看解决这个问题的过程。
1.如果你的程序是VC6.0、VC2003、VC2005写的,应该什么都不用做,默认情况下它是能正常显示招商银行密码输入控件的。
2.如果你的程序是VC2008、VC2010、VC2012或更高版本的VC写的,打开“项目”-“XX项目属性”-“配置属性”-“链接器”-“命令行”,在“其它选项”里输入“/NXCOMPAT:NO”,重新编译下就OK。
3.如果你的程序是任意版本的C#、VB.NET等NET语言写的,在编译生成了exe可执行文件后。假如最终编译出来的exe文件为c:\project\bin\Release\WindowsFormsApplication.exe,则在VisualStudio中打开“工具”-“VisualStudio命令提示”(或在开始菜单中找到“Microsoft Visual Studio 2010”-“VisualStudio Tools”-“Visual Studio 命令提示”)
输入editbin.exe /NXCOMPAT:NO c:\project\bin\Release\WindowsFormsApplication.exe
回车,OK。
4.如果你的程序是其它语言写的,自己研究一下吧,关键点就是在/NXCOMPAT:NO这个链接器开关上。
效果:
VC2010:
C#:
/NXCOMPAT:NO的意思是将可执行文件显式指定为与数据执行保护不兼容。也就是关闭数据执行保护功能。什么意思呢,就是说当开启这个选项时,不允许在堆栈上的内存上执行代码,因为允许在堆栈内存上执行代码并不安全,有可能被恶意者利用来进行溢出攻击。但是一些比较老的程序,用到了在堆栈内存上动态写入代码指令并执行的技术。招商银行的这个控件是用VC2003的ATL7.1开发的,ATL7.1里面就用到了这种技术,而VC2005以上的编译器默认开启NXCOMPAT数据执行保护,导致招商银行的这个控件无法运行而显示不出来。我们只要关闭NXCOMPAT数据执行保护,招商银行的控件就正常运行了。
关于NXCOMPAT这个链接器开关,MSDN上有介绍:http://msdn.microsoft.com/zh-cn/library/ms235442.aspx
关于C#与NXCOMPAT可以看这篇文章:http://blogs.msdn.com/b/ed_maurer/archive/2007/12/14/nxcompat-and-the-c-compiler.aspx
下面开始讲解决这个问题从头到尾的过程,这个过程花了我三天时间,第一天毫无头绪,差点就放弃了。但想到我那台电脑用了快三年了,一直挂着80G的老IDE硬盘,一直没有钱买新的,为了新硬盘,拼了。
那天在猪八戒上看到这样一个任务,有人悬赏570元求解决C#程序的WebBrowser无法显示招商银行密码输入控件的问题。然后我打开VC2010,试了一下,也是无法显示。我首先想到是WebBrowser是不是为了出于安全考虑,禁用了一些Activex控件,在网上搜索了很久,改了多处注册表,仍然无法解决。但是支付宝、工商银行这些网站的密码输入控件都是能正常显示的啊。我又在网上找了一些个人开发的基于WebBrowser的浏览器,都是无法显示招商银行的密码输入控件。
由于招商银行的密码输入控件与支付宝的密码输入控件都是Activex控件,于是我把他们都拖到MFC的窗口上,支付宝的控件能正常显示,但是招商银行的控件没有出来。调试发现容纳招商银行控件的容器,一个CWnd对象,Create的时候失败了,内部句柄为空。再单步调试进去,发现是错在COleControlSite::DoVerb这里,该方法调用这个控件的IoleObject接口的DoVerb方法,返回了E_FAIL错误。但是支付宝的控件调用DoVerb时返回的是S_OK正确。由于IoleObject::DoVerb跟不进去了,无法知道错在哪里,但是可以肯定是,这个错误是在招商银行的控件内部。晚上我看了下《深入解析ATL》关于Ativex这一章,看了下IoleObject接口,并没有什么突破。
第二天发现猪八戒上那个任务,有人交稿了,那个人使用易语言写的程序,它的程序中的WebBrowser确实能正常显示招商银行的密码输入控件。顿时信心与自信倍受打击,好在那个猪八戒客户对此并不买账,要求必须用C#实现。我又抓紧时间在网上搜啊搜啊搜,搜遍了百度、Google、Bing。总算找到了一点有用的信息,有人说换用VC2005后就正常了。于是我马上把VS2005装起来,果然,VC2005写的程序中的WebBrowser打开招商银行网站后能正常显示那个密码输入控件。又在VC6.0里面试了下,也是能正常显示。之前把那个易语言写的程序拖到IDA Pro里发现这个易语言程序的内部竟然是基于VC6.0的MFC4.2写出来的,感情那个用易语言交稿的威客什么都没做,就是拖了个WebBrowser控件就能正常显示招商银行控件了,这样也敢交稿啊。哎,不管他,回到问题上来,为什么会这么奇怪,难道这个控件和高版本的VC冲突?高版本的VC程序和低版本的VC程序有什么区别?CRT运行库不同?不对啊,这个控件是静态链接到CRT运行库的,应该影响不到啊。
依然是毫无头绪,试了一下用VS2005创建C#项目,还是无法显示招行控件,但是用VS2005创建C++/CLR项目,能正常显示招行控件!而VS2010下无论是C#项目还是C++/CLR项目都不行。哎,不管了,先把C++/CLR写的Demo程序传上去,在猪八戒上交稿,就说是用C#写的Demo,反正它两都是个WinFrom的窗口,很像,看不出来。先蒙混一下,防止易语言的那个威客抢占了先机。
后来交稿后,猪八戒客户就迅速让我中标了,完了,我还不知道该如何解决这个问题呢。只好含糊的说在VS2005上可以实现,而客户要求在VS2012上实现,我说那再研究下吧,他说好。这下糟了,还一点头绪都没有,就面临着要给客户提交最终解决方案了,这个牛皮吹大了。哎,算了,实在搞不定就放弃,撤销猪八戒交易。
又不停的在网上搜索,看身边的各种书籍,依然没找到什么突破口。于是我把VC2005和VC2010都打开,各创建一个MFC项目,把招商银行密码输入控件放到窗口上。两边一起调试,看看到哪一步,VC2005中的程序正确而VC2010中的程序错误。经过调试发现错误依然是在COleControlSite::DoVerb里,调用控件的IoleObject::DoVerb时,VC2005下S_OK正常,VC2010下E_FAIL错误。看来错误是出在控件内部的代码上。不调试进去是找不到错误了。那就跟进去看看吧。
右键-转到反汇编,VC2005和VC2010一起调试,单步慢慢的走,特别注意每一个call指令,观察EAX寄存器的不同,因为EAX寄存器一般用于存放call指令调用一个函数后的返回值,如果两边调用call指令后EAX不同,尤其是VC2010这边的EAX出现0x00000000(NULL)或0x80004005(E_FAIL),而VC2005那边不是这两个值,就更应该注意了,单步步入进去,继续慢慢的走。经过调试对比,发现执行10007DA9处的call指令后,VC2005中的EAX为非零值,VC2010中的EAX为0,则重新调试运行,到这个10007DA9处的call时单步步入。步入以后,又在10009256处的call 10009370发现了运行结果的不同,VC2005这边EAX为非零值,VC2010这边EAX为0。于是又重新调试运行,在10009256处单步步入。用这样的方法一步步找进去以后,最终发现在100093DB处call了user32.dll中的CreateWindowExA后,VC2005这边返回了一个非零值,即得到了一个窗口句柄,但是VC2010这边返回值为0,空句柄,窗口创建失败了。
正是这里窗口创建失败了,后面才导致IoleObject::DoVerb返回E_FAIL。为什么在VC2010中调用CreateWindowExA时创建窗口会失败呢?在调用完CreateWindowExA后,查看一下GetLastError返回的值不就知道了吗?查看GetLastError值的方法很多。一种是查看内存,GetLastError内部的那个保存着错误值的变量,应该是kernel32.dll中的一个全局或静态变量,那么它的相对地址应该是固定的。可以直接在kernel32.dll映射到进程的地址空间内的地址范围的内存上找,至于相对内存地址是多少,可以调用GetLastError并调试,查看反汇编代码单步跟进去得到。另一种方法是HOOK CreateWindowExA这个API,通过检查参数确定哪一次调用是招行的控件内部的代码调用的,然后调用GetLastError并查看返回值。两种方法都试过了以后,发现GetLastError返回值均为0!也就是说没有错误?没有错误的话为什么CreateWindowExA会失败返回空句柄?马上在网上搜索“调用CreateWindowExA失败但GetLastError为0”,发现这种错误一般是窗口过程函数有问题导致的,窗口过程没有处理好WM_CREATE之类的消息。为什么它的窗口过程在VC2005中就没问题,在VC2010中就有问题?一样的代码,不至于吧!哎,没办法,看一看它的窗口过程函数吧。窗口过程函数是由窗口类指定的,看看这个窗口类名是什么。CreateWindowExA的第二个参数就是窗口类名,从反汇编代码可以看出100093D7处的push eax给它填了这个参数,往上面看,这里eax的值又是从[ebp+20h]中得到的,[ebp+20h]明显是传给过程10009370的参数。看看在IDA Pro通过查看交叉参考,看看是谁调用了10009370,是谁把这个参数传进来的。发现,只有过程10009203调用了过程10009370。
在过程10009203中发现窗口类名的这个参数在[ebp+0Ch]处,是过程10009203的一个参数,也是由过程10009203的调用者传进来的。继续查看过程10009203的调用者是谁。但是通过查看交叉参考,找不到10009203的调用者,在VC中调试也是看得头大,一头雾水。等等,我的目的是要找到这个窗口类的窗口过程函数的地址,不一定非得去看它的反汇编代码。不如Hook CreateWindowExA这个API,得到它的窗口类名,再调用GetClassInfo得到这个窗口类的信息,里面不就有它的窗口过程函数地址了吗?说干就干,果然,这个地址找到了。
10009261就是它的窗口过程函数的地址,马上到IDA Pro里面去看。
这个窗口过程函数很简单,但是1000928E处调用SetWindowLongA引起了我的注意。看它调用SetWindowLongA时放入的第二个参数是什么,0xFFFFFFFC!也就是有符号数-4,查看MSDN上SetWindowLongA的参考资料发现-4就是GWL_WNDPROC。原来它调用SetWindowLongA改了窗口过程函数的地址。这种手法是MFC和ATL惯用的,并不奇怪。看一看他把窗口过程函数的地址改成了什么?再跑到它的新窗口过程函数中看看。由于这个地址是由[esi+14h]提供的,是一个动态的值,在静态反汇编中看不出来。那就到VC中调试看看。
在一次调试中发现这个新的窗口过程函数的地址是00652F88,好诡异的地址,这个地址根本不在它的模块地址范围内,也不在任何模块地址范围内!!!先不管了,既然00652F88是它的新窗口过程函数地址,那么后续的窗口消息将被发到这个新地址处的窗口过程函数处理,并且在100092A1处可以看到它call esi,原来是连当前这条windows消息也马上转到新的窗口过程函数处理了,那就到00652F88处下断点看看吧。但是下断点以后发现根本断不下来!怎么回事,转到OD中调试看看能不能断下来。
在OD中调试的时候,这个新窗口过程函数的地址是003060F8,可以看到它在这里给[esp+4]传送了一个立即数(这个立即数每次都不同)以后,马上转到了1000581F,1000581F是它模块中的一个函数,看来1000581F才是真正的新窗口过程地址,它用003060F8来中转,只不过进行了一个操作,给[esp+4]传送了一个立即数。不知道它这样做的意义是什么,但这个不是重点。重点是在OD中,003060F8断下来了,并且无法继续往下执行,提示访问违规!内存访问违规?意思是003060F8这块内存是不可执行的?但是VC2005生成的程序,运行到这里的时候并没有访问违规!
在OD中查看003060F8所在的这块内存的属性,是可读可写但不可执行的。所以访问违规了?
把这块内存强制改成可读可写可执行看看,果然,改了它的访问属性以后,003060F8处的代码就能继续往下执行,没有提示访问违规了!继续运行后,招商银行的密码输入控件也创建成功了,静静的躺在我的对话框窗口上!
哎,但是不对啊,调试VC2005生成的程序的时候,这块内存也是可读可写不可执行的,不需要改变它的访问属性,也能正常运行,没有访问违规啊!怎么会这样呢?先看看003060F8这块内存是哪里来的。
回到调用SetWindowLongA的过程10009261中去,可以看到新窗口过程函数的地址是从esi中得来的,每次都不一样。esi中的这个地址又是怎么来的呢?在IDA Pro中查看过程10009261的交叉参考图表。
发现过程10009261在调用SetWindowLongA之前调用了100092A8,而100092A8中又调用了HeapAlloc和GetProcessHeap,很明显,它在堆上分配了内存。那么esi中的地址是不是这里在堆上分配的内存的地址呢?查看过程100092A8的反汇编代码。
果然,调用HeapAlloc在堆上分配了0Dh(13)个字节的内存。13个字节!
等等,13个字节!之前代码执行到003060F8处的时候发生访问异常,那里的指令代码正好就是13个字节。可以得出结论了,它在堆上分配了13个字节的代码,并在这13个字节里写入了两条指令,并把这13字节的内存起始地址作为新窗口过程函数的地址,然后执行这两条指令。不清楚它为什么要这样做,不清楚它为什么要在堆上内存写代码,然后在堆上内存执行代码,而且看来它的新窗口过程函数的真实地址应该是它模块上的1000581F,为什么这里要给[esp+4]传送一个每次运行时都不一样的立即数呢?这些问题可以不管它,目前要解决的问题是,为什么VC2005写的程序,可以执行堆上内存的代码,而VC2010写的程序,不能执行堆上内存的代码,出现了访问违规。解决了这个问题,整个问题的答案就出来了。
查阅了一些书籍,并没有找到答案。我一直不停的思考,为什么VC2005写的程序和VC2010写的程序会存在这种差异呢?CRT运行库不同吗?CRT运行库不同带来的内存问题都是分配和释放的问题,应该和那个没关系啊。还有什么不同呢?
编译器命令和链接器命令!依稀记得链接器命令里有设置模块默认基址等选项,那是不是也有设置堆栈内存访问控制的选项呢?对比VC2005和VC2010的编译器命令和链接器命令,一发现不同之处就到网上搜一搜它的意义。后来链接器命令中的/NXCOMPAT:NO引起了我的注意,VC2005中默认有这个命令,而VC2010中默认没有。查一下它的意义,与数据执行保护兼容,再看一下他的解释。
http://msdn.microsoft.com/zh-cn/library/ms235442.aspx
http://msdn.microsoft.com/zh-cn/library/aa366553(vs.85).aspx
果然,问题就出在这里。VC2005以上版本的编译器中默认与数据执行保护兼容,也就是不允许执行堆栈内存上的代码。NET也都是默认与数据执行保护兼容,不允许执行堆栈内存上的代码。然后在VC2010的编译器命令里加上/NXCOMPAT:NO后,VC2010写的程序也能正常显示招商银行密码输入控件了,当然,用WebBrowser打开招商银行网页也能显示这个控件了。接下来在网上搜索了一下C#如何使用/NXCOMPAT:NO这个命令,也很快找到了方法。
http://blogs.msdn.com/b/ed_maurer/archive/2007/12/14/nxcompat-and-the-c-compiler.aspx
这里也提到了一些ATL7.1或更早的版本的ATL代码与数据执行保护不兼容,为了保持兼容这些程序而使用/NXCOMPAT:NO命令。而招商银行的密码输入控件正是ATL7.1开发的。
至此,整个问题终于解决了。把解决办法交付客户后拿到了570元的酬劳,可惜被可恶的猪八戒扣除交易手续费、个人所得税、提现手续费后,最终到账只有420多元。自己加了100多元后终于如愿以偿的买到了希捷2T硬盘。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。