HN 日本語サマリー

← 一覧へ戻る
プログラミング

GCCにおける実行可能スタックなしのネスト関数間接呼び出し

Indirect Calling of Nested Functions on GCC Without Executable Stack (uecker.codeberg.page)

39 pointsby uecker9 コメント

要約

この記事では、GCC 17およびClangで利用可能なネスト関数を、実行可能スタックを必要とせずにコールバックとして使用する方法について説明しています。古いGCCバージョンをサポートする必要がある場合、実行可能スタックを受け入れるか、ハックを用いて回避する方法が示されています。具体的には、ネスト関数から生成されるトランポリンコードを解析し、そのコードアドレスとスタティックチェーンを抽出して、__builtin_call_with_static_chain組み込み関数で直接呼び出す手法が解説されています。

全文翻訳

ブログ連絡先/インプリント GCCにおける実行可能スタックなしのネスト関数間接呼び出し Martin Uecker, 2026-08-29 はじめに 前回は、実行可能スタックを必要とせずにコールバックとしてGCC 17およびClangでネスト関数を使用する方法について議論しました。しかし、古いバージョンのGCCをサポートする必要がある場合はどうでしょうか。もちろん、実行可能スタックを受け入れることもできます(人々が主張するほどひどいものではありませんが)、ハックでこれを回避することも可能です。 GCC: ネスト関数とトランポリン まず、GCCがネスト関数のアドレスを取得する方法について説明します。新しいマクロを使用しない場合の簡単な例を以下に示します(Godboltの例)。 typedef int cb_f(int y); int baz(cb_f p, int x) { return p(x); } int foo(int k) { int bar(int x) { return k + x; } return baz(bar, 2 * k); } x86_64で生成されるアセンブリは以下の通りです。 bar.0: movl %edi, %eax addl (%r10), %eax ret foo: subq $56, %rsp leaq 64(%rsp), %rax movq %rax, 32(%rsp) movl %edi, (%rsp) leaq 4(%rsp), %rax movw $-17591, 4(%rsp) movabsq $bar.0, %rcx movq %rcx, 6(%rsp) movw $-17847, 14(%rsp) movq %rsp, 16(%rsp) movl $-1864106167, 24(%rsp) addl %edi, %edi call *%rax addq $56, %rsp ret このコードはスタック上にトランポリンを配置し、インライン化されたbaz関数を介してすぐにそれを呼び出します。トランポリンは、親関数からキャプチャされた変数を含むスタック上の構造体に静的フレームレジスタをロードし、ローカル関数にジャンプする短いコードシーケンスです。 定数 -17591、-17847、および -1864106167 をアセンブリ命令に変換すると、以下のx86_64コードが得られます。 movq $bar.0, r11 movq $frame, r10 jump *r11 静的チェーンとコードアドレスの両方が、トランポリンのコード内の移動命令によって使用される即値定数です。トランポリンのアドレスを使用して関数を呼び出す代わりに、トランポリンからコードアドレスと静的チェーンを抽出し、__builtin_call_with_static_chain組み込み関数を使用してローカル関数を直接呼び出すことができます。例えば、noplateのpeekおよびarray_sliceマクロを使用して、x64_64(および大規模メモリモデル)では次のように行うことができます。 unsigned char (*tramp)[24] = (void*)bar; void *code = peek(uint64_t, &array_slice(tramp, 2, 10)); void *chain = peek(uint64_t, &array_slice(tramp, 12, 20)); これらのポインタは、まだリリースされていないGCC 17で新しい組み込み関数__builtin_call_static_chainおよび__builtin_call_code_adressで取得できる情報とまったく同じであり、前述のようにローカル関数を呼び出すために使用できます。 __builtin_call_with_static_chain(((typeof(bar)*)code)(arg), chain); したがって、古いバージョンのGCCでは、トランポリンからこれらの2つのポインタ値を読み取ることをフォールバックメカニズムとして使用できます。欠点は、トランポリンが依然として作成されること、コンパイラが間接呼び出しをデビジュアライズできないこと、そしてスタックが依然として実行可能としてマークされることです。では、何が得られたのでしょうか?トランポリンを実際に呼び出さないため、以下のコマンドでスタックを実行可能でなくすることができます。これは少なくともこの機能のセキュリティ上の懸念に対処します。 patchelf --clear-execstack program このアイデアは、私の実験的なライブラリであるnoplateに実装されており、コードアドレスと静的チェーンからワイドポインタが構築されます。 トランポリンを関数ディスクリプタとして 探求する価値があると思うもう一つのアイデアがあります。トランポリン自体を関数ディスクリプタとして使用することもできます。トランポリンが作成される場所で静的チェーンとコードポインタを抽出する代わりに、通常どおりトランポリンのアドレスを渡します。しかし、トランポリンを呼び出す可能性のある場所ではどこでも、まずポインタがトランポリンを指しているかどうかを確認し、次にコードアドレスと静的チェーンを抽出して__builtin_call_with_static_chainを使用してネスト関数を直接呼び出します。ある意味では、トランポリンを呼び出すのではなく、呼び出しサイトでトランポリンのコードを、この特定のコードシーケンスのみを解釈できる非常に単純なインタープリタを使用して解釈していると言えます。これは非常に単純なためインライン化できます(Godboltの例)。 参考文献 GCC, Nested Functions GCC, Constructing Function Calls