enter image description here
enter image description here

Приветствую, дорогой читатель. Хочу представить вашему вниманию пример, как можно упростить себе жизнь при исследовании кода программ, используя скриптинг в Ghidra.

Если вы уже имели опыт работы с дизассемблером, то заметили, что читать его вывод не так легко, если целью является понять более высокие абстракции, заложенные в нём. Возможно, вы даже пытались декомпилировать его в псевдокод, но работать с переменными типа local_1-999 – то ещё удовольствие. Да, можно щёлкнуть на каждую из них и присвоить имя на основе логики. А что, если у вас 2000 строк и более?

Чтобы не натереть мозоль, давайте разберёмся, как написать скрипт, который сделает большую часть работы за нас.

Все манипуляции были проделаны на версии 11.1.2. Чтобы попасть в список доступных скриптов, откройте меню Window → Script Manager и там же создайте новый скрипт, нажав в правом верхнем углу кнопку Create New Script и выбрав язык Python.

Важное ограничение: Ghidra использует внутреннюю реализацию языка Python версии 2.7.

Пишем код.

Первым делом, нужно объявить кодировку, чтобы не получить кучу ошибок о наличии не-ASCI символов

# -*- coding: utf-8 -*-

Указываем, что исходный файл сохранён в кодировке 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.

import re

Импортируем модуль регулярных выражений для поиска и обработки строк.

class RenameDialog(JFrame):

Объявляем класс RenameDialog, наследующийся от JFrame. Он представляет окно диалога для ввода новых имён.

    def __init__(self, suggestions):

Конструктор класса, принимающий список предложенных имен (suggestions) для переименования.

        JFrame.__init__(self, "Advanced Renamer")

Инициализируем базовый класс JFrame, задавая заголовок окна “Advanced Renamer”.

        self.setSize(800, 600)

Устанавливаем размер окна – 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).

        button_panel = JPanel()

Создаём панель для размещения кнопок в окне.

        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, задавая имя задачи и некоторые флаги (например, показывать прогресс, отменяемость и т.д.).

        self.program = program

Сохраняем ссылку на текущую программу Ghidra.

        self.function = function

Сохраняем ссылку на функцию, которую собираемся анализировать и переименовывать.

        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)

… вызываем метод для переименования локальной переменной.

        else:

Если ни одно из условий не выполнено, по умолчанию обрабатываем как локальную переменную.

            return self.rename_local_variable(old_name, new_name)

Вызываем метод для переименования локальной переменной.


    def rename_function(self, old_name, new_name):

Определяем метод, который переименовывает функцию с именем old_name в new_name.

        try:

Начинаем блок обработки исключений, чтобы избежать сбоев при ошибках.

            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).

        except: pass

Если возникает ошибка (например, неверный адрес), просто игнорируем её.


    def rename_label(self, old_name, new_name):

Определяем метод для переименования метки или символа, представляющего данные.

        try:

Начинаем блок обработки исключений.

            addr_str = old_name[4:] if old_name.startswith(("LAB_", 
                                                            "DAT_", 
                                                            "PTR_", 
                                                            "UNK_")) 
                                    else old_name

Извлекаем адрес метки, убирая префикс (например, “LAB_”) если он присутствует.

            addr = toAddr(addr_str)

Преобразуем строку адреса в объект адреса 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.

                    return True

Возвращаем True, указывая, что переименование прошло успешно.

        except: pass

При возникновении исключения игнорируем его.


    def rename_local_variable(self, old_name, new_name):

Определяем метод для переименования локальной переменной или параметра.

        try:

Начинаем блок обработки исключений.

            if new_name.lower() in self.skipped:

Если новое имя (в нижнем регистре) содержится в списке ключевых слов для пропуска, то…

                return False

… прекращаем переименование, возвращая 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()

Получаем список локальных символов (переменных) из высокоуровневой функции.

                for sym in syms :

Перебираем каждый локальный символ.

                    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.

                        return True

Возвращаем True после успешного переименования локальной переменной.

            return False

Если ни один из блоков не сработал, возвращаем False – переименование не выполнено.

        except Exception as e:

Ловим исключения и сохраняем их в переменной e для отладки.

            print("Error: {} -> {} ({})".format(old_name, new_name, str(e)))

Выводим сообщение об ошибке с указанием старого и нового имени, а также текста ошибки. Обратите внимание на форматирование строки, т.к. это Python 2.7.

            return False

Возвращаем False, сигнализируя, что произошла ошибка при переименовании.


    def run(self):

Определяем метод run, который является точкой входа при выполнении задачи AdvancedRenamer.

        decompiler = DecompInterface()

Создаем экземпляр декомпилятора для работы с функцией.

        decompiler.openProgram(self.program)

Открываем программу в декомпиляторе.

        results = decompiler.decompileFunction(self.function, 60, self.monitor)

Декомпилируем функцию с таймаутом 60 секунд.

        if not results.decompileCompleted():

Если декомпиляция не завершилась успешно, то…

            print("Decompilation failed!")

… выводим сообщение об ошибке декомпиляции.

            return

Прерываем выполнение метода 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 означает центр относительно родительского окна).

        dialog.setVisible(True)

Делаем диалог видимым – ожидаем, пока пользователь внесёт изменения и нажмёт “Apply”.

        while dialog.isVisible(): pass

Активно ждём, пока окно не будет закрыто (пользователь не закончит ввод).

        success = 0

Инициализируем счётчик успешно переименованных символов.

        for line in dialog.text_area.getText().split('\n'):

Перебираем каждую строку из текста, введённого пользователем в текстовой области диалога.

            line = line.strip()

Удаляем пробелы в начале и конце строки.

            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 с очищенными от пробелов старыми и новыми именами; если переименование прошло успешно, то…

                    success += 1

… увеличиваем счётчик успешных переименований.

        print("Successfully renamed: {}/{}".format(success, len(filtered_names)))

Выводим итоговое сообщение о том, сколько из найденных идентификаторов было успешно переименовано.


if __name__ == "__main__":

Стандартная проверка: выполняется ли скрипт как главный модуль.

    func = getFunctionContaining(currentAddress)

Получаем функцию, в которой находится текущий адрес курсора в Ghidra.

    if func:

Если функция найдена, то…

        task = AdvancedRenamer(currentProgram, func)

Создаем экземпляр задачи AdvancedRenamer, передавая текущую программу и найденную функцию.

        task.run()

Запускаем выполнение задачи переименования.

    else:

Если функция не найдена (курсор не находится внутри функции), то…

        print("Position cursor inside function!")

… выводим сообщение с просьбой установить курсор внутри функции для корректной работы скрипта.


Подготовка.

Когда все готово, можем пойти в любую нейросеть. По-моему мнению самая лучшая на сегодняшний день для данной задачи будет DeepSeek-r1.
Выверенный опытным путем промпт.

Прокомментируй каждую строку и переименнуй переменные и функции нормально.
Саму структуру кода оставь, как есть, не сокращая и не меняя, 
то есть никаких "и так далее" - пиши код полностью!
Все имена должны быть подробными, то есть не tmp и прочее, 
никаких сокращенных бысмысленных имен. Опиши к чему пренадлежат переменные, 
не просто windowStruct, а UIState как напимер.
Так же переименую DAT_ как g_..новоеИмя, и LABEL_куда прыгаем

{НАШ_ПСЕВДО_КОД}


после кода выведи все, что переименовал в формате списка
старое_имя=новое_имя\n
в список включай все переменные, функции, DAT_, LAB_, param_ и прочее без исключений прям все пиши

Выбираем цель, для эксперимента

наша функция до переименования
наша функция до переименования

В итоге, у меня получился вот такой вот список для моей функции

param_1=hWndPointer
bVar1=isCursorConfinedDueToCapture
bVar2=isMarginApplied
cVar3=isCursorCapturedResult
local_res8=clientTopLeftPoint
local_res10=clientBottomRightPoint
local_38=confinedCursorRect
local_28=windowRect

Запускаем!!

После запуска скрипта (при условии, что курсор установлен внутри функции) откроется диалоговое окно, в котором будут выведены найденные идентификаторы.
Введите список переименований в формате:

ВАЖНО!!! Переменные по типу xVarN нужно сортировать по убыванию, так как они не внесены в базу. Переименование меньшей переменной приведёт к тому, что большая займёт её место, и список станет невалидным.

После ввода нажмите кнопку Apply и дождитесь окончания выполнения.

окно ввода
окно ввода

В логах появится сообщение..

метод подсчёта требует доработки..
метод подсчёта требует доработки..

В итоге должно получиться нечто подобное:

переименованный псевдо-код
переименованный псевдо-код

Спасибо за внимание! Скачать PDF и сам скрипт можно по ссылке – https://t.me/osiechan/62
А
начать путь в реверс-инжиниринге можно на увлекательном бесплатном курсе по ботостроению для ММОРПГ – https://t.me/osiechan/41

Комментарии (7)


  1. HEXFFFFFFFF
    15.02.2025 23:06

    Хм. Сначала хотел написать что бред какой то))) Но почитал и понял что не совсем бред. Не совсем понял нафига скрипты, и зачем вообще так длинно. В любом слчае это дело вкуса, что чем и как конвертить. Но основная идея - заставить ИИ расствить осмысленные имена переменным после декомпиляции это супер! Основной вопрос только в том насколько адекватно ИИ справляется с этим очень не простым заданием. Вы вообще смогли оценить насколько подставленные ИИ имена соответствуют сути? А то у меня есть подозрение что это будет немногим лучьше рандома.

    Собственно смысл статьи должен быть в том что- "я попробовал расставлять осмысленные имена после декомпиляции с помощью ИИ и у меня получилось то-то и то-то"

    И кстати. А нельзя скормить ИИ бинарник с заданием декомпилировать максимально близко к исходникам с осмысленными именами?


    1. osieman Автор
      15.02.2025 23:06

      DeepSeek-r1 справляется великолепно)
      Он по второстепенным признакам находит взаимосвязи.
      В любом случае быстро накидать названия приятне для глаза)


      1. Ilya_JOATMON
        15.02.2025 23:06

        Ida тоже умеет по функциям Вин АПИ ставить имена. Без всякого ИИ.

        Вот я бы на него посмотрел, когда никакой экстра информации нет. А это 90% кода.


        1. pfemidi
          15.02.2025 23:06

          IDA больно уж дорогой по сравнению с Ghidra и Deepseek для личного пользования.


          1. Ilya_JOATMON
            15.02.2025 23:06

            Если вы под флагом со скрещенными костями - то не дорогой.


            1. pfemidi
              15.02.2025 23:06

              Я не сторонник данного флага. Так что для меня IDA дороговат.


        1. osieman Автор
          15.02.2025 23:06

          Ghidra тоже их ставит автоматически