(CWE−14)バッファをクリアするコードをコンパイラが除去してしまう脆弱性

原文: CWE - CWE-14: Compiler Removal of Code to Clear Buffers (4.3)

 

脆弱性の概要

  ソースコードに書かれているバッファに対するクリア処理が、もう二度と読み込まない領域に対する処理であった場合、コンパイラの最適化処理の際にその該当コードを不要なものとして省いてしまう脆弱性

より詳しくは以下の状況のときに起こる。

  1. 秘密のデータがメモリ上に格納されている。
  2. そのデータのクリア処理は、中身を上書きするという方法によって行われる。
  3. ソースコードコンパイルする際に最適化オプションを指定しており、上書き処理をする関数をコンパイラは、「dead store」である、つまり今後使わない領域を書き換える関数であると解釈してその関数の存在しないファイルを吐き出す。

関連する脆弱性

child of 

CWE - CWE-733: Compiler Optimization Removal or Modification of Security-critical Code (4.3)

TODO:上の脆弱性の内容もブログにまとめてその記事のリンクをここに貼る

脆弱性が生まれるタイミング

プラットフォーム

この脆弱性によって起こるセキュリティの侵害

 この脆弱性の結果、本来クリア処理すべきパスワードなどの極秘情報がメモリ上に残されたままになってしまい、それを攻撃者が見ることができた際、その情報を使って認証を突破するなどをされる。 

対策

  • コードの実装段階で機密情報を(可能であれば)「volatile」なメモリに格納するようにする。
  • ビルドしてコンパイルする際にコンパイラが「dead store」を取り除かないように設定する。
  • 機密情報を暗号化して格納する。

検知方法

Black Box

 black box methodではこの脆弱性を検知することはできない。なぜならばコンパイラは該当するコードそのものを消し去ってしまっていて、実際に動いているコードを解析しても、書いたプログラマがとあるメモリ領域をクリアしようと意図していたことなど知る由もないからだ。

White Box

 white box methodでは検知できる。ソースコードを確認できるからである。コンパイラが除去しがちなコードを慎重に探す必要がある。

脆弱性の再現

 ここまでは原文の内容をまとめただけなのでここからがメインパートである。原文の例を参考にして脆弱性を確認する。

// problem.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char pass[64] = "secretpass";

int GetPasswordFromUser(char pwd[])
{
    printf("Type your password: ");
    int check;
    check = scanf("%64s",pwd);
    if(1==check)
    {
        return 1;
    }
    return 0;
}

int CheckPassword(char pwd[])
{   
    int checkstring = strncmp(pwd, pass, 64);
    if(!checkstring)
    {
        return 1;
    }
    return 0;
}

void GetData()
{
    char pwd[64];
    if(GetPasswordFromUser(pwd))
    {
        if(CheckPassword(pwd))
        {
            printf("Correct Password!\n");
            printf("Secret Data is Iwashiira\n");
        } else {
            printf("Invalid Password\n");
        }
    }
    // クリア処理
    memset(pwd, 0, sizeof(pwd));
}

int main()
{
    GetData();
    return 0;
}

 チェックするためのパスワードもメモリ領域に置いちゃってるじゃんというツッコミが入りそうですが、ここで確認したいのは最適化処理を指定したときにmemset関数が消えているかどうかです。

gcc problem.c -o gccNoOpt
gcc problem.c -o gccOpt1 -O1
readelf -r gccNoOpt gccOpt1

gccNoOptとgccOpt1を比べてみる(見にくくてごめんなさい)と

File: gccNoOpt

Relocation section '.rela.dyn' at offset 0x610 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003d90  000000000008 R_X86_64_RELATIVE                    11e0
000000003d98  000000000008 R_X86_64_RELATIVE                    11a0
000000004008  000000000008 R_X86_64_RELATIVE                    4008
000000003fd8  000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000003fe0  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000003fe8  000800000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0  000a00000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000003ff8  000b00000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x6d0 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003fa8  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000003fb0  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000003fb8  000400000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000003fc0  000500000007 R_X86_64_JUMP_SLO 0000000000000000 memset@GLIBC_2.2.5 + 0
000000003fc8  000700000007 R_X86_64_JUMP_SLO 0000000000000000 strcmp@GLIBC_2.2.5 + 0
000000003fd0  000900000007 R_X86_64_JUMP_SLO 0000000000000000 __isoc99_scanf@GLIBC_2.7 + 0
File: gccOpt1

Relocation section '.rela.dyn' at offset 0x610 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003d98  000000000008 R_X86_64_RELATIVE                    11c0
000000003da0  000000000008 R_X86_64_RELATIVE                    1180
000000004008  000000000008 R_X86_64_RELATIVE                    4008
000000003fd8  000100000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_deregisterTMClone + 0
000000003fe0  000400000006 R_X86_64_GLOB_DAT 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000003fe8  000600000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
000000003ff0  000900000006 R_X86_64_GLOB_DAT 0000000000000000 _ITM_registerTMCloneTa + 0
000000003ff8  000a00000006 R_X86_64_GLOB_DAT 0000000000000000 __cxa_finalize@GLIBC_2.2.5 + 0

Relocation section '.rela.plt' at offset 0x6d0 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000003fb0  000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000003fb8  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000003fc0  000500000007 R_X86_64_JUMP_SLO 0000000000000000 strcmp@GLIBC_2.2.5 + 0
000000003fc8  000700000007 R_X86_64_JUMP_SLO 0000000000000000 __printf_chk@GLIBC_2.3.4 + 0
000000003fd0  000800000007 R_X86_64_JUMP_SLO 0000000000000000 __isoc99_scanf@GLIBC_2.7 + 0

確かに最適化するとmemsetが消えています。これはclangでもおなじことが起こりました。radare2でデバッグしてみてもGetDataのmemset処理に関する部分は消去されていました。それでは対策として紹介されている方法を試してみます。

 まずvolatileをつける方法ですが、結論から言うとあまりうまくいきませんでした。volatileで宣言された領域を普通の関数に渡すとvolatileが「discard」されてしまうので扱う関数を工夫しなければいけないみたいなんですよね。strncmpの第一、第二引数はconst char*型がデフォルトですし、memsetの第一引数もvoid型としてが渡されるべきで、別の型を渡すとキャストされるみたいです。つまり別の関数を使わないといけなさそうですが、とりあえず放置してしまいました。少なくとも単純にコードのpwd[]をvolatileで宣言するだけでは改善できませんでした。

 次にコンパイラで除去しないように設定する方法ですが、コンパイラに詳しくないのでノータッチです。(なんのためにこの脆弱性の勉強をしてるんだろと言う気持ちになってきたぞ)

 最後の暗号化すると言うのは「まあ」というかんじで、結局その暗号化されたあとのデータを見ることができてしまうので根本的な解決にはなっていないような気がします。

 結論としては初期化処理をする関数だけ別ファイルに書いて最適化オプションなしでコンパイルして、後でまとめてリンクするのが(僕にとっては)丸いのかなと思っています。volatileを使いこなせればそんなことしなくてよさそうですけどね。今後もCWEを読んで勉強するぞ!