Torvalds氏、クロスプラットフォーム型ウィルスのパッチを作成

Linus Torvalds氏は、昨日のレポートにあったHans-Werner Hilse氏によるテストと分析を吟味し、その内容が正しいことを確認した。問題のウィルスが最新バージョンのカーネルでは伝染しない理由は、GCCによる特定のシステムコールに固有のレジスタ処理に関するバグに起因するという。そして同氏が今回試作したものが、最新版のLinuxカーネルであっても同ウィルスを動作可能にしてみるためのパッチなのである。

話はかなり複雑になるので、ここで順を追って説明しておこう。システムコールが行われるのは、カーネルに対してアプリケーションが(この場合は問題のウィルスだが)、データの読み出しやファイルへの書き込みなど、何らかのタスクの実行を要求した場合である。

こうしたコールの前にはアプリケーションによるハウスキーピング処理の一環として、コールの処理で必要とされる追加情報が特定のレジスタに読み込まれる。レジスタとは、CPUが高速アクセス用に使う一時作業用のストレージアドレスのことである。

たとえば「CAPZLOQ TEKNIQ 1.0」という文字列データをメモリ内で移動させる場合は、この文字列の先頭部の格納アドレスをレジスタの1つに読み込むと同時に、この文字列の移動先アドレスおよび移動させるバイト数もそれぞれ別のレジスタに読み込むことになる。

アプリケーション設計の慣例においては、ある特定のレジスタはコール中に変化しないものと想定しておくことができる。そして最新版カーネルでウィルスが動作しなかった理由なのだが、今回のウィルスはebxというレジスタの1つが変化しないことを前提に組まれていたのだが、実際にはこのレジスタへの書き換えが行われていたのである。

このバグは、私から言わせるとカーネルというよりはGCCのバグなのだが、大部分のコードで表面化することはまずないだろう。これが発現するには、手書きのアセンブラコードと、今では廃れた古いシステムコールの組み合わせという、極めて稀な条件が揃う必要があるからだ。新規ビジネスを呼び込むために大げさに騒ぎ立てているKaspersky Labの主張に反して、こうした状況証拠は、今回のウィルスは新規に登場したコードなどではない、という疑念を支持するものである。

私はTorvalds氏に対して、今度の問題はftruncateシステムコールに起因するもので、これはバギーなold_mmap関数で使われているはずだという、Hilse氏の抱いた疑念を書き送った。以下は、Torvalds氏からの返答だ。

疑念はその通りでした。sys_ftruncate()は、コンパイラに問題があると、%ebxに手を出してしまうようです。こちらでも以前に同様の問題に遭遇したことがあるのですが、これは、システムコールへの引数領域“および”退避レジスタ領域としてシステムコールスタックが使われる場合のコールに関する特殊な規約をカーネルが使用していることに関係しています。

これは不要な引数のセットアップを回避できる分だけシステムコールエントリを高速化するのですが、そうするとやっかいなことにgccは呼び出し側の関数に引数スタックがあるものと想定するので書き換えが可能となる、というわけです。こちらでは、この回避策を過去に確立していたのですが、ftruncateのケースまでは気が付きませんでした(この件が通常のアプリで問題化しない理由は後述します)。

それで、sys_ftruncate()によるgccのコンパイルは下記のようになります。

        sys_ftruncate:
                movl    4(%esp), %eax   # fd, fd
                xorl    %ecx, %ecx      # length
                movl    8(%esp), %edx   # length, length
                movl    $1, 4(%esp)     #,
                jmp     do_sys_ftruncate        #

ここにある「movl $1, 4(%esp)」がオリジナルの引数スタックを書き換えるところです(最初の引数で%ebxの待避領域)。

それでやっかいなのは、この特殊なケースが起きるのは「-mregparm=3」という場合だけで、これはかなり以前から存在していたのに、デフォルトとして採用されたのが2.6.16というわけです。Hans-Werner氏が古いバイナリで問題を確認できなかったのは、おそらくこれが原因でしょう。単にコンパイルした際の設定が違っていたんです。

さて、通常のプログラムが無関係な理由ですが、これはglibcが%ebxレジスタの待避と復帰をシステムコールパスで行っているからです。なので、通常のCライブラリを使っている限り、この件は気にする必要がないというわけです。今度のウィルスはおそらく手書きのアセンブリで作ったものであり、%ebxの待避と復帰をしないのがあだになって、システムコールで変更されることの影響を食らっているのでしょう。

(もっとややこしいのは、これが起きるのは古い「int 0x80」のシステムコールのメカニズムで、今風の「syscall」エントリポイントでは起きないだろうということです。こうした事情もあって、旧式のハードウェアで使うか“あるいは”システムコールのエントリルーチンを手書きした場合でしか発生しないようになっているのでしょう)。

それで、これは“保証”の限りではありませんが、このウィルスが発現するにあたって色々と奇妙な振る舞いをしている一方で、カーネルはユーザレジスタに手を出すのを避けようとしているんでしょう。それと2.6.16の影響というのはミスフィーチャーであって、“正常”なアプリであれば金輪際気にしないはずのものですね。今回はたまたま、ウィルスもどきの感染ロジックに食いついてしまったんです。

Hilse氏はTorvalds氏から提供された回避用パッチを検証して、下記のようにレポートをしている。

試したところ、その通りに動作しました。リコンパイルしたカーネルでは、すべて想定どおりの挙動を示してます。またint 0x80インタフェースについても、やはりアセンブリコードから引っ張ってきています。このウィルスコードでも、オーバーヘッドを避けるために、できるだけレジスタを再利用しようとしてるみたいです(こちらの分かる範囲内でですが)。

古びたウィルスコードがまともに動作するためのデバグと修正は、オープンソースのハッカー達の手に委ねるとしよう。そしてKaspersky Labを代表とするアンチウィルス業界は、昔からあるコードを新種の出現だと騒いで人々を惑わそうとしたことについて、反省をすべきだろう。

原文