Потаенные сады Windows: Исследуем недра операционной системы с помощью дебаггера и не толькоУ Стивена Кинга есть произведение «Потаенное окно, потаенный сад». Не могу сказать, что я люблю творчество этого писателя, но если ты не читал эту книгу, настоятельно советую найти и прочесть. Очень занимательная и одновременно пугающая книга. В ней со всей присущей Стивену Кингу ужасающей красотой изложения рассказывается о том, какие тайны может хранить в себе сознание любого человека. Вот и сегодня мы, наверное, не будем разговаривать на какую-то конкретную тему. Мы просто немного полазаем в потаенном саду Windows, забравшись туда через потаенное окно дебаггера :). Я попробую рассказать о скрытых местах, странностях и неизвестностях операционной системы Windows. Эти знания помогут тебе, как программисту, лучше знать, понимать и использовать эти самые потаенные места в своих грязных целях. ВведениеДаже по прошествии многих лет, потраченных на изучение внутренностей операционной системы и системного кодинга, понимаешь, что постичь все тонкости ОС вряд ли удастся. Я не имею в виду именно себя — такого мнения придерживаются многие программисты, с которыми я знаком. При этом зачастую единственным инструментом, позволяющим выпытать те или иные секреты операционной системы, становится отладчик или дебаггер. Хотя не все любят возиться с отладчиком, положения дел это не меняет — если хочешь находить, простите за каламбур, потаенные окна в Windows — без него не обойтись. Итак, начнем. Загадочный параметр LPRESERVED в DLLMAINВсем нам известна точка входа при старте библиотек — DllMain: Принимает она (точка входа) три параметра. С первыми двумя все понятно, но как быть с третьим? И действительно, зачем нужен этот параметр lpReserved, если нигде в коде при инициализации библиотеки он больше не используется? Оказывается не все так просто, как пытается это показать Microsoft. MSDN утверждает, что этот параметр используется при загрузке/выгрузке библиотеки; в частности, при статических операциях библиотеки этот параметр содержит отличное от нуля значение. И, наоборот, при динамических операциях lpReserved будет равным нулю. Открою страшную тайну: lpReserved есть ничто иное, как указатель на контекст стартующего процесса, который грузит библиотеку! Подробности таковы: при старте нового потока ядро ставит его в очередь для исполнения в виде APC — AsyncProcedureCall, который передается в функцию LdrInitializeThunk, который вызывается Ntdll.dll. Одним из параметров, который передается LdrInitializeThunk, является указатель на структуру CONTEXT, которая описывает начальное состояние потока — регистры, данные и т.п. После выполнения APC, контроль передается LdrInitializeThunk. Раз уж исполнение нового потока начинается с вызова ntdll!LdrInitializeThunk, то этой функции передается стартовый адрес, определенный функцией CreateThread. Таким образом становится понятно (а уж под отладчиком — тем более!), что CreateThread должен передать через APC в вызов LdrInitializeThunk параметры старта процесса. Подведем итоги: в случае, если dwReason равен DLL_PROCESS_ATTACH (при загрузке библиотеки), lpReserved равен NULL для динамической загрузки и non-NULL для статической загрузки. В случае, если fdwReason равен DLL_PROCESS_DETACH (при выгрузке библиотеки), lpReserved равен NUL при вызове FreeLibrary и при ошибке загрузки DLL, и nonNULL — при окончании процесса. Зачем Microsoft скрывать этот факт? На самом деле, я бы тоже его скрыл :). Подумай сам, сколько возможностей подмены контекста открывается при этом! Что? Ты никогда не слышал о контексте процесса? И системный вызов SetThreadContext тебе тоже ни о чем не говорит? Окей, рассмотрим. Во-первых, контроль над структурой CONTEXT даст нам контроль над регистрами процессора. Все регистры процессора при старте указываются в структуре CONTEXT (смотри описание этой структуры). Это могут быть DEBUG-регистры для контроля над определенным приложением или же установки перехватов вызовов функций. Или, кстати, установки флага TF в регистре EFLAGS Во-вторых, путем изменения lpReserved->Eip можно изменить точку старта библиотеки. Эта особенность также может быть использована в определении версии ОС, которая используется на целевой машине путем выбора точки входа в зависимости от версии ОС. Незаменимое свойство для обеспечения переносимости кода, кстати. Аналогичные адреса загрузки dllИ действительно, если ты обращал внимание, такие библио теки как ntdll.dll, kernel32.dll и user32.dll для всех процессов всегда загружаются по одному и тому системному адресу, хотя Microsoft это никак не объясняет. Почему? Как ты знаешь, указанные библиотеки представляют программисту набор системных функций для работы с системой. К примеру, ntdll.dll является самой важной из юзермодных библиотек. Она представляет собой своеобразную заглушку для вызова системных сервисов. И она должна быть загружена по одному и тому же адресу именно по этой причине. Например, создание любого юзермодного потока всегда происходит через вызов функции ntdll!LdrInitializeThunk. Функция ntdll!KiUserApcDispatcher нужна системе для того, чтобы поставить в очередь исполнение юзермодных асинхронных вызовов. Ядро операционной системы определяет адреса этих функций еще на стадии инициализации системы. И, так как ядро использует скэшированные указатели на эти функции (для быстродействия), ntdll.dll уже не может быть загружена по другим адресам. Kernel32.dll не может быть загружен по различным адресам, потому что большое количество предоставляемых этой библиотекой сервисов используются системой для кросспроцессовых инъекций кода. Например, kernel32.dll ответственна за обработчик событий консоли (что делает команда Ctrl+C в консоли, помнишь?). Так как консоль могут запустить многие программы, адрес обработчика Ctrl+C должен быть одним и тем же. Ну а user32.dll постоянно загружается по одному и тому же адресу по той простой причине, что она предоставляет кучу сервисов, используемых win32k.sys — драйвера, реализующего оконную подсистему Windows. Указатели на эти функции win32k.sys получает через вызов NtUserInitializeClientPfnArrays во время загрузки. Однопоточность? Не тут-то было!Часто ли ты используешь в своих программах отдельные потоки? Если программа простая, и ей не требуется обрабатывать большие массивы данных, вряд ли она для тебя будет многопотоковой. Но это только на первый взгляд. Потому что многие (если не все) Win32-приложения на самом деле являются многопотоковыми программами, даже если их разработчик утверждает обратное. К примеру, при старте программы сервисом подсистемы CSRSS в программе по умолчанию создается отдельный поток для обработки консольных событий типа Ctrl+C/Ctrl+Break. Во-вторых, большинство Win32-API-функций для выполнения своего кода используют отдельные потоки. Например, вызов WSAAsyncGetHostByName исполь зует синхронный вызов gethostbyname в отдельном потоке, после чего возвращает результаты запрашивающему через оконные сообщения. Разница между нативными x86версиями библиотек и их WOW64-аналогамиМеханизм Wow64 включает в себя полный набор 32-битных системных dll, реализующий Win32 API-функции (для их использования Wow64-программами). Так какова же разница между «нормальными» 32-битными dll и их Wow64-версиями? На 64-битных версиях Windows разницы между такими библиотеками нет — большинство dll являют собой 32-битные копии с 32-битной версии операционной системы. К примеру, Wow64-библиотека ws2_32.dll на Vista x64 — тот же самый файл, что и 32-битная ws2_32. dll на Vista x86. Вместе с тем, некоторые dll отличаются очень значительно, к примеру, ntdll.dll. Если мы глянем сквозь призму отладчика на x86 версию ntdll.dll, то легко сможем увидеть, что системный вызов уходит в ядро системы через так называемый SystemCallStub в структуре SharedUserData:
В Wow64-версии ntdll картина разительно отличается. Вызов системного сервиса происходит через поле по смещению 0xc0 в 32-битной структуре TEB (Thread Environment Block):
В свою очередь, раскрываем структуру TEB и там по смещению 0xc0 видим поле, помеченное как “WOW32Reserved”:
Кстати, в качестве лирического отступления от темы хочу заметить, что если ты планируешь использовать 32-битные программы под Wow64, будь очень внимателен при использовании таких функций как GetThreadContext/SetThreadContext, и вот почему. Данные функции требуют дополнительных привилегий при исполнении в контексте Wow64. В частности, им нужен доступ к данным THREAD_QUERY_INFORMATION. 12 способов завершить процессЧтобы ты всегда мог выйти победителем из социалистического соревнования на тему «Кто знает больше способов грохнуть процесс», проведенного в кругу друзей, любимый журнал заботливо подгоняет тебе целых 12 методов:
Кстати, код, реализующий поиск и перехват PspTerminateThreadByPointer для защиты твоего процесса от убийства таким способом, ты сможешь найти на диске. ЗаключениеЧитать доки, бесспорно, очень полезно. Поскольку они — рулез. Однако практика показывает, что самые вкусности и сочные куски ОС часто бывают недокументированными, и разработчики Windows очень неохотно раскрывают нам эти секреты. Но все тайное всегда становится явным. Так что дерзай! Удачного компилирования, и да пребудет с тобой Сила! LinksДля более конкретного изучения внутренностей ОС Windows обычных форумов недостаточно. Очень часто золотые крупинки можно отыскать в блогах системных программистов, таких как www.alex-ionescu.com или http://j00ru.vexillium.org. |