WebBrowser无法显示招商银行password输入控件的问题

本文由CharlesSimonyi发表于CSDN博客:http://blog.csdn.net/charlessimonyi/article/details/30479131转载请注明出处


之前就看到CSDN论坛上有人提问。自己写的程序中的WebBrowser打开招商银行的登录页面后(https://pbnj.ebank.cmbchina.com/CmbBank_GenShell/UI/GenShellPC/Login/Login.aspx),无法显示password输入控件,可是在IE中能够正常显示。


后来又在猪八戒威客网上看到有人提这个问题,而且还悬赏了570人民币。

重赏之下必有勇夫。这不,为了买一块新硬盘,我就一头扎进了这个问题里。

不卖关子。先把解决方法告诉大家,相信有不少人在头疼这个问题。

先把你手头的问题攻克了。假设有兴趣再继续往下看解决问题的过程。

1.假设你的程序是VC6.0、VC2003、VC2005写的,应该什么都不用做,默认情况下它是能正常显示招商银行password输入控件的。

2.假设你的程序是VC2008、VC2010、VC2012或更高版本号的VC写的。打开“项目”-“XX项目属性”-“配置属性”-“链接器”-“命令行”。在“其他选项”里输入“/NXCOMPAT:NO”,又一次编译下就OK。

3.假设你的程序是随意版本号的C#、VB.NET等NET语言写的,在编译生成了exe可运行文件后。

假如终于编译出来的exe文件为c:projectinReleaseWindowsFormsApplication.exe,则在VisualStudio中打开“工具”-“VisualStudio命令提示”(或在開始菜单中找到“Microsoft Visual Studio 2010”-“VisualStudio Tools”-“Visual Studio 命令提示”)

输入editbin.exe /NXCOMPAT:NO c:projectinReleaseWindowsFormsApplication.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无法显示招商银行password输入控件的问题。然后我打开VC2010,试了一下,也是无法显示。我首先想到是WebBrowser是不是为了出于安全考虑,禁用了一些Activex控件,在网上搜索了非常久,改了多处注冊表。仍然无法解决。

可是支付宝、工商银行这些站点的password输入控件都是能正常显示的啊。

我又在网上找了一些个人开发的基于WebBrowser的浏览器。都是无法显示招商银行的password输入控件。

因为招商银行的password输入控件与支付宝的password输入控件都是Activex控件,于是我把他们都拖到MFC的窗体上,支付宝的控件能正常显示。可是招商银行的控件没有出来。调试发现容纳招商银行控件的容器。一个CWnd对象。Create的时候失败了,内部句柄为空。

再单步调试进去。发现是错在COleControlSite::DoVerb这里,该方法调用这个控件的IoleObject接口的DoVerb方法,返回了E_FAIL错误。可是支付宝的控件调用DoVerb时返回的是S_OK正确。

因为IoleObject::DoVerb跟不进去了,无法知道错在哪里,可是能够肯定是,这个错误是在招商银行的控件内部。晚上我看了下《深入解析ATL》关于Ativex这一章,看了下IoleObject接口,并没有什么突破。

第二天发现猪八戒上那个任务,有人交稿了,那个人使用易语言写的程序,它的程序中的WebBrowser确实能正常显示招商银行的password输入控件。顿时信心与自信倍受打击,好在那个猪八戒客户对此并不买账。要求必须用C#实现。我又抓紧时间在网上搜啊搜啊搜,搜遍了百度、Google、Bing。总算找到了一点实用的信息,有人说换用VC2005后就正常了。于是我立即把VS2005装起来。果然,VC2005写的程序中的WebBrowser打开招商银行站点后能正常显示那个password输入控件。又在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项目。把招商银行password输入控件放到窗体上。

两边一起调试。看看到哪一步,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里面去看。


这个窗体过程函数非常easy,可是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处的代码就能继续往下运行。没有提示訪问违规了。继续运行后。招商银行的password输入控件也创建成功了,静静的躺在我的对话框窗体上!


哎,可是不正确啊,调试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写的程序也能正常显示招商银行password输入控件了,当然。用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命令。

而招商银行的password输入控件正是ATL7.1开发的。

至此,整个问题最终攻克了。把解决的方法交付客户后拿到了570元的报酬,可惜被可恶的猪八戒扣除交易手续费、个人所得税、提现手续费后,最终到账仅仅有420多元。自己加了100多元后最终如愿以偿的买到了希捷2T硬盘。



原文地址:https://www.cnblogs.com/yutingliuyl/p/6700732.html