
2026/01/09 3:29
「Unix v4 でバッファ・オーバーフローを修正:まるで 1973 年のことのように」
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
要約
この記事は、2025年に磁気テープから1枚のUNIX v4を回収し、PDP‑11シミュレータを使用して現代ハードウェア上で正常にコンパイルできた経緯を記述しています。ソースコードには、
/etc/passwd を読み込み TTY エコーを無効化し、パスワードを入力させて crypt() でハッシュ化し /bin/sh を起動する全約50行の su.c ユーティリティが含まれています。ただし、プログラムはパスワードを100バイトバッファに読み込む際に境界チェックを行わないため、オーバーフローでクラッシュしたりサイレントに終了したりする可能性があります。攻撃者はまずリソースを枯渇させて getpw() が失敗するようにし、その後過剰に長いパスワードを供給してこの脆弱性を悪用できるでしょう。
著者は1973年の行編集ツール
ed を使用してソースを修正し、インデックスカウンタ i とガード if (++i >= sizeof(password)) goto error; を入力ループ内に追加しました。編集後、cc su.c で a.out が生成され、これを /bin/su に移動しました。setuid ビットと適切な所有権を設定するために chmod 4755 /bin/su を実行し、パーミッションを復元しました。
この演習は、UNIX v4 が現代システム上で自己コンパイルできることを示す一方で、当時のセキュリティ重視度が現在の基準に比べて低いことを浮き彫りにしています。読者には、例えばオーバーフローが検出された際に TTY エコーを再有効化するなど、エラーハンドリング後も端末機能を保つようパッチをさらに洗練させることを提案しています。
本文
はじめに
2025 年、UNIX v4 の唯一既知のコピーが磁気テープ上で発見されました¹。
このバージョンはコンピュータ史上重要な転換点を示します――UNIX を C へ書き直した瞬間です。愛好家たちはすぐにデータを復元し、PDP‑11 シミュレータ² 上でシステムを実行することに成功しました。この貴重なアーティファクトに魅せられ、私はその調査のためにインスタンスを構築しました。
配布物にはソースコードが含まれているため、いくつかの主要ユーティリティの実装を精査しました。
su(1) プログラムを監査している最中にバグを発見したので、これを修正しましょう。
UNIX v4 の su(1)
su(1)50 年以上前の古いソフトウェアですが、su は現代版と同様に機能します。セット UID で root 実行ファイルとして設計されており、root パスワードを検証します。正しい認証情報が入力されると、root シェルが起動し、権限のないユーザーは特権から脱却できます。
su.c は 50 行未満のコードです:
/* su -- become super-user */ char password[100]; char pwbuf[100]; int ttybuf[3]; main() { register char *p, *q; extern fin; if(getpw(0, pwbuf)) goto badpw; (&fin)[1] = 0; p = pwbuf; while(*p != ':') if(*p++ == '\0') goto badpw; if(*++p == ':') goto ok; gtty(0, ttybuf); ttybuf[2] =& ~010; stty(0, ttybuf); printf("password: "); q = password; while((*q = getchar()) != '\n') if(*q++ == '\0') return; *q = '\0'; ttybuf[2] =| 010; stty(0, ttybuf); printf("\n"); q = crypt(password); while(*q++ == *p++); if(*--q == '\0' && *--p == ':') goto ok; goto error; badpw: printf("bad password file\n"); ok: setuid(0); execl("/bin/sh", "-", 0); printf("cannot execute shell\n"); error: printf("sorry\n"); }
要点は次の通りです:
を呼び出し、getpw()
から UID 0(root)のエントリを取得します。読み取りに失敗したりフォーマットが不正な場合でも su は続行します。この挙動は部分的に破損したシステムでの可用性を保つための安全策ですが、同時にセキュリティ上の問題(無権限ユーザーが/etc/passwd
を失敗させることで資源を消費できる)も孕んでいます。getpw()- TTY のエコーをオフにし、パスワード入力を促します。
- 文字単位で読み込み、改行または NUL が来るまで続けます。NUL が出た場合は即座に終了します(バッファサイズのチェックが無いため)。
- 読み込み完了後、エコーを再度有効化し、
でハッシュ化して保存済みハッシュと比較します。一致すればシェルを起動、そうでなければ終了します。crypt()
問題点:パスワードバッファは固定長(100 バイト)ですが、入力ループに境界チェックがありません。ユーザーが 100 文字以上入力するとバッファオーバーフローが発生し、プログラムがクラッシュします。実際に長い文字列を試すと、次のような挙動になります。
# su password:<long input> Memory fault -- Core dumped
TTY エコーを無効化しているため、クラッシュ後はターミナルが入力を表示しなくなります。可視性を回復するには
stty echo を打ち込み Enter を押してください。
su(1)
の修正
su(1)UNIX では自己再コンパイルに必要なソースコードが付属しているため、v4 でも同様です。これを利用すればシステム上で直接 su をパッチし、再コンパイルできます。
1973 年はエディタ機能が限定されており、
vi や emacs はまだ存在していませんでした。代わりに ed という行指向テキストエディタを使用します。ed はリスト・削除・追加といった基本操作が可能で、今回の修正には十分です。
バッファオーバーフロー防止
入力ループ中にカウンタ
i を導入し、読み込むたびにバッファサイズをチェックします。以下は差分です:
--- a/s2/su.c +++ b/s2/su.c @@ register char *p, *q; extern fin; + register int i; @@ - while((*q = getchar()) != '\n') + i = 0; + while((*q = getchar()) != '\n') { + if (++i >= sizeof(password)) + goto error; if(*q++ == '\0') return; + }
ed
での編集手順
ed# chdir /usr/source/s2 # ed su.c
起動するとファイルサイズが表示され、入力待ち状態になります。
は挿入モードに入り、現在行の前にテキストを追加します。i
は行削除、d
は内容出力です。p- 行番号を入力するとその行へ移動し、Enter を押すと行内容が表示されます。
以下は実際の編集セッション(画面録画)です:
741 8 register char *p, *q; extern fin; i register int i; . 24 printf("password: "); q = password; i i = 0; . p i = 0; while((*q = getchar()) != '\n') d i while((*q = getchar()) != '\n') { . if(*q++ == '\0') i if (++i >= sizeof(password)) goto error; . if(*q++ == '\0') return; *q = '\0'; i } . w 811 q
- まず行 8 に移動し、
を数回押して挿入位置を決めます。Return
で変数宣言を追加し、i
で挿入モードを終了します。.- while ループに入り、カウンタ初期化と境界チェックを追加しました。
で変更を書き込み、ファイルサイズが増えたことを確認後、w
で終了します。q
ビルドとデプロイ
修正済みソースは単一ファイルなのでコンパイルは簡単です:
# cc su.c
出力される実行ファイル
a.out を /bin/su に置き換えます:
# mv a.out /bin/su
しかし、セット UID ビットが設定されていないため権限を付与する必要があります:
# ls -l /bin/su -rwxrwxrwx 1 root 2740 Jun 12 19:58 /bin/su # chmod 4755 /bin/su # ls -l /bin/su -rwsr-xr-x 1 root 2740 Jun 12 19:58 /bin/su
結論
UNIX v4 は歴史的な貴重品であり、現代のシステムと驚くほど似た構造を持っています。機能は古典的ですが、ロジックは今も通用します。ソースコードがそのまま付属し、C コンパイラが利用可能という当時の哲学のおかげで、バグ修正からビルド・デプロイまでをすべてシステム上で完結できました。
今回のバッファオーバーフローは 1973 年代では「セキュリティ」の主要課題とは見なされていませんでした。実際、任意コード実行への知識も十分に発展していない時期です。このエクササイズを通じて、
ed のスキル向上とともに、古典的 UNIX の設計思想を体感してください。
最後の挑戦として、オーバーフロー検出ロジックに TTY エコー復元コード(
stty echo)を追加し、エラー発生時でもターミナルが正しく動作するようにしてみてください。