Explore "Full-Stack" in depth!

情報系の専門学校で、今は機械学習に的を絞って学習中。プログラミングを趣味でやりつつ、IT系のあらゆる知識と技術を身に付けるべく奮闘中。

DirtyCOW(CVE-2016-5195)を最低限理解する。

目次

概要

LinuxKernelに存在した脆弱性DirtyCOW

今は完全にパッチが当てられていますが、その仕組みはとても奥深く、
理解にはLinuxKernelの挙動の理解が必要となります。

ずっと仕組みが気になっていたのですが、やっと最低限の事を理解できたので記事にしたいと思います。

dirtycow.ninja

私の説明より素晴らしい方の説明はたくさんありますから、

英語が聞き取れるようなら

www.youtube.com

を参考にしてください。
私はこの動画が一番わかり易いと思います。

前提知識

まずは本題に入る前に、前提として仮想メモリについて話していきます。

私達が作ったプログラムを実行してみたり、
それ以外でもUNIXコマンドを実行した時には、
プロセスが生成されます。

プロセスはOSにおける基本的な実行単位であり、
私達が利用するOSは非常に多くのプロセスが動いて成り立っていると言えます。

次のようなコードを見てみましょう。
今回はプログラミングが本質ではないので、
出来るだけ多くの人に理解できるようにわかりやすいC言語を用いる事にします。

#include<stdio.h>

int main(void){
    int ary[5] = {1,2,3,4,5};
    printf("%p\n",&ary[0]);

    return 0;
}

配列を定義してその先頭アドレスを表示しています。
このアドレスはメモリアドレスを指していますが、

実は物理メモリの値がそのまま表示されているわけではありません。

f:id:orangebladdy:20190205002759j:plain

絵がわかりづらくて申し訳ないです。
要は物理メモリのうち一定範囲をプロセスに割り当てているということです。

割り当てられたメモリを物理メモリに対比して論理メモリとか論理アドレス空間とかいいます。

実際にはページ単位論理アドレス物理アドレス対応付けします。

この対応づけがされた表をページテーブルといい、
プロセスから利用される論理アドレスから対応する物理メモリが参照できるようになっています。

FE試験で勉強したことなのにすっかり忘れていました。


本題

さて、DirtyCOWの説明に入ります。
まずは簡単にこの脆弱性をまとめると、

「一般ユーザ権限が変更できないようなファイルに対して
レースコンディションを利用してファイル変更権限をbypassし、
特権ユーザとしてファイルを変更出来る」

というようなものです。

かなりふわっと説明したので、ここから詳しく見ていきます。

Linuxとプロセスの関係

Linuxでは殆ど全てがファイルとして扱えます。

例えばバイスに関するファイル/devに置いてあります。
ハードウェアですらもファイルで管理できます
/binにはUNIXコマンドが実装されたELF形式のファイルが置いてあります。

プロセスも勿論ファイルとして扱えます。
プロセスファイルシステムがマウントされる/procには各プロセスのファイルが置いてありますよね。

例えばBashのプロセスがPID4で動いているとしましょう。
/proc/4/fdには、0,1,2のファイルディスクリプタが名付けられたシンボリックリンクがありますよね。

もうすでにお気づきかもしれませんが、

Hello,World!
f'Explore {foo} in depth!'

というテキストファイルを用意し、

$cat foo.txt > /proc/4/fd/0

とすると、テキストの中身が標準出力に表示されるのがわかると思います。
ファイルディスクリプタ0は標準入力を意味するので、
cat foo.txtをやっているのと同じことです。

en.wikipedia.org

には(かなり詳しく書いてあり参考になりました)、

"Everything is a file" describes one of the defining features of Unix,

という記述があります。
感覚はつかめたでしょうか。

PoCを見てみる

実際にDirtyCOWの脆弱性を突くコードを解説します。
こちらのPoCを例に取りますね。

github.com

このプログラムの流れは次の通りです。

  • コマンドライン引数として渡されたファイルのストリームを開く
  • 開いたファイル(ディスク上)を仮想アドレス空間に新たにマッピング(割当)する。
    • この時、書き込み時コピー(Copy on Write)としてマップする。
    • コピーされたファイルに対する変更は元ファイルに影響を与えない
  • 2つのスレッドを大量に実行する。
    • 一つは、マッピングされた仮想メモリ領域にMADV_DONTNEEDシグナルを送信する。
      • MADV_DONTNEEDは、マッピングされた領域に対するアクセスは暫くされないカーネルに知らせるというもの。
      • つまり、カーネル必要に応じてこの領域を開放(free)する。
    • もう一つは、/proc/self/memファイルをO_RDWRフラグ付きで開こうとする。
      • このファイルは読み取り専用の為普通なら-1を返す
      • もし成功すれば(fdで判定)マッピングされたアドレス分ファイルオフセットをずらして、その部分に指定された文字列を書き込む

というものですね。

コード自体は簡単なのでわかると思います。

/proc/selfは、現在のプロセスを表します。
現在のプロセスに割り当てられたmemなので、
/proc/selfは現在のプロセスに割り当てられたメモリ領域を示します。
つまり、仮想メモリ空間です。

理論的には、このファイルを読むことでプロセスに割り当てられたメモリを読み出す事が可能です。

DirtyCOWでは、このファイルに"書き込む"事で不正な変更が引き起こされます
しかし、コピーされたファイルへの書き込みは元ファイルに影響を与えないことを思い出しましょう。
では、どのように元ファイルが変更されるのでしょうか…?
それには競合状態が深く関連しています。

レースコンディションの仕組み

まずはCoW機構において、
プロセスがファイルに書き込む流れを見ましょう。

  • いくつかのプロセスによってディスク上のファイルを更新しようとしていればコピーを生成する。
    • これにより他プロセスからはその変更が見えなくなる。
  • コピーされたファイルに書き込む
  • MADV_DONTNEEDシグナルによってファイルは開放される
  • 元ファイルが書込み可能であるか判断し、可能であればディスクに書き込む(更新する)

というような感じです。

このコピーの生成には時間がかかります。

競合状態が引き起こるのは、
上記の一連の流れにあるファイル書き込み時コピー(まさにCopy-on-Write)が完璧じゃない場合です。

ファイルの読み出し→書き込みというサイクルを
コンスタンスに繰り返す場合(上記PoCによる大量のループ)、
2つのプロセスが同じタイミングでファイルを利用するようなケースが起こります。

PoCではファイルに読み込むスレッドページアウトさせるスレッドを大量に実行していましたが、
MADV_DONTNEEDシグナルによって必要ないと判断される事によってコピーされた領域を開放します。

そして新しくファイルを参照される時には、
もう一度元ファイルをコピーし、
そこに対して論理アドレスを対応付けるようページテーブルを更新します。

そうすることでファイルに対する変更を他プロセスから見えなくし、
また元ファイルに書き込み権限がない時も問題なく作用します。

ファイルに対する変更コピーファイルがマッピングされた領域の開放が同時に引き起こる事で、
新しくコピーされたファイルにマップされているかどうかをチェックする前にファイルにに書き込んでしまうようです。

既に配布されているパッチでは、
新しくコピーされたファイルに対応するようページテーブルが更新されているかチェックしてから
書き込みを許可するように改善されていますが、
DirtyCOWの脆弱性があるときはそのチェックが甘かったようですね。

いくつかの文献を参照したのですが、
少しずつ言っている事が違うのでかなり苦戦しました…。


まとめ

もう一度まとめておきましょう。

  • プロセスがディスク上のファイルに書き込む時、いくつかのプロセスから参照される為にファイルのコピーを生成する。
  • コピーされたファイルに対応する論理アドレスをプロセスに割り当てる。
  • しかし、ファイル書き込みページアウトが同時に発生することで、論理アドレスがコピー先ファイルにマッピングされているかを確認をする前にファイルを更新してしまう

ということでしょう。

今までに勉強した知識がふんだんに盛り込まれていてとても勉強になりました。