R0 CREW

Приготовление вкусного Shellcode под Windows. Часть 0x01

shellcode
coding
ru
#1

⁠Привет! Всех с наступающим, с вами снова Жорж, давайте продолжим писать шеллкоды :sunglasses:

Часть 0x00. Низкоуровневый шеллкод
Часть 0x01. Высокоуровневый шеллкод

В прошлой статье мы создавали шеллкод на ассемблере, теперь попробуем создать более высокоуровневый шеллкод на С. В это статье я буду рассматривать компилятор Visual C++(2010 и 2012), мы будем писать С-шный код (без применения С++) под архитектуры IA86 и AMD64.

Для тех кому матчасть не интерестна, предлагаю готовый пример : тык

Основная идея

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

Проблемы разработки

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

Извлечение шеллкода

Так как у нас нет возможности манипуляции над бинарной структурой, то самый универсальный способ получение шеллкода, это извлечение его из готовых PE (.exe/.dll) файлов. Структура PE в отличии от обьектных файлов хорошо документирована и описана, а компиляторы предоставляют достаточно средств для минимальной манипуляции над укладкой данных.
Очевидно, чтобы извлечь шеллкод из .exe, нам необходимо уложить код и данные таким образом, чтобы их можно было без проблем вытащить. Для этого воспользуемся препроцессорными директивами и отделим код и данные шеллкода в отдельную секцию. Как это сделать рассмотрим чуть позже.

Компиляция

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

Visual Studio 2010-2012

  1. Отключить подстановки всякого ненужного хлама:
    Project Propeties -> C++\C -> Code generation
    Busic Runtime Checks = Uninitialized variables (/RTCu)
  • В случае несовместимости с оптимизатором, оставить поле пустым
  1. Включаем Enable Function-Level Linking, чтобы функции укладывались согласно их порядку в исходнике
    Project Propeties -> C++\C -> Code generation
    Enable Function-Level Linking = Yes (/Gy)

  2. Выключаем Incremental Linking, чтобы в промежутке между нашими функциями не вставлялись инструкции JMP
    Project Propeties -> Linker -> General
    Enable Incremental Linking = No (/INCREMENTAL:NO)

Проблемы базонезависимости

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

  • прямых адресов и релокаций
  • неявные подстановки вызовов (memcpy, memset и т.д.)

Шеллкод не может содержать прямых адресов с релокациями, иначе теряется свойство базонезависимости. Это проблематично для архитектуры x86, и частично для x64, потому что там используется RIP-относительная адресация, а следовательно прямых адресов в коде нет, но они могут быть в секции данных, об этом поговорим позже.

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

Разработка

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

Укладка кода и данных

Поскольку нам необходимо создать шеллкод однародным куском данных, придётся пошаманить. Сразу под шеллкод создадим отдельные файлы shellcode.h и shellcode.c, далее в сишном файле пропишем следующую директиву, которая отделит код из данного файла в отдельную секцию .shell :

#pragma code_seg("shell")

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

Теперь нам нужно подумать как уложить данные в нашу секцию. Конечно мы можем сделать так же как сделали с кодом немного пошаманив над компилятором, это справедливо для архитектуры AMD64, но не для IA86, так как команды доступа к данным будут содержать прямые адреса. Если честно это достаточно узкое место, я видел некоторые решения в интернете, от которых меня будоражило. Ассемблерные вставки рассмыстривать не будет, так как их не поддерживает студия для 64 битной компиляции. Более менее нормальные техники укладки данных, является хранение их в качестве литералов, например :

char str[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', '.', 'd', 'l', 'l', 0 };

Но у такого подхода есть большой недостаток, взглянем на листинг :

00182003  |.  83EC 0C       SUB ESP,0C
00182006  |.  C645 F4 77    MOV BYTE PTR SS:[LOCAL.3],77
0018200A  |.  C645 F5 73    MOV BYTE PTR SS:[LOCAL.3+1],73
0018200E  |.  C645 F6 32    MOV BYTE PTR SS:[LOCAL.3+2],32
00182012  |.  C645 F7 5F    MOV BYTE PTR SS:[LOCAL.3+3],5F
00182016  |.  C645 F8 33    MOV BYTE PTR SS:[LOCAL.2],33
0018201A  |.  C645 F9 32    MOV BYTE PTR SS:[LOCAL.2+1],32
0018201E  |.  C645 FA 2E    MOV BYTE PTR SS:[LOCAL.2+2],2E
00182022  |.  C645 FB 64    MOV BYTE PTR SS:[LOCAL.2+3],64
00182026  |.  C645 FC 6C    MOV BYTE PTR SS:[LOCAL.1],6C
0018202A  |.  C645 FD 6C    MOV BYTE PTR SS:[LOCAL.1+1],6C
0018202E  |.  C645 FE 00    MOV BYTE PTR SS:[LOCAL.1+2],0

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

Очень не хватает возможностей ассемблера, так почему же не воспользоватся ими?) Сведём к минимуму базозависимый код и посадим его на препроцессор балансируя переносимость.

Подключение MASM’a в Visual Studio 2010-2012 :

Project -> Build Customizations

И устанавливаем галочку на masm, подключая ассемблер к нашему проекту, далее в проекте создаём файл с расширением .asm в котором мы и будем хранить данные. И перезапускаем студию чтобы активировалось меню masm в свойствах проекта. Туда мы впишем макросы, по каторым будем определять архитектуру, я например использую студийные _M_IA86 и _M_AMD64.

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

Пример укладки данных в .asm файле ищите ниже.

Ограничения

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

Поэтому следующие вещи являются запрещёнными:

  1. Вызовы внешних функций (WinAPI, Runtime Library и т.п.)
    Очевидно такие вызовы пораждают выходы за пределы шеллкода (см. выше)

Пример:

void test()
{
	...
}

int main()
{
	...
	//на внешний memcpy вызов будет установлен релок
	memcpy(buf, buf2, 100);
	//а это уже внутренняя функция, на неё релок наложен не будет 
	test();
	...
}

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

  1. Обращение к глобальным переменным
    Для архитектуры х86, обращение к глобальным переменным происходит через прямые адреса.

Пример:

int global_var;
int main()
{
	int local_var;
	...
	//доступ к глобальной переменной произойдёт по адресу
	global_var = 1; 
	//доступ к локальной по смещение в стеке
	local_var = 2;
	...
}
  1. Обращение к строковым константам
    Как известно строка в С это указатель на массив, а там где указатель, там и пряомой адрес

Пример:

int main() 
{
	char *str = "Hello world";//так нельзя
	...
}
  1. Конструкции автозаполнения
    Подменяются на memset, а внешний вызов как вы уже догадались это пряомой адрес

Пример:

int main()
{
	int array[100] = {0};//подстановка memset
	...
}

Для инициализации таких массивов используйте самописанный аналог memset

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

Пример:

void MyMemset(void *src, int value, int size)
{
	...//реализация memset
}

#pragma optimize("", off)
void MyMemset2(void *src, int value, int size)
{
	...//реализация memset
}
#pragma optimize("", on)

int main()
{
	int array[100];
	//будет заменено компилятором на memset из рантайма
	MyMemset(array, 0, 100 * sizeof(int)); 
	//не будет заменено на memset, так как оптимизация для MyMemset2 отключена
	MyMemset2(array, 0, 100 * sizeof(int)); 
	...
}

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

Точка входа

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

Однако оптимизатор может заинлайнить нашу точку входа. Поэтому явно отключаем inline-подстановку:

__declspec(noinline) void entry();

Захват библиотеки Kernel32.dll

Для захвата библиотеки Kernel32.dll используется всё та же техника TEB\PEB. Поскольку мы пишем шеллкод под разные архитектуры, то воспользуемся следующим кодом, расчитанным IA86 и AMD64 :

// Get module base address
void *get_module_base_addr(wchar_t *mod_name)
{
	PPEB peb = (PPEB)GET_PEB;
	PPEB_LDR_DATA ldr = peb->Ldr;
	PLDR_DATA_TABLE_ENTRY ldr_entry = (PLDR_DATA_TABLE_ENTRY)((uintptr_t)ldr->InMemoryOrderModuleList.Blink - (sizeof(uintptr_t) * 2));
	PLDR_DATA_TABLE_ENTRY ldr_first;

	ldr_first = ldr_entry;
	do {
		if (ldr_entry->DllBase && str_cmpw(ldr_entry->BaseDllName.Buffer, mod_name)) {
			return (HMODULE)ldr_entry->DllBase;
		}
		ldr_entry = (PLDR_DATA_TABLE_ENTRY)((uintptr_t)ldr_entry->InMemoryOrderLinks.Blink - (sizeof(uintptr_t) * 2));
	} while (ldr_first != ldr_entry);

	return NULL;
}

// Get export procedure address
void *get_proc_addr(void *mod_addr, char *proc_name)
{
	PIMAGE_DOS_HEADER pdos;
	PIMAGE_FILE_HEADER pimg;
	PIMAGE_OPT_HEADER popt;
	PIMAGE_EXPORT_DIRECTORY pexp;
	PDWORD pnames, pfuncs;
	PWORD pords;
	uintptr_t i;
	char *proc;

	//getting export directory
	pdos = (PIMAGE_DOS_HEADER)mod_addr;
	if (pdos->e_magic != IMAGE_DOS_SIGNATURE) {
		return NULL;
	}

	popt = (PIMAGE_OPTIONAL_HEADER)((uintptr_t)mod_addr + pdos->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER));
	pexp = (PIMAGE_EXPORT_DIRECTORY)(popt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (uintptr_t)mod_addr);
	if (!pexp) {//export not found
		return NULL;
	}

	//searching function name
	pnames = (PDWORD)(pexp->AddressOfNames + (uintptr_t)mod_addr);
	pords = (PWORD)(pexp->AddressOfNameOrdinals + (uintptr_t)mod_addr);
	pfuncs = (PDWORD)(pexp->AddressOfFunctions + (uintptr_t)mod_addr);

	for (i = 0; i < pexp->NumberOfNames; i++) {
		proc = (char *)(pnames[i] + (uintptr_t)mod_addr);
		if (str_cmp(proc, proc_name)) {
			break;
		}
	}
	if (i == pexp->NumberOfNames) {//not found
		return NULL;
	}

	return (void *)(pfuncs[pords[i]] + (uintptr_t)mod_addr);
}


// String compare (non case-sensitive)
uintptr_t str_cmp(char *str1, char *str2)
{
	int i = 0;
	char char1, char2;
	for (i = 0; ; i++) {
		char1 = str1[i];
		if (char1 >= 'A' && char1 <= 'Z') {
			char1 += 32;
		}

		char2 = str2[i];
		if (char2 >= 'A' && char2 <= 'Z') {
			char2 += 32;
		}

		if (char1 != char2) {
			break;
		}
		if (!char1) {
			return 1;
		}
	}
	return 0;
}

// Unicode string compare (non case-sensitive)
uintptr_t str_cmpw(wchar_t *str1, wchar_t *str2)
{
	int i = 0;
	wchar_t char1, char2;
	for (i = 0; ; i++) {
		char1 = str1[i];
		if (char1 >= 'A' && char1 <= 'Z') {
			char1 += 32;
		}

		char2 = str2[i];
		if (char2 >= 'A' && char2 <= 'Z') {
			char2 += 32;
		}

		if (char1 != char2) {
			break;
		}
		if (!char1) {
			return 1;
		}
	}
	return 0;
}

Извлечение и отладка

Я много сказал об извлечении, но так и не привёл пример такого исходного кода. Код для запуска, отладки и извлечения шеллкода мы поместим в файл с точкой входа в програму(напр. main.c), так как он не помечен директивами code_seg, то при компановке обьектного файла код и данные будут помещены в стандартные секции .text, .data, .rdata.

uintptr_t unpack_shellcode(char *exe_path, char *save_path)
{
	HANDLE hfile = INVALID_HANDLE_VALUE, hmap = NULL;
	uintptr_t size, offset = 0, i;
	PIMAGE_DOS_HEADER pdos;
	PIMAGE_FILE_HEADER pimg;
	PIMAGE_OPTIONAL_HEADER32 popt32;
	PIMAGE_OPTIONAL_HEADER64 popt64;
	PIMAGE_SECTION_HEADER psects, psect = NULL;
	char sect_name[IMAGE_SIZEOF_SHORT_NAME + 1] = {0};
	char *pshell_buf;
	DWORD written;
	LPVOID pview = NULL;

	__try {
		hfile = CreateFileA(exe_path, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_ARCHIVE, NULL);
		if (hfile == INVALID_HANDLE_VALUE) {
			printf("Error, can't open file!\n");
			return 0;
		}

		hmap = CreateFileMapping(hfile, NULL, PAGE_READONLY, 0, 0, NULL);
		if (!hmap) {
			printf("Error, can't mapped file!\n");
			return 0;
		}
		CloseHandle(hfile);
		hfile = INVALID_HANDLE_VALUE;

		pview = MapViewOfFile(hmap, FILE_MAP_READ, 0, 0, 0);
		if (!pview) {
			printf("Error, can't mapped file!\n");
			return 0;
		}

		//parse DOS and PE header
		pdos = (PIMAGE_DOS_HEADER)pview;
		if (pdos->e_magic != IMAGE_DOS_SIGNATURE) {
			printf("Error, incorrect DOS header!\n");
			return 0;
		}
		offset += pdos->e_lfanew;

		if (*(DWORD *)((uintptr_t)pdos + offset) != (DWORD)IMAGE_NT_SIGNATURE) {
			printf("Error, incorrect PE header!\n");
			return 0;
		}
		offset += 4;

		pimg = (PIMAGE_FILE_HEADER)((uintptr_t)pdos + offset);
		offset += sizeof(IMAGE_FILE_HEADER);

		if (pimg->Machine != IMAGE_FILE_MACHINE_I386 && pimg->Machine != IMAGE_FILE_MACHINE_AMD64) {
			printf("Error, incorrect architecture!\n");
			return 0;
		}

		if (pimg->Machine == IMAGE_FILE_MACHINE_I386) {
			popt32 = (PIMAGE_OPTIONAL_HEADER32)((uintptr_t)pdos + offset);
			offset += sizeof(IMAGE_OPTIONAL_HEADER32);
		} else {
			popt64 = (PIMAGE_OPTIONAL_HEADER64)((uintptr_t)pdos + offset);
			offset += sizeof(IMAGE_OPTIONAL_HEADER64);
		}

		psects = (PIMAGE_SECTION_HEADER)((uintptr_t)pdos + offset);

		//search shell section
		for (i = 0; i < pimg->NumberOfSections; i++) {
			memcpy(sect_name, psects[i].Name, IMAGE_SIZEOF_SHORT_NAME);
			if (!strcmp(sect_name, SHELLCODE_SECTION)) {
				psect = &psects[i];
				break;
			}
		}
		if (!psect) {
			printf("Error, shellcode section not found!\n");
			return 0;
		}

		//shink shellcode size
		size = 0;
		pshell_buf = (char *)((uintptr_t)pdos + psect->PointerToRawData);
		for (i = psect->SizeOfRawData - 1; i >= 0; i--) {
			if (pshell_buf[i] != 0 && pshell_buf[i] != 0xCC) {
				size = i + 1;
				break;
			}
		}

		//save shellcode
		hfile = CreateFileA(save_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, NULL, NULL);
		if (hfile == INVALID_HANDLE_VALUE) {
			printf("Error, can't open output file!\n");
			return 0;
		}

		if (!WriteFile(hfile, pshell_buf, size, &written, NULL)) {
			printf("Error, can't write to output file!\n");
			return 0;
		}

	} __finally {
		if (hfile != INVALID_HANDLE_VALUE) {
			CloseHandle(hfile);
		}
		if (hmap) {
			CloseHandle(hmap);
		}
		if (pview) {
			UnmapViewOfFile(pview);
		}
	}
	
	return 1;
}

Однако есть один минус в самораспаковке, это кратность выравнивания файловых данных, равная 512 байтам, что создаёт шеллкоду нулевой хвост. Это можно обойти если добавить линкеру параметр /filealign:1, однако полученный на выходе экзешник нельзя будет запустить. Я пошел другим путём и отрезал излишки путём обрезания всех нулевых байт до первого не нулевого.

Shellcode Hello world

Готовый проект доступен по ссылке

shellcode.h

#pragma once

#include <Windows.h>
#include <intrin.h>


#define SHELLCODE_FILE "shellcode.bin"
#define SHELLCODE_SECTION "shell"


#if defined(_M_IA86)
#define GET_PEB __readfsdword(0x30)
#define PIMAGE_OPT_HEADER PIMAGE_OPTIONAL_HEADER32
#elif defined(_M_AMD64)
#define GET_PEB __readgsqword(0x60)
#define PIMAGE_OPT_HEADER PIMAGE_OPTIONAL_HEADER64
#else
#error Architechure not supported!
#endif

typedef struct _UNICODE_STRING {
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING;

typedef struct _PEB_LDR_DATA {
	BYTE Reserved1[8];
	PVOID Reserved2[3];
	LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _PEB {
	BYTE Reserved1[2];
	BYTE BeingDebugged;
	BYTE Reserved2[1];
	PVOID Reserved3[2];
	PPEB_LDR_DATA Ldr;
} PEB, *PPEB;

typedef struct _LDR_DATA_TABLE_ENTRY {
	LIST_ENTRY InLoadOrderLinks;
	LIST_ENTRY InMemoryOrderLinks;
	LIST_ENTRY InInitializationOrderLinks;
	PVOID DllBase;
	PVOID EntryPoint;
	ULONG SizeOfImage;
	UNICODE_STRING FullDllName;
	UNICODE_STRING BaseDllName;
	ULONG Flags;
	USHORT LoadCount;
	USHORT TlsIndex;
	union {
		LIST_ENTRY HashLinks;
		struct {
			PVOID SectionPointer;
			ULONG CheckSum;
		};
	};
	ULONG   TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;


#pragma pack(push, 1)
typedef struct _Shell_Static_Data {
	char phrase_ldrloaddll[16];
	char phrase_msgbox[16];
	char phrase_hello[16];
	char phrase_hello_title[16];
	wchar_t phrase_user32[16];
	wchar_t phrase_ntdll[16];
} Shell_Static_Data, *PShell_Static_Data;
#pragma pack(pop)

extern PShell_Static_Data __stdcall get_data_struct_ptr();

__declspec(noinline) void entry();

void *get_module_base_addr(wchar_t *mod_name);
void *get_proc_addr(void *mod_addr, char *proc_name);

typedef NTSTATUS (NTAPI *LdrLoadDllProc)(PWCHAR PathToFile, ULONG Flags, UNICODE_STRING *ModuleFileName, void **ModuleHandle);
typedef int (WINAPI *MessageBoxProc)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

shellcode.c

#include "shellcode.h"
#include <stdio.h>

#pragma code_seg("shell")

uintptr_t str_cmpw(wchar_t *str1, wchar_t *str2);
uintptr_t str_cmp(char *str1, char *str2);
uintptr_t str_len(char *str);
uintptr_t str_lenw(wchar_t *str);

// Shellcode entry point
__declspec(noinline) void entry()
{
	PShell_Static_Data shelldata = (PShell_Static_Data)get_data_struct_ptr();
	void *ntdll = get_module_base_addr(shelldata->phrase_ntdll);
	void *kernel32;
	LdrLoadDllProc LdrLoadDll = (LdrLoadDllProc)get_proc_addr(ntdll, shelldata->phrase_ldrloaddll);
	MessageBoxProc MsgBox;
	UNICODE_STRING uni;

	uni.Buffer = shelldata->phrase_user32;
	uni.Length = str_lenw(uni.Buffer) * 2;
	uni.MaximumLength = uni.Length + 2;
	if (LdrLoadDll(NULL, 0, &uni, &kernel32)) {
		return;
	}

	MsgBox = (MessageBoxProc)get_proc_addr(kernel32, shelldata->phrase_msgbox);

	MsgBox(NULL, shelldata->phrase_hello, shelldata->phrase_hello_title, MB_OK);
}

// Get module base address
void *get_module_base_addr(wchar_t *mod_name)
{
	PPEB peb = (PPEB)GET_PEB;
	PPEB_LDR_DATA ldr = peb->Ldr;
	PLDR_DATA_TABLE_ENTRY ldr_entry = (PLDR_DATA_TABLE_ENTRY)((uintptr_t)ldr->InMemoryOrderModuleList.Blink - (sizeof(uintptr_t) * 2));
	PLDR_DATA_TABLE_ENTRY ldr_first;

	ldr_first = ldr_entry;
	do {
		if (ldr_entry->DllBase && str_cmpw(ldr_entry->BaseDllName.Buffer, mod_name)) {
			return (HMODULE)ldr_entry->DllBase;
		}
		ldr_entry = (PLDR_DATA_TABLE_ENTRY)((uintptr_t)ldr_entry->InMemoryOrderLinks.Blink - (sizeof(uintptr_t) * 2));
	} while (ldr_first != ldr_entry);

	return NULL;
}

// Get export procedure address
void *get_proc_addr(void *mod_addr, char *proc_name)
{
	PIMAGE_DOS_HEADER pdos;
	PIMAGE_FILE_HEADER pimg;
	PIMAGE_OPT_HEADER popt;
	PIMAGE_EXPORT_DIRECTORY pexp;
	PDWORD pnames, pfuncs;
	PWORD pords;
	uintptr_t i;
	char *proc;

	//getting export directory
	pdos = (PIMAGE_DOS_HEADER)mod_addr;
	if (pdos->e_magic != IMAGE_DOS_SIGNATURE) {
		return NULL;
	}

	popt = (PIMAGE_OPTIONAL_HEADER)((uintptr_t)mod_addr + pdos->e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER));
	pexp = (PIMAGE_EXPORT_DIRECTORY)(popt->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress + (uintptr_t)mod_addr);
	if (!pexp) {//export not found
		return NULL;
	}

	//searching function name
	pnames = (PDWORD)(pexp->AddressOfNames + (uintptr_t)mod_addr);
	pords = (PWORD)(pexp->AddressOfNameOrdinals + (uintptr_t)mod_addr);
	pfuncs = (PDWORD)(pexp->AddressOfFunctions + (uintptr_t)mod_addr);

	for (i = 0; i < pexp->NumberOfNames; i++) {
		proc = (char *)(pnames[i] + (uintptr_t)mod_addr);
		if (str_cmp(proc, proc_name)) {
			break;
		}
	}
	if (i == pexp->NumberOfNames) {//not found
		return NULL;
	}

	return (void *)(pfuncs[pords[i]] + (uintptr_t)mod_addr);
}


// String compare (non case-sensitive)
uintptr_t str_cmp(char *str1, char *str2)
{
	int i = 0;
	char char1, char2;
	for (i = 0; ; i++) {
		char1 = str1[i];
		if (char1 >= 'A' && char1 <= 'Z') {
			char1 += 32;
		}

		char2 = str2[i];
		if (char2 >= 'A' && char2 <= 'Z') {
			char2 += 32;
		}

		if (char1 != char2) {
			break;
		}
		if (!char1) {
			return 1;
		}
	}
	return 0;
}

// Unicode string compare (non case-sensitive)
uintptr_t str_cmpw(wchar_t *str1, wchar_t *str2)
{
	int i = 0;
	wchar_t char1, char2;
	for (i = 0; ; i++) {
		char1 = str1[i];
		if (char1 >= 'A' && char1 <= 'Z') {
			char1 += 32;
		}

		char2 = str2[i];
		if (char2 >= 'A' && char2 <= 'Z') {
			char2 += 32;
		}

		if (char1 != char2) {
			break;
		}
		if (!char1) {
			return 1;
		}
	}
	return 0;
}

// String length
uintptr_t str_len(char *str)
{
	uintptr_t i = 0;
	for (i = 0; ; i++) {
		if (!str[i]) {
			return i;
		}
	}
	return 0;
}

// Unicode string length
uintptr_t str_lenw(wchar_t *str)
{
	uintptr_t i = 0;
	for (i = 0; ; i++) {
		if (!str[i]) {
			return i;
		}
	}
	return 0;
}

shellcode_native.asm

;IA86 and AMD64
IFDEF _M_IA86
.386
.model flat, stdcall
ENDIF

;set code section .shell
.CODE shell

;data struct
Shell_Static_Data STRUCT 
	phrase_ldrloaddll db 16 dup(0)
	phrase_msgbox db 16 dup(0)
	phrase_hello db 16 dup(0)
	phrase_hello_title db 16 dup(0)
	phrase_user32 dw 16 dup(0)
	phrase_ntdll dw 16 dup(0)
Shell_Static_Data ENDS

shelldata Shell_Static_Data <"LdrLoadDll", "MessageBoxA", "Hello hacker", "Shellcode", \
	 {'u', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l'}, {'N', 't', 'd', 'l','l', '.', 'd', 'l', 'l'}>


;getting ptr to shelldata struct

IFDEF _M_IA86

get_data_struct_ptr PROC
;delta
	call get_delta
get_delta:
	pop eax
;calc var
	sub eax, 5
	sub eax, sizeof shelldata
	ret
get_data_struct_ptr ENDP

ELSEIFDEF _M_AMD64

get_data_struct_ptr PROC
;delta
	call get_delta
get_delta:
	pop rax
;calc var
	sub rax, 5
	sub rax, sizeof shelldata
	ret
get_data_struct_ptr ENDP

ENDIF

END

main.c

#include <stdio.h>
#include <Windows.h>

#include "shellcode.h"


uintptr_t unpack_shellcode(char *exe_path, char *save_path)
{
	HANDLE hfile = INVALID_HANDLE_VALUE, hmap = NULL;
	uintptr_t size, offset = 0, i;
	PIMAGE_DOS_HEADER pdos;
	PIMAGE_FILE_HEADER pimg;
	PIMAGE_OPTIONAL_HEADER32 popt32;
	PIMAGE_OPTIONAL_HEADER64 popt64;
	PIMAGE_SECTION_HEADER psects, psect = NULL;
	char sect_name[IMAGE_SIZEOF_SHORT_NAME + 1] = {0};
	char *pshell_buf;
	DWORD written;
	LPVOID pview = NULL;

	__try {
		hfile = CreateFileA(exe_path, GENERIC_READ, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_ARCHIVE, NULL);
		if (hfile == INVALID_HANDLE_VALUE) {
			printf("Error, can't open file!\n");
			return 0;
		}

		hmap = CreateFileMapping(hfile, NULL, PAGE_READONLY, 0, 0, NULL);
		if (!hmap) {
			printf("Error, can't mapped file!\n");
			return 0;
		}
		CloseHandle(hfile);
		hfile = INVALID_HANDLE_VALUE;

		pview = MapViewOfFile(hmap, FILE_MAP_READ, 0, 0, 0);
		if (!pview) {
			printf("Error, can't mapped file!\n");
			return 0;
		}

		//parse DOS and PE header
		pdos = (PIMAGE_DOS_HEADER)pview;
		if (pdos->e_magic != IMAGE_DOS_SIGNATURE) {
			printf("Error, incorrect DOS header!\n");
			return 0;
		}
		offset += pdos->e_lfanew;

		if (*(DWORD *)((uintptr_t)pdos + offset) != (DWORD)IMAGE_NT_SIGNATURE) {
			printf("Error, incorrect PE header!\n");
			return 0;
		}
		offset += 4;

		pimg = (PIMAGE_FILE_HEADER)((uintptr_t)pdos + offset);
		offset += sizeof(IMAGE_FILE_HEADER);

		if (pimg->Machine != IMAGE_FILE_MACHINE_I386 && pimg->Machine != IMAGE_FILE_MACHINE_AMD64) {
			printf("Error, incorrect architecture!\n");
			return 0;
		}

		if (pimg->Machine == IMAGE_FILE_MACHINE_I386) {
			popt32 = (PIMAGE_OPTIONAL_HEADER32)((uintptr_t)pdos + offset);
			offset += sizeof(IMAGE_OPTIONAL_HEADER32);
		} else {
			popt64 = (PIMAGE_OPTIONAL_HEADER64)((uintptr_t)pdos + offset);
			offset += sizeof(IMAGE_OPTIONAL_HEADER64);
		}

		psects = (PIMAGE_SECTION_HEADER)((uintptr_t)pdos + offset);

		//search shell section
		for (i = 0; i < pimg->NumberOfSections; i++) {
			memcpy(sect_name, psects[i].Name, IMAGE_SIZEOF_SHORT_NAME);
			if (!strcmp(sect_name, SHELLCODE_SECTION)) {
				psect = &psects[i];
				break;
			}
		}
		if (!psect) {
			printf("Error, shellcode section not found!\n");
			return 0;
		}

		//shink shellcode size
		size = 0;
		pshell_buf = (char *)((uintptr_t)pdos + psect->PointerToRawData);
		for (i = psect->SizeOfRawData - 1; i >= 0; i--) {
			if (pshell_buf[i] != 0 && pshell_buf[i] != 0xCC) {
				size = i + 1;
				break;
			}
		}

		//save shellcode
		hfile = CreateFileA(save_path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, NULL, NULL);
		if (hfile == INVALID_HANDLE_VALUE) {
			printf("Error, can't open output file!\n");
			return 0;
		}

		if (!WriteFile(hfile, pshell_buf, size, &written, NULL)) {
			printf("Error, can't write to output file!\n");
			return 0;
		}

	} __finally {
		if (hfile != INVALID_HANDLE_VALUE) {
			CloseHandle(hfile);
		}
		if (hmap) {
			CloseHandle(hmap);
		}
		if (pview) {
			UnmapViewOfFile(pview);
		}
	}
	
	return 1;
}

uintptr_t load_shellcode(char *path)
{
	HANDLE hfile;
	unsigned int size, readed;
	char *pbuf;
	void *pspace;

	hfile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
	if (hfile == INVALID_HANDLE_VALUE) {
		printf("Error, can't open shellcode!\n");
		return 0;
	}

	size = GetFileSize(hfile, NULL);
	if (!size) {
		printf("Error, file is empty\n");
		return 0;
	}

	pbuf = (char *)malloc(size);

	if (!ReadFile(hfile, pbuf, size, (LPDWORD)&readed, NULL)) {
		printf("Error, can't read shellcode data\n");
		free(pbuf); return 0;
	}

	pspace = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (!pspace) {
		printf("Error, can't allocate virtual page\n");
		free(pbuf); return 0;
	}

	memcpy(pspace, pbuf, size);

	((void (*)())pspace)();//call shellcode

	VirtualFree(pspace, 0, MEM_RELEASE);
	free(pbuf);

	return 1;
}

int main(int argc, char *argv[])
{
	//unpack shellcode to file
	if (!unpack_shellcode(argv[0], SHELLCODE_FILE)) {
		return 1;
	}
	printf("Unpacking successful!\n");

	//for compile and debug
	printf("Calling shellcode from module!\n");
	entry();

	//for test
	printf("Calling shellcode from random base!\n");
	if (!load_shellcode(SHELLCODE_FILE)) {
		return 1;
	}

	return 0;
}

На выходе шеллкод имеет размер 531 байт для IA86 и 609 байт для AMD64.

Подведём итоги

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

Для наглядности приемуществи и недостатков я привёл сравнение с ассемблерным аналогом :

  1. Переносимость
    [INDENT]С shellcode - хорошая
    ASM shellcode - плохая[/INDENT]

  2. Размерность
    [INDENT]С shellcode - средняя (без оптимизации большая)
    ASM shellcode - маленькая (с оптимизацией сверхмаленькая)[/INDENT]

  3. Масштабируемость и читабельность кода
    [INDENT]С shellcode - хорошая
    ASM shellcode - плохая (особенно после оптимизаций)[/INDENT]

  4. Возможность отладки и тестирования
    [INDENT]С shellcode - хорошая (непосредственно в IDE)
    ASM shellcode - требуется тестовый\загрузочный код [/INDENT]

  5. Возможность манипуляции над бинарной структурой
    [INDENT]С shellcode - слабая, но достаточная
    ASM shellcode - хорошая[/INDENT]

Собственно вот и все, шеллкодьте на здаровье