How to Get Root Access to Your Sleep Number Bed

Table of Contents

Disclaimer: Following this guide will require modifying internal files on your Sleep Number hub. This will void your warranty and if something goes wrong as a result of this process, you will not receive help from Sleep Number to fix it. There are many ways you can damage your system, through both hardware and software modifications. The author provides no guarantees and you follow this process at your own risk.

As an Amazon Associate, I earn from qualifying purchases. Thank you for your support.

Motivation

I have been interested in exploring the possibility of local network access on my Sleep Number bed for a few years. A while back I created a homebridge plugin for the platform that would let me control some of the settings of the bed through HomeKit or some automations. The "bed presence" value, which indicated whether somebody was in bed or not, was especially nice for running automations such as turning off all the lights or locking the door. However, after running the plugin for a couple years and growing the user base large enough, I received a friendly phone call from corporate Sleep Number asking me to kindly disable the plugin.

You see, I had set the plugin to request data from the SleepIQ API every 5 seconds to get an accurate reading on the bed presence value. Multiply that by the couple thousand users of the plugin at that point and the result was enough strain on their network that they had taken notice and started looking for the cause. They also weren't too fond of the branding of the repository, since to the untrained eye it looked like an official Sleep Number tool, which it definitely was not. Oh well, lessons learned, I shut it down.

This was the motivator for finding a way to access the local network and bypass their servers completely. I passed my learnings from the homebridge plugin to a good friend who restarted that project in a new and improved plugin (which disables the bed presence monitoring by default and included lots of warnings before enabling it) and turned my focus to the hardware.

I cracked open the hub a couple times and looked around for anything obvious. At first, my attention was drawn to the J10 header near the CPU.

J10 Header
J10 Header (I took the pictures after installing the pin headers. Yours are probably just holes.)

In one CPU configuration, those pins would contain UART lines. However, try as I might, I was never able to get any UART data out. The CPU is in a different configuration. For some reason, I never thought to test the J16 header lower down on the board during my first attempts, but recently I finally took notice of it.

J16 Header
J16 Header

This time I wanted to be sure, so I bought a logic analyzer and connected it to all 10 available header pins.

Uart data in the logic analyzer
UART data in the logic analyzer

Bingo.

I hooked up my UART-TTY device and at long last was greeted with a device console.

UART console showing u-boot bootloader output
UART console showing u-boot bootloader output

I spent some time dumping the flash to browse the file system on the board and look through the files for different vulnerabilities. At first I was searching for a backdoor that would allow anybody to log into the hub without needing to hook up a UART, but I came up empty. Well, not empty. What I did find was a "convenient" backdoor that Sleep Number can use to SSH back into the hub (and my internal home network as a result). Likely it is to perform maintenance on the hub as needed, but the paranoid part of me was not happy when I found that. Regardless of if you choose to follow this guide or are just reading for fun, I highly recommend you disconnect the wifi on your hub and only use bluetooth controls as much as possible. That being said, for those willing to do a little hardware work, setting up local network access is straightforward, and the rest of this post will detail the steps you need to follow to do so on your own hub.

This guide will also be useful for anybody who purchased a used hub from somebody else, only to find out that Sleep Number refuses to connect to it without an associated order number. The only extra requirement is to add a bam-init.conf file to the USB drive to provide network details to the hub (probably, I haven't tested that process yet).

Pre-Requisites

This process was performed on Sleep Number Hub with model number: 360SIQ01D. It looks like this:

Sleep Number Hub
Sleep Number Hub

You will also need a UART to TTY device and some other hardware tools.

ItemDescriptionPricePaid Link
USB-C to UARTThis is the device I used because it is USB-C and less than $10 (at the time of writing)$9.99https://amzn.to/3xr1rBk
USB-A to UARTThere are also plenty of USB-A options, such as this one from the same company$13.99https://amzn.to/3RAmHeR
Raspberry Pi Pico WAnother option is to buy a Raspberry Pi Pico W and install it inside the case permanently, so you can easily remotely connect to the console in case the SSH ever stops working1$11.50https://amzn.to/3VQ5YXh
2.54mm Pin HeadersThese will let you add permanent pin headers to your board$7.99https://amzn.to/4c9Arp8
Soldering IronNeeded to solder the pin headers in place$13.99https://amzn.to/3RzKQC4
PCB Clip ClampA temporary alternative to the above options. You can clip this to the pin headers on the board$13.99https://amzn.to/3xiuXJE

Prices are approximate and may have changed since the time of writing.

To make it easier to connect to the board, you will want to either get the pin headers and the soldering iron or the PCB clip clamp.

You will communicate with the UART device through a serial console. On Linux or Mac, you can use minicom. On Windows, PuTTY is a good choice.

You will also need a USB-A flash drive formatted as FAT32 or Ext3.

Gaining Root Access

  1. Connect your UART device to header J16 on the board.The pinout is as follows:

    UART Connection
    UART Connection
    • pin 1 is TX (into hub)
    • pin 2 is RX (out of hub)
    • pin 3 is ground
  2. Connect your console to your UART device (minicom, etc.). The board uses a baud rate of 115200.

  3. Power on your hub, you have 2 seconds to press <SPACE> to stop the auto-boot sequence.

  4. Edit the boot environment variables:

    • Back up the default environment variables. Run printenv -a and save the output somewhere you can use to recover the defaults if necessary. I have included a copy in the appendix of this post but my environment might differ from yours, so be sure to note any differences.

    • Run editenv bootcmd and remove the section that says run set_bootargs; (we are going to set the bootargs manually). The variable should contain the following:

      bootcmd=run find_board_name; setenv boot_mmcdev 0; run bootcmd_mmc;setenv boot_mmcdev 1; run bootcmd_mmc
    • The /init script is stored in the hashed bootrom, so we can't modify it without flashing an entirely new bootloader. Instead, we will do a runtime modification using the bootargs environment variable. Run editenv bootargs and add the following:

      console=ttymxc0,115200 root=/dev/mmcblk${linux_mmcdev}p1 rootwait rdinit=/bin/bash -- -c "sed -i 's/LMR=`.*`/LMR=let_me_root/' /init; exec /init"

      By default, the script is looking for an encrypted file that decrypts to the let_me_root phrase. We replaced the decryption line with the decrypted value so we can bypass the encryption requirement2.

    • When everything looks correct, make the changes persistent with saveenv. You can try booting once before saving to make sure everything still works but you will need to repeat the above steps the next time you boot if you don't save the environment.

  5. For the first boot, you will need to insert a flash drive with a file named "let_me_root" on it. If your hub already has what appears to be a flash drive inserted, it is likely a Wi-Fi dongle that will cause a boot loop if it is not present. You will need a USB hub that allows you to connect multiple USB drives at once to get around this issue (just for the first boot). You can boot from the u-boot menu by running run bootcmd. Once you are booted, you should see the following output in the console:

    Starting OpenBSD Secure Shell server: sshd
    done.
    Starting ntpd: done
    Starting crond: OK
    INIT: no more processes left in this runlevel
    

    At this point, if everything worked, you should be able to hit the enter key and see root@bam-se:~#.

    If you do not see this, double-check the bootargs and make sure they match the example above. Also make sure your flash drive has the let_me_root file in place (its format should be FAT32 or EXT3, I don't know if any other formats will work but I know these will).

  6. Once you have the prompt, we will make it persistent without needing the flash drive

    • Remount the root partition as rw:
      /bin/mount -o remount,rw,async /
    • Add the let_me_root file:
      echo let_me_root > /root/let_me_root
    • Remove the flash drive, reboot, and verify you still have console access.
  7. Next, we will finish getting SSH access by adding a public key to the authorized_keys file

    • If you haven't already, generate an SSH key on the machine you want to connect from. I recommend generating an ecdsa key:
      ssh-keygen -t ed25519
    • Make sure the root partition is mounted as rw, then run the following (substituting in your public key contents):
      echo "<contents of id_ed25519.pub>" >> /root/.ssh/authorized_keys
    • Test SSH access with ssh root@$hub_ip -i <path to id_ed25519>. You should be able to connect at this point.
    • If you are not connected, verify the contents of the authorized_keys file is formatted correctly (there should be two keys in the file).

Congratulations, you now have root access!

Creating a Local Network Control and Monitoring Server

By itself, root access is great but doesn't do much for us. We want to be able to replicate the controls provided by the app so we can control the bed over our local network instead.

The hub includes Python 2.7.18. While extremely old (keep in mind the Hub appears to have been last updated in 2018), it includes the simpleHTTPServer package, which will give us all the capability we need to remotely control and monitor the bed over the local network.

On your local machine, create a python file named script_server.py:

import os
import subprocess
import BaseHTTPServer
from urlparse import urlparse, parse_qs

SCRIPT_DIR = '/bam/scripts'

class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    def do_GET(self):
        parsed_path = urlparse(self.path)
        script_name = parsed_path.path.lstrip('/')
        script_path = os.path.join(SCRIPT_DIR, script_name)
        if os.path.isfile(script_path):
            # Gather all 'arg' query parameters into a list
            query_args = parse_qs(parsed_path.query)
            args = query_args.get('arg', [])

            # Run the script with the arguments requested
            output = subprocess.check_output([script_path] + args)
            self.send_response(200)
            self.end_headers()
            self.wfile.write(output)
        else:
            self.send_response(404)
            self.end_headers()
            self.wfile.write('Script not found')

def run(server_class=BaseHTTPServer.HTTPServer, handler_class=MyHandler):
    server_address = ('', 8000)  # Listen on all interfaces, port 8000
    httpd = server_class(server_address, handler_class)
    httpd.serve_forever()

if __name__ == "__main__":
    run()

This script sets up an HTTP server that can run any of the scripts in /bam/scripts. You can provide arguments to the scripts with the query argument arg like so: ?arg=value1&arg=value2...&arg=valueN. The result will be displayed in the browser.

Make sure you remounted the root partition as rw since the last reboot, then copy this script to the hub:

scp -O script_server.py root@$hub_ip:~

At this point, you can run the script and test it out, but it won't auto-start when you reboot the hub. One method of auto-starting the script would be to tack it onto one of the other startup scripts (initBAM seems like a good candidate). Another option is to add an rc.d script so it gets started by the OS. However, there is a snag—the /etc directory is copied from disk and stored in RAM on the system you log in to, so if we want to write to the disk where the original version lives, we will need to do a few extra steps.

Adding rc.d Scripts to the /real.root Partition Stored on Disk (Outside the chroot)

The running OS instance is actually running inside a chroot that is set up by the bootloader /init script with many of the system files stored in a tmpfs partition so they can't be modified directly (past a reboot at least). However, we can mount the original partition and modify the files there, so they will be properly copied into place after we reboot.

First, create the following file on your local system named script_server.sh:

#!/bin/sh
## BEGIN INIT INFO
# Provides:          script_server
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Start daemon at boot time
# Description:       Enable service provided by daemon.
## END INIT INFO

dir="/root"
cmd="python script_server.py"

name=script_server
pid_file="/var/run/$name.pid"
stdout_log="/var/log/$name.log"
stderr_log="/var/log/$name.err"

get_pid() {
    cat "$pid_file"
}

is_running() {
    [ -f "$pid_file" ] && ps `get_pid` > /dev/null 2>&1
}

case "$1" in
    start)
    if is_running; then
        echo "Already started"
    else
        echo "Starting $name"
        cd "$dir"
        $cmd >> "$stdout_log" 2>> "$stderr_log" &
        echo $! > "$pid_file"
        if ! is_running; then
            echo "Unable to start, see $stdout_log and $stderr_log"
            exit 1
        fi
    fi
    ;;
    stop)
    if is_running; then
        echo -n "Stopping $name.."
        kill `get_pid`
        for i in {1..10}
        do
            if ! is_running; then
                break
            fi

            echo -n "."
            sleep 1
        done
        echo

        if is_running; then
            echo "Not stopped; may still be shutting down or shutdown may have failed"
            exit 1
        else
            echo "Stopped"
            if [ -f "$pid_file" ]; then
                rm "$pid_file"
            fi
        fi
    else
        echo "Not running"
    fi
    ;;
    restart)
    $0 stop
    if is_running; then
        echo "Unable to stop, will not attempt to start"
        exit 1
    fi
    $0 start
    ;;
    status)
    if is_running; then
        echo "Running"
    else
        echo "Stopped"
        exit 1
    fi
    ;;
    *)
    echo "Usage: $0 {start|stop|restart|status}"
    exit 1
    ;;
esac

exit 0

Copy it to the hub the same as you did the python script previously.

Next, we will move it into place in the pre-init system. Run the following commands to get the disk mounted properly:

mkdir tmp
ROOT_PART=/dev/mmcblk0p1
/bin/mount -a
/bin/mount -o rw,noatime ${ROOT_PART} tmp

Now we will move the script to its proper home and add the necessary symlinks:

mv script_server.sh tmp/etc/init.d
chmod +x tmp/etc/init.d/script_server.sh
ln -s ../init.d/script_server.sh tmp/etc/rc0.d/K99script_server.sh
ln -s ../init.d/script_server.sh tmp/etc/rc3.d/S99script_server.sh
ln -s ../init.d/script_server.sh tmp/etc/rc4.d/S99script_server.sh
ln -s ../init.d/script_server.sh tmp/etc/rc5.d/S99script_server.sh
ln -s ../init.d/script_server.sh tmp/etc/rc6.d/K99script_server.sh
reboot

Once it finishes rebooting, try pointing your browser at http://$hub_ip:8000/bamstat. If everything is working correctly you should see some status information about the hub displayed in your browser window.

Useful Commands

The /bio script appears to be the primary control script for the bed. You can specify commands with the url /bio?arg=XXXX. Here are some useful ones for controlling the bed:

CommandDescription
arg=PSNLGet the last set sleep number value for the left bed side
arg=PSNRGet the last set sleep number value for the right bed side
arg=PSNlGet the current sleep number value for the left bed side
arg=PSNrGet the current sleep number value for the right bed side
arg=PSNXGet the last set and current sleep number values for both bed sides
arg=PSNS&arg=L100Set the sleep number for the left bed side to 100 (L or R for side and 5..100 for value)
arg=SPAUGet the bed privacy setting
arg=SPAU&arg=onSet the bed privacy setting (off to disable)
arg=LBPLGet the presence value for the left side
arg=LBPRGet the presence value for the right side
arg=LBPXGet the presence value for both sides
arg=LRSGGet the responsive air setting for both sides
arg=LRLE&arg=onSet the responsive air setting for the left side to on
arg=LRRE&arg=offSet the responsive air setting for the right side to off
arg=MFSTGet the head and foot position for both sides
arg=MFUL&arg=100&arg=0Set the head position for the left side (0..100 for the position. The second argument is speed, 0=fast, 1=slow)
arg=MFUR&arg=100&arg=0Set the head position for the right side
arg=MFFL&arg=100&arg=0Set the foot position for the left side
arg=MFFR&arg=100&arg=0Set the foot position for the right side
arg=MFO3Get the right underbed light status (if you have a bed with other outlets or lights, MFO1..4 all work)
arg=MFSO&arg=IBT3Set the outlet values, I=index<1=R, 2=L, 3=R-under, 4=L-under, 5=Both, 6=Both-under, 7=All, 8=All-R, 9=All-L>; B=brightness<0=off, 1=on/bright, 2=dim, 3=moderate> T=timer<0-180>. Timer is optional
arg=MUAS&arg=onEnable or disable the auto setting for the underbed light (off to disable)
arg=FWGLGet the footwarming level and timer for the left side
arg=FWGRGet the footwarming level and timer for the right side
arg=FWSL&arg=100&arg=360Set the footwarming level and timer for the left side (level is 0..100 and timer is 0..360)
arg=FWSR&arg=100&arg=360Set the footwarming level and timer for the right side
arg=SBASBaseline the bed

There are many other controls in the bio script, such as getters and setters for foundation presets, snore configuration, DualTemp controls, and status information (watch out for the more destructive commands such as factory reset or the radio controls). You can explore the script to find all the available commands: if you point your browser at /bio?arg=help, you will see a list of the commands you can run. If you add a command as a second argument to the help call, it will provide detailed information for that command.

Next Steps

There are a few things to explore in the OS now that you have access:

  • The bed control functionality is all in the /bam root directory. You can explore the various scripts contained there and learn how the hub operates.

  • I spent some time exploring the system to find a backdoor that doesn't require UART access but it appears (fortunately) that the developers covered their tracks pretty well and shut down all the obvious access channels I could think of. If anybody finds something I might have missed, I would love to hear about it.

  • The hub communicates with the Sleep Number servers by opening an SSH tunnel and providing a reverse tunnel back to the hub that their developers can use to connect to the hub and do maintenance when needed. The idea that unknown users can directly connect to my internal home network is a scary thought, so I will probably be disconnecting the hub from the external internet once I am satisfied with my internal network control script. It also makes me wonder how many other internet-connected appliances include a similar backdoor into the home network like this one has.

  • It might be fun to write a simple progressive web app to control the bed settings that can directly replace the SleepIQ app.

If anybody has any feedback, please reach out! I would love to hear from you.

Appendix: Original U-Boot Env Variables on My Hub

baudrate=115200
bootcmd=run find_board_name; run set_bootargs;setenv boot_mmcdev 0; run bootcmd_mmc;setenv boot_mmcdev 1; run bootcmd_mmc
bootcmd_mmc=mmc dev ${boot_mmcdev}; mmc read ${loadaddr} 0x800 ${initrd_size};bootm ${loadaddr}#${board_name}
bootdelay=2
bootm_size=0x10000000
fdtcontroladdr=bfc831f0
find_board_name=if gpio input 102; then if gpio input 98; then setenv board_name siq-360;else setenv board_name siq-selc;fi; else setenv board_name siq-se;fi
initrd_size=0x7000
linux_mmcdev=0
loadaddr=0x82000000
set_bootargs=setenv bootargs console=ttymxc0,115200 root=/dev/mmcblk${linux_mmcdev}p1 rootwait
stderr=serial
stdin=serial
stdout=serial

Footnotes

  1. Setting this up is left as an exercise for the reader

  2. This encrypted file is a key-hole the developers put in place to grant both console and SSH access to the hub. If the original encrypted file were to be obtained or reverse-engineered, most of this process would not be necessary.

  3. On my version of the bio script, the MFSO lines were in the wrong order in the LIST and couldn't be called. I had to move them up a couple rows to get them to work.