More magic - Lessons learned from NUL byte bugs
Lessons learned from NUL byte bugs Posted on 2012-12-10
NUL バイトバグから得られた教訓
Last time I explained how sloppy representations can cause various vulnerabilities. While doing some
research for that post I stumbled across NUL byte injection bugs in two projects. Because both have
been fixed now, I feel like I can freely talk about them with a clear conscience.
前回(More magic - Structurally fixing injection bugs)
わたしは sloppy representations がどのようにしてさまざまな脆弱性を引き起こす可能性があるかについて
説明しました。ちょっとした調査をしている間、わたしは二つのプロジェクトにおける NUL バイトインジェク
ションに取り組んでいました。その二つのプロジェクトはどちらも現在では修正済みであるので、それらのバ
グについて自由に書けるだろうと判断しました。
These projects are Chicken Scheme and the C implementation of Ruby. The difference in the way these
systems deal with NUL bytes clearly shows the importance of handling security issues in a structural
way. We'll also see the importance of truly grokking the problem when implementing a fix.
その二つのプロジェクトとは、 Chicken Scheme と Ruby の C 実装です。これらのシステムにおける NUL バイ
トを扱う方法の違いは security issues を structual way で handling することの重要性をはっきりと示して
います。わたしたちはまた、修正を行うときの truly grokking the problem の重要性も見ることになるでしょ
う。
A quick recap
Remember that C uses NUL bytes to delimit strings. Many other languages store the length of the
string instead. In these languages, NUL bytes can occur inside strings. This can cause unintended
reinterpretation when strings cross the language border into C.
C は NUL バイトを文字列を終端するために使っていることを思い出してください。他の多くの言語では C とは
違って文字列の長さを保持しています。そのような言語では、NUL バイトを文字列の中に含めることもできます
が、これは言語の境界 (language border) を越えて C へ文字列を渡すときに意図していない reinterpretation
を引き起こす可能性があります。
In my previous post I already pointed out how Chicken automatically prevents this reinterpretation
in its foreign function interface (FFI). You just describe to Scheme that your C function accepts a
string, and it will take care of the rest:
前回の投稿では Chicken がその foreign function interface (FFI) によってどのようにして自動的にこの
reinterpretaion を防いでいるのかについて説明しました。行うことは文字列を受け付ける C 関数について
Scheme で記述するだけで、残りのことは Chicken がよろしくやってくれます。
(define my-length (foreign-lambda int "strlen" c-string))
;; Prints 12:
(print (my-length "hello, there"))
;; Raises an exception, showing the following message:
;; Error: (##sys#make-c-string) cannot represent string with NUL
;; bytes as C string: "hello\x00there"
(print (my-length "hello\x00there"))
The FFI's feature of automatically checking for NUL bytes in strings before passing them on to C was
only added in late 2010 (Chicken 4.6.0). However, because everything uses this interface, this
mismatch could easily be fixed, in a central location, securing all existing programs in one fell
swoop.
文字列を C へ渡す前にその文字列に含まれている NUL バイトに対する検査を自動的に行う FFI の機能は、
2010 年の終盤になってから Chicken 4.6.0 で追加されたものに過ぎません。
しかしすべてがこのインターフェースを使っているのでこの mismatch は簡単に修正することができました
in a central location,
securing all existing programs in one fell swoop.
Now, you may be thinking "well, that's nothing special; it's good engineering practice that
there must be a single point of truth, and that you Don't Repeat Yourself". And you'd be right!
In fact, this is a key insight: solid engineering is a prerequisite to secure engineering. It can
prevent security bugs from happening, and help to fix them quickly once they are discovered. A core
tenet of "structural security" is that without structure, there can be no security.
さてここであなたはこう考えるかもしれません。 “特別なことはなにもない。これは
there must be a single point of truth な優れた engineering practice だ。
繰り返し同じことをすることはない。”
と。そう、あなたは正しい! 実際、これは key insight であり、
solid engineering とは secure enginnering のための prerequisite (必要条件、あらかじめ必要となるもの)
です。
これは偶発的なセキュリティバグを防ぎ、また、セキュリティバグが見つかったときの即座の修正を手助けします。
"structural security" の core tenet (主義、主張) は、structure 抜きでは no security になる可能性が
あるということです。
When smugness backfires
To drive home the point, let's take a look at what I discovered while writing my
previous blog post. After describing Chicken's Right Way solution and feeling all smug
about it, I noticed an embarrassing problem: for various reasons (some good, others
less so), there are places in Chicken where C functions are called without going
through the FFI. Some of these contained hand-rolled string conversions!
To drive home the point,
前回の blog 記事を書いている間にわたしが発見したものを取り上げることにしましょう。
Chiken の正しい解決方法を説明してすべてを解決したつもりになった後で、
FFI を通すことなく呼び出される C 関数がさまざまな理由で Chicken に存在していることにわたしは
気がついたのです。そういったものの一部には hand-rolled string conversions があったのです!
It turns out that we overlooked these places when first introducing the NUL byte
checks, and as a consequence several critical procedures (standard R5RS ones like
with-input-from-file) were left vulnerable to exactly this bug:
このことはわたしたちが最初に NUL byte check を導入したときにそういった場所を見逃していて、
(standard R5RS ones like with-input-from-file のような) いくつかの critical procedure がまさにこの
バグであるところの脆弱性を残していたことが明らかになったのです。
;; This program outputs "yes" twice in Chickens < 4.8.0
(with-output-to-file "foo\x00bar" (lambda () (print "hai")))
(print (if (file-exists? "foo") "yes" "no"))
(print (if (file-exists? "foo\x00bar") "yes" "no"))
To me, this just validates the importance of approaching security measures in a
structural rather than an ad-hoc way; the bug was only in those parts of the code that
didn't use the FFI. Deviation from a rule is where bugs are often found!
わたしにとってはこれは単に、セキュリティにおいては ad-hoc な方法よりも
structual な方法をとることが格段に重要であることの確認です。
このバグは FFI を使っていなかった部分のコードにしかありません。
バグはルールを守っていないところからしばしば発見されたのです!
You can also see that we fixed it as thoroughly as possible, especially given the at times awkward
structure of the Chicken code. We commented every special situation extensively, assigned a new error
type C_ASCIIZ_REPRESENTATION_ERROR for this particular error, and added regression tests for at least
each class of functionality (string to number conversion, file port creation, process creation,
environment access, and low-level messaging functionality). There's definitely room for improvement
here, and I hope to one day reduce the special cases to the bare minimum. By documenting special
cases it's easy to avoid introducing new problems. It also makes them easier to find when refactoring.
The tests help there too, of course.
わたしたちが可能な限りの修正をしたこと、特に Chiken のコードの awkward structure を修正したことは
Chicken のコードで確認できます。わたしたちは広範囲にわたってすべての special situation にコメントを
つけ、特定のエラーに対して新しいエラー型 C_ASCIIZ_REPRESENTATION_ERROR を割り当て、それぞれの機能の
クラスに対する最低限のテストを行う regression test を追加しました。この機能クラスには文字列から数値
への変換、ファイルポートの作成、プロセスの生成、環境へのアクセス、低水準メッセージング機能といったも
のがあります。ここには room for improvement (改良、改善すべき点?) が間違いなく存在していて、そして、
いつの日か bare minimum のための special cases を reduce することをわたしは希望しているのです。
特別なケースをドキュメント化することによって、新しい問題を持ち込むことを排除するのが簡単になります。
It also makes them easier to find when refactoring.
また、いつリファクタリングするかの判断をより容易なものにします。
The tests help there too, of course.
When you run the above program in a Chicken version with the fix, it behaves like expected:
上記のプログラムを Chiken の修正済みバージョンで実行すると次のような結果となります:
Error: cannot represent string with NUL bytes as C string: "foo\x00bar"
Another approach
もうひとつのアプローチ
The Ruby situation is a little more complicated. It has no FFI but a C API, so it
works the other way around: you write C to interface "up" into Ruby. It has
a StringValueCStr() macro, which is documented as follows (sic):
Ruby の situation はちょっと複雑になっています。Ruby には FFI がなく C API があるので
so it works the other way around:
Ruby に対するインターフェースを C で書きます。
Ruby には StringValueCStr() というマクロがあるのですが、
このマクロのドキュメントは次のように書かれています。
You can also use the macro named StringValueCStr(). This is just
like StringValuePtr(), but always add nul character at the end of
the result. If the result contains nul character, this macro causes
the ArgumentError exception.
StringValueCStr() というマクロを使うこともできます。
このマクロは StringValuePtr() と似ていますが、常に nul 文字をその結果の末尾に追加します。
nul 文字がすでに含まれている場合にはこのマクロは ArgumentError 例外を引き起こします。
However, this isn't consistently used in Ruby's own standard library:
しかしこのマクロは Ruby 自身の標準ライブラリでも一貫性を持って使われているわけではありません:
File.open("foo\0bar", "w") { |f| f.puts "hai" }
puts File.exists?("foo")
puts File.exists?("foo\0bar")
In Ruby 1.9.3p194 and earlier, this shows the following output, indicating it's vulnerable:
1.9.3p とそれより前のバージョンのRubyでは、このコードは次のような脆弱性を示唆する出力を行います:
true
test.rb:4:in `exists?': string contains null byte (ArgumentError)
from test.rb:4:in `<main>'
It turns out that internally, Ruby strings are stored with a length, but also get a NUL byte tacked
onto the end, to prevent copying when calling C functions. This performance hack undermines the safety
of Ruby to C string conversions, and is the direct cause of these inconsistencies. True, there is a
safe function that extracts the string while checking for NUL bytes, but there are also various ways
to bypass this, and if you accidentally use the wrong macro to extract the (raw) string, your code
won't break. Of course, this is only true for benign inputs...
ここで、Ruby の文字列は内部的には長さも一緒に格納されているけれども NUL バイトが終端に置かれていて、
それが C 関数を呼び出したときのコピーを邪魔していることが明らかになりました。この performance hack
は文字列の Ruby から Cへの変換の安全性を傷つけているし、これら incosistencies の直接の原因となって
います。確かに文字列を extract する際に NUL バイトをチェックする安全な関数は存在しますが、それをバ
イパスする方法もたくさんあって、もしあなたが文字列を extract するのに accidentally に間違ったマクロ
を使ってしまってもあなたのコードが壊れることはありません。
もちろんこれはいじわるでない (benign) 入力に対してしか意味はありません。
The complexity of Ruby's implementation makes it hard to ensure that it's safe everywhere. Indeed,
the various places where strings are passed to C all do it differently. For example, the ENV hash
for manipulating the POSIX environment has its own hand-rolled test for NUL, which you can easily
verify; it produces a different error message than the one exists? gave us earlier:
Ruby の実装のこの complexity は、プログラムのどの部分も安全であることを保証すること (ensure that
it's safe everywhere) を困難にしています。実のところ、C に対して文字列を渡している場所はたくさん
あって、それぞれ違ったやり方をしているのです。たとえば POSIX 環境変数を操作するための ENV ハッシュは
独自の hand-rolled な test for NUL を持っているのですが、exits? とは異なるエラーメッセージを生成す
るのでそのことは簡単に確かめられます。
irb(main):001:0> ENV["foo\0bar"] = "test"
ArgumentError: bad environment variable name
There is no reason this couldn't just use StringValueCStr(). So, even though Ruby has this safe
macro, which provides a mechanism to check for poisoned NUL bytes in strings, it's rarely used by
Ruby's own internals. This could be fixed just like Chicken; here too, the best way to do that
would be to generalize and eliminate all special cases. Simpler code is easier to secure.
ここで StringValueCStr() を使っていけない理由はありません。そして Ruby がこの、文字列中の poisoned
NUL bytes に対する検査機構を提供する安全なマクロを持っているのにもかかわらず、Ruby 自身の内部であ
っても使われていることがまれなのです。これは Chicken と同じように修正が可能でした。この修正のため
の最善の方法は generalize を行って すべての special case を eliminetate することです。より単純な
コードは安全にするのもより容易なのです。
A fundamental misunderstanding
根本的な誤解
When I reported the bug in the File class to the Ruby project, they quickly had a fix, but
unfortunately they seemed uninterested in going through Ruby's entire code to fix all string
conversions (quoting from private e-mail conversation):
わたしが File クラスのバグを Ruby プロジェクトに報告したとき、彼らは即座にそのバグを修正したのですが、
残念なことに Ruby のコード全体を通してすべての文字列変換を修正しようということには興味がなかったよう
です (privateなやり取りをしたメールから引用します)。
> I agree that this looks like a good place to fix the File/IO
> class, but there are many other places where strings are passed to C.
> Are all of those secured?
All path names should be converted with "to_path" method if possible.
If any methods don't obey the rule, it is another bug. Please let us
know if you find such case.
In retrospect, there is the possibility that I didn't quite make myself clear enough. Perhaps this
person thought I was referring to other path strings in the code. However, to me it sounds a lot
like they made the same conceptual mistake that the PHP team made when they "fixed" NUL
injections.
振り返ってみると、わたしは自分の意図を十分明確にできていなかったのかもしれません。おそらく返答したこ
の人物は、わたしがコード中のほかの path 文字列を渡すことを言っていると考えたのでしょう。しかし、わた
しにとってその行動は、PHP team が NUL injections を「修正」したときに犯してしまったのと同じ
conceptual mistake を Ruby 開発者たちが犯してしまったように思えたのです。
The PHP solution was to add a special "p" flag for converting path strings. This happens
for all PHP functions declared in C (via zend_parse_parameters()). By the way, notice how this is a
new flag. There probably are tons of PHP extensions out there which aren't using this flag yet. Also,
who can verify that they managed to find all the strings in PHP which represent paths?
PHP で採用された解決策は特別な "p" フラグを path 文字列の変換のために追加するというものでした。これ
は (zend_parse_parameters()を通じて) C で宣言されている PHP 関数すべてに影響しました。ところでこの新
しいフラグはどのように認識するのでしょうか。おそらくこのフラグをまだ使っていない PHP extensions は山
ほどあるでしょう。また、PHP において path を表現している文字列をすべて見つけ出すのを彼らが manage し
たと誰が verify できるのでしょうか?
The PHP team was completely missing the point here. This fix means that path arguments aren't allowed
to have embedded NUL bytes. Other string type arguments are not checked. They are missing the fact
that this isn't just a path issue. Rather, as I described before, it's a fundamental mismatch at the
language boundary where strings are translated from the host language to C. However, there seems to
be a widespread belief that this can only be exploited in path strings.
ここでPHP team は完全にポイントを見失っていました。彼らの行った修正は、path 文字列が埋め込まれた NUL
バイト (embedded NUL bytes) を持つことは許されないというものです。他の文字列型の引数はチェックされま
せんでした。彼らはバグが単なる path についての問題 (path issue) でないという事実を見落としていました。
わたしが以前説明したようにこれは、文字列が host 言語から C へと変換される language boundary における
fundamental mismtach なのです。しかしながら path 文字列においてのみ exploit 可能であると広く信じられ
てしまっているようです。
I'm not entirely sure why this is, but I can guess. First off, "poisoned NUL byte" attacks
have been popularized by a 1999 Phrack article. This article shows a few attacks, but only the
path examples are really convincing. Of course, another reason is that injecting NUL bytes in path
strings really is the most obvious and practical way to exploit web scripts.
それがなぜなのかわたしは完全に確信してはいませんが、推測はできます。最初に 1989年の Phrack article に
よって "poisoned NUL byte" 攻撃はポピュラーになりました。
この article は少数の攻撃を例示しただけでしたが、path を使った例はとても説得力のあるものでした。
誤解が広まったもうひとつの理由はもちろん、 path strings への NUL bytes の injection が web script を
exploit するための 最も obvious で practical な方法であったということです。
Recently, however, different NUL byte attacks have been documented. For example, they can be used to
truncate LDAP and SQL queries and to bypass regular expression filters on SQL input, but you could
argue these are all examples of failure to escape correctly. I found a more convincing example in the
(excellent!) book The Tangled Web: it contains a one-sentence warning about using HTML sanitation C
libraries from other languages. Also, NUL bytes can sometimes be used to hide attacks from log files.
しかし最近になって異なる NUL バイト攻撃がドキュメント化されました。たとえば NUL バイト攻撃は LDAP や
SQL のクエリを truncate したり、SQL 入力に対する正規表現フィルターのバイパスするのに使用可能です。
しかしそういった例のすべては正しいエスケープに失敗しているだけだという主張は可能です。
わたしは The Tangled Web という (excellentな) 本で、もっと convincing な例を発見しました。
その本には C 以外の言語から HTML のsanitation をする Cのライブラリを使うことについての一節がありました。
また、NUL バイトはログファイルから攻撃を隠すことにも使えることがあります。
However, the most impressive recent exploit is without a doubt this common vulnerability in SSL
certificate verification systems. In an attack, an embedded NUL byte causes a certificate to be
accepted for "www.paypal.com", when the CN (Common Name) section (that is, the server's
hostname) actually contains the value "www.paypal.com\0.thoughtcrime.org". Certificate
authorities generally just accepted this as a valid subdomain of "thoughtcrime.org",
ignoring the NUL byte. Client programs (like web browsers) tended to use C string comparison
functions, which stop at the NUL byte. Luckily, this was widely reported, and has been fixed in
most programs.
しかしながら最近最も強い印象を与えた exploit はまず間違いないなく SSL certificate verification systems
における common vulnerability です。ある攻撃において、埋め込まれた NUL バイトは実際には
"www.paypal.com\0.thoughtcrime.org" であるような
CN (Common Name) section (that is, the server's hostname) に対する
certificate を "www.paypal.com" として accept させてしまいました。
certificate authorities は一般的にこれを "thoughtcrime.org" の vaild なサブドメインと
して acceptし、NUL バイトを無視します。(webブラウザのような) クライアントプログラムは
NUL バイトの場所で比較をストップする C の文字列比較関数を使う傾向にあります。
幸運にもこれは広く report されていますし、またほとんどのプログラムでは修正済みです。
I believe that NUL byte mishandling represents a big and mostly untapped source of vulnerabilities.
High-level languages are gaining popularity over C for client-side programs, but many crucial
libraries are still written in C. This combination means that the problem will grow unless this is
structurally fixed in language implementations.
NUL バイトの mishandling が巨大で最も untapped な vulnerabilities の原因の原因になっているとわたしは
確信しています。高水準言語はクライアントサイドの言語として C を越えた popularity を得ましたが、多く
の重要なライブラリ群は今でも C で書かれています。この組み合わせは、問題が言語の実装において
structurally に fix されるまでは大きくなり続けることを意味しています。
Except where otherwise noted, content on this site is licensed under a Creative Commons
Attribution 3.0 License. All code fragments on this site are hereby put in the public domain.