IEEE802.11数据帧在Linux上的抓取
终于得到了梦寐的《802.11无线网络权威指南》,虽然是复印版本,看起来也一样舒服,光看书是不行的,关键还是自己练习,这就需要搭建一个舒服的实验环境,抓包是必不可少的了,因为只有详细分析802.11数据帧,才能深入理解协议的细节。软件上就是这个理,手上没设备还是不行,这可是搭建实验环境的第一步,巧妇难为无米之炊。设备问题很好解决,买一个就行了,最好买适合DIY的那种,既便宜又不怕折腾坏了,因此淘宝是一个好去处。我搞到了一个ralink的802.11bgn的无线网卡,USB2.0的,除了有点发热之外别的都很好,软AP信号足,功率够,速度快。就是驱动不太给力啊。
硬件有了,先驱起来再说,完事之后就要想办法抓包了,千万别以为抓包很容易,一个软件就搞定,想抓取802.11的数据包,还真得下一番功夫啊,本文接下来的部分就谈谈我的802.11抓包经历。
Linux内核的抓包机制软件上全在ptype_all这个链表,而不存在所谓“网卡混杂模式”,混杂模式主要是硬件上的概念,有些网卡会在芯片内部完成MAC地址的过滤,因此必须让芯片“知道”不要过滤任何地址这一件事,因此就有了混杂模式的概念,对于无线网络,混杂模式在理解上更加复杂,因此“不要过滤任何数据帧”和802.11规定的AP地址过滤某些行为上是矛盾的。举个例子,一个没有和此AP建立关联的移动节点发来的包,AP按照802.11规范是要丢掉它的,然而混杂模式又要求接受它,这就是矛盾。矛盾的本质原因在于无线链路的“无边界”特征,这是电磁波的物理特性导致的。对于此谁也无能改变,因此ESS无线网络就不能设计成一个完全的广播网络,否则一个三维空间的广播造成的冲突要浪费多少资源啊--我们知道有线局域网以太网最初是一个一维线缆上的广播而已,现在有了交换机,CSMA/CD基本已经不再被使用了。只能由AP接入点来负责在移动节点之间转发数据帧,因为只有它知道谁跟自己建立了关联。这种区别造成了无线网络抓包的尴尬。
在设计上,操作系统完全避免了这个尴尬,要么它根本不允许在802.11这个层次上进行抓包,要么由驱动决定如何实现抓包。对于Linux,高版本内核实现了统一的802.11适配层框架,驱动的实现可选使用,低版本内核完全由驱动来决定能否实现抓包;对于Windows,如果你使用WireShark工具在无线网卡上进行抓包,会得到以下错误:
在Wireshark的网站上,也有类似的说法:
这个may not未免太无耻了。然而tcpdump的手册上却说可以在monitor mode下抓取802.11帧,然而这却需要驱动来支持。对Linux而言,虽然较高版本的内核支持了802.11适配层,然而却不是每个驱动的实现都遵循了这个框架,比如我手上的ralink的驱动就很扯,完全按照windows的那一套来写的。因此依靠monitor mode来抓取802.11帧这完全靠不住!接下来分析Linux的抓包机制和802.11适配层的关系以及衔接。
对于进入的包,包在netif_receive_skb中被截取,对于出去的包,则在dev_queue_xmit,而这两个地方全部都在802.11逻辑的上面,这就涉及到了802.11的设计。
1.作为有线局域网的扩展,要可以和有线局域网无缝桥接,也就是对arp协议透明;
2.由于无线网络物理链路的特殊性,需要设计特殊的帧格式以满足需要。
这两点看上去是矛盾的,其实不然。有两种方案可实现,其一就是将以太帧封装在802.11帧里面,类似隧道那样,其二就是直接通过以太帧构造802.11帧,两种帧只要能保证可以互相生成即可。802.11采用的正是这种方式,数据帧发往无线链路时,由以太帧生成802.11帧,反过来由802.11帧生成以太帧。
这就需要做一个适配层,用来转换两种帧,在linux中,这种适配层表述的很清晰。基于2.6.32的内核,$SRC/driver/net/mac80211目录下面的代码就是适配层的实现。值得注意的是,旧一点的内核版本虽然没有这么清晰的目录结构,代码也是很明白的,总的实现如下图所示:
注意,适配器适配的两种帧并不是平行的,在协议栈上是上下的关系,这是因为我们已经习惯了以太网,因此需要将802.11适配到以太网,而不需要反过来适配,因此对外只需要呈现一个以太网卡接口即可,没有必要再显示一个“无线网卡设备”接口。最终的结果就是802.11和802.3更像是协议栈上的上下层关系,而不是两种平行的协议。
由于802.11的处理逻辑完全在“网卡设备”接口以下,而抓包逻辑则在“网卡设备”接口以上(netif_XX/dev_YY),而Linux内核看到实现了802.11的网卡设备时,所有路径均已经经过了适配层,因此抓包逻辑看到的“无线网卡”上的流量就都成了以太帧了。另外对于实现了基础结构BSS无线AP的无线网卡而言,有时它转发的是两个无线站点之间的流量,为了使“网卡设备”接口这种流量,需要将所有的802.11数据帧在抉择是发往有线网还是发给另一个无线站点之前全部进行适配,适配成以太帧,如下图所示:
这样,我们知道了为何无法使用tcpdump抓取802.11帧了,接下来就是想办法抓取到这种帧。最关键的事情就是找到在哪里802.11帧转化为了以太帧以及相反的转化,而这很容易,在2.6.32内核中,ieee80211_invoke_rx_handlers函数里面取到的就是802.11帧,不过注意,一定要在__ieee80211_data_to_8023调用之前,对于其它的内核版本,也是类似的。找到了这个之后,接下来需要将以下的代码添加到你找到的位置:
list_for_each_entry_rcu(ptype, &ptype_all, list) {
if (ptype->dev == null_or_orig || ptype->dev == skb->dev ||
ptype->dev == orig_dev)
ret = deliver_skb(skb, ptype, skb->dev);
}
}
ptype_all并没有导出,那么可以通过/boot/System.map-2.6.32-5-amd64这个文件中取得,这样就可以使得tcpdump抓取到802.11的数据帧,原汁原味的。同样的方法也可以用于抓取发出的802.11数据帧。 到此为止,不得不插一句,来看看依靠802.11适配层也就是wireless框架如何来实现抓包,正如tcpdump的手册上写的那样,注意这只能在高版本的内核上才行得通,我手上的2.6.37则刚好。在框架内部,实现了ieee80211_rx函数,该函数被驱动调用,接收来自驱动的802.11数据帧,其中调用了ieee80211_rx_monitor来实现802.11数据帧直接上传到“网卡设备接口”,从而越过了适配层将802.11帧适配到以太帧这一步,ieee80211_rx_monitor中式这么实现的:
list_for_each_entry_rcu(sdata, &local->interfaces, list) {
if (sdata->vif.type != NL80211_IFTYPE_MONITOR)
continue;
if (sdata->u.mntr_flags & MONITOR_FLAG_COOK_FRAMES)
continue;
if (!ieee80211_sdata_running(sdata))
continue;
if (prev_dev) {
skb2 = skb_clone(skb, GFP_ATOMIC);
if (skb2) {
skb2->dev = prev_dev;
netif_receive_skb(skb2); //直接调用设备接口层的函数实现之
}
}
prev_dev = sdata->dev;
sdata->dev->stats.rx_packets++;
sdata->dev->stats.rx_bytes += skb->len;
}
这种方式看起来和我的方法没什么两样,实则更妙,hack一个链表头总显得不那么正规,而导出一个函数却很显而易见。然而如果驱动不遵照这个框架来写,框架里面再好的东西也没法使用,事实上现有的Linux无线网卡驱动很多都是基于ndiswrapper的,这样可以直接加载windows上的驱动程序,如果事情是这样,那还好,糟糕的是,有人写的驱动竟然是个四不像。下文分解。
事情往往要比你想的复杂得多,如果你认为上面的工作已经完成了一大半,那就大错特错了,实际完成的工作才不到1%...!!!我的ralink无线网卡的驱动在2.6.32上根本就编译不过,后来找了一个Debian4的虚拟机,编译过了,可是没法识别USB2.0的无线网卡,于是索性安装VMWare-tools,折腾了一上午,费了好大劲,终于装上了VMWare-tools,还是无法识别,于是就又开了一个Redhat虚拟机,内核版本2.6.18-92.el5,终于既可以编译驱动又可识别网卡了,...期间,装一台物理机器的心都有了!接下来终于改修改代码了,一打开驱动源码,MD,全部是从Windows的NDIS驱动里面移植过来的,Linux内核提供的诸多的802.11适配代码完全没有使用,全部都是自己编写的,仅仅在最上端使用register_netdevice之类的接口和内核衔接。
困难一波接着一波,那就一个一个解决。虽然NDIS的代码不是很熟悉,但基本的逻辑还是清楚的。在$SRC/sta/rtmp_data.c中找到了STAHandleRxDataFrame函数,就是它了:
// 1. skip 802.11 HEADER
{
pRxBlk->pData += LENGTH_802_11;
pRxBlk->DataSize -= LENGTH_802_11;
}
一下子就找准了,skip 802.11 HEADER说明没有skip之前就是802.11帧了,于是将上述的list_for_each_entry_rcu代码加到这个代码上面,然而且慢,我们需要自己构造skb啊,这可真的麻烦了,因为这个驱动完全是自己处理的,没有用到skb...不管怎么说,在写构造skb的代吗之前先用printk将802.11帧打印出来再说,于是添加的代吗变成了下面的样子:
{
int i = 0;
char *p = pRxBlk->pData;
printk("###################### begin:%d\n", pRxBlk->DataSize);
for (i = 0; i < pRxBlk->DataSize; i++) {
printk("%02X ", *p & 0x000000ff);
p++;
}
printk("%d \n", ret);
printk("###################### end\n");
}
然后编译,加载rt3070sta.ko,调用以下的命令:
ifconfig ra0 up
iwconfig ra0 mode Managed
iwcondig ra0 essid "zhaoya"
ifconfig ra0 190.168.1.123/24
ping 190.168.1.100
使用dmesg查看的结果如下:
这里就不再解释802.11帧每一个字段的具体含义了,涉及到很多细节,比如to ap,from ap等等,不管怎么说,成功抓取了802.11的帧,接下来就是如何使用正规的方法来做了,所谓的正规方法就是使用AF_PACKET套接字。
唉,真的不明白*那些写ralink驱动的那帮家伙为何如此写驱动呢,Linux内核现成的难道不好吗?搞的不伦不类的,光那些函数变量命名就和GNU的风格不符合,也许驱动是首先在Windows上调通的吧,这样为了重用也没什么不可以,然而可苦了我们DIY一族了,当然Win粉这里不论...
可我搞不明白,为何不直接用ndiswrapper来安装Windows的驱动程序呢,而非要把Windows的驱动移植到Linux,最终不伦不类的...ndiswrapper是什么?下面简述一下。
ndiswrapper给那些不想自己编译无线网卡的家伙们带来了福音,你可以直接使用windows的二进制驱动程序(sys文件,本质上是PE格式)。这怎么可能呢,其实很简单,ndiswrapper就是为Windows驱动程序构造了一个“家园”,一个基本的运行环境,满足Windows驱动的各类调用,这个环境分为两类,第一类就是Windows的内核基本环境,第二类就是NDIS的环境,说白了就是提供一些函数并且导出即可,ndiswrapper的示意图如下:
ndiswrapper给出了两套接口以及其Linux实现,这样Windows的PE驱动文件经过简单处理后就可以正常运行了,Windows驱动程序本身看来外部环境和Windows的一模一样,该调用的函数都有,它只管接口,并不管实现(针对接口编程!)。这个原理和Linux用户态的Wine是一样的,Wine也是提供了一个外部环境,比如诸多的dll,Wine的实现要比ndiswrapper更加复杂,因为库太多了。
如果使用ndiswrapper,怎么实现802.11的抓包呢?虽然说Wireshark的网站上的一篇《WLAN (IEEE 802.11) capture setup》明确提到:
Windows
Capturing WLAN traffic on Windows depends on WinPcap and on the underlying network adapters and drivers. Unfortunately, most drivers/adapters support neither monitor mode, nor seeing 802.11 headers when capturing, nor capturing non-data frames.
Promiscuous mode can be set; unfortunately, it's often crippled. In this mode many drivers don't supply packets at all, or don't supply packets sent by the host.
但是修改ndiswrapper还是很容易的,注意,这个NDIS并不是WIndows上的NDIS,这个NDIS只是一个接口集合,其实现还是Linux,具体怎么做,不说了。