Qt QML资源在PE文件中存储结构分析
最近分析了一个Qt QML程序,第一次接触Qt应用,记录分享一下。
开始以为它是个纯Qt程序,用IDA跟踪没有头绪,查了一下引入的动态库发现是Qt QML应用,感觉从QML的存储机制入手更容易。
下载了Qt5源码和qt creator开源版,写编了一个QtQuick的测试程序qtquicktest.exe,就用它来分析QML在PE文件中的存储结构。
首先通过qtquicktest工程输出目录中的Makefile和Makefile.Debug文件,大概了解了基本的构建机制。qt creator用rcc将所有qml资源放到qrc_qml.cpp中,QML资源就随qrc_qml.cpp一起被编译到PE中。
qtquicktest工程输出目录:
下图是Makefile.Debug中rcc的命令行:
qrc_qml.cpp有4万多行,但实际代码非常少,可以看出所有QML资源被放到qt_resource_data、qt_resource_name、 qt_resource_struct三个数组中,排除这三个数组实际代码不到100行。
以下是qrc_qml.cpp删除三个数组初始化数据的代码:
查看代码,QML资源是由qInitResources_qml调用qRegisterResourceData加载的,代码如下:
从上述代码可以知道,找到qInitResources_qml函数就能定位 qt_resource_data、qt_resource_name、 qt_resource_struct三个数组。
用IDA反汇编qtquicktest.exe,因为qtquicktest.exe带调试信息,可以直接在Names窗口搜索
qt_resource_data,如下图:
查看内容,三个数组中的内容和qrc_qml.cpp中初始化数组的数据一致。
再看是谁调用了qt_resource_data?发现两处:
上述结果和qrc_qml.cpp源码是一致的。
查看qInitResources_qml(void)函数,汇编代码如下:
用IDA跟踪qInitResources_qml,也能定位到qrc_qml.cpp
单步进qRegisterResourceData函数,可以看到它的源码在..\qtbase\src\corelib\io\qresource.cpp1080行。
qRegisterResourceData函数的参数tree、name、data分别对应qt_resource_struct、 qt_resource_name、 qt_resource_data,搞清楚这三个指针指向的内容,就可以继续分析QML在PE中的存储结构了。
上面三图显示了这三个指针指向的内容以及内容加载后的虚拟地址和PE文件的偏移。
根据上面获取的信息结合qrc_qml.cpp和qresource.cpp源代码,可以分析出QML这三块数据的存储结构,总结如下(如果不做说明字节序都是大端的):
一、qRegisterResourceData 的参数解释
|
|
|
|
|
| 参数 |
传参寄存器 |
PE文件偏移 |
含义 |
| version |
rcx |
|
资源格式版本号,支持1-3,当前为 3 |
| qt_resource_struct |
rdx |
0xBF190 |
资源树结构数组,描述目录/文件层级 |
| qt_resource_name |
r8 |
0xBE560 |
资源名称字符串表 |
| qt_resource_data |
r9 |
0x203F0 |
资源文件实际数据 |
二、三块数据的内存布局
1、qt_resource_name(name)
qt_resource_name是资源节点名称表,在qrc_qml.cpp中定义如下:
在PE文件的内容如下:
从qrc_qml.cpp的定义中可以知道qt_resource_name中定义了一些字符串,每个字符串4行,比如“Manager.qml”就是:
0x0,0xb,
0xd,0x62,0x60,0xdc,
0x0,0x4d,
0x0,0x61,0x0,0x6e,0x0,0x61,0x0,0x67,0x0,0x65,0x0,0x72,0x0,0x2e,0x0,0x71,0x0,0x6d,0x0,0x6c,
每个字符串分三部分:2字节长度、4字节字符串hash、UTF-16LE格式的字符串。上图用不同颜色显示字符串的三部分,蓝色是2字节的长度11,绿色是hash:0xD6260DC ,黑色就是“Manager.qml”11个字符。
2、qt_resource_data(data)
qt_resource_data包含了QML资源树中节点文件的内容,在qrc_qml.cpp中定义如下:
PE文件的内容如下:
Manager.qml源文件的开始几行如下:
上面三个图展示了qt_resource_data中的第一组数据就是Manager.qml文件的内容,每组数据分两部分,前4个字节是内容长度(data_length),后面就是文件内容。从PE文件的内容中我们可以看到0x000007A5(十进制1957)是Manager.qml的大小,后面就是Manager.qml文件的内容(EF BB BF是BOM头表示文件是UTF-8编码)。这里需要注意,如果文件是压缩的结构还是一样,data_length(前4个字节)是后续内容的长度,但后续内容的前4个字节是压缩文件解压后的长度,再后面是被压缩的数据。
上述代码说明data_length - sizeof(quint32)是实际的压缩数据长度。所以当是压缩文件时,data_length = 解压后大小(4字节)+ 被压缩数据的大小。
Manager.qml的大小1957,同文件管理器中显示的一致:
qt_resource_data内存布局如下:
|
|
|
| 内容长度(data_length) |
内容 |
|
| 4B |
文件内容 |
|
| … |
|
|
| 4B |
4B解压后大小 |
压缩数据 |
| … |
|
|
3、qt_resource_struct(tree)
qt_resource_struct是描述QML资源树节点的‘记录‘数组,一条记录对应一个节点,每条记录22个字节。要得到每个节点的信息,一定是先获取qt_resource_struct中节点的描述记录,再根据节点记录在qt_resource_name和qt_resource_data中得到节点名和内容。
qrc_qml.cpp中qt_resource_struct定义如下:
下图是PE中的qt_resource_struct内容,展示了开始的8条记录:
qt_resource_struct在内存布局如下:
|
| 记录1(22字节) |
| 记录2(22字节) |
| 记录3(22字节) |
| … |
| 记录n(22字节) |
| … |
记录的字段含义:
|
|
|
|
|
| 偏移 |
字节数 |
名称 |
第一条记录 |
含义 |
| 0x00 |
4 |
nameOffset |
00 00 00 00 |
指向节点名在qt_resource_name中的偏移,0表示第一个字符串:Manager.qml |
| 0x04 |
2 |
flags |
00 02 |
节点的属性: bit0:zlib压缩、bit1:是否目录、bit2:zstd压缩 0x0002表示该节点是目录。 |
| 0x06 |
4 |
locale |
00 00 00 07 |
地区和语言 |
| 0x0A |
4 |
dataOffset |
00 00 00 01 |
如果是文件:指向该节点内容在qt_resource_data表的偏移 如果是目录:表示第一个子节点的索引(从1 开始) |
| 0x0E |
8 |
lastModified |
00 00 00 00 00 00 00 00 |
最后修改时间,目录一般是0。 |
以上就是QML资源在PE文件存储的细节,根据这些信息就可以编写工具任意提取或替换PE文件中的QML资源了。我在github找到一个提取QML的工具qtextract,如果你只想提取QML资源用这个工具应该够用。qtextract通过静态扫描给qRegisterResourceData函数传递qt_resource_struct、qt_resource_data、qt_resource_name三个参数的指令,通过分析指令提取这三块数据在PE中的位置。由于不同版本、不同环境编译的Qt应用传参指令有差异,qtextract扫描指令不够智能,有些指令组合不能自动识别,像我编译的qtquicktest.exe就无法用qtextract自动提取QML资源。但qtextract有高级模式,手工分析得到这三个偏移后用--data 或 --datarva 命令行参数告诉qtextract,qtextract就能提取QML资源。所以要处理QML资源,无论是用现成的工具还是自己开发工具,都需要定位qt_resource_struct、qt_resource_data、qt_resource_name这三块数据的位置。要定位这三块数据,本质上就是要找qInitResources_qml函数。下面来看看在不带调试信息的Qt QML应用中如何定位qInitResources_qml函数?
三、定位qInitResources_qml函数
因为qInitResources_qml调用qRegisterResourceData函数,qRegisterResourceData 是Qt动态库引出的函数,所以只要在引入表搜索qRegisterResourceData找到qRegisterResourceData引入项,就可以找到qInitResources_qml函数。
在IDA引入窗口中找到qRegisterResourceData
再双击它跳转到引入表
再逐级追溯调用者就能定位到qInitResources_qml函数
上图中的sub_140001130就是qInitResources_qml函数,这样我们就得到qt_resource_struct、qt_resource_data、qt_resource_name三个数据块在PE中的偏移和虚拟地址。
你可以比较一下sub_140001130和前面分析的qtquicktest.exe中的qInitResources_qml在给qRegisterResourceData函数传参指令上的细微差异(mov ecx,xxx的位置和形式不同),更好理解qtextract为什么不能自动扫描qtquicktest.exe的QML资源。
根据上述三个图的内容,很容易确定
byte_1401C5A60是qt_resource_data,文件偏移是0x1c4260
byte_14026A4F0是qt_resource_name,文件偏移是0x268cf0
byte_14026B4C0是qt_resource_struct,文件偏移是0x269cc0
这样就完成Qt QML资源的三块数据的定位。
下面以qt_resource_struct的第四条记录为例,看看三个数组的对应关系:
从上图的数据中可以知道,第四条记录是一个未压缩的文件,名称为:main.qml,最后修改时间是:2026-03-02 05:35:46(00 00 01 9C AD 0B A0 0F)文件大小是0x1851(6225)
前几行代码是:
[C] 纯文本查看 复制代码
import QtQuick 2.15
import QtQuick.Window 2.15
import Qt.labs.platform 1.1
import QtQuick.Controls 2.15
import Clipboard 1.0
到此基本搞清楚了QML在PE中的存储结构。对于开启了QML缓存的应用,要让导入的QML资源起作用还需要搞清楚怎样关闭缓存。开始我不清楚这些,导入了QML资源后怎么运行都不起作用,浪费了不少时间。关于缓存的内容下次再找时间贴出来,主要讲一下怎样查找QML节点对应的缓存。缓存的信息在官网文档提到一些:https://doc.qt.io/archives/qt-5.15/qtquick-deployment.html。