В одном из заданий очного тура хакерского соревнования NeoQUEST-2013 участникам квеста противостоял грозный противник — гипервизор! Для побега из тюрьмы им нужно было всего лишь считать строчку из памяти процесса. Но было несколько НО… Гипервизор всячески усложнял нашим конкурсантам жизнь, а именно:
• не давал просто так читать память процесса, контролируя адресное пространство
• выполнял роль антиотладчика
• проверял целостность образа процесса, вычисляя хэш его секции
• осуществлял привязку программы к компьютеру, и т.д.
Участникам пришлось как следует поломать головы, чтобы найти нетривиальное решение обхода гипервизора! Представим себя на их месте и сразимся с гипервизором, шаг за шагом, набивая «шишки», то и дело возвращаясь на шаг назад, и, в конце концов, добьемся победы и получим ключ! Под катом — детальное рассмотрение того, как гипервизор контролировал доступ к ключу, и какие способы его обхода пытались применить участники.
Участники очного тура NeoQUEST-2013 — победители февральского онлайн-тура NeoQUEST — имели мощный стимул для победы. Им нужно было выбраться из холодных и сырых стен тюрьмы, чтобы избежать казни на вполне себе правдоподобном электрическом стуле! Тюрьма, конечно, была виртуальная, но зато стул – настоящий!
На «побег» конкурсантам было дано 8 часов, и, рассчитывая, что на прохождение каждого задания потребуется около 2 часов, мы подготовили 5 заданий (одно — запасное!), но вышло, как мы и думали — одно задание не прошел никто, так что четырех заданий оказалось достаточно).
Задания NeoQUEST-2013 касались безопасности как распространенных областей ИБ – криптографии, облачных и web технологий, бот-нетов, и многого другого, так и безопасности нестандартных устройств и технологий (гипервизоров, смарт-карт и т.д.), что требовало даже программирования «голого» железа, в частности, контроллера Arduino SDK. Немного о каждом из заданий хорошо написал победитель квеста — AVictor, как раз недавно вернувшийся из Амстердама с конференции RSA, поездка на которую была главным призом очного тура NeoQUEST-2013! Второе место занял v0s (Влад Росков), получивший в награду защищенный смартфон Cat B15 от Caterpillar.
С обходом гипервизора справился только v0s. Формулировалось задание следующим образом: доступны два идентичных компьютера с ОС Windows, на которых присутствует исполняемый файл KeyReader.exe. На вход он получает id участника, а затем выводит «Key successfully readed. Press any key for exit.» и висит, завершая свою работу при нажатии клавиши. По умолчанию участники работают под учетной записью с правами администратора. Требуется получить ключ.
При запуске на любых других машинах, кроме этих двух, KeyReader.exe сообщает об ошибке:
Представим себя на месте участников и попробуем докопаться до истины! Судя по сообщению, ключ находится в памяти процесса, и нам нужно его оттуда прочитать. Звучит несложно, попробуем это сделать. Запустим Windows Task Manager, найдем процесс KeyReader.exe, сделаем его дамп в момент ожидания нажатия клавиши, и скормим дамп утилите strings Руссиновича. Дамп весит 3.3Мб, и утилита находит 55000 строк. Не очень очевидно, как искать среди них ключ, но если поискать по слову key, то обнаружится следующая строка:
Похоже, это не то, что нужно. Пока неясно, что именно произошло, и почему дамп содержит эту строку, но это явно не пароль. Попробуем разобраться, как работает KeyReader.exe c помощью IDA, и уже с большим пониманием происходящего выполнить задание. Программа легко декомпилируется, и в общих чертах становится понятен ее смысл. Основная логика представлена ниже:
Выделяется страница памяти, выровненная по странице, обнуляется, затем читается Id участника и вызывается функция AcquireKey. Функция AcquireKey небольшая, и в ней присутствует следующий код, который раньше был ассемблерной вставкой:
В коде присутствует инструкция vmcall, которая выполняет обращение к гипервизору с параметрами, передающимися через регистры. Регистр EAX содержит значение 'NeoQ', регистр EDX – значение 'strt', регистр EBX – указатель на выделенную ранее страницу памяти, регистр ECX – размер страницы, регистр EDI – id участника. Результат вызова возвращается через регистр ESI. Для простоты можно считать, что инструкция vmcall передает управление гипервизору, если гипервизор представлен или вызывает #UD, если такового не имеется.
Полное описание алгоритма работы инструкции можно найти в документе 64-ia-32-architectures-software-developer-vol-2b-manual от Intel. Теперь ясно, что ключ хранится в гипервизоре и копируется в память программы при обращении к нему посредством инструкции vmcall с определенными параметрами.
Попробуем отладить по шагам работу программы и посмотреть, что будет находиться в памяти после обращения к гипервизору. В качестве отладчика будем использовать OllyDbg 2.01. Запускаем KeyReader.exe под отладкой, ставим breakpoint на следующую инструкцию после vmcall. Вводим id участника, нажимаем Enter, и попадаем в установленный breakpoint. Регистр EBX содержит адрес 0x1F5000, но по нему располагаются нули. В ESI тоже 0.
Нажимаем F9 и видим, что произошла ошибка.
Существует три механизма отладки:
• инструментария кода инструкциями int3
• пошаговая отладка с прерыванием int1 после каждой инструкции
• отладочные регистры D0-D7, которые позволяют отлавливать обращения к памяти
В нашем случае, при установке breakpoint на инструкцию, мы перезаписали первый байт инструкции на 0xcc (int 0x3). Когда программа дойдет до этого места, выполнится int3, управление перейдет отладчику, он восстановит перетертый байт, выполнит восстановленную инструкцию, и снова запишет int3 поверх инструкции. Таким образом, breakpoint-ы изменяют выполняемый образ, а гипервизор, судя по всему, проверяет его целостность перед копированием ключа в память программы. Также на это намекает сообщение об ошибке.
Данную проблему легко решить следующим путем. Установим breakpoint за несколько инструкций до vmcall, после попадания в breakpoint снимем его и будем выполнять код пошагово. Выполнив Vmcall, получим:
В регистре EBX адрес 0x165000, по которому располагается уже знакомая строка «key:NICE TRY, THIS IS NOT A KEY». Гипервизор по-прежнему не дает прочитать ключ.
Можно написать небольшой драйвер, который бы использовал функции KeStackAttachProcess и KeUnstackDetachProcess для чтения памяти процесса, но сразу скажем, что это бы тоже не сработало. Можно пойти тернистым путем и заставить Windows выгрузить память интересующего нас процесса на диск, чтобы затем разобрать pagefile.sys и найти страницу, соответствующую буферу с ключом. Но на этой странице мы бы увидели все ту же строчку.
Попробуем следующий способ – заинжектим нашу dll в процесс KeyReader, и из нее прочитаем память, в которой находится ключ. Для начала запустим процесс, приаттачимся к нему с помощью Olly Dbg и посмотрим адрес, по которому расположен буфер с ключом. Указатель на буфер находится по адресу 0x40eb3c.
В нашем случае память под буфер была выделена по адресу 0x600000, деаттачим дебаггер, приложение продолжает работать. Напишем dll, которая будет читать память по этому адресу и сохранять ее на диск в файл a.txt. Код будет выглядеть следующим образом:
В качестве инжектора мы использовали пример из книги Windows via C/C++. Инжектим dll и получаем на выходе файл a.txt, содержащий следующую строчку:
Это и есть ключ.
Любая вариация на тему инжекта кода не в образ процесса KeyReader была бы решением. К примеру, один из участников в дебаггере исправил код функции ReadConsoleInputA в kernel32.dll, которая вызывается функцией getch, и из нее прочитал строчку по известному адресу. Как можно увидеть, задание не самое сложное, но с ним в итоге справился только один человек. Скорее всего, это было связано с недостатком времени, да и само задание таково, что почти не предполагает логической цепочки, которая приводила бы к ответу. Участникам приходилось перебирать все известные им способы прочитать память чужого процесса, чтобы в какой-то момент наткнуться на работающий вариант.
Вообще, подготавливать это задание было даже, наверное, интереснее, чем проходить его: ). Для контроля доступа к странице нужно было контролировать изменения во всех связанных с ней таблицах трансляции из виртуального адреса в физический. Учитывая разнообразие вариантов мапинга и то, что адрес физической страницы, к которой мы контролируем доступ, может меняться, это не так уж тривиально.
По какому же принципу гипервизор в одном случае давал доступ к ключу, а в другом нет? При обращении к странице с ключом он проверял 3 условия:
• Чтение должно быть из пользовательского режима
• Хэш той секции .text образа процесса, из которого происходит обращение к ключу, должен быть строго определенным
• Чтение ключа возможно только из того адресного пространства, в котором произошел вызов vmcall для получения ключа.
Первое условие отбрасывало все попытки прочитать ключ из ядра. К ним относится функция ReadProcessMemory и написание собственного драйвера. Второе условие не давало менять код образа и ставить break point’ы. Кстати, проверялась только секция text, так что можно было добавить собственную секцию с кодом, которая называлась бы по-другому и изменить точку входа на нее. Затем вызвать из своего кода функцию получения ключа и сохранить его на диск. Третье условие не давало другим процессам, в которые замаплена память процесса KeyReader, прочитать ключ. Это условие несколько надуманно, так как для получения такой ситуации придется писать собственный драйвер и свое приложение, которое бы с ним общалось, так как готовых утилит с такой функциональностью по Windows7 мы не нашли. Под Windows XP похожим образом работал WinHex RAM Editor, когда читал физическую память. Основной целью условий стояла невозможность выполнить задание, используя только готовые утилиты, и не написав ни строчки кода.
Готовя задание с гипервизором, мы хотели заставить участников удивиться необычности задачи. Ведь, казалось бы, какие могут быть проблемы с чтением строки из памяти процесса? И, судя по тому, что задание выполнил только один участник, нам это удалось! Надеемся, что и читателям Хабра было интересно! В скором времени ожидайте статьи с подробным разбором наиболее «железного» задания NeoQUEST-2013!
• не давал просто так читать память процесса, контролируя адресное пространство
• выполнял роль антиотладчика
• проверял целостность образа процесса, вычисляя хэш его секции
• осуществлял привязку программы к компьютеру, и т.д.
Участникам пришлось как следует поломать головы, чтобы найти нетривиальное решение обхода гипервизора! Представим себя на их месте и сразимся с гипервизором, шаг за шагом, набивая «шишки», то и дело возвращаясь на шаг назад, и, в конце концов, добьемся победы и получим ключ! Под катом — детальное рассмотрение того, как гипервизор контролировал доступ к ключу, и какие способы его обхода пытались применить участники.
Как все начиналось
Участники очного тура NeoQUEST-2013 — победители февральского онлайн-тура NeoQUEST — имели мощный стимул для победы. Им нужно было выбраться из холодных и сырых стен тюрьмы, чтобы избежать казни на вполне себе правдоподобном электрическом стуле! Тюрьма, конечно, была виртуальная, но зато стул – настоящий!
На «побег» конкурсантам было дано 8 часов, и, рассчитывая, что на прохождение каждого задания потребуется около 2 часов, мы подготовили 5 заданий (одно — запасное!), но вышло, как мы и думали — одно задание не прошел никто, так что четырех заданий оказалось достаточно).
Задания NeoQUEST-2013 касались безопасности как распространенных областей ИБ – криптографии, облачных и web технологий, бот-нетов, и многого другого, так и безопасности нестандартных устройств и технологий (гипервизоров, смарт-карт и т.д.), что требовало даже программирования «голого» железа, в частности, контроллера Arduino SDK. Немного о каждом из заданий хорошо написал победитель квеста — AVictor, как раз недавно вернувшийся из Амстердама с конференции RSA, поездка на которую была главным призом очного тура NeoQUEST-2013! Второе место занял v0s (Влад Росков), получивший в награду защищенный смартфон Cat B15 от Caterpillar.
А теперь — о гипервизоре!
С обходом гипервизора справился только v0s. Формулировалось задание следующим образом: доступны два идентичных компьютера с ОС Windows, на которых присутствует исполняемый файл KeyReader.exe. На вход он получает id участника, а затем выводит «Key successfully readed. Press any key for exit.» и висит, завершая свою работу при нажатии клавиши. По умолчанию участники работают под учетной записью с правами администратора. Требуется получить ключ.
При запуске на любых других машинах, кроме этих двух, KeyReader.exe сообщает об ошибке:
Представим себя на месте участников и попробуем докопаться до истины! Судя по сообщению, ключ находится в памяти процесса, и нам нужно его оттуда прочитать. Звучит несложно, попробуем это сделать. Запустим Windows Task Manager, найдем процесс KeyReader.exe, сделаем его дамп в момент ожидания нажатия клавиши, и скормим дамп утилите strings Руссиновича. Дамп весит 3.3Мб, и утилита находит 55000 строк. Не очень очевидно, как искать среди них ключ, но если поискать по слову key, то обнаружится следующая строка:
Похоже, это не то, что нужно. Пока неясно, что именно произошло, и почему дамп содержит эту строку, но это явно не пароль. Попробуем разобраться, как работает KeyReader.exe c помощью IDA, и уже с большим пониманием происходящего выполнить задание. Программа легко декомпилируется, и в общих чертах становится понятен ее смысл. Основная логика представлена ниже:
Выделяется страница памяти, выровненная по странице, обнуляется, затем читается Id участника и вызывается функция AcquireKey. Функция AcquireKey небольшая, и в ней присутствует следующий код, который раньше был ассемблерной вставкой:
В коде присутствует инструкция vmcall, которая выполняет обращение к гипервизору с параметрами, передающимися через регистры. Регистр EAX содержит значение 'NeoQ', регистр EDX – значение 'strt', регистр EBX – указатель на выделенную ранее страницу памяти, регистр ECX – размер страницы, регистр EDI – id участника. Результат вызова возвращается через регистр ESI. Для простоты можно считать, что инструкция vmcall передает управление гипервизору, если гипервизор представлен или вызывает #UD, если такового не имеется.
Полное описание алгоритма работы инструкции можно найти в документе 64-ia-32-architectures-software-developer-vol-2b-manual от Intel. Теперь ясно, что ключ хранится в гипервизоре и копируется в память программы при обращении к нему посредством инструкции vmcall с определенными параметрами.
Попробуем отладить по шагам работу программы и посмотреть, что будет находиться в памяти после обращения к гипервизору. В качестве отладчика будем использовать OllyDbg 2.01. Запускаем KeyReader.exe под отладкой, ставим breakpoint на следующую инструкцию после vmcall. Вводим id участника, нажимаем Enter, и попадаем в установленный breakpoint. Регистр EBX содержит адрес 0x1F5000, но по нему располагаются нули. В ESI тоже 0.
Нажимаем F9 и видим, что произошла ошибка.
Существует три механизма отладки:
• инструментария кода инструкциями int3
• пошаговая отладка с прерыванием int1 после каждой инструкции
• отладочные регистры D0-D7, которые позволяют отлавливать обращения к памяти
В нашем случае, при установке breakpoint на инструкцию, мы перезаписали первый байт инструкции на 0xcc (int 0x3). Когда программа дойдет до этого места, выполнится int3, управление перейдет отладчику, он восстановит перетертый байт, выполнит восстановленную инструкцию, и снова запишет int3 поверх инструкции. Таким образом, breakpoint-ы изменяют выполняемый образ, а гипервизор, судя по всему, проверяет его целостность перед копированием ключа в память программы. Также на это намекает сообщение об ошибке.
Данную проблему легко решить следующим путем. Установим breakpoint за несколько инструкций до vmcall, после попадания в breakpoint снимем его и будем выполнять код пошагово. Выполнив Vmcall, получим:
В регистре EBX адрес 0x165000, по которому располагается уже знакомая строка «key:NICE TRY, THIS IS NOT A KEY». Гипервизор по-прежнему не дает прочитать ключ.
Можно написать небольшой драйвер, который бы использовал функции KeStackAttachProcess и KeUnstackDetachProcess для чтения памяти процесса, но сразу скажем, что это бы тоже не сработало. Можно пойти тернистым путем и заставить Windows выгрузить память интересующего нас процесса на диск, чтобы затем разобрать pagefile.sys и найти страницу, соответствующую буферу с ключом. Но на этой странице мы бы увидели все ту же строчку.
Попробуем следующий способ – заинжектим нашу dll в процесс KeyReader, и из нее прочитаем память, в которой находится ключ. Для начала запустим процесс, приаттачимся к нему с помощью Olly Dbg и посмотрим адрес, по которому расположен буфер с ключом. Указатель на буфер находится по адресу 0x40eb3c.
В нашем случае память под буфер была выделена по адресу 0x600000, деаттачим дебаггер, приложение продолжает работать. Напишем dll, которая будет читать память по этому адресу и сохранять ее на диск в файл a.txt. Код будет выглядеть следующим образом:
В качестве инжектора мы использовали пример из книги Windows via C/C++. Инжектим dll и получаем на выходе файл a.txt, содержащий следующую строчку:
Это и есть ключ.
Любая вариация на тему инжекта кода не в образ процесса KeyReader была бы решением. К примеру, один из участников в дебаггере исправил код функции ReadConsoleInputA в kernel32.dll, которая вызывается функцией getch, и из нее прочитал строчку по известному адресу. Как можно увидеть, задание не самое сложное, но с ним в итоге справился только один человек. Скорее всего, это было связано с недостатком времени, да и само задание таково, что почти не предполагает логической цепочки, которая приводила бы к ответу. Участникам приходилось перебирать все известные им способы прочитать память чужого процесса, чтобы в какой-то момент наткнуться на работающий вариант.
Вообще, подготавливать это задание было даже, наверное, интереснее, чем проходить его: ). Для контроля доступа к странице нужно было контролировать изменения во всех связанных с ней таблицах трансляции из виртуального адреса в физический. Учитывая разнообразие вариантов мапинга и то, что адрес физической страницы, к которой мы контролируем доступ, может меняться, это не так уж тривиально.
По какому же принципу гипервизор в одном случае давал доступ к ключу, а в другом нет? При обращении к странице с ключом он проверял 3 условия:
• Чтение должно быть из пользовательского режима
• Хэш той секции .text образа процесса, из которого происходит обращение к ключу, должен быть строго определенным
• Чтение ключа возможно только из того адресного пространства, в котором произошел вызов vmcall для получения ключа.
Первое условие отбрасывало все попытки прочитать ключ из ядра. К ним относится функция ReadProcessMemory и написание собственного драйвера. Второе условие не давало менять код образа и ставить break point’ы. Кстати, проверялась только секция text, так что можно было добавить собственную секцию с кодом, которая называлась бы по-другому и изменить точку входа на нее. Затем вызвать из своего кода функцию получения ключа и сохранить его на диск. Третье условие не давало другим процессам, в которые замаплена память процесса KeyReader, прочитать ключ. Это условие несколько надуманно, так как для получения такой ситуации придется писать собственный драйвер и свое приложение, которое бы с ним общалось, так как готовых утилит с такой функциональностью по Windows7 мы не нашли. Под Windows XP похожим образом работал WinHex RAM Editor, когда читал физическую память. Основной целью условий стояла невозможность выполнить задание, используя только готовые утилиты, и не написав ни строчки кода.
И в заключение...
Готовя задание с гипервизором, мы хотели заставить участников удивиться необычности задачи. Ведь, казалось бы, какие могут быть проблемы с чтением строки из памяти процесса? И, судя по тому, что задание выполнил только один участник, нам это удалось! Надеемся, что и читателям Хабра было интересно! В скором времени ожидайте статьи с подробным разбором наиболее «железного» задания NeoQUEST-2013!