Try   HackMD

TSG CTF 2021 Advanced Fisher Writeup

tags: CTF

この writeup は TSG 内部で行われた TSG CTF 2021 レビュー中に書かれたものです。

問題

フラグから 440 Hz のトーンを用いたモールス信号を生成し、それを前後にシャッフルして出力した音声が与えられる。

解法

まずは出力される音声に使われている正弦波を確認する。

wave[i] = np.sin(i * 440 / 44100 * (2 * np.pi))

44100/gcd(440,44100)=2205 より wave には
2205
種類の数値が現れ、周期も
2205
である。

よって出力結果に現れる数値も高々

2205 種類である。ここで results.wav の各値がどの wave[i] に一致するかに変換しても情報量は変わらない。また results.wav はシャッフルされているため、wave[i] が何回現れるかのマッピングに変換しても情報量は変わらない。

(実装上の注意として、音声ファイルは 24 ビット浮動小数点数で出力されているので、値同士の比較方法に注意する。)

さて、配列というデータを離散フーリエ変換の文脈でわかりやすく操作するために、以下を導入する。

W2025=exp(i2π2025)(i は虚数単位)

これを

n 乗した
W2025n
2205
種類の値を取り、周期も
2205
である。この性質は最初に見た wave[i] と一致している。

生成されるモールス信号におけるひとつの単位は、ある時点から連続した

2000 個の
1
であるから、次のように表される。

unit[i]={1(0i<2000)0(2000i<2025)

unit=i=02024unit[i]W2025i

生成される音声は以下のように表される。

result=i=0472signals[i]W20252000iunit

前述の「result.wav の各値に wave[i] が何回現れるかのマッピング」を

counts とおくと、
result
counts
は全く同一のものである。

signals はインデックスが
1
増える度に単位時間の開始が
2000
ずれていて扱いづらい。よって、インデックスが
1
増える度に単位時間の開始が
1
ずれるように並べ替えたものを考える。

signals[2000imod2205]=signals[i]

ここで

signals の長さは
2205
とし、上記にあてはまらないとき
signals[i]=0
とする。

ただし

Z/2205Z における
2000
の加法群を考えると位数は
2205/gcd(2205,2000)=441
である。
0i<473
はこれを超えるため、重複が起こってしまうことが考えられるが、今回はフラグの先頭文字列が TSGCTF{ であることがわかっているので
73i<473
に絞ることができる。

counts から TSGCTF{ の分を取り除いたものを
counts
とすると

counts=i=73472signals[2000imod2025]W20252000iunit=i=02024signals[i]W2025iunit=(i=02024signals[i]W2025i)(j=02024unit[j]W2025j)=n=02024(i+j=nsignals[i]unit[j])W2025n

counts
W2025n
成分は

counts[n]=i=0nsignals[i]unit[ni]

となり、これは畳み込みの形をしている。

よって逆畳み込みによって

signals=F1[F[counts]/F[unit]]

が得られる。あとはここから

signals を復元すれば良い。

コード

from collections import Counter import soundfile as sf import numpy as np from utils import signals_to_string, string_to_signals # Generate a cycle cycle = [0] * 2205 for i in range(len(cycle)): cycle[i] = np.sin(i * 440 / 44100 * (2 * np.pi)) # Round values to 24-bit sf.write("cycle.wav", cycle, 44100, "PCM_24") cycle, _ = sf.read("cycle.wav") cycle = list(cycle) # Get result.wav result, _ = sf.read("result.wav") # Count counts = [0] * 2205 for value, count in Counter(result).items(): index = cycle.index(value) counts[index] = count # Remove spaces counts[0] = counts[1] # Prepare leading signals leading_signals = string_to_signals("TSGCTF{") # Remove leading signals for i in range(len(leading_signals)): for j in range(2000): counts[(2000 * i + j) % len(counts)] -= leading_signals[i] # Deconvolution! def deconvolve(h, g): n = len(h) H = np.fft.rfft(h, n) G = np.fft.rfft(g, n) F = H / G f = np.fft.irfft(F, n) return np.rint(f).astype(np.int64) reordered_signals = deconvolve(counts, [1] * 2000) # Restore signals signals = [0] * 473 signals[:len(leading_signals)] = leading_signals for i in range(len(leading_signals), len(signals)): signals[i] = reordered_signals[2000 * i % len(reordered_signals)] print(signals_to_string(signals))