[TECH] Handmade anti theft system

Posted on Tue 14 April 2020 in Tech, Hardware, Setup

Following my last post I decided to play a bit more with 4G networks and the oportunities of having it on a laptop.

During those Easter's holiday I finally took the time to prototype a small project I had in mind for a long time, I wanted to design an anti-theft system for my laptop. As most of the disks are using full disk encryption, it's not possible to integrate such a mechanism inside the system itself. The obvious solution was to bundle it before the system start: the initramfs.

The roadmap was the following: - create a custom initramfs - bundle a modem management software in it - script this tool to retrieve and send GPS/4G location to a central server - bonus: take a picture using the webcam and send it to the central server

Writing an initramfs

As always, gentoo wiki is a amazing start for this kind of task:

readonly ROOTDIR=/home/ex0ns/Projects/initramfs/
mkdir --parents $ROOTDIR/{bin,dev,etc,lib,lib64,mnt/root,proc,root,sbin,sys}
cp --archive /dev/{null,console,tty,nvme0n1p4,random,urandom,cdc-wdm0} $ROOTDIR/dev/

This will create the basic structure for our initramfs, I'm using lvm and cryptsetup hence urandom and random devices are required. cdc-wdm0 is my 4G modem, and my laptop has nvme, this is why I don't have any sda device.

I then needed to bundle lvm and cryptsetup binaries into my initramfs, those can be staticaly compiled and thus easily copied into the initramfs. We will also add busybox in our initramfs in order to run the init script as well as having a few useful tools in case of failure.

cp --archive /sbin/cryptsetup $ROOTDIR/sbin/cryptsetup
cp --archive /sbin/lvm.static $ROOTDIR/sbin/lvm
cp --archive /bin/busybox $ROOTDIR/bin/busybox
ln -s $ROODIR/bin/busybox $ROOTDIR/bin/sh

Having those binary, it's now time to write a very simple (as simple as possible) init script to decrypt, mount and boot the system:

#!/bin/busybox sh

rescue_shell() {
    printf '\e[1;31m' # bold red foreground
    printf "\$1 Dropping you to a shell."
    echo "$1"
    printf "\e[00m\n" # normal colour foreground
    # load the keymap
    exec sh

mount -t proc none /proc || rescue_shell "Unable to mount proc"
mount -t sysfs none /sys || rescue_shell "Unable to mound sys"

cryptsetup --tries 5 luksOpen /dev/nvme0n1p4 root || rescue_shell "Cryptsetup failed"

lvm vgscan --mknodes || rescue_shell "vgscan failed #1"
lvm lvchange -a ly vg0/root || rescue_shell "lvchange failed"
lvm lvchange -a ly vg0/usr || rescue_shell "lvchange failed"
lvm vgscan --mknodes || rescue_shell "vgscan failed #2"

mount -o ro /dev/mapper/vg0-root /mnt/root
mount -o ro /dev/mapper/vg0-usr /mnt/root/usr

umount /proc
umount /sys

# Boot the real thing.
exec switch_root /mnt/root /sbin/init || rescue_shell "Unable to switch root"

I had the (would not recommend it) strange idea of having a separate /usr so I had to take care of that as well in my init script. The script is pretty classic, it asks for the password to decrypt the disk, scan for specific LVM partitions (no udev support needed) and finally mount them into the new root of the system /mnt/root in our case.

We can already package and test our initramfs:

find . -print0  | cpio --null -ov --format=newc | gzip -9 > ../initramfs-debug.img
mount /boot
cp ../initramfs-debug.img /boot/

Add an entry in Grub/rEFI and reboot \o/

4G capabilities at boot

My first thought was to bundle mmcli into my initramfs as I have already written a management script for it. Unfortunately, mmcli is only a userland tool that uses dbus to communicate with the underlying device.

However, other tools exists in the libqmi and libmbim package to manage 4G devices: qmicli and mbimcli. There is no static-libs useflag for those package and will hence require some .so to work, I wrote a small utility script to bundle dynamic binaries into the my initramfs:


bundle_file() {
    file="$(which $1)"
    echo "Bundling $1 in initramfs"

    for f in $file $(lddtree $"$file" | tail -n+2 | awk '/=>/ {print $NF}')
      dir=$(dirname $f)
      [ -d "./$dir" ] || mkdir -p "./$dir"
      echo "Adding $f to initramfs"
      cp "$f" "./$f"
    echo ""

bundle_file "qmicli"
bundle_file "mbimcli"

This script parses the output of the ldtree command to include the required library in the initramfs (as well as the binary itself).

After a few failure, a lot of reboot and some research here, here and here it seems that my card was requiering some kind of proxy between qmi and mbim tools, that proxy can be find at /usr/libexec/mbim-proxy, I simply added this file to my bundle script:

bundle_file "/usr/libexec/mbim-proxy"
bundle_file "mbim-network"

I also added the mbim-network binary that was missing from my setup.

4g script

This script is far from beeing 100% resilient and sometime the interface seems to be stuck or in a weird state at boot, but work well from the proof of concept I want to make:


MMCLI="mbimcli --device=/dev/cdc-wdm0 --device-open-proxy --no-close "

  local config="$1"
  local search="$2"

  echo "$config" | sed -n -E "/$search/ s/$search: '(.*)'/\1/ p"

qmicli -d /dev/cdc-wdm0 --device-open-mbim --dms-set-fcc-authentication

$MMCLI --set-radio-state=on
$MMCLI --query-device-caps # debug purpose
mbim-network /dev/cdc-wdm0 start

$MMCLI --query-radio-state # debug purpose
sleep 10

config=$($MMCLI --query-ip-configuration)

if [ $? -eq 0 ]
  ip="$(get_network_info "$config" 'IP \[0\]')"
  gateway="$(get_network_info "$config" "Gateway")"

  echo "Adding $ip via $gateway to wwan0"

  ip addr add $ip dev wwan0
  ip link set wwan0 up
  ip route add default via $gateway

  ping -c 1

  ip addr # debug purpose
  echo "something wrong happened"

Nothing really new here, only some low level device management, some logging and some parsing to properly configure the network device (only the IP and the default route are needed here).

I added this to my init script, packaged my initramfs and rebooted, and it worked: the ping worked \o/ !

The goal is now to setup as very basic server that could collect informations sent by the client. As my current goal for this project is to write a very basic proof of concept, the server will be very rudimentary (and I mean it):

while true; do; nc -l -p 1234 > "raw_data_$(date).raw"; echo "File written"; done

We sill send to this server the output from two command:

qmicli -d /dev/cdc-wdm0 --nas-get-cell-location-info
qmicli -d /dev/cdc-wdm0 --nas-get-serving-system

Those two commands are giving a lot of useful informations. but I mainly need the following:

  • 3GPP cell ID
  • MCC: Country code, Switzerland's one is 228
  • MNC: Operator code

I simply added those line to my initramfs script:

readonly IP=
readonly PORT=1234
qmicli -d /dev/cdc-wdm0 --nas-get-cell-location-info | nc $IP $PORT
qmicli -d /dev/cdc-wdm0 --nas-get-serving-system | nc $IP $PORT

No need to add nc to our initramfs as busybox already contains rudimentary nc binary. After a reboot, a new file was available on my server, containing all tower and cell informations I could get from my 4g modem.

Bonus: capturing webcam input

As I still had some time left and discover a really simple tool to capture images using the webcam, it only requires libgd, it's called fswebcam. I compiled it, bundled it into my initramfs and added the following line to my init script:

readonly PICTURE_PORT=
  /usr/bin/fswebcam -d /dev/video0 -r 640x480 --jpeg 100 -F 5 test.jpg
  cat test.jpg | nc $IP $PICTURE_PORT

And spawned a new netcat listener on a different port (cheap server, I told you):

while true; do; nc -l -p $PICTURE_PORT > "$(date)_picture.jpg"; echo "File written"; done

And then again, after a reboot, I received the picture on my server.

Next steps

Obviously writing a more advanced server would be the next step. The website OpenCellId has a list of cell with their MCC and MNC as well as the (more or less accurate) GPS coordinate of the antennas.