Эксплойт уязвимости Xen Hypervisor Sysret VM Escape

Эксплойт уязвимости Xen Hypervisor Sysret VM Escape

Привет всем! В этом блоге мы поделимся техническим анализом и эксплуатацией критической уязвимости (CVE-2012-0217), которой подвержен гипервизор Xen.

Автор: Jordan Gruskovnjak

Привет всем! В этом блоге мы поделимся техническим анализом и эксплуатацией критической уязвимости (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”:


/*
* When entering SYSCALL from kernel mode:
* %rax = hypercall vector
* %rdi, %rsi, %rdx, %r10, %r8, %9 = hypercall arguments
* %rcx = SYSCALL-saved %rip
* NB. We must move %r10 to %rcx for C function-calling ABI.
*
* When entering SYSCALL from user mode:
* Vector directly to the registered arch.syscall_addr.
*
* Initial work is done by per-CPU stack trampolines. At this point %rsp
* has been initialised to point at the correct Xen stack, and %rsp, %rflags
* and %cs have been saved. All other registers are still to be saved onto
* the stack, starting with %rip, and an appropriate %ss must be saved into
* the space left by the trampoline.
*/
ALIGN
ENTRY(syscall_enter)
sti
movl $FLAT_KERNEL_SS,24(%rsp)
pushq %rcx
pushq $0
movl $TRAP_syscall,4(%rsp)
movq 24(%rsp),%r11 /* Re-load user RFLAGS into %r11 before SAVE_ALL */
SAVE_ALL
GET_CURRENT(%rbx)
movq VCPU_domain(%rbx),%rcx
testb $1,DOMAIN_is_32bit_pv(%rcx)
jnz compat_syscall
testb $TF_kernel_mode,VCPU_thread_flags(%rbx)
jz switch_to_kernel

Если вызов исходил из пользовательского процесса, то Xen передает вызов в ядро, и затем для обработки вызова используется таблица IDT ядра. Если же системный вызов производился из режима ядра (гипервызов), то Xen использует свою таблицу IDT, чтобы обработать системный вызов. Снова код из “xen/x86/x86-64/entry.S”:

ENTRY(syscall_enter)
sti
movl $FLAT_KERNEL_SS,24(%rsp)
pushq %rcx
pushq $0
movl $TRAP_syscall,4(%rsp)
movq 24(%rsp),%r11 /* Re-load user RFLAGS into %r11 before SAVE_ALL */
SAVE_ALL
GET_CURRENT(%rbx)
movq VCPU_domain(%rbx),%rcx
testb $1,DOMAIN_is_32bit_pv(%rcx)
jnz compat_syscall
testb $TF_kernel_mode,VCPU_thread_flags(%rbx)
jz switch_to_kernel
...

/*hypercall:*/
movq %r10,%rcx
cmpq $NR_hypercalls,%rax
jae bad_hypercall ; exit in case of wrong hypercall number
...
leaq hypercall_table(%rip),%r10
PERFC_INCR(hypercalls, %rax, %rbx)
callq *(%r10,%rax,8) ; jump on hypercall handler

При возврате из гипервызова в режиме ядра выполняется следующий код:


/* %rbx: struct vcpu, interrupts disabled */
restore_all_guest:
ASSERT_INTERRUPTS_DISABLED
RESTORE_ALL
testw $TRAP_syscall,4(%rsp)
jz iret_exit_to_guest

addq $8,%rsp
popq %rcx ; RIP
popq %r11 ; CS
cmpw $FLAT_USER_CS32,%r11
popq %r11 ; RFLAGS
popq %rsp ; RSP
je 1f
sysretq ; 64 bits kernel
1: sysretl ; 32 bits kernel

В мануалах Intel утверждается, что существует особый случай, когда выполнение инструкции SYSRET может вызвать исключение. Действительно, если регистр RCX (из которого загружается значение RIP при переходе в режим пользователя) содержит неканонический адрес, то процессор вызовет исключение #GP (General Protection fault).

Канонический адрес – это адрес, располагающийся в одном из следующих диапазонов:

  • 0x0000000000000000 - 0x00007FFFFFFFFFFF
  • 0xFFFF800000000000 - 0xFFFFFFFFFFFFFFFF

Канонические адреса требуются для того, чтобы ограничить виртуальное адресное пространство 48ю битами вместо полных 64 бит.

В интеловской архитектуре процессор при вызове исключения #GP работает в ring0, в то время как значения всех регистров общего назначения уже восстановлены до значений режима ядра.

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

2. Вызов бага в Xen и CitrixXenServer

Тот факт, что инструкция SYSRET выполняется только в контексте гипервызова, подразумевает, что инструкция SYSCALL должна выполняться в режиме ядра. Первое, что приходит на ум: для вызова бага использовать модуль ядра.

Чтобы баг сработал, нам нужно отобразить страницу памяти где-нибудь рядом с неканоническими адресами и вызвать SYSCALL так, чтобы адрес инструкции следующей за SYSCALL указывал бы на неканониченский адрес.

Обычно Linux не позволяет отображать память (с помощью функции mmap()) рядом с каноническим адресом 0x7FFFFFFFF000. Поэтому в первую очередь нам нужно как-то обойти такое ограничение.

В Linux после проверки всех параметров функция mmap() вызывает функцию “mmap_region()”, описанную в файле “mm/mmap.c”. Вызов непосредственно функции с необходимыми параметрами позволит нам отобразить страницу на адрес 0x7FFFFFFFF000:

void *mapping;
unsigned int vm_flags;
vm_flags = VM_READ | VM_WRITE | VM_EXEC | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

mapping = p_mmap_region(NULL, 0x7ffffffff000, 0x1000,
MAP_ANON|MAP_PRIVATE|MAP_FIXED|MAP_POPULATE, vm_flags, 0);

В мануале по mmap утверждается, что благодаря флагу MAP_POPULATE обновление таблицы страниц произойдет еще до возникновения ошибки, если таковая будет иметь место. В контексте модуля ядра, доступ к отображенной странице без флага MAP_POPULATE приведет к ошибке.

Таким образом, после вызова mmap_region с адресом 0x7FFFFFFFF000 в качестве параметра, ядро выделит страницу размером 0x1000 байт, начинающуюся с адреса 0x7FFFFFFFF000. Поскольку размер страницы 0x1000 байт, диапазон выделенных адресов будет начинаться адресом 0x7FFFFFFFF000 и заканчиваться адресом 0x7FFFFFFFFFFF.

Код инструкции SYSCALL – 0F 05. Разместив инструкцию по адресу 0x7FFFFFFFFFFE, мы заставим баг сработать:


Address
7FFFFFFFF000
...
7FFFFFFFFFFE SYSCALL
800000000000 Non-canonical address

После вызова инструкции 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”:


struct cpu_info {
struct cpu_user_regs guest_cpu_user_regs;
unsigned int processor_id;
struct vcpu *current_vcpu;
unsigned long per_cpu_offset;
#ifdef __x86_64__ /* get_stack_bottom() must be 16-byte aligned */
unsigned long __pad_for_stack_bottom;
#endif
};

static inline struct cpu_info *get_cpu_info(void)
{
struct cpu_info *cpu_info;
__asm__ ( "and %%"__OP"sp,%0; or %2,%0"
: "=r" (cpu_info)
: "0" (~(STACK_SIZE-1)), "i" (STACK_SIZE-sizeof(struct cpu_info))
);
return cpu_info;
}

#define get_current() (get_cpu_info()->current_vcpu)
#define set_current(vcpu) (get_cpu_info()->current_vcpu = (vcpu))
#define current (get_current())

При диспетчеризации исключения #GP выполняется следующий код из “arch/x86/x86-64/entry.S”:


/* No special register assumptions. */
ENTRY(handle_exception)
SAVE_ALL
handle_exception_saved:
testb $X86_EFLAGS_IF>>8,UREGS_eflags+1(%rsp)
jz exception_with_ints_disabled
sti
1: movq %rsp,%rdi
movl UREGS_entry_vector(%rsp),%eax
leaq exception_table(%rip),%rdx
GET_CURRENT(%rbx) ; Retrieve guest_cpu_user_regs
; of cpu_info structure
PERFC_INCR(exceptions, %rax, %rbx)
callq *(%rdx,%rax,8) ; call exception handler

При входе в функцию “do_general_protection()” из “arch/x86/traps.с” выполняется несколько проверок:


asmlinkage void do_general_protection(struct cpu_user_regs *regs)
{
struct vcpu *v = current;
unsigned long fixup;

DEBUGGER_trap_entry(TRAP_gp_fault, regs);

if ( regs->error_code & 1 )
goto hardware_gp;

if ( !guest_mode(regs) )
goto gp_in_kernel;
...

gp_in_kernel:

...
DEBUGGER_trap_fatal(TRAP_gp_fault, regs);

hardware_gp:
show_execution_state(regs);
panic("GENERAL PROTECTION FAULT\n[error_code=%04x]\n", regs->error_code);

Как правило, #GP приводит к панике ядра, чего нам не надо. Макрос guest_mode можно “обмануть” и заставить его думать, что исключение #GP на самом деле было вызвано из ring3:


#define guest_mode(r) \
({ \
unsigned long diff = (char *)guest_cpu_user_regs() - (char *)(r); \
/* Frame pointer must point into current CPU stack. */ \
ASSERT(diff < STACK_SIZE); \
/* If not a guest frame, it must be a hypervisor frame. */ \
ASSERT((diff == 0) || (!vm86_mode(r) && (r->cs == __HYPERVISOR_CS))); \
/* Return TRUE if it's a guest frame. */ \
(diff == 0); \
})

Макрос “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 RBX, RSP
or RBX, 0x7FE8
mov RBX, [RBX]

and:

mov RAX, 0xFFFFFFFFFFFF8000
and RAX, RSP
or RAX, 0x7F18

Установить значения для RAX и RBX достаточно легко, так как можно просто закинуть в RSP необходимый нам адрес.

Если исключение вызвано из ring3, то выполняется следующий код:


if ( (regs->error_code & 3) == 2 )
{
...
}
else if ( is_pv_32on64_vcpu(v) && regs->error_code )
{
...
}

/* Emulate some simple privileged and I/O instructions. */
if ( (regs->error_code == 0) &&
emulate_privileged_op(regs) )
{
trace_trap_one_addr(TRC_PV_EMULATE_PRIVOP, regs->eip);
return;
}

Наконец, вызывается функция “emulate_priveleged_op()” из “xen/x86/traps.c”; но в следующем участке кода подфункция “read_descriptor()” вызовет исключение #PF:

static int read_descriptor(unsigned int sel,
const struct vcpu *v,
const struct cpu_user_regs * regs,
unsigned long *base,
unsigned long *limit,
unsigned int *ar,
unsigned int vm86attr)
{
struct desc_struct desc;

if ( !vm86_mode(regs) )
{
if ( sel < 4)
desc.b = desc.a = 0;
else if ( __get_user(desc,
(const struct desc_struct *)(!(sel & 4)
? GDT_VIRT_START(v)
: LDT_VIRT_START(v))
+ (sel >> 3)) )
return 0;

Исключение #PF в действительности вызвано ошибкой чтения из таблиц GDT/IDT. Дело в том, что макрос “__get_user()” использует текущий селектор сегмента в качестве индекса в таблицах ядра GDT/IDT.

Затем срабатывает обработчик исключения “do_page_fault” из “xen/x86/traps.c”:

asmlinkage void do_page_fault(struct cpu_user_regs *regs)
{
unsigned long addr, fixup;
unsigned int error_code;

addr = read_cr2();

/* fixup_page_fault() might change regs->error_code, so cache it here. */
error_code = regs->error_code;

DEBUGGER_trap_entry(TRAP_page_fault, regs);

perfc_incr(page_faults);

if ( unlikely(fixup_page_fault(addr, regs) != 0) )
return;

if ( unlikely(!guest_mode(regs)) )
{
if ( spurious_page_fault(addr, error_code) )
return;

Из-за того, что в стеке теперь лежат аргументы вложенного исключения, значение адреса в регистре RSP модифицировалось. Следовательно, проверка guest_modeвозвращает false, что приводит к вызову функции “spurious_page_fault()”:

static int __spurious_page_fault(unsigned long addr, unsigned int error_code)
{
unsigned long mfn, cr3 = read_cr3();
#if CONFIG_PAGING_LEVELS >= 4
l4_pgentry_t l4e, *l4t;
#endif
#if CONFIG_PAGING_LEVELS >= 3
l3_pgentry_t l3e, *l3t;
#endif
l2_pgentry_t l2e, *l2t;
l1_pgentry_t l1e, *l1t;
unsigned int required_flags, disallowed_flags;

/*
* We do not take spurious page faults in IRQ handlers as we do not
* modify page tables in IRQ context. We therefore bail here because
* map_domain_page() is not IRQ-safe.
*/
if ( in_irq() )
return 0;
...
return 1;

Проверка “in_irq()” должна возвратить false, так как в противном случае, функция “spurious_page_fault()” вернет 0, и произойдет паника ядра. К счастью, поведением макроса “in_irq()” можно управлять, ассемблерный код макроса приведен ниже:

0xffff82c4801fe2bc <+4> : mov rax,0xffffffffffff8000
0xffff82c4801fe2c3 <+11>: lea rcx,[rip+0x921b6] # 0xffff82c480290480 <irq_stat>
0xffff82c4801fe2ca <+18>: and rax,rsp
0xffff82c4801fe2cd <+21>: or rax,0x7f18
0xffff82c4801fe2d3 <+27>: movedx,DWORDPTR [rax+0xc8]
0xffff82c4801fe2d9 <+33>: xor eax,eax
0xffff82c4801fe2db <+35>: shl rdx,0x7
0xffff82c4801fe2df <+39> : cmpDWORDPTR [rcx+rdx*1+0x8],0x0

Переменная “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,
&code_base, &code_limit, &ar,
_SEGMENT_CODE|_SEGMENT_S|_SEGMENT_DPL|_SEGMENT_P) )
goto fail;
...

fail:
return 0;

Функция “emulate_privileged_code()” возвращает 0, условие “if” мы перепрыгиваем и затем достигаем функции “go_guest_trap()”. И вот тут все становится намного интереснее:

/* Emulate some simple privileged and I/O instructions. */
if ( (regs->error_code == 0) &&
emulate_privileged_op(regs) )
{
trace_trap_one_addr(TRC_PV_EMULATE_PRIVOP, regs->eip);
return;
}

/* Pass on GPF as is. */
do_guest_trap(TRAP_gp_fault, regs, 1);

Функция “go_guest_trap()” ответственна за то, чтобы передать информацию об #GP в обработчик исключения ядра. Поскольку мы обманули гипервизор, и теперь он думает, что исключение произошло в ring3, логичным было бы передать обработку исключения в ядро:

static void do_guest_trap(int trapnr, const struct cpu_user_regs *regs, int use_error_code)
{
struct vcpu *v = current;
struct trap_bounce *tb;
const struct trap_info *ti;

trace_pv_trap(trapnr, regs->eip, use_error_code, regs->error_code);

tb = &v->arch.trap_bounce;
ti = &v->arch.guest_context.trap_ctxt[trapnr];

tb->flags = TBF_EXCEPTION;
tb->cs = ti->cs;
tb->eip = ti->address;


if ( use_error_code )
{
tb->flags |= TBF_EXCEPTION_ERRCODE;
tb->error_code = regs->error_code;
}

if ( TI_GET_IF(ti) )
tb->flags |= TBF_INTERRUPT;

if ( unlikely(null_trap_bounce(v, tb)) )
gdprintk(XENLOG_WARNING, "Unhandled %s fault/trap [#%d] "
"on VCPU %d [ec=%04x]\n",
trapstr(trapnr), trapnr, v->vcpu_id, regs->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 */
restore_all_guest:
ASSERT_INTERRUPTS_DISABLED
RESTORE_ALL
testw $TRAP_syscall,4(%rsp)
jz iret_exit_to_guest
...
/* No special register assumptions. */
iret_exit_to_guest:
addq $8,%rsp
.Lft0: iretq

После достижения инструкции 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” второго уровня:


Шеллкод создаст копию исходного процесса с помощью fork(); установит EIP родительского процесса на инструкцию “int 0x80” на сей раз уже с оригинальными параметрами, так чтобы родительский процесс смог продолжать работать дальше без ошибок. Процесс-потомок в это время выполняет классический ring3-шеллкод и дарит нам ключ от нулевого королевства 

Ваш провайдер знает о вас больше, чем ваша девушка?

Присоединяйтесь и узнайте, как это остановить!