«Угон» потоков
May 4, 2010 · CommentsОбработка исключенийThread hijackingWindowsWindows KernelWow64
Бывает так, что требуется выполнить свой код в контексте произвольного потока. Либо в своем потоке, но в то время когда поток выполняет чужой код. Например, сборщик мусора может хотеть перехватить управление, даже если поток крутит бесконечный цикл. Один из методов перехвата – использование функций GetThreadContext и SetThreadContext. Эти функции позволяют манипулировать контекстом потока – т.е. состоянием регистров процессора, в том числе и указателем на текущую выполняемую инструкцию. В простейшем случае перехватчик приостанавливает поток, сохраняет текущий контекст, модифицирует EIP, чтобы тот показывал на нужный код, и снова запускает поток. При обратном переключении просто восстанавливается ранее сохраненный контекст и все. Метод простой, эффективный и … неработающий. :-)
Чтобы разобраться в чем тут дело, рассмотрим как работают функции GetThreadContext и SetThreadContext. Когда процессор переключается в режим ядра, операционная система сохраняет состояние регистров пользовательского режима в ядерном стеке. При обратном переключении, сохраненные значения вновь загружаются в регистры процессора. GetThreadContext и SetThreadContext принудительно переключает поток в режим ядра (посылают потоку kernel APC, фактически – прерывание) и читает или модифицирует пользовательский контекст прямо в стеке. Важное следствие такого подхода – поток может быть прерван в любой момент, за исключением немногочисленных участков ядерного кода, защищенных от kernel APC.
Проблема первая: при переходе в режим ядра не всегда сохраняются все регистры. С точки зрения NT существует три способа переключения в режим ядра: системный вызов, исключение (trap) и прерывание. При вызове системной функции в соответствии с соглашением о вызовах не сохраняются volatile регистры. Ну а раз они не сохраняются, то и восстанавливаться они не будут. Соответственно попытка установить с помощью SetThreadContext, скажем EAX, может и не увенчается успехом.
Проблема вторая: GetThreadContext и SetThreadContext – не единственные, кто модифицирует состояние регистров пользовательского режима. Существует функция NtContinue и диспетчер прерываний. Последний особенно интересен:
xor eax, eax ; eax = 0
mov [eax], eax ; [0] = 0 – Access Violation!
С точки зрения приложения, результат выполнения второй инструкции состоит в том, что состояние регистров процессора (CONTEXT) и информация об исключении (EXCEPTION_RECORD) сохраняется в стеке, а управление передается на код диспетчера прерываний в NTDLL. Этакая «мега» инструкция. Конечно, вся эта работа выполняется ядром OS, но проблема состоит в том, что эта «мега» инструкция - неатомарная. Поток может быть прерван в процессе обработки исключения в ядре. С точки зрения пользовательского кода – прямо в середине инструкции. Эффект от SetThreadContext в этот момент не предсказуем. Поток либо переключится на контекст, переданный в SetThreadContext, либо диспетчер исключений переключит поток на код диспетчера прерываний в NTDLL. Все зависит от того, кто установит пользовательский контекст последним.
Проблема третья: Wow64. Если совсем коротко, то точка входа в 64-х битный код сидит в ring 3. По аналогии с точками входа в ядро, этот код сохраняет 32-х битное состояние процессора. В отличие точек входа в ядро, код в Wow64 абсолютно не защищен от прерывания с помощью kernel APC. В результате в некоторых редких ситуациях GetThreadContext может просто возвращать неправильный 32-х битный контекст (хотя Wow64 очень старается, чтобы обойти такие ситуации).
Вот такая вот печальная история…
PS. А теперь давайте посчитаем количество комментариев «Windows – must die». :-)