それ図解で。・・・tohokuaikiのチラシの裏

2009-09-10

UTF-8の冗長なエンコードとは何で、なんでそれがセキュリティ的に危ないのか?を文字コード知識レヴェル3くらいの凡プログラマが考えてみる

何故かあたり前にならない文字エンコーディングバリデーション

ってあるように、いまいち文字コードの不正な判定による危険性ってのが分かってない。

SJISの問題は、第5回■注目される文字コードのセキュリティ問題 - SQLインジェクションを根絶!セキュア開発の極意:ITproの記事がわかりやすかった。

というか、やっぱりPHP使ってると誰でも一度は「なんじゃこの『¥』は?」って思うもんなんで。

なるほど、確かに↓の図のように「あるバイト」が2つの意味を持つっていう文字コード形態はやばいんだなと。

EUC-JPはそんなことはしないで、1つのバイトには1つの意味しか取らせない。

だけど、これでも文字化けが起こることがある。経験的には、「マルチバイトをXX文字で切り落としたい」とかやった場合。ちゃんと文字コードを判定してくれるPHPでいえばmb_substr何かを使えば問題ないけど、バイト数で切っちゃうsubstrだと、奇数文字で切っちゃう時は1文字が半分にちぎれる可能性がある。


んー、じゃあUTF-8は何が危ないの?

そのあたりの問題とか、他言語で文字マップ領域が被るっていう問題を解消したのがUincodeで、その実装の一つであるUTF-8なら問題ないんじゃないの〜っていう感じなんだけど、それは違うみたい。

それが書かれた記事が「本当は怖い文字コードの話:第4回 UTF-8の冗長なエンコード|gihyo.jp … 技術評論社」っていう記事。

この記事を指して最初に引用したohgakiさんのブログでは

このよう事は簡単に理解できると思っていたので

と書かれているが、これ全然簡単じゃないよ・・・って感じで色々考えてみたのがこのエントリ。(前置き長っ)

あくまで、「そうなんじゃないかな〜」って自分が考えただけなんで、間違ってたら突っ込みなどどうぞ。


そもそも、UTF-8ってどうやって文字を判別してるのか?

UTF-8は、マルチバイト1文字を表すのに、1バイト〜4バイトの可変なバイト数領域を使う。

固定にすれば良いのに、何でそんな面倒なことするんかっていうと、ASCII文字と互換性を持たせるためらしい。

で、本当は怖い文字コードの話:第4回 UTF-8の冗長なエンコード|gihyo.jp … 技術評論社によると、

Unicode文字範囲UTF-8でのバイト列
U+0000〜 U+007F0xxxxxxx
(00〜7F)
U+0080〜 U+07FF110xxxxx  10xxxxxx
(C2〜DF) (80〜BF)
U+0800〜 U+FFFF1110xxxx  10xxxxxx  10xxxxxx
(E0〜EF) (80〜BF)  (80〜BF)
U+10000〜
U+10FFFF
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
(F0〜F7)(80〜BF)(80〜BF)(80〜BF)

ってなってる。

自分が、「単に1と0のデータを文字として出力するマシン」になったつもりで、「このデータはUTF-8文字列ですよ」ってデータが来た場合に

「あ、これは『あ』だ」

「これは、『\』だ」

とか見分けるために、「何バイトで1文字を表現してるのか」を判定するにはどうしたらいいかを、↑の表を見て考えてみた。

すると、UTF-8はすこぶる簡単に「これは3バイトで1文字だな」とかが分かるようになっている。

  1. 最初の1バイトの1ビット目が0なら「1バイトで1文字」。
  2. 最初の1バイトの1ビット目と2ビット目が1なら「2バイトで1文字」なので、次の1バイトも合わせて1文字を出力する。
  3. 最初の1バイトの1〜3ビット目が1なら「3バイトで1文字」なので、次と次の次の2倍とも合わせて1文字を出力する。

という感じ。

つまり、最初の1バイトは「何バイト一緒に食えばいいか」というのを教えてくれてる。

そして、「10」のビットで始まるバイトは「文字の中間」を構成するバイトということになる。

ところで、上のテーブルは110xxxxx (C2〜DF)ってなってるけど、C0〜DFじゃないかな?)


冗長なエンコードとは?

じゃあ、冗長なエンコードって何?ってことなんだけど。

それは「本来なら1バイトで済むデータを3バイト必要なように見せる」っていうことだと思う。

その例として、本当は怖い文字コードの話:第4回 UTF-8の冗長なエンコード|gihyo.jp … 技術評論社によると、「/」を挙げているのだけど。

正しいエンコード0x2F
不正なエンコード

0xC0 0xAF  (2バイト表現)

0xE0 0x80 0xAF  (3バイト表現)

0xF0 0x80 0x80 0xAF (4バイト表現)

何でこんなんが全部「/」として認識されるんだ!?って感じだけど、実際にやってみる。PHPで。

<?php
echo $sl1 = hex2bin('002f')."\n";
echo $sl2 = hex2bin('c0af')."\n";
echo $sl3 = hex2bin('e080af')."\n";

function hex2bin($hex_str)
{
	return pack("H*" , $hex_str);
}

ってやってコマンドラインから実行してみる。ターミナルの出力はUTF-8。なので、ターミナルが「UTF-8である」と解釈された文字が私の目に映るはず。

果たして

f:id:tohokuaiki:20090910155107j:image

というように、3つの「/」が出力された。

じゃあ、これが本当にバイナリとして同じデータなのかどうかを確かめてみる。簡単に、別の文字コードに変換してみればいい。

EUC-JPに変換して出力してみると、

f:id:tohokuaiki:20090910155826j:image

ってなった。

つまり、最初の「/」は本物の1バイトのスラッシュだけど、後の2つは1バイトのスラッシュではないということ。

SJISに変換しても

f:id:tohokuaiki:20090910160027j:image

となる。

これは何が悪いのかというと、それは「変なUTF-8エンコードを見抜けないターミナル」(このキャプチャの場合はTeraTermPro UTF-8版)なんだけど、世の中にはそういうアプリが多いらしい。それはブラウザも多分にもれず、FirefoxやI.Eも同じ勘違いをしてしまうらしい。


なんでそんな勘違いをするの?

じゃあ、何でそんな勘違いを?って感じだけど、それはなんとなくだけど・・・

本当は怖い文字コードの話:第4回 UTF-8の冗長なエンコード|gihyo.jp … 技術評論社にある

の絵を見て、「0xE0 0x80 0xAF」を処理するときの動作として。。。

Firefox「現在の文字コードUTF-8。次のバイトは、0xE0 ・・・1110....とすると3バイトで1つの処理だな」

Firefox「3バイトを1つに並べてみると、1100000 10000000 10101111・・・・」

Firefox「???最初の2バイトは意味のある部分のビットが全て0・・・最後の1バイトは『0101111』これは『00101111』を表してるのかな?」

ということなんだろうか?

この辺はよーわかりませんが、なんとなくそんな感じかも。


これがXSSにつながるって言うのは

要するに、「3バイト食いますよ〜」っていうフラグを立てておきながら(つまり1100000のバイトを送る)、2バイトしか送らなかった場合、次の文字も巻き込んでしまうんじゃないかってことかな?これって、EUC-JPSJISであったのと同じ問題で。それがUTF-8でも起きるっていう。

そうすると、HTMLの閉じタグとかクォートを食っちゃえばXSSが簡単に成立するてことで。



しかし、これを「このよう事は簡単に理解できると思っていたので、」と言っちゃうohgakiさんには

      r ‐、
      | ○ |         r‐‐、
     _,;ト - イ、      ∧l☆│∧  セキュリティ エバンジェリストの諸君!
    (⌒`    ⌒・    ¨,、,,ト.-イ/,、 l  われわれPHPerにとって大切なのは
    |ヽ  ~~⌒γ⌒) r'⌒ `!´ `⌒)  すぐ使えるサンプルか、コピペで動くコードだけだ。
   │ ヽー―'^ー-' ( ⌒γ⌒~~ /|  PHPerを買いかぶらないでもらいたい
   │  〉    |│  |`ー^ー― r' |
   │ /───| |  |/ |  l  ト、 |
   |  irー-、 ー ,} |    /     i
   | /   `X´ ヽ    /   入

と、言いたい。

zu_n_dazu_n_da 2009/09/11 01:52 「「3バイト食いますよ〜」っていうフラグを立てておきながら(つまり1100000のバイトを送る)、2バイトしか送らなかった場合」
次の「文字の中間」を構成するバイトではないので、巻き込まないのではないでしょうか。

XSSにつながるのは
例えばユーザから入力された文字列から「/」を抜き出さなければいけない処理で、
「0x2F(「/」1バイト表現)」
のみで検査を行い、
冗長なエンコードの
0xC0 0xAF(「/」2バイト表現)
0xE0 0x80 0xAF(「/」3バイト表現)
0xF0 0x80 0x80 0xAF(「/」4バイト表現)
で検査を行わなかった場合に「/」を抽出仕切れないところかと思います。

参照されている記事の以下の記載が問題箇所だと思います。
本当は怖い文字コードの話:第4回 UTF-8の冗長なエンコード|gihyo.jp … 技術評論社
このように,UTF-8では特定の文字を複数の形式のバイト列で表現できるため,

1. 処理A=UTF-8のデータ中に「/」等の文字が含まれていないか検査を行う
2. 処理B=処理AからUTF-8のデータを受け取り,UTF-16等に順次解釈しながら処理する

のような流れのときに,処理Aが冗長なUTF-8を意識せずに検査していると,UTF-16に変換したデータ中に処理Aでフィルタリングされるべき文字が含まれてしまうことになります。

tohokuaikitohokuaiki 2009/09/11 03:01 なるほど!

そこが問題だったんですね。

自分もよくわかってないものを、こうじゃないかとヒーコラ考えつつ、その考えをアップしたエントリーだったので、zu_n_daさんのコメントで最後が締りました。ありがとうございます。

kick123kick123 2009/09/11 20:16 「C2〜DFはC0〜DF?」ってところですが、1バイトで表現できるのは7ビットまでで、80を表現するには8ビットが必要です。
2バイト表現にあてはめるなら、「1100 0010」と「1000 0000」になり、1バイト目はC2になる。
ってことではないですか?

tohokuaikitohokuaiki 2009/09/14 13:35 kick123さん、ありがとうございます。
http://d.hatena.ne.jp/tohokuaiki/20090914/encoding
で色々と理解したことを書きました。

はてなユーザーのみコメントできます。はてなへログインもしくは新規登録をおこなってください。

track feed