Linuxカーネルの書き込み保護された領域を変更するコーシャーの方法

カーネル内の何かをその場で変更する必要に少なくとも一度遭遇した人は、コードと一部のデータを格納するカーネルメモリページが読み取り専用としてマークされ、書き込み保護されているため、この問題は詳細な調査が必要であることを知っています!



x86の場合、よく知られている解決策は、レジスタCR0のWPビットをクリアして、ページ保護を一時的に無効にすることです。 ただし、ページ保護は多くのカーネルメカニズムの基盤であるため、これは慎重に使用する必要があります。 さらに、さまざまな不快な状況が発生する可能性がある場合は、SMPシステムでの作業の特性を考慮する必要があります。







ページ保護をオフにする





x86アーキテクチャには特別な防御メカニズムがあり、書き込み保護されたメモリ領域への書き込みを試みると例外が発生する可能性があります。 このメカニズムは「ページ防御」と呼ばれ、 COWなどの多くのカーネル機能の実装の基礎となります。 この状況でのプロセッサの動作は、CR0レジスタのWPビットによって決定され、ページへのアクセス権は、対応するPTE記述子構造に記述されます。 レジスタCR0のWPビットが設定されている場合、書き込み保護されたページへの書き込み(PTEのRWビットがリセットされる)により、プロセッサは対応する例外(#GP)を生成します。



この問題の最も簡単な解決策は、CR0レジスタのWPビットをリセットすることにより、ページ保護を一時的に無効にすることです。 このソリューションには場所がありますが、注意して適用する必要があります。前述のように、ページメカニズムは多くのコアメカニズムの基礎であるためです。 さらに、SMPシステムでは、プロセッサの1つで実行され、同じ場所でWPビットを削除するスレッドが中断され、別のプロセッサに転送される可能性があります。



それでも、本当にしたい場合は、 ここで推奨されている プリエンプションを無効にしてこれを行う必要があります



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(); preempt_enable_no_resched(); return cr0 ^ X86_CR0_WP; }
      
      







マッピングの使用





より良い、そして十分に普遍的なのは、一時的なディスプレイを作る方法です。 MMUの特性により、物理メモリフレームごとに、異なる属性を持つ複数の記述子を作成できます。 これにより、ターゲットメモリ領域の書き込み可能なディスプレイを作成できます。 このメソッドはKspliceプロジェクト(githubのfork )で使用されます。 以下は、そのようなマップを作成するmap_writable関数です。



 /* * map_writable creates a shadow page mapping of the range * [addr, addr + len) so that we can write to code mapped read-only. * * It is similar to a generalized version of x86's text_poke. But * because one cannot use vmalloc/vfree() inside stop_machine, we use * map_writable to map the pages before stop_machine, then use the * mapping inside stop_machine, and unmap the pages afterwards. */ 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); }
      
      







この関数を使用すると、任意のメモリ領域の書き込み可能なディスプレイを作成できます。 この方法で作成された領域はvfree関数を使用して解放され、その引数はページの境界に揃えられたアドレス値である必要があります。



車を止めろ!





カーネルコードの変更を安全にする最後の要素は、 stop_machineメカニズムです。



 #include <linux/stop_machine.h> int stop_machine(int (*fn)(void *), void *data, const struct cpumask *cpus)
      
      







一番下の行は、 stop_machine



が、実行時にアクティブなプロセッサの特定のセットでfn



関数を実行することcpumask



。これは、対応するcpumask



マスクによって設定されます。 これにより、このメカニズムを使用してカーネルコードを変更できます。 適切なマスクを自動的に設定すると、実行が変更されたコードに影響を与える可能性があるカーネルスレッドを追跡する必要がなくなります。



stop_machine



の制限のうち、関数をアトミックコンテキストで実行する必要があることに注意する価値があります。これにより、 vmapを介して一時表示を作成する前述のメカニズムを使用する可能性が自動的に除外されます。 ただし、 stop_machine



呼び出す前に必要なマッピングを準備できるため、この事実は重要ではありません。



All Articles