一、说明
对于 IDA MacOS 版本的汉化统属个人研究,主要方向是 MacOS 平台 QT 三方库的关键方法处理。对于新手,汉化能够降低界面功能的理解成本,有一定帮助;对于已熟练使用 IDA 的朋友,汉化没有任何帮助,甚至会带来负作用,比如某些功能不能第一时间找到了。
免责声明:
1.本补丁仅供学习和研究用途,不得用于任何非法行为。不得用做商业用途。商业使用请购买官方正版。
2.使用本补丁时,使用者应自行承担风险,开发者不对因使用者使用本补丁而造成的任何损失负责。
3. 若侵犯他人权益或违反相关法律法规,请停止使用并立即删除补丁,责任由使用者自行承担,开发者不承担任何责任。请在合法合规的前提下使用本补丁。
二、探索
对于 IDA 9.2,52pojie 已发资源:[Disassemblers] IDA Pro 9.2 leak
同时也已有 Windows 平台的汉化补丁:[原创汉化] IDA 9.2 汉化补丁,我使用的汉化翻译也是在这个基础上做了少量补充。
2.1 IDA 的界面库
安装完 IDA 9.2 后,查看 Frameworks 目录,可以看到其引用的 QT 界面库。
目录: '/Applications/IDA Professional 9.2.app/Contents/Frameworks'
通过查看 Frameworks 中dylib 的版本信息得知,界面库版本是 QT 6.8.2。
otool -L '/Applications/IDA Professional 9.2.app/Contents/Frameworks/QtCore.framework/Versions/A/QtCore'
2.2 汉化思路
到目前我想到了两种思路:
1、下载 QT 6.8.2 源代码,关键位置加入汉化相关方法(加载.qm 文件,添加 tr 功能),编译完成后替换 IDA 中的 framework。
2、开发 Hook QT 关键方法的 dylib ,使用 insert_dylib 插入到 IDA 程序,实现汉化补丁。
三、思路一:QT 6.8.2 编译(失败的尝试)
3.1 源代码下载
完整源代码:
https://download.qt.io/official_releases/qt/6.8/6.8.2/single/
代码压缩包:qt-everywhere-src-6.8.2.zip
针对本次尝试,只下载qtbase 即可,可省不少下载时间:
https://download.qt.io/official_releases/qt/6.8/6.8.2/submodules/
代码压缩包:qtbase-everywhere-src-6.8.2.zip
3.2 编译 QT
3.2.1 MacOS 环境准备
brew install cmake ninja pkg-config freetype fontconfig libpng libjpeg
3.2.2 cmake 配置
cd qtbase
./configure \
-prefix /Users/sdc1992/Qt/6.8.2/ \
-opensource -confirm-license \
-nomake tests -nomake examples \
-- \
-G Ninja \
-DQT_NAMESPACE=QT \
-DQT_FEATURE_gui=ON \
-DQT_FEATURE_clipboard=ON \
-DQT_FEATURE_framework=ON \
-DQT_FEATURE_opengl=ON \
-DQT_FEATURE_opengl_desktop=ON \
-DQT_FEATURE_draganddrop=ON \
-DBUILD_SHARED_LIBS=ON \
-DFEATURE_widgets=ON \
-DFEATURE_x86intrin=OFF \
-DQT_FORCE_FEATURE_x86intrin=OFF \
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
-DCMAKE_OSX_SYSROOT=$(xcrun --sdk macosx --show-sdk-path) \
-DOPENSSL_ROOT_DIR=$(brew --prefix openssl)
如果未安装 openssl,还要提前用 brew 安装,未安装 macos sdk,需要安装 xcode 并下载。
在高版本 MacOS 编译,会报错找不到AGL.framework,需要提前搜索并删除掉源代码中的相关编译配置。
3.2.3 开始编译:
cmake --build . --parallel
编译完成后在 lib 目录可以看到对应的 framework:
复制 QtGui、QtCore、QtWidgets .framework/Versions/A/中对应的二进制文件替换 IDA 中的同名文件即可。
如果修改源友后要重新编译,先编译清理:
cmake --build . --target clean
ninja clean
3.3 源码修改:
在qtbase/src/gui/kernel/qguiapplication.cpp插入加载.qm 文件的逻辑:
...
static QTranslator *ida_guiTranslator = nullptr; // 这里
QGuiApplicationPrivate::QGuiApplicationPrivate(int &argc, char **argv)
: QCoreApplicationPrivate(argc, argv),
inputMethod(nullptr),
lastTouchType(QEvent::TouchEnd),
ownGlobalShareContext(false)
...
QGuiApplicationPrivate::QGuiApplicationPrivate(int &argc, char **argv)
: QCoreApplicationPrivate(argc, argv),
inputMethod(nullptr),
lastTouchType(QEvent::TouchEnd),
ownGlobalShareContext(false)
{
self = this;
application_type = QCoreApplicationPrivate::Gui;
#ifndef QT_NO_SESSIONMANAGER
is_session_restored = false;
is_saving_session = false;
#endif
{
static bool loaded = false;
if (!loaded) {
loaded = true;
ida_guiTranslator = new QTranslator();
const QString locale = QLocale::system().name();
QString appDir = QFileInfo(QString::fromLocal8Bit(argv[0])).absolutePath();
const QString defaultPath = appDir + QStringLiteral("/translations");
QString path = qEnvironmentVariable("IDA_TRANSLATIONS_PATH", defaultPath);
QString filename = qEnvironmentVariable("IDA_TRANSLATION_FILENAME", QStringLiteral("ida_%1.qm").arg(locale));
if (ida_guiTranslator->load(filename, path)) {
qInfo() << "[IDA] Loaded translation file:" << path + "/" + filename;
} else {
delete ida_guiTranslator;
ida_guiTranslator = nullptr;
qWarning() << "[IDA] Failed to load translation file:" << path + "/" + filename;
}
}
}
}
void Q_TRACE_INSTRUMENT(qtgui) QGuiApplicationPrivate::init()
{
Q_TRACE_SCOPE(QGuiApplicationPrivate_init);
...
#endif // QT_CONFIG(library)
// trigger changed signal and event delivery
QGuiApplication::setLayoutDirection(layout_direction);
if (!QGuiApplicationPrivate::displayName)
QObject::connect(q, &QGuiApplication::applicationNameChanged,
q, &QGuiApplication::applicationDisplayNameChanged);
if (ida_guiTranslator) {
QCoreApplication::installTranslator(ida_guiTranslator);
qInfo() << "[IDA] Translator installed.";
}
}
重新编译后替换对应的 QtGui 库文件,运行 IDA 查看效果:
.qm 文件生成
ida_zh_CN.ts
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="zh_CN" sourcelanguage="en_US">
<context>
<name>IDA</name>
<message>
<source>OK</source>
<translation>确定</translation>
</message>
<message>
<source>Reset</source>
<translation>还原</translation>
</message>
<message>
<source>Cancel</source>
<translation>取消</translation>
</message>
</context>
</TS>
/opt/homebrew/Cellar/qttools/6.9.3/bin/lrelease ida_zh_CN.ts -qm ida_zh_CN.qm
接下来尝试替换一些控件中的 setText 方法,植入翻译逻辑:
qtbase/src/widgets/widgets/qabstractbutton.cpp
void QAbstractButton::setText(const QString &text)
{
qInfo() << "[QAbstractButton::setText]" << text;
Q_D(QAbstractButton);
QString trText = text;
trText = tr(trText.toUtf8().constData());
if (d->text == trText)
return;
d->text = trText;
#ifndef QT_NO_SHORTCUT
QKeySequence newMnemonic = QKeySequence::mnemonic(trText);
setShortcut(newMnemonic);
#endif
d->sizeHint = QSize();
update();
updateGeometry();
#if QT_CONFIG(accessibility)
QAccessibleEvent event(this, QAccessible::NameChanged);
QAccessible::updateAccessibility(&event);
#endif
}
编译后替换 QtWidgets 库文件,运行 IDA:
`使用 QT 源代码编译的 QtWidgets 库中确少一些导出符号,会导致 IDA crash 无法运行,比如:
Termination Reason: Namespace DYLD, Code 4, Symbol missing
Symbol not found: __ZN2QT17qt_tlw_for_windowEPNS_7QWindowE
需要在源码中添加修复。这个过程比较长,一点点的尝试,不在此处赘述。`
可以看到显示了设置的英文文本,但界面显示出现了异常:
到这里基本上可以确定,IDA 是在 Qt 的源代码做了一些修改,直接编译 Qt 这条路子应该行不通了,即便是可以汉化界面,界面也无法正常使用。
四、思路二:使用 dylib Hook 实现翻译
4.1 使用 tinyhook
tinyhook 源码下载:
https://github.com/Antibioticss/tinyhook/tree/main
编译:
cd tinyhook
chmod +x build.sh
./build.sh
复制 libtinyhook.a、tinyhook.h 到要开发的 hook dylib 源代码目录。
项目文件结构:
4.2 开发 Hook 测试代码
参考:https://www.52pojie.cn/thread-2064273-1-1.html
// hook_settext.cpp
#include "tinyhook.h"
#include <map>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dlfcn.h>
#include <unistd.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include <QDebug>
#include <QtGui/QAction>
#include <QtWidgets/QLabel>
#include <QtWidgets/QAbstractButton>
#include <QtCore/QString>
#include <QtCore/QFile>
#include <QtCore/QIODevice>
#include <QtCore/QDir>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QCoreApplication>
#include <QtCore/QFileInfo>
#include <QtCore/QRegularExpression>
#define HOOK_INSTANCEM(cls, sel, imp) method_setImplementation(class_getInstanceMethod(objc_getClass(cls), sel_registerName(sel)),(IMP)imp)
#define HOOK_CLASSM(cls, sel, imp) method_setImplementation(class_getClassMethod(objc_getClass(cls), sel_registerName(sel)),(IMP)imp)
using namespace std;
// ---------- 1) 全局翻译表 ----------
static std::map<QString, QString> g_translationMap;
static bool g_translationsLoaded = false;
// ---------- 2) 声明原函数的类型 ----------
void (*origin_QLabel_setText)(QLabel*, const QString&);
// ---------- 3) 从 JSON 文件加载翻译表 ----------
void loadTranslations(const QString& jsonPath)
{
QFile file(jsonPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "无法打开翻译文件:" << jsonPath;
return;
}
QByteArray data = file.readAll();
file.close();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
qWarning() << "JSON 解析错误:" << err.errorString();
return;
}
if (!doc.isObject()) {
qWarning() << "翻译文件格式错误:顶层不是对象";
return;
}
QJsonObject obj = doc.object();
g_translationMap.clear();
for (auto it = obj.begin(); it != obj.end(); ++it) {
g_translationMap.insert({ it.key(), it.value().toString() });
}
qInfo() << "[IDA] 翻译表加载完成,条目数:" << g_translationMap.size();
}
// 获取程序目录,以便载入翻译文件
QString getExeDir() {
char path[PATH_MAX];
ssize_t len = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (len != -1) {
path[len] = '\0';
QFileInfo fi(QString::fromUtf8(path));
return fi.absolutePath();
} else {
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
QFileInfo fi(QString::fromUtf8(path));
return fi.absolutePath();
}
}
return QDir::currentPath(); // fallback
}
void ensureTranslationsLoaded() {
if (!g_translationsLoaded) {
QString exeDir = getExeDir();
QString jsonPath = QDir(exeDir).filePath("ida_translations.json");
loadTranslations(jsonPath);
g_translationsLoaded = true;
}
}
bool containsChinese(const QString& txt) {
static QRegularExpression re(QStringLiteral("[\\x{4E00}-\\x{9FFF}]"));
return re.match(txt).hasMatch();
}
// ---------- 4) 我们的替换实现 ----------
void my_QLabel_setText(QLabel* self, const QString& txt) {
ensureTranslationsLoaded();
// 在这里替换文本(示例:把所有文本转成大写,或查找翻译表)
QString newTxt = txt;
// 查表替换
auto it = g_translationMap.find(txt);
if (it != g_translationMap.end()) {
newTxt = it->second;
} else {
if (!containsChinese(txt)) {
qInfo() << "[QLabel]:" << txt;
}
}
// 调用原始实现(如果已经获取)
if (origin_QLabel_setText) {
origin_QLabel_setText(self, newTxt);
} else {
// 原函数未设置:尝试查找符号并直接调用(备用)
qWarning() << "[QLabel] 原函数未设置:尝试查找符号并直接调用";
// 这里不做复杂处理;pragmatic fallback
}
}
extern void QLabel_setText(QLabel*, const QString&) asm("__ZN2QT6QLabel7setTextERKNS_7QStringE");
__attribute__((constructor(0)))
void load() {
// Inline Hook
tiny_hook((void *)QLabel_setText, (void *)my_QLabel_setText, (void **)&origin_QLabel_setText);
return;
}
其中导出符号的名称需要使用以下命令从 IDA 的 framework 中查看:
nm -gU '/Applications/IDA Professional 9.2.app/Contents/Frameworks/QtGui.framework/Versions/A/QtGui' > ~/Downloads/gui.txt
nm -gU ''/Applications/IDA Professional 9.2.app/Contents/Frameworks/QtWidgets.framework/Versions/A/QtWidgets'' > ~/Downloads/widgets.txt
...
4.3 编写 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(IdaTranslateLib LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Gui Core Widgets)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 指定生成动态库,而不是可执行程序
add_library(IdaTranslateLib SHARED
ida_translate.cpp
ida_translate.h
)
# 链接 Qt6 Widgets 模块
target_link_libraries(IdaTranslateLib PRIVATE
Qt6::Gui
Qt6::Core
Qt6::Widgets
${CMAKE_SOURCE_DIR}/libtinyhook.a
)
# 可选:指定安装输出目录
set_target_properties(IdaTranslateLib PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
# macOS 特有:生成 .dylib 而不是 .so
set_target_properties(IdaTranslateLib PROPERTIES
SUFFIX ".dylib"
)
4.4 编译 dylib
安装 qt 依赖库:
构建安装 QT,参考上文。
cmake . -DCMAKE_PREFIX_PATH=/Users/sdc1992/Qt/6.8.2/
替换自动生成的 CMakeFiles/IdaTranslateLib.dir/link.txt 中的-framework AGL为空。
cmake --build .
编译产物在 lib 目录:
libIdaTranslateLib.dylib
4.5 汉化效果验证
仅验证: button、label、action,其它控件后续添加
将其转换为 json 文件,然后放在IDA Contents/MacOS 目录:
执行命令:
cd '/Applications/IDA Professional 9.2.app/'
DYLD_INSERT_LIBRARIES=~/Documents/project/c/ida-translation/lib/libIdaTranslateLib.dylib ./Contents/MacOS/ida
五、使用 insert_dylib 将 dylib 加载命令插入 ida
为了方便使用,可以使用 insert_dylib 将 libIdaTranslateLib.dylib 永久插入 IDA,实现启动加载。
代码仓库:https://github.com/tyilo/insert_dylib
克隆到本地后使用 xcode 打开并构建:
打开产物目录:
可以将 insert_dylib 复制到 /usr/local/bin 方便使用,也可以使用绝对路径调用。
dylib 加载命令插入 ida:
insert_dylib @rpath/libIdaTranslateLib.dylib '/Applications/IDA Professional 9.2.app/Contents/MacOS/ida' --inplace --strip-codesig
重新签名app
codesign --force --deep --sign - --timestamp=none /Applications/IDA\ Professional\ 9.2.app
需要注意你的 IDA 安装目录不同,insert_dylib 后面的路径是要修改的。
六、使用说明
6.1 “重要声明”弹窗
首次运行时会弹“重要声明”(ida 目录中写了个.disclaimer文件,有这个就不再弹了)
6.2 产物说明
目前只在 Apple Silicon 机器上构建了 arm64 产物,intel 机器上的 x64 产物待构建。附件解压后 libIdaTranslateLib.dylib、ida_translations.json 放到'/Applications/IDA Professional 9.2.app/Contents/MacOS' 目录。使用 insert_dylib 修改后重新签名即可。
6.3 适配版本
只测试了 IDA 9.2,理论上其它版本不适用,因为使用的 QT 版本不同,没有做测试,不知道结果。但整个方案是通用的,可自己去尝试。
6.4 汉化点
目前只做了 QT 下面这些方法的 Hook,当然是不全的,后面再补充吧。
QLabel.setText
QAbstractButton.setText
QAction.setText
QMessageBox.setWindowTitle
QGraphicsWidget.setWindowTitle
QWidget.setWindowTitle
QWidget.setStatusTip
QWidget.setWhatsThis
QMenu.setTitle
QGroupBox.setTitle
QTabBar.setTabText
QTabWidget.setTabText
6.5 源代码
有时间了会放 GitHub,年底没时间,后面有时间补上。着急的可参考上文自己写(大部分代码我也是用 ChatGPT 帮助写的)。