BPC电波授时信号的“零成本”伪造
原载阿里聚安全公众号,现在看措辞略浮夸啊,懒得改了。
工作日,上班路上,看一眼情人节女友刚送的六局电波表。咦,出门明明还早,怎么眼看要迟到!别慌,可能只是你被“黑”了。
什么,你戴iWatch?那可以看看这篇。
电波钟/表顾名思义就是通过接收电波授时信号实现自动对时的钟表。以“电波表”为关键字在淘宝搜索,可以看到相关产品很多。其中主要是几个日系的手表大厂,如卡西欧、西铁城、精工、东方双狮等,此外国产品牌也有几个,就不一一列举了。我们后面实验中用到的是一台挂钟,由国产品牌康巴丝生产。
授时电波一般由国家负责标准时间的专门机构进行播发。所广播的时间是国家标准时,由多台高精度原子钟组成的守时钟组产生。授时电波采用频率低于100千赫的长波波段,不易被遮挡,因此一个发射站就可以基本覆盖一国国土。中英美日德等国有各自标准的长波授时服务,不仅名称不一,所用的频率和编码也不同,也就是开头提到的所谓六局(日本面积虽小,但有两局).
我国的长波授时服务BPC,由中科院国家授时中心与某企业合作建立,面向民用。BPC广播站设立在河南商丘,频率为68.5千赫。采用脉宽调制,码率1赫兹。每个编码脉冲宽度为0.1s,0.2s,0.3s或0.4s,分别代表四进制的 0,1,2,3。而这一串四进制的数字是由播发时刻的秒、时、分、星期、日、月、年插入几个校验位组成的,长度为20s,并无任何加密手段。也就是说20秒的信号才可以完整传达当前日期和时间。这一编码方式相比其他各国60秒一帧的方法,对时过程更快。另外需要提醒一下,BPC电波授时编码属于某企业的专利技术,不能私自用于商业盈利。既然是专利,就不妨再公开引用一次,编码示意图如下。
说了这么多,同学们应该对电波授时和电波钟表也有了大概的了解。下面讲讲如何“黑”的问题。思路很简单:伪造授时电波信号,盖过真正的BPC电波,电波钟也就乖乖听咱的了。
如何产生信号呢,我们采用了一台安捷伦最新款PSG系列信号发生器——开玩笑的,我司怎会有这,只有笔记本电脑。好吧,就用笔记本。是的,就用笔记本!
笔记本电脑就位后,照着专利说明书写一个程序,将日期时间翻译成BPC编码,然后将编码通过电脑自带的音频输出播放出来。为了避免笔记本自身杂散电磁辐射造成干扰,我们利用耳机作为播放设备。为了增强信号强度,我们把耳机粘在钟表背后靠近接收天线的位置,把音量调到最大。编造一个错误的时间,运行程序开始发播信号,人耳可以听见脉冲通断的声音。按下电波钟背后的对时按钮三秒钟,表针暂停,进入对时模式。静待几分钟,电波钟从信号中获取错误的时间,表针快速旋转至指定时间,对时成功!
讲到这里,肯定有同学要说,声卡最高只能输出22千赫的声音,怎么能发出68.5千赫,还是电波信号,你不要骗我!且慢,其实是笔者刚有意漏掉一个关键点现在来讲。电脑所播放的音频信号实际上是有讲究的,我们选择了68.5千赫的5分频,13.7千赫,以此作为载波在上面加载BPC编码。耳机发声就是靠线圈产生变化的磁场推动振膜实现的,因此发出听得见的声音的同时,也在发出听不见的电磁波。当我们把输出音量调至最大,耳机的非线性效应显著起来,频率成分中不仅有基频成分(13.7千赫)还有倍频成分,其中也就包含13.7千赫的5倍频信号68.5千赫,也就是BPC的载波频率。这样也就实现了标题所说的“零成本”。
作为一名合格的白帽,讲了“攻”怎么能不讲“防”?实际上仅仅从BPC的编码上来说,由于其非常简单透明,是很难防止类似攻击行为的。好在现在获取时间信息的手段多种多样,通过移动网络、有线网络、GPS、北斗等都可以获得非常准确的时间,因此采用多个时间来源相互参照,能够大大降低因被攻击造成损失的概率。
最后来段代码:
# -*- coding: utf-8 -*-
"""
Created on Tue Jan 19 15:26:45 2016
@author: zhengbowang
"""
import pyaudio
import struct
import datetime
import math
def dropandfill(l,s):return '0'*(l - len(s[2:])) + s[2:]#用0补位
def time2code(date_time, dt = datetime.timedelta(0)):
'''
将时间转换成BPC编码。
'''
date_time -= dt
date = [date_time.day, date_time.month, date_time.year]
timet = [date_time.hour,date_time.minute,date_time.weekday()+1]
date[2] = date[2]%100#year
timet[0] = timet[0]%12#am.pm
p1 = dropandfill(2,bin(date_time.second/20))#seconds
p2 = '00'#reserved
sec1 = (p1+p2)+''.join(map(dropandfill,[4,6,4],map(bin,timet)))
p31 = str(int(date_time.hour>=12))
p32 = str((sec1.count('1'))%2)
p3 = p31 + p32
sec2 = ''.join(map(dropandfill,[6,4,6],map(bin,date)))
p41 = str(int(date_time.year%1000>100))
p42 = str(((sec2.count('1'))%2))
p4 = p41 + p42
code2 = sec1 + p3 +sec2 + p4
bin2four = {'00':'1','01':'2','10':'3','11':'4'}#to base4
return '0'+''.join([bin2four[code2[2*i:2*i+2]] for i in range(len(code2)/2)])
dt = datetime.timedelta(hours = 1)#fake time shift
samp_rate = 68500
freq = 6850 * 2 #in Hertz
ttime =20 #in Sec
SAMPLE_LEN = samp_rate * ttime # 20 seconds of cosine
value = ampl = 32725
div = samp_rate/freq/2
data = 32725
# 打开声音输出流
p = pyaudio.PyAudio()
stream = p.open(format = 8,
channels = 1,
rate = samp_rate,
output = True)
while True:
date_time = datetime.datetime.now()+dt
print date_time
sec = (date_time.second+1)%20
code_str = time2code(date_time)
start = sec * samp_rate
for i in xrange((start), SAMPLE_LEN):
#if i % div == 0:value = -value#carrier generate
value = ampl * int(math.cos(math.pi / float(div) * float(i)))
pulse = (i - sec * samp_rate)/(samp_rate / 10)
packed_value = struct.pack('h', int(pulse >= int(code_str[sec]))*value)
stream.write(packed_value)
if i % samp_rate == 0 and i != start:
sec = sec + 1