Делаем собственный инжектор
Давным-давно, во времена мифов и легенд, когда древние Боги были мстительны и жестоки и обрушивали на программистов все новые и новые проклятия… На хабре была опубликована статья про сплайсинг.
В качестве примера в статье был приведен довольно масштабный код, который воспринимался не особо легко. Захотелось разобраться в процессе инжекта, а также написать более простой и менее громоздкий код.
Вкратце, инжектинг – это подгрузка нашей библиотеки в сторонний процесс, а сплайсинг – перехват какой-либо функции (мы перехватывали 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 процессы есть свои заморочки, которые остаются за рамками данной статьи.
С тех самых времен прошло много столетий… Боги были усмирены человеком, но теперь мы имеем новые проблемы, которые порой страшнее гнева всевышних – это, конечно, бурление говн недовольных разработчиков, считавших самих себя Богами программирования под всякие социальные ресурсы нынешнего времени, но свергнутых также, как когда-то и настоящие Боги.
В общем, делайте инжекты, ломайте чужой софт. Этот мир интересней, чем вам кажется. (с)