Home CVE-2023-24869 - Remote Procedure Call Runtime Remote Code Execution Vulnerability - Brief Analysis
Post
Cancel

CVE-2023-24869 - Remote Procedure Call Runtime Remote Code Execution Vulnerability - Brief Analysis

EDIT/Updated Info

So, after initial analysis and release of this post, I discovered that other CVE’s were reported in relation to RPC, and both usermode & kernelmode compnents relating to RPC were also updated.

Additional CVEs:

Usermode Components Updated:

  • rpcss.dll
  • rpchttp.dll
  • rpcrt4.dll

Intro

Hello everyone, I had some time this evening to take a quick look at another patched kernel driver on March 14th 2023 from microsoft, this time it’s reported as RCE within the Kernel RPC Provider (msrpc.sys - I believe this is the related kernel driver). If anyone else wants to create a debug environment to test and verify, that would be cool to see happen :)

Tools

  • BinDiff - https://www.zynamics.com/bindiff.html
  • Ghidra - https://ghidra-sre.org/
  • Binary Ninja - https://binary.ninja/
  • BinExport - https://github.com/google/binexport
  • WinBinDex - https://winbindex.m417z.com/

File Information

  • Windows Version: 11 22h2
  • Patched Version: 10.0.22621.1413
  • Non-Patched Version: 10.0.22621.1105
  • Binaries: https://winbindex.m417z.com/?file=msrpc.sys

Diffing

So, when patch diffing the first task that we want to complete is that of identifying the associated files related to the CVE that we are reviewing. Luckily in our case, given the information MSFT provides along with the CVE we are able to easily determine that the kernel driver responsible for (I hope this is the right one haha) msrpc.sys

You will have to utilize either Ghidra, Binary Ninja, or IDA along with the extension BinExport. Note that Binary Ninja recently updated their version so BinExport no longer works unless you use an older version of Binary Ninja. Similarly with Ghidra, you will want to download Ghidra 10.2.2 to be able to import the BinExport extension.

You will then utilize the BinExport extension, and utilize BinDiff to analyze both the Non-Patched and Patched Binaries. If we look into some of the data gathered from BinDiff we will be able to identify that only two functions appears to have been modified within the msrpc.sys kernel driver, as seen in this image.

BinDiff - Two Functions Modified

So, I am not that super familiar with BinDiff; however, I believe that given we have only 2 recorded differences in similarity that only two functions were modified.. unless does the Confidence level also matter? I looked at a few with low confidence but labeled at 1.00 and they were exactly the same…

Let’s take a look at both of these functions within Binary Ninja (We will get PDB’s loaded within Binja, so we can just easily locate the address -> function name).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
uint32_t SIMPLE_DICT::Insert(SIMPLE_DICT* this, void* __ptr64 arg2)
{
     uint64_t rdi = 0;
     int32_t rdx = *(int32_t*)((char*)this + 8);
     int64_t rax_1;
     if (rdx != 0)
     {
         rax_1 = *(int64_t*)this;
         while (*(int64_t*)(rax_1 + (rdi << 3)) != 0)
         {
             rdi = ((uint64_t)(rdi + 1));
             if (rdi >= rdx)
             {
                 break;
             }
         }
         if (*(int64_t*)(rax_1 + (rdi << 3)) == 0)
         {
             *(int64_t*)(rax_1 + (rdi << 3)) = arg2;
             *(int32_t*)((char*)this + 0xc) = (*(int32_t*)((char*)this + 0xc) + 1);
         }
     }
     uint32_t rax_2;
     int32_t rax_4;
     if ((rdx == 0 || (rdx != 0 && *(int64_t*)(rax_1 + (rdi << 3)) != 0)))
     {
         rax_2 = SIMPLE_DICT::ExpandToSize(this, (rdx + rdx));
         if (rax_2 == 0)
         {
             rax_4 = -1;
         }
         else
         {
             *(int64_t*)(*(int64_t*)this + (rdi << 3)) = arg2;
             *(int32_t*)((char*)this + 0xc) = (*(int32_t*)((char*)this + 0xc) + 1);
         }
     }
     if ((((rdx == 0 || (rdx != 0 && *(int64_t*)(rax_1 + (rdi << 3)) != 0)) && rax_2 != 0) || (rdx != 0 && *(int64_t*)(rax_1 + (rdi << 3)) == 0)))
     {
         rax_4 = rdi;
     }
     return rax_4;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint32_t SIMPLE_DICT::ExpandToSize(SIMPLE_DICT* this, uint32_t arg2)


{
    uint64_t rax = ((uint64_t)*(int32_t*)((char*)this + 8));
    if (arg2 > rax)
    {
        rax = ExAllocatePool3(0x100, (((uint64_t)arg2) << 3), 0x4d637052, &data_1c000b4b0, 1);
        if (rax != 0)
        {
            uint64_t rbx_1 = ((uint64_t)*(int32_t*)((char*)this + 8));
            uint64_t rdi_1 = (rbx_1 << 3);
            memcpy(rax, *(int64_t*)this, rdi_1);
            memset((rdi_1 + rax), 0, (((uint64_t)(arg2 - rbx_1)) << 3));
            int64_t rcx_2 = *(int64_t*)this;
            if ((rcx_2 != ((char*)this + 0x10) && rcx_2 != 0))
            {
                ExFreePoolWithTag(rcx_2, 0x4d637052);
            }
            *(int64_t*)this = rax;
            rax = ((uint64_t)arg2);
            *(int32_t*)((char*)this + 8) = arg2;
        }
    }
    return rax;
}

At this point these are both of the modified functions we gathered from BinDiff; however, what also changed?

ULongMult() To The Rescue

Well, today is when we learn about ULongMult() function, this is a sanity check function to detect integer wrapping \[over/under\]flow with multiplication operations, so before we continue, lets trace the arguments passed to SIMPLE_DICT::ExpandToSize() as we can see that we got some memory allocations/assignment operations happening here that utilize both arg2 & this+8.

1
2
3
4
5
6
7
8
9
10
11
uint32_t SIMPLE_DICT::Insert(SIMPLE_DICT* this, void* __ptr64 arg2)
{
     uint64_t rdi = 0;
     int32_t rdx = *(int32_t*)((char*)this + 8);
     int64_t rax_1;
[SNIP]
     if ((rdx == 0 || (rdx != 0 && *(int64_t*)(rax_1 + (rdi << 3)) != 0)))
     {
         rax_2 = SIMPLE_DICT::ExpandToSize(this, (rdx + rdx));
[SNIP]
}

As we can see, RDX is an int32 data type, and gets passed as the second argument to ExpandToSize() by adding it self together.

Binary Ninja - SIMPLE_DICT::Insert()

Now we will look at the patched version, it appears that the value at this+8 is now passed as an argument to ULongMult() and is multiplied by 2, which checks out within the Non-Patched function (rdx+rdx), and the resulting value is stored within arg_8; however, we also notice a check on the result from ULongMult() which according to MSDN:

If the operation results in a value that overflows or underflows the capacity of the type, the function returns INTSAFE_E_ARITHMETIC_OVERFLOW and this parameter is not valid.

If this function succeeds, it returns S_OK. Otherwise, it returns an HRESULT error code.

Binary Ninja - SIMPLE_DICT::ExpandToSize()

Reviewing the patched SIMPLE_DICT::ExpandToSize() we notice the usage of ULongMult() again and it is executed at the start before all the allocations/memory operations are performed.

I didn’t set up a environment to test and verify; however, in my mind, this all seems quite interesting :) could also be wayyyyy off because well, without verification I could be completely wrong but maybe I am on the right path, not entirely sure but seemed interesting to me.

Hope you all have a great new week ahead of you!

This post is licensed under CC BY 4.0 by the author.