Раздел 2. Шеллкод

Данный раздел сосредоточен на знакомстве вас с шеллкодом, основными различными компонентами шеллкода и, чтоб более важно, как шеллкод может применяться для проверки взлома.

Эта часть книги составлена из следующих глав:

Глава 1. За и против шеллкода

Добро пожаловать в нашу первую главу этой книги и, что ещё более важно, в самое начало вашего путешествия по обучению шеллкоду и тому как он может применяться в наступательной безопасности.

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

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

Мы рассмотрим следующие вопросы:

  • Что такое шеллкод?

  • Разбиение шеллкода

  • Изучение общих типов шеллкода

Что такое шеллкод?

Сам термин шеллкод (shellcode) первоначально происходит из его цели порождать или создавать обратную оболочку (reverse shell) посредством исполнения кода. Это не имеет ничего общего со сценариями оболочки, которые, по сути, влекут за собой написание сценариев команд bash для выполнения некой задачи.

Шеллкод взаимодействует с реестрами и функциями программы, напрямую манипулируя самой программой для получения некого результата. По причине такого взаимодействия он пишется на ассемблере и затем транслируется в шестнадцатеричные коды операций. Позднее в этой главе мы рассмотрим ассемблеры и коды операций.

После выявления некой уязвимости шеллкод может применяться для использования такой уязвимости. В зависимости от сложности самой уязвимости вы можете применить несколько строк кода для её эксплуатации. При определённых обстоятельствах размер вашего шеллкода может быть весьма существенным. Суть в том, что порой получение обратной оболочки или конкретного результата может оказаться очень простым. Это приводит к очень действенной атаке, которой можно воспользоваться, если вы предоставите своей программе верные входные данные.

Примеры шеллкода

Давайте взглянем на несколько примеров. Мы начнём с рассмотрения простого фрагмента кода, написанного на C. Основная цель этого кода состоит в возврате оболочки. Значение привилегированного уровня возвращаемой оболочки будет зависеть от значения уровня привилегий самой целевой программы на момент запуска этого шеллкода. Проще говоря, наша вновь порождаемая оболочка унаследует те же самые полномочия что и её целевая программа:


#include <stdio.h>
int main()
{
    char *args[2];
    args[0] = "/bin/sh";
    args[1] = NULL;
    execve("/bin/sh", args, NULL);
    return 0;
}
 	   

Когда он скомпилирован и дальше изменён в редакторе, его можно превратить во входные строки, которые далее можно применять к уязвимости программы для получения оболочки.

Имеются дополнительные шаги, необходимые для превращения данного фрагмента кода в применимый.

Шеллкод часто применяется при атаках переполнения буфера. Проще говоря, переполнение буфера происходит, когда некая программа записывает в память данные, которые превышают зарезервированную память. В итоге такая программа может завершиться аварийно, перезаписать данные или выполнить иной код.

В приводимом далее фрагменте кода вы заметите, что этот код ожидает вода определённого числа символов. Это определено его командой Code:


#include <stdio.h>
int main()
{
    char input[12];
    printf("Please enter your password: ");
    // Когда значение пароля длиннее 12 символов, происходит переполнение буфера;
    scanf("%s", input);
    printf("Your password is %s", input);
    return(0);
}
 	   

По причине того, что отсутствует проверка входных данных и ваша программа зарезервировала для своего ввода 12 байт памяти, если вводится строка данных длиннее 12 байт, ваше приложение завершится крушением. Такое конкретное действие может не оказываться полезным если вы ищите способ получения обратной оболочки, однако это полезно когда вы имеете целью вызывать крушение приложения.

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

Теперь перейдём к более сложному примеру шеллкода. В январе 2021 года образец вредоносного кода был передан исследовательской группе Check Point. Этот образец вредоносного кода напоминал некий загрузчик, принадлежащий известной группе APT с названием Lazarus. Данный вредоносный код применял фишинговую атаку, которая включала в себя документ, загружаемый макросом, который применялся в качестве заявления о приёме на работу в LinkedIn, популярную платформу для профессионалов.

Такой макрос в этом документе пользовался шеллкодом Visual Basic for Applications (VBA), который не содержал вызывающих сомнений API, таких как VirtualAlloc, WriteProcessMemory или CreateThread. Такие типы API обычно выявляются продуктами защиты конечных пунктов, поскольку они относятся к выделению памяти, записи в память или запуске нового потока ЦПУ.

Теперь, после исполнения данного макроса VBA, он применял ряд занятных методов. Во- первых, он создал псевдонимы для различных вызовов API, чтобы его намерения были менее очевидными. Затем, для создания исполняемой области памяти он применил различные вызовы, такие как HeapCreate и HeapAlloc. Далее он воспользовался функциями подобными FindImage, которые применяли функцию API UuidFromStringA со списком жёстко кодированных значений UUID. Такая UuidFromStringA в конечном счёте предоставляет указатель на адрес в памяти кучи, позволяя его применять для декодирования данных и записи их в память не применяя такие более распространённые функции как memcpy или WriteProcessMemory. Ниже приводится фрагмент этого шеллкода; однако, здесь он выполняет свой код для запуска приложения калькулятора Windows, на который выполняется ссылка по его названию исполняемого файла, calc, однако вы можете наблюдать всю сложность данного шеллкода:


#include <Windows.h>
#include <Rpc.h>
#include <iostream>
#pragma comment(lib, "Rpcrt4.lib")
const char* uuids[] =
{
    "6850c031-6163-636c-5459-504092741551",
    "2f728b64-768b-8b0c-760c-ad8b308b7e18",
    ..snip..
};
int main()
{
    HANDLE hc = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
    void* ha = HeapAlloc(hc, 0, 0x100000);
    DWORD_PTR hptr = (DWORD_PTR)ha;
    int elems = sizeof(uuids) / sizeof(uuids[0]);
    
    for (int i = 0; i < elems; i++) {
          RPC_STATUS status = UuidFromStringA((RPC_CSTR)uuids[i], (UUID*)hptr);
          if (status != RPC_S_OK) {
               printf("UuidFromStringA() != S_OK\n");
               CloseHandle(ha);
                return -1;
        }
         hptr += 16;
    }
    printf("[*] Hexdump: ");
    for (int i = 0; i < elems*16; i++) {
        printf("%02X ", ((unsigned char*)ha)[i]);
    }
    EnumSystemLocalesA((LOCALE_ENUMPROCA)ha, 0);
    CloseHandle(ha);
    return 0;
}
 	   

Мы не будем углубляться в анализ данного шеллкода, самого VBA или этой атаки, поскольку это выходит за рамки данной книги. Цель данного примера - показать вам сложность того как выглядит шеллкод и как он может применять множество элементов.

Сопоставление шеллкода и полезной нагрузки

Поскольку мы начинаем углубляться в компоненты шеллкода, давайте проведём чёткое разграничение между шеллкодом и полезными нагрузками. Часто о них говорят как об одном и том же, однако на самом деле они отличаются.

 

Полезные нагрузки

Полезная нагрузка- это фрагмент пользовательского кода, который злоумышленник хочет запустить в системе. Этот пользовательский код может доставляться различными способами, например, в качестве сценария или даже внутри шеллкода. Примером полезной нагрузки выступает обратная оболочка, которая создаёт подключение с Приглашением командной строки Windows. Это также может быть оболочка привязки, представляющая собой полезную нагрузку, которая привязывает оболочку для ожидания по порту от целевой машины, к которой может подключаться злоумышленник. Потенциально полезная нагрузка может быть столь же простой как и набор команд для запуска в целевой операционной системе.

Представляйте себе такую полезную нагрузку как тот код, который вы желаете запустить. Он обслуживает основную цель выполнения чего- то полезного, что бы вы хотели чтобы он делал. Полезные нагрузки могут содержаться внутри шеллкода, а потому они исполняются программой.

Ниже приводится образец шеллкода, который имеет содержащуюся в нём полезную нагрузку. Эта полезная нагрузка для видимости выделена:


#include 
  unsigned char payload[] =
    "\x38\x45\xff\x48\xf7\xe7\x65\x48\x8b\x58\x60\x48\x8b\x5b\x18\x41\x6b\x5b\x20\x48\x8b\x1b\x48\x8b\x1b\x48\x8b\x5b\x20\x49\x45\xd8\x8b"
    ...snip...
  unsigned int payload_len = 205;
  exec = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  RtlMoveMemory(exec, payload, payload_len);
  rv = VirtualProtect(exec, payload_len, PAGE_EXECUTE_READ, &oldprotect);
  th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0);
  WaitForSingleObject(th, -1);
}
		

В этом примере вы заметите что у нас имеется встроенной в приводимый шеллкод полезная нагрузка. После выполнения этого шеллкода, при помощи exec = VirtualAlloc(…) выделяется память, затем выполняется ссылка на нашу полезную нагрузку при помощи …exec, payload… и, в конечном счёте запускается сама полезная нагрузка.

 

Шеллкод

Шеллкод часто применяется как часть полезной нагрузки когда эксплуатируется некая уязвимость программного обеспечения для получения управления над скомпрометированным компьютером или его эксплуатация. Представляйте себе шеллкод как набор тщательно спроектированных команд, которые способны выполняться будучи однократно внедрёнными в исполняемое приложение. в отношении уязвимости, это некий набор инструкций применяемый в качестве полезной нагрузки. В большинстве случаев такой шеллкод пишется на языке ассемблера. В основных ситуациях после исполнения его целевым компьютером такого набора инструкций, будет предоставлена командная оболочка или оболочка Meterpreter {оболочка с расширяемыми возможностями}. Это возвращает нас к её первоначально цели, как это обсуждалось во введении к данной главе, а именно к созданию некой оболочки.

Разбираем шеллкод по частям

Шеллкод может быть написанным в различных архитектурах. основные архитектуры, которые вы скорее всего наблюдаете в своей повседневной рабочей жизни это x86-64 и ARM. Между архитектурами ЦПУ x86-64 и ARM имеются большие отличия. Например, архитектура x86-64 пользуется Complex Instruction Set Computing (CISC сложным набором команд), в то время как ARM применяет Reduced Instruction Set Computing (RISC, вычисления с сокращённым набором команд).

Приводимая ниже таблица выделяет некоторые ключевые отличия между этими двумя наборами команд:

Таблица 1-1. Отличия между CISC и RISC
CISC RISC

Пользуется бо́льшим и более обогащённым функциональностью набором инструкций, допускающим более сложные инструкции для доступа к памяти

Меньшие и более упрощённые наборы инструкций, которые обычно менее чем набор из 100 инструкций

Поддерживает обработку массивов

Не поддерживает обработку массивов

Действенно применяет оперативную память

Требует больше оперативной памяти

В основном применяется в ПК и серверах

В основном используется в телефонах и устройствах IoT

В Интернете вы можете найти более подробные сведения о различиях между архитектурами CISC и RISC. Основная цель данной книги не в том чтобы погружаться в сложность архитектур ЦПУ. Тем не менее, хорошее представление об архитектуре центрального процессора вашей цели, в конечном итоге поможет вам лучше мастерить свой шеллкод.

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

Вот некий образец программы Hello World на коде языка ассемблера, специфичном для операционных систем Linux:


section.text   
global _start     ;must be declared for linker (ld)_start:            ;tells linker entry point   movedx,len     ;message length   movecx,msg     ;message to write   movebx,1       ;file descriptor (stdout)   moveax,4       ;system call number (sys_write)   int0x80        ;call kernel   moveax,1       ;system call number (sys_exit)   int0x80        ;call kernelsection.datamsg db 'Hello, world!', 0xa  ;string to be printedlen equ $ - msg     ;length of the string
 	   

После компиляции этого предыдущего кода и его выполнения он отобразит тот текст, который определён в строке kernelsection.datamsg db 'Hello World!'.

Язык ассемблера состоит из трёх основных компонентов. Это исполняемые инструкции, директивы ассемблера и макросы. Исполняемые инструкции предоставляют инструкции для самого процессора, директивы ассемблера определяют собственно сборку (assembly), а макросы предоставляют механизм подстановки текста. В своей следующей главе мы более подробно рассмотрим язык ассемблера.

Машинный язык это язык программирования очень нижнего уровня. Он пишется двоично, иначе говоря, единицами и нулями. По причине своей бинарности он запросто понимается компьютерами. Обратной стороной этого является то, что его очень сложно понимать людям. Итак, представьте что вы пытаетесь прочесть шеллкод, который пребывает в виде машинного языка - это может быть практически невозможным, в зависимости от сложности кода. Само исполнение же машинного языка происходит чрезвычайно быстро только потому, что он пребывает в бинарном формате.

Вот образец машинного языка:


1110 0001 1010 0010 0010 0011 0000 0011
 	   

Ключевым выводом из этого является то, что для применения машинного языка необходим язык ассемблера.

Наиболее распространённый тип языка, с которым вы можете сталкиваться, это язык программирования верхнего уровня. Этот тип языка более дружествен человеку и более удобочитаем. Примерами языка программирования такого типа являются C, C++ и Python. В самом начале этой главы первый пример шеллкода был написан на C - именно так выглядит язык программирования верхнего уровня.

По мере чтения данной книги вы лучше поймёте применение различных компонентов, которые составляют шеллкод. Это включает и различный инструментарий, который может применяться для создания шеллкода, преобразования кода в язык ассемблера и получение машинного кода.

Изучение обычных видов шеллкода

При тестировании на взлом могут применяться различные категории шеллкода. В конечном счёте, шеллкод можно подразделить на две основные категории: локальный и удалённый. В каждой категории имеются различные виды шеллкода, которые выполняют разные функции. В данном разделе мы рассмотрим эти различные виды шеллкода. Имейте в виду, что это не исчерпывающий перечень, ибо постоянно разрабатываются новые виды шеллкода. Давайте рассмотрим различные имеющиеся виды шеллкода, начиная с локального шеллкода и переходя к удалённому шеллкоду.

Локальный шеллкод

Локальный шеллкод запускается на целевом компьютере и не осуществляет никакой сетевой активности. Данный тип шеллкода может применяться для повышения полномочий, исполнения полезной нагрузки, порождения оболочки или прорыва в оболочку в заключении (jailed shell, ограниченной только домашним каталогом оболочки). Давайте рассмотрим некоторые примеры локального шеллкода.

 

Шеллкод execve

execve это системный вызов, который применяется внутри систем Linux для выполнения программ в своей локальной системе. Обычно он применяется для эскалации полномочий при выполнении оболочки. В самом первом образце шеллкода в самом начале данной книги вы наблюдали пример применяемого внутри шеллкода системного вызова execve.

Вы можете получить дополнительные сведения относительно execve на странице руководства (man) этого системного вызова.

Выполняя в Linux команды man execve вам представляется полное её описание:


execve - execute program
SYNOPSIS
       #include <unistd.h>
       int execve(const char *filename, char *const argv[],
                  char *const envp[]);
DESCRIPTION       
execve() исполняет указываемую filename программу.  filename обязан быть либо двоичным исполняемым, либо неким сценарием начинающегося со строки в виде.
..snip..
 	   

Обычно execve выполняется в сочетании со следующим:

  • filename: Указатель на строку, определяющую путь к исполняемому файлу

  • argv[]: Массив переменных командной строки

  • envp[]: Массив переменных среды

Прямо в самом начале этой главы был показан образец execve. Вот резюме той команды, которая конкретно связана с execve:


execve("/bin/sh", args, NULL);
		

Согласно странице руководства, execve может применяться для исполнения программы. Поскольку данный системный вызов может исполняться как исполняемым файлом, так и сценарием, она обычно применяется шеллкодом.

 

Переполнение буфера

Атаки переполнения буфера возникают локально в результате применения уязвимости. В отношении памяти буфер это используемая работающей программой область памяти. Такое местоположение является временным и в нём имеются временные данные, хранящиеся в приложении. Переполнение буфера происходит, когда длина данных на входе превосходит (превышает) значение предела этого буфера. Такое переполнение заставляет данную программу записывать данные вне пределов своего буфера, возможно, в иные участки памяти. Такой процесс приводит в крушению этой программы. Сам по себе сбой программы не опасен, но давайте предположим, что эта программа написана с применением двоичных данных, например, setuid.

Такие двоичные данные setuid в конечном итоге позволяют запускать программу с особыми привилегированными полномочиями, полномочиями привилегий системы/ root. Итак, возвращаясь к самой программе, если вы можете вызвать переполнение буфера, в конечном итоге вы можете заставить её выполнять полезную нагрузку, которая выполняет системный вызов для порождения обратной оболочки.

 

Охота за яйцами

Когда речь заходит о написании шеллкода, применяемого для эксплуатации некой программы, одной из проблем, с которыми приходится сталкиваться, выступает ограниченное пространство. Ограниченное пространство может мешать тому, что вы пытаетесь осуществить и, в конечном итоге, привести к сбою выполнения. Рассмотрим некий базовый шеллкод, основной целью которого является предоставление обратной оболочки. В зависимости от того что вы применяете, вы можете получить размер в 32 байта или более. А что если ваша целевая программа не имеет такого объёма свободного пространства в своём выделенном буфере? Что же, этот простой шеллкод не сработает.

Именно здесь вступает в игру охота за яйцами. Основная цель охоты за яйцами состоит в поиске необходимой памяти для определения яйца, которое определяется в процессе того как вы мастерите шеллкод охоты за яйцами. Такое яйцо это некое местоположение в памяти, являющееся уникальной строкой, также именуемой тегом. Как только данное яйцо будет обнаружено, будет исполнен и сам шеллкод, расположенный сразу после этого яйца.

В Главе 4, Разработка шеллкода для Windows, мы рассмотрим охоту за яйцами более подробно на некоторых примерах.

 

Внедрение DLL с отражением в шеллкоде

Shellcode reflective DLL injection (sRDI, Внедрение DLL с отражением в шеллкоде) это механизм, который позволяет вам превращать DLL в некий шеллкод без позиционирования, который впоследствии можно внедрять, применяя предпочитаемый вами метод внедрения и исполнения шеллкода.

Чтобы разобраться с тем как работает данный метод, давайте обратимся к истории. Внедрение DLL предполагает применение вредоносного DLL- файла, который считывается с диска и загружается в в какой- то целевой процесс. Хотя несколько лет назад это и срабатывало, проблема данного метода в том, что производители антивирусов уловили его и стали помечать такие типы файлов, не говоря уж об улучшениях безопасности, выполненных с течением времени производителями операционных систем. При этом у вас может иметься возможность применения совершенно новой DLL, которая ранее не наблюдалась, и всё ещё обладать шансом на успех при обычном внедрении DLL - но мы можем предположить, что такая возможность маловероятна.

Примерно в 2009 году мы начали наблюдать внедрение DLL с отражением, в котором из вредоносной DLL применялось нечто с названием ReflectiveLoader. Будучи внедрённым, такая DLL отбрасывала поток и отрабатывала обратно для локализации своей DLL и автоматически сопоставляя её. В конечном итоге вызывался бы DLLMain и ваш код исполнялся бы в памяти.

В 2015 году мы обнаружили внедрение DLL с отражением, которое позволяло вызывать некую функцию после DLLMain и допускало передачу параметров пользователя. Это стало возможным благодаря применению шеллкода и самостоятельной раскрутки перед вызовом ReflectiveLoader. Это делало для вас возможным загрузку некой DLL, вызов необходимой точки входа и передачу данных в другую экспортируемую функцию.

Если вы пожелаете взглянуть на некоторые общедоступные ссылки на такую технологию, вы можете обратиться к публикации sRDI.

Удалённый шеллкод

Удалённый шеллкод запускается на другом компьютере через сетевую среду или посредством удалённого подключения. Удалённые шеллкоды для предоставления доступа к оболочке необходимой целевой машины применяют подключение TCP/IP. Шеллкоды данного типа разбиваются на категории на основе того как они настраиваются. Например, когда ваш шеллкод присоединяется (bind) к определённому порту в своём целевом компьютере, у вас имеется bindshell (связываемая оболочка). Когда ваш шеллкод применяется для установления соединения обратно с вами, тогда у вас имеется reverse shell (обратная оболочка).

 

Bindshell

Связываемая оболочка выполняет в точности то, что предполагает её название. Она связывает вашу оболочку с конкретным портом или сокетом. По существу, ваша целевая машина выступает в качестве сервера, ожидающего подключения к определённому порту. После установления соединения, предоставляется некая оболочка. В действительности эта технология не имеет широкого применения, поскольку на своих местах в большинстве целей имеются межсетевые экраны для блокирования входящих соединений. Тем не менее, всё ещё имеется вероятность обнаружения конечной точки, обладающей допускающим подключение к ней правилом межсетевого экрана.

Вот как выглядит пример написанного на C bindshell:


#include <stdio.h>
...snip..
int main ()
{
    struct sockaddr_in addr;    
  addr.sin_family = AF_INET;
    addr.sin_port = htons(4444);    
  addr.sin_addr.s_addr = INADDR_ANY;
  ...snip..
{
  ...snip..
  }
    execve("/bin/sh", NULL, NULL);    
return 0;
}
 	   

В нашем предыдущем примере для создания некого сокета IPv4 применяется использование AF_INET. Затем у нас имеется определяемый addr.sin_port порт и, в самом конце, у нас располагается execve, который применяется для порождения оболочки.

 

Выгрузить и исполнить

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

В современных средах продукты фильтрации веб обладают рядом расширений для блокирования потенциально вредоносного обмена. Такими расширениями обладают ещё более новые веб браузеры, например, SmartScreen в Microsoft Edge. Такие функциональные возможности создают ряд проблем при попытке заставить цель выполнять загрузку с диска или исполнения шеллкода, который пользуется явно вредоносными шаблонами.

Однако, даже при таких достижениях обнаружения всё ещё можно заставлять шеллкод загружать и выполнять нечто, например, при помощи urlmon.dll и одного из его API с названием URLDownloadToFileA.

Выводы

В данной главе мы рассмотрели основы шеллкода. Вы изучили чем именно является шеллкод и взглянули на некоторые образцы в диапазоне от простого шеллкода до сложного. Мы рассмотрели отличия между шеллкодом и полезными нагрузками и подразделили шеллкод на компоненты. Как вы видели, шеллкод требует хорошего понимания наборов инструкций, оперативной памяти и различных языков программирования. Вы ознакомились с тем потоком, коим интерпретируется шеллкод компьютерами в виде машинного языка и языка ассемблера. Наконец, мы ознакомились с различными типами применяемого на земле шеллкода.

В своей следующей главе мы окунёмся в язык ассемблера. Вы изучите что такое язык ассемблера, имеющиеся типы языка ассемблера, основные компоненты, составляющие язык ассемблера и то, как они работают.