R0 CREW

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

shellcode
coding
ru
#1

⁠Привет хакеру! Меня завут Ж0ра К0рнев, решил накидать статейку о технике разработки базонезависимого шеллкода и приземлился на вашем форуме. Тема была уложена в 2 статьи:

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

И так …

Поехали

Начнём пожалуй с определения, под шеллкодом будем понимать машинный код обладающий свойством базонезависимости и выполняющий какие-либо системные операции. Т.е. какой-то набор инструкций составляющих алгоритм, который может корректно выполнятся независимо от его текущего расположения в памяти процесса. Где пременим такой шеллкод? В основном в областях связанных с ИБ: для написания вирусов, эксплойтов, защит(упаковщики\потекторы\крипторы).

В контексте данной статьи мы рассмотрим классический низкоуровневый шеллкод написанный на ассемблере. Отличительной чертой такого шеллкода является его маленький размер, что в некоторых ситуациях играет ключевую роль, в частности при написании эксплойтов. Будем программировать в usermode под Windows(x86).

Подготовка к высадке

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

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

Запуск и передача информации

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

  • Через динамическую память
  • В теле шеллкода

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

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

Точкой входа будет считатся начало данных, а сами данные будут распологатся в хвосте, их мы поделим на внутренние и входные. Под внутренними будем понимать константные данные которые просто хранятся внутри шеллкода(строки и т.п.). Адрес входных данных можно легко получить расчётом:

input_struct* pinput = shellcode_addr + shellcode_size - sizeof(input_struct);//pseudocode

Теперь чуть изменим нашу песочницу, допишем запись в хвост шеллкода и реализуем Hellow World демонстрирующий сею передачу:

Hide

Loader.c

//Loader.c
#include <Windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
	HANDLE hfile;
	unsigned int size, readed;
	char *pbuf;
	void *pspace;

	if (argc < 2) {
		printf("Error, arguments missmatch\n");
		return 1;
	}

	hfile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hfile == INVALID_HANDLE_VALUE) {
		printf("Error, can't open shellcode '%s' (code %d)\n", argv[1], GetLastError());
		return 2;
	}

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

	pbuf = (char *)malloc(size);

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

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

	memcpy(pspace, pbuf, size);

	//set input parametr
	*(DWORD *)((DWORD)pspace + size - sizeof(DWORD)) = (DWORD)MessageBoxA;

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

	VirtualFree(pspace, size, MEM_DECOMMIT);
	free(pbuf);
	return 0;
}

shellcode.asm

BITS 32 ;using x86 architecture

;---------------------------
;code block
entry_point:
pushad

call get_eip			
get_eip:
pop ebx 						;pop up virtual address to eax
sub ebx, get_eip - entry_point	;offset normalize

;arg4
push 0
;arg3
mov eax, msg_title
add eax, ebx
push eax
;arg2
mov eax, msg_string
add eax, ebx
push eax
;arg1
push 0
;call messagebox
mov eax, msgbox_addr
add eax, ebx
call [eax]

popad
retn

;---------------------------
;internal data block
msg_title: 		db 'Message', 0
msg_string:		db 'Hello Peoplz', 0

;---------------------------
;input data block
msgbox_addr:	resd 1

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

Исполнение и захват системных данных

Очевидно что в шеллкоде нельзя использовать инструкции с прямыми адресами(напр. JMP 0x004021341), поэтому используем только относительные смещения (JMP +10bytes). Для функционирования алгоритма этого вполне достаточно, но что если мы хотим передать к примеру строку из шеллкода в функцию MessageBoxA? Возникает необходимость преобразовать относительное смещение к некоторым ресурсам шеллкода непосредственно в виртуальный адрес.

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

entry_point:
...
call get_eip			
get_eip:
pop ebp
sub ebp, get_eip - entry_point
...

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

Захват системной библиотеки на примере Kernel32.dll
Теперь владея дельта смещением мы переходим ко второму этапу, захвату адреса системных библиотек, а следовательно и их функций. Для начала вполне достаточно заполучить LoadLibrary которые находятся в библиотеке Kernel32.dll. Узнав адрес расположения библиотеки мы бы смогли получить из PE заголовка адреса любых интересующих нас функций. Мне известны 2 способа заполучения адреса Kernel32.dll:

  • по статическому смещению (запоминание адреcа Kernel32)
  • через Process Environment Block(PEB)

Метод использования статического смещения настолько плох, непереносим и ненадёжен что мы его рассматривать вообще не будем. А вот способ с TEB\PEB вполне боевой. Очень хорошо описали эту технику в этой статье, перенесём сею реализацию на ассемблер и получим следующий шеллкод, который уже независим от входных параметров:

Hide

Loader.c

//Loader.c
#include <Windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
	HANDLE hfile;
	unsigned int size, readed;
	char *pbuf;
	void *pspace;

	if (argc < 2) {
		printf("Error, arguments missmatch\n");
		return 1;
	}

	hfile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hfile == INVALID_HANDLE_VALUE) {
		printf("Error, can't open shellcode '%s' (code %d)\n", argv[1], GetLastError());
		return 2;
	}

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

	pbuf = (char *)malloc(size);

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

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

	memcpy(pspace, pbuf, size);

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

	VirtualFree(pspace, size, MEM_DECOMMIT);
	free(pbuf);
	return 0;
}

Shellcode.asm

BITS 32 ;using x86 architecture

;---------------------------
;definition
%define u16(x) __utf16__(x)
%define IMG_DOS_OFFSET 0x3C			;offset to pe offset
%define IMG_PE_EXP_OFFSET 0x78		;offset to export directory address

;--------------------------- proc 1
;code block
entry_point:
pushad
sub esp, 4							;define local var
	;calc delta
	call get_eip			
	get_eip:
	pop ebx 						;pop up virtual address to eax
	sub ebx, get_eip - entry_point	;offset normalize

	;search kernel32 address
	mov eax, wphr_kernel32
	add eax, ebx
	push eax
	call find_module_address
	test eax, eax
	je end_shell
	mov [esp], eax
	
	;search loadlibrary
	mov eax, phr_loadlibrary
	add eax, ebx
	push eax
	mov eax, [esp + 4]
	push eax
	call search_export_proc
	
	;load user32.dll
	mov edx, phr_user32
	add edx, ebx
	push edx
	call eax
	
	;search messagebox
	mov edx, phr_msgbox
	add edx, ebx
	push edx
	push eax
	call search_export_proc
	
	;call messagebox
	push 0
	mov edx, msg_title
	add edx, ebx
	push edx
	mov edx, msg_string
	add edx, ebx
	push edx
	push 0
	call eax

end_shell:
add esp, 4
popad
retn

;------------------ proc 2
find_module_address:				;find_module_address(stack wlib_name)
push ebx							;save context value
	;getting PEB
	mov ebx, dword [fs:0x30]
	test ebx, ebx
	js retn_label_2

	;getting first LDR_MODULE
	mov ebx, dword [ebx + 0x0C]
	mov ebx, dword [ebx + 0x1C]
	mov ebx, dword [ebx]
	push ebx						;save in stack

	whiling:
		mov eax, dword [ebx + 0x20]	;getting module unicode name
		test eax, eax				;if name == NULL continue
		je calc_next
		
		;compare stings
		push eax
		mov eax, dword [esp + 16]
		push eax
		call wstrcmp
		test eax, eax
		jne retn_ok_2
		
		;set next module
		calc_next:
		mov eax, dword [ebx]
		mov ecx, dword [esp]
		cmp eax, ecx 				;if end of list => break
		je retn_error_2
		mov ebx, eax
	jmp whiling
	
retn_error_2:
xor eax, eax
jmp retn_label_2
retn_ok_2:
mov eax, dword[ebx + 8]				;put return value
retn_label_2:
add esp, 4							;clear args
pop ebx								;restore context value
retn 4

;------------------ proc 3
wstrcmp:							;wstrcmp(stack wstr1, stack wstr2)
	whiling_3:
		;put wchar to dx
		mov esi, dword [esp + 4]
		lodsw
		mov dword [esp + 4], esi
		mov dx, ax
		
		;put wchar to ax
		mov esi, dword [esp + 8]	
		lodsw
		mov dword [esp + 8], esi
		
		;compare
		cmp ax, dx
		jne retn_error
		
		;break if EOS
		cmp ax, 0
		je retn_ok
		
	jmp whiling_3
retn_ok:
mov eax, 1
retn 8
retn_error:
xor eax, eax
retn 8

;------------------ proc 4
strcmp:							;strcmp(stack wstr1, stack wstr2)
xor eax, eax
xor edx, edx
	strcmp_whiling:
		;put wchar to dx
		mov esi, dword [esp + 4]
		lodsb
		mov dword [esp + 4], esi
		mov dx, ax
		
		;put wchar to ax
		mov esi, dword [esp + 8]	
		lodsb
		mov dword [esp + 8], esi
		
		;compare
		cmp ax, dx
		jne strcmp_retn_error
		
		;break if EOS
		cmp ax, 0
		je strcmp_retn_ok
		
	jmp strcmp_whiling
strcmp_retn_ok:
mov eax, 1
retn 8
strcmp_retn_error:
xor eax, eax
retn 8

;------------------ proc 5
search_export_proc:					;search_import_proc(stack lib_addr, stack lib_name)
push ebx							;save context value
sub esp, 12							;0 - AddressOfNames, 4 - AddressOfNameOrdinals, 8 - AddressOfFunctions, 12 - ebx, 16 - retn addr, 20 - lib_addr, 24 - lib_name

	;put to eax PE header VA
	mov eax, [esp + 20]
	add eax, IMG_DOS_OFFSET
	mov eax, [eax]
	add eax, [esp + 20]				;RVA -> VA
		
	;put to eax PE export descriptor VA
	add eax, IMG_PE_EXP_OFFSET
	test eax, eax
	je retn_label_5
	mov eax, [eax]
	add	eax, [esp + 20]				;RVA -> VA
	
	;put to ecx NumberOfNames
	add eax, 24
	mov ecx, [eax]
	add eax, 4
	;put to stack VA AddressOfFunctions
	mov edx, [eax]
	add	edx, [esp + 20]				;RVA -> VA
	mov [esp + 8], edx				;save RVA AddressOfFunctions
	add eax, 4
	;put to stack VA AddressOfNames
	mov edx, [eax]					;put to eax RVA AddressOfNames
	add	edx, [esp + 20]				;RVA -> VA
	mov [esp], edx					;save RVA AddressOfNames
	add eax, 4
	;put to stack VA AddressOfNameOrdinals
	mov eax, [eax]
	add	eax, [esp + 20]				;RVA -> VA
	mov [esp + 4], eax				;save RVA AddressOfNameOrdinals
	add eax, 4
	;searching procedure name
	xor eax, eax
	xor edi, edi
	whiling_5:
		;if count == 0 break
		test ecx, ecx
		je retn_label_5
		dec ecx
		
		;compare
		mov eax, [esp]
		mov edx, eax				;ptr ++
		add edx, 4
		mov [esp], edx
		mov eax, [eax]
		add eax, [esp + 20]
		push eax
		mov eax, [esp + 28]
		push eax
		call strcmp
		test eax, eax
		jne break_5
		
		inc edi						;index ++
	jmp whiling_5
	break_5:
	
	;searching procedure address
	mov eax, 2
	mul edi
	add eax, [esp + 4]
	xor edx, edx
	mov dx, [eax]
	;add eax, [esp + 20]
	mov eax, 4
	mul edx
	add eax, [esp + 8]
	mov eax, [eax]
	add eax, [esp + 20]
	
	
retn_label_5:
add esp, 12
pop ebx								;restore context value
retn 8

;---------------------------
;internal data block
msg_title: 		db 'Message', 0
msg_string:		db 'Hello Peoplz', 0
wphr_kernel32	dw u16('kernel32.dll'), 0
phr_user32		db 'user32.dll', 0
phr_loadlibrary	db 'LoadLibraryA', 0
phr_msgbox		db 'MessageBoxA', 0

;---------------------------
;input data block
; not used

Вкратце алгоритм следующий:

  1. Сперва мы расчитываем делта смещение.
  2. Пользуясь тем, что сегментный регистр FS всегда указывает на TEB, мы оттуда узнаём адрес PEB(offset:0x30), затем обращаемся к структуре PEB_LDR_DATA, содержащей указатель на колцевй список структур LDR_MODULE. Этот список описывает модули загруженные в память и содержит их виртуальный адреса, имена и т.п. информацию. Перебирая элементы этого списка мы ищем kernel32.dll.
  3. Получив адрес библиотеки парсим PE заголовок и ищем адреса экспортируемых функций LoadLibraryA, GetProcAddress
  4. Выполняем нужные нам действия
  5. Профит

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

Захват системы средствами Ntdll.dll
Данная техника чуть сложнее и менее надёжна за счёт применения еще большего числа недокументированных фитч, однако она имеет полное право на жизнь. Более полное описание техники(исходник на С) вы можете найти тут.

Для поиска адреса ntdll.dll будем использовать всё ту же технику с TEB/PEB. В качестве функции загрузки библиотеки будем использовать примитив LdrLoadDll который имеет следующую сигнатуру:

NTSYSAPI NTSTATUS NTAPI LdrLoadDll(
	IN PWCHAR PathToFile OPTIONAL,
	IN ULONG Flags OPTIONAL,
	IN PUNICODE_STRING ModuleFileName,
	OUT PHANDLE ModuleHandle );

Поскольку функцию аналог GetProcAddress мы уже разработали в примере выше, то теперь нам абсолютно не нужна библиотека Kernel32.dll.

Hide

Loader.c

//Loader.c
#include <Windows.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
	HANDLE hfile;
	unsigned int size, readed;
	char *pbuf;
	void *pspace;

	if (argc < 2) {
		printf("Error, arguments missmatch\n");
		return 1;
	}

	hfile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hfile == INVALID_HANDLE_VALUE) {
		printf("Error, can't open shellcode '%s' (code %d)\n", argv[1], GetLastError());
		return 2;
	}

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

	pbuf = (char *)malloc(size);

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

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

	memcpy(pspace, pbuf, size);

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

	VirtualFree(pspace, size, MEM_DECOMMIT);
	free(pbuf);
	return 0;
}

Shellcode.asm

BITS 32 ;using x86 architecture

;---------------------------
;definition
%define u16(x) __utf16__(x)
%define IMG_DOS_OFFSET 0x3C			;offset to pe offset
%define IMG_PE_EXP_OFFSET 0x78		;offset to export directory address

;--------------------------- proc 1
;code block
entry_point:
pushad
sub esp, 4							;define local var
	;calc delta
	call get_eip			
	get_eip:
	pop ebx 						;pop up virtual address to eax
	sub ebx, get_eip - entry_point	;offset normalize

	;search ntdll address
	mov eax, wphr_ntdll
	add eax, ebx
	push eax
	call find_module_address
	test eax, eax
	je end_shell
	mov [esp], eax
	
	;search ldrloaddll
	mov eax, phr_ldrloaddll
	add eax, ebx
	push eax
	mov eax, [esp + 4]
	push eax
	call search_export_proc
	
	;load user32.dll
	sub esp, 12						;alloc sizeof(UNICODE_STRING) + sizeof(PHANDLE)
	
	xor edx, edx
	mov [esp + 8], edx				;set handle = NULL
	mov edx, wphr_user32			;put UNICODE_STRING.Buffer
	add edx, ebx
	mov [esp + 4], edx
	mov dx, 22						;wcslen(phr_user32) * 2 + 2, UNICODE_STRING.MaximumLength
	shl edx, 16
	mov dx, 20						;wcslen(phr_user32) * 2, UNICODE_STRING.Length
	mov [esp], edx
	
	mov edx, esp
	add edx, 8
	push edx
	mov edx, esp
	add edx, 4
	push edx
	push 0
	push 0
	call eax						;call LdrLoadDll
	mov eax, [esp + 8]
	add esp, 12
	test eax, eax
	je end_shell
	
	;search messagebox
	mov edx, phr_msgbox
	add edx, ebx
	push edx
	push eax
	call search_export_proc
	test eax, eax
	je end_shell
	
	;call messagebox
	push 0
	mov edx, msg_title
	add edx, ebx
	push edx
	mov edx, msg_string
	add edx, ebx
	push edx
	push 0
	call eax

end_shell:
add esp, 4
popad
retn

;------------------ proc 2
find_module_address:				;find_module_address(stack wlib_name)
push ebx							;save context value
	;getting PEB
	mov ebx, dword [fs:0x30]
	test ebx, ebx
	js retn_label_2

	;getting first LDR_MODULE
	mov ebx, dword [ebx + 0x0C]
	mov ebx, dword [ebx + 0x1C]
	mov ebx, dword [ebx]
	push ebx						;save in stack

	whiling:
		mov eax, dword [ebx + 0x20]	;getting module unicode name
		test eax, eax				;if name == NULL continue
		je calc_next
		
		;compare stings
		push eax
		mov eax, dword [esp + 16]
		push eax
		call wstrcmp
		test eax, eax
		jne retn_ok_2
		
		;set next module
		calc_next:
		mov eax, dword [ebx]
		mov ecx, dword [esp]
		cmp eax, ecx 				;if end of list => break
		je retn_error_2
		mov ebx, eax
	jmp whiling
	
retn_error_2:
xor eax, eax
jmp retn_label_2
retn_ok_2:
mov eax, dword[ebx + 8]				;put return value
retn_label_2:
add esp, 4							;clear args
pop ebx								;restore context value
retn 4

;------------------ proc 3
wstrcmp:							;wstrcmp(stack wstr1, stack wstr2)
	whiling_3:
		;put wchar to dx
		mov esi, dword [esp + 4]
		lodsw
		mov dword [esp + 4], esi
		mov dx, ax
		
		;put wchar to ax
		mov esi, dword [esp + 8]	
		lodsw
		mov dword [esp + 8], esi
		
		;compare
		cmp ax, dx
		jne retn_error
		
		;break if EOS
		cmp ax, 0
		je retn_ok
		
	jmp whiling_3
retn_ok:
mov eax, 1
retn 8
retn_error:
xor eax, eax
retn 8

;------------------ proc 4
strcmp:							;strcmp(stack wstr1, stack wstr2)
xor eax, eax
xor edx, edx
	strcmp_whiling:
		;put wchar to dx
		mov esi, dword [esp + 4]
		lodsb
		mov dword [esp + 4], esi
		mov dx, ax
		
		;put wchar to ax
		mov esi, dword [esp + 8]	
		lodsb
		mov dword [esp + 8], esi
		
		;compare
		cmp ax, dx
		jne strcmp_retn_error
		
		;break if EOS
		cmp ax, 0
		je strcmp_retn_ok
		
	jmp strcmp_whiling
strcmp_retn_ok:
mov eax, 1
retn 8
strcmp_retn_error:
xor eax, eax
retn 8

;------------------ proc 5
search_export_proc:					;search_import_proc(stack lib_addr, stack lib_name)
push ebx							;save context value
sub esp, 12							;0 - AddressOfNames, 4 - AddressOfNameOrdinals, 8 - AddressOfFunctions, 12 - ebx, 16 - retn addr, 20 - lib_addr, 24 - lib_name

	;put to eax PE header VA
	mov eax, [esp + 20]
	add eax, IMG_DOS_OFFSET
	mov eax, [eax]
	add eax, [esp + 20]				;RVA -> VA
		
	;put to eax PE export descriptor VA
	add eax, IMG_PE_EXP_OFFSET
	test eax, eax
	je retn_label_5
	mov eax, [eax]
	add	eax, [esp + 20]				;RVA -> VA
	
	;put to ecx NumberOfNames
	add eax, 24
	mov ecx, [eax]
	add eax, 4
	;put to stack VA AddressOfFunctions
	mov edx, [eax]
	add	edx, [esp + 20]				;RVA -> VA
	mov [esp + 8], edx				;save RVA AddressOfFunctions
	add eax, 4
	;put to stack VA AddressOfNames
	mov edx, [eax]					;put to eax RVA AddressOfNames
	add	edx, [esp + 20]				;RVA -> VA
	mov [esp], edx					;save RVA AddressOfNames
	add eax, 4
	;put to stack VA AddressOfNameOrdinals
	mov eax, [eax]
	add	eax, [esp + 20]				;RVA -> VA
	mov [esp + 4], eax				;save RVA AddressOfNameOrdinals
	add eax, 4
	;searching procedure name
	xor eax, eax
	xor edi, edi
	whiling_5:
		;if count == 0 break
		test ecx, ecx
		je retn_label_5
		dec ecx
		
		;compare
		mov eax, [esp]
		mov edx, eax				;ptr ++
		add edx, 4
		mov [esp], edx
		mov eax, [eax]
		add eax, [esp + 20]
		push eax
		mov eax, [esp + 28]
		push eax
		call strcmp
		test eax, eax
		jne break_5
		
		inc edi						;index ++
	jmp whiling_5
	break_5:
	
	;searching procedure address
	mov eax, 2
	mul edi
	add eax, [esp + 4]
	xor edx, edx
	mov dx, [eax]
	;add eax, [esp + 20]
	mov eax, 4
	mul edx
	add eax, [esp + 8]
	mov eax, [eax]
	add eax, [esp + 20]
	
	
retn_label_5:
add esp, 12
pop ebx								;restore context value
retn 8

;---------------------------
;internal data block
msg_title: 		db 'Message', 0
msg_string:		db 'Hello Peoplz', 0
wphr_ntdll		dw u16('ntdll.dll'), 0, 0
phr_ldrloaddll	db 'LdrLoadDll', 0, 0, 0, 0
wphr_user32		db u16('user32.dll'), 0, 0
phr_msgbox		db 'MessageBoxA', 0

;---------------------------
;input data block
; not used

Вот собственно мы и превратили наш hello world в более менее полноценный шеллкод с плюшками.

Несколько слов об оптимизации размера
Асм код представленный в примерах выше (~500kb), на самом деле является очень большим для шеллкода, его можно сократить, но я не стал этого делать в силу того что читабельность такого кода очень тяжелая. Поэтому напишу пару строк об оптимизации размера. В отличии от высокоуровневых компиляторов, NASM делает ровно то что ему скажут без каких-либо супер оптимизаций. Мы можем уменьшить код:

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

1 и 2 пункты думаю всем ясны, а вот про данные немного расскажу подробнее. Например для того чтобы сократить место отведённое под названия WinAPI функций часто используются их хеши. Поскольку алгоритм функции GetProcAddress мы посути реализовали когда искали адреса из Kernel32.dll в ручную, мы можем полностью отказатся от GetProcAddress в пользу самописанного варианта, функцию strcmp заменить на хеширование(причём даже с большой вероятностью коллизии для строк пойдёт), а строки на хеши. Еще как вариант избавления от строк экспортируемых функций, это хранение их WORD’овских Ordinal Name(см. доки по PE), по которым и осуществлять поиск.

Завершение исполнения
Очевидно что наш шеллкод не будет выполнятся вечно, поэтому необходимо предусмотреть вариант завершения его работы. Если работоспособность программы необходимо сохранить, то верным решением будет сохранения состояния регистров в стек, а по окончанию работы восстановление(PUSHAD\POPAD) и возврат управления. Однако в таком случае обязательно необходимо соблюдать целостность стека, иначе результат получится не предсказуемый. Если восстановления работы программы невозможно, то лучше всего воспользоватся несуществующей инструкцией или JMP в 0x00000000.

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

У такого шеллкода есть свои достоинства и недостатки, которые вобщем-то и определяют его область применения, выделим их:

Достоинства:

  • Возможность максимального уменьшения размера, до экстремальности
  • Возможность составления произвольной структуры

Недостатки:

  • Сложность разработки, особенно после оптимизации хорошо если вы сможете понять что написали :slight_smile:
  • Сложность отладки, из-за отсутствия IDE необходимо либо искуственное воссоздание условий выполнения, либо аттачить дебагер к целевому процессу
  • Непереносимость на другие архитектуры

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

Спасибо за внимание.