Repository: whad-team/whad-client

Issue:
winject zigbee/802.15.4 issue
every time I run `winject -i uart0 dot15d4 -c 25 0308e9ffffffff07588e`, I get this as a return 
`[!] Invalid syntax: 0308e9ffffffff07588e`.

The same thing happens when I replace "dot15d4" with "zigbee" and/or choose a different channel number. I know for sure that uart0 is valid.

Hints from issue comments:
@gertrudem: I got it to run without errors now, but when I look on wireshark the packets I injected, every time the FCS is bad and is 0000.

When I do something like: `winject -i uart0 dot15d4 -c 25 030829ffffffff07588e`, which is a Beacon request packet with a valid FCS. The packet appears to be `030829ffffffff070000` when seen in wireshark, where the last 2 bytes that make up the FCS are turned to 00:00. Thus it's registered with a bad FCS@virtualabs: Thank you for reporting this issue !

Can you give more details about your setup:
* what hardware are you using to inject and monitor packets ?
* can you describe precisely how you perform this packet injection (steps followed, command lines and the resulting output) ?

Wrong FCS can be caused by a hardware device that is not able to capture a packet's FCS value, or possibly a bug in the framework.@virtualabs: I took some time to try to reproduce this issue and experienced the same errors with `winject`. This tool needs some improvements to work flawlessly and I'm working on a series of fixes that will solve most of the issues you had with it.

However, I managed to inject the exact raw Dot15d4 you mentioned (`0308e9ffffffff07588e`) using a nRF52840 while capturing packets with another nRF52840 dongle and did not see any error in the captured PCAP file. I used the following command to inject this packet:

```
$ winject -i uart0 -r dot15d4 -c 16 0308e9ffffffff07588e
```

And the following command to monitor the packets sent over the air:

```
$ wsniff -i uart1 dot15d4 -c 16 | wdump /tmp/zigbee-sniff.pcap
```

After a deep analysis, it looks like the nRF52840 firmware (a.k.a `ButteRFly`) reports a faulty FCS that is corrected in some way by WHAD, but this causes an issue when `wsniff` is chained with `wdump`. Therefore this firmware has a bug, but if you save dot15d4 frames (or packets) with `wsniff`'s `-o` option then everything is back to normal and your capture is OK. I'll see with @RCayre how to fix the firmware, but for now it is still buggy.
@virtualabs: After some further research, we indeed have an issue with 802.15.4 FCS in ButteRFly. A fix has been developed and will be merged pretty soon, WHAD client will be also fixed to match the FCS byte order (little-endian) and will display a warning when used with ButteRFly version prior to the 1.1.0 (in development).@gertrudem: Nice, I was also wondering how to use the injection code for IEEE 802.15.4:

```
from whad.device import WhadDevice
from whad.dot15d4 import Sniffer

# Create a compatible device instance
device = WhadDevice.create("uart0")

# Use a default Dot15d4 connector
connector = Dot15d4(device)

# Send 802.15.4 packet
connector.send(b"Hello World !")
```

As I get this error:

```
Traceback (most recent call last):
  File "/home/Desktop/test2.py", line 5, in <module>
    device = WhadDevice.create("uart0")
             ^^^^^^^^^^
NameError: name 'WhadDevice' is not defined
```@virtualabs: That's weird, `WhadDevice` is correctly imported in your python script. Looks like an error related to Python and not WHAD directly.@gertrudem: I looked a bit into the code and here's what I found:

in `whad-client/whad/dot15d4/connector/sniffer.py` like the example code, there is no Dot15d4 class, so you import Dot15d4 instead from `whad-client/whad/dot15d4/connector/__init__.py` because you get the previous `NameError`:

```
from whad.device import WhadDevice
from whad.dot15d4 import Dot15d4
``` 


However, after that I get this error:

```
Traceback (most recent call last):
  File "/home/Desktop/injecter.py", line 16, in <module>
    connector.send(b'Hello World')
  File "/home/miniconda3/lib/python3.12/site-packages/whad/dot15d4/connector/__init__.py", line 220, in send
    if Dot15d4FCS not in pdu:
       ^^^^^^^^^^^^^^^^^^^^^
TypeError: a bytes-like object is required, not 'Packet_metaclass'
```
Probably beacause `send()` in `whad-client/whad/dot15d4/connector/__init__.py` expects a `scapy.layers.dot15d4.Dot15d4, scapy.layers.dot15d4.Dot15d4FCS` input instead of something like `b'Hello World'.`

So I tried to craft a scapy packet as an input parameter:
**### BRPacket.py:**

```
from scapy.all import *
from scapy.layers.dot15d4 import *
from scapy.layers.zigbee import *
def beacon_request(seqnum=0):

    dot15d4 = scapy.layers.dot15d4.Dot15d4()
    dot15d4.fcf_frametype = 3
    dot15d4.fcf_security = 0
    dot15d4.fcf_pending = 0
    dot15d4.fcf_ackreq = 0
    dot15d4.fcf_panidcompress = 0
    dot15d4.fcf_reserved_1 = 0
    dot15d4.fcf_destaddrmode = 2
    dot15d4.fcf_framever = 0
    dot15d4.fcf_srcaddrmode = 0
    dot15d4.seqnum = seqnum

    dot15d4_fcs = Dot15d4FCS()
    dot15d4_fcs.fcf_frametype= 3
    dot15d4_fcs.fcf_destaddrmode= 2
    dot15d4_fcs.seqnum = seqnum

    return (dot15d4, dot15d4_fcs)
```


But when I try to run an adapted version of the sample code:
```
from whad.device import WhadDevice
from whad.dot15d4 import Dot15d4

import BRPacket

device = WhadDevice.create("uart1")

connector = Dot15d4(device)
channel = 19

packet = BRPacket.beacon_request(223)

connector.send(packet, channel)
```

I get another error that I'm not sure how to handle:
```
Traceback (most recent call last):
  File "/home/Desktop/injecter.py", line 16, in <module>
    connector.send(packet, channel)
  File "/home/miniconda3/lib/python3.12/site-packages/whad/dot15d4/connector/__init__.py", line 221, in send
    packet = Dot15d4FCS(raw(pdu) + Dot15d4FCS().compute_fcs(raw(pdu)))
                        ^^^^^^^^
  File "/home/miniconda3/lib/python3.12/site-packages/scapy/compat.py", line 294, in raw
    return bytes(x)
           ^^^^^^^^
TypeError: 'Dot15d4' object cannot be interpreted as an integer
```


@virtualabs: I see, this example had worked at some point it looks like we reworked the 802.15.4 API and did not update it.
Also, the documentation of `whad.dot15d4.Dot15d4.send()` method seems confusing, as it only expects a packet of type `scapy.layers.dot15d4.Dot15d4` or `scapy.layers.dot15d4.Dot15d4FCS` and not a tuple. 

I've tested sending a 802.15.4 frame with the following code and it worked pretty well:

```python
from scapy.all import *
from scapy.layers.dot15d4 import *
from scapy.layers.zigbee import *
from whad.device import WhadDevice
from whad.dot15d4 import Sniffer, Dot15d4

def beacon_request(seqnum=0):
    pkt = scapy.layers.dot15d4.Dot15d4()
    pkt.fcf_frametype = 3
    pkt.fcf_security = 0
    pkt.fcf_pending = 0
    pkt.fcf_ackreq = 0
    pkt.fcf_panidcompress = 0
    pkt.fcf_reserved_1 = 0
    pkt.fcf_destaddrmode = 2
    pkt.fcf_framever = 0
    pkt.fcf_srcaddrmode = 0
    pkt.seqnum = seqnum

    return pkt

# Create a compatible device instance
device = WhadDevice.create("uart0")

# Use a default Dot15d4 connector
connector = Dot15d4(device)

# Send 802.15.4 packet
packet = beacon_request(223)

connector.send(packet, channel=19)
``` 

Note that I used the fixed version of WHAD as well as a fixed version ButteRFly, but since it is a Python script you should not experience any error with FCS. 

For the record, sending raw packets as byte buffers must be done by calling `send_mac()` and not `send()`. `send_mac()` accepts a buffer of bytes and you can also specify if you want the FCS to be automatically added or not. It is not correctly documented but I will do a code refactoring of the `whad.dot15d4` module soon (like I did with `whad.ble` or `whad.device` in 2024). @gertrudem: Nice! This fixed the my issue. Thanks so much for the explanation.@gertrudem: Could you give me an example of how send_mac() works with a scapy packet? I can convert the the contents of a scapy packet into byte form using `raw(packet)`, or `packet.build()`. However, neither of these work when passed as pdu:

```
from scapy.compat import raw
from whad.device import WhadDevice
from whad.dot15d4 import Dot15d4

import BRPacket

# Create a compatible device instance
device = WhadDevice.create("uart1")

# Use a default Dot15d4 connector
connector = Dot15d4(device)
channel = 19

packet = BRPacket.beacon_request(223)

# pktBytes = raw(packet)
pktBytes = packet.build()

connector.send_mac(pktBytes, channel)
```

```
Traceback (most recent call last):
  File "/home/Desktop/injector.py", line 19, in <module>
    connector.send_mac(raw(packet), channel)
  File "/home/miniconda3/lib/python3.12/site-packages/whad/dot15d4/connector/__init__.py", line 248, in send_mac
    packet = pdu / raw(b'\x00\x00')
             ~~~~^~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for /: 'bytes' and 'bytes'
```

Or alternatively, be able to send type `<class 'scapy.layers.dot15d4.Dot15d4FCS'>` that is produced from this function:
```
def beacon_request2(seqnum=0):

    dot15d4_cmd = Dot15d4Cmd()
    dot15d4_cmd.cmd_id = 7 # values for proper beacon request packet
    dot15d4_cmd.dest_addr = 0xffff
    dot15d4_cmd.dest_panid = 0xffff

    dot15d4_fcs = Dot15d4FCS()
    dot15d4_fcs.fcf_frametype= 3
    dot15d4_fcs.fcf_destaddrmode= 2
    dot15d4_fcs.seqnum = seqnum

    return dot15d4_fcs / dot15d4_cmd
```


Or just update this issue thread when you've refactored the code. This library has been very useful for my pet project. Thanks!@virtualabs: The current version of `send_mac()` I've been working on while solving this issue did correctly managed bytes buffer but I took some time to rework other parts of WHAD in order to allow raw 802.15.4 frames without FCS, even if the hardware does expect an FCS value (basically by moving the last 2 bytes of payload into this FCS field :smile: ). It is a bit tricky but now I can send raw frames with `send_mac()`. It needs more testing but I guess it would be helpful to handle protocols that rely on 802.15.4 MAC with no FCS support.

If you want to give it a try, you can checkout the `bugfix/issue-112` branch that includes all my fixes related to this issue and use it with the following code. Note that you must update your nRF52 USB dongle firmware with the latest version as this branch includes a fix related to FCS values (that was not correctly handled in whad). I've attached this firmware in hex if you don't want to compile it.

```python
from whad.device import WhadDevice
from whad.dot15d4 import Dot15d4

# Create a compatible device instance
device = WhadDevice.create("uart0")

# Use a default Dot15d4 connector
connector = Dot15d4(device)

# Send raw frame (set add_fcs to True to add valid FCS field)
# This will send "This is a test" with no FCS
connector.send_mac(b"This is a test", channel=19, add_fcs=False)

# This will send a payload with a valid FCS field
connector.send_mac(b"This has valid FCS", channel=19, add_fcs=True)
```
[dfu.zip](https://github.com/user-attachments/files/18407232/dfu.zip)
@gertrudem: I've tried this branch and it seems to be working well with the new firmware. Although I still get a message telling me to upgrade to the latest firmware even though I flashed mine with `dfu.zip`. Thanks, I can send raw bytes from scapy packets now!

Solution patch:
diff --git a/doc/source/dot15d4/started.rst b/doc/source/dot15d4/started.rst
index 5cecc4e4..54ded194 100644
--- a/doc/source/dot15d4/started.rst
+++ b/doc/source/dot15d4/started.rst
@@ -28,6 +28,9 @@ channel and sniff packets:
     # Set channel
     sniffer.channel = 11
 
+    # Start sniffing
+    sniffer.start()
+
     # Listen for packets
     for packet in sniffer.sniff():
         packet.show()
diff --git a/requirements.txt b/requirements.txt
index 87fa1124..a19e7cfb 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,3 +5,4 @@ pyserial==3.5.0
 pycryptodomex==3.11.0
 pyusb==1.2.1
 cryptography==38.0.0
+packaging==24.0
diff --git a/whad/cli/app.py b/whad/cli/app.py
index 7ce24ba9..0b64940d 100644
--- a/whad/cli/app.py
+++ b/whad/cli/app.py
@@ -290,6 +290,9 @@ class CommandLineApp(ArgumentParser):
     OUTPUT_STANDARD = 1
     OUTPUT_WHAD = 2
 
+    # Unique application instance
+    instance = None
+
     def __init__(self, description: str = None, commands: bool=True, interface: bool=True, input: int = INPUT_WHAD,
                  output: int = OUTPUT_WHAD, **kwargs):
         """Instanciate a CommandLineApp
@@ -302,6 +305,10 @@ class CommandLineApp(ArgumentParser):
         :param int input: specify the input type when application has its stdin piped
         :param int output: specify the output type when application has its stdout piped
         """
+        # Save application instance as a class property
+        if CommandLineApp.instance is None:
+            CommandLineApp.instance = self
+
         super().__init__(description=description, **kwargs)
 
         self.__interface = None
@@ -370,6 +377,13 @@ class CommandLineApp(ArgumentParser):
                 help="command arguments"
             )
 
+    @staticmethod
+    def get_instance():
+        """Return unique application instance
+        """
+        return CommandLineApp.instance
+
+
     @property
     def interface(self):
         """Return the selected WHAD interface.
diff --git a/whad/dot15d4/connector/__init__.py b/whad/dot15d4/connector/__init__.py
index 24af73bc..44d38bd3 100644
--- a/whad/dot15d4/connector/__init__.py
+++ b/whad/dot15d4/connector/__init__.py
@@ -1,14 +1,20 @@
+import logging
 from typing import Union, Tuple
 
+# packaging
+from packaging.version import Version
+
 # Scapy imports
 from scapy.compat import raw
 from scapy.config import conf
 from scapy.layers.dot15d4 import Dot15d4 as Dot15d4NoFCS
 from scapy.layers.dot15d4 import Dot15d4FCS
+
 from whad.scapy.layers.dot15d4tap import Dot15d4TAP_Hdr
 from whad.hub.dot15d4 import Dot15d4Metadata
 # Main whad imports
 from whad.hub.discovery import Domain, Capability
+from whad.cli.app import CommandLineApp
 from whad.device import WhadDeviceConnector
 from whad.helpers import message_filter, is_message_type
 from whad.exceptions import UnsupportedDomain, UnsupportedCapability
@@ -20,6 +26,7 @@ from whad.hub.dot15d4 import NodeAddress, Commands, NodeAddressType, PduReceived
     RawPduReceived, EnergyDetectionSample
 from whad.hub.events import JammedEvt
 
+logger = logging.getLogger(__name__)
 
 class Dot15d4(WhadDeviceConnector):
     """
@@ -47,6 +54,27 @@ class Dot15d4(WhadDeviceConnector):
         self.device.open()
         self.device.discover()
 
+        # Display a warning message if ButteRFly version is less than 1.1.0 as
+        # a critical bug has been found and fixed in version 1.1.0. FCS values
+        # will be wrong if using a version prior to 1.1.0.
+        if device.info.fw_url == "https://github.com/whad-team/butterfly":
+            if Version(device.info.version_str) < Version("1.1.0"):
+                message = ((
+                    "You are using a ButteRFly version prior to 1.1.0 that does not correctly compute FCS values, "
+                    "this will result in invalid FCS values in packets and PCAP files that may cause errors when "
+                    "used with other WHAD tools. Please consider upgrading firmware to the latest version "
+                    "(see https://github.com/whad-team/butterfly). "
+                    "You can also use `winstall --flash butterfly` to reprogram your USB dongle."
+                ))
+
+                # Use application warning method if available
+                app = CommandLineApp.get_instance()
+                if app is not None:
+                    app.warning(message)
+                else:
+                    # If not available, use basic logging capabilities
+                    logger.warning(message)
+
         # Check if device supports 802.15.4
         if not self.device.has_domain(Domain.Dot15d4):
             raise UnsupportedDomain("IEEE 802.15.4")
diff --git a/whad/hub/dot15d4/pdu.py b/whad/hub/dot15d4/pdu.py
index 3c663630..352600ef 100644
--- a/whad/hub/dot15d4/pdu.py
+++ b/whad/hub/dot15d4/pdu.py
@@ -56,10 +56,10 @@ class SendRawPdu(PbMessageWrapper):
         """Convert message to the corresponding scapy packet
         """
         try:
-            return Dot15d4FCS(self.pdu + bytes(pack('>H', self.fcs)))
+            return Dot15d4FCS(self.pdu + bytes(pack('<H', self.fcs)))
         except StructError:
             logger.debug("[hub::dot15d4] error parsing 802.15.4 frame (%s)",
-                         bytes(self.pdu).hex() + bytes(pack(">H", self.fcs)).hex())
+                         bytes(self.pdu).hex() + bytes(pack("<H", self.fcs)).hex())
             return None
 
     @staticmethod
@@ -170,7 +170,7 @@ class RawPduReceived(PbMessageWrapper):
         try:
             # Create packet
             #print('converting %s' % (self.pdu + bytes(pack(">H", self.fcs))).hex())
-            packet = Dot15d4FCS(bytes(self.pdu) + bytes(pack(">H", self.fcs)))
+            packet = Dot15d4FCS(bytes(self.pdu) + bytes(pack("<H", self.fcs)))
 
             # Set packet metadata
             packet.metadata = Dot15d4Metadata()
@@ -188,7 +188,7 @@ class RawPduReceived(PbMessageWrapper):
             return packet
         except StructError:
             logger.debug("[hub::dot15d4] error parsing 802.15.4 frame (%s)",
-                         bytes(self.pdu).hex() + bytes(pack(">H", self.fcs)).hex())
+                         bytes(self.pdu).hex() + bytes(pack("<H", self.fcs)).hex())
             return None
 
     @staticmethod
diff --git a/whad/tools/winject.py b/whad/tools/winject.py
index 42cb09bc..34129cf6 100644
--- a/whad/tools/winject.py
+++ b/whad/tools/winject.py
@@ -13,11 +13,10 @@ from argparse import ArgumentParser
 
 # Import every possible scapy layer for packet crafting
 # pylint: disable-next=wildcard-import
-from scapy.layers import *
-from scapy.layers.bluetooth4LE import BTLE_RF, BTLE
+from scapy.layers.all import *
 
 # Import scapy helpers
-from scapy.packet import Packet
+from scapy.packet import Packet, Raw
 from scapy.config import conf
 from scapy.themes import BrightTheme
 
@@ -84,6 +83,8 @@ class WhadInjectApp(CommandLineApp):
         self.add_argument(
             '-d',
             '--delay',
+            type=float,
+            default='1.0',
             dest='delay',
             help="Delay between the transmission of two consecutive packets"
         )
@@ -203,23 +204,26 @@ class WhadInjectApp(CommandLineApp):
         self.provided_count = 0
         if hasattr(self.args, "packet"):
             for p in self.args.packet:
-                try:
-                    # pylint: disable-next=eval-used
-                    pkt = eval(p)
-                    self._input_queue.put(pkt)
-                    self.provided_count += 1
-                except SyntaxError:
-                    error(f"Invalid syntax: `{p}`")
-                except Exception:
-                    # If hexadecimal only, try to interpret as bytes
-                    if all(c in string.hexdigits for c in p):
+                # Is it some hex
+                if all(c in string.hexdigits for c in p):
+                    if len(p) % 2 == 0:
                         try:
                             pkt = bytes.fromhex(p)
                             self._input_queue.put(pkt)
                             self.provided_count += 1
                         except Exception as err:
-                            error(f"Failure during packet interpretation: {p} -> {err}")
+                            error(f"Failure during raw packet decoding: {p} -> {err}")
                     else:
+                        error("Raw packets must be provided in valid hexadecimal form")
+                else:
+                    try:
+                        # pylint: disable-next=eval-used
+                        pkt = eval(p)
+                        self._input_queue.put(pkt)
+                        self.provided_count += 1
+                    except SyntaxError:
+                        error(f"Invalid syntax: `{p}`")
+                    except Exception as err:
                         error(f"Failure during packet interpretation: {p} -> {err}")
 
     def pre_run(self):


Test patch:
diff --git a/tests/protocol/dot15d4/test_dot15d4_packets.py b/tests/protocol/dot15d4/test_dot15d4_packets.py
index 70a2adc1..97a5ccf8 100644
--- a/tests/protocol/dot15d4/test_dot15d4_packets.py
+++ b/tests/protocol/dot15d4/test_dot15d4_packets.py
@@ -60,4 +60,4 @@ def test_raw_pdu_recv(factory):
     assert packet.metadata.timestamp == 1234
     assert packet.metadata.is_fcs_valid == True
     assert packet.metadata.lqi == 120
-    assert bytes(packet) == b"FOOBAR" + b'\x11\x22'
+    assert bytes(packet) == b"FOOBAR" + b'\x22\x11'