- 10
- 38
Все таки решил я начать вновь писать статьи для бластхака, чтобы хоть как-то оживить этот форум, я думаю появится хоть какой-то актив в разделе реверса, мейби даже будут заходить новые люди.
Не помирать же разделу реверса-инжиниринга. Сегодня статья про анти-эмуляцию через cpuid/rdtsc.
Термины:
ВПО - вредоносные программные обеспечения.
Также учитывайте, подобную технику используют не все малвари(есть проверка DFI на чтение перед записью и т.д), она не является строгой защитой от анализа, а служит фильтром среды выполнения, позволяя отличить нативное железо от инструментированных сред, вообщем, я думаю понятно.
Основа
Современные AV решения(WD/Kasperky и т.д), уже несколько лет сталкиваются одной проблемой - довольное количество ВПО сегодня используют упаковщики. К примеру, самый популярный из них - UPX, либо, какой-то индивидуальный. Суть упаковщика - это скрытие кода угрозы, шифруя или обфусцируя исполняемый файл. Когда антивирус обнаруживает потенциально опасный файл который может навредить пк, статический анализ для него сильно затруднен, но, энтропия/PE структура, сигнатура все еще сканируется. Упакованный код выглядит как бессмысленный набор инструкций. Для решения данной проблемы антивирусы решения прибегают к динамическому анализу.
Как мы знаем, процессор это аппаратный инструмент, который понимает лишь набор байтов, ничего более - это значит, что упаковщик обязан расшифровать код в памяти и передать управление OEP, иначе процессор просто не сможет выполнить этот код.
Вкратце, когда AV сталкивается с подозрительным упакованным файлом она запускает его внутри контролируемой эмуляции. Эта среда полностью изолирована от основной ОС, что позволяет безопасно выполнять потенциально опасный код.
Однако есть одна проблема для современных AV, у подхода эмуляции есть фундаментальное ограничение, дело в том, что эмуляция не может длиться неограниченное количество времени, т.к это влияло бы на производительность системы, в плохом смысле, это создает довольно сложную дилемму для AV-разрабов, с одной стороны увеличение лимитов эмуляции позволило бы анализировать более сложные индивидуальные упаковщики, которые используют анти-эмуляционные техники, но с другой - это открыло бы возможность для атак. Поэтому AV устанавливают жесткие лимиты на выполнение кода в эмуляторе, обычно это ограничение выражается в кол-во исполняемых инструкций(чаще всего несколько миллион инструкций) или во времени выполнения, несколько миллисекунд.
Немного про старые техники анти-эмуляции
Максимально вкратце.
В далеком прошлом была техника, в нынешнее время она уже не пользуется каким-то большим спросом для разработчиков ВПО и редко кем используется, т.к детектируется эта техника почти любой EDR.
Дело в том, что во время эмуляции, AV-песочницы часто запускаются с ограниченными ресурсами, как уже это обсуждалось выше. Особенно это касалось старых версий. Они эмулировали обычно один процессор, одно ядро, чтобы не грузить систему.
ВПО просто брало, проверяло сколько ядер у системы обращаясь к структуре SYSTEM_INFO, сравнивая < 2 если = true - выход посчитав это за эмуляцию.
Как я уже писал, эта техника уже не работает, потому-что:
1. EDR просто может поставить хук на GetSystemInfo и вывести ложную информацию касаемо ядер процессора, малварь поведется, начнет свою работу, и когда упаковщик дойдет до OEP полностью распакуют себя - будет сделан дамп.
2. Современные эмуляторы научились подделывать инфу о системе, могут показать 4 или 8 ядер(кодовый пример выше)
Еще момент - на виртуальных машинах (не эмуляторах антивируса, а нормальных ВМ типа VMware) тоже могут быть много ядер. Так что метод ложноположительный, поэтому про него и забыли.
Анти-эмуляция на основе измерения времени выполнения CPUID и rdtsc
В современности, ВПО-разработчики или просто те, кто хочет защитить свой продукт используют более продвинутые техники анти-эмуляции.
Один из таких способов на основе измерения времени выполнения программы - cpuid/rdtsc
cpuid - это специальная инструкция процессора, которая возвращает информацию о cpu. Типа какая модель, какие фичи поддерживаются, сколько ядер и т.д.
В эмуляторах и песочницах AV эта инструкция часто перехватывается.
Важный момент про cpuid, эта инструкция не только получает информацию о процессоре, но и работает как некий барьер упорядочивания
Современные процессоры часто выполняют инструкции не по порядку, чтобы работать быстрее. Но когда нужно точно замерить время выполнения cpuid, важно чтобы rdtsc до и после не "перепрыгнули" через саму инструкцию cpuid.
Так вот, ее перехватывают с целью всунуть туда фейковые ответы, чтобы потенциальное ВПО думала что работает на нормальном железе(напоминает технику с GetSystemInfo)
Далее на помощь к нам приходит rdtsc. Это уже другая инструкция, которая читает счетчик тактов процессора.
Я написал программу, она как-раз таки и реализует эту технику, на практике:
На первых двук строчках я сохраняю регистры, дело в том, что инструкция процессора cpuid затирает эти регистры, поэтому их нужно сохранить, если еще более подробно cpuid затирает как 32 битные регистры(eax/ebx/ecx/edx), так и RBX/RCX. Только вот RBX мы в любом случае должны сохранить согласно ABI x64, а RCX сохраняется т.к он нам нужен.
Далее идет реализация rdtsc, чтобы получить текущее время, она возвращает 64-битное значение, но в двух частях - старшие 32 бита в edx, младшие в eax.
Далее через shl(сдвиг) сдвигаем старшую часть на место, после, or rax, rdx чтобы соеденить в одно 64-битное число, это начальное время сохраняется в регистр r8.
Сразу же обнуляем rax и вызываем cpuid, обнуляем мы регистр т.к регистр rax со значением 0 - это запрос базовой информации о процессоре, как раз эта инструкция и выполняется медленно в эмуляторах.
Сразу после cpuid вызывается rdtsc чтобы вновь получить время, после чего восстанавливаются регистры и вычитываются из конечного времени начальное, таким способом можно получить сколько тактов заняло выполнение инструкции cpuid, после чего сравниваем с порогом 1000:
Если значение больше мы переходим на локальную метку .emuliation и там выводится информация касаемо эмулирования:
Не обращайте внимание на то, как загружаются функции, функция GetProcAddress парсится вручную вместо прямого вызова, главное поймите принцип реализации техники, это самое важное.
В случае если эмуляция присутствует, мы выводим уведомление об этом с константой 10h что является MB_ICONNERROR.
В случае если нас не эмулируют, программа выводит сообщение об этом:
Так важно, учитывайте то, что я порог 1000 взял только для этой статьи, как пример, чтобы было понимание как это работает, мейби на реальном железе cpuid будет занимать 100-500 тактов, а в эмуляции больше тысячи, и результат зависит от CPU, энергосбережения, нагрузки, VM и т.д.
Проверяем работу нашей программы с помощью Kaspersky-AV
К счастью, у касперского есть свой собственный сайт где можно просканировать файл через static/dynamic анализ. Скомпилируем программу и проверим.
Размером программа вышла в 2 кб, отлично, проверяем:
И учитывая то, что программа от слова совсем не являвется легитимной, в ней используется максимально подозрительные моменты(RWX-память, ручной парсинг GetProcAddress), она прошла проверку от касперского(static/dynamic-анализ).
Удалось.
Не помирать же разделу реверса-инжиниринга. Сегодня статья про анти-эмуляцию через cpuid/rdtsc.
Термины:
ВПО - вредоносные программные обеспечения.
Также учитывайте, подобную технику используют не все малвари(есть проверка DFI на чтение перед записью и т.д), она не является строгой защитой от анализа, а служит фильтром среды выполнения, позволяя отличить нативное железо от инструментированных сред, вообщем, я думаю понятно.
Основа
Современные AV решения(WD/Kasperky и т.д), уже несколько лет сталкиваются одной проблемой - довольное количество ВПО сегодня используют упаковщики. К примеру, самый популярный из них - UPX, либо, какой-то индивидуальный. Суть упаковщика - это скрытие кода угрозы, шифруя или обфусцируя исполняемый файл. Когда антивирус обнаруживает потенциально опасный файл который может навредить пк, статический анализ для него сильно затруднен, но, энтропия/PE структура, сигнатура все еще сканируется. Упакованный код выглядит как бессмысленный набор инструкций. Для решения данной проблемы антивирусы решения прибегают к динамическому анализу.
Как мы знаем, процессор это аппаратный инструмент, который понимает лишь набор байтов, ничего более - это значит, что упаковщик обязан расшифровать код в памяти и передать управление OEP, иначе процессор просто не сможет выполнить этот код.
Код:
OEP - это оригинальная точка входа программы.
В программировании и реверс-инжиниринге - это адрес первой инструкции (начало) исполняемого файла после его распаковки в памяти, где должна начинаться работа программы. При упаковке (сжатии/защите) программы, OEP меняется на точку входа распаковщика.
Вкратце, когда AV сталкивается с подозрительным упакованным файлом она запускает его внутри контролируемой эмуляции. Эта среда полностью изолирована от основной ОС, что позволяет безопасно выполнять потенциально опасный код.
Однако есть одна проблема для современных AV, у подхода эмуляции есть фундаментальное ограничение, дело в том, что эмуляция не может длиться неограниченное количество времени, т.к это влияло бы на производительность системы, в плохом смысле, это создает довольно сложную дилемму для AV-разрабов, с одной стороны увеличение лимитов эмуляции позволило бы анализировать более сложные индивидуальные упаковщики, которые используют анти-эмуляционные техники, но с другой - это открыло бы возможность для атак. Поэтому AV устанавливают жесткие лимиты на выполнение кода в эмуляторе, обычно это ограничение выражается в кол-во исполняемых инструкций(чаще всего несколько миллион инструкций) или во времени выполнения, несколько миллисекунд.
Немного про старые техники анти-эмуляции
Максимально вкратце.
В далеком прошлом была техника, в нынешнее время она уже не пользуется каким-то большим спросом для разработчиков ВПО и редко кем используется, т.к детектируется эта техника почти любой EDR.
anti-vm:
SYSTEM_INFO emul;
GetSystemInfo(&emul);
if (emul.dwNumberOfProcessors < 2) {
exit(0)
}
ВПО просто брало, проверяло сколько ядер у системы обращаясь к структуре SYSTEM_INFO, сравнивая < 2 если = true - выход посчитав это за эмуляцию.
Как я уже писал, эта техника уже не работает, потому-что:
1. EDR просто может поставить хук на GetSystemInfo и вывести ложную информацию касаемо ядер процессора, малварь поведется, начнет свою работу, и когда упаковщик дойдет до OEP полностью распакуют себя - будет сделан дамп.
C:
void WINAPI HookGetSystemInfo(LPSYSTEM_INFO systeminfo)
{
pGetSystemInfoOriginal(systeminfo);
if (systeminfo->dwNumberOfProcessors < 2 )
{
systeminfo->dwNumberOfProcessors = 6; // показать 6 ядер
}
}
Еще момент - на виртуальных машинах (не эмуляторах антивируса, а нормальных ВМ типа VMware) тоже могут быть много ядер. Так что метод ложноположительный, поэтому про него и забыли.
Анти-эмуляция на основе измерения времени выполнения CPUID и rdtsc
В современности, ВПО-разработчики или просто те, кто хочет защитить свой продукт используют более продвинутые техники анти-эмуляции.
Один из таких способов на основе измерения времени выполнения программы - cpuid/rdtsc
cpuid - это специальная инструкция процессора, которая возвращает информацию о cpu. Типа какая модель, какие фичи поддерживаются, сколько ядер и т.д.
В эмуляторах и песочницах AV эта инструкция часто перехватывается.
Важный момент про cpuid, эта инструкция не только получает информацию о процессоре, но и работает как некий барьер упорядочивания
Современные процессоры часто выполняют инструкции не по порядку, чтобы работать быстрее. Но когда нужно точно замерить время выполнения cpuid, важно чтобы rdtsc до и после не "перепрыгнули" через саму инструкцию cpuid.
Так вот, ее перехватывают с целью всунуть туда фейковые ответы, чтобы потенциальное ВПО думала что работает на нормальном железе(напоминает технику с GetSystemInfo)
Далее на помощь к нам приходит rdtsc. Это уже другая инструкция, которая читает счетчик тактов процессора.
Я написал программу, она как-раз таки и реализует эту технику, на практике:
На первых двук строчках я сохраняю регистры, дело в том, что инструкция процессора cpuid затирает эти регистры, поэтому их нужно сохранить, если еще более подробно cpuid затирает как 32 битные регистры(eax/ebx/ecx/edx), так и RBX/RCX. Только вот RBX мы в любом случае должны сохранить согласно ABI x64, а RCX сохраняется т.к он нам нужен.
Далее идет реализация rdtsc, чтобы получить текущее время, она возвращает 64-битное значение, но в двух частях - старшие 32 бита в edx, младшие в eax.
asm:
push rbx
push rcx
rdtsc
shl rdx, 32
or rax, rdx
mov r8, rax
Сразу же обнуляем rax и вызываем cpuid, обнуляем мы регистр т.к регистр rax со значением 0 - это запрос базовой информации о процессоре, как раз эта инструкция и выполняется медленно в эмуляторах.
Сразу после cpuid вызывается rdtsc чтобы вновь получить время, после чего восстанавливаются регистры и вычитываются из конечного времени начальное, таким способом можно получить сколько тактов заняло выполнение инструкции cpuid, после чего сравниваем с порогом 1000:
Если значение больше мы переходим на локальную метку .emuliation и там выводится информация касаемо эмулирования:
ASM:
.emuliation:
mov rcx, [hKernel32]
lea rdx, [sLoadLib]
call [_GetProcAddress]
mov [_LoadLibraryA], rax
lea rcx, [sUser32]
call [_LoadLibraryA]
mov rcx, rax
lea rdx, [sMsgBox]
call [_GetProcAddress]
xor rcx, rcx
lea rdx, [Emulation]
lea r8, [titless]
mov r9d, 10h
call rax
xor eax, eax
jmp .end_proc
В случае если эмуляция присутствует, мы выводим уведомление об этом с константой 10h что является MB_ICONNERROR.
В случае если нас не эмулируют, программа выводит сообщение об этом:
asm:
.emulation_not:
mov rcx, [hKernel32]
lea rdx, [sLoadLib]
call [_GetProcAddress]
mov [_LoadLibraryA], rax
lea rcx, [sUser32]
call [_LoadLibraryA]
mov rcx, rax
lea rdx, [sMsgBox]
call [_GetProcAddress]
xor rcx, rcx
lea rdx, [sText]
lea r8, [sCaption]
mov r9d, 40h
call rax
mov rax, 1
jmp .end_proc
Так важно, учитывайте то, что я порог 1000 взял только для этой статьи, как пример, чтобы было понимание как это работает, мейби на реальном железе cpuid будет занимать 100-500 тактов, а в эмуляции больше тысячи, и результат зависит от CPU, энергосбережения, нагрузки, VM и т.д.
Проверяем работу нашей программы с помощью Kaspersky-AV
К счастью, у касперского есть свой собственный сайт где можно просканировать файл через static/dynamic анализ. Скомпилируем программу и проверим.
Размером программа вышла в 2 кб, отлично, проверяем:
И учитывая то, что программа от слова совсем не являвется легитимной, в ней используется максимально подозрительные моменты(RWX-память, ручной парсинг GetProcAddress), она прошла проверку от касперского(static/dynamic-анализ).
Удалось.
Последнее редактирование: