経緯
日常で最もよく遭遇する透明な文字 (Whitespace など) は、いわゆる (全角|半角) スペースそして改行 (CR, LF) タブなどがある。
Unicode にはたくさんの透明な文字が存在する。以下の文字列は "AB" ではない。
なんと、 A と B の間に WORD JOINER というゼロ幅の文字が潜んでいる!
AB
A{U+2060}B
他にも、左右のテキストの順序を変えることができる文字 RLM など、
色々と複雑な制御文字がある。
このような特性を利用してファイル名やパス名などを偽装する攻撃 が存在する。
Unicode の透明な文字を知っておくことは、セキュリティにおいても重要だと思う。
このような攻撃に使われそうな(使えそうな)、透明な文字一覧を探してみた。
The Unicode Consortium の配布する資料、 Wikipedia 、自分が Python で書いた簡易的な探し方などを用いて探した。
まず結果
U+9
U+A
U+B
U+C
U+D
U+20 SPACE
U+85
U+A0 NO-BREAK SPACE
U+2000 EN QUAD
U+2001 EM QUAD
U+2002 EN SPACE
U+2003 EM SPACE
U+2004 THREE-PER-EM SPACE
U+2005 FOUR-PER-EM SPACE
U+2006 SIX-PER-EM SPACE
U+2007 FIGURE SPACE
U+2008 PUNCTUATION SPACE
U+2009 THIN SPACE
U+200A HAIR SPACE
U+2028 LINE SEPARATOR
U+2029 PARAGRAPH SEPARATOR
U+202F NARROW NO-BREAK SPACE
U+205F MEDIUM MATHEMATICAL SPACE
U+3000 IDEOGRAPHIC SPACE
U+180E MONGOLIAN VOWEL SEPARATOR
U+200B ZERO WIDTH SPACE
U+200C ZERO WIDTH NON-JOINER
U+200D ZERO WIDTH JOINER
U+2060 WORD JOINER
U+2061 FUNCTION APPLICATION
U+2062 INVISIBLE TIMES
U+2063 INVISIBLE SEPARATOR
U+2064 INVISIBLE PLUS
U+3164 HANGUL FILLER
U+FEFF ZERO WIDTH NO-BREAK SPACE
U+FFA0 HALFWIDTH HANGUL FILLER
釈明・免責
私は Unicode のプロフェッショナルではありません。この記事の内容は無保証です。
以下は、延々と経緯
まずは困った時の Wikipedia
ここに一覧がある。 http://en.wikipedia.org/wiki/Whitespace_character
このページは Unicode 6.3.0 の http://www.unicode.org/Public/6.3.0/ucd/PropList.txt
を元に作られているので、ほぼ網羅されている。
レンダリングして調べる
Python の Pillow を使い、画像に文字を実際にレンダリングしてみた。
簡単に説明すると、元の画像と文字をレンダリングした画像とに変化がなければ、透明であると定義した。
それを Unicode の全文字に対して試した。
フォントは最大のグリフ数を誇ると思われる、 GNU Unifont を使用した。
Unicode の PUA (Private Use Area) は除外した。
この方式では、 Unicode 上で制御文字として定義されている文字は、むしろ列挙されない。
以下の画像のように、制御文字用のグリフがそのまま描画されるので透明でなくなる。
http://unifoundry.com/pub/unifont-7.0.06/unifont-7.0.06.bmp
import unicodedata
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
FONT = ImageFont.truetype('./unifont-7.0.06.ttf', 32)
def main():
assert_principles()
for char in invisible_characters():
try:
name = unicodedata.name(char)
except ValueError:
name = ''
line = 'U+{:X}\t{}'.format(ord(char), name)
print(line)
def invisible_characters():
'''空白として描画された文字のみを返却する'''
empty_draw_bytes = letter_draw_bytes(' ')
for char in unicode_characters():
draw_bytes = letter_draw_bytes(char)
if draw_bytes == empty_draw_bytes:
yield char
def unicode_characters():
'''Unicode コードポイントに含まれる文字全てを返すジェネレータ
ただし、 PUA (Private Use Area) は含めない。
'''
for codepoint in range(0, 0xE000):
yield chr(codepoint)
for codepoint in range(0xF8FF + 1, 0xF0000):
yield chr(codepoint)
for codepoint in range(0xFFFFD + 1, 0x100000):
yield chr(codepoint)
for codepoint in range(0x10FFFD + 1, 0x10FFFF):
yield chr(codepoint)
def letter_draw_bytes(char):
'''空白の画像に文字を描画した際の bytes を取得する'''
image = Image.new('RGB', (32, 32), (255, 255, 255))
draw = ImageDraw.Draw(image)
draw.text((0, 0), char, font=FONT, fill='#000')
return image.tobytes()
def assert_principles():
'''以下の原則が成立することをいくつかの例でチェック'''
f = letter_draw_bytes
assert f('あ') == f('あ') # 同じ文字の bytes は同じ
assert f('') == f(' ') == f(' ') # スペースは空文字列と同じ bytes になる
assert f('あ') != f('い') # 見た目が異なる文字の bytes は異なる
if __name__ == '__main__':
main()
結果
U+20 SPACE
U+A0 NO-BREAK SPACE
U+2000 EN QUAD
U+2001 EM QUAD
U+2002 EN SPACE
U+2003 EM SPACE
U+2004 THREE-PER-EM SPACE
U+2005 FOUR-PER-EM SPACE
U+2006 SIX-PER-EM SPACE
U+2007 FIGURE SPACE
U+2008 PUNCTUATION SPACE
U+2009 THIN SPACE
U+200A HAIR SPACE
U+205F MEDIUM MATHEMATICAL SPACE
U+3000 IDEOGRAPHIC SPACE
U+FFA0 HALFWIDTH HANGUL FILLER
このうち
U+FFA0 HALFWIDTH HANGUL FILLER
だけが未知だった。 HALFWIDTH だけが透明として検知できたのだが、 HALFWIDTH でないものも、 GNU Unifont 以外では透明であったので、一応追加した。
U+3164 HANGUL FILLER
The Unicode Consortium の配布物
The Unicode Consortium の配布物を延々と人力で調べることにした。
例えば DerivedCoreProperties など
その結果 U+2061 ... U+2064 などを発見した。
おそらく数学用の制御文字(?)。詳細は不明。
U+2060 WORD JOINER のすぐそばにあるのに、ほとんど言及されてない。