Приветствую, дорогой читатель. Хочу представить вашему вниманию пример, как можно упростить себе жизнь при исследовании кода программ, используя скриптинг в Ghidra.
Если вы уже имели опыт работы с дизассемблером, то заметили, что читать его вывод не так легко, если целью является понять более высокие абстракции, заложенные в нём. Возможно, вы даже пытались декомпилировать его в псевдокод, но работать с переменными типа local_1-999 – то ещё удовольствие. Да, можно щёлкнуть на каждую из них и присвоить имя на основе логики. А что, если у вас 2000 строк и более?
Чтобы не натереть мозоль, давайте разберёмся, как написать скрипт, который сделает большую часть работы за нас.
Все манипуляции были проделаны на версии 11.1.2. Чтобы попасть в список доступных скриптов, откройте меню Window → Script Manager и там же создайте новый скрипт, нажав в правом верхнем углу кнопку Create New Script и выбрав язык Python.
Важное ограничение: Ghidra использует внутреннюю реализацию языка Python версии 2.7.
Пишем код.
Первым делом, нужно объявить кодировку, чтобы не получить кучу ошибок о наличии не-ASCI символов
Указываем, что исходный файл сохранён в кодировке UTF-8 (поддержка Unicode символов).
from ghidra.util.task import Task
Импортируем класс Task из модуля Ghidra, который позволяет создавать задачи для выполнения операций в фоне.
from ghidra.app.decompiler import DecompInterface
Импортируем интерфейс декомпилятора, позволяющий получать C-подобный код из бинарных данных.
from ghidra.util.task import ConsoleTaskMonitor
Импортируем класс ConsoleTaskMonitor для отслеживания прогресса выполнения задач с выводом в консоль.
from ghidra.program.model.symbol import SourceType, SymbolType
Импортируем типы источников и типов символов, используемые для обозначения происхождения имен (например, USER_DEFINED) и вида символа (функция, переменная, метка).
from ghidra.program.model.pcode import HighFunctionDBUtil
Импортируем утилиты для работы с высокоуровневым представлением функции (HighFunction) в базе данных Ghidra.
from ghidra.program.model.pcode.HighFunctionDBUtil import ReturnCommitOption
Импортируем опцию фиксации (commit) изменений в базе данных при обновлении параметров функции.
from java.awt import BorderLayout
Импортируем менеджер компоновки BorderLayout для организации компонентов в окне Java.
from javax.swing import JButton, JFrame, JTextArea, JScrollPane, JPanel
Импортируем стандартные Swing-компоненты: кнопку, окно, текстовую область, панель прокрутки и панель для построения GUI.
Импортируем модуль регулярных выражений для поиска и обработки строк.
class RenameDialog(JFrame):
Объявляем класс RenameDialog, наследующийся от JFrame. Он представляет окно диалога для ввода новых имён.
def __init__(self, suggestions):
Конструктор класса, принимающий список предложенных имен (suggestions) для переименования.
JFrame.__init__(self, "Advanced Renamer")
Инициализируем базовый класс JFrame, задавая заголовок окна “Advanced Renamer”.
Устанавливаем размер окна – 800 пикселей по ширине и 600 по высоте.
self.setLayout(BorderLayout())
Задаём менеджер компоновки BorderLayout для организации компонентов внутри окна.
self.text_area = JTextArea()
Создаём текстовую область, в которой пользователь сможет редактировать имена.
self.text_area.setText("# Format: old=new\n" + "\n".join(suggestions))
Заполняем текстовую область начальным текстом с примером формата и списком предложенных имён, разделённых переносами строк.
scroll_pane = JScrollPane(self.text_area)
Создаём панель прокрутки, содержащую нашу текстовую область, чтобы можно было просматривать длинный текст.
self.add(scroll_pane, BorderLayout.CENTER)
Добавляем панель прокрутки в центр окна (согласно BorderLayout).
Создаём панель для размещения кнопок в окне.
self.apply_btn = JButton("Apply", actionPerformed=lambda _: self.setVisible(False))
Создаём кнопку “Apply” с обработчиком события: при нажатии окно будет скрыто (setVisible(False)).
button_panel.add(self.apply_btn)
Добавляем кнопку “Apply” на панель кнопок.
self.add(button_panel, BorderLayout.SOUTH)
Размещаем панель кнопок в нижней части окна.
class AdvancedRenamer(Task):
Объявляем класс AdvancedRenamer, наследующийся от Task. Он отвечает за логику переименования символов в Ghidra.
def __init__(self, program, function):
Конструктор класса принимает объект программы (program) и функцию (function), над которой будет производиться переименование.
super(AdvancedRenamer, self).__init__("Advanced Renamer", True, False, True)
Вызываем конструктор базового класса Task, задавая имя задачи и некоторые флаги (например, показывать прогресс, отменяемость и т.д.).
Сохраняем ссылку на текущую программу Ghidra.
Сохраняем ссылку на функцию, которую собираемся анализировать и переименовывать.
self.monitor = ConsoleTaskMonitor()
Создаем экземпляр монитора задач, который выводит статус выполнения в консоль.
self.skipped = {'int', 'char', 'void', 'return', 'break', 'float'}
Определяем множество ключевых слов, которые не будут изменяться при переименовании (например, базовые типы и управляющие конструкции).
def find_and_rename(self, old_name, new_name):
Определяем метод для поиска символа с именем old_name и его переименования в new_name.
decompiler = DecompInterface()
Создаём экземпляр интерфейса декомпилятора.
decompiler.openProgram(self.program)
Открываем текущую программу в декомпиляторе для дальнейшей работы.
results = decompiler.decompileFunction(self.function, 60, self.monitor)
Декомпилируем функцию с таймаутом 60 секунд, используя монитор для отслеживания прогресса.
if results.decompileCompleted():
Проверяем, успешно ли завершилась декомпиляция.
hfunction = results.getHighFunction()
Получаем высокоуровневое представление функции (HighFunction) из результатов декомпиляции.
signatureSrcType = self.function.getSignatureSource()
Получаем тип источника сигнатуры функции для дальнейшего обновления базы данных.
HighFunctionDBUtil.commitParamsToDatabase(hfunction, True, ReturnCommitOption.COMMIT, signatureSrcType)
Фиксируем изменения параметров функции в базе данных, используя указанные опции. Это необходимо для того, чтобы локальные имена, такие как lVar1, uVar2, pVar3, были согласованы с базой данных, потому, что они генерируются самим декомпилятором и просто выводятся на экран, без коммита в базу.
if old_name.startswith("FUN_"):
Если имя символа начинается с “FUN_”, считаем его именем функции.
return self.rename_function(old_name, new_name)
Вызываем метод для переименования функции.
elif old_name.startswith(("DAT_", "PTR_", "UNK_", "LAB_")):
Если имя начинается с “DAT_”, “PTR_”, “UNK_” или “LAB_”, обрабатываем его как метку или данные. Покаяние: У меня так и не получилось дать ума PTR_ и UNK_, хоть это и глобальные имена, как DAT_ и LAB_, они не переименовываются данной функцией, но я оставил их как есть :з
return self.rename_label(old_name, new_name)
Вызываем метод для переименования метки.
elif old_name.startswith(("local_", "param_", "uVar", "lVar")):
Если имя соответствует шаблону локальной переменной или параметра, то…
return self.rename_local_variable(old_name, new_name)
… вызываем метод для переименования локальной переменной.
Если ни одно из условий не выполнено, по умолчанию обрабатываем как локальную переменную.
return self.rename_local_variable(old_name, new_name)
Вызываем метод для переименования локальной переменной.
def rename_function(self, old_name, new_name):
Определяем метод, который переименовывает функцию с именем old_name в new_name.
Начинаем блок обработки исключений, чтобы избежать сбоев при ошибках.
addr_str = old_name[4:] if old_name.startswith("FUN_") else old_name
Если имя начинается с “FUN_”, удаляем этот префикс, чтобы получить строку адреса функции.
addr = toAddr("0x{}".format(addr_str))
Преобразуем строку с адресом в объект адреса Ghidra, добавляя префикс “0x”.
func = getFunctionAt(addr)
Получаем объект функции, расположенной по данному адресу.
if func and func.getName() == old_name:
Если функция найдена и её имя соответствует old_name, то…
func.setName(new_name, SourceType.USER_DEFINED)
… устанавливаем новое имя для функции, указывая, что оно задано пользователем (USER_DEFINED).
Если возникает ошибка (например, неверный адрес), просто игнорируем её.
def rename_label(self, old_name, new_name):
Определяем метод для переименования метки или символа, представляющего данные.
Начинаем блок обработки исключений.
addr_str = old_name[4:] if old_name.startswith(("LAB_", "DAT_", "PTR_", "UNK_")) else old_name
Извлекаем адрес метки, убирая префикс (например, “LAB_”) если он присутствует.
Преобразуем строку адреса в объект адреса Ghidra.
for sym in self.program.getSymbolTable().getSymbols(addr):
Перебираем все символы, зарегистрированные по этому адресу, из таблицы символов программы.
if sym.getName() == old_name:
Если имя символа совпадает с old_name, то…
sym.setName(new_name, SourceType.USER_DEFINED)
… устанавливаем новое имя для символа с типом источника USER_DEFINED.
Возвращаем True, указывая, что переименование прошло успешно.
При возникновении исключения игнорируем его.
def rename_local_variable(self, old_name, new_name):
Определяем метод для переименования локальной переменной или параметра.
Начинаем блок обработки исключений.
if new_name.lower() in self.skipped:
Если новое имя (в нижнем регистре) содержится в списке ключевых слов для пропуска, то…
… прекращаем переименование, возвращая False.
# Далее идет блок, необходимый для обработки параметров
for param in self.function.getParameters():
Перебираем все параметры текущей функции.
if param.getName() == old_name:
Если имя параметра совпадает с old_name, то…
param.setName(new_name, SourceType.USER_DEFINED)
… устанавливаем новое имя для параметра.
# Local variables (даже если наш пациент был найдет среди параметров
# необходимо пройтись и по локальным переменным,
# иначе они не переименовываются, возможно надо тут просто
decompiler = DecompInterface()
Создаём новый экземпляр декомпилятора для обработки локальных переменных.
decompiler.openProgram(self.program)
Открываем программу в декомпиляторе.
results = decompiler.decompileFunction(self.function, 60, self.monitor)
Декомпилируем функцию с таймаутом 60 секунд.
if results.decompileCompleted():
Если декомпиляция прошла успешно, то…
hfunction = results.getHighFunction()
… получаем высокоуровневое представление функции.
syms = hfunction.getLocalSymbolMap().getSymbols()
Получаем список локальных символов (переменных) из высокоуровневой функции.
Перебираем каждый локальный символ.
if sym.getName() == old_name and sym.getName() != new_name:
Если имя символа совпадает с old_name и ещё не равно new_name, то…
HighFunctionDBUtil.updateDBVariable(sym, new_name, None, SourceType.USER_DEFINED)
… обновляем имя переменной в базе данных с новым именем и источником USER_DEFINED.
Возвращаем True после успешного переименования локальной переменной.
Если ни один из блоков не сработал, возвращаем False – переименование не выполнено.
Ловим исключения и сохраняем их в переменной e для отладки.
print("Error: {} -> {} ({})".format(old_name, new_name, str(e)))
Выводим сообщение об ошибке с указанием старого и нового имени, а также текста ошибки. Обратите внимание на форматирование строки, т.к. это Python 2.7.
Возвращаем False, сигнализируя, что произошла ошибка при переименовании.
Определяем метод run, который является точкой входа при выполнении задачи AdvancedRenamer.
decompiler = DecompInterface()
Создаем экземпляр декомпилятора для работы с функцией.
decompiler.openProgram(self.program)
Открываем программу в декомпиляторе.
results = decompiler.decompileFunction(self.function, 60, self.monitor)
Декомпилируем функцию с таймаутом 60 секунд.
if not results.decompileCompleted():
Если декомпиляция не завершилась успешно, то…
print("Decompilation failed!")
… выводим сообщение об ошибке декомпиляции.
Прерываем выполнение метода run (задача не может продолжаться без декомпиляции).
code = results.getDecompiledFunction().getC()
Получаем декомпилированный C-подобный код функции в виде строки.
entities = re.findall(r'\b([A-Za-z_][A-Za-z0-9_]*)\b', code)
С помощью регулярного выражения находим все идентификаторы (слова, начинающиеся с буквы или подчёркивания) в коде.
filtered_names = [n for n in set(entities) if n not in self.skipped]
Создаём множество уникальных идентификаторов и исключаем те, что присутствуют в self.skipped (ключевые слова).
dialog = RenameDialog(filtered_names)
Создаем диалоговое окно RenameDialog, передавая список найденных имён для потенциального переименования.
dialog.setLocationRelativeTo(None)
Устанавливаем расположение диалога по центру экрана (None означает центр относительно родительского окна).
Делаем диалог видимым – ожидаем, пока пользователь внесёт изменения и нажмёт “Apply”.
while dialog.isVisible(): pass
Активно ждём, пока окно не будет закрыто (пользователь не закончит ввод).
Инициализируем счётчик успешно переименованных символов.
for line in dialog.text_area.getText().split('\n'):
Перебираем каждую строку из текста, введённого пользователем в текстовой области диалога.
Удаляем пробелы в начале и конце строки.
if line and '=' in line and not line.startswith('#'):
Если строка не пуста, содержит символ “=” и не является комментарием (не начинается с “#”), то…
old, new = line.split('=', 1)
Разбиваем строку на две части по первому символу “=”, где левая часть – старое имя, правая – новое.
if self.find_and_rename(old.strip(), new.strip()):
Вызываем метод find_and_rename с очищенными от пробелов старыми и новыми именами; если переименование прошло успешно, то…
… увеличиваем счётчик успешных переименований.
print("Successfully renamed: {}/{}".format(success, len(filtered_names)))
Выводим итоговое сообщение о том, сколько из найденных идентификаторов было успешно переименовано.
if __name__ == "__main__":
Стандартная проверка: выполняется ли скрипт как главный модуль.
func = getFunctionContaining(currentAddress)
Получаем функцию, в которой находится текущий адрес курсора в Ghidra.
Если функция найдена, то…
task = AdvancedRenamer(currentProgram, func)
Создаем экземпляр задачи AdvancedRenamer, передавая текущую программу и найденную функцию.
Запускаем выполнение задачи переименования.
Если функция не найдена (курсор не находится внутри функции), то…
print("Position cursor inside function!")
… выводим сообщение с просьбой установить курсор внутри функции для корректной работы скрипта.
Подготовка.
Когда все готово, можем пойти в любую нейросеть. По-моему мнению самая лучшая на сегодняшний день для данной задачи будет DeepSeek-r1. Выверенный опытным путем промпт.
Прокомментируй каждую строку и переименнуй переменные и функции нормально. Саму структуру кода оставь, как есть, не сокращая и не меняя, то есть никаких "и так далее" - пиши код полностью! Все имена должны быть подробными, то есть не tmp и прочее, никаких сокращенных бысмысленных имен. Опиши к чему пренадлежат переменные, не просто windowStruct, а UIState как напимер. Так же переименую DAT_ как g_..новоеИмя, и LABEL_куда прыгаем {НАШ_ПСЕВДО_КОД} после кода выведи все, что переименовал в формате списка старое_имя=новое_имя\n в список включай все переменные, функции, DAT_, LAB_, param_ и прочее без исключений прям все пиши
Выбираем цель, для эксперимента
В итоге, у меня получился вот такой вот список для моей функции
bVar1=isCursorConfinedDueToCapture
cVar3=isCursorCapturedResult
local_res8=clientTopLeftPoint
local_res10=clientBottomRightPoint
local_38=confinedCursorRect
Запускаем!!
После запуска скрипта (при условии, что курсор установлен внутри функции) откроется диалоговое окно, в котором будут выведены найденные идентификаторы. Введите список переименований в формате:
ВАЖНО!!! Переменные по типу xVarN нужно сортировать по убыванию, так как они не внесены в базу. Переименование меньшей переменной приведёт к тому, что большая займёт её место, и список станет невалидным.
После ввода нажмите кнопку Apply и дождитесь окончания выполнения.
(метод подсчета требует доработки)
В итоге должно получиться нечто подобное:
Спасибо за внимание! Скачать PDF и сам скрипт можно по ссылке – https://t.me/osiechan/62 А начать путь в реверс-инжиниринге можно на увлекательном бесплатном курсе по ботостроению для ММОРПГ – https://t.me/osiechan/41