.section .rodata .LC0: .string "%32s" .LC1: .string "%s 0x%x\n" .type .LSSH0, @object .size .LSSH0, 5 .LSSH0: .string "main" .text .globl main .type main, @function main: .LFB3: pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: subq $80, %rsp .LCFI2: movl %edi, -68(%rbp) ← main 関数の第1引数 argc movq %rsi, -80(%rbp) ← main 関数の第2引数 argv movq __guard_local(%rip), %rax ← canary 初期化 movq %rax, -8(%rbp) ← canary 初期化 xorl %eax, %eax movl $-1414717974, -52(%rbp) ← ローカル変数 x の初期化 leaq -48(%rbp), %rsi ← scanf 関数の第2引数 buf leaq .LC0(%rip), %rdi ← scanf 関数の第1引数 "%32s" movl $0, %eax call scanf@PLT ← scanf 関数呼び出し movl -52(%rbp), %edx ← printf 関数の第3引数 x leaq -48(%rbp), %rsi ← printf 関数の第2引数 buf leaq .LC1(%rip), %rdi ← printf 関数の第1引数 "%s 0x%x\n" movl $0, %eax call printf@PLT ← printf 関数呼び出し movl $0, %eax movq -8(%rbp), %rdx ← canary チェック xorq __guard_local(%rip), %rdx ← canary チェック je .L3 leaq .LSSH0(%rip), %rdi call __stack_smash_handler@PLT .L3: leave ret
このアセンブラーコードによると、スタック上のデータ配置は次のようになっています(図1)。
図1でguardと記載した領域が、先ほどcanaryと呼んでいたものです。アセンブラーコードの最後のところで、元の値と同じなのかどうかをチェックしています。
そしてxとbufの位置関係に注目してください。元のソースコードでは、最初にxの宣言、次にbufの宣言がなされていました。この順序に基づいて単純にスタック上に割り当てを行うと、xの方が後ろ、より高位のアドレスに置かれるはずです。スタックは後ろから前に向かって伸びていくからです。
しかし、実際にはスタックの後ろの方に置かれているのはbufです。これは、配列へのデータ書き込みでバッファオーバーフローが発生しやすいことを踏まえ、他のローカル変数への影響を小さくしようという工夫の1つといえるでしょう。
そしてbufの後ろ、canary(guard)の前には8バイトの空き領域があります。このため、1バイト程度の上書きでは検知できないわけです。
参考までに、コンパイル時オプションに“-fno-stack-protector”を付けて、スタックバッファオーバーフロー検知のコードを省略した場合のアセンブラーコードも見てみましょう。
.section .rodata .LC0: .string "%32s" .LC1: .string "%s 0x%x\n" .text .globl main .type main, @function main: ← main 関数のエントリポイント .LFB3: pushq %rbp .LCFI0: movq %rsp, %rbp .LCFI1: subq $64, %rsp .LCFI2: movl %edi, -52(%rbp) ← main 関数の第1引数 argc movq %rsi, -64(%rbp) ← main 関数の第2引数 argv movl $-1414717974, -4(%rbp) ← ローカル変数 x の初期化 leaq -48(%rbp), %rsi ← scanf 関数の第2引数 buf leaq .LC0(%rip), %rdi ← scanf 関数の第1引数 “%32s” movl $0, %eax call scanf@PLT ← scanf 関数呼び出し movl -4(%rbp), %edx ← printf 関数の第3引数 x leaq -48(%rbp), %rsi ← printf 関数の第2引数 buf leaq .LC1(%rip), %rdi ← printf 関数の第1引数 “%s 0x%x\n” movl $0, %eax call printf@PLT ← printf 関数呼び出し movl $0, %eax leave ret
このアセンブラーコードでは、スタック上のデータ配置は次のようになります(図2)。
この場合、bufとxの間には12バイトの空き領域があることが分かります。ここでbufの領域を超えて書き込みが行われたとしても、12バイトまでならば、プログラムは問題なく動作することになります。
Copyright © ITmedia, Inc. All Rights Reserved.