tl;dr; Calls to memmove(); that use a source buffer that is smaller than the destination buffer can be at times exploitable if the size value is bit aligned, is mapped in memory and that the original source buffer is also mapped in memory.

So, the other day I was debugging a vulnerability I had found and trying to understand the issue by performing a root cause analysis (RCA). I was, all up in windbg doing my thing, setting break points and getting all crazy. I ended up with the following breakpoint when messing in the windbg.

bp msvcrt!memmove ".if (poi(@esp+8)==0) {.printf \"calling memmove(0x%x, 0x%x, 0x%x);\\n\", poi(@esp+4), poi(@esp+8), poi(@esp+c);} .else {gc}"

So this breakpoint will only break into the debugger if a call to memmove has the second argument set to null. Checking my windbg log, I see the following: calling memmove(0x1613fe8, 0x0, 0x0161aa10);

Everyone knows that the memmove prototype across architectures is: void *memmove( void *dest, const void *src, size_t count );

So naturally, I attempt to investigate the situation.

1:001> !address 0x0161aa10
 ProcessParametrs 001812b0 in range 00180000 0018a000
 Environment 00180810 in range 00180000 0018a000
    015b0000 : 015b0000 - 00073000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageHeap
                    Handle   00530000
1:001> !heap -p -a 0x0161aa10
    address 0161aa10 found in
    _HEAP @ 530000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        01613fe0 0f48 0000  [00]   01613fe8    07a28 - (busy)
1:001> !address 0x1613fe8
 ProcessParametrs 001812b0 in range 00180000 0018a000
 Environment 00180810 in range 00180000 0018a000
    015b0000 : 015b0000 - 00073000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageHeap
                    Handle   00530000
1:001> !heap -p -a 0x1613fe8
    address 01613fe8 found in
    _HEAP @ 530000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        01613fe0 0f48 0000  [00]   01613fe8    07a28 - (busy)

As it turns out, the size value is actually a mapped heap chunk! To make it worse (or better), the least significant bytes are always mapped to offset 0xaa10, which I can control based on allocation size in the target. It looks like the developer of my target got his/her parameters mixed up! My guess is that the second and third arguments should have switched: calling memmove(0x1613fe8, 0x0161aa10, 0x0);

Anyway, when I continued execution, I was surprised to see the following output:

1:001> g
(1c38.1260): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0161aa10 ebx=00000000 ecx=00586a84 edx=00000000 esi=0161aa0c edi=02c2e9f4
eip=7687c120 esp=00129ba0 ebp=00129ba8 iopl=0         nv dn ei pl nz ac po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010612
msvcrt!memmove+0x1e0:
7687c120 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
1:001> dd @esi L1
0161aa0c  41414141
1:001> dd @edi L1
02c2e9f4  ????????
1:001> !address @edi
 ProcessParametrs 001812b0 in range 00180000 0018a000
 Environment 00180810 in range 00180000 0018a000
    01a33000 : 01a33000 - 0e5cd000
                    Type     00000000 
                    Protect  00000001 PAGE_NOACCESS
                    State    00010000 MEM_FREE
                    Usage    RegionUsageFree
1:001> !heap -p -a @esi
    address 0161aa0c found in
    _HEAP @ 530000
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        01613fe0 0f48 0000  [00]   01613fe8    07a28 - (busy)

An Out-of-Bounds write on unmapped memory? How could this be? Well as it turns out, the copy operation is doing a backwards copy. To find that out I dove into the Microsoft’s implementation of memmove within the 32bit architecture. I used the following DLL: C:\Windows\System32\msvcrt.dll v7.0.7601.1744 (latest at the time of writing).

void *__cdecl memmove(void *Dst, const void *Src, size_t Size)
{
  const void *v3;
  void *v4;
  size_t v5;
  void *result;
  int v7;
  int v8;
  unsigned int v9;
  signed int v10;
  unsigned int v11;

  v3 = Src;
  v4 = Dst;
  if ( Dst <= Src || Dst >= (char *)Src + Size )      // passed the check size dst > src (0x0) and dst < src+size
  {
    ...
  }
  v7 = (int)((char *)Src + Size - 4);                 // set the src buffer based on size value - 0x4
  v8 = (int)((char *)Dst + Size - 4);                 // set the dst buffer based on size value - 0x4
  if ( !(v8 & 3) )                                    // check if dst is bit aligned to the cpu
  {
    v9 = Size >> 2;
    v10 = Size & 3;
    if ( Size >> 2 < 8 )                              // jump if (size / 4) is < 0x8 (which, it wont be if its a large value)
    {
LABEL_36:
      switch ( -v9 )
      {
        case 0u:
          break;
      }
    }
    else                                              // else, we are here
    {
      qmemcpy((void *)v8, (const void *)v7, 4 * v9);  // out of bounds copy
      ...
    }

As you can see, its very similar to OS X’s implementation or GNU’s implementation. So the same situation would happen on Linux or Mac OS X under 32 bit implementations. What you will notice here is that there is no sanity check on the source buffer whatsoever inside memmove. The source buffer is NULL? No worries, continue execution. However, do note that if NULL is not mapped, then an access violation will occur since the backwards copy performs a – instead of a ++. Thanks to @badd1e for pointing this out.

Depending on how you are targeting your exploitation, an access violation might be ok as you can potentially use the initial out-of-bounds write to target an unhandled exception function pointer.

Regarding exploitation, the following is needed for exploitation:

  • (void *)0x0 as the source buffer. Actually, as long as the src buffer is smaller than the dst buffer, this is still possible.
  • You will likely need whatever the src value is, to be mapped in memory. If it is null, then the null page will need to be mapped to survive the copy operation.
  • A bit aligned size value, in my case it was 32bits or 4 bytes
  • The destination + size to point to a mapped and writable location in memory.
  • The size value to be a valid pointer to controlled data, rare indeed.

Summary

Now I know what a lot of you neckbeards are going to say, that developers should be careful about the parameters parsed to mem* functions. But the simple matter is, is that a simple check for a source buffer that is not mapped would have made this particular vulnerability un-exploitable.

Whilst this s a very bizarre corner case, it goes to show that the lack of sanity checking for the sake of speed can cause all sorts of undesired effects, potentially leading to an exploitable condition.

Since I can relatively control the size value (based on the allocation bucket) and the source buffer passed into memmove() is always NULL, I can trigger a relative wild write at a semi-controlled location. Sure, not the most amazing primitive, but when the application is installed and running as SYSTEM on 99% of enterprise applications, hackers become motivated.

A big thanks goes out to @rohitwas for his validation of my insanity and @badd1e for pointing out that the src buffer (be it null or not), needs to be mapped in memory!