Emitters and Receivers¶
One of the biggest problem in order to write offensive security tools for IoT is to deal with multiple hardware components. For example, you can communicate with a Bluetooth Low Energy device using an HCI dongle, but there are also a lot of sniffers available (such as the implementation of BTLEJack on the BBC Micro:Bit or the Ubertooth One). There is one main problem : every hardware component implements some specific features and provides a different API, leading the security analyst to learn a lot of technical informations that are not directly linked to the protocol itself.
Mirage tries to solve this problem by providing an user-friendly API in order to manipulate the protocol stacks. In order to harmonize the use of multiple hardware components, it provides only two main software components allowing to easily manipulate them from the modules : the
Generally, a specific hardware component is able to send datas and/or receive datas, but it can also provide some specific behaviour, such as address or channel modification. Mirage provides a generic communication architecture based on three components :
Device(mirage.libs.wireless_utils.device.Device) : this class implements the communication with the hardware component itself. It exposes four main methods :
init : this method allows to initialize the hardware component
isUp : this method returns a boolean indicating if the hardware component is up and initialized
send : this method sends a frame (if possible)
recv : this method returns a received frame (if possible)
The devices manipulates only the frames formatted as needed by the hardware component (e.g. bytes array or scapy frames). If a specific hardware component provides additional features, they can be implemented easily as standard methods, and if the method’s name is appended to the array
sharedMethods(which is a class attribute of
Device), the method is available from the module environment.
Emitter(mirage.libs.wireless.Emitter) : this class is linked to the protocol itself. It communicates with a specific device chosen by the user according to the selected interface. Its main role is to provides an user-friendly API dedicated to sending datas, but it can also acts as a software proxy if it has instantiated a
Deviceexposing additional methods. It allows the modules developpers to get a standardized API to send frames, but also to call theses additional methods directly from the module environment. An
Emitterdoesn’t use “raw” frames, but an abstract representation of a frame, which is a child class of
Packet(mirage.libs.wireless_utils.packets.Packet) . It allows to use an abstract representation of a frame allowing to integrate some dissectors, builders, … and also to have one representation for one specific packet, even if the frame formats expected by the devices are not the same (for example, HCI Devices needs HCI frames while sniffers uses the Link Layer format). As a consequence, every
Emittermust overload the method named
convert. Its role is to transform a Mirage
Packetinto the corresponding raw frames.
Receiver(mirage.libs.wireless.Receiver) : this class is linked to the protocol itself, and similarly it communicates with the Devices according to the interface parameter. One of the most important role of a Receiver is to allow to receive datas from the devices as raw frames, to convert them into Mirage Packet, thanks to the
convertmethod (overloaded). A Receiver provides a mechanism of callbacks allowing to receive frame asynchronously (
onEvent), but multiple methods (such as
skip) allows to get the corresponding Mirage Packet synchronously. It allows to access the shared methods implemented on a specific device thanks to the implementation of the Proxy device pattern.
Using an existing Emitter or Receiver¶
Mirage provides an easy way to instantiate an Emitter or a Receiver from a module environment. Two methods allow to get the right Emitter or Receiver according to the
technology attribute of your module :
getEmitter (mirage.core.module.WirelessModule.getEmitter) and
self.receiver = self.getReceiver(interface="hci0") self.emitter = self.getEmitter(interface="hci0")
packet1 = ble.BLEReadRequest(handle=0x0021) self.emitter.sendp(packet1) packet2 = ble.BLEWriteCommand(handle=0x0025,value=b"\x01\x02\x03\x04") self.emitter.sendp(packet1,packet2)
Two solutions exists if you want to receive a frame from a packet :
you can easily set a callback on a specific event (e.g. the reception of a specific type of packet), using the
onEventmethod. This method allows to receive packets asynchronously, by linking a callback function to an event, which is a string indicating when should the callback be called. Three formats exists describing an event :
* : indicating “the callback is called every times a packet is received”
n : indicating “the callback is called every times n packets have been received”
packetType : indicating “the callback is called every times a packet of type ‘packetType’ is received”
Some examples are represented in the following table:
every 3 packets
every BLE Read Requestdef show(self,packet): packet.show() def onReadRequest(self,packet,username): io.info("Hello "+username+", I have an incoming Read Request for you : "+str(packet)) def run(self): self.receiver = self.getReceiver(interface="hci0") self.receiver.onEvent("*", callback=self.show) receiver.onEvent("BLEReadRequest",callback=self.onReadRequest, args=["Romain"])
- packet = self.receiver.next(timeout=1.0) if packet is not None: packet.show() else: io.info("Timeout !") for packet in self.receiver.receive(nb=5): packet.show()
Finally, you can use the shared methods of the currently instantiated
Device from a module environment thanks to the implementation of the Proxy design pattern. The
hasCapabilities (mirage.libs.wireless_utils.device.Device.hasCapabilities) method allows to easily check if the currently instantiated device is able to perform a specific action.
Please refer to the corresponding documentation to discover the available shared methods.
if self.receiver.hasCapabilities("SCANNING"): self.receiver.setScan(enable=True,passive=False) if self.emitter.hasCapabilities("SCANNING"): self.emitter.setScan(enable=True,passive=True)
Integrating a new protocol¶
One of the main advantages of Mirage is that a new protocol can be easily added. First, the developer has to implement a new child class of
Device (mirage.libs.wireless_utils.device.Device), allowing interaction with a given specific hardware. This class should provide only four main methods:
init : initializing the hardware component
isUp : indicating if the hardware can be used
recv : allowing to receive frames
send : allowing to transmit frames
As a result, developing a driver is straightforward.
Some specific features can also be added by creating a new method and appending its name to the array
sharedMethods (which is a class attribute of every child classes of
Device, allowing them to be called from a module environment. If you want to provide the capabilities of your device, just use the instance attribute
capabilities (array of string indicating the available capabilities.
At least one of the two child classes of
Receiver (mirage.libs.wireless.Receiver) or
Emitter (mirage.libs.wireless.Emitter) must be implemented. They initialize the device previously defined in their constructor and must implement the convert method, allowing to convert a binary frame into an abstraction or an abstract representation of a frame into an array of bytes. Of course, it needs you to implement the available packets as child classes of
Finally, you must register your Emitter and/or your Receiver, using the
registerEmitter (mirage.core.module.WirelessModule.registerEmitter) and
registerReceiver (mirage.core.module.WirelessModule.registerReceiver) class methods. It allows to link your Emitter and Receiver to a specific technology, allowing to use the
getReceiver methods from a module environment to get the right Emitter / Receiver instance according to the
technology attribute. For example, the
BLEReceiver are linked to the technology “ble” using the following code :
As a result, the protocol is fully integrated and can be used from modules.