写这篇东西的起因挺朴素:有天我收拾房间翻出一根吃灰多年的 RTL-SDR 电视棒,随手接上电脑扫了一圈频谱,发现原本以为早就“退网”的 GSM 频段里竟然还有活跃的信号。GSM 在我印象里是诺基亚 3310 时代的东西,实际上一查才知道,2G 网络的生命周期远比大多数人想象的漫长。既然信号还在广播,那能不能把这套“永不消逝的电波”解出来看看里面到底是什么?于是我用一个周末重新跑通了 GS M 空口信号的接收与解码全套链路,这篇文章就是那次完整实验的沉淀。
这套玩法不是什么机密,圈子里叫 GSM 空口监听:用软件无线电外设接收基站下行信号,再用开源工具链把 GMSK 调制、TDMA 帧、逻辑信道一层层扒开,最终在 Wireshark 里看到手机和基站之间的信令交互。它解决的典型问题是:没有运营商后台权限,怎么观察一个终端在网络里的附着、寻呼、位置更新过程。适合对移动通信协议感兴趣的无线爱好者、搞安全研究的新手,还有那些手里正好有个几十块钱电视棒想搞清楚它到底还能干什么的人。
我这里记录的不是从零讲 GSM 协议,而是把“从天线到 Wireshark”这条链路上每一步该用什么工具、为什么这么选、踩了什么坑,全部摊开讲。
1. 整体设计:从天线到 Wireshark 的四级链路
1.1 系统架构先画清楚
一次完整的 GSM 空口接收解码,按信号流向可以拆成四层:射频接收层、基带处理层、逻辑信道解复用层、协议解析层。
射频接收层负责把 935MHz 到 960MHz 的下行频段采下来,交给基带。基带处理层完成 GMSK 解调、突发脉冲检测、时隙同步、信道估计,输出一个“比特流”。逻辑信道解复用层再把不同的逻辑信道(BCCH、SDCCH、AGCH、PCH 等)分拣出来,打包成 GSMTAP 格式。协议解析层就是 Wireshark 的工作了,它收到的是一帧一帧的 LAPDm 帧,可以进一步解出 RR(无线资源)、MM(移动性管理)、CM(连接管理)等信令内容。
这套链路里每一层都有专门的对应工具:RTL-SDR 外加 gr-osmosdr 驱动搞定射频和采样;GNU Radio 里的 gr-gsm 模块完成 GMSK 解调和帧同步;gr-gsm 自带的几个 python 命令行工具负责把逻辑信道转成 GSMTAP;最后 Wireshark 做协议解析和人肉阅读。层与层之间的交接点是最容易出问题的,后面实操部分我会逐个说。
1.2 硬件选型背后的真实考量
很多人问是不是必须用 USRP 才能干这活,真的不是。GSM 下行基站的等效全向辐射功率(EIRP)可以达到 40W 以上,手机能收到的信号,一个几十块钱的 RTL-SDR 也不可能收不到。我用的是 RTL2832U 加 R820T2 调谐器的版本,理论频率范围覆盖 24MHz 到 1.7GHz,采样率上限 2.4MSPS,刚好覆盖 GSM 900 下行频段。
选 RTL-SDR 的核心依据是:GSM 一个绝对射频信道号(ARFCN)只占 200kHz 带宽,即使开 1MSPS 采样也能同时覆盖 4 到 5 个频点,抓单载波的 BCCH 控制信道完全够用。如果预算高一些想抓跳频或者更高带宽的 TCH,可以考虑 HackRF 或者 SDRplay,但对于信令级分析,RTL-SDR 是性价比最优解。
这里有个很重要的认知:空口监听的方向只能是你接收基站信号,也就是下行方向。手机发射的上行信号功率很小,而且离你很远的手机信号基本收不到。这决定了我们后续研究的主要对象是“基站广播给所有手机的那些公共信道”,而不是某个具体用户的语音链路。
2. 看懂空口之前的必知协议底板
2.1 频率资源与 ARFCN 换算
GSM 900 下行频段是 935MHz 到 960MHz,上行是 890MHz 到 915MHz,收发间隔 45MHz。每个频点间隔 200kHz,于是就有了 ARFCN 这个编号。下行频率换算公式很简单:下行频率(MHz)= 935 + 0.2 × ARFCN。如果扫频发现某个信号中心在 943.4MHz,那它的 ARFCN 就是 (943.4 - 935) / 0.2 = 42。
这个换算在后面的 gr-gsm 参数配置里非常关键,因为 grgsm_livemonitor 要求你传入的是以 Hz 为单位的下行中心频率,而不是 ARFCN。我自己一开始图省事,把 ARFCN 直接填进去,出来的全是乱码,排查了半天才反应过来是单位换算的问题。
上行频段在空口监听里基本用不到,但理解 45MHz 的收发间隔很有价值:它让基站和手机可以同时收发而不需要复杂的双工器设计,这也是 GSM 系统设计的精巧之处。
2.2 TDMA 帧结构与逻辑信道映射
GSM 的时间轴是一个多级嵌套结构。一个 TDMA 帧时长 4.615ms,分成 8 个时隙,每个时隙 0.577ms。这个 0.577ms 不是随便定的,它对应一个普通突发脉冲(Normal Burst)的时长。Normal Burst 有 156.25 比特,按 270.833kbit/s 的符号速率,正好是 0.577ms。
时隙里面放什么内容由逻辑信道决定。控制信道里面,FCCH 和 SCH 用于频率校正和同步,属于“广播控制信道”的开胃菜;BCCH 承载系统消息;PCH 是寻呼信道,通知手机“有人找你”;AGCH 分配专用信道;SDCCH 承载独立专用控制信令。每个逻辑信道在 51 帧的复帧里有自己的固定位置,gr-gsm 就是根据这些位置把不同信道拆出来的。
理解 TDMA 帧结构的意义在于:你去看 GSMTAP dump 的时候,Wireshark 里的每条消息都带着 ARFCN、时隙号、帧号,看起来像天文数字,其实背后就是这一整套复帧逻辑。
2.3 GMSK 调制与突发脉冲的前导序列
GSM 用的是 GMSK(高斯最小频移键控),这是一种连续相位调制,频谱效率高、带外辐射小。说实话,用手动方式去解 GMSK 是一件很痛苦的事,但 gr-gsm 里有现成的 GNU Radio 模块,它会自动完成符号同步、频率偏移校正、信道估计和最大似然序列估计。
不过了解突发脉冲的结构有助于排查“解出来全乱”的问题。普通突发脉冲里,首尾各 3 个尾比特,中间是 26 比特的训练序列,训练序列左边 58 比特和右边 58 比特是数据,最后还有 8.25 比特的保护间隔。训练序列的作用就是让接收机做信道估计和均衡,它是一段收发两端都知道的固定序列。如果同步失败,通常第一步就是去查训练序列的位置对不对。
3. 环境搭建:从裸机到 gr-gsm 跑起来
3.1 宿主环境选择与依赖安装
我强烈建议在 Linux 环境下操作。macOS 也有部分工具可用,但 Windows 下编译 gr-gsm 会折腾到你怀疑人生。我用的是 Ubuntu 22.04,GNU Radio 3.10。如果发行版太老,建议直接上较新的 Debian 系,否则编译中的 API 兼容问题会占用大量时间。
首先是安装基础依赖。gr-gsm 的前身是 airprobe,现在的项目由 osmocom 维护,依赖 GnuRadio、gr-osmosdr、libosmocore 等。在 Ubuntu 上可以这么装:
sudo apt install gnuradio gnuradio-dev gr-osmosdr libosmocore-dev \ libosmocodec-dev libosmosdr-dev swig python3-packaging cmake build-essential依赖版本没什么玄学,装最新的通常就行。如果用的系统源里 GNU Radio 版本和你最终编译 gr-gsm 期望的不一致,后面 code 生成阶段会报错,解决办法不是求神拜佛,而是用源码重编 gr-gsm 并手动指定路径。
3.2 编译安装 gr-gsm
git clone https://github.com/osmocom/gr-gsm.git cd gr-gsm mkdir build && cd build cmake .. make -j$(nproc) sudo make install && sudo ldconfig编译过程如果顺利,大概几分钟到十几分钟。装完之后,要把 GNU Radio 的模块路径确认一下:echo $GRC_BLOCKS_PATH。找不到 gr-gsm 的模块时,多半是环境变量没指到安装目录,在 .bashrc 里加一行:
export GRC_BLOCKS_PATH=/usr/local/share/gnuradio/grc/blocks另外 gr-gsm 命令行工具默认装到 /usr/local/bin,检查which grgsm_livemonitor是否正常输出路径。
3.3 RTL-SDR 设备参数确认与校准
设备参数直接决定解调质量。先跑一下 rtl_test:
rtl_test -t常见输出是一段设备列表,显示找到 1 个设备:Generic RTL2832U OEM。然后测温度补偿晶体振荡器的频率偏移:
rtl_test -p这个命令会让设备切换到几个频点并显示 ppm(百万分之一)。普通贴片晶振的 RTL-SDR 偏差可能在 20ppm 到 60ppm 之间,这个偏移量不校准的话,解调器很难锁定信号。如果你的棒子自带 TCXO,一般能控制在 1ppm 以内。实测下来我会在命令里显式带上--ppm=38这种参数。
还有个容易忽略的点:增益不要开满。自动增益模式下,如果某个强信号阻塞了前端,gr-gsm 会看到一片白噪声。我常用的增益在 35dB 到 50dB 之间,根据实时频谱来调,宁可弱一点也不要削顶。
4. 实操:扫频、抓包、读信令三步走
4.1 先用扫描器摸清周边基站
拿到设备后别急着解调,第一步是用 grgsm_scanner 扫描 GSM 900 下行频段,它会列出周边小区的主载波。命令格式:
grgsm_scanner --args="rtl" --gain=45 --ppm=38我跑出来的结果是大概每 200kHz 出现一个谱峰。grgsm_scanner 会给出 ARFCN、信号强度、是否包含 FCCH/SCH 等信息。所谓 FCCH 是频率校正信道,它的突发脉冲是全零序列,GMSK 解调后就是一段固定频率的正弦波,这就是开发商用来让手机确认“我找到了一个载波”的标记。SCH 紧随其后承载帧同步信息。
挑一个信号强度高的载波记下它的中心频率。这里用公式换算好,比如 ARFCN 62 对应下行频率 947.4MHz。注意这个频点必须对齐到 200kHz 栅格,否则后续解调也是白搭。
4.2 用 livemonitor 把信号变成 GSMTAP
扫频确认目标后,启动实时解调监视器:
grgsm_livemonitor --freq=947.4M --args="rtl" --gain=45 --ppm=38这条命令起来后,屏幕上会显示频谱瀑布图,同时监听指定频点。它内部完成 GMSK 解调、时隙同步、信道解复用,然后把逻辑信道的数据封装成 GSMTAP 包,通过本机 UDP 4729 端口发送到 loopback。
与此同时开 Wireshark,在回环接口捕获,过滤器写:
udp.port == 4729这时候你应该能看到源源不断的 GSMTAP 包。展开一个包,检查 GSMTAP 头的字段:version、hdr_len、type、ARFCN、时隙号。再往下是 LAPDm 帧,里面有 SAPI、地址字段、控制字段。如果 LAPDm 的地址字段和 SAPI 都是正常的,说明解调链路通了,这是整条链路最让人兴奋的一刻。
如果这里没数据,优先检查:频率是否对准栅格、ppm 是否正确、增益是否过高。我自己的经验是,频率偏差只要超过 3kHz,GMSK 解调基本就起不来,所以 ppm 校准的重要性怎么强调都不过分。
4.3 在 Wireshark 里拆出附着、寻呼和短消息
GSMTAP 到 Wireshark 之后,可读性马上就不一样了。Wireshark 把 LAPDm 之上的一层层剥开,你会看到 RR、MM、CM 三大层,还会把 SMS 消息识别成 GSM SMS。
典型观察目标有三个:
第一个是位置更新与 IMSI 附着。手机开机后,会在随机接入信道(RACH)上发出信道请求,基站分配 SDCCH,随后手机上报身份。如果网络里没有这个手机的 IMSI,就会要求它发送 IMSI;很多时候网络会用 TMSI 来保护身份,所以你看到的可能是 TMSI 而不是 IMSI。但只要是未加密的信令阶段,这些号码都能直接读出来。
第二个是寻呼。有人给手机打电话时,网络在 PCH 上发寻呼请求消息,里面带被叫终端的标识(TMSI 或 IMSI)。空口监听里最激动人心的就是看到自己手机的 TMSI 出现在寻呼消息里,那感觉就像在茫茫电波里捞到了属于自己的那条鱼。
第三个是短消息。短信在 GSM 网络里走的是控制信道 SDCCH 或 SACCH,不像语音占用专用业务信道 TCH。SMS 封装在 CM 层的 RPDU 里,里面包含 TP-UD(用户数据头和数据)。如果网络没有启用加密,Wireshark 直接把 SMS 文本显示出来。现实网络基本都加密了,但自己搭的实验环境或者部分老旧网络的测试模式下,明文短信是能直接看到的。
再往深处,你可以给 Wireshark 加一个过滤,把某个“手机号碎片”相关的所有消息关联起来:
gsmtap && (gsm_sms || gsm_map || gsm_rr)这样能快速定位一张 SIM 卡的接入过程:信道请求、立即指配、位置更新请求、鉴权相关消息、TMSI 重分配、加密模式命令、附着接受。这一整套流程就是移动通信教科书里讲的“网络附着流程”的真实空口版本,和仿真软件里的截图完全不是一个观感。
5. 常见问题与排查技巧实录
5.1 问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 没有 GSMTAP 包输出 | 频率没对准 ARFCN 中心 | 检查 ARFCN 换算、ppm 参数 |
| 有 GSMTAP 包但全是 Noise/FCS 错 | 增益过高或过低 | 在频谱窗口观察信号底噪,调到 35-50dB 区间 |
| LAPDm 帧能解出但 SAPI 异常 | 捕获到了非 BCCH/CCCH 信道 | 检查频点是否是主载波(BCCH 载波) |
| 解出的系统消息种类很少 | 周边信号弱或设备带宽不够 | 换高增益天线、靠近窗口、确认目标载波强度 |
| grgsm_scanner 一个频点都扫不出 | 频段已退网或天线损坏 | 手动把整个 935-960MHz 扫一遍,确认是否还有信号 |
| 编译时找不到 gsm 头文件 | libosmocore 没装或版本太老 | apt 安装 libosmocore-dev,重新编译 |
5.2 实测环节最值得说的三个坑
第一个坑是 ppm 校准。我最初用的是没带 TCXO 的白牌电视棒,默认跑到 947.4MHz 会偏出去 20 多 kHz。GMSK 调制的判决对频偏极其敏感,20kHz 在 200kHz 信道里算很大的偏移了,解调结果基本全是乱码。后来用 rtl_test -p 测出 38ppm 的整数偏移,在 grgsm_livemonitor 命令里加上--ppm=38,瞬间就正常了。如果你打算长期玩这个,建议直接买带 TCXO 的 RTL-SDR,省下大量校准时间。
第二个坑是我一开始总想让增益更大一点,觉得信号越强越好。结果在商场附近实测时,自动增益和手动高增益都导致接收机前端饱和,频谱上看到一个巨大的“平头”信号,但解出来的全是噪声。后来把增益降到 40dB 左右,用小信号模式接收,解调效果反而更好。这个经验说明,SDR 接收不是“音量大就有细节”,前端线性度比增益重要得多。
第三个坑是确认目标频点到底是不是 BCCH 载波。GSM 的一个小区会有好几个载波,但只有主载波(C0)上承载 BCCH/CCCH 这些控制信道。如果你把频点对准了一个纯 TCH 载波,grgsm_livemonitor 也能解出一些 LAPDm 帧,但内容非常有限,因为业务突发的训练序列位置和控制突发不一样。我一开始在商业区抓到一堆 TCH 上行碎片,差点以为这套工具链有问题。
5.3 一些能直接抄作业的经验
建议把 grgsm_scanner 的输出重定向到文件存下来,方便对比不同天线摆放位置和方向的信号变化。天线摆放对接收质量影响很大,我实测在窗边和屋子中央的扫描结果差了 10dB 以上。GSM 是垂直极化,天线尽量保持竖直,我用手边的半波偶极天线和电视棒自带的小天线对比过,自带天线也够用,但偶极天线在弱信号场景下优势明显。
调试时可以把 Wireshark 的显示过滤器保存成 profile,下次打开直接套用。我常用的几个过滤器:
gsmtap gsm_sms gsmtap && gsm_map gsmtap && gsm_rr.msg_type == 0x2c // 寻呼请求另外,gr-gsm 这套工具在 Linux 下如果和 PulseAudio 有冲突,可能出现音频接口占满导致程序启动失败,但实际跟音频完全无关。遇到这种情况直接检查/tmp下的”,看到有人说“安装失败。 无法接收 agent 发出的检测信号”之类的问题,基本是设备被别的进程占用,用fuser -v /dev/bus/usb/...或者直接拔插设备就能解决。
6. 合规边界与扩展思路
最后必须说一句:空口信号接收这件事,技术本身没有立场,但使用场景必须画清楚线。未授权抓取并分析他人通信内容,在很多地区是违反通信管理相关法规的。我个人做这套实验只针对自己搭建的测试网络、已授权的研究环境,以及自己的设备和 SIM 卡在公共网络上的基础信令行为。本文仅讨论技术路径,具体可不可行、合不合法,以当地法规为准。
做一个完整的闭环实验其实不难:用一台 USRP 跑 openBSC 或 YateBTS 搭一个微型 GSM 基站,再用另一台 RTL-SDR 做接收端,把自己手机接入这个基站,之后所有信令都能明文看到。这套环境让我第一次直观感受到什么是“附着流程”里的每一步,比读十遍协议书都管用。
从这篇实验往下走,还有几个方向值得深挖:一是用 Wireshark 的统计工具分析不同寻呼消息的比例,还原一个基站下的终端活跃度;二是结合多个 SDR 做多频段同时监听,把附近所有小区的 BCCH 控制面汇总到一张图里;三是研究 GSM 的跳频机制,虽然 RTL-SDR 带宽受限,但 HackRF 可以尝试捕获完整跳频序列。还有一点,GSM 协议栈里的各种异常包、错误码,本身就是一部移动通信的演化史,看看它们能让你理解为什么今天 4G、5G 的设计会是这个样子。
做这件事给我最大的感受是,电波世界里一个“永远在线”的旧协议其实一直在用最朴素的方式通信。它没有复杂的波束成形,没有正交频分复用,有的只是精心编排的时隙和训练序列。当你用一个电视棒把这些看不见的帧一帧揪出来,在电脑屏幕上排成表格的时候,那种“摸到了空中另一张网”的实感很强烈。如果你手头正好有一根吃灰的 RTL-SDR,花一个晚上跑通这条链路,我猜你也会有很多和我一样的意外收获。