Написание шелкода на СИ About Хотел бы поделиться с общественностью, методом написания шелкодов и прочих инжектируемых функций на Си. В части использую Visual Studio. Статья рассчитана на людей, кто уже умеет настраивать проект, писать без использования CRT, имеет общее представление о работе с памятью. Желательно быть ознакомленным со статьёй об уменьшении размера программы (https://forum.antichat.net/thread270620.html ). Т.к. там затрагивается вопрос об отказе от CRT. Сразу у многих возникнет вопрос: Почему именно Си? Ведь Трукодеры пишут трушелкоды на ассемблере. На данный вопрос можно ответить следующим образом: Не все знают ассемблер. Найдется множество людей, кто знает Си на среднем уровне (чего достаточно), но не знает ассемблера. Когда шелкод очень маленькие, то можно написать его запросто и на ассемблере. А что делать, если он выходит в десятки килобайт (машинного кода)? Или же в нем используются очень сложные вычисления? Разработка такого шелкода на ассемблере займет очень много времени. Что делать с X64 платформой, которая активно набирает обороты? Изучать основы архитектуры? Учить ассемблер под X64 ради кусочка кода? На это уйдет много времени и тем более сил, и это не считая кучи подводных камней, которые будут проявляться при вызове WinApi. Как у любой монеты есть две стороны, так и у этого подхода есть свои плюсы и минусы. Плюсы: Скорость написания кода Хороший контроль ошибок со стороны компилятора Не надо задумываться над сложными конструкциями Очень удобная отладка (не выходя из IDE) Быстрое расширение и изменение кода Использование всех оптимизационных приёмов, которые знает компилятор Шелкод может быть настолько сложным и большим, насколько вам требуется Полнейшая и практически прозрачная поддержка X32/X64 платформ и возможность легкого расширения до X128 (если она появится когда-нибудь) Минусы: Некоторые конструкции с виду будут не логичны Небольшое увеличение размера, за счет того, что вы не можете использовать некоторые хитрости, которые доступны в ассемблере Неудобная работа со строками. Но т.к. работы со строками мало, то это особо не вносит минуса. Theory Перед началом разработки шелкода, требуется разобраться, что он будет делать и каким образом выполняться. Фактически все варианты можно сгруппировать на два больших типа: Шелкоды, выполняемые без передачи каких-либо дополнительных параметров. К таким обносятся: используемые при переполнении буфера, вызываемые сторонними функциями вследствие подмены адресов или контекста. Шелкоды, выполняемые по инициативе специальных функций, с возможностью передачи каких-либо параметров. К таким функциям относятся: CreateThread, CreateRemoteThread, QueueUserAPC или другие (самописные)которые могут передать параметр. В частности отличия одного типа от другого в том, что в первом случае требуется самостоятельно определить свою базу (местонахождение кода), затем найти адрес системных библиотек (в частности kernel32.dll она же kernelbase.dll для Win 7). Во втором случаем, мы может сразу передать структуру через параметры, где уже будут занесены адреса нужных нам функций, в частности LoadLibrary и GetProcAddress. Первый тип довольно специфичен, редко используется в повседневной деятельности и имеет как таковые минимальные предназначения (скачать и запустить или ему подобное), да и к тому же уже существует большое количество реализация под все случаи жизни. Поэтому разбирать более подробно будем второй тип, так как он применяется более часто и используется не только в плохих намереньях. Notes При разработке шелкода надо руководствоваться следующими принципами и концепциями мировоздания: Системные DLL грузятся во всех процессах по одинаковым адресам. Т.е. если адрес kernel32.dll в одном процессе будет равен XXXXXXXX, то и в другом процессе он будет такой же. Если конечно процессы одинаковой архитектуры (оба Win32 или Win64). Как следствие из первого принципа: адреса функций из системных DLL также одинаковы для всех процессов. При старте процесса (начиная с Win 2000) в его адресном пространстве всегда присутствуют уже загруженные две системные DLL: Для Win 2000 – Win 2003: ntdll.dll и kernel32.dll Для Win Vista – Win 7: ntdll.dll и kernelbase.dll kernelbase это усеченный аналог kernel32. Т.е. в нем нет многих функций. Важно будет отметить, что в kernelbase нет LoadLibraryA, LoadLibraryW, LoadLibraryExA, а есть только функция LoadLibraryExW. Поэтому для большей совместимости, пытайтесь использовать именно LoadLibraryExW, если не уверены в том, что на момент старта кода в адресном пространстве процесса присутствует библиотека kernel32. Если вам требуется загрузить DLL в чухой процесс, то никакого шелкода не надо, достаточно вызвать CreateRemoteThread на адрес LoadLibraryA с передаче параметра – адреса памяти (в чужом процессе), где хранится полный путь к DLL. Для выполнение шелкода требуется, чтобы участок памяти, где расположен шелкод, обладал правами на чтение и выполнение. EntryPoint Как бы всё красиво не выглядело бы, мы всё равно рано или поздно нарвёмся на трудности, связанные с особенностями компилятора и языка: Как выглядит шелкод? Как узнать размер шелкода? Как использовать оптимизацию при создании кода? И тем самым не испортить его. Как сохранить код шелкода? Как использовать строки? Как сделать так, чтобы можно было весь код раскидать по функциям, а не скидывать всё в одну? Как использовать WinApi функции? Step 0 Подготавливаем проект: Отключаем CRT Ставим везде оптимизацию по размеру Отключаем проверку переполнения буфера Ставим что используем С, а не С++ В настройках оптимизации ставим чтобы подставлялись функции Только __inline (/Ob1) Step 1 Шелкод может выглядеть по-разному, в зависимости от типа, но основа следующая: Code: DWORD __stdcall ShellCode_Start(SHELLCODE_PARAM* Param); Разберем по частям: DWORD – мы должны как-то уведомить функцию которая нас вызвала, о том как мы отработали. DWORD самый лучший тип для этого, да и чаще всего применяемый в Windows. __stdcall - стандартный тип вызова функция для Windows. Т.е. WinApi вызываются по такому принципу. Благодаря ему можно будет вызывать код через CreateRemoteThread и прочие функции. ShellCode_Start – просто имя. Почему именно стоит Start – далее опишу SHELLCODE_PARAM – структура которая описывает всякие дополнительные переменные, которые нам передаются вызывающей функцией. Там как раз могут быть адрес функции LoadLibrary и прочих. Данный тип определяем сами, в зависимости от ситуации. Param – как раз и есть указатель на нашу структуру, который может использовать когда нам захочется. Пример структуры: Code: #pragma pack(push, 1) // убираем выравнение, чтобы не было глюков typedef struct _SHELLCODE_PARAM { GET_PROC_ADDRESS fGetProcAddress; // адрес функции GetProcAddress HMODULE hKernel32; // адрес по которому загружена kernel32 DWORD Info; // какая-то доп инфа } SHELLCODE_PARAM, *PSHELLCODE_PARAM; #pragma pack(pop) // восстанавлвиаем выравнение которое было ранее Step 2 Узнать размер шелкода довольно проблематично на первый взгляд. Т.к. он находится где-то в файле. На деле мы можем узнать его, относительно других функций. К примеру, разместив код следующим образом: Code: DWORD __stdcall ShellCode_Start(SHELLCODE_PARAM* Param) { // код } void __stdcall ShellCode_End(){} Размер шелкода можно вычислить как: Code: int Size = (int)( (ULONG_PTR) ShellCode_End – (ULONG_PTR) ShellCode_Start); End и Start как раз и сделаны для удобства, чтобы понять что от чего надо отнимать. Но, не всё так хорошо, как хотелось бы. На деле мы видим следующее: В Debug версии шелкод довольно большой может выйти. Это происходит из-за большого кол-ва отладочной информации и всякого рода выравнения. В Release версии шелкод получается адекватный, но размер его может быть вообще отрицательным. При отключении оптимизации, всё становится нормально. Глюк Release версии заключается в том, что компилятор при оптимизации сам решает что, как и куда поставить в файле. Поэтому последовательность функций в исполняемом файле он сохранять не собирается. Отключать оптимизацию тоже не хочется, из-за того что размер увеличится. Step 3 Не всё так плачевно как кажется и оптимизацию всё же можно использовать. Microsoft позаботилась о задании порядка следования функций. При линковке можно задать последовательность функции через /ORDER, но это не всегда удобно. Есть другой вариант. Странным образом недокументированный MS (но при этом они его юзают). Вернее он документирован, но опущено важное для нас свойство. Microsoft позволяет задать для каждой функции то, в какой секции она будет находится. Но при этом они ничего не говорят про возможность установки последовательности функций в исполняемом файле. Выглядит это следующим образом: Code: // код будет в .text секции, но будет иметь метку aaa #pragma code_seg(push, ".text$aaa") DWORD __stdcall ShellCode_Start(SHELLCODE_PARAM* Param) { // код } #pragma code_seg(pop) // код будет в .text секции, но будет иметь метку aab #pragma code_seg(push, ".text$aab") void __stdcall ShellCode_End(){} #pragma code_seg(pop) Такие метки сортируются в алфавитном порядке и в данном порядке помещается в исполняемый файл. По этому мы даём понять компилятору, что ShellCode_End должна идти всегда после ShellCode_Start. Step 4 По началу может возникнуть сложность с сохранением шелкода. Но на деле нет ничего сложного. Создаем файл через CreateFile Получаем размер шелкода Через WriteFile пишем данные от ShellCode_Start. Размер данных мы уже знаем Закрываем файл. Вот и всё. Если требуется преобразовать в HEX или еще как нибудь, то можно Сделать так: Code: BYTE* p = (BYTE*) ShellCode_Start; for (x = 0; x < Size; x++) { wsprintfA(Buf, “%0.2X”, p[x]); //пишем Buf в файл или выводим на экран } Step 5 Как было сказано ранее, использование строк довольно проблематично. Если использовать конструкции типа Code: char name [] = "value123"; То значени "value123" будет находится за пределами функции, что нам не подходит. Можно извратится и сделать asm вставку, но будут проблемы с компиляцией по X64 По этому есть несколько вариантов решения проблемы средствами Си. Оба они основаны на инициализации буфера во время выполнения кода. Первый вариант: Code: const char name[] = {'v', 'a', 'l', 'u', 'e', '1', '2', '3', 0}; Метод довольно просто, и пораждает код: mov [esp + offset], символ Второй вариант сложнее, но по размеру машинного кода выходит меньше Code: char name[12]; *(DWORD*)name = 'ulav' *(DWORD*)(name + 4) = '321e' *(DWORD*)(name + 8) = 0 Т.е. как видно – строка разбивается по 4 байта, разворачивается и заносится в буфер. Главное не забывать в конце текста ставить символ с кодом ноль (конец строки) Какой метод использовать захотите то и используйте, если нет строгих ограничений на размер кода, то лучше первый вариант. Step 6 Когда шелкод умещается в одну функцию, то всё просто. Но порой приходится использовать сторонние функции (такие как копирование памяти, очистка памяти, сравнение памяти и прочие) или кода очень много и требуется разбить его на части. Но мы не может всё раскидать по частям, из-за того что нам надо на выходе получить одну функции. Решение этой проблемы такое: Использование макросов. Использование Inline функций Использование макросов ограничено их размером и неудобностью написания. Использование inline функций очень удобно, но чревато тем, что если одна и та же функция вызывается несколько раз, скорее всего она будет вставлена в код столько же раз. Если для маленьких функций это еще простительно, то для больших – будет очень сильно сказываться на размере. Также есть еще парочка подводных камней: Ключевое слов __ inline лишь советует компилятору подставлять код функции, а не вызывать. Так что не факт, что всё будет именно так, как нам хочется. Поэтому необходимо использовать __forceinline __inline и __forceinline функции должны находиться и вызваться в одном файле исходного кода. К примеру, возьмём функцию копирования памяти: Code: #ifdef WIN64 // вариант для X64. Пишем на Си т.к. напрямую асм нельзя использовать void __forceinline Mem_Copy(OUT ULONG_PTR DstAddr, IN ULONG_PTR SrcAddr, IN SIZE_T DataSize) { while (DataSize--) { *(BYTE*)DstAddr++ = *(BYTE*)SrcAddr++; } } #else // вариант для X32. Пишем на асм (так короче выходит). void __forceinline Mem_Copy(OUT ULONG_PTR DstAddr, IN ULONG_PTR SrcAddr, IN SIZE_T DataSize) { _asm { mov esi, SrcAddr mov edi, DstAddr mov ecx, DataSize rep movsb } } #endif Для X32 использовал ассемблер, только потому, что это самый маленький по кол-ву кода способ и функция будет очень часто вызываться. Хотя если нет особой нужды, то можно и для X64 и для X32 использовать один и тот же способ на Си. Step 7 С вызовом WinApi функций всё довольно просто. Алгоритм такой: Получаем адрес Сохраняем его в переменную Проверяем валидность Вызываем Чтобы не было много мороки, для каждой используемой функции можно создать тип переменной. К примеру, прототип VirtualAlloc: Code: WINBASEAPI __bcount_opt(dwSize) LPVOID WINAPI VirtualAlloc( __in_opt LPVOID lpAddress, __in SIZE_T dwSize, __in DWORD flAllocationType, __in DWORD flProtect ); В более упрощенной форме выглядят как Code: LPVOID __stdcall VirtualAlloc(LPVOID, SIZE_T, DWORD, DWORD); По этому типу переменной можно описать как Code: typedef LPVOID (__stdcall *VIRTUAL_ALLOC)(LPVOID, SIZE_T, DWORD, DWORD); Как видно, просто чуть поменялся вид. Теперь мы можем задать переменную: VIRTUAL_ALLOC fVirtualAlloc; Далее задать адрес. И использовать как обычный вызов WinApi функции. Т.е. в конечном итоге выходит такая обёртка: Code: VIRTUAL_ALLOC fVirtualAlloc; fVirtualAlloc = (VIRTUAL_ALLOC)Param->fGetProcAddress(Param->hKernel32, Str_VirtualAlloc); if (!fVirtualAlloc) { return 0; } Addr = fVirtualAlloc(NULL, SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); ExitThread C учетом вышеописанного получается следующий шаблон: Code: typedef LPVOID (__stdcall *VIRTUAL_ALLOC)(LPVOID, SIZE_T, DWORD, DWORD); typedef void* (__stdcall *GET_PROC_ADDRESS)(HMODULE, LPCSTR); #pragma pack(push, 1) typedef struct _SHELLCODE_PARAM { GET_PROC_ADDRESS fGetProcAddress; HMODULE hKernel32; DWORD Info; } SHELLCODE_PARAM, *PSHELLCODE_PARAM; #pragma pack(pop) void __forceinline Mem_Copy(OUT ULONG_PTR DstAddr, IN ULONG_PTR SrcAddr, IN SIZE_T DataSize) { while (DataSize--) { *(BYTE*)DstAddr++ = *(BYTE*)SrcAddr++; } } #pragma code_seg(push, ".text$aaa") DWORD __stdcall ShellCode_Start(SHELLCODE_PARAM* Param) { char Str_VirtualAlloc[16]; VIRTUAL_ALLOC fVirtualAlloc; *(DWORD*)((ULONG_PTR)&Str_VirtualAlloc) = 'triV'; *(DWORD*)((ULONG_PTR)&Str_VirtualAlloc + 4) = 'Alau'; *(DWORD*)((ULONG_PTR)&Str_VirtualAlloc + 8) = 'coll'; *(DWORD*)((ULONG_PTR)&Str_VirtualAlloc + 12) = 0; fVirtualAlloc = (VIRTUAL_ALLOC)Param->fGetProcAddress(Param->hKernel32, Str_VirtualAlloc); if (!fVirtualAlloc) { return 0; } Addr = fVirtualAlloc(NULL, SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); ********** } #pragma code_seg(pop) #pragma code_seg(push, ".text$aab") void __stdcall ShellCode_End(){} #pragma code_seg(pop) Перед вызовом шелкода требуется правильно заполнить структуру, указать нужные адреса функций. Важный момент: При инициализации структруры, адрес функций заполняются непостредственно через GetProcAddress. Конструкции вида Param.fGetProcAddress = (GET_PROC_ADDRESS)GetProcAddress; Или Param.fGetProcAddress = (GET_PROC_ADDRESS)&GetProcAddress; Недопустимы, из-за того, что будет ссылка не на саму функцию, а на заглушку в таблице импорта. ExitProcess Вот собственно и всё, что хотел донести до вас. Все вопросы постите в комментах, по возможности отвечу. Удачи в начинаниях! (С) SLESH 2011
Большое спасибо за статью. Не совсем понятно, для чего здесь: используем директиву OUT? Инициализация строк рулит ) Выглядит жестко, но эффективно. Еще как вариант, чтобы узнать длину функции можно от адреса следующей команды, которая идет после ret, отнять адрес начала функции. Но это более сложный вариант, так как нужно подключать дизассемблер длин.
2 Chrome~ OUT IN и прочие это фишки придуманные MS для своего компилятора. по факту они ничего не значат и можно их оформить как #define IN #define OUT Это сделано для того чтобы код был более понятнее, т.е. для отображения что делается с переменной - извлекаются данные или записываются. Очень актуально для работы с памятью. 2 _nic в пределах одно функции нет никаких VA. а именно 1) все переменные в стеке, по этому к ним обращения идут типа [esp + offset] и [ebp + offset] 2) вызов доп функций может привести к появлению VA, но у нас все функции становятся подставляются как inline по этому по факту их нет. 3) адреса апишек у нас хранятся также или локально или в структуре, по этому опять же все через стек. если есть сомнения, то при компиляции поставь опцию, чтобы создавался asm файл. И в нем можно всё хорошо пронаблюдать.
Если все запихать в main то VA небудет? И секцию кода из получившейся экзешки можно будет как шеллкод использовать?При этом не жертвую оптимизациями?
Ну можно и так. только не забывай что main - это crt и надо преензнать точку входа. Можно еще проще, каждую функцию в нужном порядка засунуть в определенную секцию (не .text) чтобы от туда уже выдрать. Хотя данный подход более удобен, потому что на выходе (после исполнения файла) можно с легкостью получить код в нужном формате
Всех с наступающим! по теме: обязательно если это точка входа в Ваш шеллкод и если она принимает параметры пишите с модификатором __declspec(dllexport) вот так потому что код заливающий шеллкод и запускающий его надеется на стандартный сишный порядок передачи аргументов или на __stdcall а при компиляции шеллкода в режиме агрессивной оптимизации точка входа может сменить соглашение с __cdecl или __stdcall на __usercall что значит компилятор в целях оптимизации может как угодно начать передавать ей аргументы я на это нарывался как то а когда мы пишем __declspec(dllexport) то создаем некий контракт на соглащение о вызове потому что предпологаеться что для этой функции в будущем будут генерировать вывоз по четко определенному сишному или __stdcall соглашению исходя из прототипа и компилятор не имеет права искажать вызов в целях оптимизации по наблюдениям код сгенрированный MSVC делает вызов по абсолютным предопределенным адресам только когда вызывает API через IAT или когда явно указатель на функцию передаеш например в функцию CreateThread это не страшно надо просто прибавлять дельту к такому указателю например CreateThread(ThreadEntry + Delta) и все все остальные вызовы например шеллкод состоит из нескольких функций дергаются по относительным адресам например call (call_me - ($ + 5)) или call (0 - ($ + 5 - call_me)) (просто две разные формулы вычисления одного и того же) или по абсолютным но вычисленным во время выполнения
в си несильно селён...сижу разбираюсь... не вкурил что за функция Addr = fVirtualAlloc(NULL.... ) что она делает ?
fVirtualAlloc это указатель на функцию VirtualAlloc, полученный с помощью функции GetProcAddress. Выполняет те же действия, что и VirtualAlloc.
_nic, а зачем тебе говорить? просто сделай глобальные данные или код в нужной тебе секции и она сама создастся. #pragma code_seg(push, ".mysection") код #pragma code_seg(pop) Для создания секций другого рода, юзаются другие имена (из-за прав доступа): code_seg - RE - для кода bss_seg - RW + для глобальный не инициализированных переменных (т.е. секция есть тока в памяти, и в файле её нет) data_seg - RW - для глобальных инициализированных переменных const_seg - RO - для глобальных инициализированных констант
есть obj файл скомпилированный ассемблером fasm с функцией которая линкуется к проекту на Си, задача - поставить экспортируемую функцию либо после кода на Си, либо заинлайнить ее, пробовал с разными опциями оптимизации , __forceinline не срабатывает, но если добавить этот модификатор к любой другой функции в коде на Си, то она инлайнится. Явное указание порядка следования функций через pragma code_seg(...) так же не помогает