ROPとは何か?プログラムの中にある命令を借りる仕組み(第1回/全11回)
「ROP」という言葉を初めて見ると、難しい攻撃技術のように感じるかもしれません。
しかし、最初から複雑な命令やアドレスを覚える必要はありません。まずは、ROPがどのような考え方なのかを、頭の中にイメージできるようになることが大切です。
今回の目標は、ROPを実際に組み立てることではありません。
ROPとは何か、なぜ必要になったのかを、初心者のくろちゃんへ自分の言葉で説明できるようになる。これを第1回のゴールにします。
※イメージです。
くろちゃん、ROPという言葉に出会う
くろちゃんは、白猫先生と一緒にコンピューターの保護機構を勉強していました。
[fuki-r]白猫先生!前にNXという保護機構を勉強したよ。スタックに置かれたデータを、命令として実行できなくする仕組みだったよね?[/fuki-r]
[fuki-l]そのとおりです。では、新しい命令をスタックに置いて実行できなくなったら、攻撃はすべて防げるでしょうか?[/fuki-l]
[fuki-r]新しい命令を実行できないなら、もう攻撃できないんじゃないの?[/fuki-r]
[fuki-l]そこで考え出されたのが、プログラムの中にすでにある命令を利用する方法です。その代表的な考え方がROPです。[/fuki-l]
[fuki-r]新しい命令を持ち込まずに、もともとある命令を借りるの?[/fuki-r]
[fuki-l]はい。それが今回の大切なポイントです。[/fuki-l]
ROPの正式名称
ROPは、次の言葉の略です。
Return-Oriented Programming(リターン・オリエンテッド・プログラミング)
日本語では、一般的に「戻り指向プログラミング」などと訳されます。
ここにある「Return」とは、関数から元の場所へ戻るときに使われる`ret`命令のことです。
「Programming」という言葉が含まれていますが、C言語やPythonのようなプログラムを書くという意味とは少し違います。
ROPでは、実行ファイルや共有ライブラリの中にすでに存在している短い命令列を探し、それらを目的に合わせて順番につなげます。
つまり、ROPとは次のような考え方です。
プログラム内の短い命令列を、`ret`命令を利用して順番に実行させる方法。
現段階では「短い命令を借りて、順番につなぐ」と理解できれば十分です。
通常のプログラムは決められた道を進む
通常のプログラムでは、CPUはプログラムが指定した順番に命令を実行します。
例えば、次のような処理があるとします。
1. 名前を入力する
2. 入力された名前を保存する
3. 画面にあいさつを表示する
4. 関数を終了する
5. 呼び出し元へ戻る
CPUは、基本的にこの順番に沿って処理を進めます。
関数を呼び出した場合は、関数の処理が終わった後に、元の場所へ戻らなければなりません。そのため、コンピューターは「戻る場所」を記録しておきます。
この戻る場所を示す情報が、戻り先アドレスです。
[fuki-r]戻り先アドレスは、しおりみたいなもの?[/fuki-r]
[fuki-l]よい例えですね。本を途中まで読んで別のページを確認した後、しおりを使って元のページへ戻るようなものです。[/fuki-l]
関数が正常に終了すると、CPUは保存されていた戻り先アドレスを使って、元の処理へ戻ります。
戻り先アドレスが変わるとどうなる?
戻り先アドレスには、関数の終了後に実行する命令の場所が記録されています。
ところが、プログラムにメモリ破損の脆弱性があると、戻り先アドレスが壊されたり、別の値に変更されたりする場合があります。
するとCPUは、本来戻るはずだった場所とは違う場所へ移動してしまいます。
1.正常な場合
・関数を呼び出す
↓
・関数の処理を実行する
↓
・保存されていた場所へ戻る
↓
・正常な処理を続ける
2.戻り先が変更された場合
・関数を呼び出す
↓
・関数の処理を実行する
↓
・変更された場所へ移動する
↓
・本来とは違う命令を実行する
CPUは、そのアドレスが正常な処理によって保存されたものなのか、不正に変更されたものなのかを、アドレスだけから判断することはできません。
指定された場所に実行可能な命令があれば、CPUはその命令を実行しようとします。
ROPでは、この「次にどこを実行するか」という制御の流れに注目します。
以前は新しい命令を持ち込む方法が使われていた
古典的な攻撃方法の一つに、入力データの中へ実行させたい命令を入れる方法があります。
このような小さな命令のまとまりは、一般的にシェルコードと呼ばれます。
考え方を単純化すると、次のようになります。
1. メモリへ新しい命令を置く
2. 戻り先アドレスを変更する
3. CPUを新しい命令の場所へ移動させる
4. 置かれた命令を実行させる
しかし、コンピューターにはNXという保護機構が導入されました。
NXは「No-eXecute」の略で、データを置くためのメモリ領域を、命令として実行できないようにします。
例えば、スタックに新しい命令を置いたとしても、スタックが実行禁止に設定されていれば、CPUはその命令を実行できません。
[fuki-r]新しく持ち込んだ命令を、スタックで動かせなくなったんだね。[/fuki-r]
[fuki-l]はい。そこで、新しい命令を持ち込まず、すでに実行を許可されている命令を利用する発想が重要になりました。[/fuki-l]
ROPは「すでにある命令」を利用する
実行ファイルのコード領域には、プログラムを動かすための機械語命令が数多く入っています。
共有ライブラリにも、さまざまな命令が含まれています。これらの領域は本来のプログラムを動かすため、CPUによる実行が許可されています。
ROPでは、その中から利用できる短い命令列を探します。
この短い命令列を、ROPガジェットと呼びます。
代表的な形は、次のようなものです。
pop rdi
ret
現段階で、それぞれの命令を完全に理解する必要はありません。
重要なのは、ガジェットの最後に`ret`があることです。
`ret`が実行されると、CPUはスタックから次のアドレスを取り出して、そのアドレスへ移動します。そのため、スタックにガジェットのアドレスを順番に並べることで、複数のガジェットをつないでいけます。
スタック
| ガジェットAのアドレス |
| ガジェットBのアドレス |
| ガジェットCのアドレス |
大まかな流れは次のとおりです。
・ガジェットA
↓ ret
・ガジェットB
↓ ret
・ガジェットC
このようにガジェットを連続させたものを、ROPチェーンと呼びます。
ROPを道具箱に例えてみよう
ROPは、鍵のかかった作業部屋に例えるとイメージしやすくなります。
その部屋には、外から新しい道具を持ち込めないという決まりがあります。これは、新しい命令の実行を防ぐNXに相当します。
しかし、部屋の中にはすでに次の道具が置かれています。
・ドライバー
・ペンチ
・ハンマー
・定規
・はさみ
一つ一つの道具だけでは、できることが限られています。
ところが、道具を適切な順番で使えば、少し複雑な作業もできるかもしれません。
ROPも同じです。
・部屋の中にある道具:既存の命令
・短い作業:ROPガジェット
・道具を使う順番:ROPチェーン
・次の道具を指定するもの:スタック上のアドレス
・次の作業へ移る合図:`ret`
[fuki-r]新しい道具は持ち込まないけれど、部屋にある道具の使い方を変えるんだね。[/fuki-r]
[fuki-l]その理解で合っています。ROPでは、命令そのものを新しく作るのではなく、既存の命令の組み合わせ方を変えるのです。[/fuki-l]
シェルコードとROPの違い
シェルコードとROPの違いを整理してみましょう。
| 比較する点 | シェルコード | ROP |
| 使用する命令 | 外部から置いた新しい命令 | プログラム内の既存命令 |
| 主な実行場所 | 入力データを置いた領域など | 既存の実行可能領域 |
| NXの影響 | 強く受ける | 単純なコード注入とは異なる |
| 命令の進め方 | 作成された命令を順番に実行 | `ret`を利用してガジェットを接続 |
| 必要な情報 | 命令を置いた場所など | ガジェットのアドレスなど |
ROPはNXを無効にするわけではありません。
NXは正常に働いたままです。ROPは、NXによって実行を禁止されたデータ領域を使うのではなく、もともと実行が許可されているコード領域を再利用します。
この違いは非常に重要です。
ROPは保護機構を解除する方法ではなく、既存コードを別の順番で利用する考え方である。
ROPは何でもできる魔法ではない
ROPという言葉を知ると、どのプログラムでも自由に操作できるような印象を持つかもしれません。
しかし、ROPを成立させるには複数の条件が関係します。
・制御の流れに影響するメモリ破損がある
・利用できるガジェットが存在する
・必要なアドレスが響するメモリ破損がある
・利用できるガジェットが存在する
・必要なアド分かる
・スタックの状態を適切に扱える
・ASLRやPIEなどの影響に対応できる
・Canaryなどによって先に停止されない
・CPUやOSの制御フロー保護に阻止されない
現代のコンピューターでは、NXだけでなく、ASLR、PIE、Canary、RELRO、CET、CFIなど、複数の保護機構が組み合わされています。
そのため、ROPは「NXがあれば必ず使える」という単純な技術ではありません。
ROPを学ぶ目的は、攻撃手順を暗記することではなく、次の関係を理解することです。
・脆弱性
↓
・メモリが破損する
↓
・制御の流れが変化する
↓
・既存の命令が再利用される
↓
・保護機構がそれを妨げる
ROPとは何か?プログラムの中にある命令を借りる仕組みのまとめ
ROPは「Return-Oriented Programming」の略で、プログラムや共有ライブラリの中にすでに存在する短い命令列を再利用し、`ret`命令によって順番につなぐ考え方です。短い命令列をROPガジェット、それらを並べたものをROPチェーンと呼びます。
従来のようにスタックへ新しい命令を置く方法は、NXによって実行を妨げられます。しかしROPはNXを解除するのではなく、もともと実行が許可されているコード領域を利用します。そのため、ROPを理解するには、戻り先アドレス、スタック、`ret`、レジスタ、関数呼び出しの関係を順番に学ぶ必要があります。
ただしROPは万能ではありません。成立にはメモリ破損、利用可能なガジェット、アドレス情報などが必要で、ASLR、PIE、Canary、CETなどの保護機構にも影響されます。ROP学習の本当の目的は攻撃手順の暗記ではなく、CPUがどこから命令を読み、なぜ処理の流れが変わるのかを論理的に説明できるようになることです。