空飛ぶ木造船

借り物ばかりの備忘録です。意味のあるものになると嬉しいです。

策謀本のret2libcをやってみた

はじめに

策謀本を読み始めてはや2年、長期休みのたびに挑戦しては少しずつ読み進めていましたが、今日でやっと第6章まで読み切ることができました。自分の知識不足であったり本に書かれてる環境と現在の環境が変わっている所があったりして、かなり苦労したり一部再現できないまま断念したりしていますが、一応この本についてはすべきことは終わったという気でいます。本当は次に暗号について書かれている章があるのですが、これについては別の本を読むのでも良いかなと思っているので、僕的には策謀本は終わりです。

終わった記念に一番最後に実行できたp.436(0x6b2)のret2libcについて自分が理解したことや詰まったポイントについてまとめておきたいと思います。

スタックの構造と攻撃用のバイト列

攻撃の対象となるコードは以下の通りです。

#include <stdlib.h>
#include <string.h>

int main(int argc, char const *argv[]) {
    char buffer[5];
    strcpy(buffer, argv[1]);
    return 0;
}

6行目のstrspy関数にはバッファオーバーフロー脆弱性があるので、これを利用して今回はlibc内のsystem関数を呼び出し、そこからシェルを起動したいと思います。 そのためにまずこのプログラムのメモリ上の構造を見ていきたいと思います。

スタックの構造

このプログラムをできるだけ策謀本が想定している通りにコンパイルするために、以下のようなコマンドでコンパイルを行いました。

vuln: vuln.c
    sudo chmod u+s vuln
    sudo sysctl -w kernel.randomize_va_space=0
    gcc -no-pie -fno-stack-protector -m32 -g -mpreferred-stack-boundary=2 vuln.c -o vuln
    sudo chown root vuln

32bitでコンパイルして呼び出し規約をcdecl(多分)にし、ASLRやスタックの保護を無効にしています。 また、-mpreferred-stack-boundary=2はデフォルトでは16byte境界にスタックポインタを並べるのに対し、このオプションを設定すると4byte境界に並べるようになるらしいです。 これが具体的にどのように影響しているのかはわかりませんが、これを設定することでリターンアドレスの位置を以降に述べる方法で見つけられるようになりました。 加えて、これ以前の攻撃対象についてはコンパイル時のオプションとして-z execstackをつけてスタック領域でシェルコードを実行していましたが、今回はret2libcなのでスタック領域での命令実行を無効にしています。 このとき、スタック上のメモリ構造は以下のようになっています(多分)。

一般的なコールスタック

今回ローカル変数bufferに対して好きなだけ書き込みができるので、main関数の呼び出し元__libc_start_mainへ戻るためのアドレスを上書きすることで制御の流れを書き換えます。具体的には、bufferの領域からはみ出て(A)の戻りアドレスの部分を標準ライブラリ内に存在するsystem関数のアドレスを書き込みます。 そのために、以下のことをする必要があります。

  1. bufferから(A)の領域へのオフセットを調べる
  2. 動的にリンクされるlibc内のsystem関数のアドレスを調べる
  3. libcの中から文字列"/bin/sh"のアドレスを調べる

3についてですが、system関数に実行するプログラム(今回はシェル)のパスを表す文字列へのアドレスを引数として渡す必要があるので、それをlibc内から供給します。

オフセットの調査

gdbpattcpattoを利用して戻りアドレスの位置を調べます。 これは次のようにpattcコマンドで長めの入力を生成し、その後不正な戻りアドレスにアクセスした際のアドレスを文字列化したものがpattcコマンドで生成した文字列のどこに位置するかによって、オフセットを調べることができます。

オフセットの調査の様子

写真の中で、0x416e4141にアクセスしようとしてSegmentation faultを起こしています。このアドレスの数値をASCIIコードに直すと"AnAA"となります。"AnAA"の最初の'A'がはじめから14文字目だとわかったので、オフセットは13byteです。

system関数と"/bin/sh"のアドレス

次にsystem関数と"/bin/sh"のアドレスを調べます。 これらは動的に読み込まれるlibc上に存在するので、プログラムの実行をgdb-peda$ b mainなどで途中で止めた状態で調べる必要があります。

system関数と文字列のアドレス

攻撃用のコマンドライン引数について

いよいよ攻撃用のコマンドライン引数を作成します。

ペイロード

上の画像にあるようにペイロード13byteの埋め草 + system関数のアドレス + 戻りアドレス + system関数の引数("/bin/sh"へのアドレス)という構造になります。 このときの「戻りアドレス」とは、system関数から呼び出し元に戻るためのアドレスになるのですが、今回の場合シェルを呼び出した後どこかに制御を戻すことは考えなくて良いため、適当な4byteの文字列を入れておきます。 また、アドレスの部分についてはリトルエンディアンで解釈されるため、逆順に並べています。 そのため以下のようなコードを実行することでret2libc攻撃が成立します。

> sudo ./vuln $(python -c 'print("A"*13 + "\x50\x62\xe1\xf7FAKE\x31\x5e\xf5\xf7")')

余談ですが、このようなバイト列に足してペイロードという表現を使っても問題ないのか、もっといえばセキュリティの分野でペイロードという言葉が指すものが一体何なのか、未だによくわかっていないので、誰か教えてくれると嬉しいです。

困った点

はじめ、"/bin/sh"へのアドレスを供給するために環境変数に"/bin/sh"を設定して、そのアドレスをペイロードに含めていたのですが、これを実行してもうまくいきませんでした。gdbで実行した場合の結果を見るにおそらく環境変数PATHを指していたようだったので、これではない方法でアドレスを供給しようと思い、ほかのret2libcの記事に書いてあるようにlibc内の"/bin/sh"のアドレスを使うようにしました。

また、初めコンパイル時に-mpreferred-stack-boundary=2のオプションを付けずにコンパイルしていたのですが、それではmain関数からの戻りアドレス(__libc_start_mainへの戻りアドレス)がうまく調べることができませんでした。具体的には、アドレスを上書きした部分の値がpattoで生成した文字列に含まれない文字列になってしまいました。これがなぜ解決したのか、このオプションを付けない状態でオフセットを特定するにはどうしたらよいのかなど全くわかっておらず、とりあえず詳しい人にこのオプションを付けると良いと言われてその通りにしただけなので、この点について今後調べられたら改善したいです。

参考文献