Содержание статьи
С 28 июля по 8 сентября команда FLARE (FireEye Labs Advanced Reverse Engineering team), входящая в состав компании FireEye и занимающаяся разработкой систем защиты от угроз «нулевого дня» и таргетированных атак, проводила свой второй по счету конкурс в формате CTF (capture the flag). Основной темой заданий был реверсивный анализ, поэтому в первую очередь challenge касался специалистов в области реверсивного анализа и аналитиков вредоносного программного обеспечения, ну и всех остальных специалистов в области компьютерной безопасности тоже. Разбору заданий конкурса и будет посвящена эта статья.
Предыстория
Сначала пара слов о самом мероприятии. Конкурс проводился в формате CTF. Флагом, то есть ответом к каждому заданию, был адрес электронной почты в домене flare-on.com, при отправке сообщения на который присылалось письмо со следующим заданием. Все задания, а их было одиннадцать, полностью связаны с реверсивным анализом. Все они были посвящены обходу самописных алгоритмов криптографии и самодельных способов обфускации кода.
Дело осложняло еще то, что применялось большое количество разнообразных платформ и языков (.NET, Android Package, Windows-драйвер, ELF, скомпилированный под ARM), разнообразные способы конвертации основного кода в оболочки другого кода (нпример, код Python, конвертированный в EXE, скрипт AutoIt, конвертированный в EXE), а также немного стеганографии и анализа трафика. В общем, все те вещи, с которыми может столкнуться аналитик вредоносного кода в течение своего рабочего дня, — все перечисленные методы злоумышленники активно применяют для обхода антивирусных решений. Но довольно слов, давай перейдем непосредственно к заданиям и посмотрим, как надо было их проходить.
FLARE-On Challenge 1
Первое задание можно получить по этой ссылке. Как и положено, оно очень простое. Поэтому подробно останавливаться на нем не будем. При запуске программы она ожидает ввод строки в стандартный поток ввода. Далее строка сравнивается с эталоном, жестко прописанным в коде. Но предварительно введенная строка преобразуется путем гаммирования (в нашем случае в качестве операции суммирования открытого и закрытого текстов используется операция «исключающее или», то есть xor
) с ключом 0x7d
.
Для того чтобы вычислить необходимый адрес электронной почты, достаточно просто проделать операцию «исключающего или» с тем набором байтов, который указан в коде и с которым сравнивается после преобразования введенная строка. И это возможно благодаря тому, что операция xor обратима и ключ также известен (рис. 1).
Чтобы не переводить вручную, я набросал небольшой скриптик на Python, помогающий получить искомый адрес электронной почты:
ab = bytearray(open('i_am_happy_you_are_to_playing_the_flareon_challenge.exe', 'rb').read())
begin = 0x540
for i in xrange(begin, begin+24):
ab[i] = (ab[i] ^ 0x7d) % 256
print str(ab[begin:begin+24])
В результате получаем ключ, а в итоге и второе задание.
FLARE-On Challenge 2
Второе задание уже придет по почте. После непродолжительного исследования выясняем, что его суть такая же, как и в первом, — вводимая строка сравнивается с эталонным значением (последовательностью байтов) после определенного преобразования. На рис. 2 как раз указан тот самый алгоритм, по которому введенная строка преобразуется в последовательность байтов. Попробуем в нем разобраться.
При анализе кода можно увидеть, что над каждым символом строки производится две основных операции: «исключающего или» и циклического сдвига влево. Так как обе эти операции обратимы, при знании ключа для «исключающего или» и количества битов для сдвига (а в данном случае оба этих значения известны, потому что жестко указаны в самом коде: 0x01c7 и от 0 до 2) можно получить искомую строку, то есть адрес электронной почты.
Чтобы не проводить все эти операции вручную, можно написать код на Python, реализующий все описанные операции, только в обратном порядке.
FLARE-On Challenge 3
Идем дальше. Третье задание, которое нам присылают по почте, представляет собой исполняемый файл. При более детальном рассмотрении его «внутренностей» можно заметить вхождения типа Python, Python VM, имена библиотек Python. Все это плюс оставленная авторами задания стандартная иконка дает нам возможность сделать вывод, что перед нами Python-код, сконвертированный в PE-файл.
Так как существует много готовых обратных конвертеров, нет смысла анализировать основной исполняемый файл, а лучше преобразовать его сразу в Python-код, рассматривать который уже в разы проще. Для этого воспользуемся pyinstxtractor.py, который извлекает архив с pic-файлами, pyd-файлы и также py-файлы. При непродолжительном рассмотрении полученных в результате конвертации модулей можно увидеть один с именем elfie
. Его код обфусцирован, закодирован при помощи Base64 и начинает выполняться по команде exec
(рис. 3).
Если заменить команду exec
на print
, то в результате получим уже более простой и читаемый код (рис. 4).
Этот код также обфусцирован, но уже видно, что все нажатия клавиш обрабатываются и сравниваются с необходимой строкой, в качестве которой выступает искомый адрес электронной почты, хранящийся в коде в открытом виде.
FLARE-On Challenge 4
В четвертом задании перед нами предстает исполняемый файл, который упакован UPX (о чем нам сообщает PeID, а также наличие в этом файле одноименной секции). Кстати говоря, это задание — хороший пример того, что нельзя доверять готовым распаковщикам. Иногда лучше проверять, что там происходит под капотом, во время распаковки и проводить эту операцию вручную. Запустим наше приложение в командной строке и получаем вывод: 2 + 2 = 4
. А теперь попробуем снять упаковку стандартным способом при помощи upx -d
. При запуске после распаковки получаем следующую картину: 2 + 2 = 5
.
Таким образом, даже без копания в коде можно понять, что в алгоритм распаковки, который заложен в само приложение, внесены изменения. Попробуем снять упаковку вручную при помощи отладчика ImmunityDebugger и программы по восстановлению таблицы импорта Import REConstructor. Так как данный способ давно и многократно описан, не будем подробно его рассматривать; кто еще незнаком или хочет освежить в памяти — добро пожаловать сюда.
При просмотре в IDA полученного после ручной распаковки файла можно увидеть, что в результате имеем 2 + 2 = 4
, значит, процесс распаковки прошел корректно.
Теперь уже можно рассмотреть алгоритм сравнения «правильности» введенной строки. Что имеем? В начале алгоритма проверки считывается текущее время, а именно час, затем от введенной строки получаем MD5 и сравниваем хеш с эталонными значениями. В данном случае эталонное значение представляет собой также хеш, который можно отнести к определенному часу в сутках, и таких возможных значений ровно от 0 до 23. После ввода верного значения, то есть текущего верного времени, получим необходимый адрес электронной почты (рис. 5).
В итоге получаем адрес и пятое задание.
FLARE-On Challenge 5
Пятое задание начинается с того, что, помимо исполняемого файла, в архиве, присланном нам на почту, находится также и pcap-файл. Как обычно, начинаем исследование с запуска IDA. Рассмотрев внутренности файла, видим, что приложение открывает файл key.txt
. Затем содержимое файла считывается, и над ним проводится набор математических операций, а полученная последовательность передается по сети на адрес 127.0.0.1
(рис. 6).
Теперь становится понятно предназначение pcap-файла — в нем записана та последовательность, которая получена в результате математических операций.
Если рассматривать поподробнее, то данные математические операции — это криптографический алгоритм. Для того чтобы быстро и без особых усилий определить, что за алгоритм применяется в анализируемом семпле, можно использовать один очень действенный способ. Так как во многих криптоалгоритмах применяются уникальные для данного алгоритма константы, то именно по ним и возможно определить, с чем имеешь дело, не перебирая при этом кучу кода. Посмотреть эти константы можно как в поисковых системах, так и в исходниках библиотеки OpenSSL.
В нашем случае используется Base64. Также можно обратить внимание, что в этом задании есть маленькая хитрость: в стандартном алгоритме алфавит ABCD...abcd...01234+/
, а здесь abcd...ABCD...01234+/
, поэтому можно сделать вывод, что в полученной последовательности символы верхнего и нижнего регистра поменяются местами. Соответственно, чтобы получить необходимый адрес электронной почты, нужно проделать все эти операции наоборот.
Но перед этим надо получить последовательность, над которой будем дальше колдовать. Как ты помнишь, мы пришли к выводу, что она должна находиться в pcap-файле. Число tcp stream в нем небольшое — всего двенадцать, поэтому можно скопировать данную последовательность вручную. А затем над полученными данными провести все манипуляции: перевести регистр символов и декодировать Base64. Ниже представлен кусочек кода, который поможет автоматизировать часть этапов:
import base64
str = str(open('stream', 'r').readline())
encoded = bytearray(base64.b64decode(str))
В итоге после преобразования данных, переданных по сети, получаем адрес электронной почты от следующего задания.
FLARE-On Challenge 6
Данное задание отличается от других тем, что здесь уже мы имеем дело с APK-файлами (Android application) (рис. 7).
В принципе, декомпилировать APK в Java-код не составляет никакой проблемы: вначале распаковываем android.apk
как ZIP-файл, а затем полученный classes.dex
преобразуем при помощи d2j-dex2jar в JAR.
В результате в Java-коде видим, что основную проверку выполняет библиотека libvalidate.so (Shared object — ELF, еще и скомпилированный под ARM). Анализировать libvalidate будем в IDA и исключительно в статике, хотя есть пара готовых способов анализировать APK в динамике, но в таком случае больше времени потратим на подготовку системы. После непродолжительного изучения становится понятно, что анализировать внутри библиотеки надо функцию Java_com_flareon_flare_ValidateActivity_validate
— именно в ней и происходит сравнение введенной строки с эталоном по определенному алгоритму.
А суть алгоритма проверки введенного пароля с эталонным значением такова: каждый символ переводится в свой шестнадцатеричный эквивалент, который делится без остатка на число из предварительно подготовленной таблицы, и количество таких делений без остатка для каждого элемента таблицы фиксируется в предварительно подготовленном массиве (рис. 8). Затем полученная из таких сумм таблица сверяется с эталонной таблицей для каждого символа, если таких совпадений 22 (количество символов необходимой почты), то, значит, был введен верный адрес.
Для того чтобы получить необходимый набор символов, то есть искомый адрес электронной почты, нужно просто умножать символ из таблицы с элементами, на которые делили, на количество раз, указанное в эталонных таблицах для каждого символа.
FLARE-On Challenge 7
В седьмом задании перед нами предстает .NET-файл. При попытке декомпилировать в ILSpy видно, что код обфусцирован при помощи SmartAssembly. Снимаем обфускацию с помощью de4dot и получаем код, который уже можно анализировать.
При изучении Class3
видим, что результат smethod_0
и smethod_3
объединяется и сравнивается с введенной строкой. Если строки совпадают, то объединенный результат методов smethod_0
и smethod_3
используется для расшифровки строки с необходимым адресом электронной почты (рис. 9).
Найти верный ключ (тот, что мы получаем из объединения результатов двух методов) можно, просто подсмотрев его в отладчике ILSpy Debugger. В итоге, отыскав необходимый ключ, просто передадим его на вход приложению и получим адрес электронной почты к следующему заданию.
FLARE-On Challenge 8
Восьмое задание связано со стеганографией. У нас опять был исполняемый файл, заглянув внутрь которого можно было увидеть Base64-код (рис. 10).
После декодирования этой последовательности видим, что это PNG-изображение (рис. 11).
Попробуем проверить данное изображение на заложенный в нем стегоконтейнер по алгоритму LSB (последний значимый бит) при помощи утилиты stegsolve.jar. При извлечении из каждого байта последнего бита получаем следующий файл (рис. 12).
Можно заметить, что по внешнему виду данный файл чем-то схож с PE: второй байт полученного файла равен Z
, что также похоже на сигнатуру PE файла MZ
. Попробуем проинвертировать первый байт файла 0xB2
в двоичном виде (10110010
) и получим 01001101
или 0x4D
, что соответствует символу M. Если проделать эту процедуру со вторым символом, то из Z
, который в двоичном виде 01011010
, мы получим Z
. Следовательно, чтобы получить из этого файла правильный PE-файл, необходимо инвертировать каждый байт. Этот код поможет провести данное преобразование быстрее:
def int2bin(n, count=24):
return "".join([str((n >> y) & 1) for y in range(count-1, -1, -1)])
fin = open('L0','rb')
fout = open('new','wb')
while True:
byte = fin.read(1)
if byte == "":
break
bits = int2bin(ord(byte),8)
flipped = bits[::-1]
fout.write(chr(int(flipped,2)))
fin.close()
fout.close()
В результате получим исполняемый файл, открыв который в IDA увидим необходимый нам адрес электронной почты.
FLARE-On Challenge 9
Девятый таск начался также с запуска IDA Pro. Загружаем полученный по почте исполняемый файл в этот чудесный инструмент и пытаемся пройти пару инструкций в динамике.
В результате видно, что данный файл обфусцирован, а также есть пара ложных функций, выполняться которые не будут, поэтому анализировать этот код в статике без предварительной подготовки нет смысла (рис. 13). Одним из способов решения этой проблемы может быть применение DBI Pin для составления трассы кода и получения необходимого алгоритма сравнения введенной строки с эталоном (рассмотреть данный способ решения в рамках статьи не получится, но DBI Pin как-то достаточно подробно освещался в журнале, поэтому самостоятельно полистай свою подшивку номеров).
Но можно сделать проще и обойти обфускацию в статике при помощи IDA — надо просто переопределять тот участок кода, на который указывает очередной условный или безусловный переход. Как только все это будет проделано, можно увидеть в конце две строки с успешной и неуспешной концовкой. Если идти с конца, то заметим, что на результат повлияет значение байта al
, а формируется он из результатов сложения двух значений. Первое получим в результате сравнения эталонного значения с полученным после нескольких простых обратимых математических операций («исключающего или» и циклического сдвига вправо) с введенным значением в качестве адреса электронной почты. А вторым значением выступает результат проверки, находится ли анализируемое приложение под отладкой.
В результате на каждой итерации происходит суммирование значений al
, и на последней итерации полученная сумма сравнивается с нулем. Если сумма равна нулю, то мы ввели правильное значение (рис. 14).
Таким образом, чтобы получить необходимый адрес электронной почты, нужно подставить определенные ключи для исключающего или и количество циклических сдвигов. А так как есть эталонное значение в коде, то, проделав все это в обратном направлении, получим необходимый адрес электронной почты.
FLARE-On Challenge 10
Десятое задание на первый взгляд представляет собой обычный исполняемый файл.
Но если приглядеться повнимательнее, то внутри файла можно найти строки AutoIt
, из чего можно сделать вывод, что данный семпл представляет собой скомпилированный в EXE AutoIt-скрипт. Для того чтобы получить код AutoIt-скрипта, воспользуемся утилитой Exe2AutoIt. В результате получим код, который уже в разы проще анализировать. Видно, что скрипт извлекает из начального файла в зависимости от типа операционной системы драйвер с названием challenge.sys
и файл ioctl.exe
. Драйвер регистрируется в системе, затем запускается, а после стартует приложение ioctl.exe с параметром 0x22e0dc
. Что ж, попробуем во всем этом разобраться.
Рассмотрим сначала утилиту ioctl.exe
. Так как мы имеем дело с драйвером, то название файла можно расшифровать как input/output control, и передаваемый на вход параметр, скорее всего, является кодом, который затем будет обрабатываться драйвером. Действительно, если рассмотреть файл в IDA, то можно увидеть, что введенный параметр преобразуется в целое число в шестнадцатеричной системе, затем открывается именованный канал (name pipe) — \\\\.\\challenge
, и через него управляющий код пересылается на вход драйверу. Следовательно, авторы задания намекают нам, что необходимо найти функцию внутри драйвера, которая отвечает за обработку этого кода.
Рассмотрим код драйвера в IDA. Видим, что в DriverEntry
переписываем все элементы в структуры MajorFunction
, а именно 27, одной и той же функцией, которая выполнена в виде case switch структуры.
Соответственно, необходимо найти тот case, который будет обрабатывать наш код 0x22e0dc
, — ему соответствует case 216. Рассмотрим этот case и увидим, что внутри функции полно условных переходов jz
и jnz
. Но только в одном случае функция возвращает 1
, во всех остальных возвращается 0
. Также можно заметить, что над каждым байтом проводится набор одних и тех же действий — это «логическое и» поочередно с 1, 2, 4 , 8, 16, 32, 64, 128. Результат каждой такой операции сравнивается с нулем, количество таких повторений 22. Так происходит подготовка по описанному выше алгоритму определенной строки. Попробуем пробрутить и получить необходимую строку, будем использовать алгоритм, в котором указано одно из 22 повторений:
for i in xrange(0x0, 0x7f):
if (i & 1) == 0:
if (i & 2) == 0:
if (i & 4) == 0:
if (i & 8) != 0:
if (i & 0x10) != 0:
if (i & 0x20) != 0:
if (i & 0x40) == 0:
if (i & 0x80) == 0:
print hex(i)
print chr(i)
И так для каждого символа. В результате 22 подобных повторений получим строку «try this ioctl : 22e068». Таким образом автор задания подсказал, куда двигаться дальше. Рассмотрим case, который отвечает за этот ioctl, — это case 100. Если рассматривать эту функцию в IDA, то можно увидеть даже на графе, что она очень большая. Вряд ли автор задания заставил бы нас анализировать всю эту функцию, заглянем в ее конец. Там находится еще одна функция, внутри которой можно найти пару констант. Если использовать способ определения криптоалгоритма, как в пятом задании, то можно понять, что это Tiny Encryption Algorithm, но в нашем случае это функция шифрования, соответственно, необходимо подготовить массив для этой функции и найти ключ для дешифровки.
Один из параметров функции — это указатель на массив, который изначально забит нулями, но можно увидеть, что на каждый элемент этого массива есть указатель в других функциях, в которых уже и устанавливаются значения каждого из элементов этого массива. Ключ шифрования устанавливается в начале функции, если воспользоваться всеми полученными данными, то можно легко получить необходимый адрес электронной почты.
FLARE-On Challenge 11 Completed!
Ну вот и финальное задание. При первоначальном знакомстве с ним вроде все просто — с виду обычный PE-файл. Откроем его в IDA и увидим, что он первым делом создает и открывает на запись файл с названием secret.jpg
, также видно, что есть два исхода выполнения задания. Значит, ключом к выполненному заданию станет картинка, на которой и будет отображен адрес электронной почты.
Теперь обратим внимание на вводимый параметр командной строки для этого задания. Параметр передается на вход следующей функции, и на выходе из нее получаем какое-то другое значение. Пока не будем вдаваться в подробности, что она делает, рассмотрим функцию sub_2A1910
, в качестве параметра которой передается значение, полученное в предыдущей функции. Именно в ней и формируется необходимый secret.jpg.
В этой функции происходит много работы с секцией ресурсов: считывание различных элементов секции, а затем большое количество математических операций.
Не будем рассматривать всю криптографию, которая здесь используется, потому что для выполнения этого задания это не нужно, но если тебе интересно, то можно найти такие алгоритмы, как MD5, RC4.
На первом этапе выполнения этого задания попробуем отследить, что происходит с введенным параметром, и увидим, что по выходу из первой функции, при рассмотрении ее как black box, он преобразуется из Hex в ASCII (10=0xA или 255=0xff). Попробуем отследить, что с ним происходит дальше. Преобразованный параметр подставляется в качестве элемента одного из массива, в который был считан один из элементов секции ресурсов. Затем над этим массивом происходит набор математических операций, а именно вычисление хеша, и полученное значение сравнивается с другим массивом, в который был считан другой элемент секции ресурсов (рис. 15). Затем набор подобных действий уже выполняется в цикле с большим количеством итераций.
Поэтому первым делом подберем необходимый параметр, просто поставив breakpoint на месте сравнения, то есть в тот момент, когда происходит первая итерация сравнения полученного массива, в который подставляется преобразованный введенный параметр и от которого получается хеш, с эталонным значением (хешем).
На втором этапе решения просто сократим число итераций большого цикла до 10 (данное значение определил эмпирическим путем), просто изменив opcode (рис. 16).
В итоге получим необходимый адрес электронной почты.
Заключение
Вот мы и рассмотрели все задания. К сожалению, в рамках одной статьи не получится в полном объеме раскрыть все решения, но надеюсь, что этот материал будет хорошим подспорьем для тех, кто захочет самостоятельно пройти данный конкурс. А участвовать в таких конкурсах обязательно надо! При решении заданий всегда узнаешь что-то новое (ну или вспоминаешь забытое старое). Поэтому надеюсь, что данная статья тебя мотивирует и в следующем году мы увидим гораздо больше победителей конкурса из России.