Author: k0shl of 360 Vulcan Team
简述
微软在Insider Preview引入了一个新的缓解机制来阻止普通用户创建硬链接(CreateHardlink),在逻辑漏洞的利用中,hardlink是一个非常实用且便捷的方法,当一个高完整性级别进程对低权限文件操作的时候(这里所谓低权限泛指normal user或更低权限用户可以完全控制的文件),可以利用hardlink将低权限文件链接到高权限文件(高权限是指需高权限例如SYSTEM操作的文件,比如C:\Windows目录下的绝大多数文件及子目录文件),从而会使高权限进程处理高权限文件(比如改变DACL,写入,创建等)这里简述一下利用hardlink的逻辑漏洞利用方法。
关于应用到hardlink技巧的漏洞可以参考Project Zero的James Forshaw的历史漏洞,他提交的逻辑类型漏洞中很多都应用到了hardlink的利用方法。
PS: 文中的代码示例均来自Insider preview(build 18898.1000),除了部分源码展示出处有单独说明。
Hardlink review
关于hardlink的创建可以参考James Forshaw的项目symboliclink-testing-tools (https://github.com/googleprojectzero/symboliclink-testing-tools),通过调用NtSetInformationFile设置文件的FileLinkInformation的属性将指定文件链接到目标文件上。
当然,符号链接曾经也是用来sandbox escape的有效手段,在AppContainter(或者低完整性级别进程中,例如Low Integrity)中,许多权限操作需要通过medium integrity进程来帮忙完成,这样可以通过一个AC可以操作的目录文件,硬链接到高权限文件,再利用Medium integrity进程来完成对高权限文件的操作,但微软加入了针对这种方法的Mitigation,如果是在AppContainer中调用NtSetInformationFile,会调用RtlIsSandboxToken检查进程Token,若在沙盒中,则设置需求的访问权限。其实现在ntoskrnl!NtSetInformationFile函数中:
if ( a5 == 0xB || a5 == 0x48 )// 0xB和0x48都是FileLinkInformation
{
memset(&Dst, 0, 0x20ui64);
SeCaptureSubjectContextEx(v6, *(_QWORD *)(v6 + 544), &Dst);
v65 = RtlIsSandboxedToken(&Dst, v7);//检查沙盒Token
SeReleaseSubjectContext(&Dst);
if ( v65 )
v14 |= 0x100u;//设置需求的访问权限,必须要有写权限
}
0xB和0x48是FileLinkInformation的enumerate,它最终会调用ntfs.sys中NtSetLinkInfo设置指定文件的硬链接到目标链接,其代码在ntfs!NtfsCommonSetInformation函数中实现
case 0xB:
case 0x48:
v47 = v11;
v34 = NtfsSetLinkInfo(v4, v73, v10, v9, v47);
ntfs.sys是Windows的文件系统驱动,其调用方法是在ntoskrnl的NT API中调用IofCallDriver,通过调用DriverObject的MajorFunction发送IRP封装,进入Ntfs!NtfsFsdSetInformation函数。
这里微软通过RtlIsSandboxedToken的方法阻止了AC调用硬链接,同样注册表和文件的符号链接,等方法也是通过类似方法缓解。
关于这次Insider Preview之前微软通常会通过两种方法来修补这类逻辑漏洞,第一种是模拟客户端,通过调用RpcImpersonateClient来模拟客户端Token,也就是说在接下来的上下文中,进程将以Client的权限执行代码,最后通过RpcRevertToSelf恢复原进程权限。第二种是通过调用GetFileInformationByHandle获取文件的属性,在GetFileInformationByHandle参数中有一个数据结构。
typedef struct _BY_HANDLE_FILE_INFORMATION {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD dwVolumeSerialNumber;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD nNumberOfLinks;
DWORD nFileIndexHigh;
DWORD nFileIndexLow;
} BY_HANDLE_FILE_INFORMATION, *PBY_HANDLE_FILE_INFORMATION, *LPBY_HANDLE_FILE_INFORMATION;
其中nNumberOfLinks会获取文件符号链接的数量,如果大于1则证明当前文件存在符号链接,则阻止后续操作进行。
关于修补漏洞的方法这里不做过多讨论,读者可以通过对补丁的对比找到微软修补此类漏洞的方法。
Hardlink Mitigation
在Insider Preview(build 18898.1000)中(可能更早),微软引入了针对hardlink的缓解措施,阻止普通用户创建高权限文件的硬链接,其主要思路是检查FileObject中ContextControlBlock的Flags,若其Flags不满足要求(RequirAccess),则最终调用SeAccessCheck检查进程对硬链接目标的真实访问权限,从而阻止普通用户创建高权限文件硬链接。
其主要代码在ntfs!NtfsSetLinkInfo中
if ( !(*(_WORD *)(a5 + 0x68) & 0x310) ) // mitigation
{
……
if ( !(unsigned __int8)TxfAccessCheck(
v6,
v90,
*(_QWORD *)(v5 + 168),
*(_QWORD *)(v6 + 200),
0,
0,
0x100u,
0,
0i64,
v91,
v99,
(__int64)&v156,
0i64,
&v157,
0i64) )
{
v24 = 0xC0000022;//Access Denied
if ( !NtfsStatusDebugFlags )
return (unsigned int)v24;
v84 = 995043i64;
goto LABEL_200;
}
TxfAccessCheck函数会调用SeAccessCheckEx检查进程的访问权限(写入,修改,删除等),若权限不满足,则返回0xC0000022拒绝访问。
我们需要回溯到外层函数调用NtfsCommonSetInformation来追踪a5+0x68的值,a5的值来自NtfsSetLinkInfo函数的第五个参数,关于回溯过程这里我不再赘述,a5的值来自于IRP封装的CurrentStackLocation,NtfsCommonSetInformation的第一个参数是一个IRP结构的参数,我简化了NtfsCommonSetInformation的代码,来看一下参数的传递过程。
signed __int64 __usercall NtfsCommonSetInformation@<rax>(_IRP *a1@<rdx>, __int64 a2@<rcx>, signed __int64 a3@<r15>)
{
v6 = a1->Tail.Overlay.CurrentStackLocation;
v8 = *((_QWORD *)v6 + 0x30);
v11 = *(_QWORD *)(v8 + 0x20);
case 0xB:
case 0x48:
v47 = v11;
v34 = NtfsSetLinkInfo(v4, v73, v10, v9, v47);
}
那么这个参数到底是什么呢,我们需要从CurrentStackLocation看起,CurrentStackLocation是IRP中的一个非常关键的成员,包括IRP封装调用Driver的方法MajorFunction都在此结构中,其数据类型是_IO_STACK_LOCATION。我们可以在这个地方下断点,命中时查看_IO_STACK_LOCATION结构。
3: kd> p
Breakpoint 0 hit
Ntfs!NtfsSetLinkInfo:
fffff803`8434d694 4c8bdc mov r11,rsp
1: kd> dq ffffa402da68f010+b8 l1
ffffa402`da68f0c8 ffffa402`da68f3b0
1: kd> dt _IO_STACK_LOCATION ffffa402`da68f3b0
ntdll!_IO_STACK_LOCATION
+0x000 MajorFunction : 0x6 ''
+0x001 MinorFunction : 0 ''
+0x002 Flags : 0 ''
+0x003 Control : 0xe0 ''
+0x008 Parameters : <anonymous-tag>
+0x028 DeviceObject : 0xffffa402`d68f6030 _DEVICE_OBJECT
+0x030 FileObject : 0xffffa402`dac559d0 _FILE_OBJECT
这里需要解释一下,我在调试时直接在NtfsSetLinkInfo下断点是因为其第二个参数就是IRP结构,所以可以通过IRP直接跟踪到第五个参数的值,这里IRP结构的地址是0xffffa402da68f010,而_IO_STACK_LOCATION在IRP+0xb8偏移位置。
可以看到_IO_STACK_LOCATION + 0x30位置是FileObject,这个FileObject就是硬链接目标文件的FileObject。
接着可以跟踪FileObject+0x20是什么。
1: kd> dx -id 0,0,ffffa402ddde7080 -r1 ((ntdll!_FILE_OBJECT *)0xffffa402dac559d0)
((ntdll!_FILE_OBJECT *)0xffffa402dac559d0) : 0xffffa402dac559d0 [Type: _FILE_OBJECT *]
[+0x000] Type : 5 [Type: short]
[+0x002] Size : 216 [Type: short]
[+0x008] DeviceObject : 0xffffa402d68cc860 : Device for "\Driver\volmgr" [Type: _DEVICE_OBJECT *]
[+0x010] Vpb : 0xffffa402d47fa3a0 [Type: _VPB *]
[+0x018] FsContext : 0xffffe388a39a11b0 [Type: void *]
[+0x020] FsContext2 : 0xffffe388a6297370 [Type: void *]
可以看到,其偏移+0x20处的对象是FsContext2,其值为0xffffe388a6297370,而第五个参数就是FsContext2,而a5+0x68检查的就是FsContext2+0x68位置的Flag。
我们可以从泄露的windows nt源码中找到关于FsContext2结构的蛛丝马迹,windows nt源码kdexts\ntfs.c的第963行
DumpCcb( (ULONG) File_Object.FsContext2, 1 );
Ntfs通过调用DumpFileObject函数中的DumpCcb函数获取ccb(ContextControlBlock),看下DumpCcb函数,ntfs.c的第806行:
VOID
DumpCcb (
IN ULONG Address,//FileObject->FsContext2
IN ULONG Options
)
{
……
pCcb = (PCCB) Address;
……
}
其实PCCB是上下文控制块,CCB中还包含了文件的信息,比如文件名。
1: kd> dt _FILE_OBJECT 0xffff800bbd4832c0 FsContext2
ntdll!_FILE_OBJECT
+0x020 FsContext2 : 0xffffcc8a`116bde50 Void
1: kd> dq 0xffffcc8a116bde50
ffffcc8a`116bde50 00000003`00880709 00000000`00000841
ffffcc8a`116bde60 00000000`00380026 ffffcc8a`1320b4b0
1: kd> dc ffffcc8a`1320b4b0
ffffcc8a`1320b4b0 0057005c 006e0069 006f0064 00730077 \.W.i.n.d.o.w.s.
ffffcc8a`1320b4c0 0073005c 00730079 00650074 002e006d \.s.y.s.t.e.m...
ffffcc8a`1320b4d0 006e0069 00000069 4134342d 31392d45 i.n.i
FileObject->FsContext2最终会直接被强制转换成PCCB对象,其实a5+0x68就是PCCB+0x68,这个值是由什么决定的呢?这需要经过一些逆向分析。这里我简述一下分析过程,首先,我们知道FsContext2的值来自于FileObject,而这些结构体都处于IRP封装中,在nt! NtSetInformationFile函数中:
LODWORD(v32) = IopAllocateIrpExReturn(DeviceObject, DeviceObject->StackSize, (unsigned __int8)(v31 ^ 1), retaddr);// Create IRP
v34 = v32;
Irp = v32;
if ( v32 )
{
……
*(_QWORD *)(v36 + 48) = v16; // Get FileObject
……
}
函数会创建IRP结构,如果创建成功,则会为IRP结构赋初值,其中v36+0x30是FileObject,v16的值来自于v83,而v83的则是通过句柄表获取的object,仍然在nt!NtSetInformationFile中。
v15 = ObReferenceObjectByHandle(Handle, v14, (POBJECT_TYPE)IoFileObjectType, v7, &v83, 0i64);
v16 = v83;
这个值是通过FileHandle获取的,FileHandle则是外层传入的,在James Forshaw的代码中通过OpenFile获取Handle,其打开的Access定义为MAXIMUM_ALLOWED,就是以最大的允许权限打开文件。
最终我们跟踪到NtOpenFile函数中调用ntfs!NtfsSetCcbAccessFlags设置+0x68偏移位置的flag,其调用路径为
2: kd> k
# Child-SP RetAddr Call Site
00 ffffca88`5a766e48 fffff807`52d32d20 Ntfs!NtfsSetCcbAccessFlags
01 ffffca88`5a766e50 fffff807`52d37382 Ntfs!NtfsCommonCreate+0x2080
02 ffffca88`5a767040 fffff807`4fa08829 Ntfs!NtfsFsdCreate+0x202
03 ffffca88`5a767270 fffff807`52045b3d nt!IofCallDriver+0x59
……
0c ffffca88`5a767980 fffff807`4fbdb3a5 nt!NtOpenFile+0x58
2: kd> p
Breakpoint 0 hit
Ntfs!NtfsSetCcbAccessFlags:
fffff807`52c47c90 4c8bdc mov r11,rsp
2: kd> r rdx
rdx=ffff800bc33b34e0
2: kd> dq ffff800bc33b34e0+b8 l1 // _IO_STACK_LOCATION
ffff800b`c33b3598 ffff800b`c33b3880
2: kd> dt _IO_STACK_LOCATION ffff800b`c33b3880 FileObject
ntdll!_IO_STACK_LOCATION
+0x030 FileObject : 0xffff800b`c3d13670 _FILE_OBJECT
2: kd> dt _FILE_OBJECT 0xffff800b`c3d13670 FsContext2 FileName
ntdll!_FILE_OBJECT
+0x020 FsContext2 : 0xffffcc8a`15611620 Void
+0x058 FileName : _UNICODE_STRING "\Windows\system.ini"
2: kd> dd 0xffffcc8a`15611620+68 l1
ffffcc8a`15611688 00000000
这里还未赋值,接下来跟踪到NtfsSetCcbAccessFlags如下上下文位置,具体代码如下
fffff807`52c47ce0 0fb74714 movzx eax,word ptr [rdi+14h]
fffff807`52c47ce4 6623c2 and ax,dx
fffff807`52c47ce7 66094168 or word ptr [rcx+68h],ax
3: kd> dq ffff800bc65d22b0+14
ffff800b`c65d22c4 02000000`001200a9
可以看到FsContext2+0x68值来源于0x1200a9和0x1a7与运算的结果,最后的值为0xa1,这个值会最后在NtfsSetLinkInfo中判断,而0x1200a9实际上就是ACE AccessMask,0x1200a9表示文件对当前进程只有Read Permission,而FullControl则是0x1f01ff,如果将File变成normal user可控的文件就会发现。
2: kd> dd ffff800b`bf2a7510+0x14 l1
ffff800b`bf2a7524 001f01ff
因此最后0xa1和0x310进行与运算的结果是0x0。
1: kd> p
Ntfs!NtfsSetLinkInfo+0x212:
fffff807`52d4d8a6 b910030000 mov ecx,310h
1: kd> p
Ntfs!NtfsSetLinkInfo+0x217:
fffff807`52d4d8ab 6641854d68 test word ptr [r13+68h],cx
1: kd> dd r13+68 l1
ffffcc8a`167399c8 000000a1
最终会进入SeAccessCheck检查文件访问权限,从而阻止创建硬链接。感兴趣的读者可以尝试用windbg修改FsContext2->Flag的值令其与0x310与运算后值为1,则可以创建高权限文件的硬链接。
石总nb!