Compare commits

...
Author SHA1 Message Date
Murphy 18d51daa6a Route r2frida commands and live memory through the native views.
The extra scripts, console, and hexdump tabs are removed so : commands and process bytes use the bottom console and the Hexdump widget.
2026-10-08 17:23:32 +02:00
Murphy f20ddbc357 Group r2frida views under one dock and sign the macOS plugin.
The session views are tabs on the r2frida panel, and the dylib is re-signed so macOS does not kill iaito when loading it.
2026-10-08 17:03:00 +02:00
Murphy c47985f1b0 WIP: Add an r2frida plugin that keeps the static session open.
Connect and attach through a second file descriptor so Frida does not replace the binary already under analysis.
2026-10-08 16:37:06 +02:00
23 changed files with 2867 additions and 2 deletions

No files matched your search

+20 -1
View File
@@ -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
+273
View File
@@ -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;
}
+53
View File
@@ -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_;
};
+344
View File
@@ -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();
}
+51
View File
@@ -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;
};
+511
View File
@@ -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;
}
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <QList>
class FridaSession;
class QWidget;
QList<QWidget *> createFridaPanels(FridaSession *session);
+501
View File
@@ -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);
}
}
+95
View File
@@ -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;
};
+63
View File
@@ -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;
};
+89
View File
@@ -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;
}
+18
View File
@@ -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);
};
+345
View File
@@ -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 &current)
{
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;
}
+45
View File
@@ -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 &current);
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);
+298
View File
@@ -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());
}
}
+53
View File
@@ -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;
};
+44
View File
@@ -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
+7
View File
@@ -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(">");
+8
View File
@@ -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;
+11
View File
@@ -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
View File
@@ -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();
+13
View File
@@ -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
+2
View File
@@ -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();