记一次数组越界引发的内存篡改导致的野指针引用灾难
本文最后更新于:2022年3月29日 上午
前情提要
那是一个风和日丽的晚上,我回到寝室,打开电脑,发现我的程序窗口(姑且叫它QF.exe
)消失了
- 这是一个跟随QQ窗口的辅助小程序
“可能是之前忘记开了吧”,我并没有太在意
随后便再次打开了QF.exe
刷刷B站,玩玩游戏,好不快活
可当我再次打开QQ,点击QF
的窗口时,看着旋转的鼠标指针,我好像知道接下来要发生什么了
QF
再次消失了
ta 轻轻地走了,正如 ta 轻轻地来,挥一挥鼠标,不带走一块内存
只留我一人,在风中凌乱
“ta 不是这样的人,ta 从不会不辞而别”
我这才意识到事情的严重性,其实之前也发生过类似的事情,但我却一直不以为意,让 ta 承受了太多
悔不当初,如果给我再来一次的机会,我一定会对 ta 说三个字:我焯!
加班吧
你能再表演一次吗
修正程序错误的第一步是要重现这个错误。—— Tom Duff
如果收集不到足够多的bug
样例,就没有办法对bug
进行分析
所以debug
的第一步就是复现bug
当时无论我如何测试程序,ta 表现得都极其正常,反而是在我抛之脑后几小时,才会出现闪退,并且时机极其随机
- 即:无法确定
bug
触发机制
调查陷入了僵局
搜狗,我叫你一声你敢应吗
唯一值得在意的是,异常退出前,在QtCreator
的输出窗口中出现了这么一行:D:\SogouInput\Components
可以肯定的是,这不是本程序的任何输出
也就是说,要么是QtCreator
的提示,要么是搜狗输入法搞的鬼
经过搜索,发现我并不是唯一一个受到搜狗青睐的码农
有不少人都遇到过这样的提示,但大伙都一致表示,这锅还真不能让搜狗背
而且,我的记忆中貌似也遇到过这样的提示,确实是野指针的问题
是你,又不是你
既然知道了是野指针的问题,那就找呗
但奇怪的是,在反反复复阅读了所有代码后,我确认了一个事实:
- 我的代码里没有任何可能的野指针,全员初始化
调查再次陷入僵局
Windows, 你怎么看
通过Windows
的事件查看器,我们发现,程序的异常退出触发了一次Application Error
记录
可以发现,出错的模块为:Qt5Gui.dll
至少可以确认是与GUI
相关函数出错,且通过错误偏移量可以确认函数名称(虽然当时并不知道How)
但与GUI
操作相关的类太多,依旧难以定位
对不起,搜狗
有谁还记得搜狗给我留下的线索:D:\SogouInput\Components
我尝试着进入这个目录,发现了crash
文件夹,里面含有:
- crash.dmp
- errorlog.txt
errorlog.txt
里记录了QF
的崩溃时间,模块以及当时的所有进程
而crash.dmp
是Windows
的内存转储文件,需要用windbg
打开
里面记录了QF
崩溃瞬间的内存信息,以及函数调用链
这是程序生命最后的黑匣子(www)
1 |
|
这是dll
的导出函数,虽然函数名经过处理,不过还是能大致看出其本来面目
是谁?
可以清晰地看到,元凶是ZNK4QPeneqERKS_
这个函数进行了非法内存访问,导致的崩溃
而上一级调用函数是ZN8QPainter6setPenERK4QPen
根据代码猜测,这两个函数分别是QPen::operator==()
和 QPainter::setPen()
而且本程序唯一使用QPainter::setPen()
的地方只有paintEvent()
并且貌似之前都是在窗体重新渲染时,出现的闪退
这么一来,基本可以确定是paintEvent()
中QPainter::setPen()
的问题了
为什么是你?
乍一看,已经找到了问题所在,但你仔细想想,这是十分不合理的
在paintEvent()
中利用QPainter::setPen()
设置画笔,改变颜色 & 线条粗细等,是常规操作,不应该出问题
在仔细阅读了Qt
源码后,我发现,QPainter::setPen()
中确实用到了QPen::operator==()
用以比较新旧画笔
但是这个函数想要出现指针引用错误,就只能是QPainter::setPen(const QPen& pen)
传入的参数pen
的问题了
这个pen
,嗯,嗯?嗯!嗯?
这次确实有些不同,为了减少每次构造 & 析构的开销,我使用了static
将pen
修饰为静态变量
1 |
|
不过,那又如何呢
一个static QPen
总不可能是野指针吧,况且是通过const &
引用传递
虽说QPen
类内含指针成员,但是没有任何函数修改这个static
对象呀
不,不是你
不应该不应该,怎么说都不可能是static
的问题
这非常违反我多年来的C++
常识
此案另有隐情
试试也无妨
在数次希望与失望中徘徊后,我甚至想过是Sougou
的32/64 bits
版本问题
在思索无果后,虽然很冲击我的常识,但还是决定死马当活马医,试试去掉static
扑朔迷离
在去除static
后,离奇的事情发生了
虽然程序还是崩溃了,但是dmp
的分析结果却产生了变化
1 |
|
这次发生异常的函数不再是QPen::operator==()
而是ZN9QtPrivate20QStringList_containsEPK11QStringListRK7QStringN2Qt15CaseSensitivityE
也就是:
1 |
|
惊人的相似
那么,我在什么地方使用了这个函数呢
我记得非常清楚
1 |
|
我只在这个QStringList
对象上调用了contains
函数(该函数在内部会继续调用上述函数)
发现了吗,又是一个static
对象,又是野指针非法内存访问
这种不谋而合的相似,真是让我不寒而栗
难以言说,但有种真相呼之欲出的感觉
前方是什么
为了解决这个疑问
我决定要亲眼看一看内存
用QtCreator
的debug
模式调试,并设置断点,查看BlackList
的内存
乍一看,平平无奇,但当我抬头时,我却看到了令人震撼的一幕
鬼压床
在BlackList
的内存上方,赫然立着两个数组 text
& path
,分别存储窗口标题和进程路径
如果这俩数组下溢的话,那将直接覆盖BlackList
的内存,后果不堪设想,其中的指针必然野化
顺便说一下,这俩数组为何会并列BlackList
出现在这里
那是因为 text
path
BlackList
均为static
变量,统一置于静态存储区
真相?
至此,我们可以大致还原事件的真相
可能是某数组下溢,导致BlackList
中的指针被覆盖,导致产生野指针,解引用后非法内存访问导致崩溃
不过,究竟是什么导致数组溢出?
柳暗花明
1 |
|
问题只可能出现在GetWindowTextW
& GetProcessImageFileNameW
的参数上(Windows API分为W和A版本,W代表宽字符)
但是为什么明明用了sizeof
还会溢出呢?
问题就出在这
阅读官方文档可知:
1 |
|
最后一个参数是 最大字符数,而非字节数
一字之差,导致了程序的崩溃
在ASCII
编码下,一字符为一字节,可以用sizeof
直接获取
而为了表示中文,就需要用到宽字符WCHAR
(wchar_t)
一个宽字符并不是一字节 所以使用sizeof
会导致结果大于字符数,导致text
溢出
正确的做法是使用_countof
宏来获取实际数组元素个数
path
同上
结束了吗?
并没有
仔细观察可以发现,text
在path
上方,要溢出也是先覆盖path
,并不会对BlackList
造成影响
真相就在眼前,却要峰回路转了嘛?
仔细回忆可以发现,程序貌似只在release
模式下崩溃过,在debug
模式下虽然也会有SougouInput
的信息:
- Shared library architecture i386 is not compatible with target architecture i386:x86-64.
但显然与之前不同,且不会造成崩溃
冷不丁
难道是release
& debug
模式下内存分布不同?
确是如此
在release
模式下输出三者地址:
1 |
|
发现text
更靠近BlackList
,溢出后直接导致其覆盖
还没完
知道了内部机理,但究竟是什么导致了text
溢出
一般情况下,窗口标题是不太可能超过128字符的
这也是此bug
难以复现的原因之一
在偶然情况下,我记录到了使程序崩溃的窗口标题,长达173字符
- 是
Chrome
浏览器在加载网页的瞬间,窗口标题短暂变为网址(可能还未获取到网页标题)
这也解释了必须在玩乐之后才会崩溃的原因
而且在text
越界后,程序并不会马上崩溃,必须要在QQ窗口重新获取焦点并监测BlackList
时,才会触发野指针
更增加了debug
难度
结案
该事件,十分复杂,让我们来捋一捋bug
发生的流程:
- 是
Chrome
浏览器在加载网页的瞬间,窗口标题短暂变为网址,导致text
溢出 text
溢出覆盖static
的QPen
(before) 或者 覆盖static
的QStringList
(After),导致两者内部的指针野化- 前者在
paintEvent()
时崩溃,后者在QQ获取焦点,比较BlackList
时崩溃(非法内存访问)
注意点:
release
&debug
模式下内存分布可能不同
解决办法:
- 使用
_countof
宏来获取实际数组元素个数,而非sizeof
启示:
- 使用
dmp
内存转储文件来记录程序崩溃瞬间的内存,并用windbg
分析调用链 - 使用调试模式,观察内存,注意数组越界 & 野指针
- 使用
ProcDump
工具创建dmp
文件 - 其实
Windows
系统也会自动生成dmp
文件,路径:C:\Users\用户名\AppData\Local\CrashDumps
(在系统属性-高级-启动和故障恢复中设置)
注:
- 搜狗输入法可以获取程序崩溃信息 可能是因为输入法需要
dll
注入
焯:
Ref
ProcDump - Windows Sysinternals | Microsoft Docs
Qt 利用 dmp 文件进行调试_Copy->Paste的博客-CSDN博客_dmp qt
读懂_countof,可以懂得什么 - envoy - 博客园
GetWindowTextW function (winuser.h) - Win32 apps | Microsoft Docs
WinDbg 入门(用户模式) - Windows drivers | Microsoft Docs
Qt报错:C:\Program Files (x86)\SogouInput\Components_PlasticMatrix的博客-CSDN博客
qpainter.cpp source code [qtbase/src/gui/painting/qpainter.cpp] - Woboq Code Browser