動的メモリ管理に関する脆弱性:もいちど知りたい、セキュアコーディングの基本(6)(4/4 ページ)
本連載の第2回と第3回では主にスタックバッファオーバーフローについて説明しました。今回はそれに関連して、スタックバッファオーバーフロー検知の仕組みに関する話を紹介し、その後、動的メモリ管理に関連する脆弱性を見ていきたいと思います。
単純なヒープバッファオーバーフロー
動的メモリ管理に関連する脆弱性として、まずはヒープ領域上で単純なメモリ上書きが発生するケースについて見てみましょう。ヒープ領域を使ったサンプルプログラムを以下に示します。せっかくなので、関数ポインターを使ったコードにしてみました。
#include <stdio.h> #include <stdlib.h> #include <malloc.h> void yes(){ printf("YES!\n"); } void no(){ printf("NO!\n"); } struct ftable { char resp[4]; void (*Y)(); void (*N)(); }; int main(int argc, char *argv[]){ // init struct ftable *c = malloc(sizeof (struct ftable)); if (!c){ exit(10); } c->Y = &yes; c->N = &no; scanf("%s", c->resp); // printf("%s\n", c->resp); // printf("yes(%x) at (c->Y)(%x)\n", (c->Y), &(c->Y)); // printf("no(%x) at (c->N)(%x)\n", (c->N), &(c->N)); if(0 == strncmp("yes", c->resp, 3)){ (c->Y)(); } else { (c->N)(); // free(c); } free(c); }
ユーザーの入力を読み取り、それが「yes」だったらyes()関数を実行して「YES!」を出力し、それ以外であればno()関数を実行して「NO!」を出力します。
このサンプルプログラムの問題は、scanf()関数で入力を読み取っている部分にあります。入力をコピーする先はc->respで4バイトしか用意していません。ところが、scanf()の書式指定文字列は「%s」なので、入力が続く限り読み込んでしまいます。構造体ftableには文字配列respの後ろに関数ポインターYとNが配置されます。つまり、scanf()でバッファオーバーフローが起こると、YやNが上書きされることになるのです。(図4)
サンプルコードでコメントアウトしてあるprintf()が幾つかありますが、これらの出力を有効にして4バイト以上の長い入力を幾つか処理させてみると、ポインターの値がどのように影響されるかが分かります。ここでは32ビット(i386)Linux環境で試しています。
$ echo "yes" | ./heap_of0 yes(8048454) at (c->Y)(804a00c) no(8048468) at (c->N)(804a010) YES! $ echo "no" | ./heap_of0 yes(8048454) at (c->Y)(804a00c) no(8048468) at (c->N)(804a010) NO! $
ちゃんと入力に応じて「YES!」や「NO!」が出力されているようですね。この出力例からは、
- 関数ポインターYのアドレス: 0x804a00c
- 関数ポインターNのアドレス: 0x804a010
- yes()関数のエントリポイント: 0x8048454
- no()関数のエントリポイント: 0x8048468
といったことが分かります。
4文字以上の入力ではどうでしょう。
$ echo "yes0" | ./heap_of0 ← 4文字入力 yes(8048400) at (c->Y)(804a00c) no(8048468) at (c->N)(804a010) $ echo "yes00" | ./heap_of0 ← 5文字入力 yes(8040030) at (c->Y)(804a00c) no(8048468) at (c->N)(804a010) Segmentation fault $ echo "yes000" | ./heap_of0 ← 6文字入力 yes(8003030) at (c->Y)(804a00c) no(8048468) at (c->N)(804a010) Segmentation fault $
入力する文字列が4文字、5文字と増えていくと、Yの値が侵食されていくのが分かります。5文字以上の入力ではプログラムが異常終了しています。
ではYの部分を丸ごと上書きしたらどうでしょうか。細工した入力を作って処理させてみます。
$ perl -e 'print "yesY", chr(0x68), chr(0x84), chr(0x04), chr(0x08);' | ./heap_of0 yes(8048468) at (c->Y)(804a00c) no(8048400) at (c->N)(804a010) NO! $
入力データの先頭は「yes」であり、関数ポインターYが指している関数を呼び出しているのですが、出力結果は「NO!」、つまりno()が実行されています。これは、scanf()による上書きで関数ポインターYの値が無理やりno()のエントリポイントに書き換わってしまっているからです。
ヒープ領域においても、隣接するデータがバッファオーバーフローによって書き換えられることがある、これはスタックバッファオーバーフローの場合と同じです。
今回のサンプルコードでは、関数ポインターを上書きすることでコードの実行フローを改変できる例をご紹介しました。次回はヒープ領域の管理の仕組みを悪用した攻撃について説明したいと思います。
[参考情報]
OpenBSD Journal
Is Your Stack Protector Working?
http://undeadly.org/cgi?action=article&sid=20131202075805
VERACODE BLOG
A Tale of Two Compilers
http://www.veracode.com/blog/2013/11/a-tale-of-two-compilers/
AMD64 Architecture Programmer's Manual
Volume 1: Application Programming
http://support.amd.com/TechDocs/24592.pdf
The Art of Computer Programming
Volume 1 Fundamental Algorithms Third Edition日本語版
http://ascii.asciimw.jp/books/books/detail/4-7561-4411-X.shtml
ruBSD 2013
Exploit Mitigation Techniques: An Update after 10 years
http://www.openbsd.org/papers/ru13-deraadt/
Copyright © ITmedia, Inc. All Rights Reserved.