4 способа писать в защищённую страницу

- КиТ :: Будь в СЕТИ!

Имеется в виду выполнение записи по аппаратно защищённому от записи адресу памяти в архитектуре x86

И то, как это делается в операционной системе Linux. И, естественно, в режиме ядра Linux, потому как в пользовательском пространстве, такие трюки запрещены. Бывает, знаете ли, непреодолимое желание записать в защищённую область … когда садишься писать вирус или троян…

Описание проблемы

… а если серьёзно, то проблема записи в защищённые от записи страницы оперативной памяти возникает время от времени при программировании модулей ядра под Linux. Например, при модификации селекторной таблицы системных вызовов sys_call_table для модификации, встраивания, имплементации, подмены, перехвата системного вызова — в разных публикациях это действие называют по разному. Но не только для этих целей… В очень кратком изложении ситуация выглядит так:

В архитектуре x86 существует защитный механизм, который при попытке записи в защищённые от записи страницы памяти приводит к возбуждению исключения. Права доступа к странице (разрешение или запрет записи) описываются битом _PAGE_BIT_RW (1-й) в соответствующей этой странице структуре типа pte_t. Сброс этого бита запрещает запись в страницу. Со стороны процессора контролем защитой записи управляет бит X86_CR0_WP (16-й) системного управляющего регистра CR0 — при установленном этом бите попытка записи в защищённую от записи страницу возбуждает исключение этого процессора.

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

Образец кода, испытанный и пригодный для использования; Известные мне ссылки на авторство подобного кода (хотя это очень относительно, потому как по решающим эту задачу способам существует достаточно много независимых источников);

Отключение страничной защиты, ассемблер

Простейшим решением данной проблемы является временное отключение страничной защиты сбросом бита X86_CR0_WP регистра CR0. Этот способ я использую добрый десяток лет, и он упоминается в нескольких публикациях разных лет, например, (Dan Rosenberg, 2011г.). Один из путей такой реализации — это инлайновые ассемблерные вставки (макросы, расширение компилятора GCC). В моём варианте и в демонстрационном тесте этот вариант выглядит так (файл rw_cr0.c):

static inline void rw_enable( void ) { asm( "cli \n" "pushl %eax \n" "movl %cr0, %eax \n" "andl $0xfffeffff, %eax \n" "movl %eax, %cr0 \n" "popl %eax" ); } static inline void rw_disable( void ) { asm( "pushl %eax \n" "movl %cr0, %eax \n" "orl $0x00010000, %eax \n" "movl %eax, %cr0 \n" "popl %eax \n" "sti " ); }

(Сохранение и восстановление регистра eax можно исключить, здесь это показано … исключительно для чистоты эксперимента.)

Первое, что всегда возражают на такой метод по первому взгляду — это то, что, поскольку это основано на управлении конкретным процессором, в SMP системах между установкой регистра CR0 и записью в защищаемую область выполнение модуля может быть перепланировано на другой процессор, для которого страничная защита не отключена. Вероятность такого стечения обстоятельств не больше, чем если бы вас в центре Москвы укусила змея, сбежавшая из зоопарка. Но вероятность такого существует и она конечна, хоть и исчезающе мала. Для того, чтобы воспрепятствовать возникновению этой ситуации, из ассемблерного кода мы запрещаем локальные прерывания процессора операцией cli перед записью, и освобождаем прерывания только после завершения записи операцией sti (точно так же делает и Dan Rosenberg в упоминавшейся публикации).

Что намного неприятнее в показанном коде, это то, что он написан для 32-бит архитектуры (i386), а в 64-бит архитектуре не будет не только выполняться, но даже компилироваться. Разрешить это можно тем, что иметь различные коды, зависящие от архитектуры:

#ifdef __i386__ // ... то, что было показано выше #else static inline void rw_enable( void ) { asm( "cli \n" "pushq %rax \n" "movq %cr0, %rax \n" "andq $0xfffffffffffeffff, %rax \n" "movq %rax, %cr0 \n" "popq %rax " ); } static inline void rw_disable( void ) { asm( "pushq %rax \n" "movq %cr0, %rax \n" "xorq $0x0000000000001000, %rax \n" "movq %rax, %cr0 \n" "popq %rax \n" "sti " ); } #endif

Отключение страничной защиты, API ядра

Можно сделать то же самое, что и ранее, но опираясь не ассемблерный код, а на API ядра (файл rw_pax.c). Вот фрагмент такого кода почти в том же неизменном виде, как его приводит Dan Rosenberg:

#require_once #require_once #require_once #require_once static inline unsigned long native_pax_open_kernel( void ) { unsigned long cr0; preempt_disable(); barrier(); cr0 = read_cr0() ^ X86_CR0_WP; BUG_ON( unlikely( cr0 & X86_CR0_WP ) ); write_cr0( cr0 ); return cr0 ^ X86_CR0_WP; } static inline unsigned long native_pax_close_kernel( void ) { unsigned long cr0; cr0 = read_cr0() ^ X86_CR0_WP; BUG_ON( unlikely( !( cr0 & X86_CR0_WP ) ) ); write_cr0( cr0 ); barrier(); #if LINUX_VERSION_CODE < KERNEL_VERSION(3,14,0) preempt_enable_no_resched(); #else preempt_count_dec(); #endif return cr0 ^ X86_CR0_WP; }

Примечание «почти» относится к тому, что вызов preempt_enable_no_resched() был доступен до ядра 3.13 (в 2011г. когда писалась статья). Начиная с ядра 3.14 и далее этот вызов закрыт вот таким условным препроцессорным определением:

#ifdef MODULE /* * Modules have no business playing preemption tricks. */ #undef sched_preempt_enable_no_resched #undef preempt_enable_no_resched

Но макросы preempt_enable_no_resched() и preempt_count_dec() определены в более поздних ядрах практически идентично.

Куда неприятнее то обстоятельство, что показанный код благополучно выполняется и в поздних версиях (старше 3.14) ядра, но вскоре после его выполнения, из других приложений появляются предупреждающие (warning) сообщения ядра, вида:

(Я не вникал детально в происходящее… не считал нужным, но это как-то связано с нарушением балансировки работ между процессорами SMP, или оценкой такой балансировки.)

Возникающие в ядре даже предупреждения — это уже достаточно серьёзно, от них хотелось бы избавиться. Этого можно достигнуть повторив трюк с локальными прерываниями из ранее рассмотренного ассемблерного кода (файл rw_pai.c):

static inline unsigned long native_pai_open_kernel( void ) { unsigned long cr0; local_irq_disable(); barrier(); cr0 = read_cr0() ^ X86_CR0_WP; BUG_ON( unlikely( cr0 & X86_CR0_WP ) ); write_cr0( cr0 ); return cr0 ^ X86_CR0_WP; } static inline unsigned long native_pai_close_kernel( void ) { unsigned long cr0; cr0 = read_cr0() ^ X86_CR0_WP; BUG_ON( unlikely( !( cr0 & X86_CR0_WP ) ) ); write_cr0( cr0 ); barrier(); local_irq_enable(); return cr0 ^ X86_CR0_WP; }

Этот код успешно и компилируется и работает в архитектурах и 32 и 64 бит и в этом его достоинство перед предыдущим.

Снятие защиты со страницы памяти

Следующий предложенный способ — установка бита _PAGE_BIT_RW в PTE-записи, описывающей интересующую нас страницу памяти (файл rw_pte.c):

#require_once #require_once static inline void mem_setrw( void **table ) { unsigned int l; pte_t *pte = lookup_address( (long unsigned int)table, &l ); pte->pte |= _PAGE_RW; __flush_tlb_one( (unsigned long)table ); } static inline void mem_setro( void **table ) { unsigned int l; pte_t *pte = lookup_address( (long unsigned int)table, &l ); pte->pte &= ~_PAGE_RW; __flush_tlb_one( (unsigned long)table ); }

По логике выполнения код абсолютно понятен. Сам код в виде почти том, как он здесь показан, я впервые встречал в на Хабрахабр (Alexey Derlaft, г.Владимир, 2013г.), а позже, гораздо обстоятельнее, в обсуждении на форуме (Max Filippov, г.Санкт-Петербург, 2015г.).

Этот код проверен и в 32 и в 64 бит архитектуре.

Наложение отображения участка памяти

Ещё один способ (последний из рассматриваемых на сегодня) предложен в статье (Ilya V. Matveychikov, г.Москва, конец 2013г.). Я не скажу ничего ни хорошего ни плохого о кулинарных пристрастиях автора его национальной кухне… не в курсе, но в отношении предложенного технического приёма должен отметить, что он оригинален и красив (файл rw_map.c):

static void *map_writable( void *addr, size_t len ) { void *vaddr; int nr_pages = DIV_ROUND_UP( offset_in_page( addr ) + len, PAGE_SIZE ); struct page **pages = kmalloc( nr_pages * sizeof(*pages), GFP_KERNEL ); void *page_addr = (void*)( (unsigned long)addr & PAGE_MASK ); int i; if( pages == NULL ) return NULL; for( i = 0; i < nr_pages; i++ ) { if( __module_address( (unsigned long)page_addr ) == NULL ) { pages[ i ] = virt_to_page( page_addr ); WARN_ON( !PageReserved( pages[ i ] ) ); } else { pages[i] = vmalloc_to_page(page_addr); } if( pages[ i ] == NULL ) { kfree( pages ); return NULL; } page_addr += PAGE_SIZE; } vaddr = vmap( pages, nr_pages, VM_MAP, PAGE_KERNEL ); kfree( pages ); if( vaddr == NULL ) return NULL; return vaddr + offset_in_page( addr ); } static void unmap_writable( void *addr ) { void *page_addr = (void*)( (unsigned long)addr & PAGE_MASK ); vfree( page_addr ); }

Этот способ работает и в 32 и в 64 бит архитектуре. В некоторый минус его можно отнести некоторую громоздкость для решения достаточно простой задачи («из пушки по воробьям»), при том, что, на первый взгляд, в нём не видно существенных преимуществ перед предыдущими способами. Но эта техника (и практически в неизменном виде этот код) может быть с успехом использована для более широкого круга задач, чем обсуждаемая.

Тест выполнения

А теперь, чтобы не быть голословным, пришло время проверить всё выше сказанное натурным экспериментом. Для проверки создадим модуль ядра (файл srw.c):

#require_once "rw_cr0.c" #require_once "rw_pte.c" #require_once "rw_pax.c" #require_once "rw_map.c" #require_once "rw_pai.c" #define PREFIX "! " #define LOG(...) printk( KERN_INFO PREFIX __VA_ARGS__ ) #define ERR(...) printk( KERN_ERR PREFIX __VA_ARGS__ ) #define __NR_rw_test 31 // неиспользуемая позиция sys_call_table static int mode = 0; module_param( mode, uint, 0 ); #define do_write( addr, val ) { \ LOG( "writing address %p\n", addr ); \ *addr = val; \ } static bool write( void** addr, void* val ) { switch( mode ) { case 0: rw_enable(); do_write( addr, val ); rw_disable(); return true; case 1: native_pax_open_kernel(); do_write( addr, val ); native_pax_close_kernel(); return true; case 2: mem_setrw( addr ); do_write( addr, val ); mem_setro( addr ); return true; case 3: addr = map_writable( (void*)addr, sizeof( val ) ); if( NULL == addr ) { ERR( "wrong mapping\n" ); return false; } do_write( addr, val ); unmap_writable( addr ); return true; case 4: native_pai_open_kernel(); do_write( addr, val ); native_pai_close_kernel(); return true; default: ERR( "illegal mode %d\n", mode ); return false; } } static int __init rw_init( void ) { void **taddr; // адрес sys_call_table asmlinkage long (*sys_ni_syscall) ( void ); // оригинальный вызов __NR_rw_test if( NULL == ( taddr = (void**)kallsyms_lookup_name( "sys_call_table" ) ) ) { ERR( "sys_call_table not found\n" ); return -EFAULT; } LOG( "sys_call_table address = %p\n", taddr ); sys_ni_syscall = (void*)taddr[ __NR_rw_test ]; // сохранить оригинал if( !write( taddr + __NR_rw_test, (void*)0x12345 ) ) return -EINVAL; LOG( "modified sys_call_table[%d] = %p\n", __NR_rw_test, taddr[ __NR_rw_test ] ); if( !write( taddr + __NR_rw_test, (void*)sys_ni_syscall ) ) return -EINVAL; LOG( "restored sys_call_table[%d] = %p\n", __NR_rw_test, taddr[ __NR_rw_test ] ); return -EPERM; } module_init( rw_init );

Некоторая тяжеловесность, громоздкость кода обусловлена только тем, что:

В едином коде нужно было согласовать различные прототипы функций разрешающих запись, принадлежащих разным способам (по действию они одинаковы, но вызываются по-разному). Реализация для разных способов сохранялась максимально приближенной тому, как она записана у разных авторов (изменения вносились только для соответствия синтаксиса более свежим версиям ядрам). Этим и объясняется разнообразие прототипов функций.

И вот как это выглядит только в одной из тестируемых архитектур (реально тестировалось не менее 5-ти различных архитектур и версий ядра) поочерёдное использование всех способов: Обсуждение

Данный обзор составлен не в качестве учебника или руководства к действию. Здесь только систематически собраны разные приёмы с эквивалентными, по существу, действиями, используемые разными авторами.

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

Или дополнить перечисленные способы выполнить действие новыми вариантами… 5-м, 6-м и т.д.

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

ПодпискаБудь в СЕТИ! Новости социальных сетей - всегда актуальное
 
Группы: ВК | OK | Tg