[Перевод] bdshemu: эмулятор шелл-кода в Bitdefender
Совсем скоро, 19 ноября, у нас стартует курс «Этичный хакер», а специально к этому событию мы подготовили этот перевод о bdshemu — написанном на языке C эмуляторе с открытым исходным кодом в Bitdefender для обнаружения эксплойтов на 32- и 64-битной архитектуре. Эмулятор очень прост, а благодаря нацеленности на уровень инструкций он работает с любой операционной системой. Кроме того, этот эмулятор зачастую сохраняет расшифрованный эксплойт в бинарный файл. Подробности и пример обнаружения Metasploit — под катом, ссылка на репозиторий проекта на Github — в конце статьи.
Введение
Обнаружение эксплойтов — одна из основных сильных сторон Hypervisor Memory Introspection (HVMI). Возможность мониторинга гостевых страниц физической памяти на предмет различных видов доступа, таких как запись или выполнение, позволяет HVMI накладывать ограничения на критические области памяти: например, страницы стека или кучи могут быть помечены как невыполняемыми на уровне EPT, поэтому, когда эксплойту удается получить доступ к произвольному выполнению кода, вмешивается логика интроспекции и блокирует выполнение шелл-кода.
Теоретически перехвата попыток выполнения из областей памяти, например стека или кучи, должно быть достаточно, чтобы предотвратить действия большинства эксплойтов. Реальная жизнь часто бывает сложнее, и бывает много случаев, когда легальное программное обеспечение использует приемы, напоминающие атаку — JIT-компиляция в браузерах — один из хороших примеров. Кроме того, злоумышленник может хранить полезную нагрузку в других областях памяти, за пределами стека или кучи, поэтому полезно распознавание хорошего и плохого кода.
В этом посте поговорим об эмуляторе Bitdefender Shellcode Emulator, или, для краткости, bdshemu. Это библиотека, способная эмулировать базовые инструкции x86, наблюдая при этом похожее на шелл-код поведение. Легальный код — например JIT-код — будет выглядеть иначе, чем традиционный шелл-код, bdshemu пытается определить, ведёт ли себя код в эмуляции, как шелл-код.
Обзор bdshemu
bdshemu — библиотека на C, частью проекта bddisasm (и, конечно же, она использует bddisasm для расшифровки инструкций). Библиотека bdshemu создана только для эмуляции кода x86, поэтому не имеет поддержки вызовов API. На самом деле, среда эмуляции сильно ограничена и урезана, доступны только две области памяти:
- Содержащие эмулируемый код страницы.
- Стек.
Обе эти области виртуализированы, то есть на самом деле являются копиями эмулируемой фактической памяти, поэтому внесенные в них изменения не влияют на фактическое состояние системы. Любой доступ эмулируемого кода за пределы этих областей (которые мы будем называть шелл-кодом и стеком соответственно), спровоцирует немедленное завершение эмуляции. Например, вызов API автоматически вызовет ветку вне области шелл-кода, тем самым прекратив эмуляцию. Однако в bdshemu всё, что нас волнует — это поведение кода на уровне инструкций, которого достаточно, чтобы понять, вредоносен код или нет.
Хотя bdshemu предоставляет основную инфраструктуру для обнаружения шелл-кодов внутри гостевой операционной системы, стоит отметить, что это не единственный способ, которым HVMI определяет, что выполнение определённой страницы является вредоносным. Используются ещё два важных индикатора:
- Выполняемая страница находится в стеке — это обычное дело для уязвимостей на основе стека.
- Подделка стека — когда страница выполняется впервые, а регистр RSP указывает за пределы выделенного для потока обычного стека.
Этих двух индикаторов достаточно, чтобы запустить обнаружение эксплойтов. Если эксплойты не обнаружили себя, bdshemu внимательно изучает исполняемый код и решает, нужно ли его блокировать.
Архитектура bdshemu
bdshemu создаётся как отдельная библиотека C и зависит только от bddisasm. Работать с bdshemu довольно просто, поскольку у этих двух библиотек имеется общий API:
SHEMU_STATUS
ShemuEmulate(
SHEMU_CONTEXT *Context
);
Эмулятор ожидает единственный аргумент SHEMU_CONTEXT
, содержащий всю необходимую для эмуляции подозрительного кода информацию, то есть контекст. Этот контекст разделен на две части: входные и выходные параметры.
Входные параметры должны предоставляться вызывающей стороной, и они содержат такую информацию, как код для эмуляции или начальные значения регистров. Выходные параметры содержат такую информацию, как индикаторы шелл-кода, обнаруженные bdshemu. Все эти поля хорошо документированы в исходном коде.
Первоначально контекст заполняется следующей основной информацией (обратите внимание, что результат эмуляции может измениться в зависимости от значения предоставленных регистров и стека):
- Входные регистры, такие как сегменты, регистры общего назначения, регистры MMX и SSE; их можно оставить в значении 0, если они неизвестны или не актуальны.
- Входной код, то есть код для эмуляции.
- Входной стек, который может содержать фактическое содержимое стека, или может быть оставлен со значением 0.
- Информация о среде, например режим (32 или 64 бита) или кольцо (0, 1, 2 или 3).
- Параметры управления: минимальная длина строки стека, минимальная длина цепочки NOP или максимальное количество инструкций, которые должны быть эмулированы.
Основной выходной параметр — поле Flags
, которое содержит список обнаруженных во время эмуляции индикаторов шелл-кода. Как правило, ненулевое значение этого поля убедительно свидетельствует, что код эмуляции — это шелл-код.
bdshemu построен как быстрый и простой эмулятор инструкций x86. Поскольку он работает только с самим шелл-кодом и небольшим виртуальным стеком, ему не нужно имитировать какие-то архитектурные особенности — прерывания или исключения, таблицы дескрипторов, таблицы страниц. Кроме того, поскольку мы имеем дело только с шелл-кодом и стековой памятью, bdshemu не выполняет проверку доступа к памяти потому, что не разрешает доступ даже к другим адресам. Единственное состояние, кроме регистров, к которому можно получить доступ, — это сам шелл-код и стек, и они оба — копии фактического содержимого памяти. То есть состояние системы никогда не изменяется во время эмуляции, изменяется только предоставленный SHEMU_CONTEXT
. Это делает bdshemu чрезвычайно быстрым, простым и позволяет сосредоточиться на его основной цели: обнаружении шелл-кода.
Что касается поддержки инструкций, bdshemu поддерживает все основные инструкции x86, такие как ветвления, арифметика, логика, сдвиг, манипуляции с битами, умножение и деление, доступ к стеку и инструкции передачи данных. Кроме того, он также поддерживает другие инструкции, например, некоторые базовые инструкции MMX
или AVX
. Два хороших примера — PUNPCKLBW
и VPBROADCAST
.
Методы обнаружения bdshemu
Есть несколько индикаторов, которые использует bdshemu. Чтобы определить, ведёт ли себя эмулируемый фрагмент кода как шелл-код.
NOP Sled
Это классическое представление шелл-кода; поскольку точка его входа при выполнении может быть неизвестна точно, злоумышленники обычно добавляют длинную последовательность инструкций NOP, закодированную 0×90. Параметры длины последовательностей NOP можно контролировать при вызове эмулятора через контекстное поле NopThreshold
. Значение SHEMU_DEFAULT_NOP_THRESHOLD
по умолчанию равно 75. Это означает, что минимум 75% всех эмулируемых инструкций должны быть инструкциями NOP.
RIP Load
Шелл-код задуман так, чтобы работать правильно, независимо от того, по какому адресу он загружен. Это означает, что шелл-код должен во время выполнения динамически определять адрес, по которому был загружен, поэтому абсолютную адресацию можно заменить некоторой формой относительной адресации. Обычно это делается получением значения указателя инструкции с помощью хорошо известных методов:
CALL $ + 5/POP ebp
— выполнение этих двух инструкций приведёт к тому, что значение указателя инструкции сохранится в регистреebp
; затем можно получить доступ к данным внутри шелл-кода, используя смещения относительно значенияebp
FNOP/FNSTENV [esp-0xc]/POP edi
— первая инструкция — это любая инструкцияFPU
(не обязательноFNOP
), а вторая инструкция —FNSTENV
— сохраняет средуFPU
в стеке; третья инструкция получит указатель инструкцииFPU
изesp-0xc
, который является частью средыFPU
и содержит адрес последнего выполненногоFPU
— в нашем случаеFNOP
. С этого момента для доступа к данным шелл-кода можно использовать адресацию относительноedi
- Внутренне bdshemu отслеживает все экземпляры указателя инструкции, сохранённые в стеке. Последующая загрузка указателя инструкции из стека каким-либо образом приведёт к срабатыванию этого обнаружения. Благодаря тому, что bdshemu отслеживает сохранённые указатели инструкций, не имеет значения, когда, где и как шелл-код пытается загрузить регистр
RIP
и использовать его: bdshemu всегда будет запускать обнаружение.
В 64-битном режиме относительная адресация RIP может использоваться напрямую: это позволяет кодировка инструкций. Однако, как ни странно, большое количество шелл-кода по-прежнему использует классический метод получения указателя инструкций (обычно технику CALL/POP), но, вероятно, указывает на то, что 32-битные шелл-коды были перенесены на 64-битные с минимальными изменениями.
Запись шелл-кода самим шелл-кодом
Чаще всего шелл-код закодирован или зашифрован, чтобы избежать некоторых плохих символов (например 0x00
, который должен напоминать строку, может сломать эксплойт), или чтобы избежать обнаружения технологиями безопасности — например, AV-сканерами. Это означает, что во время выполнения шелл-код должен декодировать себя — обычно на месте — изменяя свое собственное содержимое, а затем выполняя текстовый код. Типичные методы декодирования включают алгоритмы дешифрования на основе XOR
или ADD
.
Конечно, bdshemu знает о таком поведении и отслеживает каждый изменённый внутри шелл-кода байт. Каждый раз, когда предполагаемый шелл-код записывает какую-то часть себя, а затем выполняет её, срабатывает обнаружение самостоятельной записи шелл-кода.
Доступ к TIB
После того как шелл-код получил возможность выполнения, ему необходимо найти несколько функций внутри разных модулей, чтобы позаботиться о полезной нагрузке (например, загрузке файла или создании процесса). В Windows наиболее распространенный способ сделать это — разобрать структуры загрузчика пользовательского режима, чтобы найти адреса, по которым загружены требуемые модули, а затем найти необходимые функции внутри этих модулей. Ниже дана последовательность структур, к которым будет обращаться шелл-код:
- Блок среды потока (TEB), который расположен в
fs: [0]
(32-битный поток) илиgs: [0]
(64-битный поток). - Блок среды процесса (PEB), который расположен по адресу TEB + 0×30 (32 бит) или TEB + 0×60 (64 бит).
- Информация о загрузчике (PEB_LDR_DATA), расположенная внутри PEB.
Внутри PEB_LDR_DATA
есть несколько списков, содержащих загруженные модули. Шелл-код будет перебирать эти списки, чтобы найти столь необходимые ему библиотеки и функции.
При каждом обращении к памяти bdshemu увидит, пытается ли предполагаемый шелл-код получить доступ к полю PEB внутри TEB. bdshemu отслеживает обращения к памяти, даже если они выполняются без классических префиксов сегментов fs/gs
— до тех пор, пока идентифицирован доступ к полю PEB внутри TEB, будет срабатывать обнаружение доступа к TIB.
Направленный вызов SYSCALL
Легитимный код будет полагаться на несколько библиотек для вызова служб операционной системы — например для создания процесса в Windows обычный код будет вызывать одну из функций CreateProcess
. Легитимный код редко вызывает SYSCALL
напрямую, поскольку интерфейс SYSCALL
со временем может измениться. По этой причине bdshemu запускает обнаружение SYSCALL
всякий раз, когда обнаруживает, что предполагаемый шелл-код напрямую вызывает системную службу с помощью инструкций SYSCALL
, SYSENTER
или INT
.
Строки стека
Другой распространённый способ маскировки содержимого шелл-кода — динамическое построение строк в стеке. Это может устранить необходимость в написании независимого от позиции кода (PIC), поскольку шелл-код будет динамически строить нужные строки в стеке, вместо того, чтобы ссылаться на них внутри шелл-кода как на обычные данные. Типичные способы сделать это — сохранить содержимое строки в стеке, а затем ссылаться на строку с помощью указателя стека:
push 0x6578652E
push 0x636C6163
В приведённом выше коде в стеке сохраняется строка calc.exe
, которую затем можно использовать как обычную строку во всем шелл-коде.
Для каждого сохранённого в стеке значения, напоминающего строку, bdshemu отслеживает общую длину созданной в стеке строки. Как только порог, указанный полем StrLength
внутри контекста, будет превышен, будет запущено обнаружение строки стека. Значение по умолчанию для этого поля SHEMU_DEFAULT_STR_THRESHOLD
равно 8. Это означает, что динамическое построение строки длиной не менее 8 символов в стеке вызовет это обнаружение.
Методы обнаружения для шелл-кода режима ядра
Хотя вышеупомянутые методы общие и могут применяться к любому шелл-коду, в любой операционной системе, как в 32-, так и в 64-битной версии (за исключением обнаружения доступа к TIB, которое специфично для Windows) bdshemu может определять специфичное для ядра поведение шелл-кода.
Доступ KPCR
Область управления процессором ядра (KPCR) — это структура для каждого процессора в системах Windows, которая содержит много критически важной для ядра информации, но также может быть полезной для злоумышленника. Обычно шелл-код может ссылаться на текущий выполняющийся поток, который можно получить, обратившись к структуре KPCR, со смещением 0x124
в 32-битных системах и 0x188
в 64-битных системах. Так же, как и в методе обнаружения доступа к TIB, bdshemu отслеживает обращения к памяти, и когда эмулируемый код считывает текущий поток из KPCR, он запускает обнаружение доступа к KPCR.
Выполнение SWAPGS
SWAPGS
— это системная инструкция, которая выполняется только при переходе из пользовательского режима в режим ядра, и наоборот. Иногда, из-за специфики определенных эксплойтов ядра злоумышленнику в конечном счёте нужно выполнить SWAPGS
— например, полезная нагрузка ядра EternalBlues
, как известно, перехватывает обработчик SYSCALL
, поэтому нужно выполнить SWAPGS
, когда состоялся SYSCALL
, подобно тому, как это сделал бы обычный системный вызов.
bdshemu запускает обнаружение SWAPGS
всякий раз, когда инструкция SWAPGS выполняется подозрительным шелл-кодом.
Чтение и запись MSR
Иногда шелл-код (например вышеупомянутая полезная нагрузка ядра EternalBlue) должен изменить обработчик SYSCALL
, чтобы перейти в стабильную среду выполнения (например, потому что исходный шелл-код выполняется в высоких значениях диапазона IRQL, которые необходимо снизить перед вызовом полезных подпрограмм). Это делается путём изменения MSR SYSCALL
с помощью инструкции WRMSR
, а затем ожидания выполнения системного вызова (который находится на более низком уровне IRQL) для продолжения выполнения (здесь также пригодится метод SWAPGS
потому, что на 64-битной версии SWAPGS
должна выполняться после каждого SYSCALL
).
Кроме того, чтобы найти образ ядра в памяти и, следовательно, полезные процедуры ядра, быстрый и простой способ — запросить SYSCALL MSR
(которая обычно указывает на обработчик SYSCALL
внутри образа ядра), а затем пройти по страницам назад, пока не будет найдено начало образа ядра.
bdshemu будет запускает обнаружение доступа MSR
всякий раз, когда подозрительный шелл-код обращается к MSR SYSCALL
, (как в 32-, так и в 64-битном режиме).
Пример
Проект bdshemu содержит несколько синтетических тестовых примеров, но лучший способ продемонстрировать его функциональность — использовать реальный шелл-код. В этом отношении Metasploit замечателен в создании разных видов полезной нагрузки с использованием всех видов кодировщиков. В качестве чисто дидактического [прим. перев. — назидательного] примера возьмём такой код:
DA C8 D9 74 24 F4 5F 8D 7F 4A 89 FD 81 ED FE FF
FF FF B9 61 00 00 00 8B 75 00 C1 E6 10 C1 EE 10
83 C5 02 FF 37 5A C1 E2 10 C1 EA 10 89 D3 09 F3
21 F2 F7 D2 21 DA 66 52 66 8F 07 6A 02 03 3C 24
5B 49 85 C9 0F 85 CD FF FF FF 1C B3 E0 5B 62 5B
62 5B 02 D2 E7 E3 27 87 AC D7 9C 5C CE 50 45 02
51 89 23 A1 2C 16 66 30 57 CF FB F3 9A 8F 98 A3
B8 62 77 6F 76 A8 94 5A C6 0D 4D 5F 5D D4 17 E8
9C A4 8D DC 6E 94 6F 45 3E CE 67 EE 66 3D ED 74
F5 97 CF DE 44 EA CF EB 19 DA E6 76 27 B9 2A B8
ED 80 0D F5 FB F6 86 0E BD 73 99 06 7D 5E F6 06
D2 07 01 61 8A 6D C1 E6 99 FA 98 29 13 2D 98 2C
48 A5 0C 81 28 DA 73 BB 2A E1 7B 1E 9B 41 C4 1B
4F 09 A4 84 F9 EE F8 63 7D D1 7D D1 7D 81 15 B0
9E DF 19 20 CC 9B 3C 2E 9E 78 F6 DE 63 63 FE 9C
2B A0 2D DC 27 5C DC BC A9 B9 12 FE 01 8C 6E E6
6E B5 91 60 F2 01 9E 62 B0 07 C8 62 C8 8C
Сохранение этого кода как двоичного файла как shellcode.bin
с последующим просмотром его содержимого дает плотно упакованный фрагмент кода, весьма характерный для зашифрованного шелл-кода:
В disasmtool — инструменте проекта bddisasm, для запуска эмулятора шелл-кода на входе можно воспользоваться параметром -shemu
.
disasmtool -b32 -shemu -f shellcode.bin
Выполнение отобразит пошаговую информацию о каждой эмулируемой команде, но так как эта трассировка длинная, давайте перейдем непосредственно к концу if
:
Emulating: 0x0000000000200053 XOR eax, eax
RAX = 0x0000000000000000 RCX = 0x0000000000000000 RDX = 0x000000000000ee00 RBX = 0x0000000000000002
RSP = 0x0000000000100fd4 RBP = 0x0000000000100fd4 RSI = 0x0000000000008cc8 RDI = 0x000000000020010c
R8 = 0x0000000000000000 R9 = 0x0000000000000000 R10 = 0x0000000000000000 R11 = 0x0000000000000000
R12 = 0x0000000000000000 R13 = 0x0000000000000000 R14 = 0x0000000000000000 R15 = 0x0000000000000000
RIP = 0x0000000000200055 RFLAGS = 0x0000000000000246
Emulating: 0x0000000000200055 MOV edx, dword ptr fs:[eax+0x30]
Emulation terminated with status 0x00000001, flags: 0xe, 0 NOPs
SHEMU_FLAG_LOAD_RIP
SHEMU_FLAG_WRITE_SELF
SHEMU_FLAG_TIB_ACCESS
Мы видим, что последняя эмулированная инструкция — MOV edx, dword ptr fs: [eax + 0x30]
— это инструкция доступа к TEB, но она также запускает эмуляцию, которая должна быть остановлена, поскольку это — доступ за пределы памяти шелл-кода (вспомним, что bdshemu остановится при первом обращении к памяти вне шелл-кода или стека). Более того, этот небольшой шелл-код (сгенерированный с помощью Metasploit) вызвал 3 обнаружения в bdshemu:
SHEMU_FLAG_LOAD_RIP
— шелл-код загружает RIP в регистр общего назначения, чтобы определить его позицию в памяти.SHEMU_FLAG_WRITE_SELF
— расшифровывает сам себя, а затем выполняет расшифрованные фрагменты.- SHEMU_FLAG_TIB_ACCESS — обращается к PEB, чтобы найти важные библиотеки и функции.
Этих срабатываний более чем достаточно, чтобы сделать вывод, что эмулируемый код, без сомнения, является шелл-кодом. Что еще более удивительно в bdshemu, так это то, что обычно в конце эмуляции память содержит расшифрованную форму шелл-кода. disasmtool достаточно хорош, чтобы сохранить память шелл-кода после завершения эмуляции: создаётся новый файл с именем shellcode.bin_decoded.bin
, содержащий декодированный шелл-код. Давайте посмотрим на него:
Глядя на декодированный шелл-код, можно сразу увидеть не просто отличия, но и простой текст — зоркий глаз быстро обнаружит строку calc.exe
в конце шелл-кода, намекая нам, что это классический calc.exe
, порождающий шелл-код.
Заключение
В этом посте мы представили эмулятор шелл-кода Bitdefender, который является важной частью технологии обнаружения эксплойтов HVMI. bdshemu построен для обнаружения индикаторов шелл-кода на уровне двоичного кода, без необходимости эмулировать сложные вызовы API, сложное расположение памяти или архитектурные объекты, такие как таблицы страниц, таблицы дескрипторов и т. д. — bdshemu фокусируется на том, что имеет наибольшее значение, эмулируя инструкции и определяя шелл-код по поведению.
Благодаря своей простоте bdshemu работает с шелл-кодом, нацеленным на любую операционную систему: большинство методов обнаружения определены на уровне поведения инструкций, а не на высокоуровневом поведении (например на уровне вызовов API). Кроме того, он работает как с 32-битным, так и с 64-битным кодом, а также с кодом, специфичным для режимов пользователя или ядра.
Ссылка на Github
На тот случай если вы задумали сменить сферу или повысить свою квалификацию — промокод HABR даст вам дополнительные 10% к скидке указанной на баннере.