今回は Daily AlpacaHack の5日目であるpwnの問題を解き(?)ました。難易度がHardと言うことで、めっちゃ難しかったです... 想定解は gdb を用いるものなのですが、まだ自分のMacにLinuxの環境構築ができていないため、今日はとりあえず gdb なしでゴリ押しました。環境構築についても別日に記載しようと思います。
問題
提供されるもの
- 脆弱性を含むCファイル
- x86-64アーキテクチャ上でコンパイルされたバイナリファイル
- 攻め込む場所となる、公開されているIPアドレス
方向性
ヒントなども見て、これは「returnアドレスの書き換え」問題であることが分かりました。プログラムの中にローカル変数のint配列があり、この配列の範囲外のアドレスにアクセスすることで、returnアドレスを書き換え目的の関数を呼ぶ、と言う内容です。
x86の構造とこのようなreturnアドレスの書き換えについては、去年の大学のクラスでみっちりやったはずなんですが、まさかこのような形で使うことにはなると思っておらず完全に忘れてしまっていました笑
解き方は他のwriteupで詳しく書かれているので、この記事では (軽くですが) 自分が復習した x86アーキテクチャの内容についてまとめたいと思います。
x86アーキテクチャ
x86とは、
そもそもアーキテクチャとは
コンピュータの内部でメモリがどのように管理されるか、関数がどう呼ばれるか、どのような規則に則って高級言語を低級言語に変換し実行するか、などの「内部ルール」を定めたものです。今回の問題だと、例えば「関数Aが関数Bを呼ぶときに、関数Bの実行が終わったら関数Aに戻ってこないといけないけど、その戻る先 (returnアドレス) はどこに保存しておくのか」と言うものを定めたものになります。
スタックメモリ
コンピュータは、一時的な値 (今実行中の関数や今使っている変数など) を保存するためにスタックデータ構造を用います。なぜかというと、
関数A開始 → (関数Bが呼ばれる → (関数Cが呼ばれる → 関数Cがreturn) → 関数Bがreturn) → 関数Aがreturn
と言うような入れ子構造が、first-in-last-out であるスタックの理念と合致しているからです (最初に呼ばれた関数は最後にreturnする)
x86におけるスタックフレームの仕様
まず前提として、メモリの中のスタック部分は高いアドレスから低いアドレスに向かって伸びていきます。新しい関数が呼ばれた時は、今の関数の情報が書いてある部分よりも低いアドレスの方向にその関数の情報を書いていくと言うことです。この「関数の情報が書いてある部分」と言うのをスタックフレームと呼びます。
関数Aが関数Bを呼ぶ時は、
(高アドレス) ... [関数Aのフレーム] [関数Bのフレーム] ... (低アドレス)
と言うふうになります。
それぞれのスタックフレームは、
[ [関数のreturnアドレス] [callerのベースポインタ] [ローカル変数] [次に呼ぶ関数に渡す入力パラメータ] ]
と言う構成になっています。ここで重要なのはローカル変数とreturnアドレスです。
関数の実行内容は、メモリ内のどこかの場所に格納されています。プログラムを実行しているとき、コンピュータは常に「今メモリ内のどの部分を実行しているか」を保持していますが、違う関数を呼ぶ (= メモリ内の他の場所にジャンプする) 時は、「この関数の実行が終わった後に今の関数の場所に帰ってこないといけない」という問題があります。これを保持するのが return アドレスです。
例えばですが、ローカル変数に整数の配列 (array) があったとします。ここで、array[0] 配列内の正しい場所を表していますが、もし array[-2] を書き換えた場合はどうなるでしょうか?(PythonやC++などのモダン言語ではこれは IndexOutOfRange エラーのようなものを返しますが、C言語では自分でチェックしない限りこれが許されています)
この場合、配列が割り当てられている部分の外、もっと正確に言うとより低アドレス側の値を書き換えてしまうことになります。この値をうまーく設定すると、次に呼ぶ関数のreturnアドレスを書き換えることができます。
今回の問題は「どのindex部分を書き換えればreturnアドレスを書き換えられるか」と言うのを頑張って探す問題でした。
まとめ
今回は以上になります。明日は新しい問題か、または環境設定/その他の新しく学んだコマンドなどを紹介したいと思います!