• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Моделирование векторов атак и создание IOC-защищённых ID

Эта статья является переводом. Оригинал вот

1589209695009.png


Это первая часть статьи, состоящая из двух частей, в которой пойдет речь о применении шпионажа для разработки вредоносных ПО, при этом, устойчивым к форенсике и применении атрибуции. В этой первой части я собираюсь дать несколько советов и примеров того, как применять методологию моделирования угроз к процессу разработки, а также поделиться простой методикой, с которой я экспериментировал, исследуя методы создания и хранения данных, устойчивых к анализу финегрпринтов.

ДИСКЛЕЙМЕР: Следующее содержание предназначено только для использования в образовательных и исследовательских целях. Автор не потворствует противоправным действиям.

Основы фонового моделирования и моделирования угроз

Если вы разрабатываете какое-то программное обеспечение, где все должно быть вне поля зрения, все статическое - плохо. Статика означает, что вещи остаются такими, какие они есть, и на них можно указать пальцем в любое время и в любом месте в пространстве. Вещи, на которые можно указывать пальцем, можно назвать, что является природным действием людей, еще с детского возраста. И как однажды сказал Томас Л. Фридман (2007): "В мире идей киберпространства, назвать что-то - означает владеть этим."

Смотри, мам! Это ПОЛЯРНЫЙГУСЬ!


Да, мы говорим об атрибуции, которая не является чем-то желанным, особенно если вы проводите тайную операцию. Потому что больше нет прикрытия, о котором можно было бы говорить, и противник знает, что это вы. Мы также говорим о фингерпритах, которые опять же нежелательны, потому что это может быть использовано для обнаружения определенной особенности обьекта, будь то во время выполнения как часть антивирусного решения или после выполнения как часть процесса DFIR (Digital Forensics and Incident Response). Сегодня мы поговорим о фингерпринтах.

Прежде чем двигаться дальше, вспомним основы моделирования угроз и, в частности, варианты устранения атаки (Shostack, 2014):

Смягчение последствий
Использовать вещи, чтобы затруднить использование угрозы. (Например, используя многофакторную аутентификацию).​

Ликвидация
Отключение функции, не делая этого в первую очередь. (Например, отключение неиспользуемых услуг на компьютере).​

Перевод
Позволить кому-то другому справиться с риском. (Например, используя антивирусное программное обеспечение).​

Согласие
Принять риск, и рассматривать его как управляемый и/или не критичный для текущего дела. (Например, не делать этого в повседневной жизни (имеется ввиду без необходимости).​


Первая очевидная статическая часть ПО - это его исполняемый код, который является основным кандидатом на создание подписей, так как он может содержать множество уникальных шаблонов для снятия фингерпринтов. Что касается политики приложения, то вы не можете её принять, потому что не хотите, чтобы вас обнаружили. И поэтому, у нас остается два варианта: перенос и смягчение. Вы можете перенести угрозу, используя коммерческое решение для упаковки ПО, к примеру, печально известный - но опять же, какой бы была ваша целевая кибератака и что бы подумали про вас люди, вернее специалисты? И, наконец, вы можете смягчить это самостоятельно.

Для смягчения против фнигрепринтов в участках кода исполняемого файла существуют различные подходы. Один из старейших трюков - это использование поли/олиго/мета-морфического метода (почитать про это более детально на русском языке можно )- серьезно, эти методы – двигатели преобразования кода во множество вариаций - что значит, код не будет статичным (Szor, 2005). У каждой из этих техник есть свои предостережения, и я не буду вдаваться в подробности, однако более подробную информацию вы можете найти не только в упомянутой книге, но и .

Более продвинутые методы направлены на то, чтобы смягчить последствия от сканирования. И в этом есть смысл, если вы не видите чего-то, то вы и не сможете навести на это и как-то это обозначить. Существует также, множество вариаций таких методов. Насколько я помню, они начинаются с простого (чтобы усложнить выгрузку исполняемого файла), использование техник для запуска под маской другого процесса и, возможно, под маской «белого» программного обеспечения; и перетекают в более продвинутые техники, таким как: перехват системных вызовов для перенаправления потенциально угрожающих API вызовов (Blunden, 2012), и еще более непонятным и высоко продвинутым техникам, таким как: или .

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

Когда я говорю данные, я имею в виду читаемую и записываемую память или строки. Вот и все. И во многих случаях они вам не нужны, потому что обычно что-то читаемое и записываемое предназначено для людей. И как только мне кто-то сказал на IRC: "Если ты создаешь строки, ты делаешь это неправильно". В любом случае, основная идея заключается в том, что если вы разрабатываете низкоуровневое программное обеспечение, которое также можно использовать для разработки шпионских программ, то строки обычно являются побочным продуктом, и существуют лучшие средства для запроса чего-либо. В конце концов, обычно есть 8-битное или большее числовое значение, показывающее состояние вещи, которую вы действительно запрашиваете. То же самое можно сказать и об эксфильтрации, если телеметрическая коллекция является частью вашей операции, то не отправляйте просто огромное сообщение, которое читает Microsoft Windows 10, Build 18363.418, а кодируйте его в соответствии с вашими целевыми спецификациями в виде значения от 0 до 255 (используя что-то вроде [URL='https://web.archive.org/web/20190904161847/https://docs.microsoft.com/en-us/cpp/cpp/enumerations-cpp?view=vs-2019']enum[/URL], например), и отправляйте его в виде однобайтового значения. Таким образом, вы не только сокращаете время передачи сообщения, но и экономите место. Не используйте строки, пока вам это абсолютно необходимо. Поначалу это может показаться довольно простым правилом, но вы удивитесь, узнав, сколько разработчиков совершают эти несложные операционные ошибки.

Но предназначены ли строки только для передачи сообщения? В принципе, нет. Они также используются в качестве идентификаторов или имен, например, для получения адреса памяти , такого как file, mutex, named pipe и т.д.

Таким образом, всякий раз, когда нам нужно обозначить объект ядра, в том числе и файл на диске, мы должны дать ему имя. И если вы внимательно следили за мыслью, вы должны увидеть, к чему это приведет. Знаете ли, всякий раз, когда вы называете объект ядра, вы также оставляете подпись с вашим именем на нем, чтобы криминалисты могли найти, задокументировать и поделиться со всеми в качестве МОК. Так что давайте слегка затронем эту тему.


Генерация идентификатора, устойчивого к снятию фингерпринту

Так как же мы можем преодолеть эту проблему? Легко, просто не делайте так. Я имею в виду попытаться обойтись без именования объектов ядра и реализовать какой-нибудь другой метод. В отличие от того, что я говорил ранее, в данном случае приемлемым решением в качестве средства моделирования угроз будет устранение, так как данные имеют более весомый вес по сравнению с кодом. Если вам нужно разделить pipe или mutex, передайте его адрес с помощью чего-нибудь другого, что не требует использования предустановленного имени. Если вам нужно хранить данные для последующего использования, спросите у себя, критично ли это для работы и можно ли обойтись без состояния персистентности или стоит подумать о данных в NVRAM (Non Volatile Random Access Memory, энергонезависимая память) материнской платы (он все равно требует идентификатора, но при этом обладает характеристиками антисканирования для защиты от посторонних глаз).

В некоторых редких и ограниченных случаях вы можете перенести риск, например, используя ранее установленное программное обеспечение для защиты от записи на целевом компьютере, такое как . Записывать данные на диск? Юез проблем, Deep Freeze позаботится об этом (Но всегда RTFM (прочитайте документацию) и тестируйте, тестируйте, тестируйте; но что, если у них есть решение для out-of-band протоколирования?). Как я уже говорил, такие ситуации редкие, и эта, также не дает защиты во время выполнения.

Наконец, давайте поговорим о реальном, о смягчении. Всякий раз, когда возникает необходимость скрытно назвать что-то, один из моих любимых приемов - это генерация в случайном порядке. Вот куча строк, сгенерированных на :

Код:
s9QZEycn9aGjRBP98LWO
nWnp7DbLtgs8yIt8nRXQ
6GzgLhVPubTHp0GIwrDa
z16lns0dQ15fAzymiYC1
Fe4Peghp3qT4usvKlZxo
4rRQPSlCwRD7S4ALq3HG
PS2JtxlvzW2ICKTBXZit
STExZZd74MT9qJqTezRU
HFn6sFmLFuP9NkgFcUB6
FyqMQqk4GFf543vIv3AA


Однако, это ничего не решает. Даже если вы поместите их в исполняемый файл во время компиляции, они все равно будут уникальными. Но нам все равно, мы могли бы просто назвать ThisIsARandomString, и никакой разницы не было бы. Поэтому, нам нужно слегка подправить. А для этого нам нужно определить то, что я называю иерархиями временных доменов для генерации случайных значений (если для этого уже есть лучшее имя, пожалуйста, дайте мне знать). В принципе, существует несколько этапов, когда случайная величина может начать свое существование, отсортированное от общего к конкретному:
  • Global-time, глабольное время (т.е. генерируемое один раз, вставляемое в код и никогда не изменяемое впоследствии)
  • Compile-time, время компиляции (т.е. генерация нового значения для каждой новой компиляции кода)
  • Run-time, время выполнения (т.е. генерация значения во время выполнения кода)
  • Sub run-time, дополнительное время выполнения (т.е. генерация значения во время определенного кадра исполнения)

Они определенно не высечены в камне, и список может быть расширен или сокращён, в соответствии с вашими потребностями. Как правило, каждое случайное значение, которое мы видим вокруг, используемое в криптографии, генерируются в run-time домене. Потому что теоретически, генерируемые значения не привязаны ни к чему, кроме Вселенной - они, так сказать, универсально случайны - и поэтому они более безопасны. Но заметили ли вы, что я сказал "привязать"? Да, забавная и в нашем случае полезная характеристика генераторов случайных чисел заключается в том, что вы можете изменять их временной домен до тех пор, пока вы посылаете им значение, происходящее из вашего целевого домена.


Чтобы лучше понять, давайте продолжим на практике. Один из старейших трюков для проверки того, что экземпляр приложения уже запущен, это , например, mutex или semaphore. В основном, если объект ядра с заданным именем уже существует, это означает, что приложение уже запущено, и нет необходимости создавать новый экземпляр, а выходить из него безопасно. Но этот метод также имеет свои риски, если вы выберете очень распространенное имя, такое как Mutex1, существует высокий уровень риска, что вы столкнетесь с другим приложением. Поэтому более безопасной практикой является генерация случайного значения GUID (Global Unique Identify) в global-time и использование его в качестве предустановленного имени.

Однако, хотя это и может снизить риск столкновения, благодаря своему уникальному свойству он также может быть использован в качестве фингерпринта для создания подписи. Чтобы преодолеть это, нам необходимо уменьшить его временный домен, «посадив» его со значением, полученным от целевого домена. Например, если вы хотите использовать целевой фрейм выполнения для каждого пользователя в sub run-time, вы можете использовать имя пользователя и имя компьютера (для увеличения энтропии, в случае, если имя пользователя очень типичное, как user или jhon и т.д.) как показано нтже:

Python:
#!/usr/bin/env python3

# fingresid_poc1.py

import os
import itertools

global_guid = 'snxsqgvlslfdhhoykjhtryxsskcagymk'

# Получftv имена компьютеров и пользователей из переменных среды
subrun_seed = os.getenv('username') + os.getenv('computername')

# Преобразовуем символы в их числовые аналоги и XOR друг с другом
subrun_guid_ord = []
for chars in zip((ord(x) for x in global_guid), itertools.cycle(subrun_seed)):
    subrun_guid_ord.append(chars[0] ^ ord(chars[1]))

# Переделвыаем числовые значения в Ascii, для демонстрации мы просто распределяем их в пределах области 97-122, который соответствует строчной ASCII
subrun_guid = ''.join(chr((n - 97) % 26 + 97) for n in subrun_guid_ord)
print(subrun_guid) # e.g. iieoipsuuqjcwsnissrnkdtkjlvivwtx

Предыдущий PoC принимает global_guid и создает похожий subrun_guid, комбинируя его с именами компьютеров и пользователей, но на этот раз он находится во временном домене subrun и является уникальным значением только для этого компьютера и пользователя. В некотором смысле, это очень похоже на посол пароля перед сохранением сборник. Обратите внимание, что для демонстрации я выбрал простой, низкоэнтропический ASCII GUID в нижнем регистре. В производстве, вы должны использовать правильные GUID, но вам также придется иметь дело с их нормализацией в понятный вид.

Несмотря на то, что этот пример был достаточно хорош для демонстрации, мы все равно можем его немного подправить. Мы можем, например, объединить этот механизм смягчения с механизмом переноса, злоупотребив моделью памяти ASLR и Windows, а также изменить механизм генерации ID на альтернативный.

Python:
#!/usr/bin/env python3

# fingresid_poc2.py

import ctypes
import random
import string

#Получаем базовые адреса двух связанных системных библиотек DLL
subrun_address_kernel32 = ctypes.windll.kernel32.GetModuleHandleW('kernel32')
subrun_address_ntdll = ctypes.windll.kernel32.GetModuleHandleW('ntdll')

#Инициируем и запускаем PRNG с числовыми значениями адресов
subrun_prng = random.Random(subrun_address_kernel32 ^ subrun_address_ntdll)

# Создаем идентификатор во время выполнения, который изменяется случайным образом при каждой  загрузке
subrun_uid = ''.join(subrun_prng.choice(string.ascii_letters + string.digits) for _ in range(32))
print(subrun_uid) # e.g. GUfK0Jw628yFLmEo2kWctDd31MPAhcU1

В этом примере мы узнали базовые адреса двух общесвязанных системных DLL и использовали их в качестве посылки для инициирования подпроцессорного PRNG(Pseudorandom Number Generator). Затем мы использовали результирующий PRNG для выбора ASCII-символов и цифр для создания идентификатора длиной 32 символа, который гарантированно изменяет каждую перезагрузку или, другими словами, он находится во фрейме выполнения каждой перезагрузки в подпроцессе. Это прекрасно работает, потому что базовые адреса системных DLL переопределяются при каждой загрузке, а ASLR заботится о том, чтобы вещи не были статичными, давая нам достаточно энтропии. Вот более подробное объяснение (Yosifovich & Solomon & Ionescu & Russinovich, 2017):

...Для DLL, вычисление смещения нагрузки начинается со значения для каждой загрузки, общесистемного значения, называемого смещением изображения. Оно вычисляется с помощью MiInitializeRelocationsand, хранящегося в глобальной структуре состояния памяти (MI_SYSTEM_INFORMATION) в полях MiState.Sections.ImageBias (переменная MiImageBiasglobal в Windows 8.x/2012/R2). Это значение соответствует TSC (Time Stamp Counter) текущего процессора, когда эта функция вызывалась во время загрузочного цикла, смещалась и маскировалась в 8-битное значение. Это обеспечивает 256 возможных значений на 32-битных системах; аналогичные вычисления производятся для 64-битных систем с большим количеством возможных значений, так как адресное пространство - огромно. В отличие от исполняемых файлов, это значение вычисляется только один раз за загрузку и совместно используется в системе, что позволяет библиотекам DLL оставаться совместно используемыми в физической памяти и перемещаться только один раз. Если бы DLL-библиотеки находились в разных местах внутри разных процессов, код не мог бы использоваться совместно. Загрузчику пришлось бы по-разному устанавливать адресные ссылки для каждого процесса, таким образом, превращая то, что было доступно только для чтения, в данные частного процесса. Каждый процесс, использующий данную библиотеку DLL, должен был бы иметь свою собственную личную копию библиотеки DLL в физической памяти.

Это здорово, потому что теперь можно использовать результирующий идентификатор для создания mutex, чтобы проверить, не запущено ли уже приложение. А так как идентификатор сам по себе случайный и меняет каждую перезагрузку, то создать статический финегрпринт невозможно. Однако следует отметить, что если какое-то другое программное обеспечение на машине использует тот же самый метод, то опять же вы рискуете столкнуться с противоречием. Поэтому, вам может быть захочется комбинировать его со значением, полученным при компиляции, чтобы дифференцировать себя.


Заключительные мысли


В основном, мы сделали рандомизацию выбора символов. Следует помнить, что всякий раз, когда вы вводите еще один слой рандомизации, вы затрудняете снятие фингерпринтов. Если бы вы использовали последний пример, криминалисты создали бы такую строку IOC, как «строка длиной 32 символа, состоящая из алфавита ASCII и цифр от 0 до 9». Для того чтобы сделать ее более устойчивой, мы могли бы также случайным образом выделить длину идентификатора. Но все не так просто.

Прежде всего, если бы вы выбрали максимальное количество допустимых символов как число для вашей случайной длинны, и в результате вы бы получили что-то вроде 1337, то, скорее всего, это было бы помечено как аномалия. Потому что, серьезно, какой больной ублюдок выбрал бы имя такой длины? Таким образом, это знакомит нас с недостатком рандомизации: чем больше что-то случайное, тем более аномальным оно становится с точки зрения поведения.

И даже тогда анализ энтропии может быть использован для обнаружения странно выглядящих имен. Но у такого анализа есть свои минусы. Что, если какой-нибудь нетерпеливый пользователь создаст файл с именем типа asdjhajdhasdasdasdasdasgqwoekqehasold.xls? (Вы будете удивлены.) Так что из-за ложноположительного риска он может быть использован только в качестве вторичной подписи для дальнейшей поддержки других IOC.

Следует также уделить некоторое внимание целевым характеристикам. Например, если целевой компьютер находится в Азии, то только латинских символов может быть достаточно. Поэтому рекомендуется адаптировать выбранные вами методы в зависимости от того, на кого они нацелены.

Когда вы изменяет временной домен случайной величины, всегда помните, что более низкий временной домен иерархии всегда будет вытеснять более высокий. Поэтому всякий раз, когда вы комбинируете время компиляции с временем выполнения, результирующее значение будет находиться во временном домене. Всякий раз, когда вы комбинируете фрейм выполнения на перезагрузку с фреймом выполнения на один логин, это приводит к тому, что фрейм выполнения на один логин будет выполняться в подпроцессе и т.д. Более конкретный временной интервал -более высокий эффект. Последнее значение из наиболее определенного временного домена действует в качестве пароля, в то время как предыдущее значение из более высоких и более общих доменов действует в качестве соли.

Лучшее и более продвинутое применение этой техники может быть достигнуто за счет использования NLP (Natural Language Processing) для имитации человеческого письма, за счет использования примеров кода из публичных репозиториев.

Ну, пока это должно сработать. В следующей части я планирую поговорить о том, как можно использовать тактику чёрной пропаганды и дезинформации против попыток атрибуции.

Использованные источники
  1. Shostack, A. (2014). Threat Modeling: Designing for Security. John Wiley & Sons.
  2. Szor, P. (2005). The Art of Computer Virus Research and Defense. Addison Wesley Professional.
  3. Blunden, B. (2012). The Rootkit Arsenal: Escape and Evasion in the Dark Corners of the System (2nd ed.). Jones & Bartlett Learning.
  4. Yosifovich, P. & Solomon, D. A. & Ionescu, A. & Russinovich, M. E. (2017). Windows Internals, Part 1: System architecture, processes, threads, memory management, and more (7th ed.). Microsoft Press.
 
Последнее редактирование:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!