Архитектурный взломКрис Касперски Хакер, номер #112, стр. 112-060-1 MSR-регистры на службе хакераПопулярные дебагеры используют базовые отладочные возможности и игнорируют тот факт, что процессоры обросли комплексом отладочных механизмов термоядерного типа, управляемых посредством специальных MSR-регистров. С их помощью мы можем перехватывать системные вызовы, трассировать ветвления в реальном времени и много чего другого, полезного любому хакеру! ЭпиграфБывает, что человек пытается решить какую-то проблему, хотя она решена уже тысячи лет назад. А он просто об этом не знает. Или не понимает, что это именно его проблема. Виктор Пелевин, «Желтая Стрела» Количество регистров Pentium-процессоров вплотную приближается к тысяче (значительная часть из них носит сугубо служебный характер, но сути дела это не меняет). Часть регистров стандартизирована и реализована во всех моделях (например, EAX, CR3, DR1), часть – специфична и их совместимость с остальными процессорами не гарантирована, хотя и те и другие продаются под торговой маркой Pentium. О специфичных регистрах мы и поговорим. В технической документации они скрываются за аббревиатурой MSR – Model Specific Register(s), и, если мне не изменяет память, впервые такие регистры появились в P6, более известном, как Pentium Pro, и его «бюджетном» собрате Pentium-II. Революции не свершилось, но команда RDTSC с тех времен прочно вошла в лексикон всех разработчиков защит. Специальный MSR-регистр каждый такт увеличивает свое значение на единицу, а RDTSC позволяет прикладным программам считывать его содержимое. Как результат — при пошаговом прогоне защищенного кода под отладчиком количество «тиков» процессора между двумя соседними замерами резко возрастало, и защита палила хакера только так. Разработчики Xenon-процессоров (используемых главным образом в серверах и высокопроизводительных рабочих станциях) совершили огромный бросок вперед, сотворив новые отладочные механизмы, глядя на которые обладатели Pentium-II/III только облизывались. Но с появлением Pentium-4 эти возможности, хлынув в бюджетную сферу, стали доступными всем и каждому, к чему создатели отладчиков ни морально, ни физически, ни технически оказались не готовы. К счастью, большинство дебагеров поддерживают подключаемые модули, позволяя нам дописать весь необходимый функционал самостоятельно. Но использование MSR-регистров не ограничивается одной лишь отладкой и, как мы увидим по ходу статьи, они интересы и создателям rootkit'ов, и разработчикам самих защит. Чтение и запись MSR-регистровMSR-регистры имеют свои собственные имена (например, IA32_TIME_STAMP_COUNTER), однако мне не известен ни один ассемблер, «переваривающий» их. Потому приходится использовать «безликие» номера, перечисленные в приложении «MODEL-SPECIFIC REGISTERS» руководства Intel «IA-32 Architecture Software Developer's Manual Volume 3: System Programming Guide», откуда, в частности, можно узнать, что MSR-регистр IA32_TIME_STAMP_COUNTER имеет номер 10h. Чтение MSR-регистров осуществляется машинной командой RDMSR, с предварительной загрузкой номера нужного нам MSR-регистра в ECX. После выполнения команды регистровая пара EDX:EAX содержит исходный результат. В EDX помещаются старшие 32-бита значения MSR, а в EAX, соответственно, идут младшие. За запись отвечает команда WRMSR, ожидающая номер MSR-регистра в ECX, а записываемое значение – в регистровой паре EDX:EAX. Пример использования обоих команд показан ниже: Демонстрация чтения и записи MSR-регистров MOV ECX, 10h ; // IA32_TIME_STAMP_COUNTER RDMRD ; // MOV (EDX:EAX), IA32_TIME_STAMP_COUNTER INC EAX WMSR ; MOV IA32_TIME_STAMP_COUNTER, (EDX:EAX) В защищенном режиме обе команды могут вызываться только из нулевого кольца, иначе операционная система сгенерирует исключение. Другими словами, для экспериментов с MSR-регистрами нам необходимо написать драйвер, «скелет» которого представлен ниже: Исходный текст «скелета» простейшего драйвера MSR-base.asm для экспериментов с MSR-регистрами .686 .MMX .model flat, stdcall extern DbgPrint:PROC .code DriverEntry proc NOP NOP INT 03 ; // soft-ice: I3HERE DRV MOV ECX, 10h ; // IA32_TIME_STAMP_COUNTER RDMSR ; // MOV (EDX:EAX), IA32_TIME_STAMP_COUNTER MOV EAX, 666h ; LOW 32 bits WRMSR ; // MOV (EDX:EAX), IA32_TIME_STAMP_COUNTER RDMSR ; // READ AGAIN - just for fun :-) push offset mystring CALL DbgPrint pop eax mov eax, 0C0000182h ; STATUS_DEVICE_CONFIGURATION_ERROR ; RET ; Four-F says RETN 8 ; <- haron says DriverEntry endp .data mystring DB "*] MSR [*",0Dh,0Ah,0 end DriverEntry Для сборки драйвера нам понадобится NTDDK и командный файл следующего содержания (для его настройки необходимо прописать путь к библиотекам ядра): Командный файл для сборки драйвера @ECHO OFF SET FILE_NAME=MSR-base REM каждый настраивает эту строку под себя! SET ntoskrnl=D:\NTDDK\libchk\i386\ntoskrnl.lib IF EXIST %FILE_NAME%.obj DEL %FILE_NAME%.obj ml /nologo /c /coff %FILE_NAME%.asm IF NOT EXIST %FILE_NAME%.obj GOTO err link /nologo /driver /base:0x10000 /align:32 /out:%FILE_NAME%.sys /subsystem:native %FILE_NAME%.obj %ntoskrnl% GOTO end :err ECHO -ERR! :end Остается самая малость – загрузить драйвер внутрь системы, передав управление процедуре DriverEntry, которая, выполнив, что задумано, завершится с кодом STATUS_DEVICE_CONFIGURATION_ERROR, автоматически выгружая драйвер из системы. Существует множество загрузчиков драйверов (на худой конец можно написать и свой, благо в MSDN входит пример с ZwLoadDriver вместе со строгой рекомендацией не использовать эту функцию в серьезных проектах). Перебрав кучу загрузчиков, я остановился на KmdManager'е, написанным легендарным хакером Four-F, известным своим циклом статей на WASM'е, откуда можно скачать сам загрузчик вместе с исходными текстами (wasm.ru/pub/21/files/kmd3.zip также есть на DVD). Но прежде, чем загружать драйвер в систему, необходимо установить soft-ice (или syser – это кому что больше нравится) и сказать ему «I3HERE DRV», заставив его отлавливать INT 03h в драйверах. Зачем? А затем, что наблюдать за командами RDMSR/WRMSR удобнее всего из отладчика, а soft-ice – это классика жанра. ОК, soft-ice подготовлен к работе, запускаем KmdManager.exe, указываем путь к драйверу, взводим галочку, расположенную между [Register] и [Run] и нажимаем на [Reg'n'Run]. Soft-ice тут же появляется на экране в точке, где мы заботливо воткнули в драйвер команду INT 03h. Теперь мы можем трассировать драйвер сколько душе угодно, а когда надоест — нажать <CTRL-D> или «x;<ENTER>» для выхода. KmdManager, конечно, обругает нас матом, что драйвер загрузить не удалось, но все идет по плану. Именно так и было задумано. Зачем загружать драйвер, если все, что нам нужно – выполнить несколько команд на уровне нулевого кольца? Перехватываем системные вызовы Для перехода с прикладного уровня в режим ядра операционные системы NT 3.х/4.х и W2K использовали прерывание INT 2Eh, которое в изобилие водится в динамической библиотеке NTDLL.DLL, служащей своеобразными вратами в «ад», тьфу, мостом между прикладными и ядерными уровнями. Реализация вызова функции ZwCreateFile на W2K 77F88278 public ZwCreateFile 77F88278 ZwCreateFile proc near ; CODE XREF:LdrLoadAlternateResourceModule+29Fvp 77F88278 77F88278 arg_0 = dword ptr 4 77F88278 77F88278 mov eax, 20h ; NtCreateFile 77F8827D lea edx, [esp+arg_0] 77F88281 int 2Eh ; <-- врата в ад 77F88283 retn 2Ch 77F88283 ZwCreateFile endp Основной недостаток INT 2Eh в том, что выполняется она очень медленно, и потому в P6-процессорах появилась пара более быстрых команд: SYSENTER (с прикладного уровня в режим ядра) и SYSEXIT (из режима ядра назад на прикладной уровень), которую Microsoft стала использовать взамен INT 2Eh, начиная с XP и Server 2003. В результате, NTDLL.DLL оказалась чуть ли не полностью переписанной. В частности, в ней появилась пара процедур быстрого вызова ядерных функций и выхода из них обратно на прикладной уровень: Процедуры NTDLL.DLL быстрого вызова ядерных функций с прикладного уровня ntdll!KiFastSystemCall: 7C82ED50 8BD4 MOV EDX,ESP 7C82ED52 0F34 SYSENTER ntdll!KiFastSystemCallRet: 7C82ED54 C3 RET Прерывание INT 2Eh, вопреки распространенному заблуждению, не было предано анафеме и вместо морга перекочевало в процедуру KiIntSystemCall: Процедура вызова ядерных функций через INT 2Eh, оставленная в XP/Server 2003 для совместимости ntdll!KiIntSystemCall: 7C82ED60 8D542408 LEA EDX,[ESP+0X8] 7C82ED64 CD2E INT 2Eh 7C82ED66 C3 RET Однако дизассемблирование показывает, что ни та, ни другая процедуры реально не используются. NTDLL.DLL исповедует совершенно другой механизм диспетчеризации системных вызовов. Код той же функции ZwCreateFile в Server 2003 выглядит так: Реализация вызова функции ZwCreateFile на Server 2003 77F42467 public ZwCreateFile 77F42467 ZwCreateFile proc near ; CODE XREF: LdrLoadAlternateResourceModule-10Cvp 77F42467 mov eax, 27h ; NtCreateFile 77F4246C mov edx, 7FFE0300h ; offset SharedUserData!SystemCallStub 77F42471 call edx 77F42473 retn 2Ch 77F42473 ZwtCreateFile endp Что находится по адресу 7FFE0300h? Призвав на помощь soft-ice («U 7FFE0300h») мы видим, что здесь расположена машинная инструкция SYSENTER – это и требовалось доказать! Теперь немного о сути самой диспетчеризации. В таблице дескрипторов прерываний (IDT) для прерывания INT 2Eh имеется запись, указывающая, по какому адресу и селектору процессор должен передавать управление при ее выполнении. Естественно, на уровне ядра IDT легко модифицировать, установив перехватчик системных вызовов (многие rootkit'ы именно так и поступают), однако это слишком заметно. Целостность IDT проверяют многочисленные антивирусы и прочие защитные механизмы. Операционные системы семейства XP/Виста и Server 2003/2008 предоставляют в этом плане намного больше возможностей. Во-первых, мы можем изменить содержимое SharedUserData, заменив SYSENTER переходником на собственный обработчик. И хотя этот регион формально недоступен для записи с прикладного уровня, он находится в пользовательской области памяти, которая в отличие от ядра все-таки может быть модифицирована с прикладного уровня, пускай и не без извращений. Задумаемся, – а куда SYSENTER передает управление? Ведь целевой адрес нигде явным образом не указан. Курим мануал от Intel и выясняем, что SYSENTER принимает три скрытых аргумента, передаваемые через MSR-регистры. SYSENTER_CS_MSR (174h): CS регистр для перехода на уровень нулевого кольца; SYSENTER_ESP_MSR (175h): ESP регистр для перехода на уровень нулевого кольца; SYSENTER_EIP_MSR (176h): EIP регистр для перехода на уровень нулевого кольца; Селектор стека (регистр SS) получается путем сложения константы 08h со значением MSR-регистра SYSENTER_CS_MSR, так что SYENTER позволяет задавать не только CS:EIP, но и SS:ESP. Таким образом, мы имеем в своем распоряжении все четыре необходимых ингредиента для перехвата. На практике достаточно подменить целевой EIP, перенаправив его на код нашего хакерского обработчика (естественно, расположенного внутри ядра, то есть представляющего собой драйвер; теоретически, можно заставить SYSENTER вызывать обработчик, находящийся на прикладном уровне, но на этом пути слишком много подводных камней, чтобы принимать его всерьез). Весь фокус в том, что за MSR-регистрами еще мало кто следит и подобный rootkit имеет хорошие шансы остаться долгое время незамеченным (или даже не быть замеченным вообще). Ниже приведен фрагмент кода, считывающего MSR-регистры, связанные с командой SYSENTER. На NT 3.х/4.х и W2K мы получим нули, а XP/Виста/Server 2003/2008 покажут адрес диспетчера системных вызовов, который можно хакнуть через WRMSR: Фрагмент исходного текста драйвера MSR-SYSENTER.asm, определяющего адрес диспетчера системных вызовов под XP/Висла/Server 200x INT 03 ; // for soft-ice MOV ECX, 174H ; // SYSENTER_CS_MSR RDMSR ; // MOV (EDX:EAX), SYSENTER_CS_MSR MOV ECX, 175H ; // SYSENTER_ESP_MSR RDMSR ; // MOV (EDX:EAX), SYSENTER_ESP_MSR MOV ECX, 176H ; // SYSENTER_EIP_MSR RDMSR ; // MOV (EDX:EAX), SYSENTER_EIP_MSR Загружаем драйвер привычным способом (soft-ice должен быть предварительно запущен) и начинаем трассировать программу, наблюдая за значениями, возвращаемыми в регистровых парах EDX:EAX. Халтурный хронометр или хронометраж наоборотНачиная с P6, в Pentium-процессорах появился специальный счетчик производительности, увеличивающий значение MRS-регистра IA32_TIME_STAMP_COUNTER (10h) на единицу каждый такт. На самом деле, документация от Intel гарантирует просто увеличение, оставляя за собой простор для маневров, но это уже дебри технических деталей, в которые лучше не вдаваться. По умолчанию, прикладным программам даровано право читать содержимое счетчика командой RDTSC, возвращающей текущее значение в регистровой паре EDX:EAX. Причем, отладчики типа soft-ice не «замораживают» счетчик на время своей работы, что позволяет защитным механизмам легко обнаруживать факт трассировки или срабатывания точек останова (обработка которых требует значительного количества процессорного времени). В эпоху IBM XT/AT совместимых компьютеров для той же цели использовался системный таймер (грубо говоря, часы), и вот его-то soft-ice как раз и «замораживал», не позволяя защитам обнаружить себя. Эх, а сейчас что? Начнем с того, что команду RDTSC очень легко сделать привилегированной. Достаточно взвести TSD флаг (2й бит) в регистре CR4 (доступном только с нулевого кольца), после чего всякая попытка вызова RDTSC с прикладного уровня заставит процессор генерировать общее исключение защиты, перехватываемое отладчиком. Перед возвращением управления программе мы можем записать в регистровую пару EDX:EAX все, что нам захочется, а конкретно – создать видимость, что выполнение данного участка кода заняло ничуть не больше времени, чем обычно. Фрагмент кода, который взводит TSD бит, приведен ниже: Делаем команду RDTSC привилегированной инструкцией MOV EAX, CR4 ; // читаем управляющий регистр CR4 OR EAX, 4 ; // взводим TSD флаг MOV CR4, EAX ; // обновляем CR4 А как быть, если защита реализована на уровне драйвера и наш запрет на чтение IA32_TIME_STAMP_COUNTER ей не помеха? Тогда можно прибегнуть к прямой записи MSR-регистра IA32_TIME_STAMP_COUNTER командой WRMSR. Увы, здесь есть одно «но». Процессор (вот паразит!) записывает только младшие 32-бита MSR-регистра IA32_TIME_STAMP_COUNTER, а остальные сбрасывает в ноль, и защита может разоблачить наши махинации. Правда, все известные мне защиты с этим не заморачиваются, а просто сравнивают результаты двух замеров и, если в обоих старшие 32-бита обнулены, то все ОК. Или не ОК? Прикинем, насколько нам хватит младших 32-бит. Возьмем процессор с тактовой частотой в 1 ГГц, увеличивающий IA32_TIME_STAMP_COUNTER на единицу каждый такт, тогда младшие 32-бита переполнятся за 4,294967296 сек. Срок не такой уж и большой. Впрочем, учитывая многозадачную природу Windows, защиты осуществляют замеры только на коротких «трассах», выполняющихся сотые или даже тысячные доли секунды, то есть меньше одного кванта (времени, отпущенного процессу системным планировщиком, после которого происходит переключение на другой поток). В XP длительность кванта составляет ~100 мс, поэтому, результаты замера в ~50 мс уже нельзя считать достоверными – поток мог быть прерван планировщиком. Короче говоря, для борьбы с защитами младших 32-бит вполне хватит. Кстати, интересный факт. При записи IA32_TIME_STAMP_COUNTER под VMWare, старшие 32-бита MSR-регистра не обнуляются, а в младшие записывается немного большая величина, чем замышлялось (издержки эмуляции). Следовательно, анти-антиотладочный механизм должен работать так: 1. Записать что-то в IA32_TIME_STAMP_COUNTER; 2. прочитать IA32_TIME_STAMP_COUNTER; 3. если старшие 32-бита равны нулю, мы на живом процессоре и:
4. если старшие 32-бита не равны нулю, мы под VMWare и:
Ломая программы под VMWare и корректируя значение IA32_TIME_STAMP_COUNTER должным образом, мы надежно скроем присутствие отладчика от всех защит, основанных на замере временных интервалов. Трассируем ветвленияПопробуй взвести бит TF регистра флагов (E)FLAGS (он там 8-й по счету, начиная от нуля) и процессор начнет генерировать отладочное прерывание INT 01h после выполнения каждой инструкции (за исключением инструкций, записывающих сегментный регистр SS). Это еще со времен IBM XT всем хакерам хорошо известно. Недостаток такого подхода, прежде всего, в его чрезвычайной медлительности. Даже с учетом быстродействия современных процессоров, обработка исключений обходится очень дорого. «Серьезную» программу мы будет трассировать до конца сезона. А смысл? К трассировке обычно прибегают для реконструкции логики работы машинного кода, мысленно разбивая его на структурные блоки как-то: ветвления, циклы, etc. В частности, при написании универсальных распаковщиков, автоматически определяющих оригинальную точку входа в программу, нам совершенно незачем исполнять все команды. Вполне достаточно после каждого прыжка сравнить целевой код с набором сигнатур стартового кода, внедряемых компиляторами в начало программы. Вот если бы мы могли заставить процессор генерировать отладочные прерывания только после ветвлений – скорость распаковки возросла бы в сотни раз. Сравнивания два прогона защищенной программы до и после завершения испытательного срока, мы легко найдем тот самый условный переход, после которого все пошло по пути «trial expired». Естественно, полный лог трассировки нам ни к чему. Нас интересуют только ветвления. Только как их получить? Оказывается, процессор позволяет сделать это! Если бит BTF (1-й, считая от нуля) MSR-регистра MSR_DEBUGCTLA (1D9h) взведен, то процессор будет интерпретировать стандартный TF флаг как указание генерировать отладочное прерывание только после встречи с ветвлением или исключением. При этом оба флага (BTF и TF) автоматически очищаются. Проведем следующий эксперимент. Уберем из «скелета» нашего драйвера INT 03h и поместим туда следующий код, взводящий флаг BTF. Фрагмент драйвера MSR-branch-trace.asm, взводящего BTF флаг MOV ECX, 1D9H ; // MSR_DEBUGCTLA RDMSR ; // MOV (EDX:EAX), MSR_DEBUGCTLA OR EAX, 2 ; // SET BTF (single-step on branches) flag (bit 1) WRMSR ; // MOV (EDX:EAX), MSR_DEBUGCTLA Загрузим драйвер в систему и, запустив любой отладчик (например, OllyDbg), нажмем <F7> для трассировки одной инструкции. Упс! Отладчик заносит нас черт знает куда, останавливаясь на первом ветвлении (которым, чаще всего, будет инструкция CALL, вызывающая некоторую API-функцию из стартового кода). Флаг BTF при этом сбрасывается и дальше трассировка проходит нормально (шаг за шагом) – во всяком случае, пока мы вновь не загрузим наш драйвер, устанавливающий BTF-флаг или не напишем plug-in для OllyDbg. К статье прилагается исходный код и откомпилированный модуль MSR-branch-trace.sys, устанавливающий BTF-флаг и тут же проверяющий его значение, выводя результат проверки в отладочный поток. Содержимое потока можно просмотреть как с помощью soft-ice, так и утилитой DbgView от Марка Руссиновича. Если взвести BTF-флаг не удается, это означает, что на данной платформе он не поддерживается (на P-III Coppermine поддерживается, на P-4 тоже, а вот под VM Ware 4.5 и 5.2 – увы и ах!). Проверить значение BTF-бита поможет другой драйвер (MSR-branch-trace-x.sys), также прилагаемый к статье. НапоследокНа этом наше краткое знакомство с MSR-регистрами не заканчивается. Заложенный в них потенциал только начинает раскрываться. Как поется в некогда популярной песне: «то ли еще будет, ой-ой-ой». Сейчас я работаю над созданием трассера реального времени, который на самом деле никаким трассером не является. Он представляет собой своеобразный «дампер» истории выполнения команд, сохраняемой процессором как в MSR-регистрах, так и в специально отведенной области памяти. Мы не только получаем колоссальный выигрыш в производительности, но и полностью скрываем свое присутствие от защитных механизмов... Но разговор об этом еще предстоит. Такая обширная тема требует отдельной статьи, в которой MSR-регистры лишь малая часть огромного отладочного механизма, спрятанного в недрах процессора. Что нам нужно?Для экспериментов, описанных в статье, нам понадобятся следующие вещи, программы и инструменты:
Чтение/запись MSR с прикладного уровняДля чтения/записи MSR с прикладного уровня можно воспользоваться недокументированной native-API функцией NtSystemDebugControl(), экспортируемой из динамической библиотеки NTDLL.DLL. Готовый к употреблению пример работы с ней лежит на http://www.openrce.org/blog/view/535/Branch_Tracing_with_Intel_MSR_Registers. Однако для этого необходимо:
|