Реверс бинарных файлов Golang с использование GHIDRA. Часть 2
- воскресенье, 24 сентября 2023 г. в 00:00:17
Это вторая часть нашей серии о реверс-инжиниринге двоичных файлов Go с помощью Ghidra. В предыдущей статье мы обсуждали, как восстановить имена функций в удаленных файлах Go и как помочь Ghidra распознавать и определять строки в этих двоичных файлах. Мы сосредоточились на двоичных файлах ELF, лишь кратко упомянув различия между PE-файлами.
В этой статье будет обсуждаться новая тема - процесс извлечения информации о типе из двоичных файлов Go. Мы также более подробно объясним, как обращаться с файлами Windows PE. И, наконец, мы исследуем различия между различными версиями Go, включая изменения, произошедшие со времени нашей последней публикации в блоге.
На момент написания этой статьи последней версией Go была go1.18, и мы использовали последнюю версию Ghidra - 10.1.4. Как и ранее, все созданные нами скрипты Ghidra можно найти в нашем репозитории GitHub вместе с тестовыми файлами «Hello World». Вредоносные файлы, используемые в примерах, можно скачать с VirusTotal.
В следующей статье дается подробное объяснение системы типов Go
Go имеет встроенные базовые типы, такие как bool
, string
и float64
, а также так называемые составные типы, такие как структуры, функции и типы интерфейса. Go также позволяет пользователям объявлять свои собственные типы. Извлечение этих типов является важным шагом в статическом анализе вредоносного ПО и помогает аналитикам понять конкретные части кода.
Ниже вы можете найти несколько примеров определений типов из sys.x86_64_unp
с использованием исправления.
Програма redress - инструмент для анализа удаленных двоичных файлов Go, скомпилированных с помощью компилятора Go. Он извлекает данные из двоичного файла и использует их для восстановления символов и выполнения анализа. По сути, он пытается "переодеть" "раздетый" двоичный файл. Его можно скачать с его страницы GitHub .
Чтобы понять, как определения типов хранятся в двоичном файле, нам нужно взглянуть на исходный код Go .
Первая полезная информация - это список доступных типов.
Далее следуют подробные описания различных типов. Согласно документации, rtype
является общепринятой реализацией большинства значений. Он встроен в другие типы структур. Итак, самый важный шаг - понять структуру rtype
.
Здесь довольно много полезной информации, но самые важные данные для реверса - это смещение вида и имени. Это может помочь нам понять, с каким типом мы имеем дело и как называется этот конкретный тип. В приведенных выше примерах первый — это тип структуры, который называется miner.Process , а второй — это тип интерфейса, называемый exploit.exploiter .
Чтобы найти дополнительную информацию об определенных типах, нам придется изучить описание каждого типа отдельно. Ниже мы покажем один пример, но все они находятся в одном файле type.go
. Давайте посмотрим на тип структуры.
Тип структуры начинается со структуры rtype
, которую мы только что обсудили. За ним следует имя пакета, содержащего эту конкретную структуру. В случае miner.Process пакет называется shell/miner
. Наконец, есть массив полей структуры.
Структура structField
содержит поле и указатель на структуру rtype
, которая сообщает нам, к какому типу относится это поле. Итак, в нашем примере структура miner.Process содержит пять полей:
тип int
, называемый pid
За ним следуют три типа: имя, путь и cmdline
Фрагмент содержащий значения uint8
, который называетсяbuf
Теперь, мы понимаем, какую информацию нам следует искать, как хранятся самые важные данные о типах, но вопрос в том, как нам найти эти структуры внутри двоичного файла.
Во-первых, нам нужно понять так называемую структуру данных модуля. Эта таблица доступна в двоичных файлах Go начиная с версии 1.5. За прошедшие годы он претерпел некоторые изменения, поэтому каждый раз нам приходится учитывать версию Go, которая использовалась для создания конкретного двоичного файла. Хорошее введение в модульные данные можно найти здесь . Ниже мы обсудим последнюю форму этой структуры, доступную в go1.18.
Давайте еще раз посмотрим на исходный код .
Согласно этому «модуль data записывает информацию о макете исполняемого образа».
В этой таблице содержится много дополнительной полезной информации, но для извлечения типов полезны следующие данные:
types
, etypes
- адрес начала и конца раздела, содержащего описания типов
typelinks
- срез, содержащий 32-битные целые числа, которые являются смещениями структур типов от типов
В двоичных файлах ELF очень легко найти эти конкретные адреса и смещения, даже не находя структуру данных модуля. Смещения можно найти в разделе .typelinks
, а типы фактически являются началом раздела .rodata
.
Извлечение описаний типов — это рекурсивный процесс. Как видно из приведенных выше примеров, некоторые типы ссылаются на другие типы, например поля структуры внутри типа структуры.
Примечание. Наш сценарий в настоящее время извлекает эту информацию для двоичных файлов ELF, не обнаруживая структуру данных модуля. Однако имейте в виду, что этот процесс может завершиться неудачей из-за изменений в будущих версиях Go или из-за некоторой запутанности, когда имена разделов изменяются. В этом случае используйте тот же метод, что и для файлов PE, см. объяснение здесь.
В этом разделе мы суммируем необходимые шаги для извлечения информации о типе из двоичных файлов ELF.
Найдите раздел .typelinks и просмотрите смещения.
Найдите описания типов, используя смещения из раздела .rodata.
Определить вид типов
Извлеките доступную информацию о типе в зависимости от его типа.
Найдите ссылочные типы и повторите шаги 3–5 еще раз.
Наш скрипт следует описанным выше шагам и создает метки и комментарии, которые помогают перепроектировать двоичные файлы.
Каждый тип помечен его названием, а для некоторых видов дополнительная информация добавляется в виде предварительных комментариев.
В настоящее время скрипт добавляет подробные описания к типам функций, интерфейсов и структур. Далее мы рассмотрим пример того, как это выглядит в Ghidra.
Чтобы дать вам лучшее представление о том, как это выглядит, вот подробный пример, в котором извлечение информации о типе дает быстрые и простые подсказки по обратному проектированию двоичного файла Go.
После восстановления имен функций и строк (подробнее см. предыдущий пост) у нас уже есть много полезной информации о назначении файла. Мы можем легко найти основные функции и получить некоторое представление об их поведении. В приведенном ниже примере мы рассмотрим функцию main.getInfo, где мы видим, что происходит какой-то сетевой обмен. Вопрос в том, какие данные передаются через эту связь. Непосредственно перед вызовом функции runtime.newobject мы видим интересную ссылку на данные: DAT_824bd20.
К сожалению, просмотр этого раздела данных не принесет большой пользы. Однако более пристальный взгляд показывает, что там фактически хранится структура описания типа.
После выполнения нашего сценария для извлечения типов мы увидим, что в представлении листинга чуть выше объекта runtime.newobject ссылка на данные в вызове функции была переименована во что-то значимое: main.Info, которое является именем извлеченного типа.
Если мы последуем этой ссылке, мы найдем дополнительную информацию об этом конкретном типе. В данном случае main.Info — это тип структуры, содержащий два поля (RsaPublicKey и Readme), типы обоих полей — строковые. Исходя из этого, мы можем с уверенностью предположить, что именно здесь открытый ключ RSA и содержимое записки о выкупе передаются между сервером C2 и жертвой.
Как мы видели выше, извлечение информации о типе из двоичных файлов ELF требует всего нескольких простых шагов, а благодаря четко определенным разделам, таким как .typelinks (или .pclntab в случае восстановления имени функции), найти необходимые данные очень легко. легкий. К сожалению, эти именованные разделы недоступны в PE-файлах. Итак, чтобы извлечь информацию о типе, мы должны искать таблицу данных модуля напрямую.
В нашем скрипте за поиск таблицы данных модуля отвечают функции findModuledata и isModuledata. Скрипт использует тот факт, что эта таблица начинается с указателя на структуру pclntab (pcHeader для более поздних версий). Итак, сначала мы ищем структуру pclntab и используем ссылки для поиска данных модуля, поскольку одна из ссылок, указывающих на pclntab, должна быть началом модуля данных. Наконец, мы проверяем другие поля в данных модуля, чтобы убедиться, что мы нашли правильный адрес.
Функции findPclntabPE и isPclntab используются для поиска структуры pclntab, которая представляет собой отдельный раздел, называемый .gopclntab для файлов ELF. Для PE-файлов мы ищем магические значения в начале структуры и проверяем следующие несколько байтов на наличие известных значений. Дополнительную информацию о структуре pclntab можно найти в нашей предыдущей статье о реверсе двоичного кода Go.
Наш скрипт восстановления имени функции был обновлен и теперь работает как с файлами ELF, так и с PE, а также с последней версией Go. Мы используем те же функции, что и для извлечения типов, чтобы найти структуру pclntab и оттуда найти имена функций — все работает так же, как и для двоичных файлов ELF.
Самая большая трудность при анализе двоичных файлов Go — постоянные изменения версий. То, что работает для одной версии, может не работать для другой. По этой причине мы должны следить за обновлениями версий и соответствующим образом обновлять наши скрипты. Самое главное, мы должны иметь возможность определить версию Go определенного двоичного файла, чтобы правильно проанализировать этот файл.
Наш текущий подход основан на строках, что означает, что мы ищем строку "go1.x" в двоичном файле и используем первое вхождение для определения версии. Несмотря на то, что этот подход работал во всех случаях, когда мы использовали наши скрипты для анализа вредоносного ПО, у него есть несколько недостатков:
Это медленно.
Строки разных версий могут быть найдены в одном двоичном файле, если определенные пакеты Go включены из разных версий.
Можно легко подделать.
По нашему исследованию, наиболее важными изменениями версии являются следующие:
Обновления заголовка Pclntab (доступны с версии 1.2, изменения в версиях 1.16, 1.18)
Обновление структуры Moduldata (доступно с версии 1.5, изменения в версиях 1.7, 1.8, 1.10, 1.16)
Обновление структуры имени типа (1.18)
Улучшите метод извлечения версий.
Добавьте подробные определения типов для других типов, а не только для функций, структур и интерфейсов. (В настоящее время возможно, что мы пропустим несколько описаний типов, поскольку мы не выполняем шаги итерации для таких типов, как chan или map.)
https://rednaga.io/2016/09/21/reversing_go_binaries_like_a_pro/
https://2016.zeronights.ru/wp-content/uploads/2016/12/GO_Zaytsev.pdf
https://carvesystems.com/news/reverse-engineering-go-binaries-using-radare-2-and-python/
https://www.pnfsoftware.com/blog/analyzing-golang-executables/
https://github.com/strazzere/golang_loader_assist/blob/master/Bsides-GO-Forth-And-Reverse.pdf
https://securelist.com/extracting-type-information-from-go-binaries/104715/