バッファオーバーフローでeipが書き換えられない
自分の環境で脆弱性のあるプログラムを再現できなかった
pwnの練習をしようとして脆弱性のあるプログラムを書いたら、何故かその脆弱性を(幸か不幸か)exploitできなかった。 解決するまでに結構かかってしまった上、日本語の情報が見つからなかったから残しておく。 参考というか、丸パクリ元はここ。
環境
自分の環境は以下の通りである。
x86_64 GNU/Linux Ubuntu LTS-16.04
やろうとしたこと
やろうとしていたことはバッファオーバーフローによるeipを書き換えだったけど、実際にやるとクラッシュしてもeipが書き換えられてなかった。 プログラム自体はfgetsしてバッファ書き換えるだけの簡単もので、32bitアーキテクチャでSSPは無効にしていた。 ASLRは無効にしてなかったけど、gdbで実行していたので無効の状態のはず。
#include <stdio.h> int main(int argc, char** argv){ char buffer[32]; fgets(buffer, 128, stdin); return 0; }
コンパイルは、gcc -m32 -fno-stack-protector -o overflow overflow.c
gdbで調べたり、逆アセンブルして解析した結果、どうもなんらかのセキュリティが働いているような感じだった。 逆アセンブルしてみてみると、main関数の最初とret周りの処理が怪しい。
0804843b <main>: 804843b: 8d 4c 24 04 lea ecx,[esp+0x4] ;; 1 804843f: 83 e4 f0 and esp,0xfffffff0 8048442: ff 71 fc push DWORD PTR [ecx-0x4] 8048445: 55 push ebp 8048446: 89 e5 mov ebp,esp 8048448: 51 push ecx 8048449: 83 ec 24 sub esp,0x24 804844c: a1 20 a0 04 08 mov eax,ds:0x804a020 8048451: 83 ec 04 sub esp,0x4 8048454: 50 push eax 8048455: 68 80 00 00 00 push 0x80 804845a: 8d 45 d8 lea eax,[ebp-0x28] 804845d: 50 push eax 804845e: e8 ad fe ff ff call 8048310 <fgets@plt> 8048463: 83 c4 10 add esp,0x10 8048466: b8 00 00 00 00 mov eax,0x0 804846b: 8b 4d fc mov ecx,DWORD PTR [ebp-0x4] ;; 2 804846e: c9 leave 804846f: 8d 61 fc lea esp,[ecx-0x4] ;; 3 8048472: c3 ret 8048473: 66 90 xchg ax,ax 8048475: 66 90 xchg ax,ax 8048477: 66 90 xchg ax,ax 8048479: 66 90 xchg ax,ax 804847b: 66 90 xchg ax,ax 804847d: 66 90 xchg ax,ax 804847f: 90 nop
- 1で[esp+0x4]をecxに格納
- 2でecxが復帰
- 3のret前でespに[ecx-0x4]を格納
こんな感じの処理がされているらしい。 つまり、一番最初に格納したスタックポインタの値をleaveとretの間で復帰させることで、書き換えられたリターンアドレスがeipに格納されることを防いでいるのではないかと思われる。
また、callした後のスタックの状態は以下のようになるので、ebpを変更せずにリターンアドレスを変更することはできない(ebpの整合性を保つような値で上書きすることは可能かな?)。 したがって、リターンアドレスがバッファオーバーフローで書き換えられていると、ebpが異常値になって正しくespを復帰させられなくなる。 その結果としてeipが書き換えられなくなっている。
stack |
---|
argn |
... |
arg1 |
return-address |
saved-ebp |
local-variable |
解決策(?)
解決というか問題の再発というか、脆弱性を仕込むことができなければ練習できないので調べた。 幸いにも参考にしたページのコメントに解決できそうなやり方が載っていたので、ありがたく参考にさせてもらう。 このespの保存と復帰はmain関数でしか行なわれないらしいので、別の関数に脆弱性のある処理を書いてmainから呼べばいいらしい。
早速試す。 プログラムを書き換えて、以下のような感じにした。 main関数の中身を丸ごと取り出しただけで、処理は同じになる。
#include <stdio.h> void vulnerable_func(){ char buffer[32]; fgets(buffer, 128, stdin); return; } int main(int argc, char** argv){ vulnerable_func(); return 0; }
gdbで実行し、50文字の文字列を入力するとeipが書き変わっているのを確認できた。 これで問題なく練習できるはず。