首页
社区
课程
招聘
[原创] Qt6 MetaObject解析脚本
发表于: 2天前 767

[原创] Qt6 MetaObject解析脚本

2天前
767

Qt6 MetaObject解析脚本

参考自 Qt5 程序初步逆向分析+解析脚本

Github -> d53K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6q4c8f1g2q4K9r3g2^5i4K6u0r3f1i4c8y4k6i4c8S2f1r3q4J5M7$3g2J5

0. 前言

众所周知, 逆向Qt程序的时候要是能找到信号和槽的关系就能事半功倍 (¬‿¬).

但是Qt6的数据结构改变了, 导致参考文献中的脚本不再适用了, 因此需要重新分析一下.

1. Qt5 回顾

在 Qt5 中, moc编译器生成的核心数据包括:

  1. qt_meta_stringdata_XXX —— 一个含有 QByteArrayData 数组和紧随其后的 C 字符串的复合结构体
  2. qt_meta_data_XXX —— 一个 uint 数组, 包含 QMetaObjectPrivate 头部及方法/属性/枚举的描述数据
  3. 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):ida_qt5_staticMetaObject根据这些信息还可以还原出class名, method名, 以及参数类型和名称:ida_qt5_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&lt;int&gt;::of(&TsignalApp::mySignal),
            this,
            QOverload&lt;int&gt;::of(&TsignalApp::mySlot));

    connect(this,
            QOverload&lt;int, int&gt;::of(&TsignalApp::mySignalParam),
            this,
            QOverload&lt;int, int&gt;::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 &lt;QtCore/qmetatype.h&gt;

#include &lt;QtCore/qtmochelpers.h&gt;

#include &lt;memory&gt;


#include &lt;QtCore/qxptype_traits.h&gt;
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'tsignalapp.h' doesn't include &lt;QObject&gt;."
#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&lt;void()&gt;(1, 2, QMC::AccessPublic, QMetaType::Void),
        // Signal 'mySignal'
        QtMocHelpers::SignalData&lt;void(int)&gt;(1, 2, QMC::AccessPublic, QMetaType::Void, {{
            { QMetaType::Int, 3 },
        }}),
        // Signal 'mySignalParam'
        QtMocHelpers::SignalData&lt;void(int, int)&gt;(4, 2, QMC::AccessPublic, QMetaType::Void, {{
            { QMetaType::Int, 3 }, { QMetaType::Int, 5 },
        }}),
        // Slot 'mySlot'
        QtMocHelpers::SlotData&lt;void()&gt;(6, 2, QMC::AccessPublic, QMetaType::Void),
        // Slot 'mySlot'
        QtMocHelpers::SlotData&lt;void(int)&gt;(6, 2, QMC::AccessPublic, QMetaType::Void, {{
            { QMetaType::Int, 3 },
        }}),
        // Slot 'mySlotParam'
        QtMocHelpers::SlotData&lt;void(int, int)&gt;(7, 2, QMC::AccessPublic, QMetaType::Void, {{
            { QMetaType::Int, 3 }, { QMetaType::Int, 5 },
        }}),
    };
    QtMocHelpers::UintData qt_properties {
    };
    QtMocHelpers::UintData qt_enums {
    };
    return QtMocHelpers::metaObjectData&lt;TsignalApp, qt_meta_tag_ZN10TsignalAppE_t&gt;(QMC::MetaObjectFlag{}, qt_stringData,
            qt_methods, qt_properties, qt_enums);
}
Q_CONSTINIT const QMetaObject TsignalApp::staticMetaObject = { {
    QMetaObject::SuperData::link&lt;QMainWindow::staticMetaObject&gt;(),
    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&lt;TsignalApp *&gt;(_o);
    if (_c == QMetaObject::InvokeMetaMethod) {
        switch (_id) {
        case 0: _t->mySignal(); break;
        case 1: _t->mySignal((*reinterpret_cast&lt;std::add_pointer_t&lt;int&gt;>(_a[1]))); break;
        case 2: _t->mySignalParam((*reinterpret_cast&lt;std::add_pointer_t&lt;int&gt;>(_a[1])),(*reinterpret_cast&lt;std::add_pointer_t&lt;int&gt;>(_a[2]))); break;
        case 3: _t->mySlot(); break;
        case 4: _t->mySlot((*reinterpret_cast&lt;std::add_pointer_t&lt;int&gt;>(_a[1]))); break;
        case 5: _t->mySlotParam((*reinterpret_cast&lt;std::add_pointer_t&lt;int&gt;>(_a[1])),(*reinterpret_cast&lt;std::add_pointer_t&lt;int&gt;>(_a[2]))); break;
        default: ;
        }
    }
    if (_c == QMetaObject::IndexOfMethod) {
        if (QtMocHelpers::indexOfMethod&lt;void (TsignalApp::*)()&gt;(_a, &TsignalApp::mySignal, 0))
            return;
        if (QtMocHelpers::indexOfMethod&lt;void (TsignalApp::*)(int )&gt;(_a, &TsignalApp::mySignal, 1))
            return;
        if (QtMocHelpers::indexOfMethod&lt;void (TsignalApp::*)(int , int )&gt;(_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&lt;void*&gt;(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&lt;QMetaType *&gt;(_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&lt;void&gt;(this, &staticMetaObject, 1, nullptr, _t1);
}

// SIGNAL 2
void TsignalApp::mySignalParam(int _t1, int _t2)
{
    QMetaObject::activate&lt;void&gt;(this, &staticMetaObject, 2, nullptr, _t1, _t2);
}
QT_WARNING_POP

可以看到几个关键的变化 源码:

2.1 QMetaObject::Data

Q_CONSTINIT const QMetaObject TsignalApp::staticMetaObject = { {
    QMetaObject::SuperData::link&lt;QMainWindow::staticMetaObject&gt;(),
    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 字节, 包含 refsizeallocoffset 字段. 通过 &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&lt;const char *&gt;(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&lt;const char *&gt;(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: 14int]
[ClassInfo 数据: classInfoCount * 2 个 uint]
[方法数据: methodCount * 5 个 uint]           ← 每方法 5 个 uint
[方法参数数据: 变长]
[属性数据: propertyCount * 3 个 uint]
[枚举数据: ...]
[构造函数数据: ...]
0  // eod 标记

3.2 Qt6 的 data[] 布局

[QMetaObjectPrivate: 14int]
[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看的( ᐛ ).

只要理解最关键的, 也就是我们要解析的这个数据是这样变化的:qt5_cmp_qt6.png其中stringdata的结构变了, 计算method的偏移多了1个. 使用完解析脚本的效果如下:result然后看一下qt_static_metacall这个分发函数, 和解析出来的是按顺序对应的:metacall然后就可以愉快的逆向了.


[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回