Przejdź do głównej zawartości

Dekodowanie własnych protokołów

Przechwyciłeś prywatny lub wewnętrzny protokół na gniazdach, którego wbudowane formaty nie potrafią rozpoznać, więc zostaje Ci tylko sterta bajtów? Własne dekodowanie pozwala nauczyć narzędzie, jak to odczytać, za pomocą krótkiego skryptu: pociąć ciągły strumień bajtów na pojedyncze wiadomości, usunąć nagłówek protokołu, w razie potrzeby zdekompresować, a resztę zostawić automatycznemu, strukturalnemu rozpoznawaniu narzędzia.

Ta strona jest dla każdego, kto chce samodzielnie napisać dekoder. Wyjaśnia, jak napisać funkcję, kiedy jest wywoływana i co oznacza jej wartość zwracana.


Piszesz hak dekodujący, a narzędzie podaje mu bajty połączenia, wielokrotnie zadając jedno pytanie: “Zaczynając stąd, czy da się wyciąć jedną kompletną wiadomość?” Wycinasz wiadomość i zgłaszasz, ile bajtów zużyłeś; narzędzie pyta ponownie z tym, co zostało, i tak dalej, aż strumień zostanie w całości pocięty.

Jeśli kiedykolwiek pisałeś akumulujący dekoder bajtów w jakimś frameworku sieciowym, model myślowy jest ten sam: mając ciągły strumień bajtów, sam decydujesz o granicach wiadomości. Dwie kluczowe konwencje:

  • Nie martwisz się o kierunek: strumień wysyłania i strumień odbioru połączenia przechodzą przez Twój hak osobno, a narzędzie automatycznie oznacza każdą pociętą wiadomość jej kierunkiem. Twój skrypt zajmuje się wyłącznie “jak ciąć”.
  • Po pocięciu nie zajmujesz się renderowaniem: każda wiadomość, którą wypchniesz, przechodzi ponownie przez automatyczne rozpoznawanie. Jeśli zawiera protobuf, JSON albo plist, zostanie to dalej sparsowane do postaci strukturalnej. Odpowiadasz jedynie za wyznaczanie ramek, usuwanie nagłówków i dekompresję.

Cały dekoder to pojedyncza funkcja:

function decode(buf, out) {
// buf: the byte stream currently pending parse; out: the output collector; return: the number of bytes consumed this time
}

Czym są dane wejściowe, wyjściowe i wartość zwracana

Dział zatytułowany „Czym są dane wejściowe, wyjściowe i wartość zwracana”

buf (wejście): strumień bajtów oczekujący na parsowanie

  • Typ: ArrayBuffer. Jego zawartość to wszystkie pozostałe bajty “od miejsca, w którym ostatnio skończyłeś zużywanie, aż do bieżącego końca”.
  • Właściwość buf.byteLength: ile bajtów pozostaje w danej chwili (może rosnąć z każdym wywołaniem; po tym oceniasz, czy jest już dość, by wyciąć wiadomość).
  • ⚠️ To surowy bufor bajtów, a nie Uint8Array, więc nie możesz indeksować bajtów bezpośrednio (buf[0] zwraca undefined). Aby odczytać bajty, użyj wbudowanych funkcji pomocniczych z sekcji 3 (u8 / u16be / sub …) albo sam owiń widok: new Uint8Array(buf), new DataView(buf).

out (wyjście): kolektor wyjściowy

  • Typ: zwykła tablica.
  • Metoda out.push(bajty wiadomości): wypycha jedną wiadomość; w jednym wywołaniu możesz wypchnąć kilka.
  • “Bajty wiadomości” mogą być: ArrayBuffer zwróconym przez funkcję pomocniczą (taką jak sub(...), gunzip(...)) albo łańcuchem znaków. Inne typy są ignorowane.

Wartość zwracana: ile bajtów zużyłeś

  • Typ: liczba całkowita (number), liczba bajtów, które zużyłeś od początku buf.
  • > 0: zjadłeś tyle bajtów od przodu i wyciąłeś wiadomość → narzędzie je odrzuca i wywołuje Cię ponownie z tym, co zostało.
  • 0 / brak return / liczba ujemna: oznacza, że “na początku nie ma jeszcze kompletnej wiadomości” albo “nic więcej nie da się wyciąć” → narzędzie zatrzymuje pętlę i wyświetla pozostałe bajty jako surowe.
  • Wartość zwracana jest automatycznie ograniczana do długości bufora (aby zapobiec przekroczeniom), ale zwracaj proszę prawdziwą liczbę faktycznie zużytych bajtów.
  • Narzędzie wykonuje jeden przebieg po strumieniu wysyłania połączenia i jeden po strumieniu odbioru. W każdym przebiegu decode jest wywoływane w pętli: za każdym razem dostajesz pozostałe, niezużyte bajty, wycinasz jedną wiadomość i zwracasz liczbę zużytych bajtów, i tak dalej, aż zwrócisz 0.
  • Wiadomość pojawia się dopiero po potwierdzeniu jej zużycia: wiadomość, którą wypychasz, pojawia się tylko wtedy, gdy jednocześnie zwrócisz > 0. Jeśli wypchniesz, ale zwrócisz 0, pętla się zatrzymuje, a wypchnięcie zostaje odrzucone, więc wypchnięcie wiadomości musi zawsze iść w parze ze zwróceniem liczby bajtów, które zajęła.
  • Po zakończeniu pętli niezużyte końcowe bajty nie giną; są pokazywane jako pojedynczy fragment surowych danych (na przykład druga połowa niekompletnej wiadomości).
  • Każdy strumień (kierunek) dostaje osobne, całkiem nowe środowisko skryptu; strumień wysyłania i strumień odbioru nigdy się nie mieszają.
  • Aby zachować stan między wywołaniami (licznik, typ poprzedniej wiadomości i tak dalej), zadeklaruj zmienne poza decode. Utrzymują się między wywołaniami w obrębie tego samego strumienia i resetują się, gdy zmienia się kierunek albo gdy ponownie dekodujesz.
  • Dekodowanie działa na już przechwyconych danych i można je uruchamiać wielokrotnie: edytuj skrypt, zapisz i kliknij ponownie, aby na nowo zdekodować to samo połączenie według nowych reguł.
  • Jeśli skrypt rzuci wyjątkiem albo pojedyncze uruchomienie przekroczy limit czasu, jest to traktowane jako “tym razem nic nie zużyto”, pozostałe bajty są pokazywane jako surowe dane, a przechwytywanie działa dalej bez utraty danych.
  • Innymi słowy, najgorsze, co może zrobić wadliwy skrypt, to nie zdekodować danych, zostawiając Ci surowe bajty; nie zawiesi połączenia. Edytuj śmiało.
  • I błędy nie są połykane: wyjątki i przekroczenia limitu czasu są pokazywane w panelu danych debugowania nad wynikami (patrz sekcja 5), z etykietą, czy pochodzą ze strumienia wysyłania , czy odbioru . Podążaj za nimi prosto do poprawki.

3. Wbudowane funkcje pomocnicze (dostępne bezpośrednio w skrypcie)

Dział zatytułowany „3. Wbudowane funkcje pomocnicze (dostępne bezpośrednio w skrypcie)”

Typowe zadania, czyli odczyt bajtów, odczyt liczb całkowitych, konwersja na tekst i dekompresja, są wbudowane, więc nie musisz wymyślać ich od nowa:

Kategoria Sygnatura Opis
Wycięcie podfragmentu sub(buf, off[, len]) Tnie od off (pomiń len, aby dojść do końca), zwraca ArrayBuffer
Odczyt liczby całkowitej u8(buf, off) / u16be / u16le / u32be / u32le (buf, off) Odczyt liczb całkowitych bez znaku w porządku big-endian / little-endian
Konwersja na tekst hex(buf) / ascii(buf) Konwersja na łańcuch szesnastkowy / odczyt jako tekst
Wykrywanie kompresji gzipMagic(buf) Czy to gzip (zwraca wartość logiczną)
Dekompresja gunzip / inflate / unzstd / lz4dtx (buf) Dekompresja; jeśli się nie uda, zwraca dane wejściowe bez zmian, bez błędu
Debugowanie log(...args) / console.log(...args) Wypisanie do panelu debugowania (patrz sekcja 5)

Standardowe możliwości odczytu/zapisu bajtów (DataView, Uint8Array itd.) też są dostępne bezpośrednio.


① Prefiks długości [4-bajtowa długość big-endian][ładunek], najczęstsza postać prywatnego protokołu:

function decode(buf, out) {
if (buf.byteLength < 4) return 0 // the length header hasn't fully arrived, wait
const total = 4 + u32be(buf, 0) // whole message = 4-byte header + payload
if (buf.byteLength < total) return 0 // the whole message hasn't fully arrived, wait
out.push(sub(buf, 4, total - 4)) // strip the header, hand the payload to auto recognition
return total // consume this message, go on to slice the next
}

② Po separatorze / po wierszu, cięcie ramek na znaczniku takim jak znak nowej linii:

function decode(buf, out) {
const i = ascii(buf).indexOf('\n')
if (i < 0) return 0 // no newline yet, wait
out.push(sub(buf, 0, i)) // push this line (without the newline)
return i + 1 // consume the newline along with it
}

③ Przetworzenie całości naraz, traktując cały strumień jako jedną wiadomość, na przykład dekompresując wszystko od razu:

function decode(buf, out) {
out.push(gzipMagic(buf) ? gunzip(buf) : buf) // if it's gzip, unpack it
return buf.byteLength // consume everything, the loop ends right away
}

④ Rozgałęzianie według typu, z polem typu w nagłówku i innym postępowaniem dla każdego typu:

function decode(buf, out) {
if (buf.byteLength < 4) return 0
const total = 4 + u32be(buf, 0)
if (buf.byteLength < total) return 0
const type = u8(buf, 4) // the first byte is the message type
const body = sub(buf, 5, total - 5)
out.push(type === 2 ? gunzip(body) : body) // type 2 is compressed
return total
}

Edytor ma gotowe szablony dla wszystkich tych wzorców; wstaw jeden, dopasuj kilka liczb i działa.

Edytor własnego dekodera: napisz skrypt decode(buf, out); “Wstaw szablon” udostępnia gotowe szkielety dla prefiksu długości / sygnatury magicznej / separatora / stałej długości / gzip / rozgałęzienia według typu, a także szybki podręcznik funkcji


  • Napisz i zapisz: utwórz, nazwij i zapisz w edytorze dekodera; wstaw szablon, aby zacząć, z wbudowanymi funkcjami dostępnymi w podręcznym skrócie.
  • Zastosuj: dla połączenia, którego nie potrafisz zrozumieć, kliknij prawym przyciskiem myszy i wybierz “Dekoduj jako” → swój dekoder, a cały ruch wysyłania i odbioru tego połączenia zostanie natychmiast pocięty i pokazany według Twoich reguł.
  • Debuguj i sprawdzaj zmienne: wewnątrz decode użyj log(...) albo console.log(...), aby wypisać dowolną wartość: liczbę bajtów, pola typu, hex(sub(buf, 0, 8)) i tak dalej. Wynik pojawia się w panelu “Dane debugowanianad wynikami “Dekoduj jako”, z etykietą przy każdym wierszu, czy pochodzi ze strumienia wysyłania , czy odbioru . Błędy skryptu i przekroczenia limitu czasu pojawiają się w tym samym panelu, również oznaczone kierunkiem. Wypisuj i lokalizuj w ten sposób; jest to o wiele szybsze niż edytowanie po omacku.

    Piaskownica nie ma pełnego console przeglądarki / Node; podłączone do tego panelu są tylko console.log i równoważne log. TextDecoder, fetch, setTimeout i temu podobne nie są dostępne. Aby odczytać bajty jako tekst, użyj ascii(buf).

  • Edytuj i obserwuj na żywo: nie trzeba przechwytywać od nowa. Zapisz swój skrypt, kliknij ponownie “Dekoduj jako”, a to samo połączenie zostanie od razu zdekodowane na nowo według nowych reguł. Iteruj, aż pocięcie będzie takie, jak chcesz.
  • Dalej do struktury: każdy zdekodowany fragment jest przekazywany z powrotem do automatycznego rozpoznawania, więc protobuf, JSON i plist są dalej parsowane do postaci strukturalnej, do obejrzenia w różnych widokach opisanych w Podglądzie i dekodowaniu danych.
  • Dekodery można nazwać i trzymać na liście, dodawać, edytować lub usuwać w dowolnym momencie.

  • Gdy przechwycisz wewnętrzny lub prywatny protokół na gniazdach (typowa postać to prefiks długości + protobuf / JSON / dane binarne), którego wbudowane formaty nie rozpoznają, napisz dekoder, aby go pociąć i przywrócić do czytelnej struktury.
  • Gdy chcesz zamienić fragment bajtów owinięty w niestandardowy nagłówek albo przepuszczony przez rundę kompresji z powrotem w czytelną treść.

Zbiorczy szybki podręcznik: punkt wejścia dekodowania, wbudowane funkcje pomocnicze i środowisko uruchomieniowe. Każda funkcja przyjmująca buf akceptuje zarówno ArrayBuffer, jak i łańcuch znaków.

decode(buf, out) → number
Cel Jedyny punkt wejścia dekodera, musi zostać zaimplementowany. Wytnij wiadomości z początku buf, push-uj je do out i zwróć liczbę bajtów zużytych tym razem.
buf ArrayBuffer, pozostały strumień bajtów oczekujący na parsowanie. Użyj buf.byteLength dla jego długości; nie możesz indeksować bajtów, użyj poniższych funkcji pomocniczych albo new DataView(buf) / new Uint8Array(buf).
out Array, kolektor wyjściowy. out.push(bytes) wypycha jedną wiadomość (bytes to ArrayBuffer albo łańcuch znaków; inne typy są ignorowane); możesz wypchnąć kilka naraz.
Zwraca number, liczbę bajtów zużytych od początku buf. > 0 kontynuuje; 0 / ujemna / brak zwrotu → zatrzymanie, a pozostałe bajty są pokazywane jako surowe.
Konwencja wywołania Wykonaj po jednym przebiegu dla strumienia wysyłania i odbioru połączenia; każdy przebieg wywołuje wielokrotnie, przekazując za każdym razem pozostałe, niezużyte bajty, aż zwrócisz 0.

Wycięcie podfragmentu

Sygnatura Zwraca Opis
sub(buf, off) ArrayBuffer Tnie od off do końca
sub(buf, off, len) ArrayBuffer Tnie len bajtów od off; nieprawidłowy zakres off / len jest automatycznie ograniczany, bez błędu

Odczyt liczby całkowitej (bez znaku; zwraca 0, gdy off jest poza zakresem)

Sygnatura Zwraca Opis
u8(buf, off) number Odczyt 1 bajtu
u16be(buf, off) / u16le(buf, off) number Odczyt 2 bajtów, big-endian / little-endian
u32be(buf, off) / u32le(buf, off) number Odczyt 4 bajtów, big-endian / little-endian

Konwersja na tekst

Sygnatura Zwraca Opis
hex(buf) string Konwersja na małe litery szesnastkowe
ascii(buf) string Odczyt jako tekst (odpowiednie dla treści ASCII / tekstowych)

Kompresja / dekompresja (zwraca dane wejściowe bez zmian, gdy nie da się zdekompresować; limit pojedynczej dekompresji to około 16 MB)

Sygnatura Zwraca Opis
gzipMagic(buf) boolean Czy zaczyna się od magicznej liczby gzip
gunzip(buf) ArrayBuffer Dekompresja gzip
inflate(buf) ArrayBuffer Dekompresja zlib / deflate
unzstd(buf) ArrayBuffer Dekompresja zstd
lz4dtx(buf) ArrayBuffer Dekompresja blokowego strumienia LZ4

Debugowanie

Sygnatura Zwraca Opis
log(...args) (brak) Łączy argumenty w jeden wiersz i wypisuje w panelu “Dane debugowania”; ArrayBuffer jest pokazywany szesnastkowo
console.log(...args) (brak) To samo co log (wygodne dla przyzwyczajonych do console.log)
  • Standardowy JavaScript: dostępne są typowe wbudowane obiekty, takie jak ArrayBuffer, tablice typowane (Uint8Array itd.), DataView, JSON, Math, RegExp, Date; gdy potrzebujesz niskopoziomowego odczytu/zapisu bajtów, masz pod ręką new DataView(buf) / new Uint8Array(buf).
  • Debugowanie: log(...) / console.log(...) wypisują do panelu “Dane debugowania”; tam też wypisywane są błędy skryptu i przekroczenia limitu czasu, z etykietą, czy pochodzą ze strumienia wysyłania , czy odbioru .
  • Nie są dostarczane inne możliwości specyficzne dla przeglądarki / Node: metody console inne niż log, TextDecoder / TextEncoder, fetch, setTimeout, require itd. są niedostępne. Aby odczytać bajty jako tekst, użyj ascii(buf).
  • Każde wywołanie decode ma limit czasu wykonania: nieskończona pętla albo przekroczenie limitu jest przerywane i traktowane jako “nic nie zużyto”, więc nie zawiesi narzędzia.

Powrót do Przechwytywanie przez proxy · Powiązane: Podgląd i dekodowanie danych · Przechwytywanie na karcie sieciowej