安全なページに書き込む4つの方法

これは、x86アーキテクチャのハードウェア書き込み保護メモリアドレスへの書き込みを指します。 そして、これがLinuxオペレーティングシステムで行われる方法。 そしてもちろん、Linuxカーネルモードでは、ユーザー空間ではそのようなトリックは禁止されています。 それは、あなたが知っているように、保護された領域に書き込みたいという抵抗できない欲求が起こる...あなたがウイルスやトロイの木馬を書くために座っているとき...



問題の説明



...しかし、真剣に、Linuxでカーネルモジュールをプログラミングするときに、書き込み保護されたRAMページへの書き込みの問題が時々発生します。 たとえば、sys_call_tableシステムコールセレクタテーブルを変更して、システムコールを変更、埋め込み、実装、置換、インターセプトする場合、このアクションは出版物によって異なります。 しかし、これらの目的だけでなく...非常に短い要約では、状況は次のようになります。







ハードウェアで保護されたメモリ領域への書き込みタスクの関連性は、このトピックに関する多数の出版物と、問題を解決するための提案された方法の数によって示されます。 レビューの残りの部分は、メソッドの逐次的な検討に専念します...各メソッドについて、それが与えられます:







ページ保護、アセンブラーをオフにする



この問題の最も簡単な解決策は、レジスタCR0のX86_CR0_WPビットをリセットして、ページ保護を一時的に無効にすることです。 私はこの方法を10年ほど使用していますが、たとえばWP:Safe or Not?など、異なる年のいくつかの出版物で言及されています。 (Dan Rosenberg、2011)。 これを行う1つの方法は、インラインアセンブラー挿入(マクロ、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レジスタの保存と復元は除外できます。これは、実験の純度のためだけにここに示されています。)



そのような方法に常に一目で反対する最初のことは、特定のプロセッサの管理に基づいているため、CR0レジスタのインストールと保護領域への書き込みの間のSMPシステムでは、ページ保護の別のプロセッサにモジュール実行を再スケジュールできることです切断されていません。 このような状況の組み合わせの可能性は、動物園から逃げたモスクワ中心部のヘビに噛まれた場合にすぎません。 しかし、これの可能性は存在し、消失するほどではありますが、有限です。 この状況がアセンブラーコードから発生するのを防ぐため、書き込み前の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が引用したのとほぼ同じ形式のこのようなコードのスニペットを次に示します。

 #include <linux/preempt.h> #include <asm/paravirt.h> #include <asm-generic/bug.h> #include <linux/version.h> 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より古いバージョン)で正常に実行されるという事実ですが、実行後すぐ 、カーネルからの警告メッセージが他のアプリケーションから次の形式で表示されます:

 [ 337.230937] ------------[ cut here ]------------ [ 337.230949] WARNING: CPU: 1 PID: 3410 at /build/buildd/linux-lts-utopic-3.16.0/init/main.c:802 do_one_initcall+0x1cb/0x1f0() [ 337.230955] initcall rw_init+0x0/0x1000 [srw] returned with preemption imbalance
      
      





(私は詳細に何が起こっているのかを掘り下げませんでした...それが必要だとは思いませんでしたが、それはどういうわけか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ビットアーキテクチャの両方で正常にコンパイルおよび動作します。これは、以前のアーキテクチャよりも優れている点です。



メモリページの保護を解除する



次に提案する方法は、対象のメモリページを記述するPTEレコードの_PAGE_BIT_RWビットを設定することです(rw_pte.cファイル)。

 #include <asm/pgtable_types.h> #include <asm/tlbflush.h> 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 ); }
      
      





実行ロジックによると、コードは完全に明確です。 コード自体は、ここでほぼどのように表示されるかという形で、最初にHabrahabr(Alexey Derlaft、Vladimir、2013)でのディスカッションで会い、その後、 システムコールを変更するフォーラムでのディスカッション(Max Filippov、サンクトペテルブルク、2015年)。

このコードは、32ビットと64ビットの両方のアーキテクチャでテストされています。



記憶場所の表示のオーバーレイ



別の方法(今日検討された最後の方法)は、記事「Linuxカーネルの書き込み保護された領域を変更するコーシャーの方法」 (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ファイル)を作成します。

 #include "rw_cr0.c" #include "rw_pte.c" #include "rw_pax.c" #include "rw_map.c" #include "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 );
      
      





いくらか重くて扱いにくいコードは、次の事実のみに起因します。

  • 単一のコードで、異なるメソッドに属する書き込み可能な関数の異なるプロトタイプを調整する必要がありました(これらは動作は同じですが、異なる方法で呼び出されます)。
  • さまざまなメソッドの実装は、さまざまな作成者による記述に可能な限り近づけました(構文はより新しいバージョンのカーネルと一致するように変更されました)。 これは、さまざまな関数プロトタイプを説明しています。




そして、すべての方法を順番に使用して、テスト対象のアーキテクチャの1つでのみ表示される方法を示します(少なくとも5つの異なるアーキテクチャとカーネルバージョンが実際にテストされました)。

 $ uname -r 3.16.0-48-generic $ uname -m x86_64 $ sudo insmod srw.ko mode=0 insmod: ERROR: could not insert module srw.ko: Operation not permitted $ dmesg | tail -n6 [ 7258.575977] ! detected 64-bit platform [ 7258.584504] ! sys_call_table address = ffffffff81801460 [ 7258.584579] ! writing address ffffffff81801558 [ 7258.584653] ! modified sys_call_table[31] = 0000000000012345 [ 7258.584654] ! writing address ffffffff81801558 [ 7258.584666] ! restored sys_call_table[31] = ffffffff812db550 $ sudo insmod srw.ko mode=2 insmod: ERROR: could not insert module srw.ko: Operation not permitted $ dmesg | tail -n6 [ 7282.625539] ! detected 64-bit platform [ 7282.633020] ! sys_call_table address = ffffffff81801460 [ 7282.633129] ! writing address ffffffff81801558 [ 7282.633178] ! modified sys_call_table[31] = 0000000000012345 [ 7282.633228] ! writing address ffffffff81801558 [ 7282.633291] ! restored sys_call_table[31] = ffffffff812db550 $ sudo insmod srw.ko mode=3 insmod: ERROR: could not insert module srw.ko: Operation not permitted $ dmesg | tail -n6 [ 7297.040272] ! detected 64-bit platform [ 7297.059764] ! sys_call_table address = ffffffff81801460 [ 7297.065930] ! writing address ffffc900001e6558 [ 7297.066000] ! modified sys_call_table[31] = 0000000000012345 [ 7297.066035] ! writing address ffffc9000033d558 [ 7297.066073] ! restored sys_call_table[31] = ffffffff812db550 $ sudo insmod srw.ko mode=4 insmod: ERROR: could not insert module srw.ko: Operation not permitted $ dmesg | tail -n6 [ 7309.831119] ! detected 64-bit platform [ 7309.836299] ! sys_call_table address = ffffffff81801460 [ 7309.836311] ! writing address ffffffff81801558 [ 7309.836359] ! modified sys_call_table[31] = 0000000000012345 [ 7309.836368] ! writing address ffffffff81801558 [ 7309.836424] ! restored sys_call_table[31] = ffffffff812db550
      
      







議論



このレビューは、教科書や行動のガイドとして意図されていません。 ここでは、異なる作成者が使用する基本的に同等のアクションを持つさまざまな手法が体系的に収集されています。



これらの各方法の長所と短所に関する議論を続けることは興味深いでしょう。



または、新しいオプションでアクションを実行するためにリストされた方法を補完します... 5番目、6番目など



(検証、使用、またはさらなる改善のために)議論されたすべてのコードは、 ここまたはここで入手できます



All Articles