-
Writeup Hack-A-Sat : IQ
Description du challenge : Convert the provided series of transmit bits into in-phase quadrature samples.
Le challenge est disponible à cette adresse.
Connectons-nous à l’instance du challenge :
On nous demande de convertir une série de bits en un échantillon I/Q en modulation QPSK (Quadrature Phase Shift Keying).
Modulation QPSK
La modulation QPSK est une modulation de phase numérique où l’on va modifier la phase de la porteuse pour transmettre des données. Tu peux retrouver un cours sur ce qu’est la modulation juste là et sur la phase juste ici.
Concrètement, pour cette modulation, les bits sont regroupés par paires donc on a 4 combinaisons possibles : 00, 01,10,11 et ainsi 4 phases possibles. En général, chacune d’entre elles est espacée de 90°. Par exemple, on pourrait avoir 00 à 0°, 01 à 90°, 11 à 180° et 10 à 270°.
On peut aussi le voir sur ce schéma avec d’autres valeurs mais qui restent espacées de 90° :
Échantillon IQ
I (In-phase) et Q (Quadrature) sont les deux composantes orthogonales du signal modulé. Elles représentent respectivement les parties cosinus et sinus de l’onde porteuse modulée.
Si on décompose le signal avec ces deux valeurs, on peut représenter le signal modulé comme un point dans un plan à deux dimensions appelé diagramme de constellation.
Un échantillon I/Q permet ainsi de représenter les valeurs numériques des deux composantes du signal modulé. Cela nous permet de représenter la modulation en phase du signal comme pour le QPSK.
Résolution
Pour la résolution du challenge, on va faire un script en Python. Sachant qu’en QPSK, les bits sont codés par paire, on va commencer par cette étape :
{% highlight py %}
bits = “01000011 01110010 01101111 01101101 01110101 01101100 01100101 01101110 01110100 00001010”
bits_pair = []
i = 0
while i < len(bits):
if bits[i] == “ “: # Pour ignorer les espaces de notre variable bits (Oui, on aurait pu les enlever direct manuellement mais c’est pour que ça reste propre)
i += 1
pair = bits[i:i+2]
bits_pair.append(pair)
i += 2
print(bits_pair) # [‘01’, ‘00’, ‘00’, ‘11’, ‘01’, ‘11’, ‘00’, ‘10’, ‘01’, ‘10’, ‘11’, ‘11’, ‘01’, ‘10’, ‘11’, ‘01’, ‘01’, ‘11’, ‘01’, ‘01’, ‘01’, ‘10’, ‘11’, ‘00’, ‘01’, ‘10’, ‘01’, ‘01’, ‘01’, ‘10’, ‘11’, ‘10’, ‘01’, ‘11’, ‘01’, ‘00’, ‘00’, ‘00’, ‘10’, ‘10’]
{% endhighlight %}
Chaque combinaison de bits est mappée à un point spécifique dans le plan I/Q où I (In-phase) est l’axe horizontal et Q (Quadrature) l’axe vertical.
Par rapport au diagramme qui nous est donné, on peut déduire la façon de coder nos bits :
Ainsi, on peut faire une table de mappage en utilisant un dictionnaire et sortir une liste iq_samples qui contient nos bits mappés :
{% highlight py %}
bits_map = {
“00”: “-1.0 -1.0”,
“01”: “-1.0 1.0”,
“10”: “1.0 -1.0”,
“11”: “1.0 1.0”
}
iq_samples = []
for pair in bits_pair:
iq_value = bits_map.get(pair)
iq_samples.append(iq_value)
print(iq_samples) # [‘-1.0 1.0’, ‘-1.0 -1.0’, ‘-1.0 -1.0’, ‘1.0 1.0’, ‘-1.0 1.0’, ‘1.0 1.0’, ‘-1.0 -1.0’, ‘1.0 -1.0’, ‘-1.0 1.0’, ‘1.0 -1.0’, ‘1.0 1.0’, ‘1.0 1.0’, ‘-1.0 1.0’, ‘1.0 -1.0’, ‘1.0 1.0’, ‘-1.0 1.0’, ‘-1.0 1.0’, ‘1.0 1.0’, ‘-1.0 1.0’, ‘-1.0 1.0’, ‘-1.0 1.0’, ‘1.0 -1.0’, ‘1.0 1.0’, ‘-1.0 -1.0’, ‘-1.0 1.0’, ‘1.0 -1.0’, ‘-1.0 1.0’, ‘-1.0 1.0’, ‘-1.0 1.0’, ‘1.0 -1.0’, ‘1.0 1.0’, ‘1.0 -1.0’, ‘-1.0 1.0’, ‘1.0 1.0’, ‘-1.0 1.0’, ‘-1.0 -1.0’, ‘-1.0 -1.0’, ‘-1.0 -1.0’, ‘1.0 -1.0’, ‘1.0 -1.0’]
{% endhighlight %}
Et enfin, on peut construire notre échantillon QPSK :
{% highlight py %}
qpsk = ‘ ‘.join(iq_samples)
print(qpsk)
{% endhighlight %}
Plus qu’à lancer notre script en entier et on obtient ça :
python .\iq.py
-1.0 1.0 -1.0 -1.0 -1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 1.0 -1.0 -1.0 1.0 -1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 -1.0 1.0 1.0 1.0 -1.0 1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 -1.0 -1.0 -1.0 1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 1.0 -1.0 -1.0 1.0 1.0 1.0 -1.0 1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0 -1.0 1.0 -1.0
On le copie colle et on obtient le flag :)
> docker run --rm -i -e FLAG=pouet iq:challenge
[...]
Input samples: -1.0 1.0 -1.0 -1.0 -1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 1.0 -1.0 -1.0 1.0 -1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 -1.0 1.0 1.0 1.0 -1.0 1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 -1.0 -1.0 -1.0 1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0 1.0 -1.0 -1.0 1.0 1.0 1.0 -1.0 1.0 -1.0 -1.0 -1.0 -1.0 -1.0 -1.0 1.0 -1.0 1.0 -1.0
You got it! Here's your flag:
pouet
-
Writeup Hack-A-Sat : Fiddlin' John Carson
Description du challenge : Your spacecraft has provided its Cartesian ICRF position (km) and velocity (km/s). What is its orbit (expressed as Keplerian elements)?
Le challenge est disponible à cette adresse
Connectons-nous à l’instance du challenge :
> docker run --rm -i -e FLAG=pouet kepler:challenge
KEPLER
CHALLANGE
a e i Ω ω υ
. .
,' ,=, .
, / \ .
. | | .
. \ / .
+ '=' .
. .'
. . '
'
Your spacecraft reports that its Cartesian ICRF position (km) and velocity (km/s) are:
Pos (km): [8449.401305, 9125.794363, -17.461357]
Vel (km/s): [-1.419072, 6.780149, 0.002865]
Time: 2021-06-26-19:20:00.000-UTC
What is its orbit (expressed as Keplerian elements a, e, i, Ω, ω, and υ)?
Semimajor axis, a (km):
On va devoir à partir d’une position cartésienne et d’une vitesse retrouver les paramètres d’orbite de notre vaisseau spatial.
Il est important de comprendre de quoi on parle quand on évoque les paramètres d’orbite. Heureusement pour toi, j’ai fait un petit cours que tu peux consuler juste ici :)
Voici un petit schema de ce que représente les différentes données qu’on a (le placement du vaisseau est arbitraire, il ne correspond pas à ses coordonnées réelles, flemme de faire un truc en 3D) :
Bon, en vrai, les calculs pour trouver les paramètres d’orbite sont plutôt compliqués, donc on va juste apprendre à utiliser la bibliothèque Python poliastro pour arriver à nos fins.
Dans un premier temps, on va convertir nos listes qui contiennent les coordonnées de position et de vitesse en objets avec des unités physiques appropriées pour faire des calculs avec.
{% highlight py %}
from poliastro.twobody.orbit import u
pos = [8449.401305, 9125.794363, -17.461357]
vel = [-1.419072, 6.780149, 0.002865]
pos_km = [pos] * u.km
vel_kms = [vel] * u.km / u.s
{% endhighlight %}
Le u.km, c’est une unité de mesure qui provient de la bibliothèque astropy. En multipliant notre liste avec, on convertit chaque coordonnée en objet Quantity. Ainsi, les coordonnées x, y et z sont désormais traitées comme ayant pour unité le km ce qui va nous permettre de faire des calculs physiques avec.
Pareil pour le u.s qui représente des secondes et donc u.km / u.s “transforme” notre liste de vitesse avec comme unité le km/s.
Ensuite, et c’est là que la magie se fait, on a juste à appeler la méthode Orbit.from_vectors qui va s’occuper de faire tous les calculs pour nous afin qu’on puisse récupérer tout ce dont on a besoin.
{% highlight py %}
from poliastro.bodies import Earth
from poliastro.twobody import Orbit
time = “2021-06-26 19:20:00.000”
orb = Orbit.from_vectors(Earth, pos_km, vel_kms, time)
{% endhighlight %}
Earth, c’est juste un objet qui contient les paramètres gravitationnels et géométriques de la Terre. Ces derniers sont indispensables pour le calcul mais pas la peine de rentrer dans les détails.
On oublie pas aussi de lui spécifier notre temps (time) exacte qui représente le moment dans le temps où la position et la vitesse ont été mesurés. Et oui, les paramètres orbitaux peuvent être amenés à changer avec le temps en raison de divers perturbations gravitationnelles donc faut le spécifier ce temps.
Une fois, l’appel à Orbit.from_vectors fait, on a plus qu’à récupérer nos 6 paramètres d’orbite :
{% highlight py %}
a = orb.a # Demi-grand axe en km
e = orb.ecc # Excentricité
i = orb.inc.to_value(u.deg) # Inclinaison
Omega = orb.raan.to_value(u.deg) # Longitude du nœud ascendant
omega = orb.argp.to_value(u.deg) # Argument du Périastre
nu = orb.nu.to_value(u.deg) # Anomalie vraie
{% endhighlight %}
Avec la méthode to_value d’astropy, on peut convertir directement une valeur avec l’unité de son choix. En l’occurrence, comme les angles sortent en radians, on s’en sert pour les convertir en degrés.
On print tout ça, et y a plus qu’à remplir avec les bonnes valeurs :
> docker run --rm -i -e FLAG=pouet kepler:challenge
KEPLER
CHALLANGE
a e i Ω ω υ
. .
,' ,=, .
, / \ .
. | | .
. \ / .
+ '=' .
. .'
. . '
'
Your spacecraft reports that its Cartesian ICRF position (km) and velocity (km/s) are:
Pos (km): [8449.401305, 9125.794363, -17.461357]
Vel (km/s): [-1.419072, 6.780149, 0.002865]
Time: 2021-06-26-19:20:00.000-UTC
What is its orbit (expressed as Keplerian elements a, e, i, Ω, ω, and υ)?
Semimajor axis, a (km): 24732.885760723184
Eccentricity, e: 0.7068070220620631
Inclination, i (deg): 0.11790360842507447
Right ascension of the ascending node, Ω (deg): 90.22650379956278
Argument of perigee, ω (deg): 226.58745900876278
True anomaly, υ (deg): 90.389955034578
You got it! Here's your flag:
pouet
Et on a notre flag :)
-
Writeup Hack-A-Sat : Linky
Description du challenge : Years have passed since our satellite was designed, and the Systems Engineers didn’t do a great job with the documentation. Partial information was left behind in the user documentation and we don’t know what power level we should configure the Telemetry transmitter to ensure we have 10 dB of Eb/No margin over the minimum required for BER (4.4 dB)
Le challenge est disponible à cette adresse
Le but de ce challenge est de trouver certains points d’un bilan de liaison (budget link) pour compléter une documentation afin d’aider les ingénieurs satellites.
Donc, lorsque l’on accède à l’instance on nous donne ça :
> docker run --rm -i linky:challenge
...
Here's the information we have captured
************** Global Parameters *****************
Frequency (Hz): 12100000000.0
Wavelength (m): 0.025
Data Rate (bps): 10000000.0
************* Transmit Parameters ****************
Transmit Line Losses (dB): -1
Transmit Half-power Beamwidth (deg): 26.30
Transmit Antenna Gain (dBi): 16.23
Transmit Pointing Error (deg): 10.00
Transmit Pointing Loss (dB): -1.74
*************** Path Parameters ******************
Path Length (km): 2831
Polarization Loss (dB): -0.5
Atmospheric Loss (dB): -2.1
Ionospheric Loss (dB): -0.1
************** Receive Parameters ****************
Receive Antenna Diameter (m): 5.3
Receive Antenna Efficiency: 0.55
Receive Pointing Error (deg): 0.2
Receive System Noise Temperature (K): 522
Receive Line Loss (antenna to LNA) (dB): -2
Receive Demodulator Implementation Loss (dB): -2
Required Eb/No for BER (dB): 4.4
Calculate and provide the receive antenna gain in dBi:
Ok, ça fait pas mal d’informations.
Calcul du gain
On va utiliser ce site. Ce dernier nous demande 3 paramètres :
L’efficacité de réception de l’antenne. Elle nous est donné, elle vaut 0.55.
La longueur d’onde. On l’a aussi, elle vaut 0.025m.
La surface d’aperture physique de l’antenne. Alors, ça on l’a pas mais on peut le calculer facilement. C’est juste la surface géométrique réelle qui capte ou émet les ondes. On nous donne le diamètre de notre antenne donc au final, sa surface physique, c’est juste son aire qui se calcule avec la formule π*r^2 avec r le rayon. Dans notre cas, il vaut r=5.3/2=2.65m
Donc, calculons cette surface : π*r^2=π*2.65^2=22. On peut à présent rentrer toutes nos valeurs dans le calculateur de gain :
Super, on a notre gain qui vaut à peu près 54dB. On peut répondre à la question et ça nous renvoit ceci :
Good job. You get to continue
Receive Antenna Gain (dBi): 54.00
Receive Half-power Beamwidth (deg): 0.33
Receive Pointing Error (deg): 0.2
Receive Pointing Loss (dB): -4.48
Okay, now we know the receive antenna gain.
Calculate and provide the ground terminal G/T (dB/K):
Calcul du G/T (Gain-To-Noise Temperature)
On a un calculateur pour ça qui utilise la formule suivante :
Pour l’antenna gain, on l’a calculé avant, il vaut 54 mais attention, on veut le G/T de la station de sol (ground terminal), pas juste de l’antenne donc il faut aussi prendre en compte les pertes de transmission qui nous sont donnés Receive Line Loss (antenna to LNA) (dB): -2. On parle de gain effectif dans le cas où on prend en compte les pertes. Le calcul reste le même pour autant. Donc, notre gain effectif vaut 54-2=52.
Le system noise temperature nous est donné à 522K.
Calculons tout ça :
Ok, très bien, on trouve 24.8dB/K
Calculate and provide the ground terminal G/T (dB/K): 24.8
Nicely done. Let's keep going.
Determine the transmit power (in W) to achieve 10dB of Eb/No margin (above minimum for BER):
SUPER ! On peut passer à la suite.
Calcul du Transit Power
À présent, on doit calculer la puissance de transmission pour atteindre une marge de 10dB de Eb/No au-dessus du minimum requis pour le taux d’erreur binaire (BER).
Le BER (Bit Error Rate) c’est la proportion de bits reçus avec des erreurs par rapport au nombre total de bits transmis.
Pour toute la suite, ce document va nous être bien utile si ce n’est indispensable :)
Ça nous dit que pour calculer le transmit power, il y a 3 étapes :
1 : Déterminer le Eb/No pour le BER voulu.
2 : Convertir le Eb/No en C/N (Carrier-to-Noise ratio).
3 : Ajouter les pertes de chemin et les marges d’affaiblissement.
1 : Déterminer le Eb/No
Le Eb/No pour le BER, on nous le donne, c’est 4.4dB.
2 : Convertir le Eb/No en C/N
On va utiliser ce calculateur qui se sert de cette formule.
On a besoin du bit rate, tant mieux, on nous le donne aussi, c’est 10000000.0bps donc 10Mbps.
On aussi besoin de la bande passante du récepteur. D’après l’exemple sur le pdf, ça vaut la moitié du bit rate donc 10/2=5MHz.
On remplit tout ça et on obtient C/N ≈ 7.4dB
3 : Ajouter les pertes et les marges
Carrier Power
Le carrier power (puissance de la porteuse) se calcule avec la formule suivante :
C = C/N * N ou en dB C = C/N + N avec C/N en db, N le noise power en W.
Noise Power
Donc, on doit d’abord calculer le Noise Power (Puissance du bruit) avec cette formule :
N = k * T * B avec k la constante de Boltzmann qui vaut 1.380650x10-23 J/K, T la température effective en Kelvin et B la bande passante du récepteur en Hz.
Quand on parle de puissance de bruit, on parle en réalité du bruit thermique généré par l’agitation thermique des électrons dans un conducteur, c’est pour ça qu’on utilise le Kelvin. Plus la température est élevée, plus les électrons s’agitent et plus le bruit est fort. Bref, calculons tout ça :
>>> boltzman = 1.38065e-23 # J/K
>>> data_rate = 10000000.0 # nous est donné
>>> bandwidth = data_rate / 2 # Hz
>>> effective_temperature = 290 # on prend la même que le PDF
>>> noise_power = boltzman * effective_temperature * bandwidth
>>> print(noise_power)
2.0019425e-14 # watts
Super, on a N = 2.0019425e-14W
Sauf qu’il ne faut pas oublier d’ajouter le bruit naturel auquel fait fasse notre récepteur. Ainsi, il faut aussi lui ajouter le noise figure qui mesure la dégradation du SNR en prenant comme référence une température de 290K (C’est une température de réference à laquelle les mesures de bruit sont normalisées). Le noise figure est un ratio alors que le noise power est une mesure absolue.
Noise Figure
On peut utiliser ce site pour récupérer ce dernier.
Le noise temp, il nous est donné à 522K et pour la reference temp, j’utilise la même que le guide donc 290K.
Et pour le noise power, on l’avait déjà calculé (2.0019425e-14) mais le résultat était en watt. Mettons le donc en dBm :
>>> import math
>>> noise_power_w = 2.0019425e-14 # W
>>> noise_power_mw = noise_power_w * 1000 # mW
>>> noise_power_dBm = 10*math.log10(noise_power_w) # dbm
>>> print(noise_power_dBm)
-106.98548400528693
On ajoute notre noise figure et noise temp : 4.4716 - 106.98548400528693 ≈ -102.5dBm
Et enfin, on a notre carrier power : C = 7.4 - 102.5 = -95.1dB.
Il s’agit de la puissance que reçoit le récepteur en entrée.
Path Loss
La path loss (pertes de propagation) en dB pour un site en plein air suivent cette formule : PL = 22dB + 20log(d/λ) avec d la distance entre l’émetteur et le récepteur. Et λ la longueur d’onde de la porteuse. Ces 2 valeurs nous sont déjà données.
>>> distance = 2831000 # m
>>> wavelength = 0.025 # m
>>> path_loss_dB = 22 + 20*math.log10(distance/wavelength) # dB
>>> print(path_loss_dB)
183.0799972138613
Okkk, on a notre PL=183.0799972138613
Transmit Power
On arrive au bout là, plus qu’à additionner tout ça ainsi que les autres pertes qui nous sont données et on aura enfin ce que l’on recherche, le transmit power
>>> transmit_line_losses = 1
>>> transmit_pointing_loss = 1.74
>>> polarization_loss = 0.5
>>> atmospheric_loss = 2.1
>>> ionospheric_loss = 0.1
>>> receive_pointing_loss = 4.48
>>> carrier_power_dBm = -95.1
>>> path_loss_dB = 183.0799972138613
>>> margin_dB = 10
>>> tx_antenna_gain_dBi = 16.23
>>> rx_antenna_gain_dBi = 54.00
>>> tx_power_dBm = transmit_line_losses + transmit_pointing_loss + polarization_loss + atmospheric_loss + ionospheric_loss + receive_pointing_loss + carrier_power_dBm + path_loss_dB + margin_dB - tx_antenna_gain_dBi - rx_antenna_gain_dBi # dBm
>>> tx_power_w = 10 ** (tx_power_dbm / 10) * 10**-3 # watt
>>> print(tx_power_W)
5.847897089829658
SUPER, donc notre puissance de transmission serait 5.8W.
Determine the transmit power (in W) to achieve 10dB of Eb/No margin (above minimum for BER): 5.8
Sorry, you lost
Wrong! Maybe next time.
AIE 🥲. Pourtant, le résultat est vraiment super cohérent.
Du coup, on va brute force, tant pis. Voici le script :
from pwn import *
context.log_level = "critical"
output = b"Wrong answer! You lose."
tx_power = 5.0
while b"Wrong" in output:
command = "docker run --rm -i linky:challenge"
p = process(command, shell=True)
p.recvuntil(b"Calculate and provide the receive antenna gain in dBi: ")
p.send(b"54\n")
p.recvuntil(b"Calculate and provide the ground terminal G/T (dB/K): ")
p.send(b"24.8\n")
p.recvuntil(b"Determine the transmit power (in W) to achieve 10dB of Eb/No margin (above minimum for BER): ")
print(f"Trying with Power Transmission = {tx_power:.1f}")
tx_power_formatted = f"{tx_power:.1f}\n"
p.send(tx_power_formatted.encode())
output = p.recv()
p.close()
tx_power += 0.1
print(output.decode())
On lance notre super script est … :
> p pouet.py
Trying with Power Transmission = 5.0
Trying with Power Transmission = 5.1
...
Trying with Power Transmission = 9.4
Trying with Power Transmission = 9.5
Winner Winner Chicken Dinner
OK, ça a l’air d’être 9.5W, un peu loin de ce que l’on a trouvé mais tant pis, on essaie cette valeur et let’s gooo, on a le flag !
Et voilà pour ce challenge avec pleins de calculs.
Touch background to close