連載
» 2014年01月23日 18時00分 公開

動的メモリ管理に関する脆弱性もいちど知りたい、セキュアコーディングの基本(6)(2/4 ページ)

[戸田洋三(JPCERT/CC),@IT]
	.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
オプション "-fstack-protector-all" が有効な場合のアセンブラーコード

 このアセンブラーコードによると、スタック上のデータ配置は次のようになっています(図1)。

図1 OpenBSD/amd64でコンパイルした場合のデータ配置

 図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
オプション “-fno-stack-protector”付きでコンパイルした場合のアセンブラーコード

このアセンブラーコードでは、スタック上のデータ配置は次のようになります(図2)。

図2 スタックバッファオーバーフロー検知なしの場合のデータ配置

 この場合、bufとxの間には12バイトの空き領域があることが分かります。ここでbufの領域を超えて書き込みが行われたとしても、12バイトまでならば、プログラムは問題なく動作することになります。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。