libcのRealpathバッファーアンダーフローに関する脆弱性とはOSS脆弱性ウォッチ(4)

連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェアの脆弱性に関する情報を取り上げ、解説していく。2018年1月4日に「libcのRealpathバッファーアンダーフロー」との脆弱性が報告された。今回は、この脆弱性のPoCを見ていく上で重要な「Realpathのバッファーアンダーフロー」に注目して、詳しく見ていく。

» 2018年04月12日 05時00分 公開
[面和毅OSSセキュリティ技術の会]

 「OSSセキュリティ技術の会」の面和毅です。本連載「OSS脆弱性ウォッチ」では、さまざまなオープンソースソフトウェア(OSS)の脆弱(ぜいじゃく)性に関する情報を取り上げ、解説しています。

 2018年1月4日に、「libcのRealpathバッファーアンダーフロー」の脆弱性が報告されました。今回は、この脆弱性のPoCを見ていく上で重要な「Realpathのバッファーアンダーフロー」に注目して、詳しく見ていきます。

脆弱性の経緯

 この「libc Realpath バッファーアンダーフロー」は、大まかに言うとkernelの処理の変更にglibcの方がきちんと追随していなかったことが原因となっています。

 詳しい説明は、脆弱性を報告したレポートにまとめられています。以下では、このドキュメントを参考にして解説します。

kernelの処理の変更

 もともと、Linux kernelが2.6.36になった際に、getcwd()関数に「パスが見つからない場合の処理」が加わりました。

 getcwd()はシステムコールとして「liunux/fs/dcache.c」中で下記の関数で定義されています。

SYSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size)

 パスが見つからない際に"(unreachable)"を加えてディレクトリを表示します(図1)。

図1

 これは、chrootで上位ディレクトリにアクセスできなくしたときや、mountしているポイントがなくなってしまったときに出てきます。皆さんも、外部メディアなどのUSBコネクターが抜けてしまった際に、たまに目にすると思います(図2)。

図2

 この挙動はman getcwd(3)にも記載されています(残念ながら、日本語のバージョンは些か古いようで、"unreachable"の挙動が記載されていません。英語版のgetcwd(3)には載っています(参考)。

glibcの挙動

 今回の「libc_Realpath_Buffer_Underflow」の脆弱性は、「kernelでの処理の変更が行われたにもかかわらず、glibcの方ではkernel内のgetcwd()の処理が以前と同じであることを前提としていた」ことに起因しています。

 glibc中のrealpath()は図3のようにgetcwd()を呼び出して、正規化された絶対パスを求めています。この際、realpath()は全てのシンボリックリンクを展開し、「/./」「/../」などをnullで終わっている文字列に置き換えていきます。

図3

 ここで、例えば「../../x」というパスが与えられた場合には、realpath()は現在のディレクトリ「./」を先頭に付け加えてパスを求めていき、「../」を「[何らかのディレクトリ]/」で置き換えていきます。

 図4のところが、この処理の主要部分になります。realpath()で与えられたディレクトリに「..」が含まれていた場合には、「char *rpath」に保存されている現在のパスをさかのぼっていきます。「dest[-1]」が「/」になるまでポインタdestの値をずらしていくので、ディレクトリの文字列の先頭に「/」が付いていることが前提になります(図4)。

図4

 ここで、getcwdの挙動として(unreachable)が返されるようになったため、上記の「../../x」が例えば「(unreachable)/hoge/x」のように、先頭に「/」がない状態になります。この際には、(unreachable)が「/」で始まっていないため、destのポインタアドレスの位置がずれて、rpathよりも前に行ってしまい、いわゆる「バッファーアンダーフロー」の状態になってしまうことがあります。

実際にPoCを実行してglibcでのrealpath()の「バッファーアンダーフロー」を確認してみる

 実際にバッファーアンダーフローがどう起こっているかを試すために、この報告に沿った内容の単純なPoC(環境変数を用いて出力を変える)を実行して、destのアドレスがどうなっていくかを見てみましょう。

PoCの準備

 まず、PoCテスト環境として「Debian(Stretch)」のDVDからインストールしたままの状態を用意します。パッケージを更新すると、PoCが動作しなくなるので注意します。

 今回は、root権限取得の前の「バッファーアンダーフロー」の部分を見たいので、PoCの中でも最初の部分(環境変数を用いた出力の上書き)を試してみます。

  • rootで以下を実施
root$ echo 1 > /proc/sys/kernel/unprivileged_userns_clone
  • 一般ユーザー(test)で以下を実施
test$ /usr/bin/unshare -m -U --map-root-user /bin/sh
  • 一般ユーザーのターミナルが、そのまま(偽の)rootになるので、以下のそれぞれの行を実施
(root)#  mount -t tmpfs tmpfs /tmp
(root)# cd /tmp 
(root)# chmod 00755 .
(root)# mkdir -p -- "(unreachable)/tmp" "(unreachable)/tmp/from_archive/C/LC_MESSAGES" "(unreachable)/x" 
(root)# ln -s 	../x/../../AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAA	AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/A 	"(unreachable)/tmp/down" 
(root)# base64 -d <<B64-EOF | bzip2 -cd > 	"(unreachable)/tmp/from_archive/C/LC_MESSAGES/util-linux.mo" 	QlpoOTFBWSZTWTOfm9IAAGX/pn6UlARGB+FeKyZnAD/n3mACAAAgAAEgAJSIqfkpspk0eUGJ6gAG mQeoaD1PJAamlPJGCNMTIaNGmnqMQ0AAzSwpEWpQICVUw+490ohZBgZ+s4EBAZCn/TavSQshtCiv iG6HOehyAp4FPt3zkpdTxNchTYITLBkXUjsgpN2QDBNX8qmbpkVgfLXKcQc1ZhVF0FxUQOtnbGlL 5NhRmORwmQF1Dw3Yu1mds6tGAmnLwWwc2KRKGl5hcLuSKcKEgZz83pA= B64-EOF
(root)# echo "$$" 
             2299(pidの出力。値はテストのたびに変わる)
  • 他のターミナルを一般ユーザーで開き、上述の手順の最後に出力されたpidのcwdに移動
test$ cd /proc/2299/cwd
  • 以下のumountを実行する。出力が上書きされる
test$ LC_ALL=C.UTF-8 /bin/umount --lazy down / umount: AalnAAAAAAAAAAA

glibcの修正

 今回は、PoCの手順を実行した際に、同時にdestの値の動きを見ていきたいので、別の開発環境を準備し下記のようにしました。

  1. debianのglibc-2.26のソースパッケージをダウンロード
  2. 図5のように上述の"glibc-2.26/stdlib/canonicalize.c"にちょっと手を加えて各部分でdestとrpathのアドレスを出力
  3. 修正したコードを含むパッケージを「dpkg-buildpackage -us -uc -b -j3」コマンドを使って再作成
  4. 上で作成したPoCテスト環境に、改変したglibc-2.26パッケージをインストールしてPoCを実行
図5

PoCの実施

 PoCを実行し、destのアドレスの遷移を確認します。destはrpathに保存されたcharを参照して動くので、rpathのバッファーアドレス内になるはずです(図6)。

図6

 しかし、実際にPoCの手順通りに行うと、図7のように、destがrpathのアドレスよりも下側にはみ出ます。

図7

 これが、バッファーアンダーフローが起こっていることになります(図8)。

図8

 念のため、「/(unreachable)/tmp」というディレクトリを作成し、前述の「PoCの準備」と同じ手順でサブディレクトリとシンボリックリンクを作成しても、destのアドレスがrpathのアドレスを下回ることはありません(図9)。

図9

 この「バッファーアンダーフロー」を用いて、下位のアドレス内のメモリを書き換え、コードを実行させたのがPoCです(ここ以降は、よくある「バッファーオーバーフローやバッファーアンダーフローを用いたコード実行」になるので、今回の説明では触れていません)。

 簡単にまとめると、今回のPoCでは、OS、kernel、glibcのバージョンが指定された特定の環境で、Localeと、umountに付与されているroot権限のスティッキービットを用いて、うまくALSRの影響を受けずにroot権限を取得しています(環境に依存するため、即座に他の環境に適用できるわけではありません)。

脆弱性の修正

 今回の脆弱性の修正は、単純に図10のように、「rpathが/で始まっていることを確認して」dest[]を移動させていくものになります。そして、rpathのアドレスがdestのアドレスと同じ(つまりバッファーの先頭)になり、/で始まっていない(unreachableになっている)場合には、エラーを出してパスの検索が終了するようになっています。

図10

まとめ

 今回の件は、kernelの方の関数で返す値を変更していた((unreachable)を返すようにしていた)にもかかわらず、glibcの方でそれを想定した修正が行われていなかったことが原因となっています。

 今回のケースでは、ALSRの影響を受けないようにするため、特定の環境(OS、kernel、glibcのバージョンが固定)でのみ動作するPoCが公開されていますが、他の環境で適用できるコードも出てくる可能性があります。

 glibcは特に重要なライブラリであるため、更新が難しいとは思われますが、現実に特権を取れる脆弱性でPoCも公開されているため、なるべく早い対処が望まれます。

筆者紹介

面和毅

略歴:OSSのセキュリティ専門家として20年近くの経験があり、主にOS系のセキュリティに関しての執筆や講演を行う。大手ベンダーや外資系、ユーザー企業などでさまざまな立場を経験。2015年からサイオステクノロジーのOSS/セキュリティエバンジェリストとして活躍し、同社でSIOSセキュリティブログを連載中。

CISSP:#366942

近著:『Linuxセキュリティ標準教科書』(LPI-Japan)」


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。