FCSC 2025 DisplayPort
Table of contents

On commence par télécharger le fichier, de plus on en profite pour se procurer la documentation de la norme DisplayPort 1.2.
Un premier tour sur la page Wikipédia du standard DisplayPort (https://fr.wikipedia.org/wiki/DisplayPort) nous apprend que la broche 3 appartient à la ligne différentielle 0 et qu’elle correspond au signal négatif de la paire. On va donc commencer par l’inverser en créant un fichier “NotDisplayPort.bin” avec la commande suivante :
xxd -p -c 1 DisplayPort.bin | sed 's/\(..\)/\1 /g' | while read byte; do printf "%02X" $(( 0x$byte ^ 0xFF )); done | xxd -r -p > NotDisplayPort.binGrâce à elle, on comprend assez vite que la première étape sera de réussir à décoder selon l’encodage 8b/10b (https://en.wikipedia.org/wiki/8b/10b_encoding). On commence donc avec un script pour tester plusieurs configurations sur les 100 000 premiers bits. Après quelques ajustements, on réussit à décoder les bits avec cette configuration. On notera qu’on utilise cette bibliothèque Python : https://github.com/olagrottvik/encdec8b10b.
from encdec8b10b import EncDec8B10B
i = 0
for dec in range(10):
print(dec, ": " , end= "")
while 10 * (i+1) + dec < 1000000:
a = binaire[10 * i + dec: 10 * (i+1) + dec][::-1]
byte_to_dec = int(a,2)
try :
ctrl, decoded = EncDec8B10B.dec_8b10b(byte_to_dec)
except :
print("nop")
break
i += 1
print()On trouve l’offset 9, donc on peut maintenant passer aux choses sérieuses. On commence à lire la doc et, après pas mal de recherches, on trouve cette section.

On écrit donc le code suivant pour parser les caractères :
i = 0
dec = 9
while 10 * (i+1) + dec < 10000000:
a = binaire[10 * i + dec: 10 * (i+1) + dec][::-1]
byte_to_dec = int(a,2)
ctrl, decoded = EncDec8B10B.dec_8b10b(byte_to_dec)
if ctrl == 0:
pass
else:
if decoded == 251:
print("BE", i)
elif decoded == 254:
print("FS", i)
elif decoded == 247:
print("FE", i)
elif decoded == 253:
print("SE", i)
elif decoded == 28:
print("RS", i)
elif decoded == 60:
print("CP", i)
elif decoded == 92:
print("SS", i)
elif decoded == 188:
print("BS", i)
elif decoded == 124:
print("BF", i)
else:
print(decoded)
i += 1On tombe sur un grand nombre de FE et FS, on les exclus et on tombe sur ce schéma :
RS 5918
BF 5919
BF 5920
RS 5921Je me souvenais avoir vu ce schéma dans la doc. Après un peu de recherche, je retrouve la section en question.

On peut donc inférer qu’on est dans le mode frame étendu. On va donc commencer à implémenter les principales fonctions du protocole.
On va ignorer les octets entre les balises FS et FE qui sont du remplissage (pour autant, on les garde sous le coude, ça pourrait nous servir, wink wink).
Après quelques tests, on constate qu’on arrive à récupérer des sections qui correspondent exactement à un multiple de 3 fois la largeur de l’écran. On est donc en capacité de récupérer des informations et on essaie de les mettre dans un fichier :
if fill: # vrais si on est pas entre deux balise de remplisage
output_fill.write(bytes.fromhex(decoded))On essaie de l’ouvrir en tant que raw avec GIMP et on obtient :


Du bruit certes, mais du bruit passé.
Il y a quelques mois, en faisant des recherches sur le Tempest, j’ai entendu parler du LFSR dans le protocole DisplayPort. Il est à noter que j’ai un passif avec le LFSR dans la mesure où j’ai déjà dû m’y frotter durant le challenge sur le stand de l’ANSSI à Hack 2024 (ça avait été dur).
Retour à la doc, sous la section “3.1.6 Scrambling”, qui nous informe du fonctionnement du LFSR. On peut résumer son fonctionnement en disant qu’il s’applique à tous les pixels et que, si tout se passe bien, les données entre les fill devraient être 00000000.
J’ai ensuite commencé à faire des tests en réimplémentant le LFSR à partir de là, ça a été peu concluant. Mais là, je remarque une phrase : “A C code reference implementation of the scrambler/descrambler is given in Appendix G”. Bon, la doc est mal faite et c’est plutôt l’annexe E, mais je peux reprendre ce code et le traduire grossièrement en Python comme suit :
LFSR_Seed = 0xFFFF
class lfsr:
register = 0
def reset(self):
self.register = list(map(int, f"{LFSR_Seed:16b}"))
def advence(self):
out = [0] * 16
bit = self.register
out[ 0] = bit[ 8]
out[ 1] = bit[ 9]
out[ 2] = bit[10]
out[ 3] = bit[11] ^ bit[ 8]
out[ 4] = bit[12] ^ bit[ 9] ^ bit[ 8]
out[ 5] = bit[13] ^ bit[10] ^ bit[ 9] ^ bit[ 8]
out[ 6] = bit[14] ^ bit[11] ^ bit[10] ^ bit[ 9]
out[ 7] = bit[15] ^ bit[12] ^ bit[11] ^ bit[10]
out[ 8] = bit[ 0] ^ bit[13] ^ bit[12] ^ bit[11]
out[ 9] = bit[ 1] ^ bit[14] ^ bit[13] ^ bit[12]
out[10] = bit[ 2] ^ bit[15] ^ bit[14] ^ bit[13]
out[11] = bit[ 3] ^ bit[15] ^ bit[14]
out[12] = bit[ 4] ^ bit[15]
out[13] = bit[ 5]
out[14] = bit[ 6]
out[15] = bit[ 7]
self.register = outAprès, le registre du LFSR est XORé avec les données de pixels et de remplissage comme suit :
{D'[7], D'[6], D'[5], D'[4], D'[3], D'[2], D'[1], D'[0]} = {D[7], D[6], D[5], D[4], D[3], D[2], D[1], D[0]} ^ {LFSR[8]. LFSR[9], LFSR[10], LFSR[11], LFSR[12], LFSR[13], LFSR[14], LFSR[15]}On modifie donc notre fonction de récupération de données :
if fill: # vrais si on est pas entre deux balise de remplisage
lfsr_data = L.register[8:16]
tmp = ''
for a,b in zip(format(decoded, '08b'),lfsr_data):
if a == str(b):
tmp += '0'
else:
tmp += '1'
hex_value = format(int(tmp,2), '02x')
output_fill.write(bytes.fromhex(hex_value))
cpt += 1On ajoute aussi quelques parties pour contrôler le LFSR.
elif decoded == 28:
print("RS", i)
L.reset()
jmp = Trueif jmp:
jmp = False
else:
L.advence()Avant de lancer le script, on affiche le contenu entre les balises fill pour vérifier qu’il contient bien 00000000. Mais après vérification, ce n’est pas tout à fait le cas, mais c’est partiellement le cas. J’essaie donc quand même de charger le contenu dans GIMP.
Au début, j’ai pensé que j’avais encore du bruit, mais j’ai remarqué des lignes dans le bruit. En jouant un peu avec les paramètres, puis en retouchant l’image, j’arrive à obtenir ça :

Et ensuite, on utilise une méthode ultra technique digne des plus grands experts et agences en cybersécurité :

Si vous ne le voyez pas, plissez les yeux et penchez votre écran, le flag est :
FCSC{4F72790551DF94E9}BONUS
Après réflexion, en écrivant ce wu, j’ai essayé une autre méthode qui m’a permis d’avoir un résultat beaucoup plus clair.
