
ufs_create の監査に時間を費やし、バグを0件発見struct dirent オブジェクトのパディングバイトを介して情報が漏洩するこのコミット を見つけるmsdosfs_readdir へのパッチが不完全であることを発見。バグの1つのインスタンスにはパッチを当てたが、2つ目には当てていなかった。mqueuefs、autofs、smbfs、tmpfs において、完全な8バイトのポインタを漏洩できる亜種を発見。確認のためのPoCを作成。上記のように、私が見つけた元のバグは、関連するコミットのパッチを分析中に msdosfs_readdir にありました。
FreeBSDで readdir を呼び出す基本的な流れは以下の通りです:
#include <dirent.h>
int main(void) {
struct dirent *dp;
DIR *dirp;
dirp = opendir("./somedir");
dp = readdir(dirp);
}
somedir が存在するファイルシステムに応じて、FreeBSDカーネルの多くの *_readdir 関数のいずれかが呼び出される可能性があります。
上記のパッチでは、struct dirent オブジェクトをユーザー空間に返す前に呼び出されることを意図した dirent_terminate 関数が追加されています(多くの場合 uiomove 関数を使用して行われます)。この関数は、パディングバイトと構造体の d_name フィールドの残りのバイトをゼロにします。struct dirent の定義はこちら。
パッチを見ると、1562行目で dirbuf 変数を引数として dirent_terminate が呼び出されていることがわかります。続いて、uiomove が呼び出され、dirbuf の内容をユーザー空間にコピーします。しかし、これらのコード行はこのif文のブロック内にあることに注意してください。このif文の上のコメントは、この分岐はMSDOSファイルシステムのルートで readdir が呼び出された場合にのみ取られることを説明しています。したがって、ファイルシステムのルートより先の任意のサブディレクトリで readdir を呼び出すことで、このif文を簡単にスキップできます。
さらに下の方で、1691行目に uiomove への別の呼び出しがあります。しかし、コードを注意深く読むと、このインスタンスでは dirent_terminate が呼び出されていないことがわかります。つまり、パディングバイトは初期化されないままになります。残念ながら、d_name フィールドはこの関数の開始時にゼロにされていたため(ここ)、より大きな漏洩を得ることはできません。
まず、USBドライブを持っていなかったので、MSDOSファイルシステムをマウントする方法を考え出す必要がありました。以下が動作します:
$ dd if=/dev/zero of=test.img bs=512 count=256000
$ sudo mdconfig -a -t vnode -f test.img
$ sudo newfs_msdos -s 131072000 /dev/md1 # My mdconfig returned md1
$ mkdir ./temp
$ sudo mount -t msdosfs /dev/md1 ./temp
$ mkdir ./temp/test_dir
PoCは original_poc.c にあります。clang でコンパイルして上記のコマンドと同じディレクトリから実行するだけで、漏洩したバイトが表示されます。
このバグの亜種を探し始めました。uiomove\(&.*, をgrepしただけで、約15~20件の結果が返ってきて、すべて手動で確認しました。残念ながら、これらの亜種はデフォルトのFreeBSDには存在しません(ファイルシステムは手動で有効化/カーネルにコンパイルする必要があります)。亜種が存在する関数は以下の通りです:
mqfs_readdirtmpfs_dir_getdotdenttmpfs_dir_getdotdotdentsmbfs_readvdirautofs_readdir_oneこれらの関数すべてでバグはまったく同じなので、mqfs_readdir のみを取り上げます。
struct dirent entry が割り当てられますdirent_terminate が呼び出され、構造体のパディングと d_name フィールドをゼロにしますvfs_read_dirent が呼び出されます。この関数は uiomove を呼び出して構造体をユーザー空間にコピーしますここまではすべて問題なさそうですよね? 必ずしもそうとは限りません。構造体のすべてのフィールドが初期化されていることを確認する必要があります。コードをよく見ると、d_off フィールドが初期化されていないままになっていることがわかります。このフィールドの型は off_t で、実質的には int64_t です。構造体がユーザー空間にコピーされると、このフィールドに初期化されていないデータが含まれます。
この同じPoCはすべての亜種で動作します。異なるファイルシステム上で実行するだけです。mqueuefs の場合は、以下の手順を実行します(最初に mqueuefs を有効化/カーネルにコンパイルする必要があります):
$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp
PoC自体は variants_poc.c にあります。clang でコンパイルして上記のコマンドと同じディレクトリから実行するだけです。カーネルポインタが表示されます(おそらく1つのスタックポインタと1つのコードセクション/ヒープポインタですが、確認していません)。