Don’t say cheese

The paparazzi are determined to get a photo of me double-screening Netflix and Reddit. They post a tantalising link to a website that will try to activate my webcam, take a photo and post it to their file host to be auctioned to the Red Tops for their celebrity gossip pages.

Understanding the answer to stopping this means we have to understand how programs talk to hardware and how the different parts of the computer are connected and how they talk to each other. Otherwise we risk locking the door but ignoring the fact there’s no wall.

To test this, we are going to look at the browserleaks website where it will be delighted to give you all the information it can about your browser and computer. I see there that it recognises I have a video input device. It’s short on specifics, but it has a video capture permissions button which we can use to get it interested in the camera specifically.

People have already thought about this issue and all your modern browsers will trap the request for webcam access and pop up a dialogue asking if you want to allow it or not. The permissions are durable so once a website as been granted or denied access, that’s what it will get forever more. Or at least until you go into settings and manually change it. So are we done here? Not really.

Web broswers are imperfect and someone may find a way to escape the javascript sandbox and request camera permissions directly. PDF exploits are reasonably common and could potentially try and access my webcam as well. While it’s great that this layer of protection exists, we wouldn’t be overthinking the problem if we stopped there.

My next layer of defence is to sandbox chromium when it runs. I use firejail which allows us to control system access at quite a granular level. I also use Linux which has an “everything is a file” philosophy so we should be able to deny access to the webcam device files as an extra safety net. In fact firejail has already considered this for a number of hardware types and provides us with the “novideo” profile option.

Restarting chromium we find that browserleaks has stopped telling me about my video input device. At the same time, if I click the video permissions button I get an immediate access denied message rather than the normal permissions dialogue. This is progress! Next I would like to re-enable the camera for Skype. After going the long way around the barn a few times, I was pointed at the “ignore” command that can be added to the local settings that will ignore subsequent uses of that command. This means the Skype profile can be told to ignore the gobal novideo setting.

$ cat /etc/firejail/globals.local 
novideo
$ cat /etc/firejail/skypeforlinux.local
ignore novideo

The next line of reasoning is the video device file is the result of a driver claiming a generic USB device. Should we be checking that generic device? Joining into the firejail sandbox lets us evaluate what chromium can see.

$ firejail --list
13931:/usr/bin/firejail /usr/bin/chromium-browser
16523 --list
$ firejail --join=13931
Switching to pid 13932, the first child process inside the sandbox
Child process initialized in 8.26 ms
$ lsusb
lsusb: cannot open "/var/lib/usbutils/usb.ids", No such file or directory
Bus 002 Device 005: ID 03f0:231d
Bus 002 Device 003: ID 064e:a127
Bus 002 Device 002: ID 8087:0020
Bus 002 Device 001: ID 1d6b:0002
Bus 001 Device 002: ID 8087:0020
Bus 001 Device 001: ID 1d6b:0002

The file that translates device IDs to something human readable is inaccessable, but the actual list of devices is not. The thing to try next would be to prod those devices and see what we can dig out of them. C has a libusb library so let’s see what happens.

#include <libusb.h>
#include <stdio.h>

int main(int argc, char **argv) {
	int r = libusb_init(NULL);

	libusb_device **list;
	ssize_t c = libusb_get_device_list(NULL, &list);

        printf("detected %ld devices\n", c);
       
	for (int i = 0; i < c; i++) {
		uint8_t b = libusb_get_bus_number(list[i]);
		uint8_t d = libusb_get_device_address(list[i]);
		printf("bus %d device %d\n", b, d);

		int interface_number;

		if ((b == 2) && (d == 3)) {

			libusb_device_handle *handle;
			r = libusb_open(list[i], &handle);
			
			if (r == 0) { 
				printf("Success\n");
				libusb_close(handle);
			}
			else if (r == LIBUSB_ERROR_NO_MEM)
				printf("No memory\n");
			else if (r == LIBUSB_ERROR_ACCESS)
				printf("DENIED!\n");
			else if (r  == LIBUSB_ERROR_NO_DEVICE)
				printf("No device\n");
			else
				printf("Error\n");
		}

	}
        	
        libusb_free_device_list(list, 0);

	libusb_exit(NULL);

	return 0;
}
$ gcc usb.c -I/usr/include/libusb-1.0/ -o usb -lusb-1.0 && ./usb
detected 6 devices
bus 2 device 5
bus 2 device 3
DENIED!
bus 2 device 2
bus 2 device 1
bus 1 device 2
bus 1 device 1

Alright, this is looking pretty good. Direct USB access to the webcam is denied as my regular user anyway. It is permitted by root, but that's a whole other problem. Still, I can't think of a reason to have the web browser talking to any USB device directly. Even if we wanted to permit it to save files to a USB drive itself, it would still be a filesystem operation, not it talking to the USB device directly. Running strace on this program reveals a foray into the /sys/bus/usb/devices tree to find the devices to interrogate. We can blacklist that too and check on lsusb from inside the container.

$ firejail --list
18489:/usr/bin/firejail /usr/bin/chromium-browser
19060:firejail --list
$ firejail --join=18489
Switching to pid 18490, the first child process inside the sandbox
Child process initialized in 15.51 ms
$ lsusb
lsusb: cannot open "/var/lib/usbutils/usb.ids", No such file or directory

Nothing at all shows up now. I think we've pretty firmly neutered the browser talking to any USB devices. Delightfully, skype doesn't need an exemption because of course it was always playing nice and looking for video devices, not USB devices.

But the was anything talking to the USB device directly anyway? Not really. The USB devices don't connect to the CPU so something must be mediating. In our case there is a USB hub that shows up as a PCI device. Could we talk to the USB hub from userspace, bypassing the kernel control completely and via a pile of userspace drivers talk to the hub, then the usb device, then the webcam directly?

PCI devices talk to the CPU via memory mapped IO (MMIO). If the CPU writes to the correct address in memory then instead of writing to RAM, it writes to an external device. Our program can request an area of memory at a particular address via the mmap() command so can it talk to the PCI device?

No. The program is running in protected mode and has been since 1982. This means a few things, but most importantly it doesn't have a real view of the system memory. Instead a memory management unit (MMU) takes care of allocating physical memory and mapping that to process specific virtual addresses. It's conceptually similar to how the Javascript program we described above ran isolated inside chromium and has to ask permission to get to the more interesting functions. Without the MMU playing ball, we will never be able to allocate the addresses used by the memory mapped peripherals and the only program the MMU is interested in working with is the kernel.

The existence of userspace drivers says this must be possible somehow, and it's true that if you memory map the right offset from /dev/kmem you will be able to access the peripheral directly, but that is a root-only operation.

I'm now reasonably confident that nobody is getting access to my webcam via the browser. Any child processes of the browser are sandboxed with the same rules as the browser itself, so even something like a PDF exploit is still not getting into my webcam. Kernel and hardware protections prevent access at lower levels, and always did.

This won't be perfect, but I'm confident it's more robust than it was and I'm pleased that my browser now reports no webcam at all rather than a webcam you aren't allowed to use.

Now, what about that microphone.

This entry was posted in learn. Bookmark the permalink.