exit_notify, CAP_KILLを利用したexploitスクリプトを読んでみる

Java-ja温泉に来ています.熱海までの電車の中でsecurityfocusに載っているコードを解説するエントリを書いてみます.$eipもformat stringも使わないexploitです.
Today, let us explain the exploit code in securityfocus.com. Without modification of $eip, nor format string.

gw-notexit.sh

http://gyazo.com/49c400ba9c0aa548b2fd5c84514f8b3b.png


このシェルスクリプトについて

このシェルスクリプトはいくつかのコマンドを実行する過程で2つのC言語で書かれたプログラムを生成します.
The shell script generates two programs written in C, during executing several commands.

  1. bashのラッパ.ただbashを実行するだけのプログラム.
  2. exploitコード.バグをついてlogrotateの設定ファイルを/etc/logrotate.dに書き込む.
  1. bash wrapper. It only executes bash.
  2. exploit code. It exploits the bug and write a logrotate-configuration file into /etc/logrotate.d/

シェルスクリプトは2つのコードをコンパイルし,それぞれ/tmpディレクトリの中に配置し,exploit.cのほうを実行し,logroteteの設定ファイルを/etc/logrotate.dに書き込みます.
The shell script compiles these programs and writes their binaries into /tmp/ directory and runs the latter binary from exploit.c. Then, it writes a logrotate-configuration file into /etc/logroatete.d.

そして,次にlogrotateが動き出すとき*1に,logrotaeteはexploitコードによって書かれた設定ファイルを読み込みroot権限でそこにある命令を実行し1つ目のbashのラッパであるプログラムにsetuidビットを立てます.
Next time when the logrotate daemon runs, the daemon executes commands along the configuration file written by the exploit code with its root priviledge, and it gives setuid-bit to the bash wrapper.

適切な環境の元で実行すると上のスクリーンショットのようになるはずです.

suzuki@suzuki-desktop ~
~ $ sh hoge.sh
logrotate installed, that's good!
-e Compiling the bash setuid() wrapper...
-e Compiling the exploit code...
-e Setting coredump limits and running the exploit...

Segmentation fault (core dumped)
-e Successfully coredumped into the logrotate config dir
Now wait until cron.daily executes logrotate and makes your shell wrapper suid

-e The shell should be located in /tmp/.m - just run /tmp/.m after 24h and you'll be root
-e 
Your terminal is most probably screwed now, sorry for that...

exploitコード中のプロセスの関係

上で「バグをついてlogrotateの設定ファイルを/etc/logrotate.dに書き込む」と簡単に書きましたが,より食わしくexploitコードを読んでみます.
I'll explain the explioit.c in detail.

exploit.cにはmain関数と,その中でclone関数で呼び出されるchild関数があり,clone関数で呼ばれたあっとは別のプロセス(task_struct)として動きます.はじめから終わりまでの流れは次のようになります.
There are two functions "main" and "child" in exploit.c. The latter is called by clone() fucntion. They runs separetely, in respect to their task_struct.

まずmain関数が実行されます.
Firstly, the main funcition is called.

http://gyazo.com/05b309ef233a9b4083c87fb10e636602.png

次にclone関数によってchildが呼ばれます.clone関数の第3引数にSIGSEGVが指定されていることに注目してください.
Clone() calls child(). Note that the third parameter of clone() has SIGSEGV in its lowest byte (8-bits).

http://gyazo.com/88e3f265280f316e4a8b1ab9fcefa9bc.png

child関数の中では2秒間sleepが呼ばれます.その間にmain関数がexecによりchfnコマンドになり待機しています(ユーザのパスワード入力を待ちます).ここでchfnの引数として怪しげな文字列が引数として渡されているのがミソです.
In chlild(), it sleeps 2 seconds. During the sleep, main function executes (becomes) "chfn" command. The chfn command waits for password input (but in vain). Again, Note that the passed parameter of chfn is magic string.

http://gyazo.com/0f9664691c7c4167ad412539629215ce.png
http://gyazo.com/7869b12cbcf690002e15d3d773d3f26f.png
http://gyazo.com/97d0c4cdce23244e0a536f3be905e861.png

child関数が2秒間の眠りから覚め,execによりgpasswdコマンドになります.この時点でsetuidビットが立っているプログラムが2つ存在しています.つまりそれぞれがroot権限で動いています.
After the child function wakes up from 2-seconds sleep, it executes (becomes) "gpasswd" command, which has setuid-bit in its permission. At this time, there is two setuid-ed programs running with their root priviledges.

http://gyazo.com/e0738e2e5a0b09231c91694c39a5fa56.png

gpasswdコマンドは存在しないグループ"%s"を引数に実行されるのですぐに終了します.
The latter gpasswd command exits soon after it starts because of its invalid parameter "%s".

http://gyazo.com/ae7dc9c5a363ad46a3b51f200abe86c0.png

gpasswdが終了する際に,親プロセスであるchfnコマンドに通常子プロセスが終了した旨を通知するSIGCHLDではなく,SIGSEGVが通知されます.これは上のclone関数の第3引数の一番端のバイト(8bit)にその子プロセスが終了したときに通知されるシグナルの番号を設定できるためです.
At this time, gpasswd sends SIGSEGV instead of normal SIGCHLD to its parent: the chfn command. This is because the clone function has set a signal number sent to parent in its third parameter.

http://gyazo.com/cd0c9949ce7f5f3a7dd286984f287853.png

chfnコマンドは予期していなかったシグナルSIGSEGVを受けとり,segmentation faultを通知して終了します.ことときプロンプトに表れるようにメモリ領域の内容であるcoreを掃き出します.
Finally, sent unexpected SIGSEGV, the chfn command exits showing "segmentation fault". Also it generates a core file, which is the content of its memory space.

http://gyazo.com/ebea611cc4f7be33c26d83a6a5ae787d.png



というわけで,exploit.cを適切な環境で実行すると次のようになります.

http://gyazo.com/49c400ba9c0aa548b2fd5c84514f8b3b.png



そのcoreにはchfnの引数として渡した何やら怪しい文字列が書かれており,それがlogrotateの設定ファイルとなっています.exploit.cの中でchdirして/etc/logrotate.dに移動しているのでこのファイルは/etc/logrotate.dディレクトリの中に書き込まれます.

The magic string passed as parameter of chfn is now written in the core file. This file is actually a configuration file of logrotate daemon and written into /etc/logrotate.d/.

http://gyazo.com/7751635ed38c90b1d166740e7e78299a.png

I got tired for translating this article in English :)


コアは次のような怪しい文字列を含むバイナリになっています.

http://gyazo.com/7bfe85ddf4c89d014313c5e02a9f59fd.png

catするとこんなかんじです.

http://gyazo.com/6e15d81a6126482891af6accc3239409.png

coreについて

coreというのはsegmentation faultなどが起こったときに,そのプログラムのメモリ領域をファイルとして書き出すデバッグ用の仕組み.ulimitコマンドによってどのサイズのcoreが書き出されるかどうかを確認できて,デフォルトでは値が0になっていればcoreは書き出されません.上記のexploitコードの中でulimitコマンドを用いてcoreが書き出されるようにしています.

ここで問題

  • 1.上の過程では一般権限ユーザがlogrotateの設定を変更できるということが問題になっていますが,一体どこがいけないでしょうか.
  • 2.chfnコマンドの替わりにpasswdコマンドにsegmentation faultを起こさせたらcoreは書き出されるでしょうか.
  • 3.passwdでcoreが書き出されないとしたら,何が原因でしょうか.(それが原因になっていることを調べる方法はなんでしょうか.)

答え

http://www.securityfocus.com/bid/34405

後の2問はstraceすればok.

ちなみに

この脆弱性は,カーネルデバッグ用変数であるfs.suid_dumpableという変数が1以上でないと動作しないので,普通の環境では動かないので,みなさんのお手元の環境でsudo sysctl fs.suid_dumpable=1を設定してから動かしてください.


exit_notify() 関数

exit_notify関数ではプロセスが終了するとき,親プロセスなどにシグナルを配送する処理を行っています.

その中で子プロセスの送るシグナルが通常のSIGCHLDではない場合(上で書いたようにclone関数の第3引数で設定できます),プロセスの親子関係の状態を見て配送されようとしているシグナルの値をSIGCHLDに変更する箇所があります.

具体的には,終了しようとしているプロセスのtask_structが持つ二つのメンバ(self_exec_idとparent_exec_id)の値をチェックし,それらが違うと,その子プロセスは親にSIGCHLDしか伝えることができないようになっています.


しかし,バグが治される前のコードでは,今まさに死のうとしている子プロセスがcapable(CAP_KILL)というcapabilityを持っているとそのチェックがパスされるようになっていました.

CAP_KILLというcapabilityはsetuidビットが立っているプロセスは持っているので,setuidビットが立っているプロセスをcloneで呼び出し,その際に終了シグナルをセットすることで親に任意のシグナルを送信できるということになっていました.

self_exec_idとか

self_exec_idとか知らないので,カーネルのソースをgrepしてみます.

http://gyazo.com/1b2da81601b45a078c409a2609a81de8.png

プロセスを生成するときにはコピーされ,プロセスがexecをするときにはその値がインクリメントされそうだ,ということがわかります.なんとなくですが「同じしごとをしているかどうか」を表す値となりそうです.
おそらく,apacheが複数プロセスで動いているときは同じexec_idでしょうし,bashがpsコマンドを実行しているときには親であるbashとexecを経てpsコマンドになってしまったpsとではexec_idは異なりそうです.

知りたいこと

さて,つらつらとCAP_KILLやself_exec_idについて「予測」を立ててきましたが,

上のself_exec_idがどのような値になっているか,特に今回のバグを生んだexit_notify()関数の上記のif文(このパッチの部分)のときにどんな値になっているかを知るにはどうすれば簡単でしょうか.

カーネルのソースにprintkを調べたい値がでるつどにkernelをコンパイルしないといけないでしょうし,kernel moduleを使ってprintkするのは確かにできそうだけれどちょっと面倒(そしてif文の直前で止めるとかできないだろうし).

task_structの各要素の値を/proc/などを使って見れたら楽なのですが,パッと見そんな部分はないですし.23日のDebug Hacks Conferenceでよしおかさんたちに聞いてみようかしら :)

*1:logrotateはroot権限で動く