疑似ファイルの奇妙な癖
/proc/*/mem
は、そのパンチの効いたセマンティクスにあります。ターゲット仮想メモリが書き込み不可としてマークされている場合でも、このファイルを介した書き込み操作は成功します。これは意図的なものであり、この動作は JuliaJITコンパイラやrrデバッガなどのプロジェクトで積極的に使用されています。
しかし、問題は次のとおりです。特権コードは仮想メモリのアクセス許可に従いますか?ハードウェアはカーネルメモリアクセスにどの程度影響しますか?
これらの質問に答え、オペレーティングシステムとそれが実行されるハードウェアとの間の相互作用の微妙な違いを検討します。カーネルに影響を与える可能性のあるプロセッサの制限を調べて、カーネルがそれらを回避する方法を見てみましょう。
libcに/ proc / self / memをパッチします
このパンチの効いたセマンティクスはどのように見えますか?コードを考えてみましょう:
#include <fstream>
#include <iostream>
#include <sys/mman.h>
/* Write @len bytes at @ptr to @addr in this address space using
* /proc/self/mem.
*/
void memwrite(void *addr, char *ptr, size_t len) {
std::ofstream ff("/proc/self/mem");
ff.seekp(reinterpret_cast<size_t>(addr));
ff.write(ptr, len);
ff.flush();
}
int main(int argc, char **argv) {
// Map an unwritable page. (read-only)
auto mymap =
(int *)mmap(NULL, 0x9000,
PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mymap == MAP_FAILED) {
std::cout << "FAILED\n";
return 1;
}
std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";
getchar();
// Try to write to the unwritable page.
memwrite(mymap, "\x40\x41\x41\x41", 4);
std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
getchar();
std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";
getchar();
// Try to writ to the text segment (executable code) of libc.
auto getchar_ptr = (char *)getchar;
memwrite(getchar_ptr, "\xcc", 1);
// Run the libc function whose code we modified. If the write worked,
// we will get a SIGTRAP when the 0xcc executes.
getchar();
}
ここでは
/proc/self/mem
、書き込み不可能な2つのメモリページに書き込むために使用され ます。1つ目はコード自体を含み、2つ目は
libc
(関数
getchar
)に属し ます。最後の部分はさらに興味深いものです。コードはバイト0xcc(x86-64アプリケーションのブレークポイント)を書き込みます。これを実行すると、カーネルがプロセスにSIGTRAPを提供します。これは文字通りlibc実行可能ファイルを変更します。そして、次の呼び出し
getchar
でSIGTRAPを取得すると、レコードが成功したことがわかります。
プログラムを実行すると、次のようになります。
動作します!中央には、値0x41414140が正常に書き込まれ、メモリから読み取られたことを証明する式が出力されます。最後の出力は、パッチを適用した後、呼び出しの結果としてプロセスがSIGTRAPを受信したことを示しています
getchar
。
ビデオで:
この機能がユーザースペースの観点からどのように機能するかを見てきました。もっと深く掘り下げましょう。これがどのように機能するかを完全に理解するには、ハードウェアがどのようにメモリ制約を課すかを調べる必要があります。
装置
x86-64プラットフォームには、メモリにアクセスするカーネルの機能を制御する2つのプロセッサ設定があります。これらは、メモリ管理ユニット(MMU)によって使用されます。
最初の設定は書き込み保護ビット(CR0.WP)です。Intelのマニュアル(第3巻、セクション2.5)から、次のことがわかります。
書き込み保護(16番目のビットCR0)。指定すると、スーパーバイザーレベルのプロシージャが書き込み保護されたページに書き込むのを防ぎます。ビットが空の場合、スーパーバイザーレベルのプロシージャは書き込み保護されたページに書き込むことができます(U / Sビット設定に関係なく。セクション4.1.3および4.6を参照)。
これにより、カーネルが書き込み保護されたページに書き込むことができなくなり ます。これは、デフォルトで当然許可されています。
2番目の設定は、スーパーバイザーモードアクセス防止(SMAP)(CR4.SMAP)です。第3巻、セクション4.6の完全な説明は冗長です。つまり、SMAPは、カーネルからユーザースペースメモリへの書き込みまたはユーザースペースメモリからの読み取り機能を完全に奪います。これにより、実行中にカーネルが読み取らなければならない悪意のあるデータでユーザースペースを氾濫させるエクスプロイトが防止されます。
カーネルコードが承認されたチャネルのみを使用する場合(
copy_to_user
など)、SMAPは安全に無視できます。これらの関数は、メモリへのアクセスの前後に自動的にSMAPを使用します。書き込み保護はどうですか?
CR0.WPが指定されていない場合、
/proc/*/mem
カーネル実装 は実際に、書き込み保護されたユーザースペースメモリに不用意に書き込む可能性があります。
ただし、CR0.WP は起動時に設定され、通常はシステムの動作時間全体にわたって存続します。この場合、書き込もうとするとページフォールトが発生します。これはセキュリティツールというよりはコピーオンライトツールであるため、カーネルに実際の制限を課すことはありません。言い換えると、不便な障害処理が必要であり、これは特定のビットには必要ありません。
ここで実装を理解しましょう。
/ proc / * / memのしくみ
/proc/*/mem
これは、fs / proc /base.cに実装されてい ます。
構造体
file_operations
にはハンドラー関数が含まれており、mem_rw()関数 は書き込みハンドラーを完全にサポートしています。
mem_rw()
書き込み操作にaccess_remote_vm()を使用し ます。そして
access_remote_vm()
それはこれを行います:
get_user_pages_remote()
ターゲット仮想アドレスに一致する物理フレームを見つけるために呼び出します。kmap()
このフレームをカーネル仮想アドレス空間で書き込み可能としてマークするための呼び出し。copy_to_user_page()
書き込み操作の最終実行を要求します。
この実装は、書き込み不可能なユーザースペースに書き込むカーネルの機能の問題を完全に回避します。仮想メモリサブシステムに対するカーネルの制御により、MMUを完全にバイパスできるため、カーネルは独自の書き込み可能なアドレス空間に簡単に書き込むことができます。したがって、CR0.WPの説明は無関係になります。
各ステップを
見てみ ましょう 。get_user_pages_remote()
MMUをバイパスするには、カーネルは、アプリケーションのハードウェアでMMUが実行することを手動で実行する必要があります。まず、ターゲット仮想アドレスを物理アドレスに変換する必要があります。これは関数のファミリーによって行われます
get_user_pages()
..。それらはページテーブルをトラバースし、指定された範囲の仮想アドレスに一致する物理メモリフレームを探します。
呼び出し元はコンテキストを提供し、フラグを使用して動作を変更します
get_user_pages()
。
FOLL_FORCE
送信されているフラグは 特に興味深いもの
mem_rw()
です。このフラグは、check_vma_flags(アクセスチェックロジック
get_user_pages()
)をトリガーし て、書き込み不可能なページへの書き込みを無視し、検索を続行します。 「パンチの効いた」セマンティクスは完全に
FOLL_FORCE
(私のコメント)を指し ます:
static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
{
[...]
if (write) { // If performing a write..
if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..
if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..
return -EFAULT; // Return an error
[...]
return 0; // Otherwise, proceed with lookup
}
get_user_pages()
また、コピーオンライト(CoW)セマンティクスにも準拠しています。書き込み不可能なページテーブルへの書き込みが指定されている場合、
handle_mm_fault
メインページエラーハンドラーを呼び出すことにより、ページ障害がエミュレートされ ます。これにより、適切なコピーオンライト処理ルーチンが開始され、
do_wp_page
必要に応じてページがコピーされます。したがって、エントリスルー
/proc/*/mem
がlibcなどのプライベート共有マッピングによって実行される場合、それらはプロセス内でのみ表示されます。
kmap()
物理フレームが見つかったら、書き込み可能なカーネルの仮想アドレス空間にマップする必要があります。これはの助けを借りて行われ
kmap()
ます。
64ビットx86プラットフォームでは、すべての物理メモリは、カーネルの仮想アドレス空間のインラインマッピング領域を介してマッピングされます。この場合、それ
kmap()
は非常に簡単に機能します。このフレームがマップされる仮想アドレスを計算するために、線形マッピングの開始アドレスをフレームの物理アドレスに追加するだけで済みます。
32ビットx86プラットフォームでは、インラインマッピングに物理メモリのサブセットが含まれているため、関数
kmap()
は、highmemメモリを割り当ててページテーブルを操作することにより、フレームをマッピングする必要がある場合があります。
どちらの場合も、 ラインマッピングと highmemマッピングは保護されて実行されます。 書き込みを可能にするPAGE_KERNEL。
copy_to_user_page()
最後のステップは、書き込みを実行することです。これは、
copy_to_user_page()
本質的にmemcpyであるものを使用して行われ ます。これは、ターゲットがからの書き込み可能なマッピングであるために機能し
kmap()
ます。
討論
したがって、最初に、カーネルは、プログラムに属するメモリページテーブルを使用して、ユーザー空間のターゲット仮想アドレスを対応する物理フレームに変換します。次に、カーネルはこのフレームを独自の書き込み可能な仮想空間にマップします。最後に、単純なmemcpyで書き込みます。
驚くべきことに、ここではCR0.WPは使用されていません。 実装は、ユーザースペースポインタを介してメモリにアクセスする必要がないという事実を利用することにより、この点をエレガントにバイパスします。カーネルは仮想メモリを完全に制御できるため、物理フレームを任意の解像度で独自の仮想アドレス空間に再マップし、必要な処理を実行できます。
メモリのページを保護するアクセス許可は、ページに関連付けられた物理フレームではなく、そのページへのアクセスに使用される仮想アドレスに関連していることに注意することが重要 です。メモリ許可表記は、物理メモリではなく、仮想メモリのみを指します。
結論
実装のパンチの効いたセマンティクスの詳細を調べること
/proc/*/mem
で、コアとプロセッサの関係を反映できます。一見したところ、書き込み不可能なメモリに書き込むカーネルの機能は、疑問を投げかけます。プロセッサはカーネルのメモリアクセスにどの程度影響を与えることができますか?このマニュアルでは、カーネルのアクションを制限できる制御メカニズムについて説明しています。しかし、詳しく調べてみると、制限はせいぜい表面的なものです。これらは回避するための単純な障害です。