web-dev-qa-db-ja.com

Linuxが実行可能コードセグメントのアドレスをランダム化しないのはなぜですか?

私は最近、ASLR(アドレス空間のランダム化)がLinuxでどのように機能するかについて学びました。少なくともFedoraとRed Hat Enterprise Linuxでは、2種類の実行可能プログラムがあります。

  • 位置独立実行可能ファイル(PIE)は、強力なアドレスのランダム化を受け取ります。明らかに、すべての場所は、プログラムごとに個別にランダム化されています。明らかに、ネットワークに面するデーモンは、完全なランダム化を確実に受け取るために、PIEとして(-pie -fpieコンパイラフラグを使用して)コンパイルする必要があります。

  • 他の実行可能ファイルは、部分的なアドレスのランダム化を受け取ります。実行可能コードセグメントはランダム化されていません。固定された予測可能なアドレスにあり、すべてのLinuxシステムで同じです。対照的に、共有ライブラリはランダム化されています。それらは、システム上のそのようなプログラムすべてに対して同じであるランダムな位置にロードされます。

非PIE実行可能ファイルの共有ライブラリのランダム化の形式が弱い理由は理解できると思います(これは、実行可能ファイルのリンクとロードを高速化するプリリンクに必要です)。また、PIE以外の実行可能ファイルの実行可能セグメントがまったくランダム化されていない理由も理解していると思います。実行可能コードセグメントの場所をランダム化できるように、プログラムをPIEとしてコンパイルする必要があるためです。

それでも、実行可能コードセグメントの場所をランダム化しないでおくと、セキュリティリスクになる可能性があるため(たとえば、ROP攻撃が容易になります)、すべてのバイナリに完全なランダム化を提供することが可能かどうかを理解することをお勧めします。

では、すべてをPIEとしてコンパイルしない理由はありますか? PIEとしてコンパイルするとパフォーマンスのオーバーヘッドはありますか?もしそうなら、特にアドレスのランダム化が最も効果的であるx86_64で、異なるアーキテクチャでのパフォーマンスのオーバーヘッドはどのくらいですか?


参照:

30
D.W.

詳細はアーキテクチャによって大きく異なりますが、ここで言うことは、32ビットx86、64ビットx86にも同様に当てはまりますが、ARMおよびPowerPC:すべてのアーキテクチャについて同じ問題に直面しています設計者は同様のソリューションを使用しています。


(大まかに言えば)アセンブリレベルには、「位置に依存しない」システムに関連する4種類の「アクセス」があります。関数呼び出しcall opcodes)およびdata accesss、そして両方とも同じオブジェクト内のエンティティをターゲットにすることができます(オブジェクトは "共有オブジェクト」、つまりDLL、または実行可能ファイル自体)、または別のオブジェクト内。スタック変数へのデータアクセスはここでは関係ありません。 グローバル変数または静的定数データへのデータアクセスについて話している(特に、ソースレベルで、リテラル文字列であるように見えるものの内容) 。 C++のコンテキストでは、仮想メソッドは、内部的には特別なテーブル( "vtables"と呼ばれる)の関数ポインターによって参照されます。この答えの目的のために、メソッドがコードであっても、これらはdataアクセスでもあります。

callオペコードは、relativeであるターゲットアドレスを使用します。これは、現在の命令ポインター間で計算されたオフセットです(技術的には、最初のバイトcall opcode)への引数と呼び出し先アドレスの後。つまり、同じオブジェクト内の関数呼び出しは(静的)リンク時に完全に解決できます。動的シンボルテーブルには表示されず、「位置に依存しない」。一方、他のオブジェクトへの関数呼び出し(クロスDLL呼び出し、または実行可能ファイルからDLLへの呼び出し)は、ダイナミックリンカーによって処理される間接処理を経由する必要があります。 callオペコードは「どこかに」ジャンプする必要があり、動的リンカーは動的に調整したいと考えています。このフォーマットは次の2つの特性を達成しようとします。

  • 遅延リンク:呼び出しターゲットは、最初に使用されたときにのみ検索および解決されます。
  • 共有ページ:可能な限り、メモリ内の構造は実行可能ファイルの対応するバイトと同一に保つ必要があり、複数の呼び出しで共有を促進する必要があります(2つのプロセスが同じDLLをロードする場合、コードはRAMに1回だけ存在する)およびより簡単なページング(RAMがタイトである場合、ファイル内のデータのチャンクの変更されていないコピーであるページは、物理RAMから追い出すことができるため、自由自在にリロード)。

共有はページごとに行われるため、call引数(callオペコードの後の数バイト)を動的に変更することは避けてください。代わりに、コンパイルされたコードはGlobal Offsets Table(またはいくつか-私は少し単純化します)を使用します。基本的に、callは実際の呼び出しを行う小さなコードにジャンプし、動的リンカーによる変更の対象になります。特定のオブジェクトのそのような小さなラッパーはすべて、動的リンカーが変更するページに一緒に格納されます。これらのページはコードからの固定オフセットにあるため、callへの引数は静的リンク時に計算され、ソースファイルから変更する必要はありません。オブジェクトが最初にロードされるとき、すべてのラッパーは、最初の呼び出し時にリンクを行う動的リンカー関数を指します。この関数は、後続の呼び出しのために、解決されたターゲットを指すようにラッパー自体を変更します。アセンブリレベルのジャグリングは複雑ですが、うまく機能します。

データアクセスは同様のパターンに従いますが、相対アドレス指定はありません。つまり、データアクセスは絶対アドレスを使用します。そのアドレスは、レジスター内で計算され、アクセスに使用されます。 CPUのx86行canは、絶対アドレスをオペコードの一部として直接持っています。固定サイズのオペコードを持つRISCアーキテクチャの場合、アドレスは2つまたは3つの連続した命令としてロードされます。

PIE以外の実行可能ファイルでは、データ要素のターゲットアドレスは静的リンカーに認識されています。静的リンカーは、アクセスを実行するオペコードに直接ハードコードできます。 PIE実行可能ファイルまたはDLLでは、実行前にターゲットアドレスが不明であるため、これは不可能です(RAMに読み込まれる他のオブジェクトやASLRにも依存します)。代わりに、バイナリコードはGOTを再度使用する必要があります。 GOTアドレスは動的にベースレジスタに計算されます。 32ビットx86では、ベースレジスタは従来どおり%ebxであり、次のコードが一般的です。

    call nextaddress
nextaddress:
    popl %ebx
    addl somefixedvalue, %ebx

最初のcallは、次のオペコードにジャンプするだけです(したがって、ここでの相対アドレスはゼロです)。これはcallであるため、戻りアドレス(poplオペコードのアドレスも)をスタックにプッシュし、poplがそれを抽出します。その時点で、%ebxにはpoplのアドレスが含まれているため、単純な追加によってその値が変更され、GOTの先頭を指すようになります。その後、%ebxに対してデータアクセスを実行できます。


では、実行可能ファイルをPIEとしてコンパイルすると何が変更されますか?実際にはあまりありません。 「PIE実行可能ファイル」とは、メインの実行可能ファイルをDLLにし、他のDLLと同じようにロードしてリンクすることを意味します。これは次のことを意味します。

  • 関数呼び出しは変更されません。
  • データアクセスメイン実行可能ファイル内のコードから、メイン実行可能ファイル内にあるデータ要素へのアクセスには、追加のオーバーヘッドが発生します。他のすべてのデータアクセスは変更されません。

データアクセスのオーバーヘッドは、GOTを指す従来のレジスターの使用によるものです。1つの追加の間接参照、この機能に使用される1つのレジスター(これは、32ビットx86などのレジスター不足のアーキテクチャーに影響します)、および再計算する追加のコードGOTへのポインタ。

ただし、ローカル変数へのアクセスと比較すると、データアクセスはすでに多少「遅い」ので、コンパイルされたコードはすでにそのようなアクセスをキャッシュします(変数値はレジスタに保持され、必要な場合にのみフラッシュされます。フラッシュされた場合でも、変数addressもレジスターに保持されます)。これは、グローバル変数がスレッド間で共有されるという事実により、さらに大きくなります。そのため、そのようなグローバルデータを使用するほとんどのアプリケーションコードは、読み取り専用の方法でのみ使用します(書き込みが実行されるとき、それらはミューテックスの保護の下で行われます) 、そしてミューテックスを取得すると、とにかくはるかに大きなコストがかかります)。ほとんどのCPU集中型コードは、レジスターとスタック変数で機能し、コードを位置に依存しないようにすることによる影響を受けません。

多くの場合、コードをPIEとしてコンパイルすると、通常のコードではsizeオーバーヘッドが約2%必要になるため、コードの効率に測定可能な影響はありません。問題(その数値はOpenBSDの開発に携わる人々と話し合って得たものです。「+ 2%」は、ベアフロンシステムをブートフロッピーディスクに収めようとする非常に特殊な状況での問題でした)。


ただし、C/C++以外のコードでは、PIEに問題がある可能性があります。コンパイルされたコードを生成するとき、GOTを見つけるコードチャンクを含めるために、コンパイラは、DLLまたは静的実行可能ファイルのどちらであるかを知る必要があります。多くのパッケージは含まれません。問題が発生する可能性のあるLinux OSですが、EmacsはLISPのダンプとリロード機能を備えているため、問題の候補になります。

Python、Java、C#/。NET、Ruby ...のコードは、このすべての対象外です。 PIEは、CまたはC++の「従来の」コード用です。

26
Thomas Pornin

一部のLinuxディストリビューションがすべての実行可能ファイルを位置独立型実行可能ファイル(PIE)としてコンパイルすることをためらう可能性がある理由の1つは、実行可能コードがランダム化されるため、パフォーマンスに関する懸念のためです。パフォーマンスに関する懸念事項は、問題がなくてもパフォーマンスを心配することがあるということです。したがって、実際のコストの詳細な測定値を取得しておくと便利です。

幸い、次の論文では、実行可能ファイルをPIEとしてコンパイルするコストの測定値をいくつか示しています。

このペーパーでは、CPUを集中的に使用する一連のプログラム(つまり、SPEC CPU2006ベンチマーク)でPIEを有効にすることによるパフォーマンスオーバーヘッドを分析しました。この実行可能ファイルのクラスは、PIEによるパフォーマンスオーバーヘッドの最悪を示すと予想されるため、これにより、潜在的なパフォーマンス推定の控えめな最悪のケースの見積もりが得られます。

この論文の主な調査結果をまとめると、次のようになります。

  • 32ビットx86アーキテクチャでは、パフォーマンスのオーバーヘッドがかなり大きくなる可能性があります。SPECCPU2006ベンチマーク(CPU集中型プログラム)の場合は平均で約10%の速度低下になり、一部の場合は最大25%程度の速度低下になりますプログラム。

  • 64ビットx64アーキテクチャでは、CPUを集中的に使用するプログラムでのパフォーマンスオーバーヘッドははるかに小さく、平均速度は約3%低下します。パフォーマンスのオーバーヘッドは、人々が使用する多くのプログラムではさらに少なくなる可能性があります(多くのプログラムはCPUを集中的に使用しないため)。

これは、64ビットアーキテクチャ上のすべての実行可能ファイルに対してPIEを有効にすることは、セキュリティにとって妥当なステップであり、パフォーマンスへの影響は非常に小さいことを示唆しています。ただし、32ビットアーキテクチャ上のすべての実行可能ファイルに対してPIEを有効にすると、コストがかかりすぎます。

8
D.W.

位置依存の実行可能ファイルがランダム化されない理由はかなり明白です。

「位置依存」とは、少なくとも一部のアドレスがハードコードされていることを意味します。特に、これはブランチアドレスに適用される場合があります。実行可能セグメントのベースアドレスを移動すると、すべての分岐先も移動します。

このようなハードコードされたアドレスには2つの選択肢があります。IP相対アドレスで置き換える(CPUが実行時に絶対アドレスを判別できるようにする)か、ロード時に修正する(ベースアドレスがわかっている場合)かのいずれかです。

もちろん、そのような実行可能ファイルを生成できるコンパイラが必要です。

2
MSalters