Источник: http://rootkits.ru/library/ShowLib.aspx?id_l=29 | ||
Инжект: лезем через окно | ||
Одним из самых соблазнительных мест для преступника, пытающегося пробраться в чужую квартиру, безусловно, является окно... | ||
Одним из самых соблазнительных мест для преступника, пытающегося пробраться в чужую квартиру, безусловно, является окно. Да и вообще, для русских людей характерно приходить к чему-то новому не «через дверь», а «через форточку», ибо так нам завещал сам царь Петр. Вот и мы, подражая великому правителю, попытаемся проникнуть в адресное пространство чужого процесса через… окно. Применение данному методу, в большинстве случаев, наверняка будет не совсем законным, но я не ставлю перед собой цель научить толпы кулхацкеров проникать незамеченными туда, куда их не просят. Я лишь хочу раскрыть технологию, лежащую на поверхности с незапамятных времен, но, почему-то, до сих пор, ни кем не замеченную. Так сказать, хочу подтолкнуть истинных исследователей зарыться еще глубже в недра замечательной операционной системы под названием Windows. Посему, ответственность за применение материала, изложенного в этой статье, я возлагаю на плечи тех, кто будет ее применять… Если проследить за работой всем известной функции GetWindow(), то можно заметить, что для получения результата она не обращается к ядру, а берет данные прямо из адресного пространства текущего процесса. Это наталкивает на мысль, что каждый GUI-процесс имеет в себе копию таблиц, содержащих информацию об окнах. Действительно, на юзермодные адреса любого процесса, использующего user32.dll, система отображает ядерную область памяти, доступную только для чтения и имеющую атрибут супервизора. В этой области и хранятся все структуры, описывающие окна, классы и т. д. Значит, создав окно, к примеру с надписью «My Cool Window», эта самая строка сразу окажется отображенной в АП всех GUI-процессов. А если вместо строки подставить хитро составленный исполняемый код? Вот вам и инжект. После этого останется лишь найти наш код в чужом процессе и передать ему управление. Тут, правда, есть несколько нюансов, заслуживающих внимания. Дело в том, что система хранит надписи окон в кодировке Unicode, не зависимо от того, в какой кодировке надпись задавалась изначально. Поэтому после создания окна наш исполняемый код разбавится нулями и превратится в кашу. Короче говоря, для хранения программного кода текст в окне нам не подойдет. А вот класс окна – в самый раз! Он хранится в ANSI-кодировке, и система сохраняет его именно таким, каким мы его зададим, т.е. готовым к исполнению. Второй нюанс это то, что в глазах системы класс окна должен быть оканчивающейся нулевым байтом строкой. Поэтому наш код не должен содержать в себе нулей. Составление такого кода достаточно сложная инженерная задача, сродная подготовке эксплойта, поэтому мы не будем усложнять себе жизнь и напишем лишь маленький переходник, подгружающий заранее подготовленную динамическую библиотеку, которая и будет выполнять всю полезную нагрузку, а в нашем случае просто выдавать сообщение. Итак, начнем… А начнем мы с того, что попробуем найти указатель на структуру какого-либо окна в своем собственном процессе. Структура эта зовется tagWND и, к сожалению, нигде не документирована. Тут нам стоит отдать должное неизвестным героям, в свое время «позаимствовавшим» исходники Windows 2000 у корпорации Microsoft. Так же не забудем поблагодарить Ильфака Гильфанова за мощнейший дизассемблер и Oleh Yuschuk за лучший отладчик режима пользователя. Собственно говоря, эти вещи, да еще пивная открывалка – все, чем я пользовался во время своих исследований. Для того чтобы научиться находить этот самый указатель, нам необходимо понять, как его получает сама система. Для этого обратимся к коду уже упомянутой мной выше функции GetWindow(). В Windows XP SP2 она выглядит следующим образом: mov edi, edi Если пройтись по ней отладчиком, то видно, что hWnd окна, передаваемый в функцию ValidateHwnd() через регистр ecx, чудесным образом превращается в некий указатель, с которым и работает внутренняя _GetWindow(). Как не трудно догадаться, это и есть указатель на структуру окна, он называется pWnd. Чтобы не затягивать повествование и не загромождать статью лишними листингами, сразу перейду непосредственно к алгоритму работы функции ValidateHwnd(): 1. Как я уже говорил, область памяти, хранящая оконные структуры, на самом деле расположена в ядре, а на юзермодное пространство лишь спроецирована. Следовательно, все указатели в этих структурах ядерные и нам необходимо найти дельту (разницу) между соответствующими kernel и user адресами. Она лежит в PEB текущего потока по смещению 6E8h – смещение это не изменилось со времен win_2k вплоть до win_vista. Хочу заметить, что в разных процессах значения дельт отличаются. 2. Существует юзермодная структура, называемая SHAREDINFO, в ней хранятся важнейшие указатели, необходимые для работы оконной подсистемы Win32. В том числе и указатель на массив элементов HANDLEENTRY, каждый элемент (структура) которого, кроме первого, неиспользуемого, описывает соответствующий GUI-объект в ядре (Window, Pen, DC и т. д.) и хранит указатель (kernel-адрес) на него. Это как раз то, что нам нужно! Жаль только, что адрес SHAREDINFO разнится от сервиспака к сервиспаку, а гарантированного способа найти его динамически я не нашел. Поэтому остается только хранить все возможные адреса и использовать нужный в зависимости от версии системы: win_2k sp4 - 77E69088h Ниже я приведу описание этих двух ключевых структур: PSHAREDINFO = ^SHAREDINFO; 3. После того, как будет получена дельта и указатель на SHAREDINFO (в реале он называется gSharedInfo), нам уже можно будет получить pWnd по hWnd. Для этого необходимо разобраться со структурой хэндла. Она проста до безобразия – в его младшем слове хранится индекс элемента HANDLEENTRY в массиве HANDLEENTRY_ARRAY. Назначение старшего слова не вполне ясно, все что известно – это то, что оно должно совпадать со значением поля wUniq из соответствующего хэндлу элемента HANDLEENTRY. Итак, мы получаем индекс элемента в таблице, убеждаемся, что это окно, сравнивая поле bType с единицей, далее сравниваем старшее слово хэндла с полем wUniq и, в случае их равенства, от значения поля pHead, которое является kernel-указателем на tagWND, отнимаем найденую ранее дельту, чтобы получить юзермодный указатель. Все, результат готов! Теперь напишем код нашей собственной функции ValidateHwnd(): const Научившись добывать указатель на tagWND, стоило бы разобраться с самой структурой, но мы этого делать не будем, т.к. она достаточно громоздка и абсолютное большинство ее полей нас вообще не интересует. Нужно знать лишь то, что по смещению 10h от ее начала лежит указатель на саму себя в ядре – поле tagWND.head.pSelf (мы ведь помним, что ValidateHwnd() возвращает user-адрес?), а по смещению 64h находится kernel-указатель на структуру класса, к которому принадлежит окно (tagWND.pcls) – tagCLS. Чтобы получить user-указатель на tagCLS, нужно воспользоваться следующей формулой, которая универсальна для всех подобных структур, лежащих в ядре и имеющих ссылку на себя: field_useraddr := struct.field_kerneladdr – struct.pSelf + struct Здесь struct – это user-адрес имеющейся структуры, struct.pSelf – kernel-указатель на себя, а struct.field_kerneladdr – kernel-адрес, который нужно преобразовать. Получив интересующий нас user-адрес pCls, необходимо выудить из структуры класса указатель на ANSI-строку с его именем. Он лежит по смещению 54h от начала и называется lpszClientAnsiMenuName. Естественно, адрес, содержащийся в нем – ядерный. Итак, вооружившись всеми этими, сумбурно изложенными мной данными, напишем функцию, возвращающую указатель на имя класса по pWnd: function GetPClassName(pWnd: pointer): pointer; stdcall; Вот, треть дела уже сделана! Теперь хорошо бы разобраться с тем, как искать нужные нам адреса в чужом процессе. Тут на самом деле нет ни чего сложного и отличного от уже изложенного материала, разве что объемы кода возрастут из-за необходимости чтения «чужой» памяти. Посему, не имея ни малейшего желания лишний раз загромождать статью, я опишу только ключевые моменты. В крайнем случае, читатель всегда может обратиться к приложенным к статье исходникам – не зря же я их писал… 1. Адрес TEB чужого потока можно узнать с помощью экспорта ntdll.dll – функции ZwQueryInformationThread, вызвав ее с классом информации ThreadBasicInformation. Указанный буфер, в случае успеха, заполнится структурой типа THREAD_BASIC_INFORMATION, которая имеет в себе элемент TebBaseAddress – как раз то, что нам нужно. Впрочем, замечательный труд Гарри Нэббета о Native API расскажет обо всем этом намного лучше меня. 2. В Windows Vista системные библиотеки в различных процессах могут грузиться по разным адресам, поэтому чтобы найти gSharedInfo нам, сначала, необходимо получить адрес, по которому загружена user32.dll, а потом прибавить к найденному значению 6A6C0h. Это число валидно лишь для Vista SP0, для последующих сервиспаков оно, скорее всего, будет другим. Разобравшись с поиском указателей, пора приступать к подготовке шеллкода. Для начала определимся с тем, каким образом ему будет передаваться управление. Я не стал сильно мудрствовать и решил использовать избитый всеми SetThreadContext(). Это документированный и достаточно надежный способ, хотя и палится он всеми, кому не лень, собственно как и все остальные паблик-методы. Наш код, не содержащий нулей, после того, как закончит свои дела, должен будет вернуть управление потоку-жертве в то место, где выполнение последнего было нами прервано, естественно сохранив значения всех регистров. Учитывая все вышесказанное и имея ввиду то, что мы условились внедрять лишь маленький переходник, подгружающий заранее подготовленную DLL, становится возможным сделать примерный набросок нашего шеллкода: pushad И тут мы упираемся в подводный камень – каждое значение, заносимое нами в регистр eax командой mov, может и, скорее всего, будет содержать в себе нули. Здесь стоит откупорить еще бутылочку пивасика и включить воображение: нам необходимо написать генератор кода, использующий универсальный способ замены команды mov, да еще и такой, чтобы налету отсечь все нули из числа. Немного пораскинув мозгами, в голову приходит неплохой вариант. Надо заметить, что изначально мне в голову пришел совсем не тот вариант, который стоило бы публиковать, но мой друг Хакер (http://wasm.ru/forum/profile.php?id=14178) вовремя наставил на путь истинный, за что ему «респект и уважуха». Нам нужно найти маску, при xor’е с которой исходное число давало бы DWORD, не содержащий нулей. Такая маска находится по элементарному алгоритму: к исходному числу («N») прибавляем 01010101h, получая «X», проверяем каждый байт результата – если он равен нулю или FFh, то прибавляем к нему 2. Далее высчитываем «Y» - Y = X xor N. Все. Допустим, нам нужно поместить в eax число 0098BCC8h. Вот так может выглядеть код, решающий эту проблему: mov eax, 0199BDC9h // X А теперь напишем функцию, генерирующую такой код динамически: function GenCodeToPutDWORD(dwDWORD: DWORD; pCode: pointer): pointer; stdcall; Ну, вот мы и во всеоружии! Теперь приступим непосредственно к тому, ради чего все это затеяли – к внедрению кода. Порядок действий будет такой: первым делом создадим окно, класс которого будет являться полным путем к подгружаемой динамической библиотеке. Тут есть маленькое «но» - для задания пути в кодировке UNICODE класс окна не подойдет, но как нельзя лучше сгодится надпись на нем. В структуре tagWND указатель на надпись лежит по смещению 88h. Дальше найдем адрес функции LoadLibraryA() в чужом процессе (в win_vista он может отличаться от адреса в нашем АП). Сгенерируем первую часть шеллкода по приведенному выше шаблону, внеся в него предварительно найденный указатель на путь к внедряемой DLL. Следующим шагом будет приостановка целевого потока и получение его контекста. Генерируем вторую часть кода, которая возвратит управление по только что полученному eip и создаем окно с классом, содержащим в себе шеллкод. Ищем указатель на класс этого окна и устанавливаем новое значение eip для потока. И все! Вот какой код получился у меня (да не ужаснутся хакеры старой школы от использования Delphi с VCL – для простоты и наглядности такой подход в самый раз): procedure TfrmMain.cmdClick(Sender: TObject); В листинге я не стал приводить содержимое таких функций, как GetProcAddressEx(), ProcessIdFromThreadId() и т.д. – их назначение понятно из названия, а код тривиален. Любителей копипаста отправляю к исходникам, приложенным к статье. Ну вот, пожалуй, и все, что я хотел поведать. Надеюсь, информация, изложенная мной в данной статье, хоть как-то окажется полезной конечному читателю… © Twister. 2008 г. http://twister.orgfree.com/ |
||
Twister |
||