记一次数组越界引发的内存篡改导致的野指针引用灾难

本文最后更新于: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

D:\SogouInput\Components

可以肯定的是,这不是本程序的任何输出

也就是说,要么是QtCreator的提示,要么是搜狗输入法搞的鬼

经过搜索,发现我并不是唯一一个受到搜狗青睐的码农

有不少人都遇到过这样的提示,但大伙都一致表示,这锅还真不能让搜狗背

Google

跟搜狗没关系

而且,我的记忆中貌似也遇到过这样的提示,确实是野指针的问题

是你,又不是你

既然知道了是野指针的问题,那就找呗

但奇怪的是,在反反复复阅读了所有代码后,我确认了一个事实:

  • 我的代码里没有任何可能的野指针,全员初始化

调查再次陷入僵局

Windows, 你怎么看

通过Windows的事件查看器,我们发现,程序的异常退出触发了一次Application Error记录

事件查看器

可以发现,出错的模块为:Qt5Gui.dll

至少可以确认是与GUI相关函数出错,且通过错误偏移量可以确认函数名称(虽然当时并不知道How)

但与GUI操作相关的类太多,依旧难以定位

对不起,搜狗

有谁还记得搜狗给我留下的线索:D:\SogouInput\Components

我尝试着进入这个目录,发现了crash文件夹,里面含有:

  • crash.dmp
  • errorlog.txt

errorlog.txt里记录了QF的崩溃时间,模块以及当时的所有进程

crash.dmpWindows的内存转储文件,需要用windbg打开

里面记录了QF崩溃瞬间的内存信息,以及函数调用链

这是程序生命最后的黑匣子(www)

1
2
3
4
5
6
7
8
9
10
11
12
ExceptionAddress: 000000000119fa8a (Qt5Gui!ZNK4QPeneqERKS_+0x000000000000001a)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000000
Parameter[1]: ffffffffffffffff
Attempt to read from address ffffffffffffffff

STACK_TEXT:
Qt5Gui!ZNK4QPeneqERKS_+0x1a
Qt5Gui!ZN8QPainter6setPenERK4QPen+0x23
QQ_Follower+0x4d51

这是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,嗯,嗯?嗯!嗯?

这次确实有些不同,为了减少每次构造 & 析构的开销,我使用了staticpen修饰为静态变量

1
2
3
4
5
6
7
void Widget::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
static QPen pen(Qt::darkGray, 2);
painter.setPen(pen);
painter.drawRect(this->rect());
}

不过,那又如何呢

一个static QPen总不可能是野指针吧,况且是通过const &引用传递

虽说QPen类内含指针成员,但是没有任何函数修改这个static对象呀

不,不是你

不应该不应该,怎么说都不可能是static的问题

这非常违反我多年来的C++常识

此案另有隐情

试试也无妨

在数次希望与失望中徘徊后,我甚至想过是Sougou32/64 bits版本问题

在思索无果后,虽然很冲击我的常识,但还是决定死马当活马医,试试去掉static

扑朔迷离

在去除static后,离奇的事情发生了

虽然程序还是崩溃了,但是dmp的分析结果却产生了变化

1
2
3
4
5
6
7
ExceptionAddress: 000000006895a94e (Qt5Core!ZN9QtPrivate20QStringList_containsEPK11QStringListRK7QStringN2Qt15CaseSensitivityE+0x000000000000000e)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000000
Parameter[1]: ffffffffffffffff
Attempt to read from address ffffffffffffffff

这次发生异常的函数不再是QPen::operator==() 而是ZN9QtPrivate20QStringList_containsEPK11QStringListRK7QStringN2Qt15CaseSensitivityE

也就是:

1
QPrivate::QStringList_contains(const QStringList *that, const QString &str, Qt::CaseSensitivity cs)

惊人的相似

那么,我在什么地方使用了这个函数呢

我记得非常清楚

1
2
static const QStringList BlackList = { "XXX", "YYY", "ZZZ" };
if(BlackList.contains(title)){...}

我只在这个QStringList对象上调用了contains函数(该函数在内部会继续调用上述函数)

发现了吗,又是一个static对象,又是野指针非法内存访问

这种不谋而合的相似,真是让我不寒而栗

难以言说,但有种真相呼之欲出的感觉

前方是什么

为了解决这个疑问

我决定要亲眼看一看内存

QtCreatordebug模式调试,并设置断点,查看BlackList的内存

乍一看,平平无奇,但当我抬头时,我却看到了令人震撼的一幕

鬼压床

内存

BlackList的内存上方,赫然立着两个数组 text & path,分别存储窗口标题和进程路径

如果这俩数组下溢的话,那将直接覆盖BlackList的内存,后果不堪设想,其中的指针必然野化

顺便说一下,这俩数组为何会并列BlackList出现在这里

那是因为 text path BlackList均为static变量,统一置于静态存储区

真相?

至此,我们可以大致还原事件的真相

可能是某数组下溢,导致BlackList中的指针被覆盖,导致产生野指针,解引用后非法内存访问导致崩溃

不过,究竟是什么导致数组溢出?

柳暗花明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
QString Win::getWindowText(HWND hwnd)
{
static WCHAR text[128];//#
GetWindowTextW(hwnd, text, sizeof(text));//#
return QString::fromWCharArray(text);
}
QString Win::getProcessName(HWND hwnd)
{
if (hwnd == nullptr) return QString();
DWORD PID = getProcessID(hwnd);

static WCHAR path[128];//#
HANDLE Process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, PID);
GetProcessImageFileNameW(Process, path, sizeof(path));//#
CloseHandle(Process);

QString pathS = QString::fromWCharArray(path);
return pathS.mid(pathS.lastIndexOf('\\') + 1);
}

问题只可能出现在GetWindowTextW & GetProcessImageFileNameW的参数上(Windows API分为W和A版本,W代表宽字符)

但是为什么明明用了sizeof还会溢出呢?

问题就出在这

阅读官方文档可知:

1
2
3
4
5
int GetWindowTextW(
[in] HWND hWnd,
[out] LPWSTR lpString,
[in] int nMaxCount //复制到缓冲区的最大字符数,包括空字符。如果文本超出此限制,则将其截断。
);

最后一个参数是 最大字符数,而非字节数

一字之差,导致了程序的崩溃

ASCII编码下,一字符为一字节,可以用sizeof直接获取

而为了表示中文,就需要用到宽字符WCHAR(wchar_t)

一个宽字符并不是一字节 所以使用sizeof会导致结果大于字符数,导致text溢出

正确的做法是使用_countof宏来获取实际数组元素个数

path同上

结束了吗?

并没有

仔细观察可以发现,textpath上方,要溢出也是先覆盖path,并不会对BlackList造成影响

真相就在眼前,却要峰回路转了嘛?

仔细回忆可以发现,程序貌似只在release模式下崩溃过,在debug模式下虽然也会有SougouInput的信息:

  • Shared library architecture i386 is not compatible with target architecture i386:x86-64.

但显然与之前不同,且不会造成崩溃

冷不丁

难道是release & debug模式下内存分布不同?

确是如此

release模式下输出三者地址:

1
2
3
path:         0x40d140
text: 0x40d240
&BlackList: 0x40d358

发现text更靠近BlackList,溢出后直接导致其覆盖

还没完

知道了内部机理,但究竟是什么导致了text溢出

一般情况下,窗口标题是不太可能超过128字符的

这也是此bug难以复现的原因之一

在偶然情况下,我记录到了使程序崩溃的窗口标题,长达173字符

  • Chrome浏览器在加载网页的瞬间,窗口标题短暂变为网址(可能还未获取到网页标题)

这也解释了必须在玩乐之后才会崩溃的原因

而且在text越界后,程序并不会马上崩溃,必须要在QQ窗口重新获取焦点并监测BlackList时,才会触发野指针

更增加了debug难度

结案

该事件,十分复杂,让我们来捋一捋bug发生的流程:

  • Chrome浏览器在加载网页的瞬间,窗口标题短暂变为网址,导致text溢出
  • text溢出覆盖staticQPen(before) 或者 覆盖staticQStringList(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 - 博客园

野指针 Crash - 简书

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


记一次数组越界引发的内存篡改导致的野指针引用灾难
https://mrbeancpp.github.io/2022/03/21/记一次数组越界引发的内存篡改导致的野指针引用灾难/
作者
MrBeanC
发布于
2022年3月21日
许可协议