Misja Gynvaela 009

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.

AudacityAudacity

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:

Audacity: różnice w wizualizacjiAudacity: różnice w wizualizacji

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:

Audacity: spektrogramAudacity: 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:

Wizualizacja sum sampli kolejnych sekundWizualizacja sum sampli kolejnych sekund

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.

2 thoughts on “Misja Gynvaela 009

Leave a Reply

Your email address will not be published.