Home > itblogs > «Угон» потоков.

«Угон» потоков.

May 4th, 2010

Бывает так, что требуется выполнить свой код в контексте произвольного потока. Либо в своем потоке, но в то время когда поток выполняет чужой код. Например, сборщик мусора может хотеть перехватить управление, даже если поток крутит бесконечный цикл. Один из методов перехвата – использование функций 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». :-)

  1. Vital Zanko
    May 5th, 2010 at 00:49 | #1

    Long live Ubuntu! :)

  2. Михаил Романов
    May 5th, 2010 at 01:27 | #2

    Логичный вопрос – и что можно сделать, если все-таки надо?

  3. May 5th, 2010 at 01:34 | #3

    Интересно, не задумывался об этом.
    Получается, что совсем никак нельзя “попросить” нить выполнить код помимо QueueUserAPC? Конечно, хорошо бы, чтобы поток прервался не “прямо сейчас”, а когда это относительно безопасно – т.е., как позиксовый сигнал.

    От неправильного 32-битного контекста в Wow64 как-нибудь можно защититься, чтобы можно было спокойно контекст читать?

  4. May 5th, 2010 at 07:46 | #4

    Троянописатель врывается в тред: можно перехватить прерывание системного таймера и “дождавшись” нужного потока выполнить возврат из обработчика прерывания в свой код (естественно, сохранив оригинальные значения значения esp, cs:eip и остальных регистров).

  5. Gigabyte
    May 5th, 2010 at 12:25 | #5

    очередной “архитектурный” чирий винды в попытке угодить всем подряд. уже забыли и про очереди и про сериализацию и про то, что через дверь ходить удобнее и правильнее, нежели лезть в окно. R.I.P. – закапывайте.

  6. May 5th, 2010 at 21:22 | #6

    dmitry-vk :
    Получается, что совсем никак нельзя “попросить” нить выполнить код помимо QueueUserAPC? Конечно, хорошо бы, чтобы поток прервался не “прямо сейчас”, а когда это относительно безопасно – т.е., как позиксовый сигнал.

    Попросить можно тысячей и одним способом. Но для этого поток должен или ждать запроса, или периодически проверять не пришел ли запрос. Принудительно прервать произвольный поток в подходящем месте гораздо сложнее.

    Можно например делать SuspendThread и анализировать тип trap frame. В данный момент это работает на amd64 и ia64. Если написать свой драйвер, то и x86 будет тоже работать. Но не Wow64, по крайней мере не Wow64 на Itanium.

    Пожалуй стоит написать пость с разбором разных вариантов.

    dmitry-vk :
    От неправильного 32-битного контекста в Wow64 как-нибудь можно защититься, чтобы можно было спокойно контекст читать?

    На amd64 работает вариант с проверкой типа trap frame. На Itanium – контекст возвращается правильный, но узнать, что вот вот случиться прерывание нельзя.

  7. May 5th, 2010 at 21:25 | #7

    dmitry :
    Троянописатель врывается в тред: можно перехватить прерывание системного таймера и “дождавшись” нужного потока выполнить возврат из обработчика прерывания в свой код (естественно, сохранив оригинальные значения значения esp, cs:eip и остальных регистров).

    Можно и так с оговоркой, что это не работает для Wow64 и требует проверки типа trap frame.

  8. May 5th, 2010 at 21:26 | #8

    @Vital Zanko
    @Gigabyte

    3:2. Нормальные собеседники лидируют с минимальным отрывом.

  9. May 6th, 2010 at 11:33 | #9

    Я не программист. Посему, админский вопрос. Вот, чегото я видимо не пойму. Если поток является единицей планирования, то что мешает запустить свой поток рядом и просто дождаться, когда планировщик передаст управление? Зачем вообще все эти прыжки в сторону?

  10. May 6th, 2010 at 19:26 | #10

    eosfor :
    Я не программист. Посему, админский вопрос. Вот, чегото я видимо не пойму. Если поток является единицей планирования, то что мешает запустить свой поток рядом и просто дождаться, когда планировщик передаст управление? Зачем вообще все эти прыжки в сторону?

    Параллельный поток даст возможность выполнить свой код. Перехват чужого потока позволяет контролировать чужой код. Ну еще и скрытно выполнять свой код, но это в основном троянописателям интересно.

  11. Anonymous
    May 6th, 2010 at 20:55 | #11

    >Зачем вообще все эти прыжки в сторону?

    Это надо для:
    1) асинхронного взаимодействия нитей (т.е., чтобы нить отреагировала на что-то как можно скорее как только появится такая возможность)
    2) корректного кооперативного завершения нитей (т.е., внутри нити возбуждается исключение, которое разворачивает стек и вызывает все обработчики). – Кстати, а как в .NET CLR реализован Thread.Abort: каким образом он кидает исключение в другой нити?
    2) Корректной остановки нити для сборки мусора, т.к. сборщику мусора нельзя показывать частично проинициализированный объект.

  12. Anonymous
    May 7th, 2010 at 00:57 | #12

    @Алексей Пахунов
    Свои 2 копейки что ли подкинуть? ;) Благо, что с переходом на Fedora Linux копеек накопилось :D

  13. May 7th, 2010 at 02:12 | #13

    >Зачем вообще все эти прыжки в сторону?

    Это нужно для асинхронного взаимодействие нитей: нити не надо активно ожидать события. В частности, это бывает важно при реализации виртуальных машин для языков программирования.

Comments are closed.