HOME FORUMS MEMBERS RECENT POSTS LOG IN  
× Авторизация
Имя пользователя:
Пароль:
Нет аккаунта? Регистрация
Баннер 1   Баннер 2

ANTICHAT — форум по информационной безопасности, OSINT и технологиям

ANTICHAT — русскоязычное сообщество по безопасности, OSINT и программированию. Форум ранее работал на доменах antichat.ru, antichat.com и antichat.club, и теперь снова доступен на новом адресе — forum.antichat.xyz.
Форум восстановлен и продолжает развитие: доступны архивные темы, добавляются новые обсуждения и материалы.
⚠️ Старые аккаунты восстановить невозможно — необходимо зарегистрироваться заново.
Вернуться   Форум АНТИЧАТ > ИНФО > Статьи
   
 
 
Опции темы Поиск в этой теме Опции просмотра

  #1  
Старый 23.12.2011, 17:48
slesh
Познавший АНТИЧАТ
Регистрация: 05.03.2007
Сообщений: 1,985
Провел на форуме:
3288241

Репутация: 3349


По умолчанию

Написание шелкода на СИ​


About

Хотел бы поделиться с общественностью, методом написания шелкодов и прочих инжектируемых функций на Си.

В части использую Visual Studio.

Статья рассчитана на людей, кто уже умеет настраивать проект, писать без использования CRT, имеет общее представление о работе с памятью.

Желательно быть ознакомленным со статьёй об уменьшении размера программы (https://forum.antichat.net/thread270620.html ). Т.к. там затрагивается вопрос об отказе от CRT.

Сразу у многих возникнет вопрос:

Почему именно Си? Ведь Трукодеры пишут трушелкоды на ассемблере.

На данный вопрос можно ответить следующим образом:
  1. Не все знают ассемблер. Найдется множество людей, кто знает Си на среднем уровне (чего достаточно), но не знает ассемблера.
  2. Когда шелкод очень маленькие, то можно написать его запросто и на ассемблере. А что делать, если он выходит в десятки килобайт (машинного кода)? Или же в нем используются очень сложные вычисления? Разработка такого шелкода на ассемблере займет очень много времени.
  3. Что делать с X64 платформой, которая активно набирает обороты? Изучать основы архитектуры? Учить ассемблер под X64 ради кусочка кода? На это уйдет много времени и тем более сил, и это не считая кучи подводных камней, которые будут проявляться при вызове WinApi.

Как у любой монеты есть две стороны, так и у этого подхода есть свои плюсы и минусы.

Плюсы:
  1. Скорость написания кода
  2. Хороший контроль ошибок со стороны компилятора
  3. Не надо задумываться над сложными конструкциями
  4. Очень удобная отладка (не выходя из IDE)
  5. Быстрое расширение и изменение кода
  6. Использование всех оптимизационных приёмов, которые знает компилятор
  7. Шелкод может быть настолько сложным и большим, насколько вам требуется
  8. Полнейшая и практически прозрачная поддержка X32/X64 платформ и возможность легкого расширения до X128 (если она появится когда-нибудь)

Минусы:
  1. Некоторые конструкции с виду будут не логичны
  2. Небольшое увеличение размера, за счет того, что вы не можете использовать некоторые хитрости, которые доступны в ассемблере
  3. Неудобная работа со строками. Но т.к. работы со строками мало, то это особо не вносит минуса.

Theory

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

Фактически все варианты можно сгруппировать на два больших типа:
  1. Шелкоды, выполняемые без передачи каких-либо дополнительных параметров. К таким обносятся: используемые при переполнении буфера, вызываемые сторонними функциями вследствие подмены адресов или контекста.
  2. Шелкоды, выполняемые по инициативе специальных функций, с возможностью передачи каких-либо параметров. К таким функциям относятся: CreateThread, CreateRemoteThread, QueueUserAPC или другие (самописные)которые могут передать параметр.

В частности отличия одного типа от другого в том, что в первом случае требуется самостоятельно определить свою базу (местонахождение кода), затем найти адрес системных библиотек (в частности kernel32.dll она же kernelbase.dll для Win 7).

Во втором случаем, мы может сразу передать структуру через параметры, где уже будут занесены адреса нужных нам функций, в частности LoadLibrary и GetProcAddress.

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

Поэтому разбирать более подробно будем второй тип, так как он применяется более часто и используется не только в плохих намереньях.

Notes

При разработке шелкода надо руководствоваться следующими принципами и концепциями мировоздания:
  1. Системные DLL грузятся во всех процессах по одинаковым адресам. Т.е. если адрес kernel32.dll в одном процессе будет равен XXXXXXXX, то и в другом процессе он будет такой же. Если конечно процессы одинаковой архитектуры (оба Win32 или Win64).
  2. Как следствие из первого принципа: адреса функций из системных DLL также одинаковы для всех процессов.
  3. При старте процесса (начиная с 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.
  4. Если вам требуется загрузить DLL в чухой процесс, то никакого шелкода не надо, достаточно вызвать CreateRemoteThread на адрес LoadLibraryA с передаче параметра – адреса памяти (в чужом процессе), где хранится полный путь к DLL.
  5. Для выполнение шелкода требуется, чтобы участок памяти, где расположен шелкод, обладал правами на чтение и выполнение.

EntryPoint

Как бы всё красиво не выглядело бы, мы всё равно рано или поздно нарвёмся на трудности, связанные с особенностями компилятора и языка:
  1. Как выглядит шелкод?
  2. Как узнать размер шелкода?
  3. Как использовать оптимизацию при создании кода? И тем самым не испортить его.
  4. Как сохранить код шелкода?
  5. Как использовать строки?
  6. Как сделать так, чтобы можно было весь код раскидать по функциям, а не скидывать всё в одну?
  7. Как использовать WinApi функции?
Step 0

Подготавливаем проект:
  1. Отключаем CRT
  2. Ставим везде оптимизацию по размеру
  3. Отключаем проверку переполнения буфера
  4. Ставим что используем С, а не С++
  5. В настройках оптимизации ставим чтобы подставлялись функции Только __inline (/Ob1)

Step 1

Шелкод может выглядеть по-разному, в зависимости от типа, но основа следующая:

Код:
DWORD __stdcall ShellCode_Start(SHELLCODE_PARAM* Param);
Разберем по частям:
  1. DWORD – мы должны как-то уведомить функцию которая нас вызвала, о том как мы отработали. DWORD самый лучший тип для этого, да и чаще всего применяемый в Windows.
  2. __stdcall - стандартный тип вызова функция для Windows. Т.е. WinApi вызываются по такому принципу. Благодаря ему можно будет вызывать код через CreateRemoteThread и прочие функции.
  3. ShellCode_Start – просто имя. Почему именно стоит Start – далее опишу
  4. SHELLCODE_PARAM – структура которая описывает всякие дополнительные переменные, которые нам передаются вызывающей функцией. Там как раз могут быть адрес функции LoadLibrary и прочих. Данный тип определяем сами, в зависимости от ситуации.
  5. Param – как раз и есть указатель на нашу структуру, который может использовать когда нам захочется.

Пример структуры:

Код:
#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

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

Код:
DWORD __stdcall ShellCode_Start(SHELLCODE_PARAM* Param)
{
 	// код 
}
void __stdcall ShellCode_End(){}
Размер шелкода можно вычислить как:

Код:
int Size = (int)( (ULONG_PTR) ShellCode_End – (ULONG_PTR) ShellCode_Start);
End и Start как раз и сделаны для удобства, чтобы понять что от чего надо отнимать.

Но, не всё так хорошо, как хотелось бы.

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

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

Step 3

Не всё так плачевно как кажется и оптимизацию всё же можно использовать.

Microsoft позаботилась о задании порядка следования функций.

При линковке можно задать последовательность функции через /ORDER, но это не всегда удобно.

Есть другой вариант. Странным образом недокументированный MS (но при этом они его юзают). Вернее он документирован, но опущено важное для нас свойство.

Microsoft позволяет задать для каждой функции то, в какой секции она будет находится. Но при этом они ничего не говорят про возможность установки последовательности функций в исполняемом файле.

Выглядит это следующим образом:

Код:
// код будет в .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

По началу может возникнуть сложность с сохранением шелкода. Но на деле нет ничего сложного.
  1. Создаем файл через CreateFile
  2. Получаем размер шелкода
  3. Через WriteFile пишем данные от ShellCode_Start. Размер данных мы уже знаем
  4. Закрываем файл.

Вот и всё. Если требуется преобразовать в HEX или еще как нибудь, то можно

Сделать так:

Код:
BYTE* p = (BYTE*) ShellCode_Start;
	for  (x = 0; x fGetProcAddress(Param->hKernel32, Str_VirtualAlloc);

if (!fVirtualAlloc)
{
	return 0;
}
Addr = fVirtualAlloc(NULL, SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
ExitThread

C учетом вышеописанного получается следующий шаблон:

Код:
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​
 
Ответить с цитированием
 





Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT ™ © 2001- Antichat Kft.