前置き
Sudo format string vulnerability ( CVE-2012-0809 ) というバグが、sudo コマンドには存在する。CVEが出ていることからも判るように、結構深刻なバグで、セキュリティ・ホールとしての性質を伴う。sudoのページによれば運が良くても sudo はクラッシュし、
運が悪いと攻撃者に root 権限を掌握される問題が含まれているかもしれない
との事だ。で、このバグ、何がどうなっているのかについては、『てきとうなメモ: [Linux][Security] sudo-1.8のバグ』が詳しい。とても詳しいので、そちらだけ見ておけばいいやと思っていたのだが、何箇所かから、
お前も書け
という圧力を食らったので、書くことにする。ただし、新しい情報がなにかあるって言うわけじゃない。影響範囲
このバグは sudo の 1.8.0 で導入され 1.8.3-p2 で修正されました。なので、影響は 1.8.0 - 1.8.3-p1 と言うことになります。で、商用 Linux distribution の中で、影響があるのはなさそうです。
Free distribution の中も含めると、Fedora16, OpenSuSE 12.1, Rawhide の3つが影響を受けているようです。
(http://rpm.pbone.net/index.php3 でどのdistributionがどのrpmを使っているのか、分かります)
ただし、それ以外のOSでも sudo は使っているはずです。それらが安全かどうかは判りません。一応、sudo のページには対応している各種OS用の置き換えバイナリは用意されているようです。
という訳で、ほぼ影響はない、と言うことなので、以下は笑い話として。他人の失敗は蜜の味 (^w^)。
攻撃方法
攻撃方法は次のとおりです。- ln -s /sbin/sudo /tmp/%s
のようにして、sudu コマンドを「%s」というファイル名にすり替える - /tmp/%s を実行する
%s は printf() 系処理が format 文として利用しているものであれば、大抵のもので問題が生じるようです。どれでどのような症状が出るのかは、CPUなどにある程度依存します。
こうすると、argv[0] 文字列の中に %s という文字列が含まれるようになります。
技術的概略
このバグは、すごく簡単に言うと printf() 系関数を2段階かけた事に起因したバグです。printf() 系関数を多段に使うことは、セキュリティを考慮したコーディングとしては非常にまずいもので、絶対やるな
と言っても構わないぐらい、危険な行為です。修正後も2度使っていますが、まぁ、これならしょうがないかな、という所にまで治っています。
実はもう一つ、例外処理が足りていない、と言う問題もあります。sudoだから発生する確率はあまり高くありませんが…仮想メモリシステムを使い切った状態だと危険ですね。
詳細
sudo-1.8.3-p1 の ./src/sudo.c の最後に次のような関数があります。デバッグログを出力するためのコードですが、ここに悪さの元が含まれています:void sudo_debug(int level, const char *fmt, ...) { va_list ap; char *fmt2; if (level > debug_level) return; /* Backet fmt with program name and a newline to make it a single write */ easprintf(&fmt2, "%s: %s\n", getprogname(), fmt); va_start(ap, fmt); vfprintf(stderr, fmt2, ap); va_end(ap); efree(fmt2); }
通常、argv[0] は "sudo" という文字列へのポインタです。で、getprogname() は argv[0]を返すようになっています。
そこで、
fmt = "%s"
fmtの次の引数 = "hello"
のような場合について考えてみましょう。
easprintf(&fmt2, "%s: %s\n", getprogname(), fmt);
この行を通った段階で fmt2 は
fmt2 = "sudo: %s\n";
になります。で、その後:
vfprintf(stderr, fmt2, ap);
という行を通ると、stderr には
"sudo: hello\n";
という文字列が送りつけられることになります。
何も問題はありませんよね??
では、攻撃方法にあるように sudo コマンドに対してリンクを貼って './%s' という名前で起動したらどうなるでしょう?
argv[0] = "./%s"
になります。
easprintf(&fmt2, "%s: %s\n", getprogname(), fmt);
この行を通った段階で fmt2 は
fmt2 = "./%s: %s\n";
になります。で、その後:
vfprintf(stderr, fmt2, ap);
という行を通ると、stderr にはまず、第1引数である "hello" が出るので:
"./hello:
…おや、困りました。後半の %s のための文字列へのポインタが引数として渡されていません。しかも vfprintf() はそんな事を知りません。しょうがないので、stack 上にある値をポインタと解釈してしまいます。
運がよいと、「Segmentation Violation」が発生します。で sudo はクラッシュします。あぁ、core を吐かないように設定しているといいんですが…。運が悪いと、coreファイルを持っていかれますが、そこには /etc/shadow の値が書いてあるかもしれません。
まぁ、意図的にやっている奴らは
確実に core 取得できるように
設定してから実行するよね。
はっはっはっ
まぁ、運良く大した情報が保存されていない core ファイルが出来上がることを祈るしかありませんな。
ちなみに。"%s" は上記のとおりですが、"%n" という楽しい引数がございましてな…これ、引数で渡したアドレスにそこまで書いた文字数を「書きこむ」んですわ… はっはっは …
判ったと思いますが、万が一これらのバージョンを使っていたら
血の涙を流してでも upgrade しろ!!
対策
1.8.3-p2 はこう変更されました。void sudo_debug(int level, const char *fmt, ...) { va_list ap; char *buf; if (level > debug_level) return; /* Bracket fmt with program name and a newline to make it a single write */ va_start(ap, fmt); evasprintf(&buf, fmt, ap); va_end(ap); fprintf(stderr, "%s: %s\n", getprogname(), buf); efree(buf); }
えぇ、見ての通り printf は相変わらず2度使われています。
ただ、1度目は与えられたフォーマットと引数から、とりあえず buf という文字列を作るために、2度目はargv[0] と buf を fprintf() で出力するために使われます。
fmt は sudo のプログラム内で指定するフォーマット文ですし、fprintf() で使われるフォーマット文もプログラム内で指定するフォーマット文ですので、取り敢えず引数に %s などの危険な記号が含まれていても安全、とは言えるでしょう。
ただね。このコード、 buf == NULL だった場合を考慮していないんですわ。一応、glibc の場合 NULL を渡されると "(null)" という文字列を出しますけどね、これは必須ではない。たとえばSolarisは core dump するそうです。
環境によっては fprintf() の例外処理の定義が甘い所につけ込んだコードになる危険性は残されています。