☰
用SDR和Python实现GSM短信嗅探:A5/1加密原理与解码实战
2026/10/15 11:43:22 网站建设 项目流程

简介:这是一份面向网络安全研究者与Python开发者的GSM短信嗅探实践资源,基于gsmsms_sdr开源项目,利用SDR设备(如RTL-SDR、USRP)捕获并解析GSM频段信号,进而还原短信内容。由于国内GSM网络使用的A5加密算法已被破解,该技术主要应用于无线通信安全教学、协议研究和渗透测试入门。

资源包共11个文件,以Python源码为核心,覆盖信号接收、帧同步、解码与短信转发等关键模块;同时包含SQL数据库脚本用于短信存储,多张运行截图和图片辅助说明实验效果,以及Markdown说明文档梳理使用流程,整体仅544KB。

该资源已有1165人学习使用,适合具备一定Python基础、对SDR和GSM协议感兴趣的读者。通过研读代码与文档,可掌握GNU Radio/gr-gsm的调用方式、A5算法解密思路、短信解析与转发逻辑,并能快速搭建自己的嗅探实验环境。

1. GSM 短信嗅探:SDR 能收到不等于你能碰明文

“GSM 在中国没有加密”这句话,在软件无线电圈子里流传了很多年,但它并不准确。国内运营商部署 2G 网络时普遍启用了 A5/1 加密,真正的问题是这套来自 1990 年代的加密算法早已经被攻破了:攻击者抓到几个已知明文对应的加密帧,借助预计算好的彩虹表,用普通电脑就能反推出会话密钥,让加密形同虚设。对做无线安全研究和协议学习的人来说,用一套百元级的 SDR 接收机加上 Python,就可以把 GSM 的加密从“听说过”变成“看得见”的实验。但有一点必须放在最前面:空中接口的数据你能收到,不代表你有权利解密和阅读别人的短信。这篇文章先讲加密为什么失效,再讲怎么搭建接收链路、怎么解码,最后讲合规边界。

2. GSM 加密为什么挡不住 SDR:从 A5/1 到空中接口

2.1 64 位密钥与彩虹表:A5/1 被攻破的本质

A5/1 是 GSM 最早商用的流密码,由三个线性反馈移位寄存器组成,长度分别是 19、22、23 位,三者合计 64 位,会话密钥 Kc 就是 64 位。不过算法在部分实现里有效密钥长度被削减到 54 位,这让穷举空间从 2^64 降到了 2^54。这个规模依然很大,但对攻击者来说已经看到了希望,至少不再是不可能完成的穷举。

真正的突破口不在密钥长度,而在算法结构。A5/1 的三个 LFSR 采用不规则钟控:每个时钟周期只有两个寄存器被驱动,没有被驱动的那一个保持原状态。这种钟控方式让内部状态之间存在数学关联,输出的密钥流偏离了理想随机序列。密码学界在 1990 年代末就开始发表针对 A5/1 的攻击方案,核心思路是:如果攻击者知道某一段明文,就能用密文异或明文得到对应的密钥流片段,再通过密钥流反推三个寄存器的内部状态,最终恢复 Kc。

预计算的彩虹表把这种反推的成本提前预付了。表的基本构造思路是:把大量可能的内部状态加密成对应的密钥流片段,再用时间-内存权衡算法压缩存储。破解时,从捕获帧中取 64 比特已知明文对应的密钥流,查表还原寄存器状态,再正向推导出 Kc。这个过程的耗时取决于表格规模和采集帧的质量,实际从秒级到分钟级波动。

注意,理论攻击和实际工程之间距离不小。GSM 的每个突发脉冲只有 156.25 个符号,其中还包含训练序列和尾比特,可用的业务比特不到 114 个。A5/1 攻击需要足够多的连续已知明文比特,所以一般要从多个突发脉冲中凑。实际中大家更多使用“已知明文 + 校验关系”的联合方法:GSM 信道编码里大量使用卷积码,校验关系本身构成对候选密钥的约束,让攻击者不需要非常多的已知明文。

对做 SDR 实验的人来说,A5/1 的破解并不每次都成功。如果目标基站关掉了加密(A5/0),那就直接跨过破解步骤,解析第三层消息就能看到明文。国内某些网络为了兼容老旧终端,确实配置过 A5/0,这让“GSM 在中国没有加密”有了现实基础——它只描述了部分地区和部分基站,并非网络全貌。

2.2 国内 GSM 网络的实际加密配置:A5/0 尚未完全消失

国内现网 2G 以 900MHz 和 1800MHz 两个频段运行。900MHz 频段的下行(基站到手机)频率范围在 935 到 960MHz,信道间隔 200kHz,共 124 个频点。由于 900MHz 的传播特性好,语音和短信业务主要承载在这个频段,这也是 SDR 实验的首选目标。

加密模式的启用由基站配置决定。城市区域的 2G 基站普遍配置 A5/1,部分场景还会协商到更安全的 A5/3,但 GSM 的加密协商过程是“手机只支持什么,基站就兼容什么”。一部老旧的 2G 手机上报不支持 A5/3 时,基站会自动降级到 A5/1 甚至 A5/0。这种向下兼容的设计,让加密强度最终取决于网络里最弱的那台终端。所以在某些区域,你用 SDR 做频谱扫描,很可能会同时看到“加密”和“不加密”的基站混在一起。

近几年 2G 退网在持续推进,很多地方的核心城区已经扫不到连续可用的 GSM 信号。做实验时,要么选择距离城市较远的基站,要么自己搭一个微型 GSM 基站作为测试对象。微型基站方案对学习协议更友好,因为你可以完全控制加密模式,把 A5/0、A5/1 都配置一遍,每次对比接收链路的表现,比在真实网络上被动捕捉要可控得多,也安全得多。

另外要注意频段配置差异。同是 900MHz,不同运营商的频段范围不同,但 SDR 只关心频点,不关心归属。扫描时把整个下行频段扫一遍,标记出持续存在载波的频点。GSM 基站的信号和 4G/5G 不同,它在频谱上看起来是周期性突发的窄带信号,而不是持续占用的宽带信号,用 FFT 瀑布图能很直观地分辨。

2.3 SDR 接收 GSM 信号的采样与增益:2.4Msps 的取舍

SDR 接收机输出的是复数基带 IQ 数据。采样率越低,覆盖频谱越窄,但每赫兹的处理资源越多。GSM 一个频点只占 200kHz,理论上 1Msps 就够,但接收机前端的镜像抑制和滤波器过渡带都依赖过采样留出余量。

我最常用的设置是 2.4Msps。这个采样率能覆盖 12 个 GSM 频点,带宽边缘的过渡带大约各留 200kHz,中心区域的平坦度最好。如果把采样率降到 1Msps,理论上能覆盖 5 个频点,但晶振偏差和滤波器波动会让最外侧的频点解调失败率明显上升。提到 2.4Msps 是工程上的平衡点:覆盖够宽,边缘质量够好,USB 传输带宽也稳定。

增益设置是一个经验问题。接收机的总增益可调范围大约在 0 到 50dB。开 AGC 在 GSM 场景下往往不稳,因为基站突发是时分多址的,信号功率在每个时隙之间跳变,AGC 跟不上这种快速变化。做法是固定增益,先扫一遍频谱,把目标频点的噪声底抬高到比背景高 6 到 10dB 的位置,再微调增益直到信号峰值不削顶。

提示:不要试图同时解调所有频点。GSM 是时分系统,一个接收机同时跟踪多个频点的计算量很容易跑不满实时。更稳的方式是先抓一个频点的 IQ 数据存盘,再离线做处理。离线意味着你可以反复调试解调参数,而接收机不用一直跑。

3. Python + SDR 环境搭建:硬件选型与最小工具链

3.1 硬件组合:SDR 接收机、天线与时钟源

做 GSM 嗅探实验的入门硬件组合很固定:一套能覆盖 900MHz 频段的接收机、一根 900MHz 频段的 1/4 波长垂直天线,以及一个稳定的时钟源。接收机不必追求最高端的型号,GSM 信号带宽窄,动态范围要求并不苛刻,入门级设备已经能完成整个协议栈的解码。

部件作用选择要点
接收机射频到 IQ 数据转换频率覆盖到 960MHz 以上,采样率稳定到 2.4Msps
天线接收 900MHz 下行信号1/4 波长单极或垂直地网天线,垂直极化
时钟源提供稳定采样时钟TCXO 优于普通晶振,GPS 驯钟更适合长时间捕获

时钟源是很多人忽略的环节。普通晶振的频率误差通常在 10 到 20ppm,在 900MHz 频段相当于 9 到 18kHz 频偏。GSM 的 GMSK 解调对频偏有一定容忍度,但 A5/1 破解需要精确的帧号,而帧号是靠时间累积推算的,频偏会导致符号定时一步步漂移。方案有两个:买带 TCXO 的版本,把频偏控制在 1 到 2ppm;或者在实验中使用 GPS 驯钟信号做参考。对离线分析来说,TCXO 够用;要做长时间连续捕获,GPS 驯钟更稳。

3.2 Python 虚拟环境与驱动安装

第一步是建立干净的环境。我的习惯是用 venv 隔离出专门实验环境,避免系统 Python 里的依赖互相踩踏:

python3 -m venv ~/gsm_exp source ~/gsm_exp/bin/activate pip install --upgrade pip pip install numpy scipy matplotlib pyrtlsdr pyyaml

pyrtlsdr 是接收机驱动的 Python 封装。安装后可以用一个最小脚本测试设备是否工作:

from rtlsdr import RtlSdr sdr = RtlSdr() sdr.sample_rate = 2.4e6 # 采样率,单位 Hz sdr.center_freq = 947.5e6 # 中心频率,落在 GSM 900 下行 sdr.gain = 40 # 固定增益,避免 AGC 抖动 samples = sdr.read_samples(8192) # 读一段数据,验证 DMA 通路

这段代码里,sample_rate、center_freq、gain 是 SDR 实验最核心的三个旋钮。sample_rate 按 2.4e6 设置的理由上一章已经讲过;center_freq 的选择要确保目标频点落在采样带宽中央;gain 固定后,read_samples 返回的就是 8192 个复数样本。如果返回的数据全是 NaN 或数值极低,通常是驱动或 USB 控制器的问题,要先检查设备枚举和 usb 权限。

注意:Linux 下接收机驱动默认需要 udev 规则才能免 sudo 访问。如果你在设备列表里看不到设备,多半是权限问题。先用相应系统命令确认设备枚举,再确认规则已加载。Windows 下则需要安装对应驱动,并在 Python 中指定设备索引。

3.3 天线架设与信号勘察:把频点挑对

在投入解码之前,花半小时做频谱勘察比什么都值。把接收频率范围设置为 GSM 900 下行全段,用瀑布图模式持续观察几分钟。正常 GSM 基站在频谱上会呈现周期性的窄带脉冲,每个脉冲持续约 0.577ms,表现在瀑布图上是一段段短促的亮线。持续存在的宽带信号一般不是 GSM,可能是 4G/5G 的边带泄漏或工业干扰。

天线位置的影响远超想象。室内窗边和室外阳台只差三米,信号强度可能差 12dB。GSM 900MHz 波长约 33 厘米,1/4 波长垂直天线的地网和垂直度都要认真调。天线尽量靠近窗户、远离金属框架和 LED 灯具。电源适配器对 SDR 的干扰也很大,劣质开关电源会在 900MHz 附近产生宽带杂散,把本来干净的频点彻底淹没。

选频点的方法:在瀑布图上找到最强的 GSM 载波后,先不要选它。最弱但稳定的频点往往更能说明解码链路的真实性能。强信号的基站可能离你只有几十米,多径衰落反而严重;弱信号基站通常距离更远,路径相对干净,突发脉冲的形态更接近理想信号。先拿一个中等强度的频点练手,确认整条链路通了,再回头挑战强信号频点的均衡。

4. 从 IQ 数据到明文短信:Python 解码链路逐段拆解

4.1 用 SCH 突发脉冲完成时间同步与频偏校正

GSM 的每个下行帧的特定时隙承载 SCH(同步信道),SCH 突发脉冲里包含一段 64 比特的训练序列,接收端依靠它与本地参考序列做相关,就能找到时隙边界和频偏。这是整个解调链路的第一环,相关峰的位置直接决定后续所有符号的定时。

在 Python 里做 SCH 捕获的简化流程如下:

import numpy as np # iq: 复数基带信号;sch_ref: 64 比特同步序列的 GMSK 参考波形 def find_sch(iq, sch_ref, threshold=2.0): # 用翻转参考序列做匹配滤波,得到滑动相关 corr = np.convolve(iq, np.conj(sch_ref[::-1]), mode='same') corr_abs = np.abs(corr) peak_idx = np.argmax(corr_abs) # 相关峰必须显著高于平均噪声水平,防止误检 if corr_abs[peak_idx] / np.mean(corr_abs) < threshold: return None return peak_idx

这个函数用滑动相关在整段 IQ 数据里搜索 SCH,相关峰幅度与背景噪声均值的比值是判决依据。threshold 设 2.0 已经比较严格,误检一次会让后续所有帧号错位。sch_ref 是把 64 比特训练序列按 GSM 规范做 GMSK 调制后得到的复基带波形,所以相关本质上是匹配滤波。

找到 SCH 后,相关峰处复数的相位还能用来估计频偏:把实际相关输出与理论值做相位差,就能得到残余频偏。用这个频偏值对 IQ 数据做数字下变频补偿。这一步省了,GMSK 差分解调的误码率会明显抬高,尤其是采样率较高、突发持续时间较长时。频偏估计的精度直接影响后续均衡的效果。

4.2 GMSK 解调与信道均衡的工程取舍

GSM 的 GMSK 调制指数为 0.5,是带限的连续相位调制。接收端可以用差分解调实现,但性能较差;工程上常用的是基于维特比算法的最大似然序列估计。对 SDR 捕获的数据来说,先做匹配滤波,再按符号周期抽样,然后计算状态转移度量。

简化的差分解调代码只适合检查和排错:

def gmsk_diff_demod(samples, sps): sps = int(round(sps)) phase = np.unwrap(np.angle(samples)) # 相隔一个符号周期的相位差 diff = phase[sps:] - phase[:-sps] bits = (diff > 0).astype(np.uint8) return bits # 使用示例:采样率 2.4e6,符号率 270.833e3 sps_calc = 2.4e6 / 270.833e3 # 约 8.86,取整后为 9 bits = gmsk_diff_demod(iq_burst, sps_calc)

sps 是每个符号的采样点数,2.4e6 除以符号率约等于 8.86,代码中取整到 9。这段代码的问题在于它把符号间干扰和噪声一起当成相位变化,低信噪比下误码率很高。真正可靠的做法是先用训练序列估计信道冲激响应,再跑 MLSE。GMSK 的频率脉冲长度是 3 个符号周期,信道记忆很短,MLSE 的状态数很小,完全可以在 Python 里实时跑。

信道均衡的效果要看训练序列位置的误码率。GSM 每个突发的训练序列是已知的,把解调后的比特与理论比特比对,就能得到该突发的误码估计。如果训练序列误码率高于 5%,先怀疑频偏校正,再怀疑定时偏差,最后才是均衡器参数。

4.3 去交织、卷积码与 A5/1 密钥恢复

解调出原始比特后,下一个环节是去交织和信道解码。GSM 的普通突发承载 114 个加密比特,业务数据经过卷积编码后被交织到多个突发,所以必须先把多个突发的比特收集齐,按交织规则复原,再做维特比解码。这个流程在现成工具里已经封装好,自己写的价值在于理解每一层的边界。

关于 A5/1,前面讲过它的破解依赖已知明文或密文校验。短信场景下,已知明文来自 SDCCH 上的系统消息,加密前后内容不变,所以你可以通过比对加密突发和已知明文恢复一段密钥流。下面只是逻辑示意,不是完整破解代码:

# encrypted_bits: 截获的加密突发;known_bits: 对应的已知明文 # 密钥流 = 密文 XOR 明文 keystream_bits = encrypted_bits ^ known_bits # 进入 A5/1 逆推,得到 64 位会话密钥 Kc # kc = invert_a5_1(keystream_bits, frame_number)

这里的核心参数是 frame_number。A5/1 算法把 22 位帧号作为参数混入初始化过程,每个帧的密钥流不同。所以逆推时必须知道这段密钥流对应哪个帧号。帧号来自 SCH 同步后对 TDMA 帧的计数,GPS 同步在这里起作用——时间每过一帧(约 4.615ms),帧号就加 1,从 SCH 初始帧号开始推算,时间基准不对,帧号就会偏得离谱。

实际工程中,A5/1 破解很少每次成功。目标基站的信号波动、信道解码残留错误、帧号偏差都会让查表失败。成熟的实验流程是:采集一段连续 IQ 数据,重复解调解码,把同一位置的密钥流候选值做投票表决,再交给查表。这样做会显著提高成功率,也会显著增加计算时间。

4.4 第三层信令解析:从 PDU 到短信正文

A5 破解成功或遇到 A5/0 之后,SDCCH 上的第三层消息就是明文。短信内容封装在 RPDU 里,外面还包了一层 LAPDm 协议。解析顺序是:先剥离 LAPDm 的地址和控制字段,再取上层数据,最后定位到 SMS 的 TPDU。

TPDU 前面的地址字段包含短信中心号码和发送方号码,都是 BCD 编码,同一字节的高低半字节各表示一个数字。解析时最容易出错的是号码长度字段:GSM 里地址长度以半字节为单位,很多人误以为是字节,导致号码整体错位。自己实现一遍这个解析非常有价值,因为所有短信解析工具都从这里开始。

短信正文的编码取决于数据编码方案字段。常见的有 7-bit 默认字符集、8-bit 数据和 UCS-2。中文短信绝大多数是 UCS-2,即每个汉字占两个字节,按 UTF-16BE 读取。解析时,先读编码方案字段,再选择解码器,顺序颠倒就会出现乱码。这一步是后面避坑章节要展开的高频故障点。

5. GSM 嗅探避坑指南:5 个高频翻车点

5.1 采样率保护带不足导致边缘信道解调失败

现象:目标频点位于采样带宽最外侧时,相关峰能测到,但解调出的误码率始终在 20% 以上,无论怎么调增益都无效。

原因:接收机前端滤波器的边缘幅度响应跌落,且镜像频率离得近,带宽边缘信噪比明显劣于中心。

解决:把采样率从 2.0Msps 提到 2.4Msps,并让目标频点处于频谱中央附近。如果受限于 USB 带宽,就微调中心频率,让目标频点移到频率窗的中心位置。同理,如果要跟踪多个频点,也要保证它们全部落在平坦区。

5.2 自动增益让频谱在强弱信号之间反复跳变

现象:瀑布图上基站的突发脉冲时有时无,解调结果一段时间全对、一段时间全错。

原因:自动增益按整体能量调整增益,GSM 是时分信号,脉冲出现时能量瞬间抬高,增益不断追赶导致波动。

解决:关闭自动增益,手动固定。调增益时不要盯着峰值看,要看脉冲之间空隙处的噪声底。把噪声底抬到比本底高 6 到 10dB,同时确保最强脉冲不削顶,这时候的动态范围基本合适。

5.3 短信正文乱码,问题不在破解

现象:号码解析正常,但正文输出一堆乱码,有的甚至是空字符。

原因:编码方案字段判读错误。把 UCS-2 的短信按 7-bit 解码,中文字符全变乱码;反过来,把 7-bit 短信按 UCS-2 解,会得到一长串空白字符。

解决:在解析正文前先打印编码方案字段的原始字节,对照协议规范逐位解析。注意字段高两位不同时对应不同字符集。做实验时准备一个能显示 hex 的调试工具,先看两个字节的 UTF-16BE 是否可读,再去确定解码路径。

5.4 A5/1 批量破解内存爆炸

现象:批量查密钥流时,内存占用从几百 MB 暴涨到几十 GB,进程被杀。

原因:批量查表时把整个彩虹表载入内存,或者索引分段没有与密文帧的时间顺序对齐。

解决:不要一次性加载全表。把彩虹表按帧号分段,每段对应一个时间窗口,按时间顺序逐段处理密文。这样内存占用稳定在单段大小,还能利用帧号连续性做候选密钥预筛。另外,确认查表脚本的索引类型是 uint64 而不是 Python int,后者的内存开销是前者的好几倍。

5.5 帧号漂移:GPS 同步失效的连锁反应

现象:解码和密钥流恢复逻辑上没问题,但结果始终对不上密文,而且错误看起来没有规律。

原因:帧号推算依赖绝对时间基准。GPS 驯钟失效或秒脉冲中断后,帧号计数器偏差导致每帧的初始化参数都错。

解决:在软件里记录 SCH 同步时的绝对时间和帧号,之后每收到一个突发就做帧号增量校验。如果连续两帧的帧号差不固定,立刻报警。GPS 信号在室内经常丢失,实验时把 GPS 天线放在窗口,或者改用时间戳更稳的 TCXO 方案。

6. 验证你的解码链路与合规边界

6.1 用已知短信做端到端回环验证

要确认链路是通的,最可靠的方法不是在真实网络上碰运气,而是在自己的实验环境里发一条已知短信。常见做法是用一台旧手机连接自己搭的微型 GSM 基站,让基站发送一条内容固定的短信,再用 SDR 在射频端抓取并解码,比对解码结果与原始短信。这套回环验证能同时检验同步、频偏校正、GMSK 解调、信道解码和第三层解析五段链路,哪一段出错都一目了然。

如果手上没有微型基站,退而求其次的做法是抓取广播信道上的系统消息,因为系统消息内容本身已知。只要解析出来的系统消息与协议字段一一对应,就说明链路前半段是健康的。短信的解码风险主要在后半段,所以回环验证还是值得做。

6.2 看信噪比和误码率判断链路健康

链路排错时,我最看重两个指标。一是同步相关峰与噪声均值的比值,这个值低于阈值说明接收信号不够强或频偏太大。二是训练序列位置的误码率,它能直接反映解调质量。把这两项指标输出成日志后,再去看是前端的射频问题还是后端的解码参数问题。训练序列误码率很高时先调天线和增益;训练序列误码率正常而正文乱码时,问题就在第三层解析。用指标分隔问题域,是排错最快的方式。

6.3 技术研究的红线不能碰

最后必须说清楚:被动接收空中信号、学习协议、做设备自检,这些在授权范围内是有价值的技术研究。但对别人的通信进行截取和解密,无论出于什么目的,都是明确的违法行为;哪怕只是把采集到的加密数据包存在硬盘里,没有用于传播,一旦涉及非授权场景,风险同样存在。我在某实验室做微型基站回环测试时,会先确认三件事:测试环境是否完全封闭、使用的手机和基站是否属于实验设备、采集到的数据是否最终会被删除。这三条下来,实验边界就清晰了。这也是我在 GSM 嗅探这个方向上最想分享的一条经验,技术本身无善恶,但边界要自己提前画好。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询