├── README.md ├── l2fuzz.py └── l2cap_fuzzer.py /README.md: -------------------------------------------------------------------------------- 1 | # L2Fuzz 2 | 3 | A stateful fuzzer to detect vulnerabilities in Bluetooth BR/EDR Logical Link Control and Adaptation Protocol (L2CAP) layer. 4 | 5 | 6 | ## Prerequisites 7 | 8 | L2Fuzz uses python3.6.9 and scapy 2.4.4. Also, it uses Bluetooth Dongle. 9 | 10 | ``` 11 | sudo apt-get install python3-pip 12 | pip3 install scapy==2.4.4 13 | sudo apt-get install libbluetooth-dev 14 | sudo pip3 install pybluez 15 | pip3 install python-statemachine 16 | pip3 install ouilookup 17 | ``` 18 | 19 | ## Running the tests 20 | 21 | 1. move to L2Fuzz folder. 22 | 2. run l2fuzz.py . 23 | ``` 24 | sudo python3 l2fuzz.py 25 | ``` 26 | 3. Choose target device. 27 | ``` 28 | Reset Bluetooth... 29 | Performing classic bluetooth inquiry scan... 30 | 31 | Target Bluetooth Device List 32 | [No.] [BT address] [Device name] [Device Class] [OUI] 33 | 00. AA:BB:CC:DD:EE:FF DESKTOP Desktop Vendor A 34 | 01. 11:22:33:44:55:66 Smartphone Smartphone Vendor B 35 | Found 2 devices 36 | 37 | Choose Device : 38 | ``` 39 | 4. Choose target service which is supported by L2CAP. 40 | 41 | ``` 42 | Start scanning services... 43 | 44 | List of profiles for the device 45 | 00. [0x0000]: Service A 46 | 01. [0x0001]: Service B 47 | 02. [0x0002]: Service C 48 | 03. [0x0003]: Service D 49 | 04. [0x0004]: Service E 50 | 05. [0x0005]: Service F 51 | 52 | Select a profile to fuzz : 53 | ``` 54 | 5. Fuzz testing start. 55 | 56 | ### End test 57 | 58 | ``` 59 | Ctrl + C 60 | ``` 61 | 62 | ### Log file 63 | 64 | The log file will be generated after the fuzz testing in L2Fuzz folder. 65 | 66 | ## Paper 67 | 68 | L2Fuzz paper is published in Jun 27, 2022 through "The 52nd Annual IEEE/IFIP International Conference on Dependable Systems and Networks". 69 | 70 | Title : L2Fuzz: Discovering Bluetooth L2CAP Vulnerabilities Using Stateful Fuzz Testing 71 | 72 | Paper : https://arxiv.org/abs/2208.00110 73 | 74 | Video : https://youtu.be/lrc-mJTw1yM 75 | 76 | Authors : Haram Park (Korea University), Carlos Nkuba Kayembe (Korea University), Seunghoon Woo (Korea University), Heejo Lee (Korea University) 77 | 78 | Contacts : freehr94@korea.ac.kr, https://ccs.korea.ac.kr/ 79 | -------------------------------------------------------------------------------- /l2fuzz.py: -------------------------------------------------------------------------------- 1 | import bluetooth 2 | import sys, os, re 3 | import datetime, json 4 | 5 | from scapy.all import * 6 | from OuiLookup import OuiLookup 7 | from collections import OrderedDict 8 | 9 | from l2cap_fuzzer import * 10 | 11 | now = datetime.now() 12 | nowTime = now.strftime('%H%M%S') 13 | test_info = OrderedDict() 14 | test_info["starting_time"] = str(now) 15 | test_info["interface"] = "Bluetooth_L2CAP" 16 | test_info["toolVer"] = "1.0.0" 17 | 18 | def bluetooth_reset(): 19 | """ 20 | Reset linux bluetooth 21 | """ 22 | print('\nReset Bluetooth...') 23 | os.system("sudo rfkill block bluetooth") # Disable device 24 | os.system("sudo rm -r /var/lib/bluetooth/*") # Clear addresses 25 | os.system("sudo rfkill unblock bluetooth") # Enable device 26 | os.system("sudo systemctl restart bluetooth") # Restart bluetooth service 27 | test_info["reset"] = "Y" 28 | time.sleep(3) 29 | 30 | def bluetooth_class_of_device(device_class): 31 | # https://github.com/mikeryan/btclassify.git 32 | 33 | class_string = device_class 34 | 35 | m = re.match('(0x)?([0-9A-Fa-f]{6})', class_string) 36 | if m is None: 37 | #print("Invalid class, skipping (%s)" % class_string) 38 | return { "major": "None", "minor": "None", "service": "None"} 39 | 40 | hex_string = m.group(2) 41 | 42 | # "class" is a reserved word in Python, so CoD is class 43 | CoD = int(hex_string, 16) 44 | 45 | # Major Device Classes 46 | classes = ['Miscellaneous', 'Computer', 'Phone', 'LAN/Network Access Point', 47 | 'Audio/Video', 'Peripheral', 'Imaging', 'Wearable', 'Toy', 48 | 'Health'] 49 | major_number = (CoD >> 8) & 0x1f 50 | if major_number < len(classes): 51 | major = classes[major_number] 52 | elif major_number == 31: 53 | major = 'Uncategorized' 54 | else: 55 | major = 'Reserved' 56 | 57 | # Minor - varies depending on major 58 | minor_number = (CoD >> 2) & 0x3f 59 | minor = None 60 | 61 | # computer 62 | if major_number == 1: 63 | classes = [ 64 | 'Uncategorized', 'Desktop workstation', 'Server-class computer', 65 | 'Laptop', 'Handheld PC/PDA (clamshell)', 'Palm-size PC/PDA', 66 | 'Wearable computer (watch size)', 'Tablet'] 67 | if minor_number < len(classes): 68 | minor = classes[minor_number] 69 | else: 70 | minor = 'reserved' 71 | 72 | # phone 73 | elif major_number == 2: 74 | classes = [ 75 | 'Uncategorized', 'Cellular', 'Cordless', 'Smartphone', 76 | 'Wired modem or voice gateway', 'Common ISDN access'] 77 | if minor_number < len(classes): 78 | minor = classes[minor_number] 79 | else: 80 | minor = 'reserved' 81 | 82 | # network access point 83 | elif major_number == 3: 84 | minor_number >> 3 85 | classes = [ 86 | 'Fully available', '1% to 17% Utilized', '17% to 33% Utilized', 87 | '33% to 50% Utilized', '50% to 67% Utilized', 88 | '67% to 83% Utilized', '83% to 99% Utilized', 89 | 'No service available'] 90 | if minor_number < len(classes): 91 | minor = classes[minor_number] 92 | else: 93 | minor = 'reserved' 94 | 95 | # audio/video 96 | elif major_number == 4: 97 | classes = [ 98 | 'Uncategorized', 'Wearable Headset Device', 'Hands-free Device', 99 | '(Reserved)', 'Microphone', 'Loudspeaker', 'Headphones', 100 | 'Portable Audio', 'Car audio', 'Set-top box', 'HiFi Audio Device', 101 | 'VCR', 'Video Camera', 'Camcorder', 'Video Monitor', 102 | 'Video Display and Loudspeaker', 'Video Conferencing', 103 | '(Reserved)', 'Gaming/Toy'] 104 | if minor_number < len(classes): 105 | minor = classes[minor_number] 106 | else: 107 | minor = 'reserved' 108 | 109 | # peripheral, this one's gross 110 | elif major_number == 5: 111 | feel_number = minor_number >> 4 112 | classes = [ 113 | 'Not Keyboard / Not Pointing Device', 'Keyboard', 114 | 'Pointing device', 'Combo keyboard/pointing device'] 115 | feel = classes[feel_number] 116 | 117 | classes = [ 118 | 'Uncategorized', 'Joystick', 'Gamepad', 'Remote control', 119 | 'Sensing device', 'Digitizer tablet', 'Card Reader', 'Digital Pen', 120 | 'Handheld scanner for bar-codes, RFID, etc.', 121 | 'Handheld gestural input device' ] 122 | if minor_number < len(classes): 123 | minor_low = classes[minor_number] 124 | else: 125 | minor_low = 'reserved' 126 | 127 | minor = '%s, %s' % (feel, minor_low) 128 | 129 | # imaging 130 | elif major_number == 6: 131 | minors = [] 132 | if minor_number & (1 << 2): 133 | minors.append('Display') 134 | if minor_number & (1 << 3): 135 | minors.append('Camera') 136 | if minor_number & (1 << 4): 137 | minors.append('Scanner') 138 | if minor_number & (1 << 5): 139 | minors.append('Printer') 140 | if len(minors > 0): 141 | minors = ', '.join(minors) 142 | 143 | # wearable 144 | elif major_number == 7: 145 | classes = ['Wristwatch', 'Pager', 'Jacket', 'Helmet', 'Glasses'] 146 | if minor_number < len(classes): 147 | minor = classes[minor_number] 148 | else: 149 | minor = 'reserved' 150 | 151 | # toy 152 | elif major_number == 8: 153 | classes = ['Robot', 'Vehicle', 'Doll / Action figure', 'Controller', 154 | 'Game'] 155 | if minor_number < len(classes): 156 | minor = classes[minor_number] 157 | else: 158 | minor = 'reserved' 159 | 160 | # health 161 | elif major_number == 9: 162 | classes = [ 163 | 'Undefined', 'Blood Pressure Monitor', 'Thermometer', 164 | 'Weighing Scale', 'Glucose Meter', 'Pulse Oximeter', 165 | 'Heart/Pulse Rate Monitor', 'Health Data Display', 'Step Counter', 166 | 'Body Composition Analyzer', 'Peak Flow Monitor', 167 | 'Medication Monitor', 'Knee Prosthesis', 'Ankle Prosthesis', 168 | 'Generic Health Manager', 'Personal Mobility Device'] 169 | if minor_number < len(classes): 170 | minor = classes[minor_number] 171 | else: 172 | minor = 'reserved' 173 | 174 | # Major Service Class (can by multiple) 175 | services = [] 176 | if CoD & (1 << 23): 177 | services.append('Information') 178 | if CoD & (1 << 22): 179 | services.append('Telephony') 180 | if CoD & (1 << 21): 181 | services.append('Audio') 182 | if CoD & (1 << 20): 183 | services.append('Object Transfer') 184 | if CoD & (1 << 19): 185 | services.append('Capturing') 186 | if CoD & (1 << 18): 187 | services.append('Rendering') 188 | if CoD & (1 << 17): 189 | services.append('Networking') 190 | if CoD & (1 << 16): 191 | services.append('Positioning') 192 | if CoD & (1 << 15): 193 | services.append('(reserved)') 194 | if CoD & (1 << 14): 195 | services.append('(reserved)') 196 | if CoD & (1 << 13): 197 | services.append('Limited Discoverable Mode') 198 | 199 | output = {"major" : major, "minor" : minor, "service" : services} 200 | 201 | return output 202 | 203 | 204 | def bluetooth_classic_scan(): 205 | """ 206 | This scan finds ONLY Bluetooth Classic (non-BLE) devices 207 | """ 208 | print('Performing classic bluetooth inquiry scan...') 209 | 210 | while(True): 211 | # Scan for nearby devices in regular bluetooth mode 212 | nearby_devices = bluetooth.discover_devices(duration=3, flush_cache=True, lookup_names=True, lookup_class=True) 213 | i = 0 214 | print("\n\tTarget Bluetooth Device List") 215 | print("\t[No.]\t[BT address]\t\t[Device name]\t\t[Device Class]\t\t[OUI]") 216 | for addr, name, device_class in nearby_devices: 217 | device_class = bluetooth_class_of_device(hex(device_class)) 218 | oui = OuiLookup().query(addr) 219 | print("\t%02d.\t%s\t%s\t\t%s(%s)\t%s" % (i, addr, name, device_class['major'], device_class['minor'], list(oui[0].values())[0])) 220 | i += 1 221 | if len(nearby_devices) == 0: 222 | print("[-] No bluetooth device found. Did you connect an adapter?\n") 223 | sys.exit() 224 | elif len(nearby_devices) != 0: 225 | print("\tFound %d devices" % len(nearby_devices)) 226 | break 227 | else : 228 | sys.exit() 229 | 230 | while(True): 231 | user_input = int(input("\nChoose Device : ")) 232 | if user_input < len(nearby_devices) or user_input > -1: 233 | idx = user_input 234 | break 235 | else: 236 | print("[-] Out of range.") 237 | 238 | addr_chosen = nearby_devices[idx][0] 239 | test_info['bdaddr'] = str(nearby_devices[idx][0]) 240 | oui = OuiLookup().query(addr_chosen) 241 | test_info['OUI'] = list(oui[0].values())[0] 242 | test_info['name'] = str(nearby_devices[idx][1]) 243 | test_info['Class of Device Value'] = str(nearby_devices[idx][2]) 244 | test_info['Class of Device'] = bluetooth_class_of_device(hex(nearby_devices[idx][2])) 245 | 246 | return addr_chosen 247 | 248 | 249 | def bluetooth_services_and_protocols_search(bt_addr): 250 | """ 251 | Search the services and protocols of device 252 | """ 253 | print("\nStart scanning services...") 254 | print("\n\tList of profiles for the device") 255 | 256 | services = bluetooth.find_service(address=bt_addr) 257 | #print(services) 258 | if len(services) <= 0: 259 | print("No services found") 260 | return { "protocol": "None", "name": "None", "port": "None"} 261 | else: 262 | i = 0 263 | for serv in services: 264 | if len(serv['profiles']) == 0: 265 | print("\t%02d. [None]: %s" % (i, serv['name'])) 266 | else: 267 | print("\t%02d. [0x%s]: %s" % (i, serv['profiles'][0][0], serv['name'])) 268 | i += 1 269 | 270 | while(True): 271 | user_input = int(input("\nSelect a profile to fuzz : ")) 272 | if user_input < len(services) and user_input > -1: 273 | idx = user_input 274 | serv_chosen = services[idx] 275 | break 276 | else: 277 | print("[-] Out of range.") 278 | 279 | print("\n\tProtocol for the profile [%s] : %s\n" % (serv_chosen['name'], serv_chosen['protocol'])) 280 | 281 | test_info['service'] = serv_chosen['name'] 282 | test_info['protocol'] = serv_chosen['protocol'] 283 | test_info['port'] = serv_chosen['port'] 284 | 285 | return serv_chosen 286 | 287 | 288 | if __name__== "__main__": 289 | # targetting 290 | #bluetooth_reset() 291 | target_addr = bluetooth_classic_scan() 292 | target_service = bluetooth_services_and_protocols_search(target_addr) 293 | target_protocol = target_service['protocol'] 294 | target_profile = target_service['name'] 295 | target_profile_port = target_service['port'] 296 | 297 | print("\n===================Test Informatoin===================") 298 | print(json.dumps(test_info, ensure_ascii=False, indent="\t")) 299 | print("======================================================\n") 300 | 301 | 302 | # Protocol fuzzing 303 | if(target_protocol == "L2CAP"): 304 | l2cap_fuzzing(target_addr, target_profile, target_profile_port, test_info) 305 | else: 306 | print("Not Supported") 307 | 308 | 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /l2cap_fuzzer.py: -------------------------------------------------------------------------------- 1 | import sys, os, subprocess 2 | import json, datetime 3 | 4 | from statemachine import StateMachine, State 5 | from scapy.all import * 6 | from scapy.packet import Packet 7 | from random import * 8 | from collections import OrderedDict 9 | 10 | 11 | # Global 12 | OUR_LOCAL_SCID = 0x40 13 | pkt_cnt = 0 14 | crash_cnt = 0 15 | conn_rsp_flag = 0 16 | 17 | # L2CAP Command Info 18 | L2CAP_CmdDict = { 19 | 0x01: "Reject", 20 | 0x02: "Connection Request", 21 | 0x03: "Connection Response", 22 | 0x04: "Configuration Request", 23 | 0x05: "Configuration Response", 24 | 0x06: "Disconnection Request", 25 | 0x07: "Disconnection Response", 26 | 0x08: "Echo Request", 27 | 0x09: "Echo Response", 28 | 0x0A: "Information Request", 29 | 0x0B: "Information Response", 30 | 0x0C: "Create Channel Request", 31 | 0x0D: "Create Channel Response", 32 | 0x0E: "Move Channel Request", 33 | 0x0F: "Move Channel Response", 34 | 0x10: "Move Channel Confirmation Request", 35 | 0x11: "Move Channel Confirmation Response", 36 | 0x12: "Connection Parameter Update Request", 37 | 0x13: "Connection Parameter Update Response", 38 | 0x14: "LE Credit Based Connection Request", 39 | 0x15: "LE Credit Based Connection Response", 40 | 0x16: "Flow Control Credit Ind", 41 | 0x17: "Credit Based Connection Request", 42 | 0x18: "Credit Based Connection Response", 43 | 0x19: "Credit Based Reconfigure Request", 44 | 0x1A: "Credit Based Reconfigure Response", 45 | } 46 | 47 | L2CAP_Connect_Result = { 48 | 0: "Connection successful", 49 | 1: "Connection pending", 50 | 2: "Connection refused - PSM not supported", 51 | 3: "Connection refused - seccurity block", 52 | 4: "Connection refused - no resources available", 53 | 6: "Connection refused - invalid Source CID", 54 | 7: "Connection refused - Source CID already allocated", 55 | } 56 | 57 | 58 | class l2cap_state_machine(StateMachine): 59 | """ 60 | L2CAP Protocol Fuzzing with 'Stateful Fuzzing Algorithm' 61 | 62 | A state machine is created for each new L2CAP_ConnectReq received. 63 | The state machine always starts in the CLOSED state 64 | 65 | *The state machine does not necessarily represent all possible scenarios. 66 | """ 67 | 68 | #### States #### 69 | 70 | # Basic States 71 | closed_state = State('Closed State', initial = True) # Start 72 | open_state = State('Open State') # End 73 | wait_config_state = State('Wait Config State') 74 | wait_connect_state = State('Wait Connect State') 75 | wait_connect_rsp_state = State('Wait Connect Rsp State') 76 | wait_disconnect_state = State('Wait Disconnect State') 77 | 78 | # Optional States (Alternative MAC/PHY enabled operation) 79 | wait_create_state = State('Wait Create State') 80 | wait_create_rsp_state = State('Wait Create Rsp State') 81 | wait_move_confirm_state = State('Wait Move Confirm State') 82 | wait_move_state = State('Wait Move State') 83 | wait_move_rsp_state = State('Wait Move Rsp State') 84 | wait_confirm_rsp_state = State('Wait Confirm Rsp State') 85 | 86 | # Configurateion States 87 | wait_send_config_state = State('Wait Send Config State') 88 | wait_config_req_rsp_state = State('Wait Config Req Rsp State') 89 | wait_config_req_state = State('Wait Config Req State') 90 | wait_config_rsp_state = State('Wait Config Rsp State') 91 | wait_control_ind_state = State('Wait Control Ind State') 92 | wait_final_rsp_state = State('Wait Final Rsp State') 93 | wait_ind_final_rsp_state = State('Wait Ind Final Rsp State') 94 | 95 | 96 | #### Transitions #### 97 | 98 | # from open_state 99 | open_to_w_discon = open_state.to(wait_disconnect_state) 100 | open_to_closed = open_state.to(closed_state) 101 | open_to_w_conf = open_state.to(wait_config_state) 102 | open_to_w_move = open_state.to(wait_move_state) 103 | open_to_w_move_rsp = open_state.to(wait_move_rsp_state) 104 | open_to_w_move_confirm = open_state.to(wait_move_confirm_state) 105 | 106 | # from wait_config_state 107 | w_conf_to_closed = wait_config_state.to(closed_state) 108 | w_conf_to_w_discon = wait_config_state.to(wait_disconnect_state) 109 | w_conf_to_w_conf = wait_config_state.to.itself() 110 | w_conf_to_w_send_conf = wait_config_state.to(wait_send_config_state) 111 | w_conf_to_w_conf_req_rsp = wait_config_state.to(wait_config_req_rsp_state) 112 | 113 | # from closed_state 114 | closed_to_w_conn = closed_state.to(wait_connect_state) 115 | closed_to_w_conf= closed_state.to(wait_config_state) 116 | closed_to_w_conn_rsp = closed_state.to(wait_connect_rsp_state) 117 | closed_to_w_create = closed_state.to(wait_create_state) 118 | closed_to_w_create_rsp = closed_state.to(wait_create_rsp_state) 119 | 120 | # from wait_connect_state 121 | w_conn_to_closed = wait_connect_state.to(closed_state) 122 | w_conn_to_w_conf = wait_connect_state.to(wait_config_state) 123 | 124 | # from wait_connect_rsp_state 125 | w_conn_rsp_to_closed = wait_connect_rsp_state.to(closed_state) 126 | w_conn_rsp_to_w_conf = wait_connect_rsp_state.to(wait_config_state) 127 | 128 | # from wait_disconnect_state 129 | w_disconn_to_w_disconn = wait_disconnect_state.to.itself() 130 | w_disconn_to_closed = wait_disconnect_state.to(closed_state) 131 | 132 | # from wait_create_state 133 | w_create_to_closed = wait_create_state.to(closed_state) 134 | w_create_to_w_conf = wait_create_state.to(wait_config_state) 135 | 136 | # from wait_create_rsp_state 137 | w_create_rsp_to_closed = wait_create_rsp_state.to(closed_state) 138 | w_create_rsp_to_w_conf = wait_create_rsp_state.to(wait_config_state) 139 | 140 | # from wait_move_confirm_state 141 | w_move_confirm_to_open = wait_move_confirm_state.to(open_state) 142 | 143 | # from wait_move_state 144 | w_move_to_w_move_confirm = wait_move_state.to(wait_move_confirm_state) 145 | 146 | # from wait_move_rsp_state 147 | w_move_rsp_to_w_confirm_rsp = wait_move_rsp_state.to(wait_confirm_rsp_state) 148 | w_move_rsp_to_w_move = wait_move_rsp_state.to(wait_move_state) 149 | w_move_rsp_to_w_move_confirm = wait_move_rsp_state.to(wait_move_confirm_state) 150 | w_move_rsp_to_w_move_rsp = wait_move_rsp_state.to.itself() 151 | 152 | # from wait_confirm_rsp_state 153 | w_confirm_rsp_to_open = wait_confirm_rsp_state.to(open_state) 154 | 155 | # from wait_send_config_state 156 | w_send_conf_to_w_conf_rsp = wait_send_config_state.to(wait_config_rsp_state) 157 | 158 | # from wait_config_req_rsp_state 159 | w_conf_req_rsp_to_w_conf_req_rsp = wait_config_req_rsp_state.to.itself() 160 | w_conf_req_rsp_to_w_conf_req = wait_config_req_rsp_state.to(wait_config_req_state) 161 | w_conf_req_rsp_to_w_conf_rsp = wait_config_req_rsp_state.to(wait_config_rsp_state) 162 | 163 | # from wait_config_req_state 164 | w_conf_req_to_w_conf_req = wait_config_req_state.to.itself() 165 | w_conf_req_to_open = wait_config_req_state.to(open_state) 166 | w_conf_req_to_w_ind_final_rsp = wait_config_req_state.to(wait_ind_final_rsp_state) 167 | 168 | # from wait_final_rsp_state 169 | w_final_rsp_to_open = wait_final_rsp_state.to(open_state) 170 | w_final_rsp_to_w_conf = wait_final_rsp_state.to(wait_config_state) 171 | 172 | # from wait_control_ind_state 173 | w_control_ind_to_w_conf = wait_control_ind_state.to(wait_config_state) 174 | w_control_ind_to_open = wait_control_ind_state.to(open_state) 175 | 176 | # from wait_ind_final_rsp_state 177 | w_ind_final_rsp_to_w_final_rsp = wait_ind_final_rsp_state.to(wait_final_rsp_state) 178 | w_ind_final_rsp_to_w_control_ind = wait_ind_final_rsp_state.to(wait_control_ind_state) 179 | w_ind_final_rsp_to_w_conf = wait_ind_final_rsp_state.to(wait_config_state) 180 | 181 | # from wait_config_rsp_state 182 | w_conf_rsp_to_w_ind_final_rsp = wait_config_rsp_state.to(wait_ind_final_rsp_state) 183 | w_conf_rsp_to_w_conf_rsp = wait_config_rsp_state.to.itself() 184 | w_conf_rsp_to_open = wait_config_rsp_state.to(open_state) 185 | 186 | 187 | 188 | class garbage_value(Packet): 189 | fields_desc = [ 190 | LEShortField("garbage", 0) 191 | ] 192 | 193 | class new_L2CAP_ConnReq(Packet): 194 | name = "L2CAP Conn Req" 195 | fields_desc = [LEShortEnumField("psm", 0, {1: "SDP", 3: "RFCOMM", 5: "TCS-BIN", # noqa 196 | 7: "TCS-BIN-CORDLESS", 15: "BNEP", 17: "HID-Control", # noqa 197 | 19: "HID-Interrupt", 21: "UPnP", 23: "AVCTP-Control", # noqa 198 | 25: "AVDTP", 27: "AVCTP-Browsing", 29: "UDI_C-Plane", # noqa 199 | 31: "ATT", 33: "3DSP", 35: "IPSP", 37: "OTS"}), # noqa 200 | LEShortField("scid", 0), 201 | ] 202 | 203 | 204 | class new_L2CAP_ConnResp(Packet): 205 | name = "L2CAP Conn Resp" 206 | fields_desc = [LEShortField("dcid", 0), 207 | LEShortField("scid", 0), 208 | LEShortEnumField("result", 0, ["success", "pend", "cr_bad_psm", "cr_sec_block", "cr_no_mem", "reserved", "cr_inval_scid", "cr_scid_in_use"]), # noqa: E501 209 | LEShortEnumField("status", 0, ["no_info", "authen_pend", "author_pend", "reserved"]), # noqa: E501 210 | ] 211 | 212 | 213 | class new_L2CAP_ConfReq(Packet): 214 | name = "L2CAP Conf Req" 215 | fields_desc = [ LEShortField("dcid",0), 216 | LEShortField("flags",0), 217 | ByteField("type",0), 218 | ByteField("length",0), 219 | ByteField("identifier",0), 220 | ByteField("servicetype",0), 221 | LEShortField("sdusize",0), 222 | LEIntField("sduarrtime",0), 223 | LEIntField("accesslat",0), 224 | LEIntField("flushtime",0), 225 | ] 226 | 227 | 228 | class new_L2CAP_ConfResp(Packet): 229 | name = "L2CAP Conf Resp" 230 | fields_desc = [ LEShortField("scid",0), 231 | LEShortField("flags",0), 232 | LEShortField("result",0), 233 | ByteField("type0",0), 234 | ByteField("length0",0), 235 | LEShortField("option0",0), 236 | ByteField("type1",0), 237 | ByteField("length1",0), 238 | ] 239 | 240 | 241 | class L2CAP_Create_Channel_Request(Packet): 242 | name = "L2CAP Create Channel Request" 243 | fields_desc = [LEShortEnumField("psm", 0, {1: "SDP", 3: "RFCOMM", 5: "TCS-BIN", # noqa 244 | 7: "TCS-BIN-CORDLESS", 15: "BNEP", 17: "HID-Control", # noqa 245 | 19: "HID-Interrupt", 21: "UPnP", 23: "AVCTP-Control", # noqa 246 | 25: "AVDTP", 27: "AVCTP-Browsing", 29: "UDI_C-Plane", # noqa 247 | 31: "ATT", 33: "3DSP", 35: "IPSP", 37: "OTS"}), # noqa 248 | LEShortField("scid", 0), 249 | ByteField("controller_id", 0), 250 | ] 251 | 252 | 253 | class L2CAP_Create_Channel_Response(Packet): 254 | name = "L2CAP Create Channel Response" 255 | fields_desc = [LEShortField("dcid", 0), 256 | LEShortField("scid", 0), 257 | LEShortEnumField("result", 0, {0: "Connection successful", 1: "Connection pending", 2: "Connection refused - PSM not supported", 258 | 3: "Connection refused - security block", 4: "Connection refused - no resources available", 5: "Connection refused - Controller ID not supported", 259 | 6: "Connection refused - Invalid Source CID", 7: "Connection refused - Source CID already allocated"}), 260 | LEShortEnumField("status", 0, {0: "No further information available", 1: "Authentication pending", 2: "Authorization pending"}), 261 | ] 262 | 263 | 264 | class L2CAP_Move_Channel_Request(Packet): 265 | name = "L2CAP Move Channel Request" 266 | fields_desc = [LEShortField("icid", 0), 267 | ByteField("dest_controller_id", 0), 268 | ] # 0: move to Bluetooth BR/EDR, 1: move to wifi 802.11 269 | 270 | 271 | class L2CAP_Move_Channel_Confirmation_Request(Packet): 272 | name = "L2CAP Move Channel Confirmation Request" 273 | fields_desc = [LEShortField("icid", 0), 274 | LEShortEnumField("result", 0, {0: "Move success", 1: "Move failure"}), 275 | ] 276 | 277 | 278 | 279 | def log_pkt(pkt): 280 | """ 281 | get default format of each packet and update the values 282 | """ 283 | pkt_default = dict(pkt.default_fields, **pkt.payload.default_fields) 284 | pkt_default = dict(pkt_default, **pkt.payload.payload.default_fields) 285 | pkt_CmdHdr_updated = dict(pkt_default, **pkt.fields) 286 | pkt_payload_updated = dict(pkt_CmdHdr_updated, **pkt.payload.fields) 287 | pkt_garbage_updated = dict(pkt_payload_updated, ** pkt.payload.payload.fields) 288 | 289 | return pkt_garbage_updated 290 | 291 | 292 | def send_pkt(bt_addr, sock, pkt, cmd_code, state): 293 | """ 294 | Errno 295 | ConnectionResetError: [Errno 104] Connection reset by peer 296 | ConnectionRefusedError: [Errno 111] Connection refused 297 | TimeoutError: [Errno 110] Connection timed out 298 | and so on .. 299 | """ 300 | global pkt_cnt 301 | global crash_cnt 302 | pkt_cnt += 1 303 | pkt_info = "" 304 | 305 | try: 306 | sock.send(pkt) 307 | #print(pkt.summary) 308 | pkt_info = {} 309 | pkt_info["no"] = pkt_cnt 310 | pkt_info["protocol"] = "L2CAP" 311 | pkt_info["sended_time"] = str(datetime.now()) 312 | pkt_info["payload"] = log_pkt(pkt) 313 | pkt_info["crash"] = "n" 314 | pkt_info["l2cap_state"] = state 315 | 316 | except ConnectionResetError: 317 | print("[-] Crash Found - ConnectionResetError detected") 318 | if(l2ping(bt_addr) == False): 319 | print("Crash Packet :", pkt) 320 | crash_cnt += 1 321 | pkt_info = {} 322 | pkt_info["no"] = pkt_cnt 323 | pkt_info["protocol"] = "L2CAP" 324 | pkt_info["sended_time"] = str(datetime.now()) 325 | pkt_info["cmd"] = L2CAP_CmdDict.get(cmd_code,'reserved for future use') 326 | pkt_info["payload"] = log_pkt(pkt) 327 | pkt_info["l2cap_state"] = state 328 | pkt_info["sended?"] = "n" 329 | pkt_info["crash"] = "y" 330 | pkt_info["crash_info"] = "ConnectionResetError" 331 | 332 | except ConnectionRefusedError: 333 | print("[-] Crash Found - ConnectionRefusedError detected") 334 | if(l2ping(bt_addr) == False): 335 | print("Crash Packet :", pkt) 336 | crash_cnt += 1 337 | pkt_info = {} 338 | pkt_info["no"] = pkt_cnt 339 | pkt_info["protocol"] = "L2CAP" 340 | pkt_info["sended_time"] = str(datetime.now()) 341 | pkt_info["cmd"] = L2CAP_CmdDict.get(cmd_code,'reserved for future use') 342 | pkt_info["payload"] = log_pkt(pkt) 343 | pkt_info["l2cap_state"] = state 344 | pkt_info["sended?"] = "n" 345 | pkt_info["crash"] = "y" 346 | pkt_info["crash_info"] = "ConnectionRefusedError" 347 | 348 | except ConnectionAbortedError: 349 | print("[-] Crash Found - ConnectionAbortedError detected") 350 | if(l2ping(bt_addr) == False): 351 | print("Crash Packet :", pkt) 352 | crash_cnt += 1 353 | pkt_info = {} 354 | pkt_info["no"] = pkt_cnt 355 | pkt_info["protocol"] = "L2CAP" 356 | pkt_info["sended_time"] = str(datetime.now()) 357 | pkt_info["cmd"] = L2CAP_CmdDict.get(cmd_code,'reserved for future use') 358 | pkt_info["payload"] = log_pkt(pkt) 359 | pkt_info["l2cap_state"] = state 360 | pkt_info["sended?"] = "n" 361 | pkt_info["crash"] = "y" 362 | pkt_info["crash_info"] = "ConnectionAbortedError" 363 | 364 | except TimeoutError: 365 | # State Timeout 366 | print("[-] Crash Found - TimeoutError detected") 367 | print("Crash Packet :", pkt) 368 | crash_cnt += 1 369 | pkt_info = {} 370 | pkt_info["no"] = pkt_cnt 371 | pkt_info["protocol"] = "L2CAP" 372 | pkt_info["sended_time"] = str(datetime.now()) 373 | pkt_info["cmd"] = L2CAP_CmdDict.get(cmd_code,'reserved for future use') 374 | pkt_info["payload"] = log_pkt(pkt) 375 | pkt_info["l2cap_state"] = state 376 | pkt_info["sended?"] = "n" 377 | pkt_info["crash"] = "y" 378 | pkt_info["crash_info"] = "TimeoutError" 379 | 380 | except OSError as e: 381 | """ 382 | OSError: [Errno 107] Transport endpoint is not connected 383 | OSError: [Errno 112] Host is down 384 | """ 385 | if "Host is down" in e.__doc__: 386 | print("[-] Crash Found - Host is down") 387 | print("Crash Packet :", pkt) 388 | crash_cnt += 1 389 | pkt_info = {} 390 | pkt_info["no"] = pkt_cnt 391 | pkt_info["protocol"] = "L2CAP" 392 | pkt_info["sended_time"] = str(datetime.now()) 393 | pkt_info["cmd"] = L2CAP_CmdDict.get(cmd_code,'reserved for future use') 394 | pkt_info["payload"] = log_pkt(pkt) 395 | pkt_info["l2cap_state"] = state 396 | pkt_info["sended?"] = "n" 397 | pkt_info["crash"] = "y" 398 | pkt_info["DoS"] = "y" 399 | pkt_info["crash_info"] = "OSError - Host is down" 400 | print("[-] Crash packet causes HOST DOWN. Test finished.") 401 | else: 402 | pass 403 | else: 404 | pass 405 | 406 | # Reset Socket 407 | sock = BluetoothL2CAPSocket(bt_addr) 408 | return sock, pkt_info 409 | 410 | 411 | def l2ping(bt_addr): 412 | """ 413 | 414 | 1) Check the status of sockect in send() method 415 | 2) If there is error in send(), Check l2ping 416 | 3) if l2ping finds packet lost, it is crash! 417 | + You need to check the target device's condition. (Error pop-up or crash dump.) 418 | """ 419 | l2pingRes = subprocess.run(['l2ping',str(bt_addr),"-c","3"],stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 420 | try: 421 | failureRate = str(l2pingRes.stdout).split()[-2] 422 | failureRate = int(failureRate.split("%")[0]) 423 | except ValueError: 424 | failureRate = 100 425 | if(failureRate < 100): 426 | return True 427 | else: 428 | return False 429 | 430 | 431 | def random_psm(): 432 | """ 433 | random psm for connection state fuzzing 434 | 435 | Since PSMs are odd and the least significant bit of the most significant byte is zero, 436 | the following ranges do not contain valid PSMs: 0x0100-0x01FF, 0x0300-0x03FF, 437 | 0x0500-0x05FF, 0x0700-0x07FF, 0x0900-0x09FF, 0x0B00-0x0BFF, 0x0D00- 438 | 0x0DFF. All even values are also not valid as PSMs. 439 | """ 440 | # Get random invalid psm value 441 | psm4fuzz = 0 442 | opt = randint(0, 7) 443 | if(opt == 0): 444 | psm4fuzz = randrange(0x0100, 0x01FF + 0x0001) 445 | elif(opt == 1): 446 | psm4fuzz = randrange(0x0300, 0x03FF + 0x0001) 447 | elif(opt == 2): 448 | psm4fuzz = randrange(0x0500, 0x05FF + 0x0001) 449 | elif(opt == 3): 450 | psm4fuzz = randrange(0x0700, 0x07FF + 0x0001) 451 | elif(opt == 4): 452 | psm4fuzz = randrange(0x0900, 0x09FF + 0x0001) 453 | elif(opt == 5): 454 | psm4fuzz = randrange(0x0B00, 0x0BFF + 0x0001) 455 | elif(opt == 6): 456 | psm4fuzz = randrange(0x0D00, 0x0DFF + 0x0001) 457 | elif(opt == 7): 458 | psm4fuzz = randrange(0x0000, 0xFFFF + 0x0001, 2) 459 | return psm4fuzz 460 | 461 | 462 | def connection_state_fuzzing(bt_addr, sock, state_machine, packet_info): 463 | iteration = 2500 464 | 465 | # 1) Target State : Wait Connect State 466 | for i in range(0, iteration): 467 | 468 | cmd_code = 0x02 469 | pkt = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConnReq(psm=random_psm())/garbage_value(garbage=randrange(0x0000, 0x10000)) 470 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 471 | if(pkt_info == ""): pass 472 | else: packet_info["packet"].append(pkt_info) 473 | 474 | state_machine.closed_to_w_conn() 475 | 476 | cmd_code = 0x03 477 | pkt = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConnResp(dcid=randrange(0x0040, 0x10000), scid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 478 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 479 | if(pkt_info == ""): pass 480 | else: packet_info["packet"].append(pkt_info) 481 | 482 | state_machine.w_conn_to_closed() 483 | 484 | 485 | def creation_state_fuzzing(bt_addr, sock, state_machine, packet_info): 486 | iteration = 2500 487 | 488 | # 2) Target State : Wait Create State 489 | for i in range(0, iteration): 490 | 491 | cmd_code = 0x0C 492 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_Create_Channel_Request(psm=random_psm())/garbage_value(garbage=randrange(0x0000, 0x10000)) 493 | 494 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 495 | if(pkt_info == ""): pass 496 | else: packet_info["packet"].append(pkt_info) 497 | 498 | state_machine.closed_to_w_create() 499 | 500 | cmd_code = 0x0D 501 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_Create_Channel_Response(dcid=randrange(0x0040, 0x10000), scid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 502 | 503 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 504 | if(pkt_info == ""): pass 505 | else: packet_info["packet"].append(pkt_info) 506 | 507 | state_machine.w_create_to_closed() 508 | 509 | 510 | def configuration_state_fuzzing(bt_addr, sock, state_machine, profile, port, packet_info): 511 | iteration = 2500 512 | 513 | # From Connection State to Configure State (Closed State -> Wait Config State) 514 | while(1): 515 | cmd_code = 0x02 516 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConnReq(psm=port, scid=OUR_LOCAL_SCID) 517 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 518 | if(pkt_info == ""): pass 519 | else: packet_info["packet"].append(pkt_info) 520 | 521 | global conn_rsp_flag 522 | global dcid_value 523 | 524 | if(conn_rsp_flag == 0): 525 | # Only one time 526 | conn_rsp = sock.recv() # save pkt info for configuration request 527 | 528 | try: 529 | dcid_value = conn_rsp.dcid 530 | result_value = conn_rsp.result 531 | except: 532 | dcid_value = OUR_LOCAL_SCID 533 | result_value = 1 534 | 535 | conn_rsp_flag = 1 536 | # Can't connection to selected PSM. 537 | if(result_value != 0): 538 | print("[!] Device is not paired with host('{}'). \n[!] Can't test service port that you've selected. Now set port as default PSM, '1'.".format(L2CAP_Connect_Result.get(result_value, 'reserved for future use'))) 539 | port = 1 540 | continue 541 | break 542 | 543 | state_machine.closed_to_w_conf() 544 | 545 | # 1) Target State : Wait Config State 546 | for i in range(0, iteration): 547 | # ConfigReq 548 | cmd_code = 0x04 549 | pkt4fuzz = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConfReq(dcid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 550 | 551 | # logging, real sending(fuzzing in wait config state) 552 | sock, pkt_info = send_pkt(bt_addr, sock, pkt4fuzz, cmd_code, state_machine.current_state.name) 553 | if(pkt_info == ""): pass 554 | else: packet_info["packet"].append(pkt_info) 555 | 556 | # state transition 557 | state_machine.w_conf_to_w_conf() 558 | 559 | # From Wait Config State to Wait Send Config State 560 | cmd_code = 0x04 561 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConfReq(dcid=dcid_value) 562 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 563 | if(pkt_info == ""): pass 564 | else: packet_info["packet"].append(pkt_info) 565 | 566 | # state transition 567 | state_machine.w_conf_to_w_send_conf() 568 | 569 | # 2) Target State : Wait Send Config State 570 | for i in range(0, iteration): 571 | # ConfigReq 572 | cmd_code = 0x04 573 | pkt4fuzz = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConfReq(dcid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 574 | 575 | # logging, real sending here (fuzzing in wait send config state) 576 | sock, pkt_info = send_pkt(bt_addr, sock, pkt4fuzz, cmd_code, state_machine.current_state.name) 577 | if(pkt_info == ""): pass 578 | else: packet_info["packet"].append(pkt_info) 579 | 580 | # state transition (L2CAP_ConfigReq will be sent from target device. From Wait Send Config State to Wait Config Rsp state) 581 | state_machine.w_send_conf_to_w_conf_rsp() 582 | 583 | # 3) Target State : Wait Config Rsp State 584 | for i in range(0, iteration): 585 | # ConfigResp(fail) 586 | cmd_code = 0x05 587 | pkt4fuzz = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConfResp(scid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 588 | 589 | # logging, real sending here (fuzzing in wait send config state) 590 | sock, pkt_info = send_pkt(bt_addr, sock, pkt4fuzz, cmd_code, state_machine.current_state.name) 591 | if(pkt_info == ""): pass 592 | else: packet_info["packet"].append(pkt_info) 593 | 594 | # From Wait Config Rsp state to Wait Ind Final Rsp 595 | cmd_code = 0x05 596 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConfResp(scid=dcid_value) 597 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 598 | if(pkt_info == ""): pass 599 | else: packet_info["packet"].append(pkt_info) 600 | 601 | # state transition 602 | state_machine.w_conf_rsp_to_w_ind_final_rsp() 603 | 604 | # 4) Target State : Wait Ind Final Rsp State 605 | opt = randint(0, 1) 606 | if(opt == 0): 607 | for i in range(0, iteration): 608 | # ConnReq(fail) 609 | cmd_code = 0x02 610 | pkt = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConnReq(psm=random_psm())/garbage_value(garbage=randrange(0x0000, 0x10000)) 611 | 612 | # logging, real sending here 613 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 614 | if(pkt_info == ""): pass 615 | else: packet_info["packet"].append(pkt_info) 616 | 617 | # From Wait Ind Final Rsp to Wait Final Rsp 618 | cmd_code = 0x02 619 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConnReq(psm=port, scid=OUR_LOCAL_SCID) 620 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 621 | if(pkt_info == ""): pass 622 | else: packet_info["packet"].append(pkt_info) 623 | 624 | # state transition 625 | state_machine.w_ind_final_rsp_to_w_final_rsp() 626 | 627 | # 4-1) Target State : Wait Final Rsp 628 | for i in range(0, iteration): 629 | # ConfigReq 630 | cmd_code = 0x04 631 | pkt4fuzz = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConfReq(dcid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 632 | 633 | # logging, real sending here 634 | sock, pkt_info = send_pkt(bt_addr, sock, pkt4fuzz, cmd_code, state_machine.current_state.name) 635 | if(pkt_info == ""): pass 636 | else: packet_info["packet"].append(pkt_info) 637 | 638 | # From Wait Final Rsp to open 639 | cmd_code = 0x04 640 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConfReq(dcid=dcid_value) 641 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 642 | if(pkt_info == ""): pass 643 | else: packet_info["packet"].append(pkt_info) 644 | 645 | # state transition 646 | state_machine.w_final_rsp_to_open() 647 | 648 | elif(opt == 1): 649 | for i in range(0, iteration): 650 | # ConfigReq 651 | cmd_code = 0x04 652 | pkt4fuzz = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConfReq(dcid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 653 | 654 | # logging, real sending here 655 | sock, pkt_info = send_pkt(bt_addr, sock, pkt4fuzz, cmd_code, state_machine.current_state.name) 656 | if(pkt_info == ""): pass 657 | else: packet_info["packet"].append(pkt_info) 658 | 659 | # From Wait Ind Final Rsp to Wait Control Ind 660 | cmd_code = 0x04 661 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConfReq(dcid=dcid_value) 662 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 663 | if(pkt_info == ""): pass 664 | else: packet_info["packet"].append(pkt_info) 665 | 666 | # state transition 667 | state_machine.w_ind_final_rsp_to_w_control_ind() 668 | 669 | # 4-2) Target State : Wait Control Ind 670 | for i in range(0, iteration): 671 | # ConnReq(fail) 672 | cmd_code = 0x02 673 | pkt = L2CAP_CmdHdr(code=cmd_code)/new_L2CAP_ConnReq(psm=random_psm())/garbage_value(garbage=randrange(0x0000, 0x10000)) 674 | # logging, real sending here 675 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 676 | if(pkt_info == ""): pass 677 | else: packet_info["packet"].append(pkt_info) 678 | 679 | # From Wait Control Ind to open 680 | cmd_code = 0x02 681 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_ConnReq(psm=port, scid=OUR_LOCAL_SCID) 682 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 683 | if(pkt_info == ""): pass 684 | else: packet_info["packet"].append(pkt_info) 685 | 686 | # state transition 687 | state_machine.w_control_ind_to_open() 688 | 689 | 690 | def shift_state_fuzzing(bt_addr, sock, state_machine, packet_info): 691 | """ 692 | Connection Shift States : Wait Move, Wait Move Confirm, Wait Move Rsp, Wait Confirm Rsp 693 | 694 | >> From Configuration State to Connection Shift State 695 | Open State -> Wait Move 696 | [!] There is no real device which will be used for channel shift. 697 | 698 | >> Start state : Wait Move state 699 | 700 | >> Can Fuzzing : Wait Move, Wait Move Confirm 701 | 1) Wait Move : Invalid Move req and invalid packets 702 | 2) Wait Move Confirm : Invalid move chan confirm req and invalid packets 703 | 704 | >> Cannot Fuzzing : Wait Move Rsp, Wait Confirm Rsp 705 | 1) Connection shift from Device to another device : Wait Move Rsp, Wait Confirm Rsp 706 | """ 707 | iteration = 2500 708 | 709 | 710 | # 1) Target State : Wait Move State 711 | for i in range(0, iteration): 712 | # packet for moving from open state to wait move state with invalid movechanReq (with invalid dest_controller_id, 0x01(bt)-0x02(wifi) : valid id) 713 | cmd_code = 0x0E 714 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_Move_Channel_Request(dest_controller_id=randrange(0x02,0x100))/garbage_value(garbage=randrange(0x0000, 0x10000)) 715 | 716 | # logging, real sending and state transition here 717 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 718 | if(pkt_info == ""): pass 719 | else: packet_info["packet"].append(pkt_info) 720 | 721 | # state transition 722 | state_machine.open_to_w_move() 723 | 724 | # state transition 725 | state_machine.w_move_to_w_move_confirm() 726 | state_machine.w_move_confirm_to_open() 727 | 728 | # 2) Target State : Wait Move Confirm State 729 | for i in range(0, iteration): 730 | # packet for moving from open state to wait move confirm state with invalid move chan confirm req (with invalid icid) 731 | cmd_code = 0x0E 732 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_Move_Channel_Confirmation_Request(icid=randrange(0x00,0x100))/garbage_value(garbage=randrange(0x0000, 0x10000)) 733 | 734 | # logging, real sending and state transition here 735 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 736 | if(pkt_info == ""): pass 737 | else: packet_info["packet"].append(pkt_info) 738 | 739 | # state transition 740 | state_machine.open_to_w_move_confirm() 741 | 742 | # state transition 743 | state_machine.w_move_confirm_to_open() 744 | 745 | 746 | def disconnection_state_fuzzing(bt_addr, sock, state_machine, port, packet_info): 747 | """ 748 | Connection Shift States : Wait Disconnect 749 | 750 | >> From open to Disconnect 751 | Open State -> Wait disconnect 752 | 753 | >> Start state : Wait disconnect state 754 | 755 | >> Can Fuzzing : Wait Disconnect 756 | 1) Wait Disconnect : Invalid disconn req with invalid psm and invalid packets 757 | """ 758 | #print("\n\t[Disconnection State]") 759 | iteration = 2500 760 | 761 | # state transition 762 | state_machine.open_to_w_discon() 763 | 764 | # 1) Target State : Wait disconnect state 765 | for i in range(0, iteration): 766 | # packet for moving from open state to wait disconnect state 767 | cmd_code = 0x06 768 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_DisconnReq(scid=randrange(0x0040, 0x10000), dcid=randrange(0x0040, 0x10000))/garbage_value(garbage=randrange(0x0000, 0x10000)) 769 | # logging, real sending and state transition here 770 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 771 | if(pkt_info == ""): pass 772 | else: packet_info["packet"].append(pkt_info) 773 | 774 | # Valid Disconn Req 775 | cmd_code = 0x06 776 | pkt = L2CAP_CmdHdr(code=cmd_code)/L2CAP_DisconnReq(scid=OUR_LOCAL_SCID, dcid=dcid_value) 777 | 778 | # logging, real sending and state transition here 779 | sock, pkt_info = send_pkt(bt_addr, sock, pkt, cmd_code, state_machine.current_state.name) 780 | if(pkt_info == ""): pass 781 | else: packet_info["packet"].append(pkt_info) 782 | 783 | # state transition 784 | state_machine.w_disconn_to_closed() 785 | 786 | 787 | def l2cap_fuzzing(bt_addr, profile, port, test_info): 788 | """ 789 | Fuzzing in specific state = Sending packet from that state. 790 | """ 791 | if(profile == "None" or port == "None"): 792 | print('Cannot Fuzzing') 793 | return 794 | 795 | with open('log_{}.wfl'.format(test_info["starting_time"][11:19].replace(':',"",2)), 'w', encoding="utf-8") as f: 796 | logger = OrderedDict() 797 | logger.update(test_info) 798 | logger["packet"] = [] 799 | 800 | print("Start Fuzzing... Please hit Ctrl + C to finish...") 801 | sock = BluetoothL2CAPSocket(bt_addr) 802 | state_machine = l2cap_state_machine() 803 | 804 | try: 805 | while(1): 806 | print("[+] Tested %d packets" % (pkt_cnt)) 807 | # log slicing 808 | if(len(logger['packet']) > 200000): 809 | del logger['packet'][:100000] 810 | # Connection State Fuzzing (1/2) + closed 811 | connection_state_fuzzing(bt_addr, sock, state_machine, logger) 812 | 813 | # Creation State fuzzing (1/2) 814 | creation_state_fuzzing(bt_addr, sock, state_machine, logger) 815 | 816 | # Configuration State Fuzzing (6/8) 817 | configuration_state_fuzzing(bt_addr, sock, state_machine, profile, port, logger) 818 | 819 | # Connection Shift State Fuzzing (2/4) 820 | shift_state_fuzzing(bt_addr, sock, state_machine, logger) 821 | 822 | # Disconnection State Fuzzing (1/1) 823 | disconnection_state_fuzzing(bt_addr, sock, state_machine, port, logger) 824 | 825 | except Exception as e: 826 | print("[!] Error Message :", e) 827 | print("[+] Save logfile") 828 | logger["end_time"] = str(datetime.now()) 829 | logger["count"] = {"all" : pkt_cnt, "crash" : crash_cnt, "passed" : pkt_cnt-crash_cnt} 830 | json.dump(logger, f, indent="\t") 831 | 832 | except KeyboardInterrupt as k: 833 | print("[!] Fuzzing Stopped :", k) 834 | print("[+] Save logfile") 835 | logger["end_time"] = str(datetime.now()) 836 | logger["count"] = {"all" : pkt_cnt, "crash" : crash_cnt, "passed" : pkt_cnt-crash_cnt} 837 | json.dump(logger, f, indent="\t") 838 | 839 | 840 | 841 | 842 | --------------------------------------------------------------------------------