qtInspect,qt开发的gui程序界面浏览工具

背景

大概在2012年当时开发一个基于图像识别的反钓鱼系统,大概的逻辑就是先对网页进行截图然后检查是否包含某些特征。公司的服务器多数是linux系统,而且当时也只有windows的开发经验,所以最早的方案是wine + mfc + ie ocx,后面发现性能&排版存在问题,再接着找到了开源的跨平台截图命令行 http://cutycapt.sourceforge.net (基于QtWebkit)。第一次接触qt编程,感叹到c++写gui原来也可以这么优雅。项目结束后出于个人的兴趣又深入研究了一下qt的一些底层实现,信号和槽的原理,消息队列的实现,moc的实现,跨平台的封装,渲染机制 ,qtcreator对调试器的包装等等。

win32标准的ui控件都关联一个HWND对象,微软也对HWND定义了一整套api,市面上有存在若干个HWND信息查看工具,如spyxx, Window Detective,winspy等等 。 因为qt的ui是自绘的,所以上面的工具并不能直接用于qt开发的gui程序。当时研究qt原理的时候就想着写个类似的spy工具(一直觉得带着某个需求去研究会更有动力),写到20%左右感觉自己大概会了就不再继续。 2020年底转岗到云游戏,刚好团队某个工程用qt, 再次激发了动力。

原理

每一个qt gui程序都存在一个QApplication对象,QApplication有个静态类函数topLevelWidgets,那么可以注入dll/so, 先枚举所有顶层的QWidget,再从顶层QWidget递归查找子QWidget。 QWidget继承自QObject, 从QObject可以得到QMetaObject, 再从QMetaObject得到QMetaProperty,最后进行各种Get/Set操作。

这里需要重点解决2个问题
1 并不能直接在程序里面直接使用qt的类,因为不同的版本的qt abi可能不一样。 另外由于是开源,某些软件可能魔改了qt。所以我采用的方案是注入后, 定义好匹配的调用方式然后去GetProcAddress得到地址。 这里就避开了99%的abi和结构体问题,唯一需要硬编码定义的是QListData(topLevelWidgets的返回的list, 这个类并不存在导出,由c++内联到程序)

struct QListData {
	int RefCount;
	int alloc, begin, end;
	void* array[1];

	QListData() {
		memset(this, 0, sizeof(QListData));
	}
};

2 QObject::metaObject是个虚函数,自定义子类的metaObject并不存在在导出,所以我采用的方案从QObject的虚函数表先得到metaObject的索引偏移,再用子类的QObject对象直接按偏移取到地址进行调用

struct QMetaObject {
	char dummy[64];
	QObjectData() {
		memset(this, 0, sizeof(QMetaObject));
	}
};

size_t find_offset_from_vtable(size_t* vtable, size_t* func) {
	if (vtable == NULL)
	{
		return -1;
	}

	size_t* ptr = (size_t*)vtable;
	while (true)
	{
		if (*ptr == (size_t)func)
		{
			break;
		}

		++ptr;
	}

	odprintf("%p %p %d", vtable, func, ptr - vtable);
	return ptr - vtable;
}

QMetaObject* (__thiscall* g_QObject_metaObject)(QObject*) = NULL;
QMetaObject* call_QObject_metaObject_by_vtable(size_t offset, QObject* obj) {
	size_t* vtable = *(size_t**)obj;
	auto func = (decltype(g_QObject_metaObject))vtable[offset];

	return func(obj);
}

g_QObject_metaObject = (decltype(g_QObject_metaObject))GetProcAddress(g_QtCore, "?metaObject@QObject@@UEBAPEBUQMetaObject@@XZ");

g_QObject_Vtable = (size_t*)GetProcAddress(g_QtCore, "??_7QObject@@6B@");
g_QObject_metaObject_vtable_index = find_offset_from_vtable(g_QObject_Vtable, (size_t*)g_QObject_metaObject);

auto meta = call_QObject_metaObject_by_vtable(g_QObject_metaObject_vtable_index, obj);

最后注入的dll/so绑定本地的8282端口,接受ui的枚举或者属性修改操作

httplib::Server svr;
svr.new_task_queue = [] { return new httplib::ThreadPool(1); };

svr.Post("/pick", [](const httplib::Request& r, httplib::Response& res) {			
	auto str = dump_qt();
	res.set_content(str, "application/json");
});

svr.Post("/modify", [](const httplib::Request& r, httplib::Response& res) {
	modify_qt(r.body);
	res.set_content("{}", "application/json");
});

svr.listen("0.0.0.0", 8282);

pyside2写个简单的treeWidget, tableWidget,splitter分隔一下

class MyMainWindow(QMainWindow, mainwindow.Ui_MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        self.splitter.setStretchFactor(1, 2)
        self.splitter.setChildrenCollapsible(False)

        self.next_pushButton.clicked.connect(self.next_pushButton_clicked)
        self.refresh_pushButton.clicked.connect(self.refresh_pushButton_clicked)
        self.treeWidget.itemClicked.connect(self.treeWidget_itemActivated)
        self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.tableWidget.cellChanged.connect(self.tableWidget_cellChanged)

        self.lineEdit.textChanged.connect(self.lineEdit_textChanged)

效果

例如cppcheck, 关于对话框

qtInspect搜索 “分析工具” , 自动定位到节点

修改成 "hello world",回车

已经变成 "hello world"

ida首页

已经变成“测试测试测试”

posted @ 2022-07-19 17:21  tieyan  阅读(658)  评论(1编辑  收藏  举报