MISJA 009 goo.gl/q49Fw7 DIFFICULTY: ██████░░░░ [6/10] ┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅ Do naszych techników trafiło nagranie, w postaci pliku dźwiękowego, z osobliwymi piskami. Nagranie otrzymaliśmy od lokalnego radioamatora i możesz je pobrać poniżej: https://goo.gl/NeJHD2 Jeśli możesz, wyręcz naszych techników w zdekodowaniu wiadomości - są obecnie zajęci naprawą naszego elektrohydroturbobulbulatora. Powodzenia! -- Odzyskaną wiadomość umieść w komentarzu pod tym video :) Linki do kodu/wpisów na blogu/etc z opisem rozwiązania są również mile widziane! P.S. Rozwiązanie zadania przedstawię na początku kolejnego livestreama.
Kolejna misja, którą błędnie zinterpretowałem. Ale od początku.
Odkodowanie
Tutaj znajduje się plik dźwiękowy będący podstawą tej misji. Już po wstępnym odsłuchaniu widać, że w nagraniu występują dźwięki o stałej długości (≈1s) w dwóch różnych tonach. Z prawdopodobieństwem graniczącym z pewnością można stwierdzić, że tony te reprezentują kolejne binarne wartości zakodowanej wiadomości.
Sam plik miał zaledwie 136 sekund – czyli miał zakodowane ≈136 bitów. Ponieważ nie znałem żadnej biblioteki obsługującej pliki dźwiękowe, uznałem, że samodzielny odsłuch i zapisanie wartości w notatniku będzie szybsze od ściągania nieznanej mi biblioteki i oswajania się z jej dokumentacją. Tak też zrobiłem, używając do tego programu Audacity.
Przeglądając plik w Audacity, szybko zauważyłem, że fale kolejnych sekund nieznacznie się różnią w sposób odpowiadający zmianie tonów:
Ton wysoki miał gęściej upakowane fale. Wydaje się całkiem logiczne. Dzięki temu zamiast odsłuchiwać mogłem po prostu przepisać odpowiednie wartości. Ton niższy oznaczyłem zerem, a wyższy jedynką:
01100010 01001110 10100110 10001110 10101110 10100110 01110110 11000110 10011110 00000100 11101010 10000110 01001110 01001110 10010110 11110110 01001110 |
Później dowiedziałem się, że tę część mogłem sobie znacząco uprościć przełączając widok na spektrogram:
Ślepa uliczka
Pierwsza myśl po przeczytaniu misji: jeśli radio-amator przesyłający wiadomości o dwóch stanach, to na pewno wiadomość zakodowana jest Morse’em! Będąc pewnym swojej racji przystąpiłem do jej odcyfrowania:
> gem install morse Fetching: morse-0.0.2.gem (100%) Successfully installed morse-0.0.2 Parsing documentation for morse-0.0.2 Installing ri documentation for morse-0.0.2 Done installing documentation for morse after 0 seconds 1 gem installed |
Lecimy z odkodowaniem:
require 'morse' RAW = <<RAW 01100010 01001110 10100110 10001110 10101110 10100110 01110110 11000110 10011110 00000100 11101010 10000110 01001110 01001110 10010110 11110110 01001110 RAW TR = { '0' => '_', '1' => '.', } MORSE = RAW .each_line .map(&:strip) .join .gsub(Regexp.union(TR.keys), TR) puts MORSE puts Morse.decode MORSE |
.__..._.._..___._._..__._...___._._.___._._..__..___.__.__...__._..____......_..___._._._....__.._..___.._..___._.._.__.____.__.._..___. ? |
No nie bardzo. Może trzeba odwrócić mapę translacji?
TR = { '0' => '.', '1' => '_', } |
_..___.__.__..._._.__.._.___..._._._..._._.__..__..._.._..___.._.__....______.__..._._._.____..__.__...__.__..._.__._.._...._..__.__..._ ? |
Nie zraziłem się niepowodzeniem gema w wersji 0.0.2 i spróbowałem kilku online’owych dekoderów. Niestety również one sobie nie poradziły i, z jednym wyjątkiem, wszystkie wypluły ? jako wynik konwersji. Wyjątek zamiast ? wypisał #, co również oznaczało błąd konwersji.
Ostatecznym rozwiązaniem miało być ręczne skonwertowanie wiadomości do tekstu. W tym momencie Wikipedia wyprowadziła mnie z błędu: W kodowaniu Morse’a występują tak naprawdę trzy stany: kropka, kreska i pustka. Pojedyncze znaki oddzielane są ciszą o długości trzech kropek, a słowa – siedmiu kropek. Inaczej niemożliwe jest np. rozróżnienie ciągu AM (._ __) od znaku J (.___).
Rozwiązanie
Ok, jak nie Morse, to, biorąc pod uwagę trudność misji, trzeba wziąć pod uwagę, że to po prostu tekst. O ile pierwsze dwa znaki mieszczą się w zakresie ASCII, to kolejne już niekoniecznie. Może trzeba to potraktować jako string zakodowany UTF-8?
p RAW .each_line .map{ |l| l.strip.to_i(2) } .pack("C*") .force_encoding('utf-8') |
"bN\xA6\x8E\xAE\xA6v\u019E\u0004\xEA\x86NN\x96\xF6N" |
Ok, jednak nie. W takim razie może to jakiś rolling xor, gdzie każdy kolejny znak jest xorowany z xorem wszystkich poprzednich?
p bytes[1..-1].inject([bytes.first]){ |t,e| t << (t.last ^ e) } |
[98, 44, 138, 4, 170, 12, 122, 188, 34, 38, 204, 74, 4, 74, 220, 42, 100] |
Też nie – wartości nie mają sensu w ASCII. Może należy ponownie przyjrzeć się danym wejściowym:
01100010 01001110 10100110 10001110 10101110 10100110 01110110 11000110 10011110 00000100 11101010 10000110 01001110 01001110 10010110 11110110 01001110 |
Można zauważyć pewną zależność: z jednym wyjątkiem, wszystkie bajty kończą się wartościami 10, a większość kończy się 110. W uporządkowaniu grubokońcówkowym1 oznacza to zakres liter ASCII (małe zaczynają się od 0x61, a duże od 0x41). Czy wystarczy odwrócić kolejność bitów wewnątrz bajtów aby wyszedł czytelny tekst?
puts RAW .each_line .map{ |l| l.reverse.strip.to_i(2) } .pack("C*") .force_encoding('utf-8') |
Frequency Warrior |
Tak!
Bonus – obsługa pliku .wav
Mając już rozwiązanie pokusiłem się o programowe rozpracowanie dostarczonego pliku dźwiękowego. Użyłem do tego gema WaveFile. Moje pierwotne przewidywania okazały się trafne – nie znając formatu ani gema, doprowadzenie do wyświetlenia wartości kolejnych sampli zajęło mi ≈15 minut, czyli znacząco dłużej, niż ręczne zapisanie wartości.
Wiedząc, że ścieżka w pliku ma częstotliwość 44kHz, utworzyłem tablicę sekundowych buforów:
require 'wavefile' reader = WaveFile::Reader.new 'g9.wav' p reader.format arr = [] reader.each_buffer(44100){ |b| arr << b } |
Policzenie średniej wartości sampli nie dało sensownych rezultatów:
p arr.map{ |a| a.samples.inject(&:+) / arr.size } |
[0, -1, 0, -1, -1, 0, -1, -1, -1, -1, 0, 0, -1, -1, 0, 0, 0, -1, -1, 0, ... |
Zmiany wartości nie odpowiadają wartościom odczytanym ręcznie!
Policzenie sum wartości bezwzględnych dało złudnie niepoprawne wyniki:
p arr.map{ |a| a.samples.map(&:abs).inject(&:+) } |
[450729944, 450716403, 450716237, 450729878, 450730045, 450730077, 450716077, ... |
Choć kolejne wartości wyglądają na bardzo zbliżone, można je podzielić na dwie bardzo wyraźne grupy:
Na powyższym obrazku wyraźnie widać, że wartości tak naprawdę stoją po przeciwnych stronach magicznie wyglądającej wartości 450723000. Po jej odjęciu tablica przedstawia się tak:
p arr.map{ |a| a.samples.map(&:abs).inject(&:+) - 450723000 } |
[6944, -6597, -6763, 6878, 7045, 7077, -6923, 7015, 6934, ... |
Tutaj oczywistym staje się, że wartości poniżej zera odpowiadają jedynkom z ręcznie (usznie) odczytanej wiadomości, a te powyżej zera – zerom. Wobec tego można to poskładać w całość:
require 'wavefile' reader = WaveFile::Reader.new 'g9.wav' p reader.format arr = [] reader.each_buffer(44100){ |b| arr << b } puts arr .map{ |a| a.samples.map(&:abs).inject(&:+) < 450723000 } .map{ |b| b ? 1 : 0 } .each_slice(8) .to_a .map{ |a| a.reverse.map(&:to_s).join.to_i(2).chr } .join |
Frequency Warrior |
Yay!
Do następnej misji!
1Przepraszam, nie mogłem się powstrzymać. To jest lepsze niż zręb.