- xz/liblzmaバックドアは最終的なバイナリペイロードだけの問題ではなく、ビルド中に実行される Bash段階 が多層の抽出・復号手順で隠されており、分析難度が高い
- 影響を受けるバージョンは xz/liblzma 5.6.0 と 5.6.1 で、難読化されたスクリプトとバイナリペイロードはテストファイルに見える2つのファイルに含まれている
- Stage 0 は
m4/build-to-host.m4 から始まり、破損しているように見える xz ストリームを復元し、xz -d で Stage 1 スクリプト を取り出して実行する
- Stage 1 は
good-large_compressed.lzma からデータを切り出して置換した後、Stage 2 Bashスクリプト を実行し、5.6.1 では Linux 上での実行可否を5回確認するコードが追加されている
- Stage 2 は
.o ファイルをビルド・リンク工程に差し込むため、ファイルカービング、置換暗号、AWKベースの RC4派生復号 を使用し、5.6.1 には将来追加のスクリプトを実行できる拡張構造も含まれている
xz/liblzmaバックドアのBash段階の範囲
- Andres Freund が oss-security メーリングリストで xz/liblzma バックドアの発見を報告し、このバックドアは OpenSSH server に影響する
- 分析対象はバイナリバックドアそのものではなく、その前に実行される Bashベースの初期段階 と難読化方式である
- Bash の流れは Stage 0 から Stage 2 まで続き、完全には実装されていないように見える潜在的な Stage 3 も一部が明らかになっている
- 難読化・暗号化された段階と後続のバイナリバックドアは、2つのテストファイル内に隠されている
tests/files/bad-3-corrupt_lzma2.xz
tests/files/good-large_compressed.lzma
- 影響を受けるバージョンは 5.6.0 と 5.6.1 で、両バージョンの間には小さな差異がある
Stage 0: 破損したテストファイルのように見える xz ストリームの復元
- 出発点は
m4/build-to-host.m4 のコードで、ビルド工程のどこかで実行されるとみられる
- 中核となるパイプラインは
sed、tr、xz -d をつなぎ、Stage 1 スクリプト を抽出する
tests/files/bad-3-corrupt_lzma2.xz のバイトを読み取った後、tr "\t \-_" " \t_\-" で一部のバイトを置換する
0x09 タブは 0x20 空白に変わる
0x20 空白は 0x09 タブに変わる
0x2d ハイフンは 0x5f アンダースコアに変わる
0x5f アンダースコアは 0x2d ハイフンに変わる
- この置換は
bad-3-corrupt_lzma2.xz を再び正常な xz ストリーム に戻す役割を果たす
- 最後に復元されたストリームを
xz で展開し、エラーを無視したまま Stage 1 スクリプトを取得して即座に実行する
- ストリームは途中で切れているように見えるが、有意味な出力はすでに書き出されているため問題にならない
Stage 1: good-large_compressed.lzma から Stage 2 を取り出す
- Stage 1 は Andres Freund のメールで
####Hello#### から始まる短い Bash ファイルである
- 両バージョンの最初の違いは2行目のコメント内にある任意バイトである
- 5.6.0:
86 F9 5A F7 2E 68 6A BC
- 5.6.1:
E5 55 89 B7 24 04 D8 17
- 5.6.1 には
[ ! $(uname) = "Linux" ] && exit 0 形式の Linux チェック が追加されており、同じチェックが5回繰り返される
- Stage 2 の抽出は長い
export i=... 行から始まり、この値は後で eval $i により呼び出される一種の関数のように使われる
- 抽出フローは標準出力と標準入力を連結し続けるパイプライン構造である
- まず
xz -dc で good-large_compressed.lzma を展開する
eval $i は head の呼び出しを繰り返し、1024バイトを捨てて2048バイトを出力するパターンを反復する
- ファイル末尾では 5.6.0 は 724バイト、5.6.1 は 939バイトを出力する
- この
head チェーンは空の低エントロピー領域を除去し、実データが入っている領域だけを連結する
- その後
tail -c +31233 で先頭部分を捨てる
- この先頭部分には次段で抽出される バイナリバックドア が隠されている
- 5.6.0 では先頭31264バイト、5.6.1 では31232バイトが捨てられる効果になる
- 次に
tr が単純な置換暗号のように使われ、5.6.0 と 5.6.1 では対応キーが異なる
- 5.6.0:
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377"
- 5.6.1:
tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
- 置換されたデータは
xz -F raw --lzma1 -dc で再度展開され、結果として得られた Stage 2 が即座に実行される
Stage 2: ビルド工程の改変と拡張メカニズム
- Stage 2 は Andres Freund が元のメールに添付した
infected.txt ファイルで、実際の コンパイル工程の改変 が行われる Bash スクリプトである
- 難読化の観点では、Stage 2 には3つの興味深い断片があり、そのうち2つは 5.6.1 のみに 現れる
- 5.6.1 の2つの断片はほぼ同一で、
tests/files/ 内から特定シグネチャを持つファイルを探す
- Fragment 1 は
~!:_ W と |_!{ - を探す
- Fragment 3 は
jV!.^% と %.R.1Z を探す
- シグネチャが見つかるとファイル名とオフセットを抽出し、最初のシグネチャの直後から2番目のシグネチャの直前までのバイト範囲を切り出す
- 切り出したデータは 5.6.0 で使われた
tr キーで置換復号された後、xz -F raw --lzma2 -dc で展開され、eval で実行される
- 調査対象となった 5.6.0 と 5.6.1 の TAR アーカイブには、そのシグネチャを持つファイルは存在しなかった
- この構造は既存のペイロード運搬用テストファイルを再編集せず、新たなテストファイルを追加するだけで将来のスクリプトを実行できる 拡張・パッチシステム のように見える
Stage 2: バイナリバックドア .o ファイルの抽出
- Stage 2 のもう1つの中核は
.o ファイルを抽出し、コンパイル・リンク工程に差し込むコードである
- 抽出対象の成果物は
liblzma_la-crc64-fast.o で、これが最終的な バイナリバックドア である
- 抽出工程は前段と同様に複数の標準ツールをパイプラインで接続する
good-large_compressed.lzma を xz -dc で展開する
- Stage 1 で使われた
eval $i の head チェーンを再利用する
sed "s/\(.\)/\1\n/g" で各バイトの後ろに改行を付け、1バイト1行の形式にする
- 続く AWK スクリプトは入力ストリームに対して RC4に似た復号 を行う
- キー配列は
((i * 7) + 5) % 256 で生成される
- 5.6.0 は出力 PRNG ストリームの先頭4096バイトをスキップする
- 5.6.1 は 8192バイトをスキップする
- 本来の RC4 は XOR を使うが、このコードは
(v + k) % 256 形式の加算を使う
- 復号されたデータは
xz -dc --single-stream で展開される
- その後
head で必要なバイト範囲だけを切り出し、liblzma_la-crc64-fast.o として保存する
- 圧縮されているが一部改変されたペイロードサイズは 5.6.0 で 88792バイト、5.6.1 で 88664バイトである
- 最終的な抽出範囲は約86KB規模である
標準ツールだけで作られた難読化の性質
- Bash段階全体は標準的なコマンドラインツールだけで構成されている
sed
tr
xz
head
tail
awk
grep
cut
expr
- ペイロードは正常なテストファイルに見えるバイナリファイルに隠されており、必要な領域だけを ファイルカービング で取り出す方式である
- 難読化には単純な置換暗号と AWK ベースの RC4 派生復号が併用されている
- 実行は合計3段階で進み、5.6.1 には後から別のテストファイルを追加してスクリプトを実行できる構造が含まれている
- 全体構造には無害に見せつつ巧妙に隠すための多大な工夫が見られ、偶然発見された事例だとすれば、未発見の類似事例がどれほど残っているのかという疑問を残す
1件のコメント
Hacker News のコメント
簡略化された説明とノイズ画像の比較のおかげで、人々が言う巧妙さがどういう意味なのか感覚がつかめた
reddit で「サンドボックス化」の方式が点ひとつで壊されたという話も見たし、
#includeのすぐ次の行の左端に点が見えるhttps://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...
+,+.,+のように見えて、その点ひとつが目立たない単なるミスでテストが常に失敗したり常に成功したりするだけでも厄介なのに、悪意ある行為の標的になりやすい理由が分かる
a) このパッケージを cmake でビルドしている人は事実上ほとんどいない
b) cmake と
-DENABLE_SANDBOX=landlockでビルドしてみると、単にビルドが失敗する: https://i.imgur.com/7xbeWFx.pngその点はサンドボックス化を無効化するのではなく、cmake でビルドできなくするだけ。実際に誰かが cmake でビルドしていればエラーを見て何かがおかしいと気づいたはずなので、セキュリティを下げようとする悪意ある試みと見るには筋が通らない
「これが偶然見つかったのなら、まだ見つかっていないものがどれだけ残っているのか」が最も重要な問い
Andres Freund が、バックドアを仕込まれた人気オープンソースプロジェクトのうち唯一の一つを偶然見つけたとは考えにくい。すでに実環境にこういうものが十数個あっても不思議ではない
次の攻撃者は、実行時間の増加のような痕跡を残すことにははるかに慎重になるだろう
ここで不穏なのは、ユニットテストが攻撃経路を開いたということ。テストがなければ、ここまで隠すのはずっと難しかったはず
bad-3-corrupt_lzma2.xzには 3 つのストリームがあり、1 番目と 3 番目は有効な xz ストリーム。中央のストリームはストリームヘッダー、ブロックヘッダー、インデックス、ストリームフッターが正しく、LZMA2 データだけが壊れており、--single-streamを使えば展開されるはずだと書かれている####Hello####と####World####の文字列は、README の指示を実際に従ったときに、それらしい正常な結果が出るようにしている$ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream####Hello####これらの文字列はシェルコメントなので、ペイロードの実行を妨げない
最後に、後で正規表現がファイル名を直接参照したり実際の Hello/World 文字列を使ったりせずにファイルを見つけられる目印として機能する
$ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``$ echo $gl_am_configmake./tests/files/bad-3-corrupt_lzma2.xzこの検査は推移的に適用されるべきで、Linux ディストリビューションが新しい liblzma バージョンへ更新しようとしたときにも、新しいバイナリ依存関係のためにビルドが失敗するか警告が出るべき
Linux ディストリビューションのビルドでこうした慣行がどれほど一般的なのかは分からない。一般的なら整理するのに膨大な作業が必要になるだろうし、そもそも可能かどうかも不確か。bazel なら可能そうだが、他のビルドシステムはよく分からない
GitHub で似たような head | tail トリックをすでに探した人がいるのか気になる。これのために新しく発明されたとは考えにくい
巨大な
.shファイルがライセンスを表示して同意を取ったあと、自分自身をcatしてhead/tailのパイプを通し、cpioで実際のアセットを展開するというものより良い答えはないものの、この難解な bash の塊自体が怪しい匂いではないかと思う
開発の世界の他の領域でもそうだが、何が起きているのかもっと明確に見えるよう、もう少し不透明でない書き方はできなかったのだろうか?
管理者が外部コントリビューターほど厳格なレビューなしに悪意あるコードを入れられることは分かるが、「簡潔な」コードの塊、実質的に意図せぬ難読化に見えるようなやり方より、もっと良い道があるはずだ
根本的な問題は、80〜90年代にそれぞれ欠陥や足りない機能を抱えた多数の Unix 系システムがあり、ソフトウェア作者たちはビルド依存関係を最小限にしたがっていたことにある
そのため多くのコミュニティは、どこでも動くシェルスクリプトでビルドを自動化する方向に標準化した。しかしシェルスクリプトを書くのは苦痛で、人々は M4 マクロプリプロセッサのようなツールでシェルスクリプトを生成するようになった
その結果、誰かが AIX や壊れた古代の Unix でコードを実行したい場合に備えて、多くのプロジェクトが巨大で不透明なシェルスクリプトの塊を抱えることになった
この突破しにくいシェルの藪をなくすには、サポート対象プラットフォーム数を大幅に減らし、よりクリーンなビルドツールで標準化し、移植可能なビルドにシェルを必要としない言語で中核インフラをもっと作る必要がある
しかしこれは巨大な作業で、中核的な C ライブラリの多くは無給のボランティア1、2人が保守している。「Obscurnix-1997」のサポート終了は、たいていかなり論争的な判断にもなる
そのため中核インフラの多くは、いまだに正体不明の機械生成シェルスクリプトの沼に囲まれている
少なくない数のツールがこの方式でビルドされている
疑わしく見えるのは、繰り返される tr 呼び出しだ。そういうものを見ると、誰かが賢く振る舞おうとしていると考えるが、ここでの「賢さ」は悪い意味だ。自分が管理者なら、そういうコードが入ってきたとき、それが何をしているのか説明を求めただろう。そうしたチェーンを避ける、より良い解決策がほぼ常にあるからだ
本当の問題は、このコードが入るときに見てくれる別の管理者がいなかった点だ。スタックの重要な構成要素が1人に依存しており、今回の場合その人物が悪意を持っていた
execさせないように実行する方法はあるのだろうか? bash に「セキュリティモード」のようなものがあれば、と思うただし xv の configure スクリプトで、そうした仮想の「セキュリティモード」で bash をどう実行できるのか想像できないので、取り消す
https://github.com/tukaani-project/.github/issues/2
ビルドツールや古い Unix ユーティリティまで含む C エコシステム全体が、悪用されるのを待っているセキュリティの混沌であり、結局は悪用されるだろう
ドット1つですべてを壊すのがどれほど簡単かを見るだけでよい。もはや世界のセキュリティを C に賭けることはできないと、人々は気づくべきだ
モダンなツールチェーンを備えた Ada や Rust を使ってほしい
いったいこれがどうやってコードレビューを通過してマージされたのか分からない。何か見落としているのでなければ、ばかげているほど不注意に見える
さらにこれはテストファイルとして示されたバイナリファイル、つまり “good”/“bad” の xz 圧縮テストファイルの中に、ひどく難読化されていた。何を探すべきか分からなければ気づく方法はない
LTS ディストリビューションを使えば、ある程度は保護される可能性がある。Slackware はパッケージに lzma、つまり
tar.xzを使っているようだが、-currentを除く最後の安定リリースにはこの問題はなかった自由ソフトウェアの方向にさらに一歩進みたいなら、Hyperbola GNU もこの問題はなかった
さらに Slackware
-currentもsshdを xz にリンクしておらず、systemd も使っていない