0%

Hoofdstuk 4 · oefening

Maskers en controleframes

Een payload maskeren en demaskeren

Je leercodec leest elk dat een server stuurt. Hij leest geen enkel frame dat een client stuurt, omdat RFC 6455 eist dat een client elk frame maskeert, en de decoder van hoofdstuk 3 een gemaskeerd frame weigert. Sectie 5.7 van de RFC drukt “Hello” op beide manieren af, niet gemaskeerd zoals een server het stuurt en gemaskeerd zoals een client het stuurt:

81 05 48 65 6c 6c 6f
81 85 37 fa 21 3d 7f 9f 4d 51 58

De eerste byte is hetzelfde, 81: FIN gezet, 1, tekst. De tweede byte is 05 in het ene en 85 in het andere: dezelfde lengte 5, plus de MASK-bit 0x80 die je in hoofdstuk 3 las. Dan komen vier bytes die het niet-gemaskeerde frame niet heeft, 37 fa 21 3d. Dat is de . Als laatste komen vijf payloadbytes, en in het gemaskeerde frame spellen ze geen “Hello” meer. Ze zijn “Hello” gecombineerd met de sleutel, byte voor byte. Deze les schrijft die combinatie; de volgende zet haar in frames.

XOR met een roterende sleutel

RFC 6455 sectie 5.3 definieert maskeren in één regel: byte i van de gemaskeerde payload is byte i van de payload XOR byte i % 4 van de sleutel. De sleutel heeft vier bytes, en i % 4 loopt ze om de beurt langs, 0, 1, 2, 3, en dan terug naar 0, zolang de payload duurt. Hier is “Hello” met de sleutel van de RFC:

iPayloadbyteSleutelbyte (i % 4)XOR
048 (H)37 (key[0])7f
165 (e)fa (key[1])9f
26c (l)21 (key[2])4d
36c (l)3d (key[3])51
46f (o)37 (key[0])58

Positie 4 gaat rond en gebruikt key[0] opnieuw, dezelfde sleutelbyte als de H. De twee l’en op positie 2 en 3 geven 4d en 51, omdat hun posities verschillende sleutelbytes gebruiken, dus gelijke letters geven geen gelijke gemaskeerde bytes. Indexeren in een bytes- geeft een int (Networks with Python hoofdstuk 2), dus payload[i] ^ key[i % 4] is een getal van 0 tot en met 255, en een van die getallen wordt met bytes(...) weer bytes.

Met een sleutel waarvan alle vier de bytes ff zijn, is elke sleutelbyte hetzelfde, dus doet de positie er niet toe, en XOR met ff draait alle acht bits van elke byte om. Voorspel eerst twee van zulke omdraaiingen:

Wat drukt dit af?

print(hex(0x71 ^ 0xFF), hex(0x8E ^ 0xFF))

Dit blok maskeert de vier bytes van b"quiz" op die manier:

Try it

De tweede draait dezelfde bits terug en drukt b'quiz' af. Een echte sleutel heeft vier verschillende bytes, dus je heeft de positie i nodig om de juiste te kiezen.

Welke sleutelbyte maskeert payloadbyte 9?

Zijn eigen inverse

De eerste les van hoofdstuk 3 vroeg wat (0x48 ^ 0x37) ^ 0x37 afdrukt. Het antwoord was 72, en dat is 0x48, de H van “Hello” gemaskeerd met key[0] en weer gedemaskeerd. XOR met twee keer dezelfde waarde geeft altijd terug waarmee je begon: x ^ k ^ k == x. Eén functie doet dus beide klussen. De client roept haar aan met de sleutel om een payload te maskeren; de server leest de sleutel uit het frame en roept dezelfde functie aan met dezelfde sleutel om de payload terug te krijgen.

Waarom clients maskeren

Maskeren is er niet om het bericht te beschermen. Het beschermt het netwerk tussen de client en de server. RFC 6455 sectie 10.3 legt de zorg uit: een proxy of cache op het pad die niet begrijpt, zou de bytes na de kunnen lezen alsof ze waren. Als een script dat in een browser draait die bytes kon kiezen, kon het iets schrijven dat op een HTTP-verzoek en -antwoord lijkt, en een verwarde cache zou een antwoord dat de aanvaller verzon kunnen opslaan en aan andere mensen kunnen geven. De RFC noemt dit cache poisoning.

Een nieuwe, onvoorspelbare sleutel voor elk frame neemt die keuze weg. Het script kiest de payload, maar de browser kiest de sleutel, dus het script kan niet weten welke bytes het netwerk oversteken. Daarom geldt de regel voor clients: een browser draait code van websites die hij niet vertrouwt, en een server doet dat niet.

Maskeren verbergt niets voor iemand die het frame kan lezen, omdat de sleutel in hetzelfde frame reist, vier bytes vóór de payload. Iedereen die het frame ziet, kan het demaskeren, precies zoals je server zal doen. Een gesprek privé houden is de taak van , het schema wss:// dat je in hoofdstuk 9 tegenkomt.

Waarom is maskeren geen versleuteling?

Je opdracht

Dit hoofdstuk begint vanuit de eigen afgewerkte bestanden van de cursus uit hoofdstuk 3, hoe je eigen code uit hoofdstuk 3 er ook uitzag. ws_frames.py is de codec uit hoofdstuk 3 met de nieuwe functies van dit hoofdstuk toegevoegd als stubs; elke docstring noemt de les die haar schrijft, en de functies uit hoofdstuk 3 zeggen “Chapter 3 wrote it.” ws_handshake.py is de afgewerkte handshake van de cursus uit hoofdstuk 2, en tests/test_frames.py is de versie van de cursus van de suite uit hoofdstuk 3 les 6, met haar @pytest.mark.parametrize-decorators. Beide zijn er om te lezen; geen enkele les hier past ze aan. Deze lessen hebben een editor en geen , dus geen stap vraagt je de suite te draaien, maar als je het project naar je eigen machine downloadt, draait python -m pytest in de map ervan haar. Haar 15 tests dekken niet-gemaskeerde frames, en elke wijziging in dit hoofdstuk houdt alle 15 groen.

Schrijf in ws_frames.py apply_mask(payload, key). Loop over de posities van payload met range(len(payload)), XOR byte i met key[i % 4], en geef de resultaten terug als bytes. Dezelfde functie moet ook demaskeren.

Druk op Run. Run start opnieuw frames_demo.py, nu met één sectie per les van dit hoofdstuk; de laatste regels ervan draaien ze alleen als het bestand als programma wordt gestart, achter de bewaker if __name__ == "__main__":. Met apply_mask geschreven begint de uitvoer zo:

Lesson 1: Hello masked with 37 fa 21 3d
7f 9f 4d 51 58
Lesson 2: a masked text frame
  not written yet

Druk daarna op Submit. De controle maskeert het “Hello” van de RFC en maskeert het resultaat opnieuw, en doet daarna hetzelfde met sleutels en payloads die je nog niet hebt gezien: een tekst van 7 bytes, een lege payload, en 65.536 bytes. Ze verwacht elke keer bytes terug. In de volgende les gebruiken encode_frame en decode_frame je functie om gemaskeerde frames te bouwen en te lezen.

Opdracht

Schrijf apply_mask(payload, key) in ws_frames.py. Byte i van het resultaat is byte i van payload XOR byte i % 4 van de key van 4 bytes. Loop over de posities met range(len(payload)), gebruik ^ en % 4, en geef het resultaat terug als bytes. Opnieuw aangeroepen met dezelfde sleutel op haar eigen resultaat geeft de de oorspronkelijke payload terug.

Druk op Run: de sectie van les 1 in frames_demo.py moet 7f 9f 4d 51 58 afdrukken. Druk daarna op Submit. De controle maskeert het “Hello” van de RFC met 37 fa 21 3d en maskeert het resultaat opnieuw, en doet daarna hetzelfde voor sleutels en payloads die je nog niet hebt gezien: een tekst van 7 bytes, een lege payload, en 65.536 bytes. Ze verwacht elke keer bytes met de lengte van de payload. Laat de andere functies in ws_frames.py zoals ze zijn.