Привет всем! В этом блоге мы поделимся техническим анализом и эксплуатацией критической уязвимости (CVE-2012-0217), которой подвержен гипервизор Xen.
Привет всем! В этом блоге мы поделимся техническим анализом и эксплуатацией критической уязвимости (CVE-2012-0217), которой подвержен гипервизор Xen. Уязвимость совсем недавно была обнаружена Рафалом Вайджуком (Rafal Wojtczuk) и Яном Бейлихом (Jan Beulich).
Уязвимости подвержены системы, работающие на интеловском аппаратном обеспечении. Благодаря уязвимости, нарушитель, находящийся внутри гостевой операционной системы, может обойти ограничения виртуальной среды и выполнить произвольный код на системе хоста с правами самого привилегированного домена (“dom0”). Обладая правами dom0 можно напрямую управлять аппаратным обеспечением и непривилегированными доменами (“domU”).
Если ваша виртуальная или облачная инфраструктура работает на гипервизоре Xen, то вам настоятельно рекомендуется обновиться до версии 4.1.3, в которой критическая уязвимость устранена.
1. Технический анализ уязвимости
Инструкции SYSCALL/SYSRET позволяют быстро переключить контекст между режимом пользователя и режимом ядра. Согласно спецификации Intel, SYSCALL осуществляет переход по адресу, указанному в регистре MSR_IA32_LSTAR. В Xen способ обработки системных вызовов зависит от того, является ли гостевая машина полностью виртуализируемой (Hardware Virtual Machine - HVM) или только паравиртуализироваемой (ParaVirtualized Machine- PVM):
Если мы имеем дело с HVM, то у гостевой системы есть собственная таблица прерываний IDT и MSR регистры, поэтому гостевая машина не нуждается в помощи гипервизора при обработке прерываний и системных вызовов.
С PVM дело обстоит немного по-другому, поскольку гостевая система знает, что ею управляет гипервизор. Чтобы паравиртуализируемая машина функционировала нормально, необходимо модифицировать ядро так, чтобы оно работало не в ring0, а в ring1.
Ядро обращается к ключевым структурам (GDT, IDT, и.т.п.) в Xen посредством гипервызовов. Гипервызовы идентичны системным вызовам, но производятся они не из пользовательского режима в режим ядра, а из режима ядра в режим гипервизора.
Системный вызов из пользовательского процесса напрямую обращается в ring0 к диспетчеру гипервизора, а диспетчер уже затем решает, из какого режима (пользовательского или режима ядра) производился вызов. Код процедуры выбора находится в файле “xen/x86/x86-64/entry.S”:
|
Если вызов исходил из пользовательского процесса, то Xen передает вызов в ядро, и затем для обработки вызова используется таблица IDT ядра. Если же системный вызов производился из режима ядра (гипервызов), то Xen использует свою таблицу IDT, чтобы обработать системный вызов. Снова код из “xen/x86/x86-64/entry.S”:
ENTRY(syscall_enter)
|
При возврате из гипервызова в режиме ядра выполняется следующий код:
|
В мануалах Intel утверждается, что существует особый случай, когда выполнение инструкции SYSRET может вызвать исключение. Действительно, если регистр RCX (из которого загружается значение RIP при переходе в режим пользователя) содержит неканонический адрес, то процессор вызовет исключение #GP (General Protection fault).
Канонический адрес – это адрес, располагающийся в одном из следующих диапазонов:
Канонические адреса требуются для того, чтобы ограничить виртуальное адресное пространство 48ю битами вместо полных 64 бит.
В интеловской архитектуре процессор при вызове исключения #GP работает в ring0, в то время как значения всех регистров общего назначения уже восстановлены до значений режима ядра.
Управляя регистрами общего назначения, можно повлиять на работу гипервизора, обойти ограничения виртуальной среды и выполнить произвольный код в контексте гипервизора.
2. Вызов бага в Xen и CitrixXenServer
Тот факт, что инструкция SYSRET выполняется только в контексте гипервызова, подразумевает, что инструкция SYSCALL должна выполняться в режиме ядра. Первое, что приходит на ум: для вызова бага использовать модуль ядра.
Чтобы баг сработал, нам нужно отобразить страницу памяти где-нибудь рядом с неканоническими адресами и вызвать SYSCALL так, чтобы адрес инструкции следующей за SYSCALL указывал бы на неканониченский адрес.
Обычно Linux не позволяет отображать память (с помощью функции mmap()) рядом с каноническим адресом 0x7FFFFFFFF000. Поэтому в первую очередь нам нужно как-то обойти такое ограничение.
В Linux после проверки всех параметров функция mmap() вызывает функцию “mmap_region()”, описанную в файле “mm/mmap.c”. Вызов непосредственно функции с необходимыми параметрами позволит нам отобразить страницу на адрес 0x7FFFFFFFF000:
void *mapping;
|
В мануале по mmap утверждается, что благодаря флагу MAP_POPULATE обновление таблицы страниц произойдет еще до возникновения ошибки, если таковая будет иметь место. В контексте модуля ядра, доступ к отображенной странице без флага MAP_POPULATE приведет к ошибке.
Таким образом, после вызова mmap_region с адресом 0x7FFFFFFFF000 в качестве параметра, ядро выделит страницу размером 0x1000 байт, начинающуюся с адреса 0x7FFFFFFFF000. Поскольку размер страницы 0x1000 байт, диапазон выделенных адресов будет начинаться адресом 0x7FFFFFFFF000 и заканчиваться адресом 0x7FFFFFFFFFFF.
Код инструкции SYSCALL – 0F 05. Разместив инструкцию по адресу 0x7FFFFFFFFFFE, мы заставим баг сработать:
|
После вызова инструкции SYSCALL адресом возврата будет неканонический адрес 0x800000000000. Следовательно, в результате выполнения инструкции SYSRET, мы вызовем исключение #GP с привилегиями ring0 и подконтрольными нам регистрами.
3. Эксплуатация уязвимости в Xen и CitrixXenServer
При эксплуатации использовались 64битная паравиртуализируемая Linux-машина, Citrix XenServer версии 6.0.0 и гипервизор Xen версии 4.1.1. Описываемый метод работает также и в других средах.
3.1 Изменение адреса возврата
Так как гостевая система не является полностью виртуализируемой, инструкция “sidt” в действительности вернет адрес таблицы IDT гипервизора Xen. Поэтому перезапись таблицы IDT гипервизора позволит выполнить произвольный код. Локальная атака на IDT схожа с удаленной атакой в том плане, что неизвестно, как восстановить исходное состояние таблицы IDT ее модификации. Даже если мы попытаемся последовательно восстановить таблицу, используя сохраненные записи, такие как DivideError, у нас, скорее всего, ничего не получится. Поэтому описываемый эксплойт прекрасно работает на одних версиях гипервизора Xen, но оказывается полностью бесполезным на других версиях.
Необходим более надежный способ, не требующий внедрения в адресное пространство Xen.
После конференции BlackHat мне выпал шанс поговорить с Рафалом Вайджуком, человеком, который и обнаружил уязвимость. Ему пришла в голову хитрая идея, как использовать уязвимость, полагаясь не на аппаратные адреса, а на некоторые особенности обработки гипервизором исключений.
В основе эксплойта лежит способ обработки переменной “current ”. Переменная является указателем на структуру vCPU (Virtual CPU). vCPU содержит информацию о состоянии виртуальной машины (регистры общего назначения, виртуальная IDT, таблицы страниц и.т.п.)
Xen получает указатель current_vcpu из низа стека с помощью макроса “get_current”:
|
При диспетчеризации исключения #GP выполняется следующий код из “arch/x86/x86-64/entry.S”:
|
При входе в функцию “do_general_protection()” из “arch/x86/traps.с” выполняется несколько проверок:
|
Как правило, #GP приводит к панике ядра, чего нам не надо. Макрос guest_mode можно “обмануть” и заставить его думать, что исключение #GP на самом деле было вызвано из ring3:
|
Макрос “guest_cpu_user_regs()” определен в “xen/include/asm-x86/current.h” следующим образом:
#define guest_cpu_user_regs() (&get_cpu_info()->guest_cpu_user_regs) |
Если удастся присвоить переменной diff значение NULL, то проверка вернет true, и макрос будет считать, что исключение было вызвано из ring3.
Ассемблерный код макросов GET_CURRENT и “guest_cpu_user_regs()” соответственно:
mov RBX, 0xFFFFFFFFFFF8000
and:
|
Установить значения для RAX и RBX достаточно легко, так как можно просто закинуть в RSP необходимый нам адрес.
Если исключение вызвано из ring3, то выполняется следующий код:
|
Наконец, вызывается функция “emulate_priveleged_op()” из “xen/x86/traps.c”; но в следующем участке кода подфункция “read_descriptor()” вызовет исключение #PF:
static int read_descriptor(unsigned int sel,
|
Исключение #PF в действительности вызвано ошибкой чтения из таблиц GDT/IDT. Дело в том, что макрос “__get_user()” использует текущий селектор сегмента в качестве индекса в таблицах ядра GDT/IDT.
Затем срабатывает обработчик исключения “do_page_fault” из “xen/x86/traps.c”:
asmlinkage void do_page_fault(struct cpu_user_regs *regs)
|
Из-за того, что в стеке теперь лежат аргументы вложенного исключения, значение адреса в регистре RSP модифицировалось. Следовательно, проверка guest_modeвозвращает false, что приводит к вызову функции “spurious_page_fault()”:
static int __spurious_page_fault(unsigned long addr, unsigned int error_code)
|
Проверка “in_irq()” должна возвратить false, так как в противном случае, функция “spurious_page_fault()” вернет 0, и произойдет паника ядра. К счастью, поведением макроса “in_irq()” можно управлять, ассемблерный код макроса приведен ниже:
0xffff82c4801fe2bc <+4> : mov rax,0xffffffffffff8000
|
Переменная “current” содержится в регистре RAX. Смещение из структуры “current” (DWORD PTR [rax+0xc8]) используется в качестве индекса в массиве irq_array. Если значение по индексу равно 0, “in_irq” вернет false, функция “spurious_page_fault()” вернет 1, и мы покинем обработчик исключения “do_page_fault()”.
Затем выполнение кода перейдет к той инструкции, которая следует непосредственно за инструкцией, вызвавшей исключение #PF в “read_descriptor()”. В результате функция “read_descriptor()” вернет 0.
Значение, возвращаемое “read_descriptor()” затем проверяется в “emulate_privileged_code()”:
if ( !read_descriptor(regs->cs, v, regs,
|
Функция “emulate_privileged_code()” возвращает 0, условие “if” мы перепрыгиваем и затем достигаем функции “go_guest_trap()”. И вот тут все становится намного интереснее:
/* Emulate some simple privileged and I/O instructions. */
|
Функция “go_guest_trap()” ответственна за то, чтобы передать информацию об #GP в обработчик исключения ядра. Поскольку мы обманули гипервизор, и теперь он думает, что исключение произошло в ring3, логичным было бы передать обработку исключения в ядро:
static void do_guest_trap(int trapnr, const struct cpu_user_regs *regs, int use_error_code)
|
Указатель “current” используется для инициализации структуры trap_bounce. Адреса двух структур (tb, ti) представляют собой смещения от указателя arch. Arch, в свою очередь, это одно из полей в указателе “current”. Можно легко передать необходимый нам указатель arch, и перезаписать старые значения tb->cs и tb->eip. Соответственно, после выполнения инструкции SYSRET значения регистров CS и RIP также изменятся.
Затем производится возврат из функции “do_general_protection()” и выполняется следующий код:
/* %rbx: struct vcpu, interrupts disabled */
|
После достижения инструкции IRETQ, из стека достается новый селектор сегмента и осуществляется дальний переход на измененный адрес возврата.
Как уже утверждалось ранее, все структуры располагаются в пользовательских участках адресного пространства, причем никакие аппаратные адреса не задействованы. Благодаря вышеописанным манипуляциям, нам удастся выполнить произвольный код с правами ring0.
3.2. Выполнение кода в контексте "dom0”
Поскольку эксплойт работает в режиме ядра, то подразумевается, что нарушитель уже обладает (на законных основаниях или повысив свои привилегии с помощью другого эксплойта) правами rootна гостевой машине. С помощью описываемого в статье эксплойта можно получить права самого привилегированного домена “dom0”, домена, который по умолчанию имеет прямой доступ к аппаратному обеспечению. Из “dom0” можно управлять самим гипервизором, а также запускать другие непривилегированные домены (domU).
В Citrix XenServer dom0 представляет собой 32битную виртуальную машину. 32битная конфигурация была выбрана из соображений производительности.
Наша тактика будет следующей: внедриться в dom0 и получить права root c помощью bindshell или reverse shell.
Будет использоваться такая же идея, как и при удаленной эксплуатации уязвимости ядра: модифицировать обработчик исключения 0x80 и дождаться, когда случится прерывание в dom0. После исключения необходимо убедиться, что виртуальные страницы dom0 отображены в памяти.
Страницы памяти Xen отображаются в одно и то же адресное пространство во всех виртуализируемых ядрах, точно так же, как адрес 0xc0000000 отображается во всех 32битных пользовательских процессах.
Так как память Xen отображается с правами RWX, то нам нужно только записать в неиспользуемое адресное пространство новый обработчик исключения 0x80 и переписать соответствующую запись в таблице IDT.
В обработчике первого уровня производятся следующие действия:
Если пользовательский процесс произвел системный вызов из домена dom0, то нам нужно сохранить контекст процесса; изменить аргументы системного вызова, так чтобы осуществить системный вызов mmap() с правами RWX; установить указатель EIP процесса на инструкцию “int 0x80” и вызвать исходный обработчик исключения 0x80.
После удачного выполнения системного вызова процесс возвратится к инструкции “int 0x80” и выполнит системный вызов, причем значение регистра EAX должно быть больше 0x1000 (для вызова mmap()). Затем выполняется обработчик “int 0x80” второго уровня: