跳到主要内容C++ 逆向入门实战:Qt 程序分析与调试 | 极客日志C++算法
C++ 逆向入门实战:Qt 程序分析与调试
通过构建一个基于 Qt6 的登录注册 Demo,演示了如何使用 IDA 和 x64dbg 进行逆向分析。内容包括项目搭建、编译运行、动态调试(下断点、查看寄存器)、静态分析(字符串搜索、伪代码还原)以及修改汇编指令绕过密码校验的完整流程。旨在帮助初学者理解逆向工程的基本思路与工具使用。
奶糖兔152 浏览 CMakeLists.txt(Qt6 + Widgets)入门逆向最舒服的一条路就是先写一个你完全理解的程序,再用 IDA + x64dbg 去'拆自己',学习成本最低、反馈最快。下面将用一个 Qt6 登录/注册 Demo,介绍如何在 IDA 和 x64dbg 里一步步定位关键逻辑。
目标 1:做一个'可逆向练习'的登录/注册程序
功能设计:
- 注册:用户名 + 密码 → 保存到本地(JSON 或 SQLite)
- 登录:读本地数据 → 校验
- UI:两个页面(QStackedWidget):Login / Register
- 校验函数:
bool checkCredentials(user, pass)
1) 目录结构
创建文件夹 QtLoginLab/,里面放这些文件:
QtLoginLab/
CMakeLists.txt
main.cpp
mainwindow.h
mainwindow.cpp
auth.h
auth.cpp
storage.h
storage.cpp
CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(QtLoginLab LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
add_executable(QtLoginLab main.cpp mainwindow.h mainwindow.cpp auth.h auth.cpp storage.h storage.cpp )
target_link_libraries(QtLoginLab PRIVATE Qt6::Widgets)
# 逆向学习友好:Debug 更好看,Release 更接近真实
if (MSVC)
target_compile_options(QtLoginLab PRIVATE $<$<CONFIG:Debug>:/Od /Zi> $<$<CONFIG:Release>:/O2>)
target_link_options(QtLoginLab PRIVATE $<$<CONFIG:Debug>:/DEBUG>)
else()
target_compile_options(QtLoginLab PRIVATE $<$<CONFIG:Debug>:-O0 -g> $<$<CONFIG:Release>:-O2>)
endif()
main.cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MainWindow w;
w.();
a.();
}
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
show
return
exec
#pragma once
#include <QString>
QString weakHash(const QString& s);
bool timingSafeEqual(const QString& a, const QString& b);
#include "auth.h"
#include <QByteArray>
QString weakHash(const QString& s) {
QByteArray b = s.toUtf8();
quint32 x = 0x12345678;
for (auto ch : b) {
x = (x << 5) ^ (x >> 27) ^ quint8(ch);
x += 0x9e3779b9;
}
return QString::number(x, 16);
}
bool timingSafeEqual(const QString& a, const QString& b) {
QByteArray ba = a.toUtf8();
QByteArray bb = b.toUtf8();
if (ba.size() != bb.size()) return false;
quint8 diff = 0;
for (int i = 0; i < ba.size(); ++i) diff |= quint8(ba[i]) ^ quint8(bb[i]);
return diff == 0;
}
#pragma once
#include <QString>
#include <QMap>
class Storage {
public:
explicit Storage(QString path);
bool load();
bool save() const;
bool hasUser(const QString& user) const;
bool addUser(const QString& user, const QString& passHash);
QString getHash(const QString& user) const;
private:
QString m_path;
QMap<QString, QString> m_users;
};
#include "storage.h"
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
Storage::Storage(QString path) : m_path(std::move(path)) {}
bool Storage::load() {
QFile f(m_path);
if (!f.exists()) return true;
if (!f.open(QIODevice::ReadOnly)) return false;
auto doc = QJsonDocument::fromJson(f.readAll());
if (!doc.isObject()) return false;
m_users.clear();
auto obj = doc.object();
for (auto it = obj.begin(); it != obj.end(); ++it) m_users[it.key()] = it.value().toString();
return true;
}
bool Storage::save() const {
QJsonObject obj;
for (auto it = m_users.begin(); it != m_users.end(); ++it) obj[it.key()] = it.value();
QFile f(m_path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false;
f.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
return true;
}
bool Storage::hasUser(const QString& user) const {
return m_users.contains(user);
}
bool Storage::addUser(const QString& user, const QString& passHash) {
if (m_users.contains(user)) return false;
m_users[user] = passHash;
return true;
}
QString Storage::getHash(const QString& user) const {
return m_users.value(user);
}
#pragma once
#include <QMainWindow>
#include <QStackedWidget>
#include <QLineEdit>
#include <QLabel>
#include "storage.h"
class MainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private slots:
void gotoLogin();
void gotoRegister();
void onRegisterClicked();
void onLoginClicked();
private:
void buildUi();
void setStatus(const QString& msg, bool ok);
QStackedWidget* m_stack = nullptr;
QWidget* m_loginPage = nullptr;
QLineEdit* m_loginUser = nullptr;
QLineEdit* m_loginPass = nullptr;
QWidget* m_regPage = nullptr;
QLineEdit* m_regUser = nullptr;
QLineEdit* m_regPass = nullptr;
QLabel* m_status = nullptr;
Storage m_storage;
};
#include "mainwindow.h"
#include "auth.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QStandardPaths>
#include <QDir>
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
, m_storage([&]{
const auto base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(base);
return base + "/users.json";
}()) {
buildUi();
if (!m_storage.load()) {
setStatus("LOAD_JSON_FAIL", false);
} else {
setStatus("READY", true);
}
}
void MainWindow::buildUi() {
setWindowTitle("QtLoginLab");
resize(420, 240);
auto central = new QWidget(this);
auto root = new QVBoxLayout(central);
m_stack = new QStackedWidget(central);
m_loginPage = new QWidget(m_stack);
{
auto v = new QVBoxLayout(m_loginPage);
auto title = new QLabel("Login", m_loginPage);
title->setStyleSheet("font-size:18px; font-weight:600;");
m_loginUser = new QLineEdit(m_loginPage);
m_loginUser->setPlaceholderText("Username");
m_loginPass = new QLineEdit(m_loginPage);
m_loginPass->setPlaceholderText("Password");
m_loginPass->setEchoMode(QLineEdit::Password);
auto btnRow = new QHBoxLayout();
auto loginBtn = new QPushButton("Login", m_loginPage);
auto toRegBtn = new QPushButton("Go Register", m_loginPage);
btnRow->addWidget(loginBtn);
btnRow->addWidget(toRegBtn);
v->addWidget(title);
v->addWidget(m_loginUser);
v->addWidget(m_loginPass);
v->addLayout(btnRow);
connect(loginBtn, &QPushButton::clicked, this, &MainWindow::onLoginClicked);
connect(toRegBtn, &QPushButton::clicked, this, &MainWindow::gotoRegister);
}
m_regPage = new QWidget(m_stack);
{
auto v = new QVBoxLayout(m_regPage);
auto title = new QLabel("Register", m_regPage);
title->setStyleSheet("font-size:18px; font-weight:600;");
m_regUser = new QLineEdit(m_regPage);
m_regUser->setPlaceholderText("New Username");
m_regPass = new QLineEdit(m_regPage);
m_regPass->setPlaceholderText("New Password");
m_regPass->setEchoMode(QLineEdit::Password);
auto btnRow = new QHBoxLayout();
auto regBtn = new QPushButton("Create Account", m_regPage);
auto toLoginBtn = new QPushButton("Go Login", m_regPage);
btnRow->addWidget(regBtn);
btnRow->addWidget(toLoginBtn);
v->addWidget(title);
v->addWidget(m_regUser);
v->addWidget(m_regPass);
v->addLayout(btnRow);
connect(regBtn, &QPushButton::clicked, this, &MainWindow::onRegisterClicked);
connect(toLoginBtn, &QPushButton::clicked, this, &MainWindow::gotoLogin);
}
m_stack->addWidget(m_loginPage);
m_stack->addWidget(m_regPage);
m_status = new QLabel("STATUS", central);
m_status->setTextInteractionFlags(Qt::TextSelectableByMouse);
root->addWidget(m_stack);
root->addWidget(m_status);
setCentralWidget(central);
m_stack->setCurrentWidget(m_loginPage);
}
void MainWindow::setStatus(const QString& msg, bool ok) {
m_status->setText(msg);
m_status->setStyleSheet(ok ? "color: #1a7f37;" : "color: #c62828;");
}
void MainWindow::gotoLogin() {
m_stack->setCurrentWidget(m_loginPage);
setStatus("READY_LOGIN", true);
}
void MainWindow::gotoRegister() {
m_stack->setCurrentWidget(m_regPage);
setStatus("READY_REGISTER", true);
}
void MainWindow::onRegisterClicked() {
const QString user = m_regUser->text().trimmed();
const QString pass = m_regPass->text();
if (user.isEmpty() || pass.isEmpty()) {
setStatus("REG_EMPTY_INPUT", false);
return;
}
if (m_storage.hasUser(user)) {
setStatus("USER_EXISTS", false);
return;
}
const QString h = weakHash(pass);
if (!m_storage.addUser(user, h)) {
setStatus("ADD_USER_FAIL", false);
return;
}
if (!m_storage.save()) {
setStatus("WRITE_JSON_FAIL", false);
return;
}
setStatus("WRITE_JSON_OK", true);
gotoLogin();
}
void MainWindow::onLoginClicked() {
const QString user = m_loginUser->text().trimmed();
const QString pass = m_loginPass->text();
if (user.isEmpty() || pass.isEmpty()) {
setStatus("LOGIN_EMPTY_INPUT", false);
return;
}
if (!m_storage.hasUser(user)) {
setStatus("NO_SUCH_USER", false);
return;
}
const QString inputHash = weakHash(pass);
const QString savedHash = m_storage.getHash(user);
if (timingSafeEqual(inputHash, savedHash)) {
setStatus("AUTH_OK", true);
} else {
setStatus("AUTH_FAIL", false);
}
}
2) 编译运行
这里直接用 Qt Creator 打开 CMakeLists.txt。第一次打开时会弹出 Configure Project。需要配置一下。配置完之后点击左下角 绿色 ▶ Run。如果一切正常,应该能看到窗口底部有状态字符串(READY / AUTH_OK / AUTH_FAIL)。
目标 2:逆向程序
逆向就是一个不断尝试、推理、反向证明的过程。不能盲目尝试,程序都有它自己的特征,常见的特征有:系统函数、字符串以及一些通用业务所有的特征。可以根据这些去下断点。X64 动态调试,IDA 静态分析去证明找对了地方。
1. 先用 x64dbg 调试
- 用 x64dbg(x64) 打开
QtLoginLab.exe(64 位就用 x64dbg)。
- 先按 F9 跑到程序主界面稳定显示。
- 非常明显可以看到弹出来的是一个对话框,那么它大概率会调用系统函数 MessageBox。我们就根据这个来下断点。打开 x64dbg,在 CPU 窗口里,按住 CTRL+G 然后分别输入 MessageBoxW,MessageBoxA。
- 鼠标移动到这一行按 F2 下断点,同理,对 MessageBoxA 下断点也是如此。
- 下完断点后我们可以点这个窗口,就可以看到自己下的断点位置。
- 然后输入随便的用户名密码点登录按钮,看会不会出来弹窗来告诉我们登录成功还是失败。如果出现弹窗且如果断到了 MessageBox,就看调用栈往上两三层,通常就是'判断成功/失败'的函数。
可惜我们自己写的程序,并没有弹窗来告诉登录成功与否。那对于密码校验,很可能用的就是 ucrtbase.memcmp, msvcrt.memcmp, ucrtbase.strcmp, msvcrt.strcmp 这些函数,不确定哪些存在的话就都试一遍,能下成功就会生效。但在这之前,先把之前的断点删掉以免影响判断,右键那一行,点击删除即可。
接着下断点,这里在 strcmp 下了一个断点,可以看到已经成功断住。
但是他是不是校验账号密码的函数呢?要接着分析。先观察寄存器窗口。
此时 RCX = 第一个字符串,RDX = 第二个字符串。且截图里已经说明
RCX -> "QMenuBar"
RDX -> "QPaintDevice"
这说明程序正在比较两个字符串。翻译成 C++ 代码就是
strcmp("QMenuBar", "QPaintDevice")
好了,可以证明这次 strcmp 和'登录密码'**无关。**再尝试别的路子,通过刚刚那个窗口其实还能得到一个信息就是登录结果是通过 QLabel::setText() 显示的,字符串 NO_SUCH_USER 一定是在'登录判断结果之后'才被用到。所以策略是:从 NO_SUCH_USER 这个字符串反向,找到'是谁决定用它的'。这时就要用到 IDA 去分析了。
2. 用 IDA 去分析
打开 IDA 后,打开 Strings 窗口(快捷键:Shift + F12),在字符串列表里搜索:NO_SUCH_USER。
从上图得到重要信息 DATA XREF: sub_1400026C0+85↑o。这说明了 sub_1400026C0 函数用了这个字符串,直接搜这个函数,操作如下图所示。
发现这个函数就是判断登录是否成功的一个重要函数,这就是一个关键点。接着拿着这个地址去 x64dbg 里面,看他是否能断到,这里函数的地址是 '140002910',尝试一下在 x64dbg 里能不能断到。
结果发现他是无效地址,这是因为 IDA 里没有加上执行文件模块的偏移。现在 x64dbg 下找一下,先点击视图再点模块。
这里 qtloginlab.exe 的基址是 00007FF6C1870000,可以用下面操作复制。
在这个输入框里输入 0x7FF6C1870000,然后点击 OK 让他修正。
修正完之后再用找字符串的方式在 IDA 里找一下那个关键函数。
又回到这里了,可以看到函数地址跟之前已经是不一样的了。把这串地址拿到放到 x64dbg 里看一下能不能找到,这里是有效的。
把断点打到这,点击登录,看会不会断进来。断成功了说明找的没错。
结合反汇编代码找一下判断登录逻辑的函数。可以找到这个下面这个关键的判断。
这里就是输入了已经注册的用户名,接着来判断密码正不正确的位置。输一个已经注册过的用户名,但输入一个错误的密码,点击登录。
发现确实断到了这里,同时也证明了猜想,说明找对了。同时在此时观察一下寄存器窗口哦,发现 ZF 是 1,ZF = 1 ⇒ 条件'为 0 / 为假'。这样再往下运行是不会成功的,改一下 ZF 的值看一下效果会怎样。右键 ZF,点击切换,再按 F9 运行。
可以看到效果,输入了正确的值,但是输入了错误的密码,但这样还是登录成功了,下方已经显示 AUTH_OK。说明努力是有效的。
如何让他永久生效呢?可以参考一下下面的文章 x64dbg 使用详解。
作者也简单阐述一下
- 鼠标选中需要修改的汇编代码(可选中多行),选择'二进制' -> '编辑'(或者使用快捷键 Ctrl + E)。
- 修改十六进制代码后,点击确定。
- 修改十六进制代码后,按下快捷键 CTRL+ P,或者右击,选择'补丁',弹出'补丁对话框'。
- 选择'修补文件'按钮,另存为。
- 接着命名一个 exe,然后保存,这样就永久修改好了。