**回答の一部を修正し、追記しました:**2019/05/09 00:22
mainの中でバッファオーバーフローをしようとすると、EIPを書き換える直前でESPの書き換わるような挙動
...
これは何かのセキュリティが働いているのでしょうか?
セキュリティの問題ではなく、bof.c のmain
関数で実装しているコードに問題があり意図する動きを阻害しています。逆アセンブルした結果で、ret
直前のコードが
0x5655622d <+116>: lea esp,[ecx-0x4] <= この部分でespに[ecx - 0x4](0x4141413d)がロードされる
0x56556230 <+119>: ret
とのことでしたが、esp
に不正な値がロードされていることで、ret
命令の実行時に不正なアドレスのスタック領域、ss:[0x4141413d]をアクセスしてeip
にロードしようとするので、SIGSEGVが発生し、ret
が完了せず、結果としてeip
に所望の値、すなわち0x4141....がロードできずに終わっています。
シンプルな例で行うとmain
のreturn時にeip
が書き換わることを確認できます。
C
1 /* t7.c simple bof */
2 int i ; /* ※ループ実行時に破壊されないよう、static領域に配置*/
3 int main ( int argc , char * argv [ ] ) {
4 char s [ 8 ] ;
5 for ( i = 0 ; i < 128 ; i ++ ) {
6 s [ i ] = 'A' ;
7 }
8
9 return 0 ;
10 }
以下、上のソースコードをUbuntu16.04 (x64)の環境でコンパイルしてgdbで追った例です。(長くなりますがご容赦ください)return 0;
のところにブレイクポイントを張り、runします。
bash
1 user01@ubuntu1604-x64:~$ gdb ./t7
2 GNU gdb ( Ubuntu 7.11 .1-0ubuntu1~16.5 ) 7.11 .1
3 Copyright ( C ) 2016 Free Software Foundation, Inc.
4 License GPLv3+: GNU GPL version 3 or later < http://gnu.org/licenses/gpl.html >
5 This is free software: you are free to change and redistribute it.
6 There is NO WARRANTY, to the extent permitted by law. Type "show copying"
7 and "show warranty" for details.
8 This GDB was configured as "x86_64-linux-gnu" .
9 Type "show configuration" for configuration details.
10 For bug reporting instructions, please see:
11 < http://www.gnu.org/software/gdb/bugs/ > .
12 Find the GDB manual and other documentation resources online at:
13 < http://www.gnu.org/software/gdb/documentation/ > .
14 For help, type "help" .
15 Type "apropos word" to search for commands related to "word" .. .
16 Reading symbols from ./t7 .. .done.
17 ( gdb ) list
18 1 int i ;
19 2 int main ( int argc, char *argv [ ] ) {
20 3 char s [ 8 ] ;
21 4 for ( i = 0 ; i < 128 ; i++ ) {
22 5 s [ i ] = 'A' ;
23 6 }
24 7
25 8 return 0 ;
26 9 }
27 10
28 ( gdb ) b 8
29 Breakpoint 1 at 0x804840e: file t7.c, line 8 .
30 ( gdb ) run
31 Starting program: /home/user01/t7
ブレイクした時点でスタックが0x41
で上書きされているので、argc
やargv
が0x41...に書き換わっていることも分かります。
Breakpoint 1, main (argc=1094795585, argv=0x41414141) at t7.c:8
8 return 0;
(gdb) disassemble
Dump of assembler code for function main:
0x080483db <+0>: push %ebp
0x080483dc <+1>: mov %esp,%ebp
0x080483de <+3>: sub $0x10,%esp
0x080483e1 <+6>: movl $0x0,0x804a01c
0x080483eb <+16>: jmp 0x8048404 <main+41>
0x080483ed <+18>: mov 0x804a01c,%eax
0x080483f2 <+23>: movb $0x41,-0x8(%ebp,%eax,1)
0x080483f7 <+28>: mov 0x804a01c,%eax
0x080483fc <+33>: add $0x1,%eax
0x080483ff <+36>: mov %eax,0x804a01c
0x08048404 <+41>: mov 0x804a01c,%eax
0x08048409 <+46>: cmp $0x7f,%eax
0x0804840c <+49>: jle 0x80483ed <main+18>
=> 0x0804840e <+51>: mov $0x0,%eax
0x08048413 <+56>: leave
0x08048414 <+57>: ret
End of assembler dump.
(gdb) info r
eax 0x80 128
ecx 0x2fa1db1e 799136542
edx 0xffffdc14 -9196
ebx 0x0 0
esp 0xffffdbd8 0xffffdbd8
ebp 0xffffdbe8 0xffffdbe8
esi 0xf7fb2000 -134537216
edi 0xf7fb2000 -134537216
eip 0x804840e 0x804840e <main+51>
eflags 0x212 [ AF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
ここからgdbのstepi
コマンドでret
直前までステップ実行していきます。
bash
1 ( gdb ) stepi
2 9 }
3 ( gdb ) info r
4 eax 0x0 0
5 ecx 0x2fa1db1e 799136542
6 edx 0xffffdc14 -9196
7 ebx 0x0 0
8 esp 0xffffdbd8 0xffffdbd8
9 ebp 0xffffdbe8 0xffffdbe8
10 esi 0xf7fb2000 -134537216
11 edi 0xf7fb2000 -134537216
12 eip 0x8048413 0x8048413 < main+5 6 >
13 eflags 0x212 [ AF IF ]
14 cs 0x23 35
15 ss 0x2b 43
16 ds 0x2b 43
17 es 0x2b 43
18 fs 0x0 0
19 gs 0x63 99
20 ( gdb ) disassemble
21 Dump of assembler code for function main:
22 0x080483db < + 0 > : push %ebp
23 0x080483dc < + 1 > : mov %esp,%ebp
24 0x080483de < + 3 > : sub $0x10 ,%esp
25 0x080483e1 < + 6 > : movl $0x0 ,0x804a01c
26 0x080483eb < +1 6 > : jmp 0x8048404 < main+4 1 >
27 0x080483ed < +1 8 > : mov 0x804a01c,%eax
28 0x080483f2 < +2 3 > : movb $0x41 ,-0x8 ( %ebp,%eax,1 )
29 0x080483f7 < +2 8 > : mov 0x804a01c,%eax
30 0x080483fc < +3 3 > : add $0x1 ,%eax
31 0x080483ff < +3 6 > : mov %eax,0x804a01c
32 0x08048404 < +4 1 > : mov 0x804a01c,%eax
33 0x08048409 < +4 6 > : cmp $0x7f ,%eax
34 0x0804840c < +4 9 > : jle 0x80483ed < main+1 8 >
35 0x0804840e < +5 1 > : mov $0x0 ,%eax
36 = > 0x08048413 < +5 6 > : leave
37 0x08048414 < +5 7 > : ret
38 End of assembler dump.
39
40 ( gdb ) stepi
41 0x08048414 9 }
42 ( gdb ) disassemble
43 Dump of assembler code for function main:
44 0x080483db < + 0 > : push %ebp
45 0x080483dc < + 1 > : mov %esp,%ebp
46 0x080483de < + 3 > : sub $0x10 ,%esp
47 0x080483e1 < + 6 > : movl $0x0 ,0x804a01c
48 0x080483eb < +1 6 > : jmp 0x8048404 < main+4 1 >
49 0x080483ed < +1 8 > : mov 0x804a01c,%eax
50 0x080483f2 < +2 3 > : movb $0x41 ,-0x8 ( %ebp,%eax,1 )
51 0x080483f7 < +2 8 > : mov 0x804a01c,%eax
52 0x080483fc < +3 3 > : add $0x1 ,%eax
53 0x080483ff < +3 6 > : mov %eax,0x804a01c
54 0x08048404 < +4 1 > : mov 0x804a01c,%eax
55 0x08048409 < +4 6 > : cmp $0x7f ,%eax
56 0x0804840c < +4 9 > : jle 0x80483ed < main+1 8 >
57 0x0804840e < +5 1 > : mov $0x0 ,%eax
58 0x08048413 < +5 6 > : leave
59 = > 0x08048414 < +5 7 > : ret
60 End of assembler dump.
61 ( gdb ) info r
62 eax 0x0 0
63 ecx 0x2fa1db1e 799136542
64 edx 0xffffdc14 -9196
65 ebx 0x0 0
66 esp 0xffffdbec 0xffffdbec
67 ebp 0x41414141 0x41414141
68 esi 0xf7fb2000 -134537216
69 edi 0xf7fb2000 -134537216
70 eip 0x8048414 0x8048414 < main+5 7 >
71 eflags 0x212 [ AF IF ]
72 cs 0x23 35
73 ss 0x2b 43
74 ds 0x2b 43
75 es 0x2b 43
76 fs 0x0 0
77 gs 0x63 99
78
79 ( gdb ) x/20xb 0xffffdbec
80 0xffffdbec: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
81 0xffffdbf4: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
82 0xffffdbfc: 0x41 0x41 0x41 0x41
この時点でebp
は0x4141...になっていますが、esp
は壊されていません。なので、ret
できます。
ここからgdb で再度stepiコマンドを実行し、ret
命令が実行されると、eip
が書き換わっているのが分かります。
bash
1 ( gdb ) stepi
2 0x41414141 in ?? ( )
3 ( gdb ) stepix/20bx 0xffffdbec 0xffffdbecb 20info r
4 eax 0x0 0
5 ecx 0x2fa1db1e 799136542
6 edx 0xffffdc14 -9196
7 ebx 0x0 0
8 esp 0xffffdbf0 0xffffdbf0
9 ebp 0x41414141 0x41414141
10 esi 0xf7fb2000 -134537216
11 edi 0xf7fb2000 -134537216
12 eip 0x41414141 0x41414141
13 eflags 0x212 [ AF IF ]
14 cs 0x23 35
15 ss 0x2b 43
16 ds 0x2b 43
17 es 0x2b 43
18 fs 0x0 0
19 gs 0x63 99
eip
=0x4141...のまま続行すると、当然SIGSEGVが発生します。
bash
1 ( gdb ) stepi
2
3 Program received signal SIGSEGV, Segmentation fault.
4 0x41414141 in ?? ( )
5 ( gdb ) quit
**以下、追記部分です:**2019/05/09 00:22
mainの中でバッファオーバーフローをしようとすると、EIPを書き換える直前でESPの値が書き換わるような挙動を起こしてしまうためEIPを書き換えられません。
追って調べたところ、言葉のとおりでした。main
関数に限って、ret
直前に lea esp, [ecx-4]
のコードが入る場合と入らない場合があります。質問者であるkonataroさんのC言語コードでは入ってしまい、拙作の例示用C言語コードでは入りません。その為、動きに差が出ています。
bof.c を"-S"オプション付きでコンパイルしてアセンブリ言語ソースで出力すると、main
関数のret
命令付近のエピローグコードは、以下のようになります。注目すべきコードであるleal -4(%ecx), %esp
が入っています。(質問者のkonataroさんが既に提示されたものとほぼ等価なものですが、説明の為に掲げています)
call strcpy
addl $16, %esp
.loc 1 13 0
movl $0, %eax
.loc 1 14 0
movl -4(%ebp), %ecx
.cfi_def_cfa 1, 0
leave
.cfi_restore 5
leal -4(%ecx), %esp
.cfi_def_cfa 4, 4
ret
.cfi_endproc
これをkonataroさんが既に指摘されているように、main
ではなく、普通の関数func
に変えてコンパイルすると、
C
1 //bof.c
2 # include <stdio.h>
3 # include <string.h>
4
5 char buffer [ 32 ] ;
6
7 int func ( int argc , char * argv [ ] )
8 {
9 char local [ 32 ] ;
10 printf ( "buffer 0x%x\n" , & buffer ) ;
11 fgets ( local , 128 , stdin ) ;
12 strcpy ( buffer , local ) ;
13 return 0 ;
14 }
15
16 int main ( int argc , char * argv [ ] )
17 {
18 return func ( argc , argv ) ;
19 }
アセンブリ言語のソースは、以下のように変わります。leal -4(%ecx), %esp
は消滅しています。
call strcpy
addl $16, %esp
.loc 1 13 0
movl $0, %eax
.loc 1 14 0
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.cfi_restore
とは何か?をWEBで検索してみると以下のas(gas)のドキュメントがヒットし、
7.10 CFI directives
内容として下の説明がありました。
7.10.17 .cfi_restore register
.cfi_restore says that the rule for register is now the same as it was at the beginning of the function, after all initial instruction added by .cfi_startproc were executed.
どうも、このディレクティブで関数先頭でのレジスターの値と同じようにすることを指示するもののようで、main
内で関数を呼び出すコードを書くとleal -4(%ecx), %esp
のコードが挟み込まれます。拙作のmain
の例示コードでは関数を呼び出さない為、目的を達成できるコードが生成できたようです。あくまで勝手な推測ですが、SSPなどの仕組みの一部で、main
を呼び出したCランタイムのスタートアップルーチンに正しく戻れるよう保証する動作の一端でしょうか。少し検索した限りでは、なぜそうなっているかを説明しているgccやbinutilのドキュメントは見つけることはできませんでした。この辺り、gcc周りの事情にお詳しい方にフォローや突っ込みをお願いしたいところです。
ですので、少なくとも本質問の内容の限りにおいては、gccに"-fno-stack-protector"オプションを追加した上で、
main関数以外の関数内に目的のコードを書く。
main関数内に書くときは、標準関数を含む他の関数を呼び出さない。
ようにすると所望の動作をするコードになるので、それを踏まえてコードを書く必要があると考えます。
私自身の参考として - IPA - 第10章 著名な脆弱性対策 - バッファオーバーフロー: #5 運用環境における防御