高林の雑記ブログ

こんにちは。

バッファオーバーフロー

はい、皆さんこんばんは。

今回は、手元の環境で動作を確認しやすいバッファオーバーフローという脆弱性について述べたい。
バッファオーバーフローとは、メモリ上のバッファに、不正なシェルコードなどの入力をすることで、その実行位置IPを奪う攻撃である。
スタックを用いた攻撃の他に、ヒープや静的領域などを用いた攻撃が存在するが、今回はスタックを用いた攻撃について詳しく述べたい。

スタックベースのバッファオーバーフロー

まずは、スタックを用いたバッファオーバーフローの原理について述べる。
ユーザーの作ったプログラムが実行されるとき、main関数のローカル変数は、スタック上に配置される。スタックにはほかにも、関数を呼び出す際の引数や、その関数を呼び出した後に戻ってくるためのリターンアドレスや、関数プロローグに用いられるebpの値などが積まれている。

これを踏まえて、もしローカル変数があふれた場合にどうなるかについて考える。そのあふれた入力は、そのローカル変数に隣接する無関係の領域に書き込まれてしまう。例えば、入力に使われるローカル変数の近くに、リターンアドレスがある場合、そこに別の関数へのアドレスを書いておくことで、別の処理をさせることができるのである。

ローカル変数の破壊

次に、具体的な例を示す。
まずはローカル変数が破壊できていることを確認してみる。なお、以下ではSSPを無効としてコンパイルする。
コンパイラgccを用いる。
そのために、まずはbof1.cというプログラムを作成する。
今回は、vimで書く。

f:id:kakyouim:20190523205001p:plain
bof1.c

bof1.cはバッファのサイズを10バイトとしている。

次に、実行結果を確認する。
実行すると次のようになった。


f:id:kakyouim:20190523205749j:plain

まず、普通に10バイト以内の入力を入れると、zero変数には、0が値として入っていることがわかる。
次に、10バイト以上の入力を与えると、zero変数には1111638594という値が入っている。
これは、buffer変数からあふれた入力が、隣にあるzero変数の領域に書き込まれたことを示している。
つまり、書き込まれた0xBBBBを十進数に直してintにすると0xBBBB=0x42424242=1111638594となる。
Segmantation faultを起こしているので、gdbでどの部分がエラーになっているか確認してみる。

f:id:kakyouim:20190523210410j:plain
すると、EIPが0x0となっている。つまり、正しい実行位置にないためSegmentation faultを起こしている。
次に、入力を先ほどよりも多くすると、次のようになった。


f:id:kakyouim:20190523210607j:plain
この時のEIPは0x565555fbとなっている。
それでは、その部分のアセンブリをradare2を用いてみてみる。


f:id:kakyouim:20190523210723j:plain

すると、ff invalidとなっている。
X86の公式リファレンスでこの意味を確認すると、オペコードが無効となっていることがわかる。
ここから、やはり実行位置がただしく配置されなかったことがわかる。

このような方法で、変数の値を書き換えるような攻撃に対しては、変数の順序に気を付ける、入力文字数をチェックするなどの対策があげられる。
順序を変えれば、zero変数はbuffer変数より下のアドレスに配置されるため、buffer変数からあふれた入力がzero変数の領域に来ることはなくなる。

リターンアドレスの書き換え

次に、リターンアドレスを書き換える攻撃について述べる。次のようにbof2.cを作成する。


f:id:kakyouim:20190523211419p:plain

これは、グローバル変数のbuffer変数がstrcpy関数で、local変数からはみ出して上書きされてしまう脆弱性を持ったプログラムである。
リターンアドレスが書き換えられると、ret命令で、espが指しているアドレスに格納されているリターンアドレスをeipレジスタに書き込むときに、書き換えられた値をeipに書き込んでしまうからである。

では、このプログラムに不正な入力である、Aを32文字以上入れてみる。
実行結果は次の通りである。

f:id:kakyouim:20190523211641j:plain

すると、eipには0x41414141となるはずである。
ところが、実際にgdbで見てみると、eipには0x56555614となっており、ret命令から進んでいないことがわかる。

次に、この原因を探るためにmain関数を逆アセンブルしてみる。

f:id:kakyouim:20190523211740p:plain

すると、retの前にlea esp,[ecx-0x4]という記述がみられる。
この時、espに書き換えられたリターンアドレスが存在するアドレスが入っていれば、eipを奪えるはずである。
では、ecxについてみてみると、pop ecxが存在する。
さて、実際にバッファオーバーフローを発生させると、スタック上のリターンアドレスに書き込まれるが、その時に同時にpop ecxでespが指し示している値(ecxに代入される値)も書き変わる。
pop ecxでecxに代入される値が異常値になり、最終的なlea esp, [ecx-0x4]で代入されるespの値も異常値になる。
したがって、バッファオーバーフローSSP無効の状態でも検出することができている。

それでは、プログラムのeipは奪えないのかというと、そんなことはなくて、プログラムをbof2_2.cのように書き換えると、eipを奪うことができた。

f:id:kakyouim:20190523211934p:plain

Bof2_2は、main関数内の処理を別の関数から呼び出したものである。
このプログラムをgdbで実行すると、図のようになった。

f:id:kakyouim:20190523212104p:plain

ここから、eipには0x41414141という値が書き込まれていることがわかる。
このプログラムの逆アセンブル結果は次のようになった。

f:id:kakyouim:20190523212251p:plain

ここでgdbのesp, ecxの値を見ると、先ほどとは違い、有効なアドレスが書き込まれていることがわかる。よってバッファオーバーフローは検出されない。
この理由としては、gccコンパイルしたため、main関数でほかの関数を呼び出すときにgccがこういうコンパイルの仕方をするから、と考えられる。
それでは、この時にeipを書き換えて実行を操作してみる。
そのためにまずオーバーフローした入力の何文字目がeipに書きこまれるのかを知るために、pedaをgithubからcloneして使う。
Pedaのpattern_create、pattern_offsetを使うことで入力の何文字目かわかる。

f:id:kakyouim:20190524133738p:plain

ここで実行してみると、eipにはAFAAが書き込まれているため、44文字目から書き込まれていることがわかる。
よって、これ以降にアドレスを書き込めばよい。
リターンアドレスにmain関数のはじめの値を書き込めば、main関数が二回実行されるはずである。
gdbでmain関数のはじめのアドレスを確認すると、0x56555601となっている。
入力するときに、0x01に対応する文字列は存在しないため、echoコマンドを用いて入力する。ここでは、
Echo -e ‘AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x01\x56\x55\56’ | ./bof2_2 を実行した。

f:id:kakyouim:20190524133808p:plain

しかし、main関数は二回呼び出されず、Segmentation faultとなった。
この理由は、pieが有効になっているからと考えられる。
PIEは、実行されるファイルのアドレスがランダムになるものである。一方、ASLRはスタック領域・ヒープ領域・共有ライブラリが置かれるアドレスがランダムになるものである。
つまり、今回の場合は、main関数の最初のアドレスはプログラムを実行するときに毎回変化していると考えられる。

PIEが有効になっているかどうかは、readelf -a bof2_2 | lessを実行することで確認できるはずである。


f:id:kakyouim:20190524133846p:plain

これを実行すると、型のところに「DYN(共有オブジェクトファイル)」という記述がみられる。動的リンクは、共有ライブラリを動的に配置するものであるため、ここではファイルを動的に配置している、つまりファイルをランダムに配置していると考えられる。また、これはfileコマンドでも「shared object」という記述があることから確認できる。

f:id:kakyouim:20190524133929p:plain

また、checksec.shというファイルの持つセキュリティ機構を表示するシェルスクリプトを用いると、次のようになった。

f:id:kakyouim:20190524134308p:plain

PIEの部分は「Not an ELF FILE」というエラーが出ている。fileコマンドからもELFであることはわかるので、この記述はおかしい。
この原因はおそらくchecksecが壊れているからだと考えられる。

よって、この場合はアドレスのリークをすることで、eipにmain関数の値を書き込むことができる。
その方法としては、別のターミナルでbof2_2を実行した状態で、sudo gdb -q -p ‘pidof bof2_2’を実行して、gdb-peda$x/10xi main を実行すると0x56562665であることがわかる。ここで、ファイルの位置がランダムになっていることがわかる。


f:id:kakyouim:20190524135034p:plain

しかし、gdb上で入力すると0x9dなどは入力できない。しかし、プログラムを実行させてアドレスをリークさせてから入力する必要がある。
そのため、socatコマンドを用いて、仮想的にサーバー上でプログラムが待機しているようにして、そのポートにnetcatでアクセスすることで実行することを考える。

f:id:kakyouim:20190524135058p:plain

今回は、ncコマンドの代わりに、pythonなどで、リモートエクスプロイトをするコードを作成することで実現できる。

それでは、リターンアドレスを書き換えるような脆弱性の対策について考える。
先述のアセンブリバッファオーバーフローを防ぐ機能を持っていたbof2.cはセキュリティが完全かというとそういうわけではない。
今回のアセンブリの場合は、lea esp,[ecx-0x4]のecxの値を正しく書き込めればespが正しくなり、リターンアドレスを書き換えた状態でeipを奪うことができるはずである。


参考資料

https://raintrees.net/projects/a-painter-and-a-black-cat/wiki/CTF_Pwn
https://ja.wikipedia.org/wiki/バッファオーバーラン
http://www.ref.x86asm.net/
https://hiziriai.hatenablog.com/entry/2017/05/17/234505
https://tkmr.hatenablog.com/entry/2017/02/28/030528
https://tkmr.hatenablog.com/entry/2017/02/28/030528
https://teratail.com/questions/48377
http://warabanshi.hatenablog.com/entry/2013/05/18/231628
「HACKING: 美しき策謀」 Jon Ericckson著、村上雅章 訳 オライリージャパン
「セキュリティコンテストチャレンジブック」碓井利宣 竹迫良範 廣田一貴 保要隆明 前田優人 美濃圭佑 三村聡志 八木橋優 著 マイナビ出版
「セキュリティコンテストのためのCTF問題集」清水祐太郎 竹迫良範 新穂隼人 長谷川千広 廣田一貴 保要隆明 美濃圭佑 三村聡志 森田浩平 八木橋優 渡部裕 著 マイナビ出版