《一个dump引发的Qt Webkit分析》 第一篇
Note Dump分析系列文章,于2017年工作中记录,发到博客里备份. 花了点时间分析了下dump,先从LOLTheme的代码来说,打开游戏广场会调用OnOpenPlazzUrl,关闭则调用OnClosePlazzUserWnd,plazz_user_wnd_内部包含一个QWebView
QT会依次析构ChildWidget,不难理解。
下面开始精确定位错误的根源,期间需要阅读5.51 qtwebkit,主要是WebCore的代码。 从异常来看,访问了空指针,此时ecx为0,ub向上反汇编看下调用过程。
根据符号找到对应源码:qtwebkit\source\webkit\qt\webcoresupport\texturemapperlayerclientqt.cpp @ 74
QWebFrameAdapter* m_frame是其成员指针变量,即是eax(mov eax,dword ptr [esi]),从TextureMapperLayerClientQt类的内存布局可知:
//this(从函数头的mov esi,ecx可知,编译器的惯用方式),因此eax是m_frame 6226086d 8b06 mov eax,dword ptr [esi]
//eax是m_frame(QWebFrameAdapter),该类含有虚函数,因此[eax]是其虚函数表,eax+4则是第一个成员变量,即是pageAdapter 6226086f 8b4004 mov eax,dword ptr [eax+4]
//分析方式同上,ecx是client成员变量 62260872 8b4810 mov ecx,dword ptr [eax+10h]
//client是Qt的一个模板类实例(QScopedPointer参考c++的RAII),QScopedPointer client,如下图,重载了->操作符,且内联函数,编译后,[ecx]即是client的QWebPageClient实例。 62260875 8b01 mov eax,dword ptr [ecx]
分析到这里,应该比较清楚了,ecx为什么是nullptr,从QScopedPointer可知,无非两种情况,调用reset函数(默认参数)、take函数把实例转移出去。
这里是分析的第一阶段,直接从异常点入手,跟踪代码,找到异常的最直接的原因。
下面开始第二阶段的分析,还是从qtwebkit代码入手,QWebView析构会delete d(QWebViewPrivate):
从源码可知,在析构QWebPage *page之前,会调用page->d->client.take(),从第一阶段分析可知,这里会将实例转移出去,并置nullptr,然而这里调用者并没有使用转移出来的实例,“因此感觉QT这里貌似存在内存泄露”。 再通过Windbg验证一下这个client是否是同一个对象。
再切换到StackFrame 38,查看 38 0014cd28 6fc7b44f Qt5WebKitWidgets!QWebViewPrivate::detachCurrentPage+0x70 [e:\qt5vs2010full2\qtwebkit\source\webkit\qt\widgetapi\qwebview.cpp @ 237]
两者地址一致,并且QWebPagePrivate其实继承于QWebPageAdapter,因此这个client就是异常的时候的client。
至此,第二阶段分析完毕。
分析第三阶段(最后阶段): 关键问题是detachCurrentPage调用了client.take置空之后,为何还会用到?
这里会停掉所有Loader和渲染,如果有页面元素还在pending渲染状态,那么就强制停掉,调用一些通知函数,但后面还会访问WebPageClient。
怎么解决: 可以尝试在PlazzUserWnd::~PlazzUserWnd()析构的时候,调用QWebView::stop停掉页面加载之后,再析构掉QWebView。 周一的时候来论证下是否可行。。
版本:0.3.1(当前官网最新 2017.8.2,貌似很久没更新过) zeal-0.3.1-windows-x86.msi(百度云有)
Zeal每次下完文档程序就崩溃了,文档包也没生成。
这个地方死循环了 我看这个函数像是CRT的函数 但是符号匹配不上,而且crt是动态链接的,所以这个函数肯定是它自己实现的 翻了crt的代码,有关errorno的,找到了_dosmaperror,那两张表刚好能对上 但是crt没有这样的代码逻辑,所以肯定是它自己实现了一份,看了这个函数的调用,在CreateFile失败之后,把GetLastError放进去,找映射的值,结果这二货不在表里,就一直越界了 哈哈,追了很长一段
这就是那张表
这是crt里面的实现,没问题。 你看它的代码,循环根本就没有边界。 然后我用Windbg把这个循环过掉,直接jmp,后面程序不崩溃了,打了个调试日志 Could not create D:/Zeal/*****
原来是这里,如果Zeal文件夹不存在就挂了, CreateFile失败,GetLastError是120,这个错误没在那张表里面 突然在它的官网下面有个Github地址,原来是开源的。。。 我去,我就去翻了它的代码,看他为什么要这么写。 把代码Checkout下来,结果没搜到这部分的实现 开始我以为是Qt写的,查看这个函数的调用地方,有些字符串关键字,结果我就去Qt源码里搜,顿时SB了,Qt是动态链接的,当时没想起。
洗完澡出来发现搜了10万多个文件,没收到,然后在github看到这个。
用到了libarchive,我把libarchive的代码拖了下来,搜索找到关键的字符串。
libarchive在09年加入这部分代码,16年才修复这个bug。 这个代码是从PostgresSQL扣过来的,人家用LENGTHOF 我看他的提交记录说改成MinGW编译,就手贱改成了sizeof 刚刚那个Zeal就是用的这个老包。 但是我始终没明白为什么编译器会搞成 for(1),而不是sizeof数组的大小 我查看了Zeal的构建记录,它编译的lib是静态库,里面确实是for(1),但是代码肯定不可能编译成那个样子的啊 我试过了,VS、MinGW的Gcc都不会 奇葩啊,我准备抽空给它写个issue。喊他创建文件夹和更新一下官网的包 昨天晚上准备把go备份redmine的代码写完的,结果用这个去下go的文档,就挂了。试了好几次都这样,终于不能忍了。 结果后面追代码花了很长时间,代码也没写成。。。
ClientKey是类似SSO的token,拿到后可以访问各种QQ服务,如空间、邮箱、微博等,2014年分析着玩的,在此记录一下。
分析记录: QQ:6.3、6.8 7.4 PlatformCore
1、找到KernelUtil!Util::Misc::GetSignature,挂QQ调试,跟进这些函数里看找函数地址,看偏移是否对得上
2、CTXStringW* KernelUtil!Util::Misc::GetSignature 获取ClientKey
struct ITXCore { char* c1; // 00 c1->common_func1(ITXCore*, 675585B0, &Prelogin); // 24 char* c2; // 18 common_func2(c2, ITXCore*,675585B0,&Prelogin); // 20 char* c3; //2c c3->common_func3(ITXCore*,675585B0,&Prelogin) // 4 c4 = poi(c3+0c)+poi(c3+4) c4->common_func4(ITXCore*,675585B0,&Prelogin)// 8 c5 = poi(c4+1c) common_func5(c5,ITXCore*,675585B0,&Prelogin)// 34 c6 = c5+14 c6->search1(&&Prelogin,675585B0,c4) index = poi(poi(c6+4)+4)+c; memcmp(index,675585B0) }; find_clientkey_script: 1、找到PreLogin对象。 as platform_core 004b69b8 as c2 poi(platform_core+0x18) as c3 poi(c2+0x2c) as c4 poi(c3+0x0c)+poi(c3+0x4) as c5 poi(c4+0x1c) as c6 c5 + 0x14 step1: r $t1=004b69b8;r $t2=poi($t1+0x18);r $t3=poi($t2+0x2c);r $t4=poi($t3+0x0c)+poi($t3+0x4);r $t5=poi($t4+0x1c);r $t6=$t5+0x14; step2: r $t3=poi($t6+4); r $t0=poi(poi($t6+4)+4);.