├── LICENSE ├── README.md ├── agent └── simple-agent.py ├── dbus └── de.syss.btkbdservice.conf ├── doc └── paper │ ├── Rikki_Dont_Lose_That_Bluetooth_Device.pdf │ └── Security_of_Modern_Bluetooth_Keyboards.pdf ├── images └── bluetooth_keyboard_emulator.png ├── keyboard.conf.example ├── keyboard └── keyboard_client.py ├── server ├── keyboard_server.py └── sdp_record.xml ├── setup.sh └── start.sh /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SySS GmbH 4 | Copyright (c) 2017 quangthanh010290 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bluetooth Keyboard Emulator 2 | 3 | The SySS Bluetooth Keyboard Emulator is a simple proof-of-concept software 4 | tool for emulating Bluetooth BR/EDR (a.k.a. Bluetooth Classic) keyboards based 5 | on the [BlueZ 5 Bluetooth Keyboard Emulator for Raspberry Pi (YAPTB Bluetooth keyboard emulator)](https://github.com/0xmemphre/BL_keyboard_RPI) 6 | by Thanh Le. 7 | 8 | It was developed for modern Linux operating systems like [Arch Linux](https://www.archlinux.org/) with a 9 | modern BlueZ 5 Bluetooth protocol stack and utilities. 10 | 11 | So far, the keyboard emulator has only been tested on Arch Linux systems. 12 | 13 | **_Caution: The software tool is still work in progress._** 14 | 15 | ## Requirements 16 | 17 | - Modern Linux operating system with BlueZ 5 protocol stack (e. g. [Arch Linux](https://www.archlinux.org/)) 18 | - BlueZ 5 utilities [bluez-utils](https://www.archlinux.org/packages/extra/x86_64/bluez-utils/) 19 | - Bluetooth adapter, e.g. Bluetooth dongle with supported chipset (so far only tested CSR8510 and BCM20702A0) 20 | - Python 3 21 | - [https://pypi.org/project/dbus-python/](https://pypi.org/project/dbus-python/) 22 | - [pynput](https://pypi.org/project/pynput/) 23 | 24 | 25 | ## Setup 26 | 27 | For setting up the Bluetooth Keyboard Emulator simply run the provided shell 28 | script **setup.sh** with root privileges. 29 | 30 | ``` 31 | sudo ./setup.sh 32 | ``` 33 | 34 | Or manually copy the file **dbus/de.syss.btkbdservice.conf** to **/etc/dbus-1/system.d/**. 35 | 36 | ``` 37 | sudo cp dbus/de.syss.btkbdservice.conf /etc/dbus-1/system.d/ 38 | ``` 39 | 40 | ## Configuration 41 | 42 | Rename or copy the provided sample configuration file **keyboard.conf.example** 43 | to **keyboard.conf** and edit it with your favorite text editor. 44 | 45 | ## Usage 46 | 47 | In order to start the Bluetooth Keyboard Emulator, run the provided shell script 48 | **start.sh** in a terminal with root privileges. 49 | 50 | ``` 51 | sudo ./start.sh 52 | ``` 53 | 54 | Afterwards, you can attach to the created tmux session named **kbdemu**. 55 | 56 | ``` 57 | sudo tmux attach -t kbdemu 58 | ``` 59 | 60 | **_Caution: The client component of the running Bluetooth Keyboard Emulator will register and process all keypresses!_** 61 | 62 | For stopping the keyboard emulation, you can simply kill the tmux session. 63 | 64 | ``` 65 | sudo tmux kill-session -t kbdemu 66 | ``` 67 | 68 | The following screenshot illustrates the keyboard emulator usage. 69 | 70 | ![Screenshot of a Bluetooth Keyboard Emulator tmux session](https://github.com/SySS-Research/bluetooth-keyboard-emulator/blob/master/images/bluetooth_keyboard_emulator.png) 71 | 72 | # Disclaimer 73 | 74 | Use at your own risk. Do not use without full consent of everyone involved. For educational purposes only. 75 | -------------------------------------------------------------------------------- /agent/simple-agent.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Bluetooth Keyboard Emulator D-Bus Agent 6 | 7 | Based on the BlueZ simple-agent 8 | 9 | http://www.bluez.org/ 10 | """ 11 | 12 | import dbus 13 | import dbus.service 14 | import dbus.mainloop.glib 15 | 16 | from gi.repository import GObject 17 | from optparse import OptionParser 18 | 19 | BUS_NAME = 'org.bluez' 20 | AGENT_INTERFACE = 'org.bluez.Agent1' 21 | AGENT_PATH = "/test/agent" 22 | 23 | bus = None 24 | device_obj = None 25 | dev_path = None 26 | 27 | 28 | def ask(prompt): 29 | try: 30 | return raw_input(prompt) 31 | except: 32 | return input(prompt) 33 | 34 | 35 | def set_trusted(path): 36 | props = dbus.Interface(bus.get_object("org.bluez", path), 37 | "org.freedesktop.DBus.Properties") 38 | props.Set("org.bluez.Device1", "Trusted", True) 39 | 40 | 41 | def dev_connect(path): 42 | dev = dbus.Interface(bus.get_object("org.bluez", path), 43 | "org.bluez.Device1") 44 | dev.Connect() 45 | 46 | 47 | class Rejected(dbus.DBusException): 48 | _dbus_error_name = "org.bluez.Error.Rejected" 49 | 50 | 51 | class Agent(dbus.service.Object): 52 | exit_on_release = True 53 | 54 | def set_exit_on_release(self, exit_on_release): 55 | self.exit_on_release = exit_on_release 56 | 57 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="") 58 | def Release(self): 59 | print("Release") 60 | if self.exit_on_release: 61 | mainloop.quit() 62 | 63 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="") 64 | def AuthorizeService(self, device, uuid): 65 | print("AuthorizeService ({}, {})".format(device, uuid)) 66 | authorize = ask("Authorize connection (yes/no): ") 67 | if (authorize == "yes"): 68 | return 69 | raise Rejected("Connection rejected by user") 70 | 71 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="s") 72 | def RequestPinCode(self, device): 73 | print("RequestPinCode ({})".format(device)) 74 | set_trusted(device) 75 | return ask("Enter PIN Code: ") 76 | 77 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="u") 78 | def RequestPasskey(self, device): 79 | print("RequestPasskey ({})".format(device)) 80 | set_trusted(device) 81 | passkey = ask("Enter passkey: ") 82 | return dbus.UInt32(passkey) 83 | 84 | @dbus.service.method(AGENT_INTERFACE, in_signature="ouq", out_signature="") 85 | def DisplayPasskey(self, device, passkey, entered): 86 | print("DisplayPasskey ({}, {:06u} entered {:u})". 87 | format(device, passkey, entered)) 88 | 89 | @dbus.service.method(AGENT_INTERFACE, in_signature="os", out_signature="") 90 | def DisplayPinCode(self, device, pincode): 91 | print("DisplayPinCode ({}, {})".format(device, pincode)) 92 | 93 | @dbus.service.method(AGENT_INTERFACE, in_signature="ou", out_signature="") 94 | def RequestConfirmation(self, device, passkey): 95 | print("RequestConfirmation ({}, {:06d}".format(device, passkey)) 96 | confirm = ask("Confirm passkey (yes/no): ") 97 | if (confirm == "yes"): 98 | set_trusted(device) 99 | return 100 | raise Rejected("Passkey doesn't match") 101 | 102 | @dbus.service.method(AGENT_INTERFACE, in_signature="o", out_signature="") 103 | def RequestAuthorization(self, device): 104 | print("RequestAuthorization ({})".format(device)) 105 | auth = ask("Authorize? (yes/no): ") 106 | if (auth == "yes"): 107 | return 108 | raise Rejected("Pairing rejected") 109 | 110 | @dbus.service.method(AGENT_INTERFACE, in_signature="", out_signature="") 111 | def Cancel(self): 112 | print("Cancel") 113 | 114 | 115 | def pair_reply(): 116 | print("Device paired") 117 | set_trusted(dev_path) 118 | dev_connect(dev_path) 119 | mainloop.quit() 120 | 121 | 122 | def pair_error(error): 123 | err_name = error.get_dbus_name() 124 | if err_name == "org.freedesktop.DBus.Error.NoReply" and device_obj: 125 | print("Timed out. Cancelling pairing") 126 | device_obj.CancelPairing() 127 | else: 128 | print("Creating device failed: {}".format(error)) 129 | 130 | mainloop.quit() 131 | 132 | 133 | if __name__ == '__main__': 134 | dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 135 | 136 | bus = dbus.SystemBus() 137 | 138 | capability = "KeyboardDisplay" 139 | 140 | path = "/test/agent" 141 | agent = Agent(bus, path) 142 | 143 | mainloop = GObject.MainLoop() 144 | 145 | obj = bus.get_object(BUS_NAME, "/org/bluez") 146 | manager = dbus.Interface(obj, "org.bluez.AgentManager1") 147 | manager.RegisterAgent(path, capability) 148 | 149 | print("[*] Agent registered") 150 | manager.RequestDefaultAgent(path) 151 | mainloop.run() 152 | -------------------------------------------------------------------------------- /dbus/de.syss.btkbdservice.conf: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /doc/paper/Rikki_Dont_Lose_That_Bluetooth_Device.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/bluetooth-keyboard-emulator/da242cf01d527530f8b936323d7d8f457dc2d552/doc/paper/Rikki_Dont_Lose_That_Bluetooth_Device.pdf -------------------------------------------------------------------------------- /doc/paper/Security_of_Modern_Bluetooth_Keyboards.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/bluetooth-keyboard-emulator/da242cf01d527530f8b936323d7d8f457dc2d552/doc/paper/Security_of_Modern_Bluetooth_Keyboards.pdf -------------------------------------------------------------------------------- /images/bluetooth_keyboard_emulator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SySS-Research/bluetooth-keyboard-emulator/da242cf01d527530f8b936323d7d8f457dc2d552/images/bluetooth_keyboard_emulator.png -------------------------------------------------------------------------------- /keyboard.conf.example: -------------------------------------------------------------------------------- 1 | [default] 2 | BluetoothAddress = 00:01:02:03:04:05 3 | DeviceName = SySS_Keyboard 4 | DeviceShortName = SySSKbd 5 | 6 | # device interface 7 | Interface = hci0 8 | 9 | # different Bluetooth chipsets require different spoofing methods for changing 10 | # the MAC address, e.g. CSR8510 requires bdaddr, or BCM20702A0 requires btmgmt 11 | # valid spoofing options are: none, bdddr, btmgmt 12 | SpoofingMethod = btmgmt 13 | 14 | [auto_connect] 15 | AutoConnect = false 16 | Target= 00:00:00:00:00:00 17 | -------------------------------------------------------------------------------- /keyboard/keyboard_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Bluetooth Keyboard Emulator D-Bus Client 6 | 7 | by Matthias Deeg , SySS GmbH 8 | 9 | based on BlueZ 5 Bluetooth Keyboard Emulator for Raspberry Pi 10 | (YAPTB Bluetooth keyboard emulator) by Thanh Le 11 | Source code and information of this project can be found via 12 | https://github.com/0xmemphre/BL_keyboard_RPI, 13 | http://www.mlabviet.com/2017/09/make-raspberry-pi3-as-emulator.html 14 | 15 | MIT License 16 | 17 | Copyright (c) 2018 SySS GmbH 18 | Copyright (c) 2017 quangthanh010290 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in 28 | all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | """ 38 | 39 | __version__ = '0.8' 40 | __author__ = 'Matthias Deeg' 41 | 42 | 43 | import dbus 44 | import dbus.service 45 | 46 | from pynput import keyboard 47 | 48 | # operating modes 49 | NON_INTERACTIVE_MODE = 0 50 | INTERACTIVE_MODE = 1 51 | 52 | # USB HID keyboard modifier 53 | MODIFIER_NONE = 0 54 | MODIFIER_CONTROL_LEFT = 1 << 0 55 | MODIFIER_SHIFT_LEFT = 1 << 1 56 | MODIFIER_ALT_LEFT = 1 << 2 57 | MODIFIER_GUI_LEFT = 1 << 3 58 | MODIFIER_CONTROL_RIGHT = 1 << 4 59 | MODIFIER_SHIFT_RIGHT = 1 << 5 60 | MODIFIER_ALT_RIGHT = 1 << 6 61 | MODIFIER_GUI_RIGHT = 1 << 7 62 | 63 | # USB HID key codes 64 | KEY_NONE = 0x00 65 | KEY_A = 0x04 66 | KEY_B = 0x05 67 | KEY_C = 0x06 68 | KEY_D = 0x07 69 | KEY_E = 0x08 70 | KEY_F = 0x09 71 | KEY_G = 0x0A 72 | KEY_H = 0x0B 73 | KEY_I = 0x0C 74 | KEY_J = 0x0D 75 | KEY_K = 0x0E 76 | KEY_L = 0x0F 77 | KEY_M = 0x10 78 | KEY_N = 0x11 79 | KEY_O = 0x12 80 | KEY_P = 0x13 81 | KEY_Q = 0x14 82 | KEY_R = 0x15 83 | KEY_S = 0x16 84 | KEY_T = 0x17 85 | KEY_U = 0x18 86 | KEY_V = 0x19 87 | KEY_W = 0x1A 88 | KEY_X = 0x1B 89 | KEY_Y = 0x1C 90 | KEY_Z = 0x1D 91 | KEY_1 = 0x1E 92 | KEY_2 = 0x1F 93 | KEY_3 = 0x20 94 | KEY_4 = 0x21 95 | KEY_5 = 0x22 96 | KEY_6 = 0x23 97 | KEY_7 = 0x24 98 | KEY_8 = 0x25 99 | KEY_9 = 0x26 100 | KEY_0 = 0x27 101 | KEY_RETURN = 0x28 102 | KEY_ESCAPE = 0x29 103 | KEY_BACKSPACE = 0x2A 104 | KEY_TAB = 0x2B 105 | KEY_SPACE = 0x2C 106 | KEY_MINUS = 0x2D 107 | KEY_EQUAL = 0x2E 108 | KEY_BRACKET_LEFT = 0x2F 109 | KEY_BRACKET_RIGHT = 0x30 110 | KEY_BACKSLASH = 0x31 111 | KEY_EUROPE_1 = 0x32 112 | KEY_SEMICOLON = 0x33 113 | KEY_APOSTROPHE = 0x34 114 | KEY_GRAVE = 0x35 115 | KEY_COMMA = 0x36 116 | KEY_PERIOD = 0x37 117 | KEY_SLASH = 0x38 118 | KEY_CAPS_LOCK = 0x39 119 | KEY_F1 = 0x3A 120 | KEY_F2 = 0x3B 121 | KEY_F3 = 0x3C 122 | KEY_F4 = 0x3D 123 | KEY_F5 = 0x3E 124 | KEY_F6 = 0x3F 125 | KEY_F7 = 0x40 126 | KEY_F8 = 0x41 127 | KEY_F9 = 0x42 128 | KEY_F10 = 0x43 129 | KEY_F11 = 0x44 130 | KEY_F12 = 0x45 131 | KEY_PRINT_SCREEN = 0x46 132 | KEY_SCROLL_LOCK = 0x47 133 | KEY_PAUSE = 0x48 134 | KEY_INSERT = 0x49 135 | KEY_HOME = 0x4A 136 | KEY_PAGE_UP = 0x4B 137 | KEY_DELETE = 0x4C 138 | KEY_END = 0x4D 139 | KEY_PAGE_DOWN = 0x4E 140 | KEY_ARROW_RIGHT = 0x4F 141 | KEY_ARROW_LEFT = 0x50 142 | KEY_ARROW_DOWN = 0x51 143 | KEY_ARROW_UP = 0x52 144 | KEY_NUM_LOCK = 0x53 145 | KEY_KEYPAD_DIVIDE = 0x54 146 | KEY_KEYPAD_MULTIPLY = 0x55 147 | KEY_KEYPAD_SUBTRACT = 0x56 148 | KEY_KEYPAD_ADD = 0x57 149 | KEY_KEYPAD_ENTER = 0x58 150 | KEY_KEYPAD_1 = 0x59 151 | KEY_KEYPAD_2 = 0x5A 152 | KEY_KEYPAD_3 = 0x5B 153 | KEY_KEYPAD_4 = 0x5C 154 | KEY_KEYPAD_5 = 0x5D 155 | KEY_KEYPAD_6 = 0x5E 156 | KEY_KEYPAD_7 = 0x5F 157 | KEY_KEYPAD_8 = 0x60 158 | KEY_KEYPAD_9 = 0x61 159 | KEY_KEYPAD_0 = 0x62 160 | KEY_KEYPAD_DECIMAL = 0x63 161 | KEY_EUROPE_2 = 0x64 162 | KEY_APPLICATION = 0x65 163 | KEY_POWER = 0x66 164 | KEY_KEYPAD_EQUAL = 0x67 165 | KEY_F13 = 0x68 166 | KEY_F14 = 0x69 167 | KEY_F15 = 0x6A 168 | KEY_CONTROL_LEFT = 0xE0 169 | KEY_SHIFT_LEFT = 0xE1 170 | KEY_ALT_LEFT = 0xE2 171 | KEY_GUI_LEFT = 0xE3 172 | KEY_CONTROL_RIGHT = 0xE4 173 | KEY_SHIFT_RIGHT = 0xE5 174 | KEY_ALT_RIGHT = 0xE6 175 | KEY_GUI_RIGHT = 0xE7 176 | 177 | # mapping for special characters (pynput enum values) 178 | KEYMAP_SPECIAL_CHARS = { 179 | keyboard.Key.alt : (MODIFIER_ALT_LEFT, KEY_NONE), 180 | keyboard.Key.alt_l : (MODIFIER_ALT_LEFT, KEY_NONE), 181 | keyboard.Key.alt_r : (MODIFIER_ALT_RIGHT, KEY_NONE), 182 | keyboard.Key.alt_gr : (MODIFIER_ALT_RIGHT, KEY_NONE), 183 | keyboard.Key.backspace : (MODIFIER_NONE, KEY_BACKSPACE), 184 | keyboard.Key.caps_lock : (MODIFIER_NONE, KEY_CAPS_LOCK), 185 | keyboard.Key.cmd : (MODIFIER_GUI_LEFT, KEY_NONE), 186 | keyboard.Key.cmd_l : (MODIFIER_GUI_LEFT, KEY_NONE), 187 | keyboard.Key.cmd_r : (MODIFIER_GUI_RIGHT, KEY_NONE), 188 | keyboard.Key.ctrl : (MODIFIER_CONTROL_LEFT, KEY_NONE), 189 | keyboard.Key.ctrl_l : (MODIFIER_CONTROL_LEFT, KEY_NONE), 190 | keyboard.Key.ctrl_r : (MODIFIER_CONTROL_RIGHT, KEY_NONE), 191 | keyboard.Key.delete : (MODIFIER_NONE, KEY_DELETE), 192 | keyboard.Key.down : (MODIFIER_NONE, KEY_ARROW_DOWN), 193 | keyboard.Key.end : (MODIFIER_NONE, KEY_END), 194 | keyboard.Key.enter : (MODIFIER_NONE, KEY_RETURN), 195 | keyboard.Key.esc : (MODIFIER_NONE, KEY_ESCAPE), 196 | keyboard.Key.f1 : (MODIFIER_NONE, KEY_F1), 197 | keyboard.Key.f2 : (MODIFIER_NONE, KEY_F2), 198 | keyboard.Key.f3 : (MODIFIER_NONE, KEY_F3), 199 | keyboard.Key.f4 : (MODIFIER_NONE, KEY_F4), 200 | keyboard.Key.f5 : (MODIFIER_NONE, KEY_F5), 201 | keyboard.Key.f6 : (MODIFIER_NONE, KEY_F6), 202 | keyboard.Key.f7 : (MODIFIER_NONE, KEY_F7), 203 | keyboard.Key.f8 : (MODIFIER_NONE, KEY_F8), 204 | keyboard.Key.f9 : (MODIFIER_NONE, KEY_F9), 205 | keyboard.Key.f10 : (MODIFIER_NONE, KEY_F10), 206 | keyboard.Key.f11 : (MODIFIER_NONE, KEY_F11), 207 | keyboard.Key.f12 : (MODIFIER_NONE, KEY_F12), 208 | keyboard.Key.f13 : (MODIFIER_NONE, KEY_F13), 209 | keyboard.Key.f14 : (MODIFIER_NONE, KEY_F14), 210 | keyboard.Key.f15 : (MODIFIER_NONE, KEY_F15), 211 | keyboard.Key.home : (MODIFIER_NONE, KEY_HOME), 212 | keyboard.Key.left : (MODIFIER_NONE, KEY_ARROW_LEFT), 213 | keyboard.Key.page_down : (MODIFIER_NONE, KEY_PAGE_DOWN), 214 | keyboard.Key.page_up : (MODIFIER_NONE, KEY_PAGE_UP), 215 | keyboard.Key.right : (MODIFIER_NONE, KEY_ARROW_RIGHT), 216 | keyboard.Key.shift : (MODIFIER_SHIFT_LEFT, KEY_NONE), 217 | keyboard.Key.shift_l : (MODIFIER_SHIFT_LEFT, KEY_NONE), 218 | keyboard.Key.shift_r : (MODIFIER_SHIFT_RIGHT, KEY_NONE), 219 | keyboard.Key.space : (MODIFIER_NONE, KEY_SPACE), 220 | keyboard.Key.tab : (MODIFIER_NONE, KEY_TAB), 221 | keyboard.Key.up : (MODIFIER_NONE, KEY_ARROW_UP), 222 | keyboard.Key.insert : (MODIFIER_NONE, KEY_INSERT), 223 | keyboard.Key.menu : (MODIFIER_GUI_RIGHT, KEY_NONE), 224 | keyboard.Key.num_lock : (MODIFIER_NONE, KEY_NUM_LOCK), 225 | keyboard.Key.pause : (MODIFIER_NONE, KEY_PAUSE), 226 | keyboard.Key.print_screen : (MODIFIER_NONE, KEY_PRINT_SCREEN), 227 | keyboard.Key.scroll_lock : (MODIFIER_NONE, KEY_SCROLL_LOCK) 228 | } 229 | 230 | # key mapping for printable characters of default German keyboard layout 231 | KEYMAP_GERMAN = { 232 | ' ' : (MODIFIER_NONE, KEY_SPACE), 233 | '!' : (MODIFIER_SHIFT_LEFT, KEY_1), 234 | '"' : (MODIFIER_SHIFT_LEFT, KEY_2), 235 | '#' : (MODIFIER_NONE, KEY_EUROPE_1), 236 | '$' : (MODIFIER_SHIFT_LEFT, KEY_4), 237 | '%' : (MODIFIER_SHIFT_LEFT, KEY_5), 238 | '&' : (MODIFIER_SHIFT_LEFT, KEY_6), 239 | '(' : (MODIFIER_SHIFT_LEFT, KEY_8), 240 | ')' : (MODIFIER_SHIFT_LEFT, KEY_9), 241 | '*' : (MODIFIER_NONE, KEY_KEYPAD_MULTIPLY), 242 | '+' : (MODIFIER_NONE, KEY_KEYPAD_ADD), 243 | ',' : (MODIFIER_NONE, KEY_COMMA), 244 | '-' : (MODIFIER_NONE, KEY_KEYPAD_SUBTRACT), 245 | '.' : (MODIFIER_NONE, KEY_PERIOD), 246 | '/' : (MODIFIER_SHIFT_LEFT, KEY_7), 247 | '0' : (MODIFIER_NONE, KEY_0), 248 | '1' : (MODIFIER_NONE, KEY_1), 249 | '2' : (MODIFIER_NONE, KEY_2), 250 | '3' : (MODIFIER_NONE, KEY_3), 251 | '4' : (MODIFIER_NONE, KEY_4), 252 | '5' : (MODIFIER_NONE, KEY_5), 253 | '6' : (MODIFIER_NONE, KEY_6), 254 | '7' : (MODIFIER_NONE, KEY_7), 255 | '8' : (MODIFIER_NONE, KEY_8), 256 | '9' : (MODIFIER_NONE, KEY_9), 257 | ':' : (MODIFIER_SHIFT_LEFT, KEY_PERIOD), 258 | ';' : (MODIFIER_SHIFT_LEFT, KEY_COMMA), 259 | '<' : (MODIFIER_NONE, KEY_EUROPE_2), 260 | '=' : (MODIFIER_SHIFT_LEFT, KEY_0), 261 | '>' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_2), 262 | '?' : (MODIFIER_SHIFT_LEFT, KEY_MINUS), 263 | '@' : (MODIFIER_ALT_RIGHT, KEY_Q), 264 | 'A' : (MODIFIER_SHIFT_LEFT, KEY_A), 265 | 'B' : (MODIFIER_SHIFT_LEFT, KEY_B), 266 | 'C' : (MODIFIER_SHIFT_LEFT, KEY_C), 267 | 'D' : (MODIFIER_SHIFT_LEFT, KEY_D), 268 | 'E' : (MODIFIER_SHIFT_LEFT, KEY_E), 269 | 'F' : (MODIFIER_SHIFT_LEFT, KEY_F), 270 | 'G' : (MODIFIER_SHIFT_LEFT, KEY_G), 271 | 'H' : (MODIFIER_SHIFT_LEFT, KEY_H), 272 | 'I' : (MODIFIER_SHIFT_LEFT, KEY_I), 273 | 'J' : (MODIFIER_SHIFT_LEFT, KEY_J), 274 | 'K' : (MODIFIER_SHIFT_LEFT, KEY_K), 275 | 'L' : (MODIFIER_SHIFT_LEFT, KEY_L), 276 | 'M' : (MODIFIER_SHIFT_LEFT, KEY_M), 277 | 'N' : (MODIFIER_SHIFT_LEFT, KEY_N), 278 | 'O' : (MODIFIER_SHIFT_LEFT, KEY_O), 279 | 'P' : (MODIFIER_SHIFT_LEFT, KEY_P), 280 | 'Q' : (MODIFIER_SHIFT_LEFT, KEY_Q), 281 | 'R' : (MODIFIER_SHIFT_LEFT, KEY_R), 282 | 'S' : (MODIFIER_SHIFT_LEFT, KEY_S), 283 | 'T' : (MODIFIER_SHIFT_LEFT, KEY_T), 284 | 'U' : (MODIFIER_SHIFT_LEFT, KEY_U), 285 | 'V' : (MODIFIER_SHIFT_LEFT, KEY_V), 286 | 'W' : (MODIFIER_SHIFT_LEFT, KEY_W), 287 | 'X' : (MODIFIER_SHIFT_LEFT, KEY_X), 288 | 'Y' : (MODIFIER_SHIFT_LEFT, KEY_Z), 289 | 'Z' : (MODIFIER_SHIFT_LEFT, KEY_Y), 290 | '[' : (MODIFIER_ALT_RIGHT, KEY_8), 291 | '\\' : (MODIFIER_ALT_RIGHT, KEY_MINUS), 292 | ']' : (MODIFIER_ALT_RIGHT, KEY_9), 293 | '^' : (MODIFIER_NONE, KEY_GRAVE), 294 | '_' : (MODIFIER_SHIFT_LEFT, KEY_SLASH), 295 | '`' : (MODIFIER_SHIFT_LEFT, KEY_EQUAL), 296 | 'a' : (MODIFIER_NONE, KEY_A), 297 | 'b' : (MODIFIER_NONE, KEY_B), 298 | 'c' : (MODIFIER_NONE, KEY_C), 299 | 'd' : (MODIFIER_NONE, KEY_D), 300 | 'e' : (MODIFIER_NONE, KEY_E), 301 | 'f' : (MODIFIER_NONE, KEY_F), 302 | 'g' : (MODIFIER_NONE, KEY_G), 303 | 'h' : (MODIFIER_NONE, KEY_H), 304 | 'i' : (MODIFIER_NONE, KEY_I), 305 | 'j' : (MODIFIER_NONE, KEY_J), 306 | 'k' : (MODIFIER_NONE, KEY_K), 307 | 'l' : (MODIFIER_NONE, KEY_L), 308 | 'm' : (MODIFIER_NONE, KEY_M), 309 | 'n' : (MODIFIER_NONE, KEY_N), 310 | 'o' : (MODIFIER_NONE, KEY_O), 311 | 'p' : (MODIFIER_NONE, KEY_P), 312 | 'q' : (MODIFIER_NONE, KEY_Q), 313 | 'r' : (MODIFIER_NONE, KEY_R), 314 | 's' : (MODIFIER_NONE, KEY_S), 315 | 't' : (MODIFIER_NONE, KEY_T), 316 | 'u' : (MODIFIER_NONE, KEY_U), 317 | 'v' : (MODIFIER_NONE, KEY_V), 318 | 'w' : (MODIFIER_NONE, KEY_W), 319 | 'x' : (MODIFIER_NONE, KEY_X), 320 | 'y' : (MODIFIER_NONE, KEY_Z), 321 | 'z' : (MODIFIER_NONE, KEY_Y), 322 | '{' : (MODIFIER_ALT_RIGHT, KEY_7), 323 | '|' : (MODIFIER_ALT_RIGHT, KEY_EUROPE_2), 324 | '}' : (MODIFIER_ALT_RIGHT, KEY_0), 325 | '~' : (MODIFIER_ALT_RIGHT, KEY_BRACKET_RIGHT), 326 | u'\'' : (MODIFIER_SHIFT_LEFT, KEY_EUROPE_1), 327 | u'Ä' : (MODIFIER_SHIFT_LEFT, KEY_APOSTROPHE), 328 | u'Ö' : (MODIFIER_SHIFT_LEFT, KEY_SEMICOLON), 329 | u'Ü' : (MODIFIER_SHIFT_LEFT, KEY_BRACKET_LEFT), 330 | u'ä' : (MODIFIER_NONE, KEY_APOSTROPHE), 331 | u'ö' : (MODIFIER_NONE, KEY_SEMICOLON), 332 | u'ü' : (MODIFIER_NONE, KEY_BRACKET_LEFT), 333 | u'ß' : (MODIFIER_NONE, KEY_MINUS), 334 | u'€' : (MODIFIER_ALT_RIGHT, KEY_E) 335 | } 336 | 337 | 338 | class Keyboard(): 339 | """Emulated keyboard""" 340 | 341 | def __init__(self): 342 | # state structure of the emulated Bluetooth keyboard 343 | self.state = [ 344 | 0xA1, # input report 345 | 0x01, # usage report : keyboard 346 | 0x00, # modifier byte 347 | 0x00, # Vendor reserved 348 | 0x00, # 6 bytes for key codes 349 | 0x00, 350 | 0x00, 351 | 0x00, 352 | 0x00, 353 | 0x00] 354 | 355 | self.pressed_key_count = 0 # initialize keypress counter 356 | self.keymap = KEYMAP_GERMAN # set keymap 357 | self.mode = INTERACTIVE_MODE # interactive mode is default 358 | 359 | # initialize D-Bus client 360 | print("[*] Initialize D-Bus keyboard client") 361 | self.bus = dbus.SystemBus() 362 | self.btkservice = self.bus.get_object("de.syss.btkbdservice", 363 | "/de/syss/btkbdservice") 364 | self.iface = dbus.Interface(self.btkservice, "de.syss.btkbdservice") 365 | 366 | def change_state(self, keydata, keypress=True): 367 | """Change keyboard state""" 368 | 369 | print("key count {}".format(self.pressed_key_count)) 370 | 371 | if keypress: 372 | if keydata[0] != MODIFIER_NONE and keydata[1] != KEY_NONE: 373 | if keydata[1] not in self.state[4:]: 374 | # increase key count 375 | self.pressed_key_count += 1 376 | 377 | # find free key slot 378 | i = self.state[4:].index(0) 379 | 380 | # set key 381 | self.state[4 + i] = keydata[1] 382 | print("Key press {}".format(keydata[1])) 383 | 384 | self.state[2] = keydata[0] 385 | 386 | 387 | elif keydata[1] != KEY_NONE: 388 | if keydata[1] not in self.state[4:]: 389 | # increase key count 390 | self.pressed_key_count += 1 391 | 392 | # find free key slot 393 | i = self.state[4:].index(0) 394 | 395 | # set key 396 | self.state[4 + i] = keydata[1] 397 | print("Key press {}".format(keydata[1])) 398 | 399 | elif keydata[0] != MODIFIER_NONE and keydata[1] == KEY_NONE: 400 | # process modifier keys 401 | print("{} pressed".format(keydata[0])) 402 | 403 | self.state[2] |= keydata[0] 404 | print("Modify modifier byte {}".format(self.state[2])) 405 | 406 | else: 407 | if keydata[1] != KEY_NONE: 408 | # decrease keypress count 409 | self.pressed_key_count -= 1 410 | print("Key release {}".format(keydata[1])) 411 | 412 | # update state 413 | i = self.state[4:].index(keydata[1]) 414 | self.state[4 + i] = 0 415 | 416 | if keydata[0] != MODIFIER_NONE: 417 | print("{} released".format(keydata[0])) 418 | self.state[2] &= ~keydata[0] 419 | 420 | print(self) 421 | self.send_input() 422 | 423 | def send_input(self): 424 | """Forward keyboard events to the D-Bus service""" 425 | 426 | modifier_byte = self.state[2] 427 | self.iface.send_keys(modifier_byte, self.state[4:10]) 428 | 429 | def on_press(self, key): 430 | """Change keyboard state on key presses""" 431 | if self.pressed_key_count < 6: 432 | # set state of newly pressed key but consider limit of max. 6 keys 433 | # pressed at the same time 434 | try: 435 | # process printable characters 436 | k = self.keymap[key.char] 437 | 438 | # change keyboard state 439 | self.change_state(k) 440 | except AttributeError: 441 | # process special keys 442 | k = KEYMAP_SPECIAL_CHARS[key] 443 | 444 | # change keyboard state 445 | self.change_state(k) 446 | except KeyError: 447 | print("KeyError: {}".format(key)) 448 | 449 | def on_release(self, key): 450 | """Change keyboard state on key releases""" 451 | 452 | try: 453 | # process printable characters 454 | k = self.keymap[key.char] 455 | 456 | # change keyboard state 457 | self.change_state(k, False) 458 | except AttributeError: 459 | # process special keys 460 | k = KEYMAP_SPECIAL_CHARS[key] 461 | 462 | # change keyboard state 463 | self.change_state(k, False) 464 | except KeyError: 465 | print(key) 466 | 467 | # if key == keyboard.Key.esc: 468 | # # Stop listener 469 | # return False 470 | 471 | def event_loop(self): 472 | """Collect events until released""" 473 | 474 | with keyboard.Listener( 475 | on_press=self.on_press, 476 | on_release=self.on_release) as listener: 477 | listener.join() 478 | 479 | def __str__(self): 480 | """Keyboard state as string""" 481 | 482 | return repr(self.state) 483 | 484 | # main 485 | if __name__ == "__main__": 486 | print("[*] Intialize keyboard") 487 | kbd = Keyboard() 488 | 489 | print("[*] Start event loop ...") 490 | kbd.event_loop() 491 | -------------------------------------------------------------------------------- /server/keyboard_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Bluetooth Keyboard Emulator D-Bus Server 6 | 7 | by Matthias Deeg , SySS GmbH 8 | 9 | based on BlueZ 5 Bluetooth Keyboard Emulator for Raspberry Pi 10 | (YAPTB Bluetooth keyboard emulator) by Thanh Le 11 | Source code and information of this project can be found via 12 | https://github.com/0xmemphre/BL_keyboard_RPI, 13 | http://www.mlabviet.com/2017/09/make-raspberry-pi3-as-emulator.html 14 | 15 | MIT License 16 | 17 | Copyright (c) 2018 SySS GmbH 18 | Copyright (c) 2017 quangthanh010290 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in 28 | all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | """ 38 | 39 | __version__ = '1.0' 40 | __author__ = 'Matthias Deeg' 41 | 42 | 43 | import configparser 44 | import dbus 45 | import dbus.service 46 | import dbus.mainloop 47 | import dbus.mainloop.glib 48 | import gi 49 | import os 50 | import subprocess 51 | import sys 52 | import time 53 | 54 | from bluetooth import BluetoothSocket, L2CAP 55 | from dbus.mainloop.glib import DBusGMainLoop 56 | from struct import pack 57 | gi.require_version('Gtk', '3.0') 58 | from gi.repository import Gtk 59 | 60 | # sleep time after Bluetooth command line tools 61 | OS_CMD_SLEEP = 1.5 62 | 63 | 64 | class BTKbdBluezProfile(dbus.service.Object): 65 | """Bluez 5 profile for emulated Bluetooth Keyboard""" 66 | fd = -1 67 | 68 | def __init__(self, bus, path): 69 | dbus.service.Object.__init__(self, bus, path) 70 | 71 | @dbus.service.method("org.bluez.Profile1", in_signature="", 72 | out_signature="") 73 | def Release(self): 74 | print("[*] Release") 75 | dbus.mainloop.quit() 76 | 77 | @dbus.service.method("org.bluez.Profile1", 78 | in_signature="", out_signature="") 79 | def Cancel(self): 80 | print("[*] Cancel") 81 | 82 | @dbus.service.method("org.bluez.Profile1", in_signature="oha{sv}", 83 | out_signature="") 84 | def NewConnection(self, path, fd, properties): 85 | self.fd = fd.take() 86 | print("[*] NewConnection({}, {:d})".format(path, self.fd)) 87 | for key in properties.keys(): 88 | if key == "Version" or key == "Features": 89 | print(" {} = 0x{:04x}".format(key, properties[key])) 90 | else: 91 | print(" {} = {}".format(key, properties[key])) 92 | 93 | @dbus.service.method("org.bluez.Profile1", in_signature="o", 94 | out_signature="") 95 | def RequestDisconnection(self, path): 96 | print("[*] RequestDisconnection({})".format(path)) 97 | 98 | if (self.fd > 0): 99 | os.close(self.fd) 100 | self.fd = -1 101 | 102 | 103 | class BTKbDevice(): 104 | """Bluetooth HID keyboard device""" 105 | 106 | # control and interrupt service ports 107 | P_CTRL = 17 # Service port (control) from SDP record 108 | P_INTR = 19 # Service port (interrupt) from SDP record 109 | 110 | # D-Bus path of the BlueZ profile 111 | PROFILE_DBUS_PATH = "/bluez/syss/btkbd_profile" 112 | 113 | # file path of the SDP record 114 | SDP_RECORD_PATH = "{}{}".format(sys.path[0], "/sdp_record.xml") 115 | 116 | # device UUID 117 | UUID = "00001124-0000-1000-8000-00805f9b34fb" 118 | 119 | def __init__(self): 120 | """Initialize Bluetooth keyboard device""" 121 | 122 | # read config file with address and device name 123 | print("[*] Read configuration file") 124 | config = configparser.ConfigParser() 125 | config.read("../keyboard.conf") 126 | 127 | try: 128 | self.bdaddr = config['default']['BluetoothAddress'] 129 | self.device_name = config['default']['DeviceName'] 130 | self.device_short_name = config['default']['DeviceShortName'] 131 | self.interface = config['default']['Interface'] 132 | self.spoofing_method = config['default']['SpoofingMethod'] 133 | self.auto_connect = config['auto_connect']['AutoConnect'] 134 | self.connect_target = config['auto_connect']['Target'] 135 | except KeyError: 136 | sys.exit("[-] Could not read all required configuration values") 137 | 138 | print("[*] Initialize Bluetooth device") 139 | self.configure_device() 140 | self.register_bluez_profile() 141 | 142 | def configure_device(self): 143 | """Configure bluetooth hardware device""" 144 | 145 | print("[*] Configuring emulated Bluetooth keyboard") 146 | 147 | # power on Bluetooth device 148 | p = subprocess.run(['btmgmt', '--index', self.interface, 'power', 149 | 'off'], stdout=subprocess.PIPE) 150 | time.sleep(OS_CMD_SLEEP) 151 | 152 | # spoof device address if configured 153 | if self.spoofing_method == 'bdaddr': 154 | print("[+] Spoof device {} address {} via btmgmt". 155 | format(self.interface, self.bdaddr)) 156 | 157 | # power on Bluetooth device 158 | p = subprocess.run(['btmgmt', '--index', self.interface, 'power', 159 | 'on'], stdout=subprocess.PIPE) 160 | time.sleep(OS_CMD_SLEEP) 161 | 162 | # set Bluetooth address using bdaddr software tool with manual 163 | # reset, so that we have to power on the device 164 | p = subprocess.run(['bdaddr', '-i', self.interface, '-r', 165 | self.bdaddr], stdout=subprocess.PIPE) 166 | time.sleep(OS_CMD_SLEEP) 167 | 168 | # power on Bluetooth device 169 | p = subprocess.run(['btmgmt', '--index', self.interface, 'power', 170 | 'on'], stdout=subprocess.PIPE) 171 | time.sleep(OS_CMD_SLEEP) 172 | 173 | # set device class 174 | print("[+] Set device class") 175 | p = subprocess.run(['btmgmt', '--index', self.interface, 'class', 176 | '5', '64'], stdout=subprocess.PIPE) 177 | time.sleep(OS_CMD_SLEEP) 178 | 179 | # set device name and short name 180 | print("[+] Set device name: {} ({})". 181 | format(self.device_name, self.device_short_name)) 182 | p = subprocess.run(['btmgmt', '--index', self.interface, 'name', 183 | self.device_name, self.device_short_name], 184 | stdout=subprocess.PIPE) 185 | 186 | # set device to connectable 187 | p = subprocess.run(['btmgmt', '--index', self.interface, 188 | 'connectable', 'on'], stdout=subprocess.PIPE) 189 | time.sleep(OS_CMD_SLEEP) 190 | 191 | # power on Bluetooth device 192 | p = subprocess.run(['btmgmt', '--index', self.interface, 'power', 193 | 'on'], stdout=subprocess.PIPE) 194 | time.sleep(OS_CMD_SLEEP) 195 | 196 | elif self.spoofing_method == 'btmgmt': 197 | print("[+] Spoof device {} address {} via btmgmt". 198 | format(self.interface, self.bdaddr)) 199 | 200 | # set Bluetooth address 201 | print("[+] Set Bluetooth address: {}".format(self.bdaddr)) 202 | p = subprocess.run(['btmgmt', '--index', self.interface, 203 | 'public-addr', self.bdaddr], 204 | stdout=subprocess.PIPE) 205 | 206 | print(p.stdout) 207 | if "fail" in str(p.stdout, "utf-8"): 208 | print("[-] Error setting Bluetooth address") 209 | sys.exit(1) 210 | 211 | # power on Bluetooth device using btmgmt software tool 212 | p = subprocess.run(['btmgmt', '--index', self.interface, 'power', 213 | 'on'], stdout=subprocess.PIPE) 214 | # print(p.stdout) 215 | time.sleep(OS_CMD_SLEEP) 216 | 217 | # set device class 218 | print("[+] Set device class") 219 | p = subprocess.run(['btmgmt', '--index', self.interface, 'class', 220 | '5', '64'], stdout=subprocess.PIPE) 221 | # print(p.stdout) 222 | time.sleep(OS_CMD_SLEEP) 223 | 224 | # set device name and short name 225 | print("[+] Set device name: {} ({})". 226 | format(self.device_name, self.device_short_name)) 227 | 228 | p = subprocess.run(['btmgmt', '--index', self.interface, 'name', 229 | self.device_name, self.device_short_name], 230 | stdout=subprocess.PIPE) 231 | # print(p.stdout) 232 | time.sleep(OS_CMD_SLEEP) 233 | 234 | # set device to connectable 235 | p = subprocess.run(['btmgmt', '--index', self.interface, 236 | 'connectable', 'on'], stdout=subprocess.PIPE) 237 | # print(p.stdout) 238 | 239 | time.sleep(OS_CMD_SLEEP) 240 | 241 | # turn on discoverable mode 242 | print("[+] Turn on discoverable mode") 243 | p = subprocess.run(['bluetoothctl', 'discoverable', 'on'], 244 | stdout=subprocess.PIPE) 245 | # print(p.stdout) 246 | 247 | def register_bluez_profile(self): 248 | """Setup and register BlueZ profile""" 249 | 250 | print("Configuring Bluez Profile") 251 | 252 | # setup profile options 253 | service_record = self.read_sdp_service_record() 254 | 255 | opts = { 256 | "ServiceRecord": service_record, 257 | "Role": "server", 258 | "RequireAuthentication": False, 259 | "RequireAuthorization": False 260 | } 261 | 262 | # retrieve a proxy for the bluez profile interface 263 | bus = dbus.SystemBus() 264 | manager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez"), 265 | "org.bluez.ProfileManager1") 266 | 267 | profile = BTKbdBluezProfile(bus, BTKbDevice.PROFILE_DBUS_PATH) 268 | 269 | manager.RegisterProfile(BTKbDevice.PROFILE_DBUS_PATH, BTKbDevice.UUID, 270 | opts) 271 | 272 | print("[*] Profile registered") 273 | 274 | def read_sdp_service_record(self): 275 | """Read SDP service record""" 276 | 277 | print("[*] Reading service record") 278 | try: 279 | fh = open(BTKbDevice.SDP_RECORD_PATH, "r") 280 | except Exception: 281 | sys.exit("[*] Could not open the SDP record. Exiting ...") 282 | 283 | return fh.read() 284 | 285 | def listen(self): 286 | """Listen for incoming client connections""" 287 | 288 | print("[*] Waiting for connections") 289 | self.scontrol = BluetoothSocket(L2CAP) 290 | self.sinterrupt = BluetoothSocket(L2CAP) 291 | 292 | # bind these sockets to a port - port zero to select next available 293 | self.scontrol.bind((self.bdaddr, self.P_CTRL)) 294 | self.sinterrupt.bind((self.bdaddr, self.P_INTR)) 295 | 296 | # start listening on the server sockets (only allow 1 connection) 297 | self.scontrol.listen(1) 298 | self.sinterrupt.listen(1) 299 | 300 | self.ccontrol, cinfo = self.scontrol.accept() 301 | print("[*] Connection on the control channel from {}" 302 | .format(cinfo[0])) 303 | 304 | self.cinterrupt, cinfo = self.sinterrupt.accept() 305 | print("[*] Connection on the interrupt channel from {}" 306 | .format(cinfo[0])) 307 | 308 | def connect(self, target): 309 | """Connect to target MAC (the keyboard must already be known to the 310 | target)""" 311 | 312 | print("[*] Connecting to {}".format(target)) 313 | self.scontrol = BluetoothSocket(L2CAP) 314 | self.sinterrupt = BluetoothSocket(L2CAP) 315 | 316 | self.scontrol.connect((target, self.P_CTRL)) 317 | self.sinterrupt.connect((target, self.P_INTR)) 318 | 319 | self.ccontrol = self.scontrol 320 | self.cinterrupt = self.sinterrupt 321 | 322 | def send_string(self, message): 323 | """Send a string to the host machine""" 324 | 325 | self.cinterrupt.send(message) 326 | 327 | 328 | class BTKbdService(dbus.service.Object): 329 | """D-Bus service for emulated Bluetooth keyboard""" 330 | 331 | def __init__(self): 332 | print("[*] Inititalize D-Bus Bluetooth keyboard service") 333 | 334 | # set up as a D-Bus service 335 | bus_name = dbus.service.BusName("de.syss.btkbdservice", 336 | bus=dbus.SystemBus()) 337 | dbus.service.Object.__init__(self, bus_name, "/de/syss/btkbdservice") 338 | 339 | # create and setup our device 340 | self.device = BTKbDevice() 341 | 342 | if self.device.auto_connect == "true": 343 | # Switch into paring mode or connect to already known target? 344 | # files = os.listdir('/var/lib/bluetooth/{}'.format(self.device.bdaddr)) 345 | # mac_regex = re.compile(r'([0-9A-F]{2}:){5}([0-9A-F]{2})') 346 | # files = list(filter(mac_regex.match, files)) 347 | # if files: 348 | # connect to configured target 349 | self.device.connect(self.device.connect_target) 350 | else: 351 | # start listening for new connections 352 | self.device.listen() 353 | 354 | @dbus.service.method('de.syss.btkbdservice', in_signature='yay') 355 | def send_keys(self, modifiers, keys): 356 | """Send keys""" 357 | 358 | # create 10 byte data structure 359 | byte_list = [0xA1, 0x01, modifiers, 0x00] 360 | for key_code in keys: 361 | byte_list.append(key_code) 362 | 363 | # add some padding bytes to have a 10 byte packet 364 | if len(byte_list) < 10: 365 | padding = len(byte_list) - 10 366 | for i in range(padding): 367 | byte_list.append(0) 368 | 369 | data = pack("10B", *byte_list) 370 | 371 | self.device.send_string(data) 372 | 373 | 374 | # main routine 375 | if __name__ == "__main__": 376 | # check for required root privileges 377 | if not os.geteuid() == 0: 378 | sys.exit("[-] Please run the keyboard server as root") 379 | 380 | # start D-Bus Bluetooth keyboard emulator service 381 | DBusGMainLoop(set_as_default=True) 382 | btkbdservice = BTKbdService() 383 | Gtk.main() 384 | -------------------------------------------------------------------------------- /server/sdp_record.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo cp dbus/de.syss.btkbdservice.conf /etc/dbus-1/system.d/ 4 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # stop the Bluetooth background process 4 | sudo systemctl stop bluetooth 5 | 6 | # get current Path 7 | export C_PATH=$(pwd) 8 | 9 | # create tmux session 10 | tmux has-session -t kbdemu 11 | if [ $? != 0 ]; then 12 | tmux new-session -s kbdemu -n os -d 13 | tmux split-window -h -t kbdemu 14 | tmux split-window -v -t kbdemu:os.0 15 | tmux split-window -v -t kbdemu:os.1 16 | tmux split-window -v -t kbdemu:os.3 17 | tmux send-keys -t kbdemu:os.0 'cd $C_PATH && sudo /usr/lib/bluetooth/bluetoothd --nodetach --debug -p time ' C-m 18 | tmux send-keys -t kbdemu:os.1 'cd $C_PATH/server && sudo python3 keyboard_server.py ' C-m 19 | tmux send-keys -t kbdemu:os.2 'cd $C_PATH && sudo /usr/bin/bluetoothctl' C-m 20 | tmux send-keys -t kbdemu:os.3 'cd $C_PATH/keyboard/ && sleep 5 && sudo python3 keyboard_client.py' C-m 21 | tmux send-keys -t kbdemu:os.4 'cd $C_PATH/agent/ && sleep 5 && sudo python3 simple-agent.py' C-m 22 | fi 23 | --------------------------------------------------------------------------------