この記事では、2018年12月の2.30リリースにGNU ldで追加された小さなセキュリティ機能に焦点を当てます。 ロシア語では、この改善はopennetで次の注釈付きで言及されました 。
「-z個別コード」モード。サイズとメモリ消費をわずかに増加させますが、実行可能ファイルのセキュリティが向上します
それを理解しましょう。 私たちが話しているセキュリティ問題の種類と解決策を説明するために、バイナリ脆弱性エクスプロイトの一般的な機能から始めましょう。
悪用制御フローの問題
攻撃者は、さまざまな脆弱性の助けを借りて、プログラムにデータを転送し、この方法で操作できます。配列境界を超えるインデックスによる書き込み、文字列の安全でないコピー、リリース後のオブジェクトの使用。 このようなエラーは、CおよびC ++プログラムコードでは一般的であり、プログラムの特定の入力データでメモリが破損する可能性があります。
CWE-20:不適切な入力検証
CWE-118:インデックス可能なリソースへの不正なアクセス(「範囲エラー」)
CWE-119:メモリバッファーの境界内での操作の不適切な制限
CWE-120:入力サイズをチェックしないバッファーコピー(「クラシックバッファーオーバーフロー」)
CWE-121:スタックベースのバッファオーバーフロー
CWE-122:ヒープベースのバッファオーバーフロー
CWE-123:書き込み場所条件
CWE-124:バッファーアンダーライト(「バッファーアンダーフロー」)
CWE-125:範囲外読み取り
CWE-126:バッファーのオーバーリード
CWE-127:バッファーアンダーリード
CWE-128:ラップアラウンドエラー
CWE-129:配列インデックスの不適切な検証
CWE-130:長さパラメーターの不整合の不適切な処理
CWE-131:バッファーサイズの誤った計算
CWE-134:外部制御のフォーマット文字列の使用
CWE-135:マルチバイト文字列の長さの誤った計算
CWE-170:不適切なヌル終了
CWE-190:整数オーバーフローまたはラップアラウンド
CWE-415:ダブルフリー
CWE-416:解放後使用
CWE-476:NULLポインター逆参照
CWE-787:境界外書き込み
CWE-824:初期化されていないポインターのアクセス
...
メモリ破損のような脆弱性の典型的な悪用要素は、メモリ内のポインタを上書きすることです。 その後、プログラムはポインタを使用して、別のコードに制御を渡します。つまり、別のモジュールからクラスメソッドまたは関数を呼び出し、関数から戻ります。 また、ポインターが上書きされたため、コントロールは攻撃者に傍受されます。つまり、攻撃者が準備したコードが実行されます。 これらの手法のバリエーションや詳細に興味がある場合は、 ドキュメントを読むことをお勧めします 。
このようなエクスプロイトの操作のこの一般的な瞬間は知られており、ここでは攻撃者にとって長い間障壁が置かれています。
- 制御を渡す前にポインターの整合性をチェックする:スタックCookie、制御フローガード、ポインター認証
- コードとデータを使用したセグメントアドレスのランダム化:アドレス空間レイアウトのランダム化
- コードがコードセグメント外で実行されないようにする:実行可能領域の保護
次に、後者のタイプの保護に焦点を当てます。
実行可能スペース保護
プログラムメモリは異種であり、読み取り、書き込み、実行の各権限を持つセグメントに分割されます。 これは、ページテーブルのアクセスフラグでメモリページをマークするプロセッサの機能によって保証されます。 保護の概念は、コードとデータの厳密な分離に基づいています:処理中に攻撃者から受信したデータは、 実行不可能なセグメント(スタック、ヒープ)に配置し、プログラム自体のコードは別々の不変のセグメントに配置する必要があります。 したがって、これにより、攻撃者はメモリ内に無関係なコードを配置して実行することができなくなります。
データセグメントでのコード実行の禁止を回避するために、コード再利用技術が使用されます。 つまり、攻撃者は実行可能ページにあるコードフラグメント(以降、ガジェットと呼びます)に制御を渡します。 この種の技術は、難易度がさまざまで、昇順です。
- 攻撃者に十分な機能を実行する関数に制御を移す:任意のシェルコマンドを実行する制御された引数を持つシステム()関数(ret2libc)
- 保護を無効にするか、メモリの一部を実行可能にする(たとえば、
mprotect()
呼び出すmprotect()
ガジェットの機能またはチェーンに制御を転送し、その後に任意のコードを実行する - ガジェットの長いチェーンを使用したすべての目的のアクションの実行
したがって、攻撃者は、あるボリュームまたは別のボリュームで既存のコードを再利用するタスクに直面しています。 これが1つの関数に戻るよりも複雑な場合、ガジェットのチェーンをコンパイルする必要があります 。 実行可能セグメントでガジェットを検索するには、ツールropper 、 ropgadgetがあります。
ホールREAD_IMPLIES_EXEC
ただし、データのあるメモリ領域が実行可能な場合があり、上記のコードとデータの分離の原則に明らかに違反しています。 このような場合、攻撃者はコードを再利用するためのガジェットや機能を見つける手間を省きます。 この種の興味深い発見は、実行可能なスタックと1つの「産業用ファイアウォール」上のすべてのデータセグメントでした。
リスト/proc/$pid/maps
:
00008000-00009000 r-xp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00010000-00011000 rwxp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00011000-00032000 rwxp 00000000 00:00 0 [heap] 40000000-4001f000 r-xp 00000000 1f:02 429 /lib/ld-linux.so.2 4001f000-40022000 rwxp 00000000 00:00 0 40027000-40028000 r-xp 0001f000 1f:02 429 /lib/ld-linux.so.2 40028000-40029000 rwxp 00020000 1f:02 429 /lib/ld-linux.so.2 4002c000-40172000 r-xp 00000000 1f:02 430 /lib/libc.so.6 40172000-40179000 ---p 00146000 1f:02 430 /lib/libc.so.6 40179000-4017b000 r-xp 00145000 1f:02 430 /lib/libc.so.6 4017b000-4017c000 rwxp 00147000 1f:02 430 /lib/libc.so.6 4017c000-40b80000 rwxp 00000000 00:00 0 be8c2000-be8d7000 rwxp 00000000 00:00 0 [stack]
ここでは、テストユーティリティプロセスのメモリカードが表示されます。 マップは、メモリ領域-テーブル行で構成されます。 最初に、右の列に注意してください-エリアの内容(コードセグメント、関数ライブラリのデータまたはプログラム自体)またはそのタイプ(ヒープ、スタック)を説明します。 左側には、各メモリ領域が占有するアドレスの範囲と、さらにアクセス権フラグr(読み取り)、w(書き込み)、x(実行)が順番に表示されます。 これらのフラグは、これらのアドレスでメモリの読み取り、書き込み、および実行を試みるときのシステムの動作を決定します。 指定されたアクセスモードに違反すると、例外が発生します。
プロセス内のほとんどすべてのメモリ(スタック、ヒープ、すべてのデータセグメント)が実行可能であることに注意してください。 これは問題です。 明らかに、メモリのrwxページの存在は、攻撃者にとって、データ(パケット、ファイル)を処理のためにデータを転送するときにコードが取得する任意の場所で、そのようなプロセスでコードを自由に実行できるため、生活を楽にします。
ハードウェアのデータページでのコード実行の禁止をサポートする最新のデバイスでこのような状況が発生したのはなぜですか?企業および産業ネットワークのセキュリティはデバイスに依存しており、問題とその解決策は非常に長い間知られていますか?
この状況は、プロセスの初期化(スタックの割り当て、ヒープ、メインELFのロードなど)中および核プロセス呼び出しの実行中のカーネルの動作によって決まります。 これに影響する重要な属性は、性格フラグREAD_IMPLIES_EXEC
です。 このフラグの効果は、読み取り可能なメモリも実行可能になることです。 いくつかの理由により、プロセスにフラグを設定できます。
- 非常に興味深いメカニズムを実装するために、レガシーはELFヘッダーのソフトウェアフラグによって明示的に要求できます:スタック上のスプリングボード( 1、2、3 )
- 親から子プロセスに継承できます。
- カーネルによってすべてのプロセスに個別にインストールできます! まず、アーキテクチャが非実行可能メモリをサポートしていない場合。 第二に、 念のため、他の古代の松葉杖をサポートします。 このコードはカーネル2.6.32(ARM)にあり、非常に長い寿命がありました。 これはまさに私たちのケースでした。
ELF画像でガジェットを見つけるためのスペース
関数ライブラリとプログラム実行可能ファイルはELF形式です。 gccコンパイラーは、言語構造をマシンコードに変換して1つのセクションに配置し、このコードが他のセクションで操作するデータを配置します。 多くのセクションがあり、ldリンカーによってセグメントにグループ化されます。 したがって、ELFには、セクションのテーブルとセグメントのテーブルという2つの表現を持つプログラムイメージが含まれています。
$ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x804bee9 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 RE 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x1e40c 0x1e40c RE 0x1000 LOAD 0x01ef00 0x08067f00 0x08067f00 0x00444 0x01078 RW 0x1000 DYNAMIC 0x01ef0c 0x08067f0c 0x08067f0c 0x000f0 0x000f0 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x018b74 0x08060b74 0x08060b74 0x00814 0x00814 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x01ef00 0x08067f00 0x08067f00 0x00100 0x00100 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
ここでは、ELFイメージのセグメントへのセクションのマッピングを確認できます。
セクションテーブルは、ユーティリティがプログラムとライブラリを分析するために使用されますが、ローダーがELFをプロセスメモリに投影するためには使用されません。 セクションテーブルは、セグメントテーブルよりも詳細にELF構造を説明します。 複数のセクションを1つのセグメント内に含めることができます。
インメモリELFイメージは、 セグメントテーブルの内容に基づいてELFローダーによって作成されます。 パーティションテーブルは、ELFをメモリにロードするために使用されなくなりました。
たとえば、自然には、ARMアーキテクチャ用のELF ld.soローダー用のDebian開発者パッチがあります。これは、 SHT_ARM_ATTRIBUTESなどの特別な「.ARM.attributes」セクションを探しており、そのようなシステムで切断されたセクションテーブルを持つバイナリはロードされません...
ELFセグメントには、セグメントがメモリ内で持つ許可を決定するフラグがあります。 従来、GNU / Linuxのほとんどのソフトウェアは、2つのPT_LOAD
(メモリロード)セグメントがセグメントテーブルで宣言されるように構築されました-上記のリストのように:
RE
フラグ付きのセグメント
1.1。 ELF 実行可能コード:
.init
、.text
、.fini
1.2。 ELFの不変データ:
.symtab
、.rodata
RW
フラグセグメント
2.1。 ELFの変更可能なデータ:セクション
.plt
、.got
、.data
、.bss
最初のセグメントの構成とそのアクセスフラグに注意を払うと、このようなレイアウトにより、コード再利用技術のガジェットを検索するためのスペースが拡大することが明らかになります。 libcryptoなどの大規模なELFでは、サービステーブルおよびその他の不変データが実行可能セグメントの最大40%を占める可能性があります。 このデータ内のコードの断片に似たものの存在は、セクションテーブルとシンボルなしで実行可能セグメント内の大量のデータを含むそのようなバイナリファイルを逆アセンブルする試みによって確認されます。 この単一の実行可能セグメント内の各バイトシーケンスは、マシンコードおよびスプリングボードの攻撃フラグメントに役立つと見なすことができます-プログラムからのデバッグメッセージの少なくとも一部、シンボルテーブル内の関数名の一部、または暗号アルゴリズムの定数を含むこのバイトシーケンスです...
ELFイメージの最初のセグメントの先頭にある実行可能なヘッダーとテーブルは、約15年前のWindowsの状況に似ています。 ファイルに感染するウイルスが多数あり、PEヘッダーにコードを書き込んでおり、PEヘッダーもそこで実行可能でした。 アーカイブでそのようなサンプルを掘り下げることができました。
ご覧のとおり、PEヘッダーの領域のセクションテーブルの直後にウイルス本体が圧縮されています。 仮想メモリへのファイルの投影では、通常、約3 KBの空き領域があります。 ウイルスの本体の後に空のスペースがあり、最初のセクションはプログラムコードで始まります。
ただし、Linuxの場合、VXシーンのさらに興味深い作品、 報復がありました 。
解決策
- 上で説明された問題は長い間知られていました 。
- 2018年1月12日修正: `ld -z Separate-code:key is created:"オブジェクトに個別のコード "PT_LOAD"セグメントヘッダーを作成します。これは、命令のみを含む必要があり、他のデータから完全に切り離されたメモリセグメントを指定します。 noseparate-codeを使用する場合は、別個のコード「PT_LOAD」セグメントを作成しないでください。 ")。 この機能はリリース2.30でリリースされました 。
- さらに、この機能は次のリリース2.31でデフォルトで含まれていました。
- Ubuntu 18.10リポジトリなど、新しい
binutils
パッケージに存在します。 ElfMasterの研究者が遭遇し、文書化したこの新機能を使用して、多くのパッケージが既に組み立てられています
レイアウトアルゴリズムの変更の結果、新しいELF画像が取得されます。
$ readelf -l ls Elf file type is DYN (Shared object file) Entry point 0x41aa There are 11 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00000034 0x00000034 0x00160 0x00160 R 0x4 INTERP 0x000194 0x00000194 0x00000194 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x00000000 0x00000000 0x01e6c 0x01e6c R 0x1000 LOAD 0x002000 0x00002000 0x00002000 0x14bd8 0x14bd8 RE 0x1000 LOAD 0x017000 0x00017000 0x00017000 0x0bf80 0x0bf80 R 0x1000 LOAD 0x0237f8 0x000247f8 0x000247f8 0x0096c 0x01afc RW 0x1000 DYNAMIC 0x023cec 0x00024cec 0x00024cec 0x00100 0x00100 RW 0x4 NOTE 0x0001a8 0x000001a8 0x000001a8 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x01c3f8 0x0001c3f8 0x0001c3f8 0x0092c 0x0092c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x0237f8 0x000247f8 0x000247f8 0x00808 0x00808 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt 03 .init .plt .plt.got .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 06 .dynamic 07 .note.ABI-tag .note.gnu.build-id 08 .eh_frame_hdr 09 10 .init_array .fini_array .data.rel.ro .dynamic .got
コードとデータの境界がより正確になりました。 唯一の実行可能セグメントには、実際にはコードセクションのみが含まれます:.init、.plt、.plt.got、.text、.fini。
ご存じのとおり、出力ELFファイルの構造は、 リンカースクリプトによって記述されます 。 次のようなデフォルトのスクリプトを見ることができます:
$ ld --verbose GNU ld (GNU Binutils for Ubuntu) 2.26.1 * * * using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ /* Copyright (C) 2014-2015 Free Software Foundation, Inc. * * *
異なるプラットフォームおよびオプションの組み合わせのためのその他の多くのスクリプトは、 ldscripts
ディレクトリにあります。 separate-code
オプション用の新しいスクリプトが作成されました。
$ diff elf_x86_64.x elf_x86_64.xe 1c1 < /* Default linker script, for normal executables */ --- > /* Script for -z separate-code: generate normal executables with separate code segment */ 46a47 > . = ALIGN(CONSTANT (MAXPAGESIZE)); 70a72,75 > . = ALIGN(CONSTANT (MAXPAGESIZE)); > /* Adjust the address for the rodata segment. We want to adjust up to > the same address within the page on the next page up. */ > . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)));
ここでは、コードセグメントの後に読み取り専用セクションを持つ新しいセグメントを宣言するためのディレクティブが追加されていることがわかります。
ただし、スクリプトに加えて、リンカーソースに変更が加えられました。 つまり、関数_bfd_elf_map_sections_to_segments
参照してください。 現在、セクションのセグメントを選択するときに、セクションが前のセクションとSEC_CODE
フラグで異なる場合、新しいセグメントが追加されます。
おわりに
以前と同様に 、ソフトウェアを開発する際に、コンパイラーとリンカーに組み込まれたセキュリティフラグを使用することを開発者が忘れないことをお勧めします。 このような小さな変更は、攻撃者の生活を大幅に複雑にし、あなたをより穏やかにします。