My laptop only has one USB port.

I understand that the times are changing. Wireless devices are more and more popular, wi-fi and Bluetooth takes over that pesky cables and bulky physical connectors. We’re entering a new cable-less era.

This doesn’t change the fact, that my mouse and my keyboard connect over USB. That’s two devices. And I want both.

So of course I’ve decided to write a Bluetooth keyboard proxy using my Raspberry Pi as a gateway. I couldn’t find any working tutorials, and after a night of cursing at the screen, I’ve decided to share my findings with future generations.

The code should work with any Bluetooth-enabled Linux device. I’ve tested the code on Raspberry Pi and Ubuntu, non-Debian distributions may require a bit different steps.

D-Bus, how does it work?

D-Bus is a IPC framework designed to facilitate communication between multiple processes in a composable and extensible way. It’s a shared channel used by applications to communicate. For example, I can ask Spotify to change song using dbus-send:

$ dbus-send --print-reply --dest=org.mpris.MediaPlayer2.spotify /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player.Next

What just happened is I’ve called a org.mpris.MediaPlayer2.Player.Next “method” on the /org/mpris/MediaPlayer2 object using org.mpris.MediaPlayer2.spotify connection.

Bluez is a Linux implementation of Bluetooth, and it has a D-Bus API 1. It implements the org.freedesktop.DBus.Introspectable interface, so we can look take a look at it via cli:

pi@tmicro:~ $ sudo gdbus introspect --system --dest org.bluez --object-path /org/bluez
node /org/bluez {
  interface org.freedesktop.DBus.Introspectable {
    methods:
      Introspect(out s xml);
    signals:
    properties:
  };
  interface org.bluez.AgentManager1 {
    methods:
      RegisterAgent(in  o agent,
                    in  s capability);
      UnregisterAgent(in  o agent);
      RequestDefaultAgent(in  o agent);
    signals:
    properties:
  };
  interface org.bluez.ProfileManager1 {
    methods:
      RegisterProfile(in  o profile,
                      in  s UUID,
                      in  a{sv} options);
      UnregisterProfile(in  o profile);
    signals:
    properties:
  };
  interface org.bluez.HealthManager1 {
    methods:
      CreateApplication(in  a{sv} config,
                        out o application);
      DestroyApplication(in  o application);
    signals:
    properties:
  };
  node hci0 {
  };
};

We see three interfaces and one node.

We can call methods the interfaces

which we can introspect further with

sudo gdbus introspect --system --dest org.bluez --object-path /org/bluez/hci0
# (...)

Other useful debugging commands are:

  • sudo btmon (monitor Bluetooth activity on the device)
  • sudo busctl monitor [service] (introspect selected bus)

Keyboard service

To serve as a keyboard we only need to do two things: register our service and wait for connection.

Registration is done by calling the org.bluez.ProfileManager1.RegisterProfile method with appropriate parameters.

# UUID for HID service (1124)
# https://www.bluetooth.com/specifications/assigned-numbers/service-discovery
UUID = "00001124-0000-1000-8000-00805f9b34fb"
PROFILE_DBUS_PATH = "/bluez/msm/bluekeyboard"

print("Registering the profile...")
opts = {
    "Role": "server",
    "RequireAuthentication": False,
    "RequireAuthorization": False,
    "AutoConnect": True,
    "ServiceRecord": (Path(__file__).parent / "service.xml").read_text(),
}
bluez = bus.get_object("org.bluez", "/org/bluez")
manager = dbus.Interface(bluez, "org.bluez.ProfileManager1")
manager.RegisterProfile(PROFILE_DBUS_PATH, UUID, opts)

Looks pretty straightforward, right? Wait, what is service.xml? Glad you’ve asked (service.xml):

<?xml version="1.0" encoding="UTF-8" ?>

<record>
    <attribute id="0x0001">
        <sequence>
            <uuid value="0x1124" />
        </sequence>
    </attribute>
    <attribute id="0x0004">
        <sequence>
            <sequence>
                <uuid value="0x0100" />
                <uint16 value="0x0011" />
            </sequence>
            <sequence>
                <uuid value="0x0011" />
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x0005">
        <sequence>
            <uuid value="0x1002" />
        </sequence>
    </attribute>
    <attribute id="0x0006">
        <sequence>
            <uint16 value="0x656e" />
            <uint16 value="0x006a" />
            <uint16 value="0x0100" />
        </sequence>
    </attribute>
    <attribute id="0x0009">
        <sequence>
            <sequence>
                <uuid value="0x1124" />
                <uint16 value="0x0100" />
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x000d">
        <sequence>
            <sequence>
                <sequence>
                    <uuid value="0x0100" />
                    <uint16 value="0x0013" />
                </sequence>
                <sequence>
                    <uuid value="0x0011" />
                </sequence>
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x0100">
        <text value="Raspberry Pi Virtual Keyboard" />
    </attribute>
    <attribute id="0x0101">
        <text value="USB > BT Keyboard" />
    </attribute>
    <attribute id="0x0102">
        <text value="Raspberry Pi" />
    </attribute>
    <attribute id="0x0200">
        <uint16 value="0x0100" />
    </attribute>
    <attribute id="0x0201">
        <uint16 value="0x0111" />
    </attribute>
    <attribute id="0x0202">
        <uint8 value="0x40" />
    </attribute>
    <attribute id="0x0203">
        <uint8 value="0x00" />
    </attribute>
    <attribute id="0x0204">
        <boolean value="false" />
    </attribute>
    <attribute id="0x0205">
        <boolean value="false" />
    </attribute>
    <attribute id="0x0206">
        <sequence>
            <sequence>
                <uint8 value="0x22" />
                <text encoding="hex" value="05010906a101850175019508050719e029e715002501810295017508810395057501050819012905910295017503910395067508150026ff000507190029ff8100c0050c0901a1018503150025017501950b0a23020a21020ab10109b809b609cd09b509e209ea09e9093081029501750d8103c0" />
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x0207">
        <sequence>
            <sequence>
                <uint16 value="0x0409" />
                <uint16 value="0x0100" />
            </sequence>
        </sequence>
    </attribute>
    <attribute id="0x020b">
        <uint16 value="0x0100" />
    </attribute>
    <attribute id="0x020c">
        <uint16 value="0x0c80" />
    </attribute>
    <attribute id="0x020d">
        <boolean value="true" />
    </attribute>
    <attribute id="0x020e">
        <boolean value="false" />
    </attribute>
    <attribute id="0x020f">
        <uint16 value="0x0640" />
    </attribute>
    <attribute id="0x0210">
        <uint16 value="0x0320" />
    </attribute>
</record>

Ok, what is that? It’s an SDP record. SDP (Service Discovery Protocol) records describe characteristics of the device that may be used by remote devices. I won’t explain how it works: partly because it’s out of scope of this post, and partly because I have no idea myself. But it works.

The second part of the puzzle is waiting for a connection. Fortunately Python3 natively supports Bluetooth sockets, so no external dependencies are required:

scontrol = socket.socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)
scontrol.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
scontrol.bind((address, P_CTRL))
scontrol.listen(1)

sinterrupt = socket.socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP)
sinterrupt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sinterrupt.bind((address, P_INTR))
sinterrupt.listen(1)

scontrol, sinfo = scontrol.accept()
print(f"Connected on the control socket {sinfo[0]}")

cinterrupt, cinfo = sinterrupt.accept()
print(f"Connected on the interrupt channel {cinfo[0]}")

Scan codes

Only one thing left to code - we want to send keystrokes to the connected device. This is done by sending a command packet to the socket. The format is:

[0xA1, 0x01, modifier, 0, key0, key1, key2, key3, key4, key5]

But remember that you always have to notify the remote that the keys were released, by zeroing out the keys in the next packet. So let’s implement that:

def send_char(char, cinterrupt):
    keycode, shift = char_to_keycode(char)
    modkey = (1 << 6) if shift else 0
    cinterrupt.send(bytes([0xA1, 1, modkey, 0, keycode, 0, 0, 0, 0, 0]))
    time.sleep(0.01)
    cinterrupt.send(bytes([0xA1, 1, 0, 0, 0, 0, 0, 0, 0, 0]))
    time.sleep(0.01)

The trick is that we’re sending keycodes, not chars. You can observe keycodes with many utilities, for example, xev. I couldn’t find an easy way to convert char to keycode in python, so I went the easy way and just hardcoded the ones I needed (keycodes.py):

def char_to_keycode(char):
    keymap = {
        "1": (30, False),
        "2": (31, False),
        "3": (32, False),
        # ...
        "!": (30, True),
    }
    return keymap[char]

For the demo we’ll just read user input in a loop and send it char by char to the remote:

while True:
    text = input()
    for c in text + "\n":
        send_char(c, cinterrupt)

That’s all! Put all the pieces together in bluetooth_server.py.

Connect the victim

We’re almost done! Now we must disable input plugin in Bluetooth, otherwise the keyboard code will not work:

sudo vim /etc/systemd/system/bluetooth.target.wants/bluetooth.service

Add the -P input parameter:

9c9
< ExecStart=/usr/lib/bluetooth/bluetoothd
---
> ExecStart=/usr/lib/bluetooth/bluetoothd -P input

And restart the service:

sudo systemctl daemon-reload
sudo systemctl restart Bluetooth

Make sure that the service runs on your machine and has the expected parameter:

$ ps aux | grep bluetoothd
root  230  0.0  0.0  8628  5108 ? Ss   02:08   0:00 /usr/lib/bluetooth/bluetoothd -P

Time to start our program:

$ sudo python3 bluetooth_server.py
Registering the profile...
Waiting for connections...

Now let’s connect the victim to your new “keyboard”. Start the agent with bluetoothctl:

$ sudo bluetoothctl
Agent registered
[bluetooth]# power on
Changing power on succeeded
[bluetooth]# discoverable on
Changing discoverable on succeeded
[CHG] Controller 00:21:5C:B0:89:56 Discoverable: yes
[bluetooth]# default-agent 
Default agent request successful

Your machine should now be discoverable. You’ll need to confirm pin in the terminal (and optionally authorize some services):

[NEW] Device 23:1D:C1:F4:10:1D Z2K21E1
Request confirmation
[agent] Confirm passkey 216956 (yes/no): yes

If everything went right, the remote machine should now connect to your “keyboard” and you can send your keystrokes. And without any USB cables. The future is now.

Closing thoughts

The (second) real reason for this post was that I’ve wanted to play with dbus and bluetooth for a long time. Now I had a good reason to do both.

My day job and main interest is security, so obviously I immediately thought how to abuse this. And I’m not impressed - I can easily masquerade my keyboard-wannabe Raspberry Pi as an audio player device. It’s easy to imagine an attack scenario where someone pairs with innocent looking speakers, only to be hacked by injected keystrokes. Nevertheless, I’m glad the USB flaws are not going away, and we can port Rubber Ducky to Bluetooth.

All the code for this post is on Github: https://github.com/msm-code/RandomCodes/tree/master/bluetooth-keyboard.


  1. In fact, the API changed completely a few years ago, and that’s the reason why old code doesn’t work anymore. ↩︎