-
-
[原创] Qt6 MetaObject解析脚本
-
发表于: 2天前 767
-
Qt6 MetaObject解析脚本
Github -> d53K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6q4c8f1g2q4K9r3g2^5i4K6u0r3f1i4c8y4k6i4c8S2f1r3q4J5M7$3g2J5
0. 前言
众所周知, 逆向Qt程序的时候要是能找到信号和槽的关系就能事半功倍 (¬‿¬).
但是Qt6的数据结构改变了, 导致参考文献中的脚本不再适用了, 因此需要重新分析一下.
1. Qt5 回顾
在 Qt5 中, moc编译器生成的核心数据包括:
qt_meta_stringdata_XXX—— 一个含有QByteArrayData数组和紧随其后的 C 字符串的复合结构体qt_meta_data_XXX—— 一个uint数组, 包含QMetaObjectPrivate头部及方法/属性/枚举的描述数据staticMetaObject—— 一个QMetaObject实例, 其d成员指向以上数据
const QMetaObject TsignalApp::staticMetaObject = {
{ &QMainWindow::staticMetaObject, qt_meta_stringdata_TsignalApp.data,
qt_meta_data_TsignalApp, qt_static_metacall, Q_NULLPTR, Q_NULLPTR}
};
其对应的结构体QMetaObject.d为 源码:
// Qt5 qobjectdefs.h
struct QMetaObject {
struct { // private data
const QMetaObject *superdata;
const QByteArrayData *stringdata; // 指向 QByteArrayData 数组
const uint *data;
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall;
const QMetaObject * const *relatedMetaObjects;
void *extradata;
} d;
};
看一下在ida中对应的数据形如(supderdata为NULL):
根据这些信息还可以还原出class名, method名, 以及参数类型和名称:
2. Qt6 moc代码
用 Qt 6.11.0 重新编译一下参考文章中的代码:
#include "tsignalapp.h"
#include <QMessageBox>
#include <QString>
TsignalApp::TsignalApp(QWidget *parent)
: QMainWindow(parent)
{
// Qt6 语法
connect(this,
QOverload<>::of(&TsignalApp::mySignal),
this,
QOverload<>::of(&TsignalApp::mySlot));
connect(this,
QOverload<int>::of(&TsignalApp::mySignal),
this,
QOverload<int>::of(&TsignalApp::mySlot));
connect(this,
QOverload<int, int>::of(&TsignalApp::mySignalParam),
this,
QOverload<int, int>::of(&TsignalApp::mySlotParam));
}
void TsignalApp::mySlot()
{
QMessageBox::about(this,
"Tsignal",
"This is a signal/slot sample without parameter.");
}
void TsignalApp::mySlot(int x)
{
QMessageBox::about(this,
"Tsignal",
QString("This is a signal/slot sample with one parameter. x=%1").arg(x));
}
void TsignalApp::mySlotParam(int x, int y)
{
QMessageBox::about(this,
"Tsignal",
QString("x: %1 y: %2").arg(x).arg(y));
}
void TsignalApp::slotFileNew()
{
emit mySignal();
emit mySignal(5);
emit mySignalParam(5, 100);
}
拿到新的moc编译器产出的代码:
/****************************************************************************
** Meta object code from reading C++ file 'tsignalapp.h'
**
** Created by: The Qt Meta Object Compiler version 69 (Qt 6.11.0)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include "../../../tsignalapp.h"
#include <QtCore/qmetatype.h>
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'tsignalapp.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 69
#error "This file was generated using the moc from 6.11.0. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
#ifndef Q_CONSTINIT
#define Q_CONSTINIT
#endif
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
namespace {
struct qt_meta_tag_ZN10TsignalAppE_t {};
} // unnamed namespace
template <> constexpr inline auto TsignalApp::qt_create_metaobjectdata<qt_meta_tag_ZN10TsignalAppE_t>()
{
namespace QMC = QtMocConstants;
QtMocHelpers::StringRefStorage qt_stringData {
"TsignalApp",
"mySignal",
"",
"x",
"mySignalParam",
"y",
"mySlot",
"mySlotParam"
};
QtMocHelpers::UintData qt_methods {
// Signal 'mySignal'
QtMocHelpers::SignalData<void()>(1, 2, QMC::AccessPublic, QMetaType::Void),
// Signal 'mySignal'
QtMocHelpers::SignalData<void(int)>(1, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::Int, 3 },
}}),
// Signal 'mySignalParam'
QtMocHelpers::SignalData<void(int, int)>(4, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::Int, 3 }, { QMetaType::Int, 5 },
}}),
// Slot 'mySlot'
QtMocHelpers::SlotData<void()>(6, 2, QMC::AccessPublic, QMetaType::Void),
// Slot 'mySlot'
QtMocHelpers::SlotData<void(int)>(6, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::Int, 3 },
}}),
// Slot 'mySlotParam'
QtMocHelpers::SlotData<void(int, int)>(7, 2, QMC::AccessPublic, QMetaType::Void, {{
{ QMetaType::Int, 3 }, { QMetaType::Int, 5 },
}}),
};
QtMocHelpers::UintData qt_properties {
};
QtMocHelpers::UintData qt_enums {
};
return QtMocHelpers::metaObjectData<TsignalApp, qt_meta_tag_ZN10TsignalAppE_t>(QMC::MetaObjectFlag{}, qt_stringData,
qt_methods, qt_properties, qt_enums);
}
Q_CONSTINIT const QMetaObject TsignalApp::staticMetaObject = { {
QMetaObject::SuperData::link<QMainWindow::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN10TsignalAppE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN10TsignalAppE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN10TsignalAppE_t>.metaTypes,
nullptr
} };
void TsignalApp::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
auto *_t = static_cast<TsignalApp *>(_o);
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
case 0: _t->mySignal(); break;
case 1: _t->mySignal((*reinterpret_cast<std::add_pointer_t<int>>(_a[1]))); break;
case 2: _t->mySignalParam((*reinterpret_cast<std::add_pointer_t<int>>(_a[1])),(*reinterpret_cast<std::add_pointer_t<int>>(_a[2]))); break;
case 3: _t->mySlot(); break;
case 4: _t->mySlot((*reinterpret_cast<std::add_pointer_t<int>>(_a[1]))); break;
case 5: _t->mySlotParam((*reinterpret_cast<std::add_pointer_t<int>>(_a[1])),(*reinterpret_cast<std::add_pointer_t<int>>(_a[2]))); break;
default: ;
}
}
if (_c == QMetaObject::IndexOfMethod) {
if (QtMocHelpers::indexOfMethod<void (TsignalApp::*)()>(_a, &TsignalApp::mySignal, 0))
return;
if (QtMocHelpers::indexOfMethod<void (TsignalApp::*)(int )>(_a, &TsignalApp::mySignal, 1))
return;
if (QtMocHelpers::indexOfMethod<void (TsignalApp::*)(int , int )>(_a, &TsignalApp::mySignalParam, 2))
return;
}
}
const QMetaObject *TsignalApp::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
void *TsignalApp::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_staticMetaObjectStaticContent<qt_meta_tag_ZN10TsignalAppE_t>.strings))
return static_cast<void*>(this);
return QMainWindow::qt_metacast(_clname);
}
int TsignalApp::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QMainWindow::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 6)
qt_static_metacall(this, _c, _id, _a);
_id -= 6;
}
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 6)
*reinterpret_cast<QMetaType *>(_a[0]) = QMetaType();
_id -= 6;
}
return _id;
}
// SIGNAL 0
void TsignalApp::mySignal()
{
QMetaObject::activate(this, &staticMetaObject, 0, nullptr);
}
// SIGNAL 1
void TsignalApp::mySignal(int _t1)
{
QMetaObject::activate<void>(this, &staticMetaObject, 1, nullptr, _t1);
}
// SIGNAL 2
void TsignalApp::mySignalParam(int _t1, int _t2)
{
QMetaObject::activate<void>(this, &staticMetaObject, 2, nullptr, _t1, _t2);
}
QT_WARNING_POP
可以看到几个关键的变化 源码:
2.1 QMetaObject::Data
Q_CONSTINIT const QMetaObject TsignalApp::staticMetaObject = { {
QMetaObject::SuperData::link<QMainWindow::staticMetaObject>(),
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN10TsignalAppE_t>.stringdata,
qt_staticMetaObjectStaticContent<qt_meta_tag_ZN10TsignalAppE_t>.data,
qt_static_metacall,
nullptr,
qt_staticMetaObjectRelocatingContent<qt_meta_tag_ZN10TsignalAppE_t>.metaTypes,
nullptr
} };
->
// qtbase/src/corelib/kernel/qobjectdefs.h (Qt6)
struct QMetaObject {
struct Data {
SuperData superdata; // 父类 QMetaObject 指针
const uint *stringdata; // ★ 变更: uint* 而非 QByteArrayData*
const uint *data; // QMetaObjectPrivate + 方法/属性/枚举数据
typedef void (*StaticMetacallFunction)(QObject *, QMetaObject::Call, int, void **);
StaticMetacallFunction static_metacall; // qt_static_metacall 函数指针
const SuperData *relatedMetaObjects; // 关联的元对象
const QtPrivate::QMetaTypeInterface *const *metaTypes; // ★ 新增: 元类型接口数组
void *extradata; // 保留
} d;
};
主要变化为stringdata类型变了, 多了一个metaTypes字段, 因此解析的逻辑需要适配这个新的stringdata.
2.2 其他数据结构
- QMetaObjectPrivate
// qtbase/src/corelib/kernel/qmetaobject_p.h
struct QMetaObjectPrivate {
// revision 历史:
// 7 = Qt 5.0
// 8 = Qt 5.12 (枚举名)
// 9 = Qt 6.0 (属性和方法的 metatype)
// 10 = Qt 6.2 (metaobject 的 metatype 存入 metaTypes 数组; const 方法标记_
// 11 = Qt 6.5 (void 的 metatype 存入 metaTypes 数组)
// 12 = Qt 6.6 (枚举的 metatype)
// 13 = Qt 6.9 (64-bit QFlags; 方法 revision 移入新位置)
enum { OutputRevision = 13 };
int revision; // [0] 当前值 = 13
int className; // [1] 类名在 stringdata 中的索引
int classInfoCount; // [2] classInfo 数量
int classInfoData; // [3] classInfo 数据在 data[] 中的偏移
int methodCount; // [4] 方法总数(信号+槽+普通方法)
int methodData; // [5] 方法数据在 data[] 中的偏移
int propertyCount; // [6] 属性数量
int propertyData; // [7] 属性数据在 data[] 中的偏移
int enumeratorCount; // [8] 枚举数量
int enumeratorData; // [9] 枚举数据在 data[] 中的偏移
int constructorCount; // [10] 构造函数数量
int constructorData; // [11] 构造函数数据在 data[] 中的偏移
int flags; // [12] 标志位
int signalCount; // [13] 信号数量
};
// 与 Qt5 完全相同的字段布局. 差异仅在 revision 值上: Qt5 为 7, Qt6 为 9~13 取决于具体版本.
- QMetaMethod::Data
// qtbase/src/corelib/kernel/qmetaobject.h
struct QMetaMethod::Data {
enum { Size = 6 }; // ★ Qt5 为 5
uint name() const { return d[0]; } // 方法名在 stringdata 中的索引
uint argc() const { return d[1]; } // 参数个数
uint parameters() const { return d[2]; } // 参数类型数据在 data[] 中的偏移
uint tag() const { return d[3]; } // tag 在 stringdata 中的索引
uint flags() const { return d[4]; } // 标志位(Access | MethodType | ...)
uint metaTypeOffset() const { return d[5]; } // ★ 新增: 在 metaTypes 数组中的偏移
const uint *d;
};
关键变化:Size 从 5 → 6, 新增 metaTypeOffset, 因此, 索引为 id 的方法在 data[] 中的偏移计算为:
方法偏移 = QMetaObjectPrivate.methodData + id * 6 //(Qt5 为 `methodData + id * 5`)
flags 位域定义(来自 qtmocconstants.h):
namespace QtMocConstants {
enum MethodFlags : uint {
AccessPrivate = 0x00,
AccessProtected = 0x01,
AccessPublic = 0x02,
AccessMask = 0x03,
MethodMethod = 0x00, // 普通方法
MethodSignal = 0x04, // 信号
MethodSlot = 0x08, // 槽
MethodConstructor= 0x0c, // 构造函数
MethodTypeMask = 0x0c,
MethodCompatibility = 0x10,
MethodCloned = 0x20,
MethodScriptable = 0x40,
MethodRevisioned = 0x80,
MethodIsConst = 0x100, // ★ Qt6 新增
};
};
其他数据结构让AI直接分析一下就好了。
2.3 字符串数据的变化
- Qt5 的字符串存储
Qt5 使用 QByteArrayData 数组, 每个元素 24 字节, 包含 ref、size、alloc、offset 字段. 通过 &arrayData + offset 计算字符串地址。
// Qt5 moc 生成
struct qt_meta_stringdata_TsignalApp_t {
QByteArrayData data[10]; // 10 个 QByteArrayData 元素
char stringdata[78]; // 原始 C 字符串
};
- Qt6 的字符串存储
Qt6 彻底重写了字符串存储机制. QMetaObject.d.stringdata 现在指向 const uint*, 使用紧凑的二进制编码.
在 Qt6 最新版的 moc 代码中, 字符串通过 QtMocHelpers::StringRefStorage 模板类构建:
// Qt6 moc 生成的代码
QtMocHelpers::StringRefStorage qt_stringData {
"TsignalApp", // 索引 0
"mySignal", // 索引 1
"", // 索引 2
"x", // 索引 3
"mySignalParam", // 索引 4
"y", // 索引 5
"mySlot", // 索引 6
"mySlotParam" // 索引 7
};
在编译后的二进制中, stringdata 指向一个 uint 数组, 其编码格式为:
[偏移和长度信息...][null终止的字符串数据紧随其后]
每个字符串在 uint 数组中用 一对 uint(共 8 字节)表示:
stringdata[2*index]= 该字符串相对于stringdata起始位置的字节偏移stringdata[2*index + 1]= 该字符串的长度(不含 null 终止符)
字符串内容本身紧跟在索引区之后, 以 \0 分隔.
这一格式可在 Qt6 源码 qmetaobject.cpp 中确认:
// qtbase/src/corelib/kernel/qmetaobject.cpp
static inline const char *rawStringData(const QMetaObject *mo, int index)
{
uint offset = mo->d.stringdata[2*index];
return reinterpret_cast<const char *>(mo->d.stringdata) + offset;
}
static inline QByteArrayView stringDataView(const QMetaObject *mo, int index)
{
uint offset = mo->d.stringdata[2*index];
uint length = mo->d.stringdata[2*index + 1];
const char *string = reinterpret_cast<const char *>(mo->d.stringdata) + offset;
return {string, qsizetype(length)};
}
2.4 MetaType 系统的引入
Qt6 的新增特性之一是在元对象中引入了 QMetaTypeInterface 指针数组.QMetaObject.d.metaTypes 指向一个 const QMetaTypeInterface* 数组, 包含了:
- 每个方法的返回类型和参数类型的
QMetaTypeInterface指针 - 每个属性的类型
- 元对象自身的类型(revision 10+)
void类型(revision 11+)- 枚举类型(revision 12+)
QMetaMethod::Data 中新增的 metaTypeOffset 字段就是该方法对应的类型信息在 metaTypes 数组中的起始索引.
3. data[] 数组总体布局对比
3.1 Qt5 的 data[] 布局
[QMetaObjectPrivate: 14 个 int]
[ClassInfo 数据: classInfoCount * 2 个 uint]
[方法数据: methodCount * 5 个 uint] ← 每方法 5 个 uint
[方法参数数据: 变长]
[属性数据: propertyCount * 3 个 uint]
[枚举数据: ...]
[构造函数数据: ...]
0 // eod 标记
3.2 Qt6 的 data[] 布局
[QMetaObjectPrivate: 14 个 int]
[ClassInfo 数据: classInfoCount * 2 个 uint]
[方法数据: methodCount * 6 个 uint] ← 每方法 6 个 uint (★)
[方法参数数据: 变长]
[属性数据: propertyCount * 5 个 uint] ← 每属性 5 个 uint (★)
[枚举数据: enumeratorCount * 5 个 uint] ← 每枚举 5 个 uint (★)
[构造函数数据: ...]
0 // eod 标记
主要变化总结(每个条目的 uint 数):
| 数据类型 | Qt5 Size | Qt6 Size |
|---|---|---|
| QMetaMethod::Data | 5 | 6 |
| QMetaProperty::Data | 3 | 5 |
| QMetaEnum::Data | 4 (rev 7) / 5 (rev 8+) | 5 |
| QMetaClassInfo::Data | 2 | 2 |
4. 其他细节 - QT_NO_DATA_RELOCATION 模式
许多 Qt6 程序定义了 QT_NO_DATA_RELOCATION 宏. 此时 SuperData 从 1 个指针扩展为 2 个指针:
struct SuperData {
const QMetaObject *direct; // 通常为 NULL
Getter indirect; // 函数指针, 运行时调用以获取父类 QMetaObject
};
此时 QMetaObject::Data 的内存布局变为 8 个指针(64 字节). 位于源码第597行.
5. 脚本效果
写了这么多, 其实大部分数据结构我也没仔细看, 都是opus4.6看的( ᐛ ).
只要理解最关键的, 也就是我们要解析的这个数据是这样变化的:
其中stringdata的结构变了, 计算method的偏移多了1个. 使用完解析脚本的效果如下:
然后看一下qt_static_metacall这个分发函数, 和解析出来的是按顺序对应的:
然后就可以愉快的逆向了.
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。