off-by-one error でreturn addressが上書きされるまで

Phrack Magazine (http://phrack.org/phrack/55/P55-08) の off-by-one exploit を読みました。要約しておきます。

void func(const char* sm)
{
  char buffer[256];
  for(int i=0; i<=256; ++i) { // 255が正解
    buffer[i] = sm[i];
  }
}
int main(int argc, char** argv) {
  func(argv[1]);
  return 0;
}

このような、1バイトだけオーバーフローするbugのあるコードを書いてしまい、更に引数 sm に自由なバイト列を設定できる場合、それがどのようにして arbitrary code execution に繋がるかが書いてあります。


以下、簡単な解説です。


buffer[256] には saved_ebp (4バイトのうちの1バイト) が書かれています。ちゃんと書くと、buffer[256〜259] が saved_ebp で、buffer[260〜263] が saved_eip つまりリターンアドレス。260〜263まで上書きできるのなら、即Gotcha!なんですが、今回はbuffer[256]までしか書けないのでそれは不可能です。saved_ebpの部分的な書き換えでどうにして任意コード実行が起きるかを見てみましょう。


saved_ebpって何?というあたりも含め、アセンブラレベルでの関数呼び出しから復習してみます。実は Intel x86 Function-call Conventions - Assembly View の "Calling a __cdecl function" が詳しいんですけど、日本語で書いておきます。


最初は呼ぶ側から。

// 呼ぶ側
// (1) まず引数を積む
pushl XXX
pushl XXX
// (2) 関数を呼ぶ
call func

call命令を実行すると、暗に pushl %eip 相当の操作が行われる事に注意してください。%eipはcall命令の直後の命令をポイントしており、それがスタックに退避されます。


次、呼ばれる側。

// 呼ばれる側 (func関数) - プロローグ
func:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $260, %esp
    // callee-saved な register を必要に応じてpush

最初に%ebpをスタック退避。次に%espを%ebpにコピー。最後に%espを減算。減算した$260分が、関数funcのローカル変数の領域となります。このプロローグ部分の3命令はenter命令一つで置換できるんですが、gccはそっちは使わないようですね*1

この時点でスタックは次のような配置になっておりまして、%ebpを通じてアクセスされることになります。

  16(%ebp)	- third function parameter 	
  12(%ebp)	- second function parameter 	
   8(%ebp)	- first function parameter 	
   4(%ebp)	- old %EIP (the function's "return address")  	
   0(%ebp)	- old %EBP (previous function's base pointer)  	
  -4(%ebp)	- first local variable 	
  -8(%ebp)	- second local variable 	
 -12(%ebp)	- third loca

関数から抜けるときは次が行われます。

// 呼ばれる側 (func関数) - エピローグ
    // callee-saved な register がpushされたなら、逆順でpop
	leave
	ret

leaveはenterの逆で、次の2命令と等価と考えてOKです。%espが%ebpで上書きされ(プロローグで$280減算されたのがチャラになるわけです)、その後%ebpが昔の値に復元(pop)されます。

	movl %ebp, %esp
	popl %ebp

ret は、まぁ pop %eip だと考えてOKでしょう。call命令が積んだリターンアドレスを復元します。


以上が、関数呼び出しの概要になります。より詳しくは「IA-32 インテルアーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル 中巻 : 命令セット・リファレンス・マニュアル」を無償ダウンロード&参照のこと。


ここで忘れてはいけないのは、pushとpopの実際の処理内容でしょう。

push:
  %espを4減らしてから %espの指すメモリに4バイト書き込む
pop:
  %espの指すメモリから4バイト読み込んで %espを4増やす

です。ですから、もし%espがおかしな値になっていたならpopしてきた値もおかしな物になるわけです。


ret命令は(その内部処理で)popしたアドレスを関数からの復帰先とみなすのですから、%espがおかしな値になっている時にretしたならば、戻り先もおかしなものになってしまいます。


さらに、もし%espを好きに設定できるのであれば、リターンアドレスを好きなメモリから読ませることができ、任意コード実行も夢ではなくなります。以下その手順。

(1) buffer[0〜251] にshellcodeが書かれる様にします。

(2) buffer[252〜255] の4バイトに、&(buffer[0])のアドレス値が書かれる様にします。

(3) buffer[256] を上手く設定し、buffer[256〜259] が &(buffer[252]) のアドレス値が書かれるようにします。


手順は以上です。これを行っておくと…

  1. 関数funcから抜けるとき、leave命令で pop %ebp 相当の処理が走りますが、そのpopされる値が(3)で改竄されたものになります。つまり%ebpが改竄されます。
  2. その直後のret命令で、正常にmain関数に復帰します。
  3. main関数から抜けるとき、leave命令で movl %ebp, %esp 相当の処理が走りますが、movl元の%ebpは改竄されたものです。つまり%espが改竄されます。
  4. その直後のret命令で、復帰先をpopしますが、%espが改竄されているため復帰先をbuffer[252〜259]から取得してしまいます。
  5. %eipが&(buffer[0])に設定され、shellcodeが走ります。

となります。

実際にこの攻撃を成功させるためにはいくつかの条件があります。

  • 溢れさせる配列が、saved_ebpのすぐ隣にあること、つまり配列が関数の最初の変数として宣言されていること。かつ、スタックのアライメントの問題(後述)にひっかかっていないこと。
  • 攻撃対象のマシンがリトルエンディアンであること。

後者について少々解説します。x86で(3)の改竄が成功するのは、x86がリトルエンディアンであることに起因します。

$ gcc -ggdb -O0 -mpreferred-stack-boundary=2 call.c
$ gdb a.out
(gdb) b func
(gdb) r
Breakpoint 1, func (sm=0x0) at call.c:5 
 ...
  普通にfuncでbreakすると、関数のプロローグが実行されてから止まる
 ...
(gdb) x/4b buffer+256 
0xbffff9d8:     0xe8    0xf9    0xff    0xbf 
(gdb) printf "%p?n", &(buffer[252]) 
0xbffff9d4 
(gdb)  

上記を見ると、buffer[256]の値を 0xe8 から 0xd4 に書き換えてしまえば、(3)が実現できることがわかると思います。ビッグエンディアンアーキテクチャ上ではこの方法は使えないでしょう。

off-by-one exploit のまとめ

saved_ebpが書き換えられてしまうと、任意コードを実行されてしまう可能性がある

(特に -mpreferred-stack-boundary=2 や -Os でコンパイルされたコードに) off-by-oneバグがあると、任意コード実行に悪用されるかもしれないよ?

preferred stack boundaryって?


上で「スタックのアライメントの問題」と書いた件について解説します。gccで、

void func(void) { char x[256]; } 

のような関数を作成して gcc -O0 -S してみると、

func: 
  pushl %ebp 
  movl %esp, %ebp 
  subl $264, %esp 
  leave 
  ret 

このような結果になります。ローカル変数を確保しているsubl命令のオペランド、$264に注目。$256じゃないんですよね。


これはなぜかと言うと、gccはデフォルトで、スタックの末尾を16バイト境界にアラインさせるからです。sublした直後の段階で%espの値が16で割り切れる値になるっつーことですね。call命令(%eip退避)と関数先頭のpushl(%ebp退避)で、それぞれ4バイトづつ%espが減算されるんで、sublする値は常に、16で割ると8余る数字になります。264 == 16*16 + 8 なのでビンゴですね。

void func(void) { char x[256]; int y; } 

だと、subl $280, %esp になります。280-8は16の倍数なので上の説明どおりではありますが、$264 のままでも256+4バイトは収まるので、$280に増えてしまう理由は不明です。GCCのソース読まないとわからんってのが私の調べた範囲での皆様のご意見のようです。ソース読む元気はアリマセン。


sublした領域をどのように各自動変数に割り当てるか、どうpaddingするかはGCCの気分次第ということで。


なお、16バイトアラインという動作を変更することは可能で、

gcc -mpreferred-stack-boundary=N ...

などとすればOKでございます。これで、スタックが2^Nバイトにアラインするようになります。x86でのデフォルトは(-Osで最適化した時を除いて)16バイトアラインですから、上記オプションに4を与えたのと同じ状態です。


GCCのマニュアルを読むと、組み込み用途などで、速度より省メモリが重要な場面では -mpreferred-stack-boundary=2 がお勧めとあります。上記の2例でも、N=2とすればスタック確保量はそれぞれ256,260バイトとCソースコード上の値とぴったり一致するようになります。


このオプションを使うときは一つだけ注意点があり、それは「Nの小さいコードからNの大きいコードを呼ぶと、そこから先はずっとミスアライメントしたままになる」という点です。ですから、(ユーザコードを呼び返す可能性のある)ライブラリはデフォルトのアライメントでコンパイルされるべき、だそうで。

To ensure proper alignment of this values on the stack, the stack boundary must be as aligned as that required by any value stored on the stack. Further, every function must be generated such that it keeps the stack aligned. Thus calling a function compiled with a higher preferred stack boundary from a function compiled with a lower preferred stack boundary will most likely misalign the stack. It is recommended that libraries that use callbacks always use the default setting.

まぁ、gccはたまに andl $-16, %esp のようなコードを挿入し、%espの下位4ビットをクリアすることで無理矢理アライメントを修正したりするので、、案外影響は少なめだったりするのかもしれませんが。


あ、上記のアラインメントの調整の件ですが、もう少しだけ詳しく書いておきます。g++ -S でアセンブリリストを見ていると、よくわからないスタックポインタ操作が散見されると思います。

andl    $-16, %esp

こういうヤツです。これがまさに「調整」のためのコードなんですね。スタックポインタの値をキリのいいところにそろえ(16バイトアライン)、速度を速めることを狙っているんですね〜。-16は0xfffffff0だから、andlすると%espの値は最大で15減る。%espは、増やしちゃまずいが減らす分にはOKだから、アラインさせるにはこれで問題ないです。


以上

*1:sublの減算が$260より大きいよ!という方はこの記事の末尾付近を参照いただき、gcc -mpreferred-stack-boundary=2 -O0 -S で再コンパイルしてみてくださいませ・・。また、 -fomit-frame-pointer でコンパイルされると、%ebpのスタックへの退避が完全に省略され、call命令前後での%espに対する即値の加算減算操作で代替されるようになります。このオプションは外してください。逆に、このオプションが使用されているとoff-by-oneに対する攻撃は成功しなくなります :-)