(CWE−14)バッファをクリアするコードをコンパイラが除去してしまう脆弱性
原文: CWE - CWE-14: Compiler Removal of Code to Clear Buffers (4.3)
脆弱性の概要
ソースコードに書かれているバッファに対するクリア処理が、もう二度と読み込まない領域に対する処理であった場合、コンパイラの最適化処理の際にその該当コードを不要なものとして省いてしまう脆弱性。
より詳しくは以下の状況のときに起こる。
- 秘密のデータがメモリ上に格納されている。
- そのデータのクリア処理は、中身を上書きするという方法によって行われる。
- ソースコードをコンパイルする際に最適化オプションを指定しており、上書き処理をする関数をコンパイラは、「dead store」である、つまり今後使わない領域を書き換える関数であると解釈してその関数の存在しないファイルを吐き出す。
関連する脆弱性
child of
CWE - CWE-733: Compiler Optimization Removal or Modification of Security-critical Code (4.3)
TODO:上の脆弱性の内容もブログにまとめてその記事のリンクをここに貼る
脆弱性が生まれるタイミング
- コードの実装段階
- ビルドしてコンパイルする時
プラットフォーム
- C
- C++
この脆弱性によって起こるセキュリティの侵害
この脆弱性の結果、本来クリア処理すべきパスワードなどの極秘情報がメモリ上に残されたままになってしまい、それを攻撃者が見ることができた際、その情報を使って認証を突破するなどをされる。
対策
- コードの実装段階で機密情報を(可能であれば)「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を読んで勉強するぞ!