Microsoft Hardlink缓解机制简单分析

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,则可以创建高权限文件的硬链接。

Comments
Write a Comment
  • 123 reply

    石总nb!