Финансы Сайт на котором знают все про финансы

Делаем собственный инжектор

Давным-давно, во времена мифов и легенд, когда древние Боги были мстительны и жестоки и обрушивали на программистов все новые и новые проклятия… На хабре была опубликована статья про сплайсинг.

В качестве примера в статье был приведен довольно масштабный код, который воспринимался не особо легко. Захотелось разобраться в процессе инжекта, а также написать более простой и менее громоздкий код.
Вкратце, инжектинг — это подгрузка нашей библиотеки в сторонний процесс, а сплайсинг — перехват какой-либо функции (мы перехватывали WinAPI) и модификация её работы средствами этой самой библиотеки.
Пример будет состоять из двух частей: 1 — библиотека, 2 — инжектор, который будет внедрять библиотеку в целевой процесс. Библиотеку будем писать на masm, что позволит в разы сократить объемы кода, а инжектор — на Си.

Начнем с кода библиотеки [last updated on 15 aug 2010]. Что она делает? При подгрузке она заменяет первые 5 байт функции WinAPI, которую мы хотим перехватить, на JUMP на наш перехватчик. Мы можем это сделать в большинстве случаев, когда функция имеет пролог (то есть 100%, если функция принимает больше нуля аргументов, в этом случае в начале ее будут инструкции mov edi,edi; push ebp; mov ebp;esp, или же если функция имеет локальные переменные). Иногда функция не имеет пролога, но мы все равно можем осуществить перехват, если в начале функции имеются несущественные команды, например, NOP’ы (такое было обнаружено в функции GetTickCount на Windows Vista x64). Таких функций мало, судя по всему. Я приведу пример перехвата GetTickCount, но далеко не факт, что он сработает на вашей системе. Скажем, в XP SP3 кучи NOP’ов в начале тела функции нет. Всегда смотрите, как начинается функция без аргументов в системных библиотеках, прежде чем осуществлять ее перехват. У меня GetTickCount начиналась так:

Address   Hex dump          Command                                  Comments
756F1110    EB 05           JMP SHORT 756F1117                       ; INT kernel32.GetTickCount(void)
756F1112    90              NOP
756F1113    90              NOP
756F1114    90              NOP
756F1115    90              NOP
756F1116    90              NOP
756F1117    8B0D 2403FE7F   MOV ECX,DWORD PTR DS:[7FFE0324]
...

Слишком короткие функции без пролога перехватить так просто не удастся (например, GetCurrentProcess или GetCommandLineA), потому что первые 5 байт — это уже их тело. Их перехват можно осуществить, например, изменив адреса в таблице импортов, если они там есть, но этот материал в данной статье не затронут. Что ж, теперь к коду. Я решил написать несколько макросов, чтобы удобно ставить и убирать хуки, а также вызывать оригинал функции.

.486                      ; create 32 bit code
.model flat, stdcall      ; 32 bit memory model
option casemap :none      ; case sensitive

;подключаем макросы и библиотеки
include \masm32\include\windows.inc
include \masm32\macros\macros.asm
include \masm32\macros\windows.asm
uselib kernel32, user32, masm32

;структурка, в которую мы запишем свой jump
;на код нашего перехватчика функции
JUMPNEAR STRUCT
opcd BYTE ?
reladdr DWORD ?
JUMPNEAR ENDS

global_hook_cnt = 0

;Макрос для вызова оригинальной функции
;с оригинальными параметрами из тела перехватчика
;(пример - далее)
;func - имя перехваченной функции
;args - число аргументов
;have_prologue - имеет ли функция пролог
;(то есть, есть ли в ее начале команды mov edi,edi, push ebp; mov ebp, esp)
;(не задавайте для функций с числом аргументов больше 0, они всегда имеют пролог)

HOOK_ORIGINAL_CALL MACRO func:REQ, args:=< 0 >, have_prologue:=< 1 >
	cnt = 0
	REPEAT args
	  push [ebp+(&args - cnt + 1)*4]
	  cnt = cnt + 1
	ENDM

	push offset @CatStr(next_inst_, %global_hook_cnt)

	IF have_prologue EQ 1
	  push ebp
	  mov ebp,esp
	ENDIF

	mov eax,@CatStr(&func, _hook)
	add eax,5
	jmp eax
	@CatStr(next_inst_, %global_hook_cnt):

	global_hook_cnt = global_hook_cnt + 1
ENDM


;Макрос для вызова оригинальной функции
;с произвольными параметрами из любого места программы
;(если перехват установлен)
;(пример - далее)
;func - имя перехваченной функции
;have_prologue - имеет ли функция пролог
;(не задавайте для функций с числом аргументов больше 0, они всегда имеют пролог)
;params - список аргументов функции

HOOK_ORIGINAL_CALL_PARAM MACRO func:REQ, have_prologue:=< 1 >, params:VARARG
	count = 0
	FOR xparam, 
	  count = count + 1
	  @CatStr(var,%count) TEXTEQU @CatStr(&xparam)
	ENDM

	REPEAT count
	  push @CatStr(var,%count)
	  count = count - 1
	ENDM

	push offset @CatStr(next_inst_, %global_hook_cnt)

	IF have_prologue EQ 1
	  push ebp
	  mov ebp,esp
	ENDIF

	mov eax,@CatStr(&func, _hook)
	add eax,5
	jmp eax
	@CatStr(next_inst_, %global_hook_cnt):

	global_hook_cnt = global_hook_cnt + 1
ENDM

;Макрос для установки перехвата
;(пример - далее)
;lib - имя библиотеки, в которой содержится функция
;func - имя функции
;hook_label - название метки, по которой лежит наше тело перехватчика
;ifload - загрузить ли предварительно библиотеку (0 по умолчанию)
;have_prologue - имеет ли функция пролог
;(не задавайте для функций с числом аргументов больше 0, они всегда имеют пролог)


SET_HOOK MACRO lib:REQ, func:REQ, hook_label:REQ, ifload:=< 0 >, have_prologue:=< 1 >
	%ECHO [The hook on &func is SET on @CatStr(%@Line) Line]

	.data?
	@CatStr(libn, %global_hook_cnt) dd ?

	IFNDEF &func&_hook_
	  @CatStr(&func, _hook) dd ?
	  @CatStr(&func, _hook_) EQU <1>
	ENDIF

	IF have_prologue EQ 0
	  IFNDEF &func&_prologue_
	    @CatStr(&func, _prologue1) dw ?
	    @CatStr(&func, _prologue2) db ?
	    @CatStr(&func, _prologue3) dw ?
	    @CatStr(&func, _prologue_) EQU <1>
	  ENDIF
	ENDIF

	@CatStr(protect_, %global_hook_cnt) dd ?

	.code
	IF ifload EQ 1
	  mov @CatStr(libn, %global_hook_cnt), FUNC(LoadLibrary,chr$("&lib"))
	ELSE
	  mov @CatStr(libn, %global_hook_cnt), FUNC(GetModuleHandle,chr$("&lib"))
	ENDIF

	mov @CatStr(&func, _hook), FUNC(GetProcAddress, @CatStr(libn, %global_hook_cnt), chr$("&func"))

	invoke VirtualProtect, @CatStr(&func, _hook), sizeof JUMPNEAR, PAGE_READWRITE,  offset @CatStr(protect_, %global_hook_cnt)

	mov eax, @CatStr(&func, _hook)

	IF have_prologue EQ 0
	  mov cx, word ptr [eax]
	  mov @CatStr(&func, _prologue1), cx
	  mov cl, byte ptr [eax+2]
	  mov @CatStr(&func, _prologue2), cl
	  mov cx, word ptr [eax+3]
	  mov @CatStr(&func, _prologue3), cx
	ENDIF

	assume eax: ptr JUMPNEAR
	mov [eax].opcd, 0e9h
	mov ecx, offset &hook_label
	sub ecx,@CatStr(&func, _hook)
	sub ecx,5
	mov [eax].reladdr,ecx
	assume eax:nothing

	invoke VirtualProtect, @CatStr(&func, _hook), sizeof JUMPNEAR, @CatStr(protect_, %global_hook_cnt), offset @CatStr(protect_, %global_hook_cnt)

	global_hook_cnt = global_hook_cnt + 1
ENDM

;Макрос для снятия перехвата
;(пример - далее)
;func - имя функции
;have_prologue - имеет ли функция пролог
;(не задавайте для функций с числом аргументов больше 0, они всегда имеют пролог)

REMOVE_HOOK MACRO func:REQ, have_prologue:=< 1 >
	%ECHO [The hook on &func is REMOVED on @CatStr(%@Line) Line]

	.data?
	@CatStr(protect_, %global_hook_cnt) dd ?

	.code
	invoke VirtualProtect, @CatStr(&func, _hook), sizeof JUMPNEAR, PAGE_READWRITE,  offset @CatStr(protect_, %global_hook_cnt)

	mov eax, @CatStr(&func, _hook)

	IF have_prologue EQ 0
	  mov cx, @CatStr(&func, _prologue1)
	  mov word ptr [eax], cx
	  mov cl, @CatStr(&func, _prologue2)
	  mov byte ptr [eax+2], cl
	  mov cx, @CatStr(&func, _prologue3)
	  mov word ptr [eax+3], cx
	ELSE
	  mov word ptr [eax], 0ff8bh
	  mov byte ptr [eax+2], 55h
	  mov word ptr [eax+3], 0e589h
	ENDIF

	invoke VirtualProtect, @CatStr(&func, _hook), sizeof JUMPNEAR, @CatStr(protect_, %global_hook_cnt), offset @CatStr(protect_, %global_hook_cnt)

	global_hook_cnt = global_hook_cnt + 1
ENDM




;начало примера
.code

;Тело нашего перехватчика для функции MessageBoxA
MyFunc:
	push ebp
	mov ebp,esp
	;сюда можно поместить любой код
	;[ebp+8] будет содержать первый аргумент функции
	;[ebp+12] - второй аргумент и т.д.
	;[ebp+4] будет содержать адрес возврата из функции


	;вызываем оригинальную функцию с переданными параметрами
	HOOK_ORIGINAL_CALL MessageBoxA, 4
	;это эквивалентно записи
	;HOOK_ORIGINAL_CALL_PARAM MessageBoxA,1,  [ebp+8], [ebp+12], [ebp+16], [ebp+20]

	;а теперь подменим параметры (текст месадж бокса и иконку):
	HOOK_ORIGINAL_CALL_PARAM MessageBoxA,1,  [ebp+8], chr$("ахаха, перехват быдлозащиты!"), [ebp+16], MB_SYSTEMMODAL or MB_ICONERROR


	;сюда тоже можно поместить любой код
	;[ebp+8] будет содержать первый аргумент функции
	;[ebp+12] - второй аргумент и т.д.
	;[ebp+4] будет содержать адрес возврата из функции
	;регистр eax будет содержать возвращенное функцией значение
	;его можно заменить здесь

	pop ebp
	retn 4*4; (число аргументов функции MessageBoxA) * 4



;Тело перехватчика GetTickCount (помните, не факт, что это заработает на вашей системе, как я уже говорил!)
MyFunc2:
	push ebp
	mov ebp,esp
	;сюда можно поместить любой код
	;[ebp+4] будет содержать адрес возврата из функции


	;вызываем оригинальную функцию
	HOOK_ORIGINAL_CALL GetTickCount, 0, 0
	;это эквивалентно записи
	;HOOK_ORIGINAL_CALL_PARAM GetTickCount,0


	;сюда тоже можно поместить любой код
	;[ebp+4] будет содержать адрес возврата из функции
	;регистр eax будет содержать возвращенное функцией значение, его можно заменить здесь

        mov eax,1337 ;вот мы и подменили ответ функции

	pop ebp
	ret ;(0 аргументов у GetTickCount, поэтому просто ret)


;Точка входа в DLL

LibMain proc instance:DWORD,reason:DWORD,reserved:DWORD
  LOCAL buf [20] :byte ;локальная переменная для хранения значения, возвращенного GetTickCount

  .if reason == DLL_PROCESS_ATTACH ;если наша DLL свежезагружена
    ;устанавливаем перехват MessageBoxA
    SET_HOOK user32.dll, MessageBoxA, MyFunc

    ;этот вызов будет перехвачен
    invoke MessageBox,0,chr$("Hooked message box"),chr$("Test"),MB_SYSTEMMODAL or MB_ICONINFORMATION

    ;этот вызов не будет перехвачен - он выполняется в обход тела перехватчика
    HOOK_ORIGINAL_CALL_PARAM MessageBoxA,1,  0,chr$("Message box call without hook"),chr$("Test"),MB_SYSTEMMODAL or MB_ICONINFORMATION

    ;снимаем перехват
    REMOVE_HOOK MessageBoxA

    ;этот вызов уже не перехватывается
    invoke MessageBox,0,chr$("Original message box call"),chr$("Test"),MB_SYSTEMMODAL or MB_ICONINFORMATION

    ;теперь установим перехват GetTickCount (эта функция не имеет пролога)
    SET_HOOK kernel32.dll, GetTickCount, MyFunc2, 0, 0

    ;этот вызов перехвачен, и значение тут подменено - 1337
    invoke GetTickCount

    ;преобразуем ответ функции в текстовый вид
    invoke wsprintf,addr buf,chr$("%u"),eax

    ;выведем ответ, который вернула GetTickCount (он тут подменен)
    invoke MessageBox,0,addr buf,chr$("Test"),MB_SYSTEMMODAL or MB_ICONINFORMATION

    ;убираем перехват
    REMOVE_HOOK GetTickCount, 0

    ;этот вызов уже не перехвачен
    invoke GetTickCount

    ;преобразуем ответ функции в текстовый вид
    invoke wsprintf,addr buf,chr$("%u"),eax

    ;выведем ответ, который вернула GetTickCount
    invoke MessageBox,0,addr buf,chr$("Test"),MB_SYSTEMMODAL or MB_ICONINFORMATION

    mov eax,1 ;сообщаем об успешной загрузке нашей DLL

  .endif
ret
LibMain ENDP

end LibMain

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

Теперь рассмотрим код самого инжектора:

#undef UNICODE
#include 
#include 
#include 
#include 
#include <sys/stat.h>

#define BUFSIZE 100
//Имя внедряемой библиотеки
#define DLL_NAME "mem.dll"

//структура описывает поля, в которых содержится код внедрения
#pragma pack(push, 1)
struct INJECTORCODE
{
	BYTE  instr_push_loadlibrary_arg; //инструкция push
	DWORD loadlibrary_arg;            //аргумент push

	WORD  instr_call_loadlibrary;     //инструкция call
	DWORD adr_from_call_loadlibrary;

	BYTE  instr_push_exitthread_arg;
	DWORD exitthread_arg;

	WORD  instr_call_exitthread;
	DWORD adr_from_call_exitthread;

	DWORD addr_loadlibrary;
	DWORD addr_exitthread;     //адрес функции ExitThread
	BYTE  libraryname[100];    //имя и путь к загружаемой библиотеке
};

#pragma pack(pop)

BOOL InjectDll(DWORD pid, char *lpszDllName)
{
	HANDLE hProcess;
	BYTE *p_code;
	INJECTORCODE cmds;
	DWORD wr, id;

	//Открыть процесс с нужным доступом
	hProcess = OpenProcess(PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|0x1000|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ, FALSE, pid);
	if(hProcess == NULL)
	{
		MessageBoxA(NULL, "You have not enough rights to attach dlls", "Error!", 0);
		return FALSE;
	}

	//Зарезервировать память в процессе
	p_code = (BYTE*)VirtualAllocEx(hProcess, 0, sizeof(INJECTORCODE),
		MEM_COMMIT, PAGE_READWRITE);
	if(p_code == NULL)
	{
		MessageBox(NULL, "Unable to alloc memory in remote process", "Error!", 0);
		return FALSE;
	}



	//Инициализировать  машинный код
	cmds.instr_push_loadlibrary_arg = 0x68; //машинный код инструкции push
	cmds.loadlibrary_arg = (DWORD)((BYTE*)p_code 
		+ offsetof(INJECTORCODE, libraryname));

	cmds.instr_call_loadlibrary = 0x15ff; //машинный код инструкции call
	cmds.adr_from_call_loadlibrary = 
		(DWORD)(p_code + offsetof(INJECTORCODE, addr_loadlibrary));

	cmds.instr_push_exitthread_arg  = 0x68;
	cmds.exitthread_arg = 0;

	cmds.instr_call_exitthread = 0x15ff; 
	cmds.adr_from_call_exitthread = 
		(DWORD)(p_code + offsetof(INJECTORCODE, addr_exitthread));

	cmds.addr_loadlibrary = 
		(DWORD)GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");

	cmds.addr_exitthread  = 
		(DWORD)GetProcAddress(GetModuleHandle("kernel32.dll"),"ExitThread");

	if(strlen(lpszDllName) > 99)
	{
		MessageBox(NULL, "Dll Name too long", "Error!", 0);
		return FALSE;
	}
	strcpy((char*)cmds.libraryname, lpszDllName );

	/*После инициализации cmds в мнемонике ассемблера выглядит следующим
	образом:
	push  adr_library_name               ;аргумент ф-ции loadlibrary
	call dword ptr [loadlibrary_adr]     ;вызвать LoadLibrary 
	push exit_thread_arg                 ;аргумент для ExitThread
	call dword ptr [exit_thread_adr]     ;вызвать ExitThread     
	*/

	//Записать машинный код по зарезервированному адресу
	WriteProcessMemory(hProcess, p_code, &cmds, sizeof(cmds), &wr);


	//Выполнить машинный код
	DWORD old_rights;

	HANDLE z = CreateRemoteThread(hProcess, NULL, 0, 
		(unsigned long (__stdcall *)(void *))p_code, 0, 0, &id);

	
	//Ожидать завершения удаленного потока
	WaitForSingleObject(z, INFINITE);
	//Освободить память
	VirtualFreeEx(hProcess, (void*)p_code, sizeof(cmds), MEM_RELEASE);

	return TRUE;
}

//Функция установки привилегий отладчика
int AdjustPrivileges() 
{
	HANDLE hToken;
	TOKEN_PRIVILEGES tp;
	TOKEN_PRIVILEGES oldtp;
	DWORD dwSize = sizeof(TOKEN_PRIVILEGES);
	LUID luid;

	if(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
	{
		if(GetLastError() == ERROR_CALL_NOT_IMPLEMENTED)
			return 1;
		
		return 0;
	}

	if(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
	{
		CloseHandle(hToken);
		return 0;
	}

	ZeroMemory(&tp, sizeof(tp));
	tp.PrivilegeCount = 1;
	tp.Privileges[0].Luid = luid;
	tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

	if(!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &oldtp, &dwSize))
	{
		CloseHandle(hToken);
		return 0;
	}

	CloseHandle(hToken);

	return 1;
}

//Функция проверки существования файла
int file_exists(char * fileName)
{
	struct stat buf;
	return stat(fileName, &buf) ? 0 : 1;
}


int gogo(DWORD pid)
{
	//Настраиваем привилегии
	if(!AdjustPrivileges())
	{
		MessageBox(NULL, "Can't adjust privileges", "Error", 0);
		return 0;
	}
	

	if(pid)
	{
		if(file_exists(DLL_NAME))
		{
			//Получаем полный путь к библиотеке и инжектим её в целевой процесс
			char dll_path[BUFSIZE];
			GetFullPathName(DLL_NAME, BUFSIZE, dll_path, NULL);
			InjectDll(pid, dll_path);
		}
		else
		{
			MessageBox(NULL, "Can't find dll file", "Error", 0);
		}
	}
	else
	{
		MessageBox(NULL, "Can't find process", "Error", 0);
	}
	return 0;
}

//Интерфейс программы
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	static HWND button, edit, label;
	HINSTANCE hInst;
	HANDLE hFont = CreateFont
	(
		14, 0, 0, 0, FW_EXTRALIGHT, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
		OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
		DEFAULT_PITCH | FF_SWISS, "Courier New"
	);
		
	switch(msg)
	{
		case WM_CREATE:
			//Создаем элементы интерфейса
			button = CreateWindow
			(
				"button", "Inject!", WS_CHILD | WS_VISIBLE,  
				85, 10, 60, 22, hwnd, (HMENU) 2,
				NULL, NULL
			);
			edit = CreateWindow
			(
				"Edit", "1", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_NUMBER,
				30, 10, 50, 22, hwnd, (HMENU) 3,
				NULL, NULL
			);
			label = CreateWindow
			(
				"static", "PID", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
				5, 13, 20, 22, hwnd, (HMENU) 4,
                NULL, NULL
			);
			//Устанавливаем шрифт для элементов интерфейса
			SendMessage(button, WM_SETFONT, WPARAM(hFont), TRUE);
			SendMessage(edit, WM_SETFONT, WPARAM(hFont), TRUE);
			SendMessage(label, WM_SETFONT, WPARAM(hFont), TRUE);

		break;
	
		case WM_COMMAND:
			//Обрабатываем нажатие на клавишу
			if(LOWORD(wParam) == 2) 
			{
				char buf[10];
				DWORD pid;
				
				GetWindowText(edit, buf, 10);
				sscanf(buf, "%u", &pid);
				
				gogo(pid);
			}
		
		break;

		case WM_DESTROY:
			PostQuitMessage(0);
		break;
	}
	
	return DefWindowProc(hwnd, msg, wParam, lParam);
}

//Точка входа
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	MSG  msg ;    
	WNDCLASS wc = {0};
	wc.lpszClassName = "Edit Control";
	wc.hInstance     = hInstance;
	wc.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
	wc.lpfnWndProc   = WndProc;
	wc.hCursor       = LoadCursor(0, IDC_ARROW);

  
	RegisterClass(&wc);
	CreateWindow
	(
		wc.lpszClassName, "Injector",
		WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_VISIBLE,
		220, 220, 155, 80, 0, 0, hInstance, 0
	);  
	
	while(GetMessage(&msg, NULL, 0, 0))
	{
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}
	
	return 0;
}

Исходный код dll’ки и инжектора одним архивом: скачать

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

С тех самых времен прошло много столетий… Боги были усмирены человеком, но теперь мы имеем новые проблемы, которые порой страшнее гнева всевышних — это, конечно, бурление говн недовольных разработчиков, считавших самих себя Богами программирования под всякие социальные ресурсы нынешнего времени, но свергнутых также, как когда-то и настоящие Боги.

В общем, делайте инжекты, ломайте чужой софт. Этот мир интересней, чем вам кажется. (с)

0 Ответов на “Делаем собственный инжектор”

    Добавить комментарий

    Ваш e-mail не будет опубликован. Обязательные поля помечены *