web-dev-qa-db-ja.com

ASLRとDEPはどのように機能しますか?

脆弱性の悪用を防ぐという点で、アドレススペースレイアウトランダム化(ASLR)とデータ実行防止(DEP)はどのように機能しますか?バイパスできますか?

115
Polynomial

アドレス空間レイアウトランダム化(ASLR)は、シェルコードが成功しないようにするために使用されるテクノロジです。これは、モジュールの位置と特定のメモリ内構造をランダムにオフセットすることによって行われます。データ実行防止(DEP)は、特定のメモリセクター(たとえば、スタック。実行されません。組み合わせると、シェルコードまたはリターン指向プログラミング(ROP)技術を使用するアプリケーションの脆弱性を悪用することが非常に困難になります。

最初に、通常の脆弱性がどのように悪用される可能性があるかを見てみましょう。詳細はすべてスキップしますが、スタックバッファオーバーフローの脆弱性を使用しているとしましょう。 0x41414141値の大きなblobをペイロードにロードし、eip0x41414141に設定されているため、悪用可能であることがわかります。その後、適切なツール(Metasploitのpattern_create.rbなど)を使用して、eipにロードされている値のオフセットを検出しました。これは、悪用コードの開始オフセットです。確認するには、このオフセットの前に0x41を、オフセットに0x42424242を、オフセットの後に0x43をロードします。

非ASLRおよび非DEPプロセスでは、スタックアドレスはプロセスを実行するたびに同じです。私たちはそれが記憶のどこにあるかを正確に知っています。それでは、上で説明したテストデータでスタックがどのように見えるかを見てみましょう。

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343

ご覧のように、esp000ff6b0に設定されている0x42424242を指しています。これより前の値は0x41で、後の値は0x43です。 000ff6b0に保存されているアドレスにジャンプすることがわかりました。したがって、制御できるメモリのアドレスに設定します。

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

eip000ff6b0-スタック内の次のオフセットに設定されるように、000ff6b4に値を設定しました。これにより、0xcc命令が実行されるint3が実行されます。 int3はソフトウェア割り込みブレークポイントであるため、例外が発生し、デバッガーが停止します。これにより、エクスプロイトが成功したことを確認できます。

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4

これで、ペイロードを変更して、000ff6b4のメモリをシェルコードに置き換えることができます。これでエクスプロイトは終了です。

これらのエクスプロイトの成功を防ぐために、データ実行防止が開発されました。 DEPは、スタックを含む特定の構造を強制的に実行不可としてマークします。これは、CPUがハードウェアレベルで実行権限を適用できるようにするXDビット、EVPビット、またはXNビットとも呼ばれる非実行(NX)ビットによるCPUサポートによって強化されています。 DEPは2004年にLinux(カーネル2.6.8)で導入され、MicrosoftはWinXP SP2の一部として2004年に導入されました。 Apple 2006年にx86アーキテクチャに移行したときにDEPサポートが追加されました。DEPが有効になっていると、以前のエクスプロイトは機能しません。

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4

これは、スタックが実行不可としてマークされているために失敗し、実行しようとしました。これを回避するために、Return-Oriented Programming(ROP)と呼ばれる技術が開発されました。これには、プロセス内の正当なモジュールでROPガジェットと呼ばれる小さなコードスニペットを探すことが含まれます。これらのガジェットは、1つ以上の指示とその後に続くリターンで構成されます。これらをスタック内の適切な値と一緒にチェーンすると、コードを実行できます。

まず、現在のスタックの外観を見てみましょう。

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

000ff6b4でコードを実行できないことがわかっているため、代わりに使用できる正当なコードを見つける必要があります。最初のタスクがeaxレジスタに値を取得することであると想像してください。プロセス内の任意のモジュールのどこかでpop eax; retの組み合わせを検索します。アドレスが見つかったら、たとえば00401f60で、そのアドレスをスタックに入れます。

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

このシェルコードが実行されると、再びアクセス違反が発生します。

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

CPUは次の処理を実行しました。

  • pop eax00401f60命令にジャンプしました。
  • スタックからcccccccceaxにポップしました。
  • retを実行し、43434343eipにポップしました。
  • 43434343は有効なメモリアドレスではないため、アクセス違反が発生しました。

ここで、43434343の代わりに、000ff6b8の値が別のROPガジェットのアドレスに設定されていると想定します。つまり、pop eaxが実行され、次に次のガジェットが実行されます。このようにガジェットをつなぐことができます。私たちの最終的な目標は通常、VirtualProtectなどのメモリ保護APIのアドレスを見つけて、スタックを実行可能としてマークすることです。次に、jmp esp等価命令を実行し、シェルコードを実行するための最後のROPガジェットを含めます。 DEPのバイパスに成功しました!

これらのトリックと戦うために、ASLRが開発されました。 ASLRは、ランダムにメモリ構造とモジュールベースアドレスをオフセットして、ROPガジェットとAPIの場所を推測することを非常に困難にします。

Windows Vistaおよび7では、ASLRはメモリ内の実行可能ファイルとDLLの場所、およびスタックとヒープをランダム化します。実行可能ファイルがメモリに読み込まれると、Windowsはプロセッサのタイムスタンプカウンター(TSC)を取得し、4桁シフトし、除算254を実行してから1を加算します。次に、この数値に64KBが乗算され、実行可能イメージがこのオフセットに読み込まれます。つまり、実行可能ファイルには256の可能な場所があります。 DLLはプロセス間でメモリ内で共有されるため、それらのオフセットは、ブート時に計算されるシステム全体のバイアス値によって決定されます。この値は、MiInitializeRelocations関数が最初に呼び出され、シフトされ、8ビット値にマスクされたときに、CPUのTSCとして計算されます。この値は、ブートごとに1回だけ計算されます。

DLLがロードされると、それらは0x500000000x78000000の間の共有メモリ領域に入ります。ロードされる最初のDLLは常にntdll.dllであり、これは0x78000000 - bias * 0x100000にロードされます。ここで、biasはブート時に計算されるシステム全体のバイアス値です。 ntdll.dllのベースアドレスがわかっている場合、モジュールのオフセットを計算するのは簡単です。モジュールがロードされる順序もランダム化されます。

スレッドが作成されると、それらのスタックベースの場所はランダム化されます。これは、メモリ内の32の適切な場所を見つけ、5ビット値にマスクされた現在のTSCシフトに基づいて1つを選択することによって行われます。ベースアドレスが計算されると、TSCから別の9ビット値が導出され、最終的なスタックベースアドレスが計算されます。これにより、理論上の高度のランダム性が提供されます。

最後に、ヒープの場所とヒープの割り当てがランダム化されます。これは、64ビットを乗算した5ビットTSC派生値として計算され、00000000から001f0000の可能なヒープ範囲を提供します。

これらのメカニズムをすべてDEPと組み合わせると、シェルコードを実行できなくなります。これは、スタックを実行できないためですが、ROP命令がメモリ内のどこに配置されるのかもわかりません。特定のトリックをnopスレッドで実行して確率的エクスプロイトを作成できますが、それらは完全には成功せず、常に作成できるとは限りません。

DEPとASLRを確実にバイパスする唯一の方法は、ポインターリークを経由することです。これは、信頼できる場所にあるスタック上の値を使用して、使用可能な関数ポインターまたはROPガジェットを見つける可能性がある状況です。これが完了すると、両方の保護メカニズムを確実にバイパスするペイロードを作成できる場合があります。

出典:

参考文献:

153
Polynomial

@Polynomialの自己回答を補足するために:DEPは実際には古いNX86マシン(NXビットより古い)に強制できますが、価格が高くなります。

古いx86ハードウェアでDEPを実行する簡単で限定的な方法は、セグメントレジスタを使用することです。このようなシステムの現在のオペレーティングシステムでは、アドレスはフラットな4 GBアドレス空間の32ビット値ですが、内部的に各メモリアクセスは暗黙的に32ビットアドレスを使用しますandと呼ばれる特別な16ビットレジスタ「セグメントレジスタ」。

いわゆるプロテクトモードでは、セグメントレジスタは内部テーブル(「記述子テーブル」-実際にはそのようなテーブルが2つありますが、これは技術的なものです)を指し、テーブルの各エントリはセグメントの特性を指定します。特に、許可されるアクセスのタイプ、およびセグメントのsize。さらに、コード実行は暗黙的にCSセグメントレジスタを使用しますが、データアクセスは主にDSを使用します(およびPushおよびpopオペコードなどのスタックアクセスはSSを使用します)。これにより、オペレーティングシステムはアドレス空間を2つの部分に分割できます。下位アドレスはCSとDSの両方の範囲内にあり、上位アドレスはCSの範囲外です。たとえば、CSによって記述されるセグメントは、サイズが512 MBになるように作成されます。つまり、0x20000000を超えるアドレスはデータとしてアクセスできますが(ベースレジスタとしてDSを使用して読み取りまたは書き込み)、実行の試行ではCSを使用します。この時点で、CPUは例外を発生させます(カーネルはSIGILLやSIGSEGVのような適切な信号に変換します。通常、問題のプロセスの終了を意味します)。

(セグメントはアドレス空間に適用されることに注意してください。 [〜#〜] mmu [〜#〜] は下位層でまだアクティブなので、上記で説明したトリックはプロセスごとです。)

これは安価です。x86ハードウェアdoesは、体系的にセグメントを適用します(そして、最初の80386はすでにそれを行っていました。実際、80286にはすでに境界のあるセグメントがありましたが、16ビットのオフセットしかありません)。正常なオペレーティングシステムはセグメントをオフセット0から開始して4 GBの長さに設定するため、通常はそれらを忘れることがありますが、それ以外の場合は、まだ設定していないオーバーヘッドはありません。ただし、DEPメカニズムとしては柔軟性がありません。あるデータブロックがカーネルから要求された場合、境界が固定されているため、カーネルはこれがコード用かそうでないかを判断する必要があります。特定のページをコードモードとデータモードの間で動的に変換することはできません。

DEPを行うには楽しくても多少コストがかかる方法では、 PaX と呼ばれるものを使用します。それが何をするかを理解するには、いくつかの詳細に入る必要があります。

X86ハードウェアの [〜#〜] mmu [〜#〜] は、メモリ内のテーブルを使用します。これは、アドレス空間内の4 kBのページごとのステータスを示します。アドレス空間は4 GBなので、1048576ページあります。各ページは、サブテーブルの32ビットエントリで記述されます。 1024個のサブテーブルがあり、それぞれが1024個のエントリを保持しています。メインテーブルが1つあり、1024個のエントリが1024個のサブテーブルをポイントしています。各エントリは、ポイントされたオブジェクト(サブテーブル、またはページ)がRAMのどこにあるか、またはそれが存在するかどうか、およびそのアクセス権を示します。問題の根本は、アクセス権が特権レベル(カーネルコードvsユーザーランド)に関するものであり、アクセスタイプに対して1ビットのみであるため、「読み取り/書き込み」または「読み取り専用」を許可することです。 「実行」は、読み取りアクセスの一種と見なされます。したがって、MMUには、データアクセスとは異なる「実行」という概念はありません。読み取り可能なものは実行可能です。

(前の世紀に戻ったPentium Pro以降、x86プロセッサは [〜#〜] pae [〜#〜] と呼ばれるテーブルの別のフォーマットを認識しています。これにより、エントリのサイズが2倍になり、より多くの物理RAMをアドレス指定し、NXビットを追加する余地を残しますが、その特定のビットはハードウェアによって2004年頃にのみ実装されました。

しかし、トリックがあります。 RAMは遅いです。メモリアクセスを実行するには、プロセッサはまずメインテーブルを読み取って調べる必要のあるサブテーブルを見つけ、次にそのサブテーブルに対してもう一度読み取りを行う必要があります。その時点でのみ、プロセッサはメモリアクセスが必要かどうかを認識します。許可されているかどうか、および物理RAMのどこにアクセスされたデータが実際にあるか。これらは完全な依存関係を持つ読み取りアクセスであり(各アクセスは以前の読み取り値に依存します)、これにより完全なレイテンシが発生し、最新のCPUでは数百のクロックサイクルを表すことができます。したがって、CPUには、最後にアクセスされたMMUテーブルエントリを含む特定のキャッシュが含まれます。このキャッシュは Translation Lookaside Buffer です。

80486以降、x86 CPUにはone TLBがありませんが、-twoがあります。キャッシングはヒューリスティックで機能し、ヒューリスティックはアクセスパターンに依存します。コードのアクセスパターンはデータのアクセスパターンと異なる傾向があります。そのため、Intel/AMD /その他の賢い人々は、コードアクセス(実行)専用のTLBとデータアクセス専用のTLBを用意する価値があると感じました。さらに、80486には、特定のエントリをTLBから削除できるオペコード(invlpg)があります。

したがって、アイデアは次のとおりです。2つのTLBに同じエントリの異なるビューを持たせます。すべてのページは、テーブル内(RAM内)で「不在」としてマークされているため、アクセス時に例外がトリガーされます。カーネルは例外をトラップします。例外には、アクセスのタイプに関するいくつかのデータが含まれます。特に、コード実行用かどうかは関係ありません。次に、カーネルは新しく読み取られたTLBエントリ(「存在しない」と表示されているもの)を無効にし、RAMのエントリにアクセスを許可するいくつかの権限を入力してから、必要なタイプの1つのアクセスを強制します(データの読み取りまたはコード実行)、対応するTLBにエントリをフィードしますonlyそのエントリ。次に、カーネルはRAMのエントリを即座に不在に設定し、最後にプロセスに戻ります(例外をトリガーしたopcodeを再試行することに戻ります)。

正味の効果は、実行がプロセスコードに戻ったときに、コードのTLBまたはデータのTLBに適切なエントリが含まれているが、他のTLB しないおよびnot RAMのテーブルがまだ「存在しない」と言っているため。その時点で、カーネルは、データアクセスを許可するかどうかに関係なく、実行を許可するかどうかを決定する立場にあります。したがって、NXのようなセマンティクスを適用できます。

悪魔は細部に隠れています。この場合、悪魔の軍団全体が入る余地があります。このようなハードウェアでのダンスは、適切に実装するのが容易ではありません。特にマルチコアシステムでは。

オーバーヘッドは次のとおりです。アクセスが実行され、TLBに関連するエントリが含まれていない場合、RAMのテーブルにアクセスする必要があり、それだけで数百サイクルが失われることになります。そのコストに、PaXは例外のオーバーヘッドと正しいTLBを満たす管理コードを追加し、「数百サイクル」を「数千サイクル」に変えます。幸い、TLBミスは正しいです。 PaXの人々は、大規模なコンパイルジョブで 測定 2.7%のスローダウンがあると主張しています(ただし、これはCPUタイプによって異なります)。

NXビットにより、これらすべてが廃止されます。PaXパッチセットには、ASLRなどの他のセキュリティ関連機能も含まれています。新しい公式カーネルの機能。

40
Thomas Pornin