Deploying Raspberry Pi Headlessly

Configuring Raspberry Pi without a monitor

One of the things I do on a regular basis is build customized raspberry pi devices. I love my Single Board Computers (SBC) and have a small fleet of them varying between the ODroid-C2, ODroid-XU4, Pine A64, and various Raspberry Pi 2/3/4 models among others.

I use them for all kinds of things both personally and professionally. In penetration testing, dropping one of these onto a target network is if not the fastest way, one of the most reliable ways to get from 0 to full network access. Recently I built a small desktop environment on a raspberry pi so that a friend could have remote access to my lab.

Usually the workflow for building a SBC is to simply copy your OS and boot partitions to an SDCard, physically load it into your device, and boot it up. Once booted, you make sure it has network access, plug in your usual peripherals (keyboard, mouse, display) and you can start using it locally. Raspbian has a couple of cool features, such as being able to add a file called ‘ssh’ to the boot partition, and it will enable the SSH service on boot without needing to manually configure it. Most of the OSs that I use, such as Ubuntu or Kali, do not come with this feature, nor is the SSHd services preinstalled so you have to access a the device after boot to configure it and install/enable the SSHd server service.

I got tired of setting up devices with the latest OS, having to attach to a monitor/keyboard/mouse and get devices up and running so I set up a quick workflow that I can use to image a Raspberry Pi boot drive, resize the partitions so it isnt limited to the default image size on my larger 32GB SDcards, and then install software and update the image on the disk for latest versions so my deployed devices are running the latest and greatest patches before being released into the wild.

Gathering your Tools

I do this installation with the following equipment:

  • Linux Desktop - I use a Kali VM, 2020.2 for this tutorial but these instructions should translate to most Linux OSs
  • SD Card USB adapter - I use the SDcard to USB adapter that came with my Raspberry Pi 4 kit
  • SD Card - for this project i used sandisk 32GB class 10 cards.
  • Raspberry Pi 4
  • Power Adapter for Raspberry Pi 4
  • USB SSD (optional) - the MMC can be very limiting in performance as well as concerns around SDCard durability with write limits.
    • USB3 to NVMe M.2 adapter
    • NVMe M.2 Hard drive (I used WDBlue)

Installing OS to SD Card

The OS im going to use for this install is Kali for Raspberry Pi. To keep things simple I used balena Etcher to write my SD card and USB SSD. I have also run this process using Ubuntu 20.04 LTS for Raspberry Pi.

  1. Download your operating system
  2. Plug in your SD card into your USB SD card adapter, then attach it to your PC
  3. Open balena Etcher
  4. select your downloaded OS from step #1
  5. select your SD card
  6. click the “flash” button to write the image to disk
  7. (optional) if using an external USB SSD, repeat the above steps (skip step #1) to perform the exact same process for your USB SSD drive. then delete the second partition (OS partition) from your boot SD card, and delete the first partition (Boot partition) from your USB SSD.

Emulating your Raspberry Pi

It is normally this point where you would configure and cable up your SBC and deploy it, however I often want to be able to deploy my SBCs headlessly without needing that extra manual step of plugging in peripherals and setting up SSH so we are going to mount our new SBC boot disk in a linux OS, run the SBC ARM version of /bin/bash and from there execute the commands as if we are already on the device that we are trying to configure.

Identify your SBC boot / OS device

Before I connect any devices to my linux OS (in this example, I’m using Kali 2020.2) I first look to see what devices are attached through using lsblk.

╭─root@carbonaraKali ~ 
╰─# lsblk
NAME   MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda      8:0    0  65G  0 disk 
├─sda1   8:1    0  57G  0 part /
├─sda2   8:2    0   1K  0 part 
└─sda5   8:5    0   8G  0 part [SWAP]

I then compare this to the output of the same command after attaching the SBC boot drive. if you are using a separate USB SSD to boot your SBC, you will want to do this process with your OS drive, aka the SSD. if you are using a single boot/os drive, proceed using the SDcard.

Attach the boot drive to your OS, and then run lsblk again

╭─root@carbonaraKali ~ 
╰─# lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sda      8:0    0    65G  0 disk 
├─sda1   8:1    0    57G  0 part /
├─sda2   8:2    0     1K  0 part 
└─sda5   8:5    0     8G  0 part [SWAP]
sdb      8:16   1  29.8G  0 disk 
├─sdb1   8:17   1 122.1M  0 part 
└─sdb2   8:18   1  29.7G  0 part 

You can easily see that the new device is mounted as /dev/sdb in this case. The first and smaller partition, /dev/sdb1 is the firmware or boot partition, the second partition, /dev/sdb2 is the OS partition.

Emulating your SBC

From here, the easiest way to edit your device’s OS is to emulate an ARM platform and place ourselves into the SBC’s hard drive as if we are actually running that file system natively. To do this, we are going to use 2 incredibly useful tools:

  • chroot - chroot is a tool that allows you to functionally execute a command from a different position in the filesystem. This will basically let us launch the version of bash located at our mount point (in this example, /mnt/sbc/bin/bash) as if it is running from the root directory (/bin/bash)
  • qemu - qemu is an emulation tool which is going to allow us to emulate ARM hardware. This is important since the Operating system we have installed is specifically designed to run on ARM hardware, so all of the binaries and code are designed to be run on ARM hardware. desktop that is running linux is actually running on x86_64 hardware, and it wont natively understand the ARM instructions. By running our ARM bash binary through qemu, we can basically emulate the Raspberry Pi and work on the file system from inside our desktop before we ever put the boot drive in our SBC.

You may need to install qemu if you dont already have it installed with apt install qemu qemu-user-static binfmt-support. These contain the static linked version of the ARM emulation binary, the full qemu emulation tool, and some support to natively run binaries that we have loaded support for.

We need to mount our SBC OS partition using the previously identified drive letters.

if you are using separate SD and USB SSD drives, you will have different drive letters for your boot and OS drives.
╭─root@carbonaraKali ~ 
╰─# mkdir -p /mnt/sbc
╭─root@carbonaraKali ~ 
╰─# mount -orw /dev/sdb2 /mnt/sbc
╭─root@carbonaraKali ~ 
╰─# mount -orw /dev/sdb1 /mnt/sbc/boot 
╭─root@carbonaraKali ~ 
╰─# cd /mnt/sbc 
╭─root@carbonaraKali /mnt/sbc 
╰─# ls
bin  boot  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

At this point we have mounted our entire SBC hard drive into our Linux OS so we can manipulate it.

We should copy our Qemu static binary to a directory where our emulated Raspberry Pi shell will find it in the $PATH

╭─root@carbonaraKali /mnt/sbc
╰─# cp $(which qemu-aarch64-static) /mnt/sbc/bin 
╭─root@carbonaraKali /mnt/sbc 
╰─# 
I wasn’t sure where the qemu static binary was stored so thats why i used the bash subtitution command $(which qemu-aarch64-static). it serves no other purpose.

To make our lives a little easier, we are going to use some bind mounts to mount some important devices in our Linux OS. These commands mount our /dev, /dev/pts, /proc, and /sys

╭─root@carbonaraKali /mnt/sbc
╰─# mount --bind /dev /mnt/sbc/dev 
╭─root@carbonaraKali /mnt/sbc 
╰─# mount --bind /dev/pts /mnt/sbc/dev/pts 
╭─root@carbonaraKali /mnt/sbc 
╰─# mount --bind /proc /mnt/sbc/proc      
╭─root@carbonaraKali /mnt/sbc 
╰─# mount --bind /sys /mnt/sbc/sys 
╭─root@carbonaraKali /mnt/sbc 
╰─# 

We are now ready to emulate our ARM processor

╭─root@carbonaraKali /mnt/sbc/boot 
╰─# chroot /mnt/sbc /bin/bash
root@carbonaraKali:/# 

There’s been only a minor change in my shell prompt, since my linux OS uses a customized ZSH and we launched bash. to confirm that we are actually running the ARM binary of bash we can run the linux command file on any binary in our new chrooted shell.

root@carbonaraKali:~# file /bin/bash
/bin/bash: ELF 64-bit LSB pie executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, BuildID[sha1]=3951fd7c1824802b0203cfb7863bad8f9d11dab8, for GNU/Linux 3.7.0, stripped

You can see in this example we have the ARM aarch64 binary type. If you run this same command on an x86-64 binary you will get something similar to /bin/bash: ELF 64-bit LSB shared object, x86-64, ... as your output

Since we are now inside our chrooted /mnt/sbc directory, running the arm version of bash, you can run shell commands just as if you were working directly on the SBC. Depending on your SBC OS, you may need to make a slight modification to your /etc/resolv.conf file so that DNS works.

The first thing i do when i get into the chrooted bash environment is update the OS, which can take quite a while if you are running the update on an SD card. If you are using an external SSD, it will be much faster.

root@carbonaraKali:~# echo "nameserver 192.168.128.1" >> /etc/resolv.conf
root@carbonaraKali:~# ping  -c3 google.ca
PING google.ca (172.217.0.227) 56(84) bytes of data.
64 bytes from dfw06s38-in-f3.1e100.net (172.217.0.227): icmp_seq=1 ttl=113 time=7.66 ms
64 bytes from dfw06s38-in-f3.1e100.net (172.217.0.227): icmp_seq=2 ttl=113 time=9.26 ms
64 bytes from dfw06s38-in-f3.1e100.net (172.217.0.227): icmp_seq=3 ttl=113 time=7.88 ms

--- google.ca ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2006ms
rtt min/avg/max/mdev = 7.657/8.264/9.261/0.710 ms
root@carbonaraKali:~# apt update
Get:2 http://http.re4son-kernel.com/re4son kali-pi InRelease [8,133 B]
Get:1 http://kali.download/kali kali-rolling InRelease [30.5 kB]
Get:3 http://http.re4son-kernel.com/re4son kali-pi/main arm64 Packages [13.8 kB]
Get:4 http://kali.download/kali kali-rolling/contrib Sources [62.2 kB]
Get:5 http://kali.download/kali kali-rolling/main Sources [13.1 MB]
Get:6 http://kali.download/kali kali-rolling/non-free Sources [125 kB]                                                                                                                                      
Get:7 http://kali.download/kali kali-rolling/main arm64 Packages [16.5 MB]                                                                                                                                  
Get:8 http://kali.download/kali kali-rolling/non-free arm64 Packages [118 kB]                                                                                                                               
Get:9 http://kali.download/kali kali-rolling/contrib arm64 Packages [78.1 kB]                                                                                                                               
Fetched 30.0 MB in 11s (2,720 kB/s)                                                                                                                                                                         
Reading package lists... Done
Building dependency tree       
Reading state information... Done
789 packages can be upgraded. Run 'apt list --upgradable' to see them.
root@carbonaraKali:~# 
root@carbonaraKali:~# apt upgrade
Reading package lists... Done
Building dependency tree       
Reading state information... 
################# Redacted for Space #################

This upgrade process can take quite some time, again likely due to the read/write speed of SD cards, but we now have the ability to write to this card as if you had booted the OS, and are using a local terminal. We can do things like:

  • install SSH server services to enable remote headless operation apt install openssh-server
  • install OpenVPN or wireguard to deploy the device in a hub-spoke deployment, headless access from behind firewalls. apt install openvpn
  • install iodined to enable DNS tunnelling to get through a strictly firewalled perimeter (iodined as tunneled protocol will be a future post)

In reality, this process evolved from a simple need: the requirement to treat SBCs like Raspberry Pis and Odroid-C2s as if they are 100% headless devices.

Matt
Matt
Security Architect and TechnoHobbiest

My professional interests include information security, free and open-source technologies, and cloud services.