
本脆弱性は、drivers/media/dvb-core/dmxdev.c に存在する競合状態の脆弱性です。 そのため、この dmxdev を使用する usb デバイスドライバでは競合状態が発生する可能性があります。
この脆弱性は、3つの UAF を引き起こすために使用される可能性があります。
まず、エクスプロイトの順序は次のとおりです(drivers/media/usb/ttusb-dec/ttusb_dec.c を使用してデバッグしました):```
cpu0 cpu1 cpu 2
1. dvb_dvr_open()
2. ttusb_dec_disconnect()
ttusb_dec_exit_dvb()
dvb_dmxdev_release()
wait_event(dmxdev->dvr_dvbdev->wait_queue, …)
3. dvb_demux_open()
dvb_demux_ioctl()
dvb_usercopy()
copy_from_user() <- userfaultfd stuck
4. dvb_dvr_release()
5. vfree(dmxdev->filter)
6. copy_from_user() <- userfaultfd release
dvb_demux_do_ioctl()
struct dmxdev *dmxdev = dmxdevfilter->dev; <- UAF!!
詳細なエクスプロイトの流れは以下の通りです。
1. デバイスノードの中で `dvr0` を `open()` します。これにより `dvb_dvr_open()` が呼び出され、`dvbdev->users++` が実行されます。
この場合、USBデバイスの取り外しによって `ttusb_dec_disconnect()` が実行されると、`wait_event(dmxdev->dvr_dvbdev->wait_queue, …)` の条件が捕捉されます。
2. USBデバイスを物理的に取り外します。
この場合、`ttusb_dec_disconnect()` 関数の実行中に、`wait_event(dmxdev->dvr_dvbdev->wait_queue, …)` で待機します。
3. `demux0` ノードを `open()` して、`dvb_demux_open()` 関数を呼び出します。
この関数も、ステップ1の `dvb_dvr_open()` 関数と同様に `dvbdev->users++` を実行します。
しかし、この不適切な参照カウントが発生するのは、現在待機中の `dvb_dmxdev_release()` 関数が `dvbdev->users` のチェックをすでに通過しているためです。```
void dvb_dmxdev_release(struct dmxdev *dmxdev)
{
dmxdev->exit = 1;
if (dmxdev->dvbdev->users > 1) { // This check is bypassed. improper reference counting.
wait_event(dmxdev->dvbdev->wait_queue,
dmxdev->dvbdev->users == 1);
}
if (dmxdev->dvr_dvbdev->users > 1) {
wait_event(dmxdev->dvr_dvbdev->wait_queue, // Currently wait()ing here.
dmxdev->dvr_dvbdev->users == 1);
}
dvb_unregister_device(dmxdev->dvbdev);
dvb_unregister_device(dmxdev->dvr_dvbdev);
vfree(dmxdev->filter);
dmxdev->filter = NULL;
dmxdev->demux->close(dmxdev->demux);
}
その後、demux0 に対して ioctl() を呼び出します。呼び出し時、第3引数は userfaultfd(または FUSE fs)によって設定されたユーザー空間アドレスを渡します。そして ioctl の実行中、dvb_usercopy() の copy_from_user() でハングします:``` int dvb_usercopy(struct file *file, unsigned int cmd, unsigned long arg, int (*func)(struct file *file, unsigned int cmd, void *arg)) { char sbuf[128]; void *mbuf = NULL; void *parg = NULL; int err = -EINVAL;
/* Copy arguments into temp kernel buffer */
switch (_IOC_DIR(cmd)) {
case _IOC_NONE:
/*
* For this command, the pointer is actually an integer
* argument.
*/
parg = (void *) arg;
break;
case _IOC_READ: /* some v4l ioctls are marked wrong ... */
case _IOC_WRITE:
case (_IOC_WRITE | _IOC_READ):
if (_IOC_SIZE(cmd) <= sizeof(sbuf)) {
parg = sbuf;
} else {
/* too big to allocate from stack */
mbuf = kmalloc(_IOC_SIZE(cmd), GFP_KERNEL);
if (NULL == mbuf)
return -ENOMEM;
parg = mbuf;
}
err = -EFAULT;
if (copy_from_user(parg, (void __user *)arg, _IOC_SIZE(cmd))) // here
goto out;
break;
}
4. dvr0ノードをopen()したスレッドでそのfdをclose()します。
するとdvb_dvr_release()が呼び出され、`dvbdev->users--`と`wake_up(&dvbdev->wait_queue)`が実行されて、.disconnectフローであるdvb_dmxdev_release()関数を起床させます。
5. .disconnectフローでは、dvb_dmxdev_release()関数内で`vfree(dmxdev->filter)`が実行されます。
6. userfaultfdを設定したdemux0 ioctlスレッドからuserfaultfdを解放します。
これにより、.disconnectで解放したばかりの`dmxdev->filter`アドレスを読み取ることでUAFが発生します。
これでUAFはdvb_demux_do_ioctl()のすべてのケースで利用可能になり、eBPFと組み合わせればLPEも可能です。