Подготовка
Для начала берем свежий golang 1.8 под windows/amd64. Для бинарника я возьму одну из своих поделок. Компилируем с помощью
go build
. Размер файла — 7256576 байт, DWARF на месте. Так не пойдет. Чаще всего, при релизе, из бинарника вырезают все лишнее. Стандартные утилиты вроде strip плохо работают с go, находим в гугле один из популярных вариантов обрезания бинарника: go build -ldflags "-w -s"
. Смотрим что делают эти флаги, узнаем что -w удаляет DWARF, а -s убирает таблицу символов и информацию для отладки. Компилируем, смотрим размер файла — 4894720, ну вроде как никто не обещал что будет мало.Ищем функции
Открываем наш бинарник в ida и грустим, ведь из обнаруженных функций только entrypoint. Копаем чуть глубже и видим что ida уперлась в:
lea rax, qword_452018+128h
jmp rax
Переходим по адресу, объявляем функцию, запускаем анализ — обнаружено 215 функций.
Если продолжать в таком духе, то на разбор уйдет много времени. Настало время подумать. Вспоминаем что reflect из стандартной библиотеки позволяет вызывать функции по их именам, а значит названия функций, ну или хотя бы их хеши должны где-то храниться, и мы сможем их получить чтобы скормить иде.
Проматываем в начало файла в надежде с ходу напороться на указатели к нужным нам структурам, но к встречаем строку
Go build ID: "e07bfb8669c13efb74574c0ad220c5f2cfae5cd4"
. Грепаем исходники golang, благо они нам доступны, и находим упоминание об этой строке. Замечаем в коде линкера строкуctxt.Syms.Lookup("go.buildid", 0)
которая говорит о поиске в какой-то таблице символов go.buildid. Предполагаем что найдем эту строку в бинарнике, и находим.
Прямо над строкой видим число 401000h, которое является указателем на нашу строку. Ниже видим определения функций, а выше — массив пар, в котором одно из значений похоже на указатели. Проверяем куда указывают эти указатели и не промахиваемся — на функции.
Чтобы узнать что мы видели выше, скомпилируем код оставив дебаг информацию. Откроем его в ida, узнаем что этот массив называется pclntab, а заполняется он в линкере функцией func (ctxt *Link) pclntab().
У нас есть информация о таблице pclntab в которой хранятся названия функций и есть способ выхода на эту таблицу через buildid. Пишем код:
def go_find_pclntab():
pos = idaapi.get_segm_by_name(".text").endEA
textstart = idaapi.get_segm_by_name(".text").startEA
while True:
# hex дата содержит строку "go.buildid" и некоторые другие значения структуры,
# которые не должны меняться
gobuilddefpos = FindBinary(
pos, SEARCH_UP,
"67 45 23 01 " + "00 "*20 +
"67 6f 2e 62 75 69 6c 64 69 64")
if gobuilddefpos < 100 or gobuilddefpos > pos:
# указатель невалидный, все варианты пройдены
# buildid pclntab entry не найден :(
break
# проверяем что buildid entry валидный
if Dword(gobuilddefpos-0x10) == textstart:
# вычитаем из имени символа смещение имени символа от pclntab
# тем самым получаем смещение pclntab
return gobuilddefpos + 24 - Dword(gobuilddefpos-0x8)
pos = gobuilddefpos
return None
def go_pclntab_travel(pclntab):
nfunc = Dword(pclntab+8)
# проходимся по всем функциям в массиве pclntab
for i in xrange(nfunc):
entry = pclntab + 0x10 + i * 0x10
sym = Qword(entry)
info = Qword(entry + 8) + pclntab
symnameoff = Dword(info + 8) + pclntab
symname = GetString(symnameoff)
# объявляем функцию
go_pclntab_handle_function(sym, symname, info)
Запускаем и смотрим на результаты:
Почти все 5728 функции имеют название
Ищем типы
Второе что меня интересовало после названия функций — идентификация типов. Первым делом интересуемся как выделяется динамическая память для структур, что нас сразу приводит к функции newobject, которой передается указатель на runtime._type.
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldalign uint8
kind uint8
alg *typeAlg
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
Заметили? Подсказка: поле
str nameOff
. Подсказка: str — строка. Подсказка: nameOff > name > имя > имя структуры типа > имя типа. Структура типа содержит имя типа! Только оно какое-то относительное. Выясняем что str — это смещение от начала types секции модуля, указатель на которую можно получить из moduledata структуры модуля. Значит для того чтобы узнать тип, нам надо найти moduledata. Для простоты примем что у нас всегда один модуль. Находим в функции runtime.resolveNameOff что первый модуль находится в переменной firstmoduledata, её надо найти в нашем бинарнике. Для этого ходить далеко не надо, можно посмотреть в той же функции resolveNameOff, ведь адреса и названия всех функций мы уже собрали скриптом выше. Остается только найти типы для распознавания, для этого я просто взял все вызовы функции newobject и забрал из параметров указатели на типы.lea rbx, _type_p_elliptic_p256Point_6dad60 ; *elliptic.p256Point
mov [rsp+0A8h+var_A8], rbx
call runtime_newobject ; теперь то мы знаем что за тип тут аллоцируется
Итог
Можно и дальше продолжать разбирать этот бинарник, но цели мы своей уже добились и показали что golang бинарники достаточно легко исследовать, а скриптинг в IDA сильно облегчает работу.
Исходный код: github.com/mogaika/golang_ida_scripts
Код написан на правах «proof of concept» и может содержать недоработки, все претензии можно выразить в личных сообщениях тут, либо на github.
Текст написан на правах «всем добра», претензии к качеству текста ожидаются в личных сообщениях.
Комментарии (6)
icoz
03.04.2017 20:17+3А не смотрели в сторону bokken+Radare2?
mogaika
04.04.2017 04:31К своему сожалению постоянно откладываю на потом. Из-за того что последнее время работать с бинарями получается только во время ctf, то приходится использовать известный набор инструментов. К следующей статье обещаю разобраться с r2 :)
dukebarman
04.04.2017 19:49+1Bokken не рекомендуется к использованию. Проект забросили и подходит только для небольших бинарных файлов. Недавно вышла «альфа» dev-версия GUI на Qt для radare2 — iaito. Она более приятна и удобна в использовании, но лучше хотя бы с месяц подождать, пока исправят основные баги, или пока не появится инсталлятор.
В radare2 для анализа Go исполняемых файлов можно использовать Zignatures. Об этом есть отдельная статья в блоге radare2. И несколько презентаций с примерами использования: моя небольшая с основами с ZeroNights 2014, а другая — более подробная c R2con 2016 от Sergi Martinez.
В свою очередь для IDA есть несколько готовых и интересных решений. У Tim Strazzere — подобная статья и плагин. А Георгий Зайцев рассказывал об этом на ZeroNights 2016 и опубликовал плагины. mogaika вы их смотрели перед написанием своего?
trik
04.04.2017 21:38bokken мертв уже давно. Сейчас там на шарпе пишут новый гуй, но это пока не точно.
Blogoslov
имена и параметры функций получили — а содержимое функции? Тоже так же легко достается или все же ларчик не так легко открывается? Ведь по-моему самое интересное ради чего затеяли реверсинг это изучить функцию, её алгоритм и что она вообще делает. Или нет?
mogaika
Код легок для чтения, видимо из-за того что в go внедрили свой ассемблер, который должен работать на всех платформах, а оптимизировать его еще толком не успели.
Лекго отслеживать путешествие значений — аргументы, как на вход, так и на выход передаются через стек.
lea rdx, _type_p_png_decoder_71b020
mov [rsp], rdx ; передаем аргумент (push и pop не используются)
call runtime_newobject
mov rax, [rsp+8] ; получаем результат
Из минусов только разве что hex-ray декомпилятор не справляется. Неверно определяет передачу аргументов в функции и возвращаемые значения, а научить его мне не удалось.
Ну и строки хранятся без нуль терминатора, а размер этих строк передается только при вызове функций. Но и это дело можно распарсить для удобства.
Изучать алгоритм гораздо проще, если ты знаешь названия функций и типов :)