mirror of
https://github.com/radareorg/iaito.git
synced 2026-11-07 18:36:29 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d51daa6a | ||
|
|
f20ddbc357 | ||
|
|
c47985f1b0 |
No files matched your search
@@ -48,6 +48,25 @@ plugin core_plugin: iaito
|
||||
rm -f $(R2_USER_PLUGINS)/IaitoPlugin.$(R2_LIBEXT)
|
||||
cp -f build/CorePlugin.$(R2_LIBEXT) $(R2_USER_PLUGINS)/IaitoPlugin.$(R2_LIBEXT)
|
||||
|
||||
# iaito plugin: r2frida as a second IO descriptor, not a native debug session.
|
||||
ifeq ($(shell uname),Darwin)
|
||||
IAITO_FRIDA_PLUGIN_DIR=$(HOME)/Library/Application Support/radareorg/iaito/plugins/native
|
||||
else
|
||||
IAITO_FRIDA_PLUGIN_DIR=$(HOME)/.local/share/radareorg/iaito/plugins/native
|
||||
endif
|
||||
|
||||
frida-plugin:
|
||||
mkdir -p build/frida-plugin
|
||||
cd build/frida-plugin && $(QMAKE) ../../src/plugins/r2frida/IaitoFridaPlugin.pro $(QMAKE_FLAGS)
|
||||
$(MAKE) -C build/frida-plugin
|
||||
mkdir -p "$(IAITO_FRIDA_PLUGIN_DIR)"
|
||||
cp -f build/plugins/native/libIaitoFridaPlugin.* "$(IAITO_FRIDA_PLUGIN_DIR)/"
|
||||
ifeq ($(shell uname),Darwin)
|
||||
# The linker’s ad-hoc signature is not valid for dlopen. macOS then
|
||||
# SIGKILLs iaito with “Code Signature Invalid” while loading the plugin.
|
||||
codesign --force --sign - "$(IAITO_FRIDA_PLUGIN_DIR)/libIaitoFridaPlugin.dylib"
|
||||
endif
|
||||
|
||||
asan:
|
||||
export CXXFLAGS=-fsanitize=address ; \
|
||||
export CFLAGS=-fsanitize=address ; \
|
||||
@@ -69,7 +88,7 @@ endif
|
||||
mrproper: clean
|
||||
git clean -xdf
|
||||
|
||||
.PHONY: install run user-install dist macos clean mrproper install-translations
|
||||
.PHONY: install run user-install dist macos clean mrproper install-translations frida-plugin
|
||||
.PHONY: dockindent dockindent-image indent indent-host
|
||||
.PHONY: install-deeplink uninstall-deeplink
|
||||
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "FridaBackend.h"
|
||||
|
||||
#include "common/R2Task.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
|
||||
FridaBackend::FridaBackend(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
bool FridaBackend::pluginAvailable()
|
||||
{
|
||||
if (availabilityChecked_) {
|
||||
return available_;
|
||||
}
|
||||
availabilityChecked_ = true;
|
||||
const QString listed = Core()->cmd(QStringLiteral("oL"));
|
||||
available_ = listed.contains(QStringLiteral("frida"), Qt::CaseInsensitive);
|
||||
return available_;
|
||||
}
|
||||
|
||||
int FridaBackend::currentFd() const
|
||||
{
|
||||
bool ok = false;
|
||||
const int fd = Core()->cmd(QStringLiteral("oqq")).trimmed().toInt(&ok);
|
||||
return ok ? fd : -1;
|
||||
}
|
||||
|
||||
QSet<int> FridaBackend::openFds() const
|
||||
{
|
||||
QSet<int> fds;
|
||||
const QJsonArray files = Core()->cmdj(QStringLiteral("oj")).array();
|
||||
for (const QJsonValue value : files) {
|
||||
fds.insert(value.toObject().value(QStringLiteral("fd")).toInt());
|
||||
}
|
||||
return fds;
|
||||
}
|
||||
|
||||
int FridaBackend::findNewFridaFd(const QSet<int> &before) const
|
||||
{
|
||||
const QJsonArray files = Core()->cmdj(QStringLiteral("oj")).array();
|
||||
for (const QJsonValue value : files) {
|
||||
const QJsonObject file = value.toObject();
|
||||
const int fd = file.value(QStringLiteral("fd")).toInt();
|
||||
const QString uri = file.value(QStringLiteral("uri")).toString();
|
||||
if (!before.contains(fd) && uri.startsWith(QStringLiteral("frida://"))) {
|
||||
return fd;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void FridaBackend::closeFd(int fd)
|
||||
{
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
Core()->cmd(QStringLiteral("o-") + QString::number(fd));
|
||||
if (fridaFd_ == fd) {
|
||||
fridaFd_ = -1;
|
||||
}
|
||||
}
|
||||
|
||||
int FridaBackend::restoreFd() const
|
||||
{
|
||||
const int current = currentFd();
|
||||
if (current > 0 && current != fridaFd_) {
|
||||
return current;
|
||||
}
|
||||
if (hostFd_ > 0 && hostFd_ != fridaFd_) {
|
||||
return hostFd_;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void FridaBackend::runBatch(const QString &batch, const std::function<void(QString)> &done)
|
||||
{
|
||||
if (busy_) {
|
||||
if (done) {
|
||||
done(QString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
|
||||
QString script = QStringLiteral("e scr.color=0\n");
|
||||
script += batch;
|
||||
if (!script.endsWith(QLatin1Char('\n'))) {
|
||||
script += QLatin1Char('\n');
|
||||
}
|
||||
script += QStringLiteral("e scr.color=");
|
||||
script += savedColor;
|
||||
script += QLatin1Char('\n');
|
||||
|
||||
busy_ = true;
|
||||
task_ = QSharedPointer<R2Task>(new R2Task(script));
|
||||
connect(task_.data(), &R2Task::finished, this, [this, done, savedColor]() {
|
||||
const QString output = task_ ? task_->getResult() : QString();
|
||||
task_.clear();
|
||||
busy_ = false;
|
||||
Core()->setConfig(QStringLiteral("scr.color"), savedColor);
|
||||
if (done) {
|
||||
done(output);
|
||||
}
|
||||
});
|
||||
task_->startTask();
|
||||
}
|
||||
|
||||
QString FridaBackend::fridaCommand(const QString &command)
|
||||
{
|
||||
if (fridaFd_ < 0 || command.isEmpty()) {
|
||||
return QString();
|
||||
}
|
||||
const int back = restoreFd();
|
||||
QString batch = QStringLiteral("o=") + QString::number(fridaFd_) + QStringLiteral("\n:")
|
||||
+ command + QLatin1Char('\n');
|
||||
if (back > 0) {
|
||||
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
|
||||
}
|
||||
return Core()->cmd(batch);
|
||||
}
|
||||
|
||||
static QString stripAnsi(QString text)
|
||||
{
|
||||
static const QRegularExpression ansi(QStringLiteral("\x1b\\[[0-9;]*m"));
|
||||
text.replace(ansi, QString());
|
||||
text.replace(QChar(0x2502), QLatin1Char('|'));
|
||||
return text;
|
||||
}
|
||||
|
||||
static bool isSeparatorLine(const QString &line)
|
||||
{
|
||||
if (line.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
for (const QChar ch : line) {
|
||||
if (ch.isSpace() || ch == QLatin1Char('-') || ch == QLatin1Char('|')
|
||||
|| ch == QLatin1Char('+') || ch == QChar(0x2500) || ch == QChar(0x250c)
|
||||
|| ch == QChar(0x2510)
|
||||
|| ch == QChar(0x2514) || ch == QChar(0x2518) || ch == QChar(0x251c)
|
||||
|| ch == QChar(0x2524) || ch == QChar(0x252c) || ch == QChar(0x2534)
|
||||
|| ch == QChar(0x253c)) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static QStringList cellsOf(const QString &line)
|
||||
{
|
||||
QString trimmed = line.trimmed();
|
||||
if (trimmed.startsWith(QLatin1Char('|'))) {
|
||||
QStringList cells;
|
||||
const QStringList parts = trimmed.split(QLatin1Char('|'));
|
||||
for (QString part : parts) {
|
||||
part = part.trimmed();
|
||||
if (!part.isEmpty()) {
|
||||
cells.append(part);
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}
|
||||
return trimmed.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
}
|
||||
|
||||
QList<FridaDeviceInfo> FridaBackend::parseDevices(const QString &output)
|
||||
{
|
||||
QList<FridaDeviceInfo> devices;
|
||||
const QStringList lines = stripAnsi(output).split(QLatin1Char('\n'));
|
||||
for (const QString &raw : lines) {
|
||||
if (isSeparatorLine(raw.trimmed())) {
|
||||
continue;
|
||||
}
|
||||
const QStringList cells = cellsOf(raw);
|
||||
if (cells.size() < 3) {
|
||||
continue;
|
||||
}
|
||||
const QString type = cells.at(1).toLower();
|
||||
if (type != QStringLiteral("local") && type != QStringLiteral("usb")
|
||||
&& type != QStringLiteral("remote")) {
|
||||
continue;
|
||||
}
|
||||
FridaDeviceInfo device;
|
||||
device.id = cells.at(0);
|
||||
device.type = type;
|
||||
device.name = cells.mid(2).join(QLatin1Char(' '));
|
||||
devices.append(device);
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
QList<FridaTargetInfo> FridaBackend::parseTargets(const QString &output, FridaListKind kind)
|
||||
{
|
||||
QList<FridaTargetInfo> targets;
|
||||
const QStringList lines = stripAnsi(output).split(QLatin1Char('\n'));
|
||||
for (const QString &raw : lines) {
|
||||
if (isSeparatorLine(raw.trimmed())) {
|
||||
continue;
|
||||
}
|
||||
const QStringList cells = cellsOf(raw);
|
||||
if (cells.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
const QString pid = cells.at(0);
|
||||
const bool pidOk = pid == QStringLiteral("-")
|
||||
|| QRegularExpression(QStringLiteral("^\\d+$")).match(pid).hasMatch();
|
||||
if (!pidOk || pid.compare(QStringLiteral("PID"), Qt::CaseInsensitive) == 0) {
|
||||
continue;
|
||||
}
|
||||
FridaTargetInfo target;
|
||||
target.pid = pid;
|
||||
if (kind == FridaListKind::Applications && cells.size() >= 3) {
|
||||
target.identifier = cells.last();
|
||||
target.name = cells.mid(1, cells.size() - 2).join(QLatin1Char(' '));
|
||||
} else if (cells.size() >= 2) {
|
||||
target.name = cells.mid(1).join(QLatin1Char(' '));
|
||||
}
|
||||
targets.append(target);
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
QJsonDocument FridaBackend::parseJsonLoose(const QString &text)
|
||||
{
|
||||
const int arrayAt = text.indexOf(QLatin1Char('['));
|
||||
const int objectAt = text.indexOf(QLatin1Char('{'));
|
||||
int start = -1;
|
||||
if (arrayAt >= 0 && (objectAt < 0 || arrayAt < objectAt)) {
|
||||
start = arrayAt;
|
||||
} else {
|
||||
start = objectAt;
|
||||
}
|
||||
if (start < 0) {
|
||||
return QJsonDocument();
|
||||
}
|
||||
const QChar open = text.at(start);
|
||||
const QChar close = open == QLatin1Char('[') ? QLatin1Char(']') : QLatin1Char('}');
|
||||
const int end = text.lastIndexOf(close);
|
||||
if (end < start) {
|
||||
return QJsonDocument();
|
||||
}
|
||||
return QJsonDocument::fromJson(text.mid(start, end - start + 1).toUtf8());
|
||||
}
|
||||
|
||||
quint64 FridaBackend::parseAddress(const QJsonValue &value)
|
||||
{
|
||||
if (value.isString()) {
|
||||
QString text = value.toString().trimmed();
|
||||
bool ok = false;
|
||||
int base = 10;
|
||||
if (text.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) {
|
||||
text = text.mid(2);
|
||||
base = 16;
|
||||
}
|
||||
const quint64 parsed = text.toULongLong(&ok, base);
|
||||
return ok ? parsed : 0;
|
||||
}
|
||||
if (value.isDouble()) {
|
||||
return static_cast<quint64>(value.toDouble());
|
||||
}
|
||||
if (value.isObject()) {
|
||||
const QJsonObject object = value.toObject();
|
||||
for (const char *key : {"address", "value", "$handle", "base"}) {
|
||||
if (object.contains(QLatin1String(key))) {
|
||||
return parseAddress(object.value(QLatin1String(key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "FridaTypes.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonValue>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QSharedPointer>
|
||||
#include <functional>
|
||||
|
||||
class R2Task;
|
||||
|
||||
// Talks to io_frida through the already-open radare2 core.
|
||||
// The static file stays the current descriptor. Frida is opened with `of`
|
||||
// (no maps) and selected only for the duration of a `:` command.
|
||||
class FridaBackend : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FridaBackend(QObject *parent = nullptr);
|
||||
|
||||
bool pluginAvailable();
|
||||
bool busy() const { return busy_; }
|
||||
int currentFd() const;
|
||||
int hostFd() const { return hostFd_; }
|
||||
int fridaFd() const { return fridaFd_; }
|
||||
void setHostFd(int fd) { hostFd_ = fd; }
|
||||
void setFridaFd(int fd) { fridaFd_ = fd; }
|
||||
|
||||
QSet<int> openFds() const;
|
||||
int findNewFridaFd(const QSet<int> &before) const;
|
||||
void closeFd(int fd);
|
||||
|
||||
void runBatch(const QString &batch, const std::function<void(QString)> &done);
|
||||
QString fridaCommand(const QString &command);
|
||||
|
||||
static QList<FridaDeviceInfo> parseDevices(const QString &output);
|
||||
static QList<FridaTargetInfo> parseTargets(const QString &output, FridaListKind kind);
|
||||
static QJsonDocument parseJsonLoose(const QString &text);
|
||||
static quint64 parseAddress(const QJsonValue &value);
|
||||
|
||||
private:
|
||||
int restoreFd() const;
|
||||
|
||||
int hostFd_ = -1;
|
||||
int fridaFd_ = -1;
|
||||
bool busy_ = false;
|
||||
bool availabilityChecked_ = false;
|
||||
bool available_ = false;
|
||||
QSharedPointer<R2Task> task_;
|
||||
};
|
||||
@@ -0,0 +1,344 @@
|
||||
#include "FridaConnectDialog.h"
|
||||
|
||||
#include "FridaSession.h"
|
||||
#include "FridaUri.h"
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QComboBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QStyle>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
QPushButton *railButton(const QString &text, QStyle::StandardPixmap icon, QWidget *parent)
|
||||
{
|
||||
auto *button = new QPushButton(text, parent);
|
||||
button->setCheckable(true);
|
||||
button->setIcon(parent->style()->standardIcon(icon));
|
||||
button->setStyleSheet(QStringLiteral(
|
||||
"QPushButton { text-align: left; padding: 8px 14px; border: none; border-radius: 6px; }"
|
||||
"QPushButton:checked { background-color: palette(highlight);"
|
||||
" color: palette(highlighted-text); }"));
|
||||
return button;
|
||||
}
|
||||
|
||||
QPushButton *kindButton(const QString &text, QWidget *parent)
|
||||
{
|
||||
auto *button = new QPushButton(text, parent);
|
||||
button->setCheckable(true);
|
||||
button->setStyleSheet(QStringLiteral(
|
||||
"QPushButton { padding: 6px 12px; border: 1px solid palette(mid); border-radius: 4px; }"
|
||||
"QPushButton:checked { background-color: palette(highlight);"
|
||||
" color: palette(highlighted-text); }"));
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
FridaConnectDialog::FridaConnectDialog(FridaSession *session, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, session(session)
|
||||
{
|
||||
setWindowTitle(tr("Connect to r2frida"));
|
||||
setMinimumSize(640, 460);
|
||||
|
||||
auto *layout = new QHBoxLayout(this);
|
||||
auto *rail = new QVBoxLayout();
|
||||
transportGroup = new QButtonGroup(this);
|
||||
localButton = railButton(tr("Local"), QStyle::SP_ComputerIcon, this);
|
||||
usbButton = railButton(tr("USB"), QStyle::SP_DriveHDIcon, this);
|
||||
remoteButton = railButton(tr("Remote"), QStyle::SP_DriveNetIcon, this);
|
||||
localButton->setChecked(true);
|
||||
transportGroup->addButton(localButton, int(FridaTransport::Local));
|
||||
transportGroup->addButton(usbButton, int(FridaTransport::Usb));
|
||||
transportGroup->addButton(remoteButton, int(FridaTransport::Remote));
|
||||
rail->addWidget(localButton);
|
||||
rail->addWidget(usbButton);
|
||||
rail->addWidget(remoteButton);
|
||||
rail->addStretch();
|
||||
layout->addLayout(rail);
|
||||
|
||||
auto *mainColumn = new QVBoxLayout();
|
||||
mainColumn->addWidget(new QLabel(tr("Select a device"), this));
|
||||
auto *deviceRow = new QHBoxLayout();
|
||||
devices = new QComboBox(this);
|
||||
auto *refreshDevicesButton = new QPushButton(tr("Refresh"), this);
|
||||
deviceRow->addWidget(devices, 1);
|
||||
deviceRow->addWidget(refreshDevicesButton);
|
||||
mainColumn->addLayout(deviceRow);
|
||||
|
||||
remoteHost = new QLineEdit(this);
|
||||
remoteHost->setPlaceholderText(tr("10.0.0.3:9999"));
|
||||
remoteHost->setVisible(false);
|
||||
mainColumn->addWidget(remoteHost);
|
||||
|
||||
auto *kindRow = new QHBoxLayout();
|
||||
kindGroup = new QButtonGroup(this);
|
||||
appsButton = kindButton(tr("Applications"), this);
|
||||
processButton = kindButton(tr("Processes"), this);
|
||||
appsButton->setChecked(true);
|
||||
kindGroup->addButton(appsButton, int(FridaListKind::Applications));
|
||||
kindGroup->addButton(processButton, int(FridaListKind::Processes));
|
||||
kindRow->addWidget(appsButton);
|
||||
kindRow->addWidget(processButton);
|
||||
kindRow->addStretch();
|
||||
mainColumn->addLayout(kindRow);
|
||||
|
||||
search = new QLineEdit(this);
|
||||
search->setPlaceholderText(tr("Search"));
|
||||
mainColumn->addWidget(search);
|
||||
|
||||
targets = new QTreeWidget(this);
|
||||
targets->setHeaderLabels({tr("Name"), tr("Identifier"), tr("PID")});
|
||||
targets->setRootIsDecorated(false);
|
||||
targets->setAlternatingRowColors(true);
|
||||
targets->header()->setStretchLastSection(false);
|
||||
targets->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
targets->header()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
mainColumn->addWidget(targets, 1);
|
||||
|
||||
status = new QLabel(this);
|
||||
status->setWordWrap(true);
|
||||
mainColumn->addWidget(status);
|
||||
|
||||
auto *buttons = new QHBoxLayout();
|
||||
auto *attachButton = new QPushButton(tr("Attach"), this);
|
||||
auto *spawnButton = new QPushButton(tr("Spawn"), this);
|
||||
auto *launchButton = new QPushButton(tr("Launch"), this);
|
||||
auto *closeButton = new QPushButton(tr("Cancel"), this);
|
||||
attachButton->setDefault(true);
|
||||
buttons->addWidget(attachButton);
|
||||
buttons->addWidget(spawnButton);
|
||||
buttons->addWidget(launchButton);
|
||||
buttons->addStretch();
|
||||
buttons->addWidget(closeButton);
|
||||
mainColumn->addLayout(buttons);
|
||||
layout->addLayout(mainColumn, 1);
|
||||
|
||||
const auto transportChanged = [this](bool checked) {
|
||||
if (!checked) {
|
||||
return;
|
||||
}
|
||||
remoteHost->setVisible(transport() == FridaTransport::Remote);
|
||||
applyDevices(deviceCache);
|
||||
refreshTargets();
|
||||
};
|
||||
connect(localButton, &QPushButton::toggled, this, transportChanged);
|
||||
connect(usbButton, &QPushButton::toggled, this, transportChanged);
|
||||
connect(remoteButton, &QPushButton::toggled, this, transportChanged);
|
||||
connect(appsButton, &QPushButton::toggled, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
refreshTargets();
|
||||
}
|
||||
});
|
||||
connect(processButton, &QPushButton::toggled, this, [this](bool checked) {
|
||||
if (checked) {
|
||||
refreshTargets();
|
||||
}
|
||||
});
|
||||
connect(
|
||||
devices,
|
||||
qOverload<int>(&QComboBox::currentIndexChanged),
|
||||
this,
|
||||
[this](int) { refreshTargets(); });
|
||||
connect(refreshDevicesButton, &QPushButton::clicked, this, &FridaConnectDialog::refreshDevices);
|
||||
connect(search, &QLineEdit::textChanged, this, &FridaConnectDialog::filterTargets);
|
||||
connect(attachButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Attach); });
|
||||
connect(spawnButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Spawn); });
|
||||
connect(launchButton, &QPushButton::clicked, this, [this]() { choose(FridaAction::Launch); });
|
||||
connect(closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(targets, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem *, int) {
|
||||
const FridaTargetInfo target = selectedTarget();
|
||||
const bool running = target.pid != QStringLiteral("-") && !target.pid.isEmpty();
|
||||
choose(running ? FridaAction::Attach : FridaAction::Spawn);
|
||||
});
|
||||
|
||||
if (!session->checkAvailable()) {
|
||||
status->setText(
|
||||
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
|
||||
attachButton->setEnabled(false);
|
||||
spawnButton->setEnabled(false);
|
||||
launchButton->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
refreshDevices();
|
||||
}
|
||||
|
||||
FridaTransport FridaConnectDialog::transport() const
|
||||
{
|
||||
return static_cast<FridaTransport>(transportGroup->checkedId());
|
||||
}
|
||||
|
||||
FridaListKind FridaConnectDialog::listKind() const
|
||||
{
|
||||
return static_cast<FridaListKind>(kindGroup->checkedId());
|
||||
}
|
||||
|
||||
QString FridaConnectDialog::deviceId() const
|
||||
{
|
||||
if (transport() == FridaTransport::Remote) {
|
||||
const QString host = remoteHost->text().trimmed();
|
||||
if (!host.isEmpty()) {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
return devices->currentData().toString();
|
||||
}
|
||||
|
||||
QString FridaConnectDialog::deviceLabel() const
|
||||
{
|
||||
if (transport() == FridaTransport::Remote && !remoteHost->text().trimmed().isEmpty()) {
|
||||
return remoteHost->text().trimmed();
|
||||
}
|
||||
const QString text = devices->currentText();
|
||||
return text.isEmpty() ? deviceId() : text;
|
||||
}
|
||||
|
||||
FridaTargetInfo FridaConnectDialog::selectedTarget() const
|
||||
{
|
||||
FridaTargetInfo target;
|
||||
QTreeWidgetItem *item = targets->currentItem();
|
||||
if (!item) {
|
||||
return target;
|
||||
}
|
||||
target.name = item->text(0);
|
||||
target.identifier = item->text(1);
|
||||
target.pid = item->text(2);
|
||||
return target;
|
||||
}
|
||||
|
||||
void FridaConnectDialog::refreshDevices()
|
||||
{
|
||||
if (!session->checkAvailable()) {
|
||||
return;
|
||||
}
|
||||
status->setText(tr("Listing devices..."));
|
||||
const int generation = ++probeGeneration;
|
||||
session->probe(FridaUri::listDevices(), [this, generation](const QString &output) {
|
||||
if (generation != probeGeneration) {
|
||||
return;
|
||||
}
|
||||
deviceCache = FridaBackend::parseDevices(output);
|
||||
applyDevices(deviceCache);
|
||||
if (deviceCache.isEmpty()) {
|
||||
status->setText(
|
||||
output.trimmed().isEmpty() ? tr("No devices found.") : output.trimmed());
|
||||
}
|
||||
refreshTargets();
|
||||
});
|
||||
}
|
||||
|
||||
void FridaConnectDialog::applyDevices(const QList<FridaDeviceInfo> &devices)
|
||||
{
|
||||
const QString previous = deviceId();
|
||||
this->devices->blockSignals(true);
|
||||
this->devices->clear();
|
||||
const QString want = transport() == FridaTransport::Local
|
||||
? QStringLiteral("local")
|
||||
: (transport() == FridaTransport::Usb ? QStringLiteral("usb")
|
||||
: QStringLiteral("remote"));
|
||||
for (const FridaDeviceInfo &device : devices) {
|
||||
if (device.type != want) {
|
||||
continue;
|
||||
}
|
||||
const QString label
|
||||
= device.name.isEmpty()
|
||||
? device.id
|
||||
: device.name + QStringLiteral(" (") + device.id + QLatin1Char(')');
|
||||
this->devices->addItem(label, device.id);
|
||||
}
|
||||
const int index = this->devices->findData(previous);
|
||||
if (index >= 0) {
|
||||
this->devices->setCurrentIndex(index);
|
||||
}
|
||||
this->devices->blockSignals(false);
|
||||
}
|
||||
|
||||
void FridaConnectDialog::refreshTargets()
|
||||
{
|
||||
if (!session->checkAvailable() || session->busy()) {
|
||||
return;
|
||||
}
|
||||
status->setText(tr("Listing targets..."));
|
||||
targets->clear();
|
||||
const int generation = ++probeGeneration;
|
||||
const QString uri = FridaUri::listTargets(transport(), listKind(), deviceId());
|
||||
session->probe(uri, [this, generation](const QString &output) {
|
||||
if (generation != probeGeneration) {
|
||||
return;
|
||||
}
|
||||
const QList<FridaTargetInfo> parsed = FridaBackend::parseTargets(output, listKind());
|
||||
applyTargets(parsed);
|
||||
if (parsed.isEmpty()) {
|
||||
status->setText(
|
||||
output.trimmed().isEmpty() ? tr("No targets found.") : output.trimmed());
|
||||
} else {
|
||||
status->setText(tr("%1 targets").arg(parsed.size()));
|
||||
}
|
||||
filterTargets(search->text());
|
||||
});
|
||||
}
|
||||
|
||||
void FridaConnectDialog::applyTargets(const QList<FridaTargetInfo> &targets)
|
||||
{
|
||||
this->targets->clear();
|
||||
for (const FridaTargetInfo &target : targets) {
|
||||
auto *item = new QTreeWidgetItem(this->targets);
|
||||
item->setText(0, target.name);
|
||||
item->setText(1, target.identifier);
|
||||
item->setText(2, target.pid);
|
||||
}
|
||||
}
|
||||
|
||||
void FridaConnectDialog::filterTargets(const QString &text)
|
||||
{
|
||||
const QString needle = text.trimmed();
|
||||
for (int row = 0; row < targets->topLevelItemCount(); ++row) {
|
||||
QTreeWidgetItem *item = targets->topLevelItem(row);
|
||||
const bool match = needle.isEmpty() || item->text(0).contains(needle, Qt::CaseInsensitive)
|
||||
|| item->text(1).contains(needle, Qt::CaseInsensitive)
|
||||
|| item->text(2).contains(needle, Qt::CaseInsensitive);
|
||||
item->setHidden(!match);
|
||||
}
|
||||
}
|
||||
|
||||
void FridaConnectDialog::choose(FridaAction action)
|
||||
{
|
||||
const FridaTargetInfo target = selectedTarget();
|
||||
QString id;
|
||||
if (action == FridaAction::Attach) {
|
||||
bool pidOk = false;
|
||||
const int pid = target.pid.toInt(&pidOk);
|
||||
if (pidOk && pid > 0) {
|
||||
id = target.pid;
|
||||
}
|
||||
}
|
||||
if (id.isEmpty()) {
|
||||
id = !target.identifier.isEmpty() ? target.identifier : target.name;
|
||||
}
|
||||
if (!FridaUri::targetIsSafe(id)) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
tr("Connect to target"),
|
||||
tr("Select an application or process."));
|
||||
return;
|
||||
}
|
||||
if (transport() == FridaTransport::Remote && deviceId().isEmpty()) {
|
||||
QMessageBox::warning(this, tr("Connect to target"), tr("Enter a remote host:port."));
|
||||
return;
|
||||
}
|
||||
const QString uri = FridaUri::target(transport(), action, deviceId(), id);
|
||||
QString transportName = tr("local");
|
||||
if (transport() == FridaTransport::Usb) {
|
||||
transportName = tr("usb");
|
||||
} else if (transport() == FridaTransport::Remote) {
|
||||
transportName = tr("remote");
|
||||
}
|
||||
session->openTarget(uri, action == FridaAction::Spawn, deviceLabel(), transportName);
|
||||
accept();
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "FridaTypes.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
class FridaSession;
|
||||
class QButtonGroup;
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QLabel;
|
||||
class QTreeWidget;
|
||||
class QPushButton;
|
||||
|
||||
class FridaConnectDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FridaConnectDialog(FridaSession *session, QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
void refreshDevices();
|
||||
void refreshTargets();
|
||||
void applyDevices(const QList<FridaDeviceInfo> &devices);
|
||||
void applyTargets(const QList<FridaTargetInfo> &targets);
|
||||
void filterTargets(const QString &text);
|
||||
void choose(FridaAction action);
|
||||
FridaTransport transport() const;
|
||||
FridaListKind listKind() const;
|
||||
QString deviceId() const;
|
||||
QString deviceLabel() const;
|
||||
FridaTargetInfo selectedTarget() const;
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QButtonGroup *transportGroup = nullptr;
|
||||
QButtonGroup *kindGroup = nullptr;
|
||||
QPushButton *localButton = nullptr;
|
||||
QPushButton *usbButton = nullptr;
|
||||
QPushButton *remoteButton = nullptr;
|
||||
QPushButton *appsButton = nullptr;
|
||||
QPushButton *processButton = nullptr;
|
||||
QLineEdit *remoteHost = nullptr;
|
||||
QComboBox *devices = nullptr;
|
||||
QLineEdit *search = nullptr;
|
||||
QTreeWidget *targets = nullptr;
|
||||
QLabel *status = nullptr;
|
||||
QList<FridaDeviceInfo> deviceCache;
|
||||
int probeGeneration = 0;
|
||||
};
|
||||
@@ -0,0 +1,511 @@
|
||||
#include "FridaPanels.h"
|
||||
|
||||
#include "FridaSession.h"
|
||||
#include "FridaWidget.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QClipboard>
|
||||
#include <QComboBox>
|
||||
#include <QFileInfo>
|
||||
#include <QHeaderView>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
#include <QTreeView>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
QString formatSize(quint64 size)
|
||||
{
|
||||
if (size >= (1024ull * 1024ull * 1024ull)) {
|
||||
return QString::number(size / (1024.0 * 1024.0 * 1024.0), 'f', 1) + QStringLiteral(" GB");
|
||||
}
|
||||
if (size >= (1024ull * 1024ull)) {
|
||||
return QString::number(size / (1024.0 * 1024.0), 'f', 1) + QStringLiteral(" MB");
|
||||
}
|
||||
if (size >= 1024ull) {
|
||||
return QString::number(size / 1024.0, 'f', 1) + QStringLiteral(" KB");
|
||||
}
|
||||
return QString::number(size) + QStringLiteral(" B");
|
||||
}
|
||||
|
||||
QStandardItem *textItem(const QString &text)
|
||||
{
|
||||
auto *item = new QStandardItem(text);
|
||||
item->setEditable(false);
|
||||
return item;
|
||||
}
|
||||
|
||||
QStandardItem *addressItem(const QString &text, quint64 runtime)
|
||||
{
|
||||
auto *item = textItem(text);
|
||||
item->setData(QVariant::fromValue(runtime), Qt::UserRole);
|
||||
return item;
|
||||
}
|
||||
|
||||
quint64 roleAddress(const QModelIndex &index)
|
||||
{
|
||||
return index.data(Qt::UserRole).toULongLong();
|
||||
}
|
||||
|
||||
void host(QWidget *page, QWidget *child)
|
||||
{
|
||||
auto *layout = new QVBoxLayout(page);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->addWidget(child);
|
||||
}
|
||||
|
||||
void loadJava(FridaSession *session, QPlainTextEdit *view)
|
||||
{
|
||||
if (!session->isAttached()) {
|
||||
view->setPlainText(QObject::tr("Not attached."));
|
||||
return;
|
||||
}
|
||||
view->setPlainText(QObject::tr("Loading..."));
|
||||
const QString source = QStringLiteral(
|
||||
"if (!Java.available) { console.log('Java runtime is not available'); }\n"
|
||||
"else Java.perform(function () {\n"
|
||||
" var n = 0;\n"
|
||||
" Java.enumerateLoadedClasses({\n"
|
||||
" onMatch: function (name) { if (n++ < 2000) console.log(name); },\n"
|
||||
" onComplete: function () { if (n >= 2000) console.log('... truncated'); }\n"
|
||||
" });\n"
|
||||
"});\n");
|
||||
const QString path = session->writeScript(source);
|
||||
if (path.isEmpty()) {
|
||||
view->setPlainText(QObject::tr("Could not write the Java script."));
|
||||
return;
|
||||
}
|
||||
session->command(QStringLiteral(". ") + path, [view](const QString &output) {
|
||||
view->setPlainText(output);
|
||||
});
|
||||
}
|
||||
|
||||
void showCommand(FridaSession *session, const QString &command, QPlainTextEdit *view)
|
||||
{
|
||||
if (!session->isAttached()) {
|
||||
view->setPlainText(QObject::tr("Not attached."));
|
||||
return;
|
||||
}
|
||||
view->setPlainText(QObject::tr("Loading..."));
|
||||
session->command(command, [view](const QString &output) {
|
||||
QString text = output;
|
||||
if (text.size() > 1000000) {
|
||||
text.truncate(1000000);
|
||||
text += QObject::tr("\n... truncated");
|
||||
}
|
||||
view->setPlainText(text);
|
||||
});
|
||||
}
|
||||
|
||||
class FridaOutputDock : public QWidget
|
||||
{
|
||||
public:
|
||||
FridaOutputDock(
|
||||
const QString &title,
|
||||
const QString &objectName,
|
||||
FridaSession *fridaSession,
|
||||
const std::function<void(QPlainTextEdit *)> &load)
|
||||
: QWidget(nullptr)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setObjectName(objectName);
|
||||
setWindowTitle(title);
|
||||
auto *root = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(root);
|
||||
auto *refresh = new QPushButton(tr("Refresh"), root);
|
||||
view = new QPlainTextEdit(root);
|
||||
view->setReadOnly(true);
|
||||
layout->addWidget(refresh);
|
||||
layout->addWidget(view);
|
||||
host(this, root);
|
||||
connect(refresh, &QPushButton::clicked, this, [this, load]() { load(view); });
|
||||
connect(session, &FridaSession::changed, this, [this]() {
|
||||
if (!session->isAttached()) {
|
||||
view->clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
FridaSession *session = nullptr;
|
||||
QPlainTextEdit *view = nullptr;
|
||||
};
|
||||
|
||||
class FridaModulesDock : public QWidget
|
||||
{
|
||||
public:
|
||||
FridaModulesDock(FridaSession *fridaSession)
|
||||
: QWidget(nullptr)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setObjectName(QStringLiteral("FridaModulesDock"));
|
||||
setWindowTitle(tr("Modules"));
|
||||
model = new QStandardItemModel(this);
|
||||
model->setHorizontalHeaderLabels({tr("Name"), tr("Base"), tr("Size"), tr("Path")});
|
||||
proxy = new QSortFilterProxyModel(this);
|
||||
proxy->setSourceModel(model);
|
||||
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy->setFilterKeyColumn(-1);
|
||||
|
||||
auto *root = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(root);
|
||||
auto *row = new QHBoxLayout();
|
||||
auto *filter = new QLineEdit(root);
|
||||
filter->setPlaceholderText(tr("Filter"));
|
||||
auto *mode = new QComboBox(root);
|
||||
mode->addItem(tr("Runtime"), int(FridaAddressMode::Runtime));
|
||||
mode->addItem(tr("Static"), int(FridaAddressMode::Static));
|
||||
mode->addItem(tr("Module + offset"), int(FridaAddressMode::ModuleOffset));
|
||||
row->addWidget(filter, 1);
|
||||
row->addWidget(mode);
|
||||
view = new QTreeView(root);
|
||||
view->setModel(proxy);
|
||||
view->setRootIsDecorated(false);
|
||||
view->setAlternatingRowColors(true);
|
||||
view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
view->header()->setStretchLastSection(true);
|
||||
view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
layout->addLayout(row);
|
||||
layout->addWidget(view);
|
||||
host(this, root);
|
||||
|
||||
connect(
|
||||
filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);
|
||||
connect(mode, qOverload<int>(&QComboBox::currentIndexChanged), this, [this, mode](int index) {
|
||||
session->setAddressMode(static_cast<FridaAddressMode>(mode->itemData(index).toInt()));
|
||||
});
|
||||
connect(session, &FridaSession::changed, this, [this]() { refill(); });
|
||||
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
|
||||
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
|
||||
});
|
||||
connect(view, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) {
|
||||
const QModelIndex index = view->indexAt(pos);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
const QModelIndex nameIndex = index.sibling(index.row(), 0);
|
||||
const quint64 runtime = roleAddress(nameIndex);
|
||||
const QString name = nameIndex.data().toString();
|
||||
QMenu menu(this);
|
||||
menu.addAction(tr("Seek static address"), this, [this, runtime]() {
|
||||
fridaSeekRuntime(session, runtime);
|
||||
});
|
||||
menu.addAction(tr("Copy runtime address"), this, [runtime]() {
|
||||
QApplication::clipboard()->setText(RAddressString(runtime));
|
||||
});
|
||||
menu.addAction(tr("Live hexdump"), this, [this, runtime]() {
|
||||
fridaShowLiveHex(this, session, runtime);
|
||||
});
|
||||
menu.addAction(tr("Show exports"), this, [this, name]() {
|
||||
fridaShowExports(this, session, name);
|
||||
});
|
||||
menu.addAction(tr("Import symbols into analysis"), this, [this, name]() {
|
||||
session->importExports(name);
|
||||
});
|
||||
menu.exec(view->viewport()->mapToGlobal(pos));
|
||||
});
|
||||
refill();
|
||||
}
|
||||
|
||||
private:
|
||||
void refill()
|
||||
{
|
||||
model->removeRows(0, model->rowCount());
|
||||
for (const FridaModuleInfo &module : session->modules()) {
|
||||
auto *name = textItem(module.name);
|
||||
name->setData(QVariant::fromValue(module.base), Qt::UserRole);
|
||||
name->setToolTip(session->describe(module.base));
|
||||
model->appendRow(
|
||||
{name,
|
||||
addressItem(session->formatRuntime(module.base), module.base),
|
||||
textItem(formatSize(module.size)),
|
||||
textItem(module.path)});
|
||||
}
|
||||
}
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QStandardItemModel *model = nullptr;
|
||||
QSortFilterProxyModel *proxy = nullptr;
|
||||
QTreeView *view = nullptr;
|
||||
};
|
||||
|
||||
class FridaMapsDock : public QWidget
|
||||
{
|
||||
public:
|
||||
FridaMapsDock(FridaSession *fridaSession)
|
||||
: QWidget(nullptr)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setObjectName(QStringLiteral("FridaMemoryMapDock"));
|
||||
setWindowTitle(tr("Memory Map"));
|
||||
model = new QStandardItemModel(this);
|
||||
model->setHorizontalHeaderLabels({tr("Start"), tr("End"), tr("Perm"), tr("Module")});
|
||||
proxy = new QSortFilterProxyModel(this);
|
||||
proxy->setSourceModel(model);
|
||||
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
proxy->setFilterKeyColumn(-1);
|
||||
|
||||
auto *root = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(root);
|
||||
auto *filter = new QLineEdit(root);
|
||||
filter->setPlaceholderText(tr("Filter"));
|
||||
view = new QTreeView(root);
|
||||
view->setModel(proxy);
|
||||
view->setRootIsDecorated(false);
|
||||
view->setAlternatingRowColors(true);
|
||||
view->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
view->header()->setStretchLastSection(true);
|
||||
layout->addWidget(filter);
|
||||
layout->addWidget(view);
|
||||
host(this, root);
|
||||
|
||||
connect(
|
||||
filter, &QLineEdit::textChanged, proxy, &QSortFilterProxyModel::setFilterFixedString);
|
||||
connect(session, &FridaSession::changed, this, [this]() { refill(); });
|
||||
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
|
||||
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
|
||||
});
|
||||
connect(view, &QTreeView::customContextMenuRequested, this, [this](const QPoint &pos) {
|
||||
const QModelIndex index = view->indexAt(pos);
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
const quint64 runtime = roleAddress(index.sibling(index.row(), 0));
|
||||
QMenu menu(this);
|
||||
menu.addAction(tr("Seek static address"), this, [this, runtime]() {
|
||||
fridaSeekRuntime(session, runtime);
|
||||
});
|
||||
menu.addAction(tr("Live hexdump"), this, [this, runtime]() {
|
||||
fridaShowLiveHex(this, session, runtime);
|
||||
});
|
||||
menu.addAction(tr("Copy address"), this, [runtime]() {
|
||||
QApplication::clipboard()->setText(RAddressString(runtime));
|
||||
});
|
||||
menu.exec(view->viewport()->mapToGlobal(pos));
|
||||
});
|
||||
refill();
|
||||
}
|
||||
|
||||
private:
|
||||
void refill()
|
||||
{
|
||||
model->removeRows(0, model->rowCount());
|
||||
for (const FridaMapInfo &map : session->maps()) {
|
||||
const QString module = QFileInfo(map.path).fileName();
|
||||
auto *start = addressItem(session->formatRuntime(map.start), map.start);
|
||||
start->setToolTip(session->describe(map.start));
|
||||
auto *name = textItem(module.isEmpty() ? map.path : module);
|
||||
name->setToolTip(map.path);
|
||||
model->appendRow(
|
||||
{start,
|
||||
addressItem(session->formatRuntime(map.end), map.end),
|
||||
textItem(map.permission),
|
||||
name});
|
||||
}
|
||||
}
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QStandardItemModel *model = nullptr;
|
||||
QSortFilterProxyModel *proxy = nullptr;
|
||||
QTreeView *view = nullptr;
|
||||
};
|
||||
|
||||
class FridaHooksDock : public QWidget
|
||||
{
|
||||
public:
|
||||
FridaHooksDock(FridaSession *fridaSession)
|
||||
: QWidget(nullptr)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setObjectName(QStringLiteral("FridaHooksDock"));
|
||||
setWindowTitle(tr("Hooks"));
|
||||
model = new QStandardItemModel(this);
|
||||
model->setHorizontalHeaderLabels({tr("Address"), tr("Summary")});
|
||||
view = new QTreeView(this);
|
||||
view->setModel(model);
|
||||
view->setRootIsDecorated(false);
|
||||
view->setAlternatingRowColors(true);
|
||||
view->header()->setStretchLastSection(true);
|
||||
host(this, view);
|
||||
connect(session, &FridaSession::changed, this, [this]() { refill(); });
|
||||
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
|
||||
fridaSeekRuntime(session, roleAddress(index.sibling(index.row(), 0)));
|
||||
});
|
||||
refill();
|
||||
}
|
||||
|
||||
private:
|
||||
void refill()
|
||||
{
|
||||
model->removeRows(0, model->rowCount());
|
||||
for (const FridaHookInfo &hook : session->hooks()) {
|
||||
model->appendRow(
|
||||
{addressItem(session->formatRuntime(hook.address), hook.address),
|
||||
textItem(hook.summary)});
|
||||
}
|
||||
}
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QStandardItemModel *model = nullptr;
|
||||
QTreeView *view = nullptr;
|
||||
};
|
||||
|
||||
class FridaTracesDock : public QWidget
|
||||
{
|
||||
public:
|
||||
FridaTracesDock(FridaSession *fridaSession)
|
||||
: QWidget(nullptr)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setObjectName(QStringLiteral("FridaTracesDock"));
|
||||
setWindowTitle(tr("Traces"));
|
||||
model = new QStandardItemModel(this);
|
||||
model->setHorizontalHeaderLabels({tr("Function"), tr("Hits")});
|
||||
view = new QTreeView(this);
|
||||
view->setModel(model);
|
||||
view->setRootIsDecorated(false);
|
||||
view->setAlternatingRowColors(true);
|
||||
view->header()->setStretchLastSection(true);
|
||||
view->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
auto *root = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(root);
|
||||
auto *refresh = new QPushButton(tr("Refresh"), root);
|
||||
layout->addWidget(refresh);
|
||||
layout->addWidget(view);
|
||||
host(this, root);
|
||||
connect(refresh, &QPushButton::clicked, this, [this]() { reload(); });
|
||||
connect(session, &FridaSession::changed, this, [this]() {
|
||||
if (!session->isAttached()) {
|
||||
model->removeRows(0, model->rowCount());
|
||||
}
|
||||
});
|
||||
connect(view, &QTreeView::activated, this, [this](const QModelIndex &index) {
|
||||
const QString line = index.sibling(index.row(), 0).data().toString();
|
||||
static const QRegularExpression addressRe(QStringLiteral("0x[0-9a-fA-F]+"));
|
||||
const QRegularExpressionMatch match = addressRe.match(line);
|
||||
if (!match.hasMatch()) {
|
||||
return;
|
||||
}
|
||||
bool ok = false;
|
||||
const quint64 addr = match.captured(0).toULongLong(&ok, 16);
|
||||
if (ok) {
|
||||
fridaSeekRuntime(session, addr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
void reload()
|
||||
{
|
||||
if (!session->isAttached()) {
|
||||
model->removeRows(0, model->rowCount());
|
||||
return;
|
||||
}
|
||||
session->command(QStringLiteral("dt"), [this](const QString &output) {
|
||||
model->removeRows(0, model->rowCount());
|
||||
for (const QString &raw : output.split(QLatin1Char('\n'))) {
|
||||
const QString line = raw.trimmed();
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
QString function = line;
|
||||
QString hits;
|
||||
const int space = line.lastIndexOf(QLatin1Char(' '));
|
||||
if (space > 0) {
|
||||
bool ok = false;
|
||||
line.mid(space + 1).toULongLong(&ok);
|
||||
if (ok) {
|
||||
function = line.left(space).trimmed();
|
||||
hits = line.mid(space + 1);
|
||||
}
|
||||
}
|
||||
model->appendRow({textItem(function), textItem(hits)});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QStandardItemModel *model = nullptr;
|
||||
QTreeView *view = nullptr;
|
||||
};
|
||||
|
||||
class FridaRuntimePage : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit FridaRuntimePage(FridaSession *fridaSession)
|
||||
: QWidget(nullptr)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setWindowTitle(tr("ObjC/Java Runtime"));
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
auto *row = new QHBoxLayout();
|
||||
kind = new QComboBox(this);
|
||||
kind->addItem(tr("Objective-C"));
|
||||
kind->addItem(tr("Java"));
|
||||
auto *refresh = new QPushButton(tr("Refresh"), this);
|
||||
view = new QPlainTextEdit(this);
|
||||
view->setReadOnly(true);
|
||||
row->addWidget(kind);
|
||||
row->addWidget(refresh);
|
||||
row->addStretch();
|
||||
layout->addLayout(row);
|
||||
layout->addWidget(view);
|
||||
connect(refresh, &QPushButton::clicked, this, [this]() { reload(); });
|
||||
connect(session, &FridaSession::changed, this, [this]() {
|
||||
if (!session->isAttached()) {
|
||||
view->clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
void reload()
|
||||
{
|
||||
if (kind->currentIndex() == 0) {
|
||||
showCommand(session, QStringLiteral("ic"), view);
|
||||
} else {
|
||||
loadJava(session, view);
|
||||
}
|
||||
}
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QComboBox *kind = nullptr;
|
||||
QPlainTextEdit *view = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
QList<QWidget *> createFridaPanels(FridaSession *session)
|
||||
{
|
||||
const auto command = [session](const QString &name) {
|
||||
return [session, name](QPlainTextEdit *view) { showCommand(session, name, view); };
|
||||
};
|
||||
QList<QWidget *> panels;
|
||||
panels << new FridaModulesDock(session);
|
||||
panels << new FridaOutputDock(
|
||||
QObject::tr("Threads"),
|
||||
QStringLiteral("FridaThreadsPage"),
|
||||
session,
|
||||
command(QStringLiteral("dpt")));
|
||||
panels << new FridaMapsDock(session);
|
||||
panels << new FridaOutputDock(
|
||||
QObject::tr("Breakpoints"),
|
||||
QStringLiteral("FridaBreakpointsPage"),
|
||||
session,
|
||||
command(QStringLiteral("db")));
|
||||
panels << new FridaRuntimePage(session);
|
||||
panels << new FridaHooksDock(session);
|
||||
panels << new FridaTracesDock(session);
|
||||
return panels;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <QList>
|
||||
|
||||
class FridaSession;
|
||||
class QWidget;
|
||||
|
||||
QList<QWidget *> createFridaPanels(FridaSession *session);
|
||||
@@ -0,0 +1,501 @@
|
||||
#include "FridaSession.h"
|
||||
|
||||
#include "FridaUri.h"
|
||||
#include "core/Iaito.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
FridaSession::FridaSession(QObject *parent)
|
||||
: QObject(parent)
|
||||
{}
|
||||
|
||||
bool FridaSession::checkAvailable()
|
||||
{
|
||||
available_ = backend_.pluginAvailable();
|
||||
return available_;
|
||||
}
|
||||
|
||||
void FridaSession::setAddressMode(FridaAddressMode mode)
|
||||
{
|
||||
if (addressMode_ == mode) {
|
||||
return;
|
||||
}
|
||||
addressMode_ = mode;
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void FridaSession::setState(FridaSessionState state)
|
||||
{
|
||||
state_ = state;
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void FridaSession::clearTarget()
|
||||
{
|
||||
suspended_ = false;
|
||||
process_ = FridaProcessInfo();
|
||||
modules_.clear();
|
||||
maps_.clear();
|
||||
hooks_.clear();
|
||||
staticBase_ = 0;
|
||||
runtimeBase_ = 0;
|
||||
matchedSize_ = 0;
|
||||
slide_ = 0;
|
||||
uri_.clear();
|
||||
deviceLabel_.clear();
|
||||
transportLabel_.clear();
|
||||
}
|
||||
|
||||
bool FridaSession::containsStatic(quint64 addr) const
|
||||
{
|
||||
return matchedSize_ > 0 && addr >= staticBase_ && addr - staticBase_ < matchedSize_;
|
||||
}
|
||||
|
||||
bool FridaSession::containsRuntime(quint64 addr) const
|
||||
{
|
||||
return matchedSize_ > 0 && addr >= runtimeBase_ && addr - runtimeBase_ < matchedSize_;
|
||||
}
|
||||
|
||||
quint64 FridaSession::toRuntime(quint64 staticVa) const
|
||||
{
|
||||
if (!containsStatic(staticVa)) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
return runtimeBase_ + (staticVa - staticBase_);
|
||||
}
|
||||
|
||||
quint64 FridaSession::toStatic(quint64 runtimeVa) const
|
||||
{
|
||||
if (!containsRuntime(runtimeVa)) {
|
||||
return RVA_INVALID;
|
||||
}
|
||||
return staticBase_ + (runtimeVa - runtimeBase_);
|
||||
}
|
||||
|
||||
QString FridaSession::formatRuntime(quint64 runtimeVa) const
|
||||
{
|
||||
if (addressMode_ == FridaAddressMode::ModuleOffset) {
|
||||
for (const FridaModuleInfo &module : modules_) {
|
||||
if (module.size > 0 && runtimeVa >= module.base
|
||||
&& runtimeVa - module.base < module.size) {
|
||||
return module.name + QStringLiteral(" + ")
|
||||
+ RAddressString(runtimeVa - module.base);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addressMode_ == FridaAddressMode::Static) {
|
||||
const quint64 staticVa = toStatic(runtimeVa);
|
||||
if (staticVa != RVA_INVALID) {
|
||||
return RAddressString(staticVa);
|
||||
}
|
||||
}
|
||||
return RAddressString(runtimeVa);
|
||||
}
|
||||
|
||||
QString FridaSession::describe(quint64 addr) const
|
||||
{
|
||||
quint64 staticVa = addr;
|
||||
quint64 runtimeVa = addr;
|
||||
if (containsStatic(addr)) {
|
||||
runtimeVa = toRuntime(addr);
|
||||
} else if (containsRuntime(addr)) {
|
||||
staticVa = toStatic(addr);
|
||||
} else {
|
||||
return tr("No static mapping for %1").arg(RAddressString(addr));
|
||||
}
|
||||
QString moduleText = RAddressString(runtimeVa);
|
||||
for (const FridaModuleInfo &module : modules_) {
|
||||
if (module.size > 0 && runtimeVa >= module.base && runtimeVa - module.base < module.size) {
|
||||
moduleText = module.name + QStringLiteral(" + ")
|
||||
+ RAddressString(runtimeVa - module.base);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tr("static %1\nruntime %2\n%3\nslide %4")
|
||||
.arg(
|
||||
RAddressString(staticVa),
|
||||
RAddressString(runtimeVa),
|
||||
moduleText,
|
||||
RAddressString(slide_));
|
||||
}
|
||||
|
||||
void FridaSession::probe(const QString &uri, const std::function<void(QString)> &done)
|
||||
{
|
||||
if (!checkAvailable()) {
|
||||
if (done) {
|
||||
done(tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (backend_.busy()) {
|
||||
if (done) {
|
||||
done(tr("r2frida is busy."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const QSet<int> before = backend_.openFds();
|
||||
// r2frida prints the table with r_cons_gprintf, which follows the
|
||||
// thread-local console. A background task drains a child console, so the
|
||||
// table never comes back. Run here, on the console cmd() captures.
|
||||
// `of` still performs the plugin open, and stays quiet when it returns
|
||||
// no descriptor. `o` would log "Cannot open file" for that.
|
||||
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
|
||||
const QString batch = QStringLiteral("e scr.color=0\nof ") + FridaUri::quote(uri)
|
||||
+ QStringLiteral("\ne scr.color=") + savedColor + QLatin1Char('\n');
|
||||
RCons *previous = r_cons_singleton();
|
||||
RCons *cons = r_core_get_cons(Core()->core_);
|
||||
if (cons) {
|
||||
r_cons_global(cons);
|
||||
}
|
||||
const QString output = Core()->cmd(batch);
|
||||
if (previous && previous != cons) {
|
||||
r_cons_global(previous);
|
||||
}
|
||||
const int leaked = backend_.findNewFridaFd(before);
|
||||
backend_.closeFd(leaked);
|
||||
QStringList kept;
|
||||
for (const QString &line : output.split(QLatin1Char('\n'))) {
|
||||
if (line.contains(QStringLiteral("Cannot open"))) {
|
||||
continue;
|
||||
}
|
||||
kept.append(line);
|
||||
}
|
||||
if (done) {
|
||||
done(kept.join(QLatin1Char('\n')));
|
||||
}
|
||||
}
|
||||
|
||||
void FridaSession::openTarget(
|
||||
const QString &uri, bool suspended, const QString &deviceLabel, const QString &transportLabel)
|
||||
{
|
||||
if (!checkAvailable()) {
|
||||
lastError_ = tr(
|
||||
"r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito.");
|
||||
setState(FridaSessionState::Error);
|
||||
emit errorMessage(lastError_);
|
||||
return;
|
||||
}
|
||||
if (backend_.busy()) {
|
||||
emit errorMessage(tr("r2frida is busy."));
|
||||
return;
|
||||
}
|
||||
if (backend_.fridaFd() >= 0) {
|
||||
detach();
|
||||
}
|
||||
|
||||
const int generation = ++generation_;
|
||||
const QSet<int> before = backend_.openFds();
|
||||
backend_.setHostFd(backend_.currentFd());
|
||||
deviceLabel_ = deviceLabel;
|
||||
transportLabel_ = transportLabel;
|
||||
uri_ = uri;
|
||||
suspended_ = suspended;
|
||||
setState(FridaSessionState::Connecting);
|
||||
emit consoleMessage(tr("opening %1").arg(uri));
|
||||
|
||||
const QString batch = QStringLiteral("of ") + FridaUri::quote(uri);
|
||||
backend_.runBatch(batch, [this, before, generation, suspended](const QString &output) {
|
||||
const int fd = backend_.findNewFridaFd(before);
|
||||
if (generation != generation_) {
|
||||
backend_.closeFd(fd);
|
||||
return;
|
||||
}
|
||||
if (fd < 0) {
|
||||
lastError_ = output.trimmed().isEmpty()
|
||||
? tr("Could not open the r2frida target.")
|
||||
: output.trimmed();
|
||||
clearTarget();
|
||||
setState(FridaSessionState::Error);
|
||||
emit errorMessage(lastError_);
|
||||
emit consoleMessage(lastError_);
|
||||
return;
|
||||
}
|
||||
backend_.setFridaFd(fd);
|
||||
const int host = backend_.hostFd();
|
||||
if (host > 0 && backend_.currentFd() != host) {
|
||||
Core()->cmd(QStringLiteral("o=") + QString::number(host));
|
||||
}
|
||||
suspended_ = suspended;
|
||||
readSnapshot();
|
||||
setState(FridaSessionState::Attached);
|
||||
emit consoleMessage(
|
||||
tr("attached pid %1 (%2)").arg(process_.pid).arg(process_.name));
|
||||
});
|
||||
}
|
||||
|
||||
void FridaSession::detach()
|
||||
{
|
||||
++generation_;
|
||||
const int fd = backend_.fridaFd();
|
||||
const int host = backend_.hostFd();
|
||||
if (host > 0 && backend_.currentFd() == fd) {
|
||||
Core()->cmd(QStringLiteral("o=") + QString::number(host));
|
||||
}
|
||||
backend_.closeFd(fd);
|
||||
clearTarget();
|
||||
lastError_.clear();
|
||||
setState(FridaSessionState::Disconnected);
|
||||
emit consoleMessage(tr("detached"));
|
||||
}
|
||||
|
||||
void FridaSession::resume()
|
||||
{
|
||||
if (!isAttached()) {
|
||||
return;
|
||||
}
|
||||
command(QStringLiteral("dc"), [this](const QString &output) {
|
||||
suspended_ = false;
|
||||
emit consoleMessage(output.trimmed().isEmpty() ? tr("resumed") : output.trimmed());
|
||||
emit changed();
|
||||
});
|
||||
}
|
||||
|
||||
void FridaSession::command(const QString &fridaCmd, const std::function<void(QString)> &done)
|
||||
{
|
||||
if (!isAttached()) {
|
||||
if (done) {
|
||||
done(tr("Not attached."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (backend_.busy()) {
|
||||
if (done) {
|
||||
done(tr("r2frida is busy."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const int generation = generation_;
|
||||
const int fd = backend_.fridaFd();
|
||||
const int back = backend_.currentFd() == fd ? backend_.hostFd() : backend_.currentFd();
|
||||
QString batch = QStringLiteral("o=") + QString::number(fd) + QStringLiteral("\n:") + fridaCmd
|
||||
+ QLatin1Char('\n');
|
||||
if (back > 0 && back != fd) {
|
||||
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
|
||||
}
|
||||
backend_.runBatch(batch, [this, generation, done](const QString &output) {
|
||||
if (generation != generation_) {
|
||||
return;
|
||||
}
|
||||
if (done) {
|
||||
done(output);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QByteArray FridaSession::readBytes(quint64 runtime, int length)
|
||||
{
|
||||
if (!isAttached() || length <= 0 || backend_.busy()) {
|
||||
return {};
|
||||
}
|
||||
const int fd = backend_.fridaFd();
|
||||
const int current = backend_.currentFd();
|
||||
const int back = current == fd ? backend_.hostFd() : current;
|
||||
const QString savedColor = Core()->getConfig(QStringLiteral("scr.color"));
|
||||
QString batch = QStringLiteral("e scr.color=0\no=") + QString::number(fd);
|
||||
batch += QStringLiteral("\np8 ") + QString::number(length);
|
||||
batch += QStringLiteral(" @ ") + RAddressString(runtime) + QLatin1Char('\n');
|
||||
if (back > 0 && back != fd) {
|
||||
batch += QStringLiteral("o=") + QString::number(back) + QLatin1Char('\n');
|
||||
}
|
||||
batch += QStringLiteral("e scr.color=") + savedColor + QLatin1Char('\n');
|
||||
|
||||
RCons *previous = r_cons_singleton();
|
||||
RCons *cons = r_core_get_cons(Core()->core_);
|
||||
if (cons) {
|
||||
r_cons_global(cons);
|
||||
}
|
||||
const QString output = Core()->cmd(batch);
|
||||
if (previous && previous != cons) {
|
||||
r_cons_global(previous);
|
||||
}
|
||||
if (output.contains(QStringLiteral("ERROR")) || output.contains(QStringLiteral("Cannot"))) {
|
||||
return {};
|
||||
}
|
||||
QString hex;
|
||||
hex.reserve(output.size());
|
||||
for (const QChar ch : output) {
|
||||
if (ch.isDigit() || (ch >= QLatin1Char('a') && ch <= QLatin1Char('f'))
|
||||
|| (ch >= QLatin1Char('A') && ch <= QLatin1Char('F'))) {
|
||||
hex.append(ch);
|
||||
}
|
||||
}
|
||||
return QByteArray::fromHex(hex.toLatin1());
|
||||
}
|
||||
|
||||
void FridaSession::addHook(quint64 address, const QString &summary)
|
||||
{
|
||||
FridaHookInfo hook;
|
||||
hook.address = address;
|
||||
hook.summary = summary;
|
||||
hooks_.append(hook);
|
||||
emit changed();
|
||||
}
|
||||
|
||||
void FridaSession::note(const QString &text)
|
||||
{
|
||||
emit consoleMessage(text);
|
||||
}
|
||||
|
||||
QString FridaSession::writeScript(const QString &source)
|
||||
{
|
||||
QString dir = QDir::tempPath();
|
||||
if (dir.contains(QLatin1Char(' '))) {
|
||||
dir = QStringLiteral("/tmp");
|
||||
}
|
||||
auto *file = new QTemporaryFile(dir + QStringLiteral("/iaito-frida-XXXXXX.js"), this);
|
||||
file->setAutoRemove(true);
|
||||
if (!file->open()) {
|
||||
delete file;
|
||||
return QString();
|
||||
}
|
||||
file->write(source.toUtf8());
|
||||
file->flush();
|
||||
return file->fileName();
|
||||
}
|
||||
|
||||
void FridaSession::importExports(const QString &moduleName)
|
||||
{
|
||||
if (!isAttached() || moduleName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const QString commandText = QStringLiteral("iEj ") + moduleName;
|
||||
command(commandText, [this, moduleName](const QString &output) {
|
||||
const QJsonDocument doc = FridaBackend::parseJsonLoose(output);
|
||||
const QJsonArray exports = doc.array();
|
||||
QString script = QStringLiteral("fs frida\n");
|
||||
int imported = 0;
|
||||
const QString prefix = QStringLiteral("frida.") + moduleName + QLatin1Char('.');
|
||||
for (const QJsonValue value : exports) {
|
||||
const QJsonObject exp = value.toObject();
|
||||
const quint64 runtime
|
||||
= FridaBackend::parseAddress(exp.value(QStringLiteral("address")));
|
||||
const quint64 staticVa = toStatic(runtime);
|
||||
if (staticVa == RVA_INVALID) {
|
||||
continue;
|
||||
}
|
||||
QString name = exp.value(QStringLiteral("name")).toString();
|
||||
name.replace(QRegularExpression(QStringLiteral("[^A-Za-z0-9._]")), QStringLiteral("_"));
|
||||
if (name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
script += QStringLiteral("f ");
|
||||
script += prefix;
|
||||
script += name;
|
||||
script += QStringLiteral(" = ");
|
||||
script += RAddressString(staticVa);
|
||||
script += QLatin1Char('\n');
|
||||
++imported;
|
||||
if (imported >= 20000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
script += QStringLiteral("fs *\n");
|
||||
if (imported == 0) {
|
||||
emit errorMessage(tr("No exports from %1 map into the static file.").arg(moduleName));
|
||||
return;
|
||||
}
|
||||
Core()->cmd(script);
|
||||
Core()->triggerRefreshAll();
|
||||
emit consoleMessage(tr("imported %1 symbols from %2").arg(imported).arg(moduleName));
|
||||
});
|
||||
}
|
||||
|
||||
void FridaSession::readSnapshot()
|
||||
{
|
||||
const QString blob = backend_.fridaCommand(
|
||||
QStringLiteral("ij\n?e ---IAITO-FRIDA---\n:ilj\n?e ---IAITO-FRIDA---\n:dmj"));
|
||||
QStringList parts = blob.split(QStringLiteral("---IAITO-FRIDA---"));
|
||||
if (parts.size() < 3) {
|
||||
parts = QStringList{blob, QString(), QString()};
|
||||
}
|
||||
applyInfo(FridaBackend::parseJsonLoose(parts.value(0)).object());
|
||||
applyModules(FridaBackend::parseJsonLoose(parts.value(1)).array());
|
||||
applyMaps(FridaBackend::parseJsonLoose(parts.value(2)).array());
|
||||
}
|
||||
|
||||
void FridaSession::applyInfo(const QJsonObject &info)
|
||||
{
|
||||
process_ = FridaProcessInfo();
|
||||
process_.pid = info.value(QStringLiteral("pid")).toInt(-1);
|
||||
process_.arch = info.value(QStringLiteral("arch")).toString();
|
||||
process_.bits = info.value(QStringLiteral("bits")).toInt();
|
||||
process_.os = info.value(QStringLiteral("os")).toString();
|
||||
process_.moduleName = info.value(QStringLiteral("modulename")).toString();
|
||||
process_.moduleBase = FridaBackend::parseAddress(info.value(QStringLiteral("modulebase")));
|
||||
const QString app = info.value(QStringLiteral("appname")).toString();
|
||||
const QString packageName = info.value(QStringLiteral("packageName")).toString();
|
||||
const QString bundle = info.value(QStringLiteral("bundle")).toString();
|
||||
process_.identifier = !bundle.isEmpty() ? bundle : packageName;
|
||||
if (!app.isEmpty()) {
|
||||
process_.name = app;
|
||||
} else if (!packageName.isEmpty()) {
|
||||
process_.name = packageName;
|
||||
} else if (!bundle.isEmpty()) {
|
||||
process_.name = bundle;
|
||||
} else {
|
||||
process_.name = process_.moduleName;
|
||||
}
|
||||
}
|
||||
|
||||
void FridaSession::applyModules(const QJsonArray &modules)
|
||||
{
|
||||
modules_.clear();
|
||||
const QString opened = QFileInfo(Core()->getFilePath()).fileName();
|
||||
int match = -1;
|
||||
for (const QJsonValue value : modules) {
|
||||
const QJsonObject object = value.toObject();
|
||||
FridaModuleInfo module;
|
||||
module.name = object.value(QStringLiteral("name")).toString();
|
||||
module.path = object.value(QStringLiteral("path")).toString();
|
||||
module.base = FridaBackend::parseAddress(object.value(QStringLiteral("base")));
|
||||
module.size = static_cast<quint64>(object.value(QStringLiteral("size")).toDouble());
|
||||
const QString fileName = QFileInfo(module.path).fileName();
|
||||
const bool nameMatch = !opened.isEmpty() && module.name == opened;
|
||||
const bool pathMatch = !opened.isEmpty() && fileName == opened;
|
||||
modules_.append(module);
|
||||
if (nameMatch || pathMatch) {
|
||||
if (match < 0 || nameMatch) {
|
||||
match = modules_.size() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
staticBase_ = 0;
|
||||
runtimeBase_ = 0;
|
||||
matchedSize_ = 0;
|
||||
slide_ = 0;
|
||||
if (match < 0) {
|
||||
return;
|
||||
}
|
||||
const QJsonObject bin
|
||||
= Core()->cmdj(QStringLiteral("ij")).object().value(QStringLiteral("bin")).toObject();
|
||||
staticBase_ = bin.value(QStringLiteral("baddr")).toVariant().toULongLong();
|
||||
FridaModuleInfo &module = modules_[match];
|
||||
module.matched = true;
|
||||
runtimeBase_ = module.base;
|
||||
matchedSize_ = module.size;
|
||||
slide_ = runtimeBase_ - staticBase_;
|
||||
}
|
||||
|
||||
void FridaSession::applyMaps(const QJsonArray &maps)
|
||||
{
|
||||
maps_.clear();
|
||||
for (const QJsonValue value : maps) {
|
||||
const QJsonObject object = value.toObject();
|
||||
FridaMapInfo map;
|
||||
map.start = FridaBackend::parseAddress(object.value(QStringLiteral("base")));
|
||||
map.end = map.start + static_cast<quint64>(object.value(QStringLiteral("size")).toDouble());
|
||||
map.permission = object.value(QStringLiteral("protection")).toString();
|
||||
const QJsonValue file = object.value(QStringLiteral("file"));
|
||||
if (file.isObject()) {
|
||||
map.path = file.toObject().value(QStringLiteral("path")).toString();
|
||||
} else if (file.isString()) {
|
||||
map.path = file.toString();
|
||||
}
|
||||
maps_.append(map);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "FridaBackend.h"
|
||||
#include "FridaTypes.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <functional>
|
||||
|
||||
class FridaSession : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FridaSession(QObject *parent = nullptr);
|
||||
|
||||
bool checkAvailable();
|
||||
bool available() const { return available_; }
|
||||
FridaSessionState state() const { return state_; }
|
||||
bool isAttached() const { return state_ == FridaSessionState::Attached && fridaFd() >= 0; }
|
||||
bool isSuspended() const { return suspended_; }
|
||||
bool busy() const { return backend_.busy(); }
|
||||
QString lastError() const { return lastError_; }
|
||||
QString deviceLabel() const { return deviceLabel_; }
|
||||
QString transportLabel() const { return transportLabel_; }
|
||||
QString uri() const { return uri_; }
|
||||
|
||||
const FridaProcessInfo &process() const { return process_; }
|
||||
const QList<FridaModuleInfo> &modules() const { return modules_; }
|
||||
const QList<FridaMapInfo> &maps() const { return maps_; }
|
||||
const QList<FridaHookInfo> &hooks() const { return hooks_; }
|
||||
quint64 staticBase() const { return staticBase_; }
|
||||
quint64 slide() const { return slide_; }
|
||||
bool hasStaticMapping() const { return matchedSize_ > 0; }
|
||||
|
||||
FridaAddressMode addressMode() const { return addressMode_; }
|
||||
void setAddressMode(FridaAddressMode mode);
|
||||
|
||||
quint64 toRuntime(quint64 staticVa) const;
|
||||
quint64 toStatic(quint64 runtimeVa) const;
|
||||
bool containsStatic(quint64 addr) const;
|
||||
bool containsRuntime(quint64 addr) const;
|
||||
QString formatRuntime(quint64 runtimeVa) const;
|
||||
QString describe(quint64 addr) const;
|
||||
|
||||
void probe(const QString &uri, const std::function<void(QString)> &done);
|
||||
void openTarget(
|
||||
const QString &uri,
|
||||
bool suspended,
|
||||
const QString &deviceLabel,
|
||||
const QString &transportLabel);
|
||||
void detach();
|
||||
void resume();
|
||||
void command(const QString &fridaCmd, const std::function<void(QString)> &done);
|
||||
QByteArray readBytes(quint64 runtime, int length);
|
||||
void importExports(const QString &moduleName);
|
||||
QString writeScript(const QString &source);
|
||||
void note(const QString &text);
|
||||
void addHook(quint64 address, const QString &summary);
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
void errorMessage(const QString &message);
|
||||
void consoleMessage(const QString &message);
|
||||
|
||||
private:
|
||||
int fridaFd() const { return backend_.fridaFd(); }
|
||||
void setState(FridaSessionState state);
|
||||
void clearTarget();
|
||||
void readSnapshot();
|
||||
void applyInfo(const QJsonObject &info);
|
||||
void applyModules(const QJsonArray &modules);
|
||||
void applyMaps(const QJsonArray &maps);
|
||||
|
||||
FridaBackend backend_;
|
||||
FridaSessionState state_ = FridaSessionState::Disconnected;
|
||||
bool available_ = false;
|
||||
bool suspended_ = false;
|
||||
int generation_ = 0;
|
||||
QString lastError_;
|
||||
QString deviceLabel_;
|
||||
QString transportLabel_;
|
||||
QString uri_;
|
||||
FridaProcessInfo process_;
|
||||
QList<FridaModuleInfo> modules_;
|
||||
QList<FridaMapInfo> maps_;
|
||||
QList<FridaHookInfo> hooks_;
|
||||
quint64 staticBase_ = 0;
|
||||
quint64 runtimeBase_ = 0;
|
||||
quint64 matchedSize_ = 0;
|
||||
quint64 slide_ = 0;
|
||||
FridaAddressMode addressMode_ = FridaAddressMode::Runtime;
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
enum class FridaTransport { Local, Usb, Remote };
|
||||
|
||||
enum class FridaAction { Attach, Spawn, Launch };
|
||||
|
||||
enum class FridaListKind { Devices, Processes, Applications };
|
||||
|
||||
enum class FridaAddressMode { Static, Runtime, ModuleOffset };
|
||||
|
||||
enum class FridaSessionState { Disconnected, Connecting, Attached, Error };
|
||||
|
||||
struct FridaDeviceInfo
|
||||
{
|
||||
QString id;
|
||||
QString type;
|
||||
QString name;
|
||||
};
|
||||
|
||||
struct FridaTargetInfo
|
||||
{
|
||||
QString pid;
|
||||
QString name;
|
||||
QString identifier;
|
||||
};
|
||||
|
||||
struct FridaModuleInfo
|
||||
{
|
||||
QString name;
|
||||
QString path;
|
||||
quint64 base = 0;
|
||||
quint64 size = 0;
|
||||
bool matched = false;
|
||||
};
|
||||
|
||||
struct FridaMapInfo
|
||||
{
|
||||
quint64 start = 0;
|
||||
quint64 end = 0;
|
||||
QString permission;
|
||||
QString path;
|
||||
};
|
||||
|
||||
struct FridaProcessInfo
|
||||
{
|
||||
int pid = -1;
|
||||
QString name;
|
||||
QString identifier;
|
||||
QString arch;
|
||||
int bits = 0;
|
||||
QString os;
|
||||
QString moduleName;
|
||||
quint64 moduleBase = 0;
|
||||
};
|
||||
|
||||
struct FridaHookInfo
|
||||
{
|
||||
quint64 address = 0;
|
||||
QString summary;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "FridaUri.h"
|
||||
|
||||
static QString actionName(FridaAction action)
|
||||
{
|
||||
switch (action) {
|
||||
case FridaAction::Spawn:
|
||||
return QStringLiteral("spawn");
|
||||
case FridaAction::Launch:
|
||||
return QStringLiteral("launch");
|
||||
case FridaAction::Attach:
|
||||
break;
|
||||
}
|
||||
return QStringLiteral("attach");
|
||||
}
|
||||
|
||||
static QString linkName(FridaTransport transport)
|
||||
{
|
||||
switch (transport) {
|
||||
case FridaTransport::Usb:
|
||||
return QStringLiteral("usb");
|
||||
case FridaTransport::Remote:
|
||||
return QStringLiteral("remote");
|
||||
case FridaTransport::Local:
|
||||
break;
|
||||
}
|
||||
return QStringLiteral("local");
|
||||
}
|
||||
|
||||
QString FridaUri::quote(const QString &text)
|
||||
{
|
||||
QString escaped = text;
|
||||
escaped.replace(QLatin1Char('\''), QStringLiteral("'\\''"));
|
||||
return QStringLiteral("'") + escaped + QStringLiteral("'");
|
||||
}
|
||||
|
||||
bool FridaUri::targetIsSafe(const QString &targetId)
|
||||
{
|
||||
if (targetId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const QString forbidden = QStringLiteral("/'\"`;|@\n\r");
|
||||
for (const QChar ch : targetId) {
|
||||
if (forbidden.contains(ch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QString FridaUri::listDevices()
|
||||
{
|
||||
// Three components with an empty device id: r2frida dumps every device
|
||||
// (local, usb, and remote), then fails the open on purpose.
|
||||
return QStringLiteral("frida://list/usb/");
|
||||
}
|
||||
|
||||
QString FridaUri::listTargets(
|
||||
FridaTransport transport, FridaListKind kind, const QString &deviceId)
|
||||
{
|
||||
const QString action = kind == FridaListKind::Applications ? QStringLiteral("apps")
|
||||
: QStringLiteral("list");
|
||||
QString id = deviceId;
|
||||
if (transport == FridaTransport::Usb && (id.isEmpty() || id == QStringLiteral("usb"))) {
|
||||
id.clear();
|
||||
}
|
||||
if (transport == FridaTransport::Local) {
|
||||
id.clear();
|
||||
}
|
||||
return QStringLiteral("frida://") + action + QStringLiteral("/") + linkName(transport)
|
||||
+ QStringLiteral("/") + id + QStringLiteral("/");
|
||||
}
|
||||
|
||||
QString FridaUri::target(
|
||||
FridaTransport transport,
|
||||
FridaAction action,
|
||||
const QString &deviceId,
|
||||
const QString &targetId)
|
||||
{
|
||||
const QString verb = actionName(action);
|
||||
if (transport == FridaTransport::Local) {
|
||||
return QStringLiteral("frida://") + verb + QStringLiteral("/") + targetId;
|
||||
}
|
||||
QString id = deviceId;
|
||||
if (transport == FridaTransport::Usb && (id.isEmpty() || id == QStringLiteral("usb"))) {
|
||||
id.clear();
|
||||
}
|
||||
return QStringLiteral("frida://") + verb + QStringLiteral("/") + linkName(transport)
|
||||
+ QStringLiteral("/") + id + QStringLiteral("/") + targetId;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "FridaTypes.h"
|
||||
|
||||
class FridaUri
|
||||
{
|
||||
public:
|
||||
static QString listDevices();
|
||||
static QString listTargets(
|
||||
FridaTransport transport, FridaListKind kind, const QString &deviceId);
|
||||
static QString target(
|
||||
FridaTransport transport,
|
||||
FridaAction action,
|
||||
const QString &deviceId,
|
||||
const QString &targetId);
|
||||
static QString quote(const QString &text);
|
||||
static bool targetIsSafe(const QString &targetId);
|
||||
};
|
||||
@@ -0,0 +1,345 @@
|
||||
#include "FridaWidget.h"
|
||||
|
||||
#include "FridaConnectDialog.h"
|
||||
#include "FridaPanels.h"
|
||||
#include "FridaSession.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "widgets/HexdumpWidget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QTabWidget>
|
||||
#include <QTreeWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
quint64 fridaRuntimeAddress(FridaSession *session, quint64 addr)
|
||||
{
|
||||
if (!session) {
|
||||
return addr;
|
||||
}
|
||||
const quint64 runtime = session->toRuntime(addr);
|
||||
return runtime == RVA_INVALID ? addr : runtime;
|
||||
}
|
||||
|
||||
void fridaSeekRuntime(FridaSession *session, quint64 runtimeVa)
|
||||
{
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const quint64 staticVa = session->toStatic(runtimeVa);
|
||||
if (staticVa == RVA_INVALID) {
|
||||
Core()->message(QObject::tr("No static mapping for %1").arg(RAddressString(runtimeVa)));
|
||||
session->note(session->describe(runtimeVa));
|
||||
return;
|
||||
}
|
||||
Core()->seekAndShow(staticVa);
|
||||
}
|
||||
|
||||
void fridaTrace(FridaSession *session, quint64 addr)
|
||||
{
|
||||
if (!session || !session->isAttached()) {
|
||||
return;
|
||||
}
|
||||
const quint64 runtime = fridaRuntimeAddress(session, addr);
|
||||
const QString command = QStringLiteral("dt ") + RAddressString(runtime);
|
||||
session->command(command, [session](const QString &output) {
|
||||
session->note(output.trimmed().isEmpty() ? QObject::tr("trace installed") : output);
|
||||
});
|
||||
}
|
||||
|
||||
void fridaBreakpoint(FridaSession *session, quint64 addr)
|
||||
{
|
||||
if (!session || !session->isAttached()) {
|
||||
return;
|
||||
}
|
||||
const quint64 runtime = fridaRuntimeAddress(session, addr);
|
||||
const QString command = QStringLiteral("db ") + RAddressString(runtime);
|
||||
session->command(command, [session](const QString &output) {
|
||||
session->note(output.trimmed().isEmpty() ? QObject::tr("breakpoint set") : output);
|
||||
});
|
||||
}
|
||||
|
||||
void fridaShowHookDialog(QWidget *parent, FridaSession *session, quint64 addr)
|
||||
{
|
||||
if (!session || !session->isAttached()) {
|
||||
return;
|
||||
}
|
||||
const quint64 runtime = fridaRuntimeAddress(session, addr);
|
||||
auto *dialog = new QDialog(parent);
|
||||
dialog->setWindowTitle(QObject::tr("Hook %1").arg(RAddressString(runtime)));
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
auto *layout = new QVBoxLayout(dialog);
|
||||
layout->addWidget(new QLabel(session->describe(addr), dialog));
|
||||
auto *logArgs = new QCheckBox(QObject::tr("Log arguments"), dialog);
|
||||
auto *logRet = new QCheckBox(QObject::tr("Log return value"), dialog);
|
||||
auto *backtrace = new QCheckBox(QObject::tr("Backtrace"), dialog);
|
||||
logArgs->setChecked(true);
|
||||
logRet->setChecked(true);
|
||||
layout->addWidget(logArgs);
|
||||
layout->addWidget(logRet);
|
||||
layout->addWidget(backtrace);
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, dialog);
|
||||
buttons->button(QDialogButtonBox::Ok)->setText(QObject::tr("Install Hook"));
|
||||
layout->addWidget(buttons);
|
||||
QObject::connect(buttons, &QDialogButtonBox::rejected, dialog, &QDialog::reject);
|
||||
QObject::connect(buttons, &QDialogButtonBox::accepted, dialog, [=]() {
|
||||
QString body = QStringLiteral("Interceptor.attach(ptr('%1'), {\n")
|
||||
.arg(RAddressString(runtime));
|
||||
body += QStringLiteral(" onEnter(args) {\n");
|
||||
if (logArgs->isChecked()) {
|
||||
body += QStringLiteral(
|
||||
" console.log('hook', this.context.pc, args[0], args[1], args[2], args[3]);\n");
|
||||
}
|
||||
if (backtrace->isChecked()) {
|
||||
body += QStringLiteral(
|
||||
" console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)"
|
||||
".map(DebugSymbol.fromAddress).join('\\n'));\n");
|
||||
}
|
||||
body += QStringLiteral(" },\n onLeave(retval) {\n");
|
||||
if (logRet->isChecked()) {
|
||||
body += QStringLiteral(" console.log('return', retval);\n");
|
||||
}
|
||||
body += QStringLiteral(" }\n});\n");
|
||||
const QString path = session->writeScript(body);
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
session->addHook(runtime, QObject::tr("Interceptor.attach"));
|
||||
session->command(QStringLiteral(". ") + path, [session](const QString &output) {
|
||||
session->note(output.trimmed().isEmpty() ? QObject::tr("hook installed") : output);
|
||||
});
|
||||
dialog->accept();
|
||||
});
|
||||
dialog->open();
|
||||
}
|
||||
|
||||
void fridaShowLiveHex(QWidget *parent, FridaSession *session, quint64 runtimeAddr)
|
||||
{
|
||||
if (!session || !session->isAttached() || !parent) {
|
||||
return;
|
||||
}
|
||||
auto *main = qobject_cast<MainWindow *>(parent->window());
|
||||
if (!main) {
|
||||
return;
|
||||
}
|
||||
HexdumpWidget *hex = nullptr;
|
||||
const auto dumps = main->findChildren<HexdumpWidget *>();
|
||||
for (HexdumpWidget *dump : dumps) {
|
||||
if (!dump->isHidden()) {
|
||||
hex = dump;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hex && !dumps.isEmpty()) {
|
||||
hex = dumps.first();
|
||||
}
|
||||
if (!hex) {
|
||||
return;
|
||||
}
|
||||
auto *toggle = hex->findChild<QCheckBox *>(QStringLiteral("fridaLiveToggle"));
|
||||
if (toggle && !toggle->isChecked()) {
|
||||
toggle->setChecked(true);
|
||||
} else if (auto *view = hex->findChild<HexWidget *>(QStringLiteral("hexTextView"))) {
|
||||
view->refresh();
|
||||
}
|
||||
const quint64 staticVa = session->toStatic(runtimeAddr);
|
||||
hex->show();
|
||||
hex->raise();
|
||||
if (IaitoSeekable *seekable = hex->getSeekable()) {
|
||||
seekable->seek(staticVa == RVA_INVALID ? runtimeAddr : staticVa);
|
||||
}
|
||||
}
|
||||
|
||||
void fridaShowExports(QWidget *parent, FridaSession *session, const QString &moduleName)
|
||||
{
|
||||
const QString command = QStringLiteral("iEj ") + moduleName;
|
||||
session->command(command, [parent, session, moduleName](const QString &output) {
|
||||
const QJsonDocument doc = FridaBackend::parseJsonLoose(output);
|
||||
auto *dialog = new QDialog(parent);
|
||||
dialog->setWindowTitle(QObject::tr("Exports %1").arg(moduleName));
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->resize(640, 480);
|
||||
auto *view = new QTreeWidget(dialog);
|
||||
view->setHeaderLabels(
|
||||
{QObject::tr("Address"), QObject::tr("Type"), QObject::tr("Name")});
|
||||
view->setRootIsDecorated(false);
|
||||
for (const QJsonValue value : doc.array()) {
|
||||
const QJsonObject exp = value.toObject();
|
||||
const quint64 runtime
|
||||
= FridaBackend::parseAddress(exp.value(QStringLiteral("address")));
|
||||
auto *item = new QTreeWidgetItem(view);
|
||||
item->setText(0, session->formatRuntime(runtime));
|
||||
item->setText(1, exp.value(QStringLiteral("type")).toString());
|
||||
item->setText(2, exp.value(QStringLiteral("name")).toString());
|
||||
item->setData(0, Qt::UserRole, QVariant::fromValue(runtime));
|
||||
}
|
||||
auto *layout = new QVBoxLayout(dialog);
|
||||
layout->addWidget(view);
|
||||
QObject::connect(
|
||||
view, &QTreeWidget::itemActivated, session, [session](QTreeWidgetItem *item, int) {
|
||||
fridaSeekRuntime(session, item->data(0, Qt::UserRole).toULongLong());
|
||||
});
|
||||
dialog->open();
|
||||
});
|
||||
}
|
||||
|
||||
FridaWidget::FridaWidget(FridaSession *fridaSession, MainWindow *main)
|
||||
: IaitoDockWidget(main)
|
||||
, session(fridaSession)
|
||||
{
|
||||
setObjectName(QStringLiteral("FridaWidget"));
|
||||
setWindowTitle(tr("r2frida"));
|
||||
|
||||
auto *root = new QWidget(this);
|
||||
auto *layout = new QVBoxLayout(root);
|
||||
|
||||
auto *header = new QHBoxLayout();
|
||||
statusDot = new QWidget(root);
|
||||
statusDot->setFixedSize(10, 10);
|
||||
stateLabel = new QLabel(root);
|
||||
connectButton = new QPushButton(tr("Connect"), root);
|
||||
resumeButton = new QPushButton(tr("Resume"), root);
|
||||
detachButton = new QPushButton(tr("Detach"), root);
|
||||
detachButton->setStyleSheet(QStringLiteral(
|
||||
"QPushButton { background-color: #E5534B; color: white; border: none;"
|
||||
" border-radius: 4px; padding: 4px 12px; }"
|
||||
"QPushButton:disabled { background-color: palette(mid); color: palette(button-text); }"));
|
||||
header->addWidget(statusDot, 0, Qt::AlignVCenter);
|
||||
header->addWidget(stateLabel, 1);
|
||||
header->addWidget(resumeButton);
|
||||
header->addWidget(connectButton);
|
||||
header->addWidget(detachButton);
|
||||
layout->addLayout(header);
|
||||
|
||||
deviceCombo = new QComboBox(root);
|
||||
targetCombo = new QComboBox(root);
|
||||
pidLabel = new QLabel(root);
|
||||
archLabel = new QLabel(root);
|
||||
sessionLabel = new QLabel(root);
|
||||
sessionLabel->setWordWrap(true);
|
||||
sessionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
|
||||
auto *targetRow = new QHBoxLayout();
|
||||
targetRow->addWidget(targetCombo, 1);
|
||||
targetRow->addWidget(pidLabel);
|
||||
auto *targetWrap = new QWidget(root);
|
||||
targetWrap->setLayout(targetRow);
|
||||
|
||||
auto *form = new QFormLayout();
|
||||
form->setContentsMargins(0, 0, 0, 0);
|
||||
form->addRow(tr("Device"), deviceCombo);
|
||||
form->addRow(tr("Target"), targetWrap);
|
||||
form->addRow(tr("Architecture"), archLabel);
|
||||
form->addRow(tr("Session"), sessionLabel);
|
||||
layout->addLayout(form);
|
||||
|
||||
auto *tabs = new QTabWidget(root);
|
||||
for (QWidget *page : createFridaPanels(session)) {
|
||||
tabs->addTab(page, page->windowTitle());
|
||||
}
|
||||
layout->addWidget(tabs, 1);
|
||||
setWidget(root);
|
||||
|
||||
connect(session, &FridaSession::changed, this, &FridaWidget::refresh);
|
||||
connect(session, &FridaSession::errorMessage, this, [this](const QString &message) {
|
||||
QMessageBox::warning(this, tr("r2frida"), message);
|
||||
});
|
||||
connect(connectButton, &QPushButton::clicked, this, &FridaWidget::showConnectDialog);
|
||||
connect(resumeButton, &QPushButton::clicked, session, &FridaSession::resume);
|
||||
connect(detachButton, &QPushButton::clicked, session, &FridaSession::detach);
|
||||
const auto openConnect = [this](int index) {
|
||||
if (index == 1) {
|
||||
showConnectDialog();
|
||||
deviceCombo->setCurrentIndex(0);
|
||||
targetCombo->setCurrentIndex(0);
|
||||
}
|
||||
};
|
||||
connect(deviceCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, openConnect);
|
||||
connect(targetCombo, qOverload<int>(&QComboBox::currentIndexChanged), this, openConnect);
|
||||
|
||||
refresh();
|
||||
hide();
|
||||
}
|
||||
|
||||
void FridaWidget::showConnectDialog()
|
||||
{
|
||||
auto *dialog = new FridaConnectDialog(session, this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->open();
|
||||
}
|
||||
|
||||
void FridaWidget::fillChoice(QComboBox *combo, const QString ¤t)
|
||||
{
|
||||
const bool blocked = combo->blockSignals(true);
|
||||
combo->clear();
|
||||
combo->addItem(current.isEmpty() ? tr("Not connected") : current);
|
||||
combo->addItem(tr("Connect..."));
|
||||
combo->setCurrentIndex(0);
|
||||
combo->blockSignals(blocked);
|
||||
}
|
||||
|
||||
void FridaWidget::setLive(bool live)
|
||||
{
|
||||
const QString color = live ? QStringLiteral("#3DDC84") : QStringLiteral("#E5534B");
|
||||
statusDot->setStyleSheet(
|
||||
QStringLiteral("background-color: %1; border-radius: 5px;").arg(color));
|
||||
}
|
||||
|
||||
void FridaWidget::refresh()
|
||||
{
|
||||
const bool attached = session->isAttached();
|
||||
const bool spawned = attached && session->isSuspended();
|
||||
QString stateText = tr("Disconnected");
|
||||
if (session->state() == FridaSessionState::Connecting) {
|
||||
stateText = tr("Connecting");
|
||||
} else if (spawned) {
|
||||
stateText = tr("Spawned");
|
||||
} else if (attached) {
|
||||
stateText = tr("Connected");
|
||||
} else if (session->state() == FridaSessionState::Error) {
|
||||
stateText = tr("Error");
|
||||
}
|
||||
stateLabel->setText(stateText);
|
||||
setLive(attached);
|
||||
|
||||
const FridaProcessInfo info = session->process();
|
||||
QString device = session->deviceLabel();
|
||||
if (!session->transportLabel().isEmpty()) {
|
||||
device = device.isEmpty()
|
||||
? session->transportLabel()
|
||||
: session->transportLabel() + QStringLiteral(": ") + device;
|
||||
}
|
||||
fillChoice(deviceCombo, attached ? device : QString());
|
||||
fillChoice(targetCombo, attached ? info.name : QString());
|
||||
const QString pid = info.pid >= 0 ? QString::number(info.pid) : QStringLiteral("—");
|
||||
pidLabel->setText(tr("PID: %1").arg(pid));
|
||||
archLabel->setText(info.arch.isEmpty() ? QStringLiteral("—") : info.arch);
|
||||
QString sessionText = info.identifier;
|
||||
if (sessionText.isEmpty()) {
|
||||
sessionText = session->uri().isEmpty() ? QStringLiteral("—") : session->uri();
|
||||
}
|
||||
sessionLabel->setText(sessionText);
|
||||
sessionLabel->setToolTip(session->uri());
|
||||
|
||||
resumeButton->setEnabled(spawned);
|
||||
detachButton->setEnabled(attached || session->state() == FridaSessionState::Connecting);
|
||||
if (attached && !wasAttached) {
|
||||
show();
|
||||
raise();
|
||||
}
|
||||
wasAttached = attached;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "widgets/IaitoDockWidget.h"
|
||||
|
||||
class FridaSession;
|
||||
class QComboBox;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QWidget;
|
||||
|
||||
class FridaWidget : public IaitoDockWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit FridaWidget(FridaSession *session, MainWindow *main);
|
||||
|
||||
void showConnectDialog();
|
||||
|
||||
private:
|
||||
void refresh();
|
||||
void fillChoice(QComboBox *combo, const QString ¤t);
|
||||
void setLive(bool live);
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
QWidget *statusDot = nullptr;
|
||||
QLabel *stateLabel = nullptr;
|
||||
QLabel *pidLabel = nullptr;
|
||||
QLabel *archLabel = nullptr;
|
||||
QLabel *sessionLabel = nullptr;
|
||||
QComboBox *deviceCombo = nullptr;
|
||||
QComboBox *targetCombo = nullptr;
|
||||
QPushButton *connectButton = nullptr;
|
||||
QPushButton *resumeButton = nullptr;
|
||||
QPushButton *detachButton = nullptr;
|
||||
bool wasAttached = false;
|
||||
};
|
||||
|
||||
quint64 fridaRuntimeAddress(FridaSession *session, quint64 addr);
|
||||
void fridaSeekRuntime(FridaSession *session, quint64 runtimeVa);
|
||||
void fridaTrace(FridaSession *session, quint64 addr);
|
||||
void fridaBreakpoint(FridaSession *session, quint64 addr);
|
||||
void fridaShowHookDialog(QWidget *parent, FridaSession *session, quint64 addr);
|
||||
void fridaShowLiveHex(QWidget *parent, FridaSession *session, quint64 runtimeAddr);
|
||||
void fridaShowExports(QWidget *parent, FridaSession *session, const QString &moduleName);
|
||||
@@ -0,0 +1,298 @@
|
||||
#include "IaitoFridaPlugin.h"
|
||||
|
||||
#include "FridaSession.h"
|
||||
#include "FridaWidget.h"
|
||||
#include "core/Iaito.h"
|
||||
#include "core/MainWindow.h"
|
||||
#include "widgets/ConsoleWidget.h"
|
||||
#include "widgets/HexdumpWidget.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCheckBox>
|
||||
#include <QClipboard>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QPointer>
|
||||
#include <QSignalBlocker>
|
||||
#include <QStatusBar>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
ConsoleWidget *sessionConsole(MainWindow *main)
|
||||
{
|
||||
return main ? main->findChild<ConsoleWidget *>() : nullptr;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void IaitoFridaPlugin::setupPlugin() {}
|
||||
|
||||
void IaitoFridaPlugin::terminate()
|
||||
{
|
||||
HexdumpWidget::setCustomizeHook(nullptr);
|
||||
if (mainWindow) {
|
||||
if (ConsoleWidget *console = sessionConsole(mainWindow)) {
|
||||
console->setCommandHandler(nullptr);
|
||||
}
|
||||
for (HexdumpWidget *dump : mainWindow->findChildren<HexdumpWidget *>()) {
|
||||
if (auto *view = dump->findChild<HexWidget *>(QStringLiteral("hexTextView"))) {
|
||||
view->setDataReader({});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (session && session->isAttached()) {
|
||||
session->detach();
|
||||
}
|
||||
}
|
||||
|
||||
void IaitoFridaPlugin::setupInterface(MainWindow *main)
|
||||
{
|
||||
mainWindow = main;
|
||||
session = new FridaSession(main);
|
||||
widget = new FridaWidget(session, main);
|
||||
main->addPluginDockWidget(widget);
|
||||
widget->hide();
|
||||
|
||||
statusButton = new QToolButton(main);
|
||||
statusButton->setAutoRaise(false);
|
||||
statusButton->setToolTip(tr("r2frida session"));
|
||||
statusDetail = new QLabel(main);
|
||||
main->statusBar()->addPermanentWidget(statusButton);
|
||||
main->statusBar()->addPermanentWidget(statusDetail);
|
||||
connect(statusButton, &QToolButton::clicked, this, [this]() {
|
||||
widget->show();
|
||||
widget->raise();
|
||||
});
|
||||
connect(session, &FridaSession::changed, this, &IaitoFridaPlugin::updateStatus);
|
||||
|
||||
if (ConsoleWidget *console = sessionConsole(main)) {
|
||||
console->setCommandHandler(
|
||||
[this](const QString &command) { return handleConsoleCommand(command); });
|
||||
connect(session, &FridaSession::consoleMessage, console, &ConsoleWidget::addOutput);
|
||||
}
|
||||
HexdumpWidget::setCustomizeHook(
|
||||
[this](HexdumpWidget *hexdump) { installLiveHexdump(hexdump); });
|
||||
|
||||
QTimer::singleShot(0, this, [this, main]() { buildMenu(main); });
|
||||
buildContextMenus(main);
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
bool IaitoFridaPlugin::handleConsoleCommand(const QString &command)
|
||||
{
|
||||
const QString line = command.trimmed();
|
||||
if (!line.startsWith(QLatin1Char(':'))) {
|
||||
return false;
|
||||
}
|
||||
ConsoleWidget *console = sessionConsole(mainWindow);
|
||||
if (!console) {
|
||||
return false;
|
||||
}
|
||||
const QString frida = line.mid(1);
|
||||
if (frida.contains(QLatin1Char(';')) || frida.contains(QLatin1Char('|'))
|
||||
|| frida.contains(QLatin1Char('`'))) {
|
||||
console->addOutput(tr("That command is not sent to r2frida."));
|
||||
return true;
|
||||
}
|
||||
if (!session->isAttached()) {
|
||||
console->addOutput(tr("r2frida is not attached."));
|
||||
return true;
|
||||
}
|
||||
QPointer<ConsoleWidget> target = console;
|
||||
session->command(frida, [target](const QString &output) {
|
||||
if (target) {
|
||||
target->addOutput(output);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray IaitoFridaPlugin::readLiveBytes(uint64_t addr, int len) const
|
||||
{
|
||||
if (!session || !session->isAttached() || len <= 0) {
|
||||
return QByteArray(qMax(len, 0), '\0');
|
||||
}
|
||||
quint64 runtime = RVA_INVALID;
|
||||
if (session->containsStatic(addr)) {
|
||||
runtime = session->toRuntime(addr);
|
||||
} else if (session->containsRuntime(addr)) {
|
||||
runtime = addr;
|
||||
}
|
||||
if (runtime == RVA_INVALID) {
|
||||
return QByteArray(len, '\0');
|
||||
}
|
||||
QByteArray bytes = session->readBytes(runtime, len);
|
||||
if (bytes.size() < len) {
|
||||
bytes.append(QByteArray(len - bytes.size(), '\0'));
|
||||
} else if (bytes.size() > len) {
|
||||
bytes.truncate(len);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void IaitoFridaPlugin::installLiveHexdump(HexdumpWidget *hexdump)
|
||||
{
|
||||
if (!hexdump || hexdump->findChild<QCheckBox *>(QStringLiteral("fridaLiveToggle"))) {
|
||||
return;
|
||||
}
|
||||
auto *live = new QCheckBox(tr("Live process"), hexdump);
|
||||
live->setObjectName(QStringLiteral("fridaLiveToggle"));
|
||||
live->setToolTip(tr("Show bytes from the attached r2frida target"));
|
||||
if (QLayout *layout = hexdump->widget() ? hexdump->widget()->layout() : nullptr) {
|
||||
if (auto *box = qobject_cast<QVBoxLayout *>(layout)) {
|
||||
box->insertWidget(0, live);
|
||||
}
|
||||
}
|
||||
auto *view = hexdump->findChild<HexWidget *>(QStringLiteral("hexTextView"));
|
||||
connect(live, &QCheckBox::toggled, this, [this, live, view](bool on) {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
if (!on) {
|
||||
view->setDataReader({});
|
||||
return;
|
||||
}
|
||||
if (!session || !session->isAttached()) {
|
||||
const QSignalBlocker blocker(live);
|
||||
live->setChecked(false);
|
||||
if (ConsoleWidget *console = sessionConsole(mainWindow)) {
|
||||
console->addOutput(tr("r2frida is not attached."));
|
||||
}
|
||||
return;
|
||||
}
|
||||
view->setDataReader([this](uint64_t addr, int len) { return readLiveBytes(addr, len); });
|
||||
});
|
||||
connect(session, &FridaSession::changed, this, [this, live, view]() {
|
||||
if (!session->isAttached()) {
|
||||
if (live->isChecked()) {
|
||||
live->setChecked(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (live->isChecked() && view) {
|
||||
view->refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void IaitoFridaPlugin::buildMenu(MainWindow *main)
|
||||
{
|
||||
QMenu *debugMenu = main->getMenuByType(MainWindow::MenuType::Debug);
|
||||
if (!debugMenu) {
|
||||
return;
|
||||
}
|
||||
debugMenu->addSeparator();
|
||||
auto *menu = debugMenu->addMenu(tr("r2frida"));
|
||||
connectAction = menu->addAction(tr("Connect..."), widget, &FridaWidget::showConnectDialog);
|
||||
resumeAction = menu->addAction(tr("Resume"), session, &FridaSession::resume);
|
||||
detachAction = menu->addAction(tr("Detach"), session, &FridaSession::detach);
|
||||
menu->addSeparator();
|
||||
menu->addAction(tr("Show panel"), this, [this]() {
|
||||
widget->show();
|
||||
widget->raise();
|
||||
});
|
||||
if (!session->checkAvailable()) {
|
||||
connectAction->setEnabled(false);
|
||||
connectAction->setToolTip(
|
||||
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
|
||||
}
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
void IaitoFridaPlugin::addFridaMenu(QMenu *pluginMenu)
|
||||
{
|
||||
if (!pluginMenu) {
|
||||
return;
|
||||
}
|
||||
auto *menu = pluginMenu->addMenu(tr("Frida"));
|
||||
auto *trace = menu->addAction(tr("Trace"));
|
||||
auto *hook = menu->addAction(tr("Hook"));
|
||||
auto *breakpoint = menu->addAction(tr("Set breakpoint"));
|
||||
auto *live = menu->addAction(tr("Read live bytes"));
|
||||
auto *copy = menu->addAction(tr("Copy runtime address"));
|
||||
auto *mapping = menu->addAction(tr("Show address mapping"));
|
||||
const auto enable = [this, menu]() {
|
||||
const bool on = session->isAttached();
|
||||
for (QAction *action : menu->actions()) {
|
||||
action->setEnabled(on);
|
||||
action->setData(menu->menuAction()->data());
|
||||
}
|
||||
};
|
||||
connect(menu, &QMenu::aboutToShow, this, enable);
|
||||
connect(trace, &QAction::triggered, this, [this, trace]() {
|
||||
fridaTrace(session, trace->data().toULongLong());
|
||||
});
|
||||
connect(hook, &QAction::triggered, this, [this, hook]() {
|
||||
fridaShowHookDialog(mainWindow, session, hook->data().toULongLong());
|
||||
});
|
||||
connect(breakpoint, &QAction::triggered, this, [this, breakpoint]() {
|
||||
fridaBreakpoint(session, breakpoint->data().toULongLong());
|
||||
});
|
||||
connect(live, &QAction::triggered, this, [this, live]() {
|
||||
const quint64 addr = live->data().toULongLong();
|
||||
fridaShowLiveHex(mainWindow, session, fridaRuntimeAddress(session, addr));
|
||||
});
|
||||
connect(copy, &QAction::triggered, this, [this, copy]() {
|
||||
const quint64 runtime = fridaRuntimeAddress(session, copy->data().toULongLong());
|
||||
QApplication::clipboard()->setText(RAddressString(runtime));
|
||||
});
|
||||
connect(mapping, &QAction::triggered, this, [this, mapping]() {
|
||||
Core()->message(session->describe(mapping->data().toULongLong()));
|
||||
});
|
||||
}
|
||||
|
||||
void IaitoFridaPlugin::buildContextMenus(MainWindow *main)
|
||||
{
|
||||
addFridaMenu(main->getContextMenuExtensions(MainWindow::ContextMenuType::Disassembly));
|
||||
addFridaMenu(main->getContextMenuExtensions(MainWindow::ContextMenuType::Addressable));
|
||||
}
|
||||
|
||||
void IaitoFridaPlugin::updateStatus()
|
||||
{
|
||||
if (!statusButton) {
|
||||
return;
|
||||
}
|
||||
const bool live = session->isAttached();
|
||||
const QString color = live ? QStringLiteral("#2E7D32") : QStringLiteral("#C62828");
|
||||
statusButton->setText(tr("FRIDA"));
|
||||
statusButton->setStyleSheet(QStringLiteral(
|
||||
"QToolButton { color: white; background-color: %1; border: none;"
|
||||
" border-radius: 3px; padding: 0 6px; font-weight: 600; }")
|
||||
.arg(color));
|
||||
if (!session->checkAvailable()) {
|
||||
statusButton->setToolTip(
|
||||
tr("r2frida is not loaded. Install it with r2pm -ci r2frida and restart iaito."));
|
||||
statusDetail->clear();
|
||||
} else if (live) {
|
||||
const FridaProcessInfo info = session->process();
|
||||
QString arch = info.arch;
|
||||
if (!arch.isEmpty()) {
|
||||
arch = arch.toUpper();
|
||||
}
|
||||
statusDetail->setText(
|
||||
tr("%1 %2 [%3] %4 %5")
|
||||
.arg(
|
||||
session->deviceLabel(),
|
||||
info.name,
|
||||
QString::number(info.pid),
|
||||
arch,
|
||||
session->transportLabel().toUpper()));
|
||||
statusButton->setToolTip(tr("%1 | %2 | slide %3")
|
||||
.arg(
|
||||
session->transportLabel(),
|
||||
session->deviceLabel(),
|
||||
RAddressString(session->slide())));
|
||||
} else if (session->state() == FridaSessionState::Connecting) {
|
||||
statusButton->setToolTip(tr("r2frida is connecting"));
|
||||
statusDetail->setText(tr("connecting"));
|
||||
} else {
|
||||
statusButton->setToolTip(tr("r2frida is disconnected"));
|
||||
statusDetail->clear();
|
||||
}
|
||||
if (resumeAction) {
|
||||
resumeAction->setEnabled(session->isSuspended());
|
||||
}
|
||||
if (detachAction) {
|
||||
detachAction->setEnabled(session->isAttached());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include "IaitoPlugin.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QObject>
|
||||
|
||||
class FridaSession;
|
||||
class FridaWidget;
|
||||
class HexdumpWidget;
|
||||
class MainWindow;
|
||||
class QAction;
|
||||
class QMenu;
|
||||
class QLabel;
|
||||
class QToolButton;
|
||||
|
||||
class IaitoFridaPlugin : public QObject, public IaitoPlugin
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_PLUGIN_METADATA(IID "org.radare.iaito.plugins.IaitoPlugin")
|
||||
Q_INTERFACES(IaitoPlugin)
|
||||
|
||||
public:
|
||||
void setupPlugin() override;
|
||||
void setupInterface(MainWindow *main) override;
|
||||
void terminate() override;
|
||||
|
||||
QString getName() const override { return QStringLiteral("r2frida"); }
|
||||
QString getAuthor() const override { return QStringLiteral("radareorg"); }
|
||||
QString getDescription() const override
|
||||
{
|
||||
return QStringLiteral("Dynamic instrumentation through r2frida.");
|
||||
}
|
||||
QString getVersion() const override { return QStringLiteral("1.0"); }
|
||||
|
||||
private:
|
||||
void buildMenu(MainWindow *main);
|
||||
void buildContextMenus(MainWindow *main);
|
||||
void updateStatus();
|
||||
void addFridaMenu(QMenu *pluginMenu);
|
||||
void installLiveHexdump(HexdumpWidget *hexdump);
|
||||
bool handleConsoleCommand(const QString &command);
|
||||
QByteArray readLiveBytes(uint64_t addr, int len) const;
|
||||
|
||||
FridaSession *session = nullptr;
|
||||
FridaWidget *widget = nullptr;
|
||||
MainWindow *mainWindow = nullptr;
|
||||
QToolButton *statusButton = nullptr;
|
||||
QLabel *statusDetail = nullptr;
|
||||
QAction *connectAction = nullptr;
|
||||
QAction *detachAction = nullptr;
|
||||
QAction *resumeAction = nullptr;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
TEMPLATE = lib
|
||||
CONFIG += plugin
|
||||
CONFIG -= import_plugins
|
||||
QT += widgets svg network
|
||||
|
||||
TARGET = IaitoFridaPlugin
|
||||
DESTDIR = $$PWD/../../../build/plugins/native
|
||||
|
||||
QMAKE_CXXFLAGS += $$system(pkg-config --cflags r_core)
|
||||
QMAKE_CXXFLAGS += -std=c++20
|
||||
INCLUDEPATH += \
|
||||
$$PWD \
|
||||
$$PWD/.. \
|
||||
$$PWD/../.. \
|
||||
$$PWD/../../common \
|
||||
$$PWD/../../core \
|
||||
$$PWD/../../widgets \
|
||||
$$PWD/../../dialogs \
|
||||
$$PWD/../../menus \
|
||||
$$PWD/../../plugins \
|
||||
$$system(r2 -H R2_INCDIR)
|
||||
|
||||
macx: QMAKE_LFLAGS += -undefined dynamic_lookup
|
||||
unix:!macx: QMAKE_LFLAGS += -Wl,--unresolved-symbols=ignore-all
|
||||
|
||||
HEADERS += \
|
||||
FridaTypes.h \
|
||||
FridaUri.h \
|
||||
FridaBackend.h \
|
||||
FridaSession.h \
|
||||
FridaConnectDialog.h \
|
||||
FridaWidget.h \
|
||||
FridaPanels.h \
|
||||
IaitoFridaPlugin.h \
|
||||
../IaitoPlugin.h
|
||||
|
||||
SOURCES += \
|
||||
FridaUri.cpp \
|
||||
FridaBackend.cpp \
|
||||
FridaSession.cpp \
|
||||
FridaConnectDialog.cpp \
|
||||
FridaWidget.cpp \
|
||||
FridaPanels.cpp \
|
||||
IaitoFridaPlugin.cpp
|
||||
@@ -261,6 +261,13 @@ void ConsoleWidget::executeCommand(const QString &command)
|
||||
QString cmd_line = "[" + RAddressString(Core()->getOffset()) + "]> " + command;
|
||||
addOutput(cmd_line);
|
||||
|
||||
if (commandHandler && commandHandler(command)) {
|
||||
historyAdd(command);
|
||||
ui->r2InputLineEdit->setEnabled(true);
|
||||
ui->r2InputLineEdit->setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
RVA oldOffset = Core()->getOffset();
|
||||
bool refreshAll = consoleCommandRefreshesAll(command);
|
||||
bool isPiped = command.contains(">");
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QSocketNotifier>
|
||||
#include <QStringListModel>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
class QCompleter;
|
||||
@@ -32,6 +33,12 @@ public:
|
||||
|
||||
void setMaxHistoryEntries(int max) { maxHistoryEntries = max; }
|
||||
|
||||
// Return true to handle the command instead of sending it to radare2.
|
||||
void setCommandHandler(const std::function<bool(const QString &command)> &handler)
|
||||
{
|
||||
commandHandler = handler;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
QWidget *widgetToFocusOnRaise() override;
|
||||
@@ -86,6 +93,7 @@ private:
|
||||
void redirectOutput();
|
||||
|
||||
QSharedPointer<CommandTask> commandTask;
|
||||
std::function<bool(const QString &)> commandHandler;
|
||||
|
||||
std::unique_ptr<Ui::ConsoleWidget> ui;
|
||||
QAction *actionWrapLines;
|
||||
|
||||
@@ -924,6 +924,17 @@ void HexWidget::refresh()
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void HexWidget::setDataReader(const std::function<QByteArray(uint64_t, int)> &reader)
|
||||
{
|
||||
if (auto *memory = dynamic_cast<MemoryData *>(data.get())) {
|
||||
memory->setReader(reader);
|
||||
}
|
||||
if (auto *memory = dynamic_cast<MemoryData *>(oldData.get())) {
|
||||
memory->setReader(reader);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void HexWidget::setItemEndianess(bool bigEndian)
|
||||
{
|
||||
itemBigEndian = bigEndian;
|
||||
|
||||
+15
-1
@@ -6,6 +6,7 @@
|
||||
#include "common/SparseHexLayout.h"
|
||||
#include "dialogs/HexdumpRangeDialog.h"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <QColor>
|
||||
#include <QIcon>
|
||||
@@ -151,10 +152,19 @@ public:
|
||||
m_blocks.clear();
|
||||
uint64_t addr = alignedAddr;
|
||||
for (ut64 i = 0; i < len / blockSize; ++i, addr += blockSize) {
|
||||
m_blocks.append(Core()->ioRead(addr, blockSize));
|
||||
QByteArray block = reader ? reader(addr, int(blockSize))
|
||||
: Core()->ioRead(addr, blockSize);
|
||||
if (block.size() < int(blockSize)) {
|
||||
block.append(QByteArray(int(blockSize) - block.size(), '\0'));
|
||||
} else if (block.size() > int(blockSize)) {
|
||||
block.truncate(int(blockSize));
|
||||
}
|
||||
m_blocks.append(block);
|
||||
}
|
||||
}
|
||||
|
||||
void setReader(const std::function<QByteArray(uint64_t, int)> &next) { reader = next; }
|
||||
|
||||
bool copy(void *out, uint64_t addr, size_t len) override
|
||||
{
|
||||
if (addr < m_firstBlockAddr || addr > m_lastValidAddr
|
||||
@@ -187,6 +197,7 @@ private:
|
||||
QVector<QByteArray> m_blocks;
|
||||
uint64_t m_firstBlockAddr = 0;
|
||||
uint64_t m_lastValidAddr = 0;
|
||||
std::function<QByteArray(uint64_t, int)> reader;
|
||||
};
|
||||
|
||||
class HexSelection
|
||||
@@ -324,6 +335,9 @@ public:
|
||||
// TODO: add currentAddress
|
||||
};
|
||||
Selection getSelection();
|
||||
// When set, displayed bytes come from this reader instead of the open file.
|
||||
void setDataReader(const std::function<QByteArray(uint64_t, int)> &reader);
|
||||
|
||||
public slots:
|
||||
void seek(uint64_t address);
|
||||
void refresh();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "core/MainWindow.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <QByteArray>
|
||||
#include <QClipboard>
|
||||
#include <QCoreApplication>
|
||||
@@ -28,6 +29,15 @@
|
||||
#include <QTextOption>
|
||||
#include <QtEndian>
|
||||
|
||||
namespace {
|
||||
std::function<void(HexdumpWidget *)> hexdumpCustomize;
|
||||
}
|
||||
|
||||
void HexdumpWidget::setCustomizeHook(const std::function<void(HexdumpWidget *)> &hook)
|
||||
{
|
||||
hexdumpCustomize = hook;
|
||||
}
|
||||
|
||||
HexdumpWidget::HexdumpWidget(MainWindow *main)
|
||||
: MemoryDockWidget(MemoryWidgetType::Hexdump, main)
|
||||
, ui(new Ui::HexdumpWidget)
|
||||
@@ -260,6 +270,9 @@ HexdumpWidget::HexdumpWidget(MainWindow *main)
|
||||
make_nav("hex.navUpSelect", Qt::Key_Up, Qt::ShiftModifier);
|
||||
make_nav("hex.navRightSelect", Qt::Key_Right, Qt::ShiftModifier);
|
||||
}
|
||||
if (hexdumpCustomize) {
|
||||
hexdumpCustomize(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Event filter to catch resize events on the side tabs and refresh values
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QTextEdit>
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "HexWidget.h"
|
||||
@@ -37,6 +38,7 @@ public:
|
||||
Highlighter *highlighter;
|
||||
|
||||
static QString getWidgetType();
|
||||
static void setCustomizeHook(const std::function<void(HexdumpWidget *)> &hook);
|
||||
|
||||
public slots:
|
||||
void initParsing();
|
||||
|
||||
Reference in new issue
Block a user