diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index c17c8c9ac..466d22ab2 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -968,6 +968,27 @@ endif (NOT SKIPPYTHON EQUAL 1) target_link_directories(proxmark3 PRIVATE ${ADDITIONAL_LNKDIRS}) install(TARGETS proxmark3 DESTINATION "bin") + +# Grant the capabilities the BLE connect-by-name scan needs (raw HCI: CAP_NET_RAW +# + CAP_NET_ADMIN). Linux + Bluez only, and non-fatal: if setcap is absent, the +# target filesystem lacks xattr support, or this is a staged/packaged install, +# it just warns. Connect-by-address (-p ble:) never needs this. +if (BLUEZ_FOUND AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_program(SETCAP_EXECUTABLE setcap) + if (SETCAP_EXECUTABLE) + install(CODE " + set(_pm3_bin \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin/proxmark3\") + execute_process( + COMMAND \"${SETCAP_EXECUTABLE}\" cap_net_raw,cap_net_admin+eip \"\${_pm3_bin}\" + RESULT_VARIABLE _pm3_setcap_rc) + if (NOT _pm3_setcap_rc EQUAL 0) + message(WARNING \"setcap failed - BLE connect-by-name (-p ble:) will need sudo; connect-by-MAC is unaffected\") + endif () + ") + else () + message(STATUS "setcap not found - BLE connect-by-name will need sudo at runtime") + endif () +endif () install(DIRECTORY cmdscripts lualibs luascripts pyscripts resources dictionaries DESTINATION "share/proxmark3") add_custom_command(OUTPUT lualibs/pm3_cmd.lua diff --git a/client/Makefile b/client/Makefile index e6eb01104..7e2248b33 100644 --- a/client/Makefile +++ b/client/Makefile @@ -1068,6 +1068,9 @@ install: all ifneq (,$(INSTALLBIN)) $(Q)$(INSTALLSUDO) $(MKDIR) $(DESTDIR)$(PREFIX)$(PATHSEP)$(INSTALLBINRELPATH) $(Q)$(INSTALLSUDO) $(CP) $(INSTALLBIN) $(DESTDIR)$(PREFIX)$(PATHSEP)$(INSTALLBINRELPATH) +ifeq ($(platform),Linux) + $(Q)$(INSTALLSUDO) setcap 'cap_net_raw,cap_net_admin+eip' $(DESTDIR)$(PREFIX)$(PATHSEP)$(INSTALLBINRELPATH)$(PATHSEP)$(INSTALLBIN) || echo "[!] setcap failed - BLE connect-by-name (-p ble:) will need sudo; connect-by-MAC is unaffected" +endif endif ifneq (,$(INSTALLSHARE)) $(Q)$(INSTALLSUDO) $(MKDIR) $(DESTDIR)$(PREFIX)$(PATHSEP)$(INSTALLSHARERELPATH) diff --git a/client/src/uart/ble_posix.c b/client/src/uart/ble_posix.c index 588c3f5e1..19afd25e2 100644 --- a/client/src/uart/ble_posix.c +++ b/client/src/uart/ble_posix.c @@ -17,13 +17,17 @@ #include #include +#include // strcasecmp #include #include #include +#include #include #include #include #include +#include +#include #include "ui.h" // PrintAndLogEx #include "pm3_cmd.h" // PM3_* (only for messaging parity; returns are 0/neg here) @@ -183,14 +187,168 @@ static int att_subscribe(int fd, uint16_t cccd_handle) { return (n >= 1) ? 0 : -1; } -int ble_connect(const char *mac, uint16_t chr_uuid16, ble_conn_t *conn) { - memset(conn, 0, sizeof(*conn)); - conn->fd = -1; +// ---- name -> address resolution (active LE scan over raw HCI) ---- +// AD types carrying the device name (Core Spec, Supplement, Part A). +#define BLE_ADV_NAME_SHORT 0x08 +#define BLE_ADV_NAME_FULL 0x09 + +static long ble_now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000L + ts.tv_nsec / 1000000L; +} + +// Walk the AD structures of one advertising/scan-response report, looking for a +// Local Name (complete or shortened) that equals `want` (case-insensitive). +// Copies whatever name it finds into `found` for logging. Returns 1 on match. +static int ble_ad_name_matches(const char *want, const uint8_t *ad, size_t adlen, + char *found, size_t foundsz) { + size_t i = 0; + while (i < adlen) { + uint8_t flen = ad[i]; // length byte counts the type + value + if (flen == 0) break; // padding -> end of meaningful data + if (i + 1 + flen > adlen) break; // truncated field -> stop, don't overrun + uint8_t type = ad[i + 1]; + if (type == BLE_ADV_NAME_FULL || type == BLE_ADV_NAME_SHORT) { + size_t nlen = flen - 1; + if (nlen >= foundsz) nlen = foundsz - 1; + memcpy(found, &ad[i + 2], nlen); + found[nlen] = 0; + if (strcasecmp(found, want) == 0) return 1; + } + i += flen + 1; + } + return 0; +} + +// Print an exact, copy-pasteable command to grant the capabilities the LE scan +// needs, using the running binary's own path so it survives rebuilds/renames. +// Also points at the no-privilege alternative (connect by address). +static void ble_print_caps_hint(void) { + char exe[4096]; + ssize_t n = readlink("/proc/self/exe", exe, sizeof(exe) - 1); + if (n > 0) { + exe[n] = 0; + PrintAndLogEx(INFO, "BLE: scanning by name needs extra privileges. Grant them once with:"); + PrintAndLogEx(INFO, " " _YELLOW_("sudo setcap 'cap_net_raw,cap_net_admin+eip' %s"), exe); + } else { + PrintAndLogEx(INFO, "BLE: scanning by name needs " _YELLOW_("CAP_NET_RAW+CAP_NET_ADMIN") + " on the client binary (or run with sudo)"); + } + PrintAndLogEx(INFO, " or connect by address instead: " _YELLOW_("-p ble:") " (no extra privileges)"); +} + +int ble_resolve_name(const char *name, char *out_mac, size_t out_mac_sz, int timeout_ms) { + if (name == NULL || out_mac == NULL || out_mac_sz < 18) return -1; + out_mac[0] = 0; + + int dev_id = hci_get_route(NULL); + if (dev_id < 0) { + PrintAndLogEx(ERR, "BLE: no local Bluetooth adapter found (%s)", strerror(errno)); + return -1; + } + int dd = hci_open_dev(dev_id); + if (dd < 0) { + PrintAndLogEx(ERR, "BLE: cannot open hci%d (%s)", dev_id, strerror(errno)); + if (errno == EPERM || errno == EACCES) + ble_print_caps_hint(); + return -1; + } + + // BlueZ's hci_le_set_scan_* take the open descriptor (dd), not dev_id, + // despite the arg name - passing dev_id silently fails. + // Active scan (0x01) so SCAN_RSP (where the BWM puts its name) is seen. + if (hci_le_set_scan_parameters(dd, 0x01, htobs(0x0010), htobs(0x0010), 0x00, 0x00, 1000) < 0) + PrintAndLogEx(INFO, "BLE: set scan parameters failed (%s), continuing", strerror(errno)); + + // A scan already left enabled makes the enable below return EIO; clear it first. + hci_le_set_scan_enable(dd, 0x00, 0x00, 1000); + if (hci_le_set_scan_enable(dd, 0x01, 0x00, 1000) < 0) { // filter_dup off: names may only be in SCAN_RSP + PrintAndLogEx(ERR, "BLE: cannot start LE scan (%s)", strerror(errno)); + if (errno == EPERM || errno == EACCES) + ble_print_caps_hint(); + hci_close_dev(dd); + return -1; + } + + // Receive only HCI event packets carrying LE meta events; save/restore the + // socket's previous filter so we leave the handle as we found it. + struct hci_filter of, nf; + socklen_t olen = sizeof(of); + int have_of = (getsockopt(dd, SOL_HCI, HCI_FILTER, &of, &olen) == 0); + hci_filter_clear(&nf); + hci_filter_set_ptype(HCI_EVENT_PKT, &nf); + hci_filter_set_event(EVT_LE_META_EVENT, &nf); + setsockopt(dd, SOL_HCI, HCI_FILTER, &nf, sizeof(nf)); + + PrintAndLogEx(INFO, "BLE: scanning up to " _YELLOW_("%d") " ms for " _YELLOW_("%s") " ...", timeout_ms, name); + + int rc = -1; + char nm[64]; + long deadline = ble_now_ms() + timeout_ms; + uint8_t buf[HCI_MAX_EVENT_SIZE]; + + while (ble_now_ms() < deadline) { + long rem = deadline - ble_now_ms(); + if (rem <= 0) break; + struct timeval tv = { .tv_sec = rem / 1000, .tv_usec = (rem % 1000) * 1000 }; + fd_set rs; + FD_ZERO(&rs); + FD_SET(dd, &rs); + int s = select(dd + 1, &rs, NULL, NULL, &tv); + if (s < 0) { if (errno == EINTR) break; break; } // Ctrl-C or error -> give up cleanly + if (s == 0) break; // timeout + + int n = read(dd, buf, sizeof(buf)); + if (n < (int)(1 + HCI_EVENT_HDR_SIZE + 1)) continue; + uint8_t *pkt_end = buf + n; + + // layout: [0]=HCI_EVENT_PKT, [1..2]=hci_event_hdr, then evt_le_meta_event + evt_le_meta_event *meta = (evt_le_meta_event *)(buf + 1 + HCI_EVENT_HDR_SIZE); + if ((uint8_t *)meta->data > pkt_end) continue; + if (meta->subevent != EVT_LE_ADVERTISING_REPORT) continue; + + uint8_t *p = meta->data; + uint8_t num = *p++; // number of reports in this event + for (uint8_t r = 0; r < num; r++) { + if (p + LE_ADVERTISING_INFO_SIZE > pkt_end) break; + le_advertising_info *info = (le_advertising_info *)p; + uint8_t *next = p + LE_ADVERTISING_INFO_SIZE + info->length + 1; // +1 trailing RSSI + if (next > pkt_end) break; // malformed / truncated report + if (ble_ad_name_matches(name, info->data, info->length, nm, sizeof(nm))) { + ba2str(&info->bdaddr, out_mac); + PrintAndLogEx(SUCCESS, "BLE: found " _GREEN_("%s") " at " _GREEN_("%s"), nm, out_mac); + rc = 0; + break; + } + p = next; + } + if (rc == 0) break; + } + + hci_le_set_scan_enable(dd, 0x00, 0x00, 1000); + if (have_of) setsockopt(dd, SOL_HCI, HCI_FILTER, &of, sizeof(of)); + hci_close_dev(dd); + + if (rc != 0) + PrintAndLogEx(ERR, "BLE: no advertising device named " _YELLOW_("%s") " seen within %d ms", name, timeout_ms); + return rc; +} + +// Retry the L2CAP connect briefly: a create-connection issued as an LE scan +// tears down can be refused, and the peer may be between advertising events. +#define BLE_CONNECT_TRIES 6 +#define BLE_CONNECT_BACKOFF_MS 150 + +// Open an L2CAP/ATT socket and connect to `mac`. Returns the fd on success, +// -1 on a retryable connect failure (errno set to the connect error), or +// -2 on a hard setup failure (socket/bind), which is already logged. +static int ble_l2cap_connect(const char *mac) { int fd = socket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_L2CAP); if (fd < 0) { PrintAndLogEx(ERR, "BLE: cannot create L2CAP socket (%s)", strerror(errno)); - return -1; + return -2; } // Enlarge the kernel RX buffer so bursts of notifications queue instead of @@ -206,7 +364,7 @@ int ble_connect(const char *mac, uint16_t chr_uuid16, ble_conn_t *conn) { if (bind(fd, (struct sockaddr *)&src, sizeof(src)) < 0) { PrintAndLogEx(ERR, "BLE: bind failed (%s)", strerror(errno)); close(fd); - return -1; + return -2; } struct sockaddr_l2 dst = {0}; @@ -215,8 +373,31 @@ int ble_connect(const char *mac, uint16_t chr_uuid16, ble_conn_t *conn) { str2ba(mac, &dst.l2_bdaddr); dst.l2_cid = htobs(ATT_CID); if (connect(fd, (struct sockaddr *)&dst, sizeof(dst)) < 0) { - PrintAndLogEx(ERR, "BLE: cannot connect to " _YELLOW_("%s") " (%s)", mac, strerror(errno)); + int e = errno; // close() may clobber errno; preserve it for the caller close(fd); + errno = e; + return -1; + } + return fd; +} + +int ble_connect(const char *mac, uint16_t chr_uuid16, ble_conn_t *conn) { + memset(conn, 0, sizeof(*conn)); + conn->fd = -1; + + int fd = -1; + int last_errno = 0; + for (int attempt = 1; attempt <= BLE_CONNECT_TRIES; attempt++) { + fd = ble_l2cap_connect(mac); + if (fd >= 0) break; + if (fd == -2) return -1; // hard setup failure, already logged + last_errno = errno; // retryable connect failure + if (attempt < BLE_CONNECT_TRIES) { + usleep(BLE_CONNECT_BACKOFF_MS * 1000); + } + } + if (fd < 0) { + PrintAndLogEx(ERR, "BLE: cannot connect to " _YELLOW_("%s") " (%s)", mac, strerror(last_errno)); return -1; } diff --git a/client/src/uart/ble_posix.h b/client/src/uart/ble_posix.h index d67487a7a..9db502353 100644 --- a/client/src/uart/ble_posix.h +++ b/client/src/uart/ble_posix.h @@ -39,18 +39,30 @@ typedef struct { size_t leftover_len; } ble_conn_t; +// Default time budget for a name -> address scan. +#define BLE_SCAN_TIMEOUT_MS 8000 + +// Resolve an advertised device name to its LE address via an active HCI scan. +// Matches the Complete (0x09) or Shortened (0x08) Local Name, case-insensitively. +// On success writes "XX:XX:XX:XX:XX:XX" into out_mac (needs >= 18 bytes) and +// returns PM3_SUCCESS; returns PM3_EINVARG on bad arguments, PM3_EIO on a +// local adapter/scan failure, or PM3_ETIMEOUT if no such device is seen in time. +// Needs CAP_NET_RAW+CAP_NET_ADMIN (raw HCI), i.e. more privilege than connect. +int ble_resolve_name(const char *name, char *out_mac, size_t out_mac_sz, int timeout_ms); + // Connect to `mac` (LE public), discover the SPP char, subscribe to notifications. // `chr_uuid16` selects the data characteristic (pass BLE_SPP_CHR_UUID16). -// Returns 0 on success (conn filled), negative on error. On error conn->fd == -1. +// Returns PM3_SUCCESS (conn filled) or PM3_EIO on error. On error conn->fd == -1. int ble_connect(const char *mac, uint16_t chr_uuid16, ble_conn_t *conn); // Send len bytes to the device, chunked to (mtu-3) as ATT Write Commands. -// Returns 0 on success, negative on error. +// Returns PM3_SUCCESS, PM3_ENOTTY if not connected, or PM3_EIO on write error. int ble_send(ble_conn_t *conn, const uint8_t *data, size_t len); // Receive up to maxlen payload bytes (drains leftover first, then waits up to -// timeout_ms for notifications). *out_len set to bytes copied. Returns 0 on -// success (including 0 bytes on timeout), negative on error/disconnect. +// timeout_ms for notifications). *out_len set to bytes copied. Returns +// PM3_SUCCESS on success (including 0 bytes on timeout), PM3_ENOTTY if not +// connected, or PM3_EIO on error/disconnect. int ble_recv(ble_conn_t *conn, uint8_t *buf, size_t maxlen, size_t *out_len, int timeout_ms); void ble_close(ble_conn_t *conn); diff --git a/client/src/uart/uart_posix.c b/client/src/uart/uart_posix.c index 3111eb466..b0240edd9 100644 --- a/client/src/uart/uart_posix.c +++ b/client/src/uart/uart_posix.c @@ -39,6 +39,7 @@ #include #ifdef HAVE_BLUEZ +#include // isxdigit (ble: name vs address check) #include #include #include "ble_posix.h" @@ -115,6 +116,22 @@ uint32_t uart_get_timeouts(void) { return newtimeout_value; } +#ifdef HAVE_BLUEZ +// True if `s` is a plain 6-octet BD address "XX:XX:XX:XX:XX:XX" (hex, colon-sep). +// Used to tell a literal address from a device name after the "ble:" prefix. +static bool uart_is_bdaddr(const char *s) { + if (s == NULL || strlen(s) != 17) return false; + for (int i = 0; i < 17; i++) { + if ((i % 3) == 2) { + if (s[i] != ':') return false; + } else if (isxdigit((unsigned char)s[i]) == 0) { + return false; + } + } + return true; +} +#endif // HAVE_BLUEZ + serial_port uart_open(const char *pcPortName, uint32_t speed, bool slient) { serial_port_unix_t_t *sp = calloc(sizeof(serial_port_unix_t_t), sizeof(uint8_t)); @@ -416,8 +433,22 @@ serial_port uart_open(const char *pcPortName, uint32_t speed, bool slient) { free(sp); return INVALID_SERIAL_PORT; } - // pcPortName is "ble:" - if (ble_connect(pcPortName + 4, BLE_SPP_CHR_UUID16, bc) != 0) { + + // pcPortName is "ble:". If the part after the prefix is + // a literal BD address use it as-is; otherwise treat it as an advertised + // device name and resolve it to an address via an active LE scan. + const char *ble_arg = pcPortName + 4; + char resolved[18]; + if (uart_is_bdaddr(ble_arg) == false) { + if (ble_resolve_name(ble_arg, resolved, sizeof(resolved), BLE_SCAN_TIMEOUT_MS) != 0) { + free(bc); + free(sp); + return INVALID_SERIAL_PORT; + } + ble_arg = resolved; + } + + if (ble_connect(ble_arg, BLE_SPP_CHR_UUID16, bc) != 0) { free(bc); free(sp); return INVALID_SERIAL_PORT; diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index 3c11866f1..9fafe84f3 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -38,7 +38,7 @@ // Over the BWM/FPC link the forward buffers (AT32 DMA ring, ESP UART RX) are // small, so a full PM3_CMD_DATA_SIZE frame overruns them. Cap the payload the // device advertises and sends when replying via FPC. USB is unaffected. -#define PM3_FPC_MAX_DATA 2048 +#define PM3_FPC_MAX_DATA 4096 typedef struct { uint64_t cmd;