Added an article
All checks were successful
Build and Publish / build (push) Successful in 1m53s

This commit is contained in:
Nicola Belluti 2024-05-01 12:54:36 +02:00
parent b3f83117b5
commit 0d71d72454
10 changed files with 696 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -0,0 +1,358 @@
+++
title = "Studiare un protocollo di comunicazione"
summary = "Step 2: usare uno squalo per sniffare pacchetti"
date = "2024-05-01"
tags = ["Reverse Engineering", "Lettore di Presenze", "TCP", "Sniffing", "Wireshark"]
categories = ["Progetti"]
series = ["Lettore di presenze"]
series_order = 2
+++
Nell'articolo precedente abbiamo iniziato a studiare come funziona il client
del lettore di presenze, andando anche a decompilarne l'eseguibile. In questo
articolo vorrei cercare di comprendere il protocollo di comunicazione che il
client usa per interfacciarsi col lettore.
I motivi per cui non ho fatto subito il reverse engineering del protocollo sono
sostanzialmente due:
1. Se fossi riuscito a decompilare correttamente il codice dell'eseguibile
avrei potuto creare un client alternativo con molta più semplicità;
2. A volte non si può eseguire (almeno in modo semplice) lo *sniffing* di una
comunicazione per via del
[TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security).
Purtroppo però la decompilazione dei DLL non è affatto facile perchè:
> Non esiste un pulsante magico per tornare indietro, ne esiste uno per
> generare codice C di merda con nomi di variabili a caso, ma non è un bel
> bottone
>
> Traduzione di **fasterthanlime** nel video [How does the detour crate
> work?](https://youtube.com/watch?v=aLeMCUXFJwY&t=174s)
Nel caso foste interessati, l'NSA ha sviluppato il suo decompilatore chiamato
[Ghidra](https://ghidra-sre.org/); vi consiglio di darci un occhiata.
## Configurazione del client
Nello scorso articolo abbiamo solo installato il client per Windows, ma non lo
abbiamo mai aperto.
Dato che per intercettare la comunicazione dobbiamo avere un client che vada
effettivamente ad interrogare il lettore, riapro la mia macchina virtuale con
[Windows 10 AME](https://archive.org/details/windows10-ame-21h1-2021-08-09/) e
finisco la configurazione del client:
{{< carousel images="images/01-client-setup/*" aspectRatio="16-9"
interval="1000" >}}
Una volta finita la configurazione (e aver modificato qualche file di
configurazione a mano perchè il client comunque non vedeva il lettore nella
rete) abbiamo la possibilità di poter richiedere le presenze tramite la rete.
Una volta aperto il client **come amministratore**, aver premuto il tasto per
scaricare i dati e aver aspettato **due lunghi minuti**, la bellezza di 3543
presenze compaiono sullo schermo.
Qualcosa mi puzza: perchè impiega due minuti a trasferire l'equivalente di un
file che pesa poco meno di 200kiB?
Facendo un secondo due calcoli:
{{< katex >}}
$$
\frac{3543\ \textrm{righe}}{120\ \textrm{secondi}} \ \cdot\sim460\ \textrm{bit
per riga} = 13.26\ kib/s
$$
13kibps di throughput utile su una connessione da 100Mbps? ***Che schifo!***
Non voglio sapere quale disastro di programmazione aziendale italiana può aver
causato questo, ma ho come l'impressione che sto per scoprirlo...
## *The quieter you become...*
Come analizzatore di reti andrò ad installare ed utilizzare
[Wireshark](https://wireshark.org), uno strumento molto popolare per questo
tipo di operazioni.
Una volta installato ed aver aggiunto il nostro utente al gruppo `wireshark`
possiamo avviarlo ed incominciare a *sniffare* tutti i pacchetti sulla nostra
interfaccia di rete.
![Wireshark in funzionamento](images/02-wireshark-working.png "Ecco Wireshark
mentre ascolta tutti i pacchetti che circolano sulla mia rete")
Se è la prima volta che usate uno strumento del genere, potrete accorgervi che
anche in una rete locale di piccole dimensioni circolano davvero tanti
pacchetti, troppi per essere analizzati uno ad uno.
È proprio qui che entrano in nostro soccorso i filtri: se digitiamo nella barra
dei filtri la seguente stringa:
```
ip.addr == <IP del dispositivo>
```
Vedremmo solo i pacchetti che provengono *da* o sono diretti *verso*
l'indirizzo IP che abbiamo specificato. Possiamo anche decidere di filtrare il
traffico che passa attraverso una specifica porta TCP con:
```
ip.addr == <IP> && tcp.port == <Porta>
```
Il mondo dei filtri in Wireshark è molto vasto, lascio il [link alla
documentazione ufficiale](https://wiki.wireshark.org/DisplayFilters) per gli
interessati.
Una volta fatta partire la registrazione con i filtri corretti possiamo far
ripartire un'altra scansione completa delle presenze sul client ufficiale e
dovremmo vedere tutti i pacchetti che si scambiano client e dispositivo in
tempo reale.
![Wireshark con il filtro per IP](images/03-wireshark-with-filter.png "I
pacchetti scambiati fra il client ed il dispositivo")
Alla fine del processo abbiamo registrato la bellezza di 14423 pacchetti, che
trasportano 3543 presenze. *Il tutto diventa ancora più strano...*
Dando un'occhiata veloce al traffico possiamo intuire un po' di cose:
1. Nel layer di trasporto viene usato il protocollo TCP sulla porta `5005`
2. Non viene utilizzato
[TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) per
crittografare i dati, *phew*
3. Ci sono almeno tre fasi:
* Una prima fase di inizializzazione;
* Una seconda fase di scambio di dati nella quale vengono inviati pochi
pacchetti ma molto grandi;
* Una terza fase, nella quale vengono inviati moltissimi pacchetti di
piccole dimensioni, dove si può intravedere di tanto in tanto il nome dei
dipendenti in formato ASCII.
![Utente "test" nella finestrella ASCII](images/04-test-name.png "Ecco che
compare un nome familiare nella finestrella ASCII, in basso a destra")
Per studiare più approfonditamente il protocollo avremmo bisogno del solo
contenuto dei pacchetti TCP. Qui ci aiuta Wireshark con una funzionalità molto
utile.
Se infatti andiamo a selezionare un pacchetto della comunicazione TCP a cui
siamo interessati e premiamo il tasto destro, selezionando `Follow` > `TCP
Stream`, Wireshark aprirà in automatico il payload di tutti i pacchetti e ci
mostrerà solo il traffico di livello 7.
Se andiamo a visualizzare i dati come `Raw`, Wireshark ci mostrerà i dati
scambiati in formato esadecimale, mostrando in rosso i messaggi inviati dal
client ed in blu le risposte date dal lettore di presenze.
Adesso possiamo copiare le richieste nel nostro text editor di fiducia ed
incominciare a studiare il protocollo.
![Lo stream TCP mostrato da Wireshark](images/05-wireshark-tcp-stream.png "Ecco
come appare lo scambio di messaggi quando si vanno ad aprire solo i pacchetti
TCP")
## *Fuck around and find out*
Ora non rimane che comprendere il protocollo di comunicazione che,
sfortunatamente, non è in un formato testuale come l'ASCII o l'UTF-8.
Può sembrare complesso, ma mi ci è voluto solo un pomeriggio per trovare una
soluzione abbastanza completa per quello che devo fare.
### Richieste
Le richieste inviate dal client sono tutte lunghe 16 byte e hanno questa
struttura:
```rexeg
^55aa([0-9a-f]{24})([0-9a-f]{4})$
```
* I primi due byte sono sempre `55 aa` (`01010101 10101010` in binario);
* I 12 byte successivi specificano il comando del client, che d'ora in poi
chiamerò "payload";
* Infine, ci sono due byte **little-endian** che specificano il numero del
pacchetto, iniziando da `00 00`.
Ho notato anche che il server non verifica che gli ultimi due byte siano
inviati in modo sequenziale, quindi possono rimanere a `00 00` per tutto lo
scambio di messaggi.
### Risposte
Le risposte del server invece non hanno una lunghezza fissa e sono divise in
due parti, che d'ora in poi andrò a chiamare "header" ed "payload". L'header è
sempre presente ed è lungo 10 byte, mentre il payload può anche essere assente.
Quando il payload è assente il messaggio si comporta come una specie di
`null`/`ACK`.
```regex
^aa55([0-9a-f]{16})(?:55aa([0-9a-f]+))?$
```
* I primi due byte sono sempre `aa 55` (`10101010 01010101` in binario);
* Gli 8 byte successivi sono l'header. Di solito sono `01 01 00 00 00 00 00
00`, ma possono cambiare;
* Se è presente un payload, il messaggio continua con `55 aa` (`01010101
10101010` in binario);
* I byte rimanenti rappresentano il payload.
---
### Ping
Se vogliamo eseguire un "ping" e verificare che il server risponda possiamo
inviare una richiesta col payload impostato a `01 80 00 00 00 00 00 00 00 00 00
00`:
```
55aa0180000000000000000000000100
aa550101000000000000
```
Il server risponderà poi con un pacchetto senza payload con l'header impostato
a `01 01 00 00 00 00 00 00`.
### Nome del dipendente
Sapendo l'ID di un dipendente, è possibile richiedere al server il suo nome
tramite una richiesta con payload impostato a `01 c7 xx xx xx xx 00 00 00 00 14
00`, dove `xx xx xx xx` è un intero a 32 bit **little-endian** che rappresenta
l'ID del dipendente.
```
55aa01c7xxxxxxxx0000000014000100
aa55010100000000000055aaxxxxxxxxxxxxxxxxxxxx4c0000000000595a7c7c0000
```
I primi 10 bit del payload sono il nome del dipendente, in caso il nome sia
più corto di 10 caratteri lo spazio rimanente sarà riempito con dei caratteri
terminatori `\0`.
Questi messaggi compongono quasi la totailtà della terza fase che ho descritto
nel capitolo precedente, quella nella quale ci sono tanti piccoli messaggi.
Questo fa intuire che il client che prima di tutto fa il dump delle presenze in
modo quasi istantaneo , poi aspetta due minuti scaricando **per ogni presenza
rilevata** il nome del dipendente, anche se questo è già stato richiesto in
precedenza. Qualcuno insegni il concetto di
[memoizzazione](https://it.wikipedia.org/wiki/Memoizzazione) a questi
informatici...
### Numero totale di presenze
Per chiedere quante presenze sono registrate sul dispositivo bisogna effettuare
una richiesta con payload `01 b4 08 00 00 00 00 00 ff ff 00 00`:
```
55aa01b4080000000000ffff00000100
aa550101xxxx00000000
```
Dove `xx xx` sarà il numero delle presenze salvate rappesentato in un intero a
16 bit **little-endian**.
65535 richieste massime sembrano un po' troppo poche, ma immagino che sarà un
problema del me del futuro.
### Scaricamento di tutte le presenze
La lista di tutte le presenze va scaricata a blocchi, continuando a chiedere al
server dei blocchi da 1024 byte (ovvero 85,333 presenze alla volta) finchè non
viene estratto il tutto.
Per fare ciò dobbiamo prima di tutto richiedere il numero totale delle
presenze, poi dobbiamo inviare una richiesta con payload `01 a4 00 00 00 00 xx
xx 00 00 00 04`, dove `xx xx` è il numero delle presenze totali
**little-endian**.
```
55aa01a400000000xxxx000000040100
aa55010100000000000055aa ...
```
Il server ci risponderà con un payload da 1026 byte, contenente le prime
registrazioni seguite da due byte a zero.
Possiamo richiedere un altro blocco da 1026 byte inviando una richiesta con
payload `01 a4 00 00 00 00 00 00 xx xx 00 04`, dove `xx xx` è un intero
**little-endian** che parte da `01 00`:
```
55aa01a4000000000000010000040100
aa55010100000000000055aa ...
```
Una volta finite le registrazioni il server incomincerà ad inviare dei byte di
padding impostati a `ff` per arrivare a 1026 byte di payload.
### Struttura delle presenze
Una volta ottenuti tutti i blocchi di registrazioni, possiamo andarli a
scomporre in singole registrazioni da 12 byte ciascuno. Non sono riuscito a
comprendere per cosa stessero tutti i singoli byte, ma quelli importanti sono
questi:
```regex
..([26ae]).{5}([0-9a-f]{8})([0-9a-f]{8})
```
* Del secondo byte ci interessano i due bit più significativi per capire se la
registrazione rappresenta un'entrata o un'uscita:
* Se sono `00` allora la registrazione è la prima entrata;
* Se sono `01` allora la registrazione è la prima uscita;
* Se sono `10` allora la registrazione è la seconda entrata;
* Se sono `11` allora la registrazione è la seconda uscita;
* I penultimi quattro byte rappresentano l'ID del dipendende (in
little-endian);
* Gli ultimi 4 byte rappresentano la data e l'ora della presenza (in
little-endian).
All'inizio pensavo che la data fosse rappresentata come una UNIX Epoch, invece
a quanto pare ha questo formato (quando rappresentato come big-endian):
* I primi 6 bit rappresentano i minuti;
* I 5 bit successivi rappresentano le ore;
* I 5 bit successivi rappresentano i giorni;
* I 4 bit successivi rappresentano i mesi;
* Infine, gli ultimi 12 bit rappresentano gli anni.
---
Sospetto inoltre che nei primi quattro byte di ogni presenza siano presenti
anche:
* I secondi;
* La modalità di registrazione (se con il PIN, con l'impronta o col badge);
* L'ID del registratore.
Ma dato che non sono campi molto importanti ho deciso che per il momento li
lascerò perdere.
## Provare col terminale
Se volessimo provare la comunicazione senza dover scrivere alcun programma che
invia byte su un socket TCP, possiamo utilizzare un po' di utils di sistema
come `netcat` e `xxd`:
```shell
# Se usate Bash o Zsh
function send_bytes { echo -n "$3" | xxd -r -p | timeout 1 nc "$1" "$2" | xxd; }
# Oppure, se state usate Fish
function send_bytes -a ip porta dati
echo -n "$dati" | xxd -r -p | timeout 1 nc "$ip" "$porta" | xxd
end
send_bytes 127.0.0.1 5005 55aa0180000000000000000000000100
```
Provando qualche comando preso dagli esempi sopra posso confermare che tutto
sembra funzionare correttamente. Nel prossimo articolo vedremo come creare una
piccola libreria in Rust per ricavare i dati dal lettore.

View File

@ -0,0 +1,338 @@
+++
title = "Studying a communication protocol"
summary = "Step 2: Using a shark to sniff packets"
date = "2024-05-01"
tags = ["Reverse Engineering", "Attendance Reader", "TCP", "Sniffing", "Wireshark"]
categories = ["Projects"]
series = ["Attendance Reader"]
series_order = 2
+++
In the previous article, we started studying how the attendance reader client
works, we even attempted to decompile its executable. In this article, I'd like
to explore the communication protocol that the client uses to talk to the
reader.
There are basically two reasons why I didn't immediately reverse-engineer the
protocol:
1. If I could decompile the executable code, I could create an alternative
client much more easily;
2. Sometimes it's not possible (not easily, at least) to *sniff* a
communication 'cause of
[TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security).
However, decompiling DLLs is far from easy because:
> There's no magic "go back" button, there's a "generate shitty C code with
> random-ass variable names" button, but that's not a very good button
>
> **fasterthanlime** in the [How does the detour crate
> work?](https://www.youtube.com/watch?v=aLeMCUXFJwY&t=174s) video
If you're interested, the NSA has developed its own decompiler called
[Ghidra](https://ghidra-sre.org/), check it out.
## Client configuration
In the last article, we only installed the client for Windows but never opened
it.
Since we need a client that can actually interact with the reader to intercept
the communication, I reopened my VM with [Windows 10
AME](https://archive.org/details/windows10-ame-21h1-2021-08-09/) and finished
configuring the client:
{{< carousel images="images/01-client-setup/*" aspectRatio="16-9"
interval="1000" >}}
Once the configuration is completed (and after manually modifying some
configuration files because the client still couldn't see the reader on the
network), we can request the reader's data over the network.
After opening the client **as an administrator**, pressing the button to
download data, and waiting **two minutes**, a total of 3543 attendances
appeared on the screen.
Something's odd: why does it take two minutes to transfer the equivalent of a
file weighing just under 200 kiB?
Doing some quick math:
{{< katex >}}
$$
\frac{3543\ \textrm{lines}}{120\ \textrm{seconds}} \ \cdot\sim460\ \textrm{bit
per row} = 13.26\ kib/s
$$
13 kibps of useful throughput on a 100 Mbps connection? ***This sucks!***
I don't want to know what disaster of italian corporate coding could have
caused this, but I have a feeling Im about to find out...
## *The quieter you become...*
To analyze the network, I will use [Wireshark](https://wireshark.org), a very
popular tool for this type of operations.
After installing it and adding our user to the `wireshark` group, we can run it
and begin to *sniff* all packets on our network interface.
![Wireshark in operation](images/02-wireshark-working.png "Here's Wireshark
listening to all the packets circulating on my network.")
If this is your first time using a tool like this, you might notice that even
in a small Local Area Network there are a lot of packets flying around — too
many to analyze individually.
This is where filters come and save the day. If we type the following string
into the filter bar:
```
ip.addr == <Device's IP>
```
We will see only packets that come *from* or are directed *to* the specified IP
address. We can also filter traffic that passes through a specific TCP port
with:
```
ip.addr == <IP> && tcp.port == <Port>
```
Filters in Wireshark are a vast argument; here's a [link to the official
documentation](https://wiki.wireshark.org/DisplayFilters) for those interested.
Once we start recording with the correct filters, we can start another full
scan of attendances on the official client, and we should see the packet
exchange between the client and the device in real-time.
![Wireshark with the IP filter](images/03-wireshark-with-filter.png "The
packets exchanged between the client and the device.")
At the end of the process, we've recorded an astonishing 14,423 packets,
carrying 3,543 attendances. *Things just get stranger...*
By taking a quick look at the traffic, we can deduce a few things:
1. The transport layer uses the TCP protocol on port `5005`;
2. [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) is not used,
*phew*;
3. There are at least three phases:
* An initial setup phase;
* A second phase in which data is exchanged with a few but large packets;
* A third phase with many but small packets, where you can occasionally
observe employee names in ASCII.
!["test" user in the ASCII box](images/04-test-name.png "A familiar name
appears in the ASCII box at the bottom right.")
To study the protocol in more depth, we'd need only the content of the TCP
packets. This is where Wireshar comes in handy.
If we select a packet from the TCP communication we're interested in and
right-click, selecting `Follow` > `TCP Stream`, Wireshark will automatically
open the payload of all packets and show only the level-7 traffic.
If we view the data as `Raw`, Wireshark will display the exchanged data in
hexadecimal format, with messages sent by the client in red and responses from
the attendance reader in blue.
Now we can copy the payloads into our preferred text editor and start to study
the protocol.
![The TCP stream shown by Wireshark](images/05-wireshark-tcp-stream.png "This
is what the message exchange looks like when we open the TCP packets.")
## Fuck around and find out
Now we just need to understand the communication protocol, which,
unfortunately, isn't in a text-based format like ASCII or UTF-8.
It may seem complex, but it only took me an afternoon to find a comprehensive
enough solution for what I need to do.
### Requests
Client requests are all 16 bytes long and have this structure:
```regex
^55aa([0-9a-f]{24})([0-9a-f]{4})$
```
* The first two bytes are always `55 aa` (`01010101 10101010` in binary);
* The next 12 bytes specify the client command. I will call them "payload" from
now on;
* Finally, there are two **little-endian** bytes indicating the packet number,
starting from `00 00`.
I noticed that the server doesn't check if the last two bytes are sent
sequentially, so they can remain at `00 00` throughout the message exchange.
### Responses
Server responses do not have a fixed length and are divided into two parts,
which I will call "header" and "payload." The header is always present and is
10 bytes long, while the payload can be absent.
When there's no payload, the message acts like a kind of `null`/`ACK`.
```regex
^aa55([0-9a-f]{16})(?:55aa([0-9a-f]+))?$
```
* The first two bytes are always `aa 55` (`10101010 01010101` in binary);
* The following eight bytes are the header. Usually, they are `01 01 00 00 00
00 00 00`, but they can change;
* If a payload is present, the message continues with `55 aa` (`01010101
10101010` in binary);
* The remaining bytes are the payload.
---
### Ping
If we want to perform a "ping" and check if the server responds, we can send a
request with the payload set to `01 80 00 00 00 00 00 00 00 00 00 00`:
```
55aa0180000000000000000000000100
aa550101000000000000
```
The server will then respond with a packet without a payload and the header set
to `01 01 00 00 00 00 00 00`.
### Employee uame
Knowing the ID of an employee, it's possible to ask the server for their name
by sending a request with a payload set to `01 c7 xx xx xx xx 00 00 00 00 14
00`, where `xx xx xx xx` is a 32-bit **little-endian** integer representing the
employee ID.
```
55aa01c7xxxxxxxx0000000014000100
aa55010100000000000055aaxxxxxxxxxxxxxxxxxxxx4c0000000000595a7c7c0000
```
The first 10 bits of the payload contain the employee's name; if it's shorter
than 10 characters, the remaining space will be filled with null terminators
(`\0`).
These messages comprise almost the entirety of the third phase I described in
the last chapter, the one with many but small messages. This suggests that the
client quickly dumps the attendance data, then spends two whole minutes
downloading the employee's name **for each attendance**, even if it's been
requested before. Someone should teach these developers the concept of
[memoization](https://en.wikipedia.org/wiki/Memoization)...
### Total number of records
To ask for the total number of attendances registered on the device, you need
to send a request with a payload of `01 b4 08 00 00 00 00 00 ff ff 00 00`:
```
55aa01b4080000000000ffff00000100
aa550101xxxx00000000
```
Where `xx xx` is the number of saved attendances represented as a 16-bit
**little-endian** integer.
65535 maximum requests seem a bit too few, but I guess it's a future-me
problem.
### Downloading all records
The list of all attendances must be downloaded in blocks, continuing to request
1024-byte blocks from the server (approximately 85.333 attendances at a time)
until the entire list is extracted.
To do this, we first have to request the total number of attendances, then send
a request with a payload of `01 a4 00 00 00 00 xx xx 00 00 00 04`, where `xx
xx` is the total number of attendances in **little-endian**.
```
55aa01a400000000xxxx000000040100
aa55010100000000000055aa ...
```
The server will respond with a 1026-byte payload, containing the initial
records followed by two zero bytes.
We can request another 1026-byte block by sending a request with a payload of
`01 a4 00 00 00 00 00 00 xx xx 00 04`, where `xx xx` is a **little-endian**
integer starting from `01 00`:
```
55aa01a4000000000000010000040100
aa55010100000000000055aa ...
```
Once the records are finished, the server will start sending padding bytes set
to `ff` to reach 1026-byte.
### Record structure
Once we have all the registration blocks, we can break them down into
individual registrations, each one 12 bytes long. I wasn't able to
understand what all the bytes represent, but the important ones are:
```regex
..([26ae]).{5}([0-9a-f]{8})([0-9a-f]{8})
```
* The second byte's two most significant bits indicate if the registration
represents an entry or an exit:
* If it's `00`, it's the first entry;
* If it's `01`, it's the first exit;
* If it's `10`, it's the second entry;
* If it's `11`, it's the second exit;
* The second-to-last four bytes represent the employee ID (in
**little-endian**);
* The last four bytes represent the date and time of the attendance (in
**little-endian**).
Initially I thought the date was represented as a UNIX Epoch, but it seems to
have this format when shown as big-endian:
* The first 6 bits represent the minutes;
* The next 5 bits represent the hours;
* The next 5 bits represent the days;
* The next 4 bits represent the months;
* The last 12 bits represent the years.
---
I suspect that the first four bytes of each attendance may contain:
* The seconds;
* The recording method (if the employee checked-in with the PIN, fingerprint,
or the badge);
* The recorder ID.
But since these aren't very important fields, I've decided to ignore them for
now.
## Testing using the terminal
If you want to test communication without writing any program that sends bytes
over a TCP socket, you can use some basic core utilities like `netcat` and
`xxd`:
```shell
# If you're using Bash or Zsh
function send_bytes { echo -n "$3" | xxd -r -p | timeout 1 nc "$1" "$2" | xxd; }
# If you're using Fish
function send_bytes -a ip port data
echo -n "$data" | xxd -r -p | timeout 1 nc "$ip" "$port" | xxd
end
send_bytes 127.0.0.1 5005 55aa0180000000000000000000000100
```
Trying some requests from the examples above, I can confirm everything seems to
work correctly. In the next article, we'll see how to create a small Rust
library to extract data from the reader.