Источник: https://github.com/AnaktaCTF/CTF/blob/main — Reverse/GhidraAPI-Python.md
Обзор возможностей Ghidra API
Ghidra предоставляет мощный набор программных интерфейсов, позволяющих управлять внутренними объектами анализа: от загрузчика и декомпилера до элементов представления программы (Function, Listing, Program). С помощью Jython-сценариев можно:
- Открывать и автоматически анализировать указанный двоичный файл.
- Перебирать все найденные функции, строить граф вызовов и сохранять его в виде данных.
- Выявлять известные шаблоны конструкций (поиск строковых функций, memcpy, крипто-библиотек).
- Собирать метрики по функциям и инструкции (количество перекрытий, глубина вложенности, количество локальных переменных и использование стека).
- Экспортировать все полученные данные в формате JSON или CSV для объединения с результатами других инструментов.
Далее мы пройдем каждый из пунктов выше пошагово.
Подготовка окружения
Прежде чем приступить к написанию скриптов, необходимо убедиться, что на вашей системе установлено свежее издание Ghidra (версия 10.x или выше), а также скриптовый движок Jython, который входит в комплект Ghidra. Для удобства работы рекомендуется создать отдельную папку для своих сценариев:
- Откройте каталог установки Ghidra и перейдите в папку
Ghidra/Features/Python. - Создайте там новую директорию, например,
AutoAnalysis. - Убедитесь, что внутри каталога
AutoAnalysisнет конфликтующих файлов — вы будете хранить туда свои.py-скрипты.
Если вы планируете обращаться к внешним библиотекам (например, для более удобного экспорта JSON), убедитесь, что Jython видит пути к ним: отредактируйте файл Ghidra/Support/analyzeHeadless или соответствующий скрипт запуска, добавив в переменную PYTHONPATH путь к каталогу с вашими модулями.
Первый скрипт: загрузка и анализ файла
Начнем с создания самого простого скрипта, который принимает на вход имя исполняемого файла, загружает его в Ghidra и запускает необходимый набор анализаторов. Откройте новый файл AutoAnalysis/LoadAndAnalyze.py и вставьте следующий код:
from ghidra.app.script import GhidraScript
class LoadAndAnalyze(GhidraScript):
def run(self):
# Получаем путь к целевому файлу
target_path = askFile('Выберите бинарный файл для анализа', 'Открыть').absolutePath
# Загружаем программу
program = self.importer.importByUsingLoader(target_path, False, monitor)
# Запускаем анализ
analysis_manager = self.getAnalysisManager(program)
analysis_manager.initializeOptions(monitor)
analysis_manager.reAnalyzeAll(program, monitor)
# Выводим сообщение об успешном завершении
self.println('Анализ завершен для: {}'.format(program.getName()))
if __name__ == '__main__':
LoadAndAnalyze().run()
В этом коде мы описываем класс-наследник GhidraScript, реализуем метод run, где последовательно вызываем загрузчик, менеджер анализа и выводим сообщение об окончании. Обратите внимание, что функция askFile интерактивно запрашивает у пользователя путь к файлу, а analysis_manager.reAnalyzeAll обеспечивает применение всех стандартных анализаторов Ghidra.
Пояснения к коду
В первом фрагменте мы обращаемся к диалогу выбора файла. Во втором — используем importByUsingLoader, чтобы Ghidra автоматически определила формат (ELF, PE, Mach-O). Затем контейнер AnalysisManager инициализирует все параметры по умолчанию и запускает анализ. Такой шаблон станет основой для всех последующих сценариев, поэтому я рекомендую скопировать его к себе и сохранить под базовым именем.
Извлечение функций и построение Call Graph
После того как файл загружен и проанализирован, нам нужно перебрать все функции и собрать информацию об их вызовах. Для этого создадим скрипт AutoAnalysis/CallGraphExtractor.py:
from ghidra.app.script import GhidraScript
from ghidra.program.model.listing import Function
import json
class CallGraphExtractor(GhidraScript):
def run(self):
program = currentProgram
fm = program.getFunctionManager()
functions = fm.getFunctions(True)
call_graph = {}
for func in functions:
callees = []
# Получаем итератор по всем call-операторам в функции
instructions = getInstructions(func.getBody(), True)
for instr in instructions:
if instr.getMnemonicString().upper().startswith('CALL'):
ref = instr.getPrimaryReference(0)
if ref and ref.getReferenceType().isCall():
target = ref.getToAddress()
callee = fm.getFunctionAt(target)
if callee:
callees.append(callee.getName())
call_graph[func.getName()] = callees
# Экспортируем в JSON
output = askFile('Сохранить Call Graph', 'Сохранить')
with open(output.absolutePath, 'w') as f:
json.dump(call_graph, f, indent=2, ensure_ascii=False)
self.println('Call Graph сохранен в {}'.format(output))
if __name__ == '__main__':
CallGraphExtractor().run()
Здесь мы перебираем все функции через getFunctions(True), после чего внутри каждой функции сканируем инструкции на предмет инструкций CALL. Шаг за шагом мы получаем ссылки на целевые адреса, определяем функцию-цель и добавляем ее имя в список вызовов. В конце формируется словарь call_graph, который сохраняется в JSON.
Распознавание и переименование типовых конструкций
Автоматическое переименование функций существенно облегчает дальнейший разбор кода. Например, стоит найти все вызовы memcpy или функций крипто-библиотек, и дать им осмысленные имена. Создадим скрипт PatternRecognizer.py:
from ghidra.app.script import GhidraScript
import re
class PatternRecognizer(GhidraScript):
def run(self):
program = currentProgram
byte_patterns = [
(b'\x48\x83\xec\x08', 'stack_frame_setup'),
(b'\x8b\x45\x0c', 'load_arg'),
]
fm = program.getFunctionManager()
# Проходим по всем функциям
for func in fm.getFunctions(True):
body = getBytes(func.getEntryPoint(), func.getBody().getNumAddresses())
for pattern, name in byte_patterns:
if body is not None and pattern in body:
new_name = "{}_{}".format(func.getName(), name)
func.setName(new_name, ghidra.program.model.symbol.SourceType.ANALYSIS)
break
# Переименование строковых функций
for string_ref in getDataReferencesTo(currentProgram.getDefaultPointerSize()):
data = getDataAt(string_ref.getFromAddress())
if data and data.isDefined() and data.getValue().startswith("printf"):
func = fm.getFunctionContaining(string_ref.getFromAddress())
if func:
func.setName("PRINTF_{}".format(func.getEntryPoint()),
ghidra.program.model.symbol.SourceType.USER_DEFINED)
self.println('Паттерны распознаны и функции переименованы.')
if __name__ == '__main__':
PatternRecognizer().run()
В этом примере мы задаем список байтовых паттернов и проверяем, встречаются ли они в теле функции. Если да, то добавляем к имени функции суффикс, поясняющий её роль. Затем отдельно обрабатываем ссылки на данные, содержащие строки формата (printf), и переименовываем соответствующие функции.
Сбор метрик по функциям
Чтобы получить количественную оценку сложности или глубины функций, необходимо собрать такие метрики, как число перекрывающихся областей, глубину вложенности ветвлений и использование локальных (стековых) переменных. Скрипт MetricsCollector.py может выглядеть следующим образом:
from ghidra.app.script import GhidraScript
import csv
class MetricsCollector(GhidraScript):
def run(self):
program = currentProgram
fm = program.getFunctionManager()
metrics = []
for func in fm.getFunctions(True):
instr_count = 0
max_depth = 0
local_vars = len(func.getLocalVariables())
# Подсчет инструкций и глубины ветвлений
stack = [(func.getEntryPoint(), 0)]
visited = set()
while stack:
addr, depth = stack.pop()
if addr in visited:
continue
visited.add(addr)
if depth > max_depth:
max_depth = depth
instr = getInstructionAt(addr)
while instr and instr.getAddress() in func.getBody():
instr_count += 1
mnemonic = instr.getMnemonicString()
if mnemonic in ('JMP', 'JNZ', 'JE', 'JL', 'JLE'):
# Добавляем переход в стек с увеличением глубины
for ref in instr.getReferencesFrom():
if ref.getReferenceType().isConditional() or ref.getReferenceType().isJump():
stack.append((ref.getToAddress(), depth + 1))
elif mnemonic == 'RET':
break
instr = instr.getNext()
metrics.append({
'name': func.getName(),
'instructions': instr_count,
'max_depth': max_depth,
'local_vars': local_vars
})
# Экспортируем в CSV
output = askFile('Сохранить метрики', 'Сохранить', 'func_metrics.csv')
with open(output.absolutePath, 'w', newline='') as csvfile:
fieldnames = ['name', 'instructions', 'max_depth', 'local_vars']
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
for m in metrics:
writer.writerow(m)
self.println('Метрики функций сохранены в CSV.')
if __name__ == '__main__':
MetricsCollector().run()
В этом сценарии мы обходим каждую функцию, выполняем обход графа выполнения для подсчета инструкций и оценки глубины вложенности ветвлений, а также считаем число локальных переменных. Наконец, формируем CSV-файл с результатами.
Экспорт и объединение результатов
После выполнения всех предыдущих скриптов вы получите на диске несколько файлов: call_graph.json, func_metrics.csv и, возможно, дополнительные JSON-файлы с переименованными функциями. Для дальнейшего объединения с данными других инструментов (например, динамического анализа) можно использовать Python-скрипт вне Ghidra:
import json
import csv
def load_call_graph(path):
with open(path, 'r', encoding='utf-8') as f:
return json.load(f)
def load_metrics(path):
metrics = {}
with open(path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
metrics[row['name']] = {
'instructions': int(row['instructions']),
'max_depth': int(row['max_depth']),
'local_vars': int(row['local_vars'])
}
return metrics
if __name__ == '__main__':
cg = load_call_graph('call_graph.json')
mt = load_metrics('func_metrics.csv')
merged = []
for func, callees in cg.items():
data = mt.get(func, {})
merged.append({
'function': func,
'callees': len(callees),
'instructions': data.get('instructions', 0),
'max_depth': data.get('max_depth', 0),
'local_vars': data.get('local_vars', 0)
})
# Сохраняем общий отчет
with open('merged_report.csv', 'w', newline='', encoding='utf-8') as f:
fieldnames = ['function', 'callees', 'instructions', 'max_depth', 'local_vars']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for row in merged:
writer.writerow(row)
print('Объединенный отчет сохранен в merged_report.csv')
Этот утилитарный скрипт читает данные из ранее созданных файлов и сводит их в единый CSV-отчет, где для каждой функции указано число вызовов callees, общее число инструкций, максимальная глубина ветвлений и количество локальных переменных. Готовый файл merged_report.csv может быть загружен в табличный редактор, BI-систему или объединен с результатами динамического анализа.