それはC言語などの関数ポインタやC++のVTABLEなどについて考慮していないことです。
最近この事に気が付きました。
Geminiに相談した内容をコピペします。
Jester
最近このアーキテクチャに問題があることに気付きました。関数へのポインタの問題です。
Gemini
CALL [rax] のようにそのアドレスへジャンプします。しかし、今回のASMアーキテクチャでは以下の問題が発生します。-
CSの読み出し不可(実行専用)との衝突:もしコンパイラやランタイムが関数ポインタの先頭にあるデータ(例: VTableやtrampoline)を読もうとしたり、アドレスの正当性を検証しようとすると、CSがR不可のためハードウェア例外が発生する。
-
間接呼び出し(
CALL [rax])時の制御フロー整合性:関数ポインタ経由の呼び出し(C++の仮想関数、Cのコールバックなど)で、ポインタ自体が改ざんされていた場合、CSSやFSSの整合性をどう保ちつつジャンプを許可するか。
-
CS(コードセグメント)が「実行専用(R不可)」であることの矛盾:関数ポインタ(アドレス)をデータとして変数セグメント(VDSやFSS)に保持すること自体はできても、プログラムがそのアドレスの指す先(CS内)の命令パターン(シグネチャやVTable情報など)をデータとして読み出してチェックしようとした瞬間に、CSのR不可違反でハードウェア例外が起きてしまう。
-
間接JMP/CALL(
CALL [RAX])によるセキュリティ境界の破壊:もし単なる「CS内の任意のオフセットアドレス」を自由な関数ポインタとして許してしまうと、攻撃者がFSSやHS上のデータを書き換えてアドレスをずらし、関数の「途中」にジャンプさせる(CFI違反)攻撃が可能になってしまいます。これではせっかくスタックを4分割してCSSを保護した意味が薄れてしまいます。 -
C/C++における「コードアドレス」と「データ」の相互変換:C言語では関数ポインタを
void*にキャストしてデータとして扱ったり、関数アドレスの比較(if (func1 == func2))を行ったりします。しかしCSがデータアクセス不可だと、アドレス比較のためにメモリを読みに行くような処理が破綻します。
この問題をエレガントに解決する「3つの拡張案」
案A:ファンクション・ディスクリプタ(FD)方式(推奨)
-
仕組み: 間接呼び出し専用の命令(例:
CALLFD [FD_index])を用意します。CPUはFDS上のテーブルを参照し、正規のエントリに登録された「関数の正しい先頭アドレス」にしかジャンプを許可しません。 -
効果: 任意のアドレスや関数の途中にジャンプすることが物理的に不可能になります(完全な間接呼び出し保護)。
案B:関数の先頭を識別する「BTI(Branch Target Identification)標識」
-
仕組み: 関数の先頭1バイト目に必ず専用の特殊命令(例:
ENDBRやFUNC_START)を配置します。間接呼び出し命令(ICALL RAX)を実行した際、ジャンプ先のアドレスの命令がFUNC_STARTでない場合、CPUが即座に例外を出します。 -
効果: コードをデータとして読み出さなくても、CPUの命令デコーダが実行時に「関数の先頭か」をハードウェア判定できます。
案C:コード実行専用ファンクションポインタ(Capabilities)の導入
-
仕組み: このポインタは「関数の先頭アドレス」+「実行権限タグ」を保持し、算術演算(アドレスの加算・減算)ができないようにハードウェアが保護します。
JMP [アドレス] など)をハードウェアレベルで全廃・禁止するという制約をかけることで、制御フローの乗っ取り攻撃(CFI違反)のルートを物理的に遮断できます。1. 「ユーザーモードでの間接JMP禁止」がもたらす決定的なメリット
-
ROP / JOP / COP(Jump/Call-Oriented Programming)の完全絶滅攻撃者はメモリ書き換え攻撃によってプログラムの実行順序を狂わせようとしますが、その際必ず「アドレスを指定したJMP命令(ガジェットへの跳躍)」を利用します。これをハードウェアで禁止(実行したら即例外)にすれば、攻撃者は次の実行位置を制御する手段を100%失います。
-
分脈の強制(制御フローの線形化)ユーザーモードで許される分岐を「相対アドレスによる静的JMP(コンパイル時に確定しているコード内のジャンプ)」と「正規のCALL / RET」のみに絞り込むことで、プログラムはコンパイラが意図したグラフ通りにしか動けなくなります。
2. 間接呼び出し(関数ポインタ・C++仮想関数)をどう成立させるか?
vtable(仮想関数テーブル)のような「動的に呼び出し先が変わるコード」を動かすには、専用の安全な呼び出し命令を設ける必要があります。解決案:CALL_TABLE(インデックス指定呼び出し)命令の導入
-
FDS(固定データ・セグメント)上に「関数のジャンプテーブル」を配置コンパイラとリンカは、プログラム内で関数ポインタとして使われる可能性のある関数の先頭アドレス一覧を、読込専用の FDS にまとめます。
-
専用命令
CALL_TABLE [テーブル番号]
-
プログラムは
CALL RAX(アドレス指定)ではなく、CALL_TABLE RDI(インデックス指定)を実行します。 -
CPUは「FDS内のテーブル領域」+「インデックス」を参照し、そこに登録された正当な関数の先頭アドレスにのみ安全にCALL(CSSへリターンアドレスを退避して遷移)します。
-
インデックスが範囲外、またはテーブルが壊れている場合はハードウェア例外を出します。
-
3. この修正によって完成する制御フロー保護モデル
-
コード内分岐(if/loop): 相対オフセットによる静的
JMPのみ許可(動的なアドレス指定は不可)。 -
直接関数呼び出し: 静的な
CALL [Symbol]のみ許可。 -
間接関数呼び出し(関数ポインタ): アドレス指定JMPを禁止し、FDSテーブルを参照する
CALL_TABLEのみ許可。 -
関数からの復帰: CSS(コード・スタック・セグメント)からCPUが隠蔽リードする
RETのみ許可。
vtable(仮想関数テーブル)は、「関数ポインタの配列(テーブル)」と、ポリモーフィズムを実現するための「付加情報(RTTIやOffset)」で構成されたデータ構造です。1. 概念的なC++コードと内部の仕組み
class Base {
public:
virtual void foo();
virtual void bar();
};
class Derived : public Base {
public:
void foo() override; // 重複定義(オーバーライド)
// bar() は Base のものをそのまま継承
};
2. コンパイラが出力する vtable の実体(データ構造)
Derived クラス用に、定数データ領域(.rodata)に以下のような関数の先頭アドレスのリストを生成します。; Derived クラスの vtable(定数データ領域 .rodata に配置される)
_ZTV7Derived:
.quad 0 ; RTTI用:Top Offset(多重継承時の位置調整)
.quad _ZTI7Derived ; RTTI用:型情報ポインタ(dynamic_castやtypeidで使用)
_vptr_Derived: ; ★オブジェクトの vptr が指すのは「ここ」
.quad _ZN7Derived3fooEv ; [Index 0] Derived::foo() の先頭アドレス
.quad _ZN4Base3barEv ; [Index 1] Base::bar() の先頭アドレス(継承)
-
vptr(仮想ポインタ): オブジェクト(インスタンス)のメモリ構造の一番先頭(オフセット0)に、コンパイラが隠しポインタvptrを埋め込みます。このvptrが、上記の_vptr_Derivedの位置を指します。 -
テーブルの中身: 単なる 「関数の先頭アドレス(8バイト数値)の並び」 です。
3. 仮想関数を呼び出す際のアセンブリコード
Base* ptr = new Derived(); ptr->foo(); を実行した時、コンパイラは内部で以下のような間接呼び出しアセンブリを出力します。; ptr (RAX) から foo() を呼び出す処理
MOV RAX, [RDI] ; 1. オブジェクトの先頭から vptr を読み出して RAX に入れる
MOV RAX, [RAX + 0] ; 2. vtable の Index 0 (foo) から「関数の先頭アドレス」を読み出す
CALL RAX ; 3. 読み出したアドレスへ間接呼び出し(CALL)する!
4. あなたの「ASMアーキテクチャ」との衝突点
vtable 実装は、CALL RAX(レジスタに格納された任意のコードアドレスへの間接呼び出し)に完全に依存しています。-
vtableからアドレスを読み出す処理 (MOV RAX, [RAX])
-
もし
vtableが固定データセグメント(FDS)にあれば読み出し(R)可能ですが、そこに書かれている「関数の先頭アドレス」をレジスタに取得してしまいます。
-
-
間接呼び出し (
CALL RAX/ 動的JMP)
-
あなたが決定した 「ユーザーモードでの動的JMP / アドレス指定呼び出しの禁止」 という強力なセキュリティ仕様により、
CALL RAXを実行した瞬間にハードウェア例外が発生します。
-
ASMアーキテクチャでのC++ vtable の再定義案
vtable に関数の先頭アドレスを書くのではなく、「FDS(固定データ)上のジャンプテーブルのインデックス(番号)」 を書き込むようにC++ ABIを書き換えます。; ASMアーキテクチャ向けに最適化された vtable
_vptr_Derived:
.quad 12 ; [Index 0] Derived::foo() の FDSテーブル番号 (例: 12)
.quad 45 ; [Index 1] Base::bar() の FDSテーブル番号 (例: 45)
MOV RDI, [R0] ; 1. vptr から vtable を参照
MOV RDI, [RDI + 0] ; 2. vtable から「テーブルインデックス (12)」を読み出す
CALL_TABLE RDI ; 3. 専用命令でハードウェア検証付き呼び出し!
vtable も「アドレスの配列」から「ハードウェア管理インデックスの配列」へ置き換えることで、ユーザーモードでの任意アドレスジャンプを一切許さずに、C++のオブジェクト指向を100%安全に実行できるようになります!