Diving Into QueueJumper

Background

Check Point Research recently disclosed three security bugs in the Microsoft Windows MSMQ service. The most critical bug disclosed was CVE-2023-21554. They’ve named this bug QueueJumper and claim that it can result in unauthenticated remote code execution.

Recently, a few coworkers discovered some vulnerable systems on their network penetration tests and were trying to find public exploits for this bug. The best they were able to find was this PoC on Github. The README didn’t say what this PoC was supposed to do (at least not in English, anyway), but looking at the screenshot and the code, it seemed likely that it would simply crash the service.

I thought I’d try to dig into this PoC and see if I could find a way to modify it to do something useful, like execute arbitrary code.

Lab Setup

Vulnerable Windows Version

In order to even test out the PoC, I needed a lab system running the MSMQ service. I found some older Windows 10 ISOs hosted on archive.org. I downloaded one from July 2022, figuring that one should be vulnerable. I installed it into a VM and disabled Windows Updates.

Installing MSMQ

The Microsoft Message Queuing (MSMQ) service isn’t installed by default. You can install it easily using Windows’ “Turn Windows Features on or off” feature.

Install MSMQ

Once installed, there will be a new service entry called “Message Queuing”.

New Service

You can also view and manage queues from the “Computer Management” control pane.

Computer Management

Installing tools

I also installed the following tools on the vulnerable server:

  • WinDbg Preview
  • IDA Free
  • Ghidra
  • Visual Studio

Proof of Concept

Testing the PoC (PoC)

The next step was to actually test the PoC exploit against the service. I attached WinDbg to the MSMQ service (mqsvc.exe) and executed the PoC against my Windows VM and found that the service crashed.

(2c70.81c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
MQQM!CQmPacket::CQmPacket+0x90a:
00007ffb`dc9e88aa 48c7030c000000  mov     qword ptr [rbx],0Ch ds:000002e4`5c9a0320=????????????????

The application tried to write a single byte of 0x0C to the address stored in RBX (000002e4`5c9a0320). In this case, that location was unmapped so the byte could not be written and the service crashed.

Analyzing the PoC

The author of that PoC (zoemurmure) has a detailed write up about what they found when reverse engineering the Windows patch and how the PoC works. The post is not in English, but I found that Google translate worked really well and I was able to understand everything just fine.

Without getting into too much detail, the application has a CQmPacket::CQmPacket function which is used to parse incoming TCP queue messages. The messages can contain various “sections”, each with its own header. The included sections and headers depends on how the sender configured the message. This function checks for the presence of the various sections and parses each one, building the packet in memory.

Zoemurmure found that one particular header attempts to build a segment of the packet without performing any checks against the header data.

Note: Many variable names below were manually configured in Ghidra. Zoemurmure did the hard work of figuring out which variables corresponded to which parts of the message. I just copied those variable names. I have not double checked them to confirm accuracy.

if ((userHeaderFlag >> 2 & 1) != 0) {
    baseHeader_3 = this->pBaseHeader;
    if ((undefined *)
        ((longlong)&baseHeader_3->versionNumber + (ulonglong)baseHeader_3->PacketSize) <
        &nxtSec->field_0xc) goto LAB_00038c07;
    if (nxtSec < baseHeader_3) goto LAB_00038bfa;
    this->unknown5 = (undefined8 *)nxtSec;
    userHeaderFlag = (byte)(userHeader_1->Flags >> 0x18);
    nxtSec = (CBaseHeader *)
            ((longlong)&nxtSec->versionNumber +
            ((ulonglong)(*(int *)&nxtSec->field_0x4 + nxtSec->PacketSize + 0xf) & 0xfffffffc)
            );
}

The important line of code is here:

nxtSec = (CBaseHeader *)
            ((longlong)&nxtSec->versionNumber +
            ((ulonglong)(*(int *)&nxtSec->field_0x4 + nxtSec->PacketSize + 0xf) & 0xfffffffc)
            );

The nxtSec pointer points to the current “section” of the message being processed in memory. The relevant properties of the nxtSec object are:

  • nxtSec->versionNumber
    • This points to the beginning of the message section. It is effectively nxtSec + offset 0x0.
  • nxtSec->field_0x4
    • This points to a DWORD stored at offset 0x4 in the section.
  • nxtSec->PacketSize
    • This points to a DWORD at offset 0x8 in the section.

This line of code takes the current section address and adds the two DWORDS which are located in the message header itself. It then adds a static value of 0xf before performing an AND against the static value 0xfffffffc. There are no checks performed on those DWORDS to ensure that the resulting nxtSec pointer is actually inside the current message object.

As the message sender, we can control the values of those two DWORDS, meaning that we can obtain some level of control over where the nxtSec pointer will point in memory. We can’t set the pointer to any arbitrary value, but we can add two arbitrary DWORDs to the current address. The PoC message sets both of these DWORDS to 0x40000000 and 0x40000000. Zoemurmure says this should result in a large negative number (0x80000000) so that the final calculated address will be inaccessible, thus triggering the crash.

The 0xC byte which ends up being written to this location is a static value which cannot be changed:

if ((param_5 != (_TA_ADDRESS *)0x0) && (param_6 == 0)) {
    nxtSec->versionNumber = 0xc;    // 0xC byte here
    nxtSec->field_0x1 = 0;
    nxtSec->field2_0x2 = 0;
    *(undefined5 *)&nxtSec->field_0x3 = 0;
    nxtSec->PacketSize = 0;
...

This operation happens at MQQM+0x388aa:

.text:00000000000388AA mov     qword ptr [rbx], 0Ch ; Trigger
.text:00000000000388B1 mov     rcx, r15
.text:00000000000388B4 mov     [rbx+8], r15d

It’s noteworthy that after this single byte overwrite is another overwrite of an entire DWORD. 0x00000000 is written to nxtSec + 0x8. This could be important depending on how this is exploited.

PoC Summary

The PoC causes the application to write the static value 0xC to an unmapped memory location. This causes the application to crash. To use this in a functional exploit, we would have to find an object in memory within range of the nxtSec object. This object would need to be useful to us in some way where if we corrupted one byte with the 0xC value, we could use our corrupted object to gain further read or write access to memory.

For example, if we could create an object in memory that stores an array of bytes in a buffer with a configurable size, we might create an instance of this object of size 0x0100. We could then attempt to corrupt the object size property to 0x0c00. This would trick the system into thinking the object’s size is much larger than it really is. If we then attempted to read back the bytes from the object’s buffer, it is conceivable that the system would read back 0x0c00 bytes instead of the original 0x0100. This could lead to a large memory leak. If we can abuse it to leak some pointers, we may then be on our way to bypassing ASLR. We could try to do the same thing for an object that allows us to write to a buffer. Then we could potentially build a write primitive which we could use to gain RCE.

This sounds fine, but we would need some way of remotely creating and manipulating such objects in the MSMQ process. These objects would need to be in range of our 1-byte overwrite primitive. It seems like a long shot, but I thought I would dig in a bit and see if I could come up with anything. It felt like a good way to practice some of the skills I’ve been learning while studying for the OSEE exam.

Message Queue Research

Test Code

After some reading up on MSMQ, I found that the most basic way to create and interact with objects in this memory segment is to simply send and receive messages into the message queue. I found some sample code provided by Microsoft to do this in Visual Studio .NET and built a program to let me send a message to a remote queue via TCP and then receive it.

The below test program allows you to send and/or receive messages to a remote queue at a hardcoded IP address. You can supply command line arguments to set the operation, queue name, message, a priority level, and the number of duplicate messages to send or receive. For example, the below command will send a message to a queue called “test” with message “AAAAAAAAAAAAAAAA”. The priority is set to -1, which means don’t set it, and it will send the message 1 time. Since the operation is set to “both”, it will then receive a single message from the queue. If the queue was empty before this message was sent, then it will retrieve this message. Otherwise it will retrieve the oldest message in the queue. If the priority is used, then it will retrieve the oldest message with the highest priority.

MSMQTest2.exe both test AAAAAAAAAAAAAAAA -1 1
using System;
using System.Messaging;
using System.Net.NetworkInformation;
using static System.Net.Mime.MediaTypeNames;

namespace MSMQTest2
{

    // This class represents an object the following example
    // sends to a queue and receives from a queue.
    public class Order
    {
        public int orderId;
        public DateTime orderTime;
        public string orderName;
    };

    /// <summary>
    /// Provides a container class for the example.
    /// </summary>
    public class MyNewQueue
    {

        //**************************************************
        // Provides an entry point into the application.
        //		
        // This example sends and receives a message from
        // a queue.
        //**************************************************

        public static void Main(string[] args)
        {

            string arg_operation = args[0];             // "send", "recv", or "both"
            string arg_queue = args[1];                 // Queue name
            string arg_data = args[2];                  // String data to send in message
            int arg_priority = Int32.Parse(args[3]);    // -1 = Disable, 0 = LOWEST, 7 = HIGHEST
            int arg_count = Int32.Parse(args[4]);       // Number of duplicate messages to send/recv

            // Create a new instance of the class.
            MyNewQueue myNewQueue = new MyNewQueue();

            // Send a message to a queue.
            if (arg_operation == "send" || arg_operation == "both")
            {
                Console.WriteLine("[+] Sending message(s)...");
                for (int i = 0; i < arg_count; i++)
                {
                    myNewQueue.SendMessage(arg_queue, arg_data, arg_priority);
                }
            }


            if (arg_operation == "recv" || arg_operation == "both")
            {

                Console.WriteLine("[X] Press enter to receive");
                while (Console.ReadKey().Key != ConsoleKey.Enter) { }

                Console.WriteLine("[+] Receiving message(s)...");

                // Receive a message from a queue.
                for (int i = 0; i < arg_count; i++)
                {
                    myNewQueue.ReceiveMessage(arg_queue, arg_priority);
                }
            }

            return;
        }

        //**************************************************
        // Sends an Order to a queue.
        //**************************************************

        public void SendMessage(string queueName, string value, int priority)
        {

            // Connect to a queue on the local computer.
            MessageQueue myQueue = new MessageQueue(@"FormatName:DIRECT=TCP:172.16.155.132\private$\" + queueName);
            myQueue.Formatter = new BinaryMessageFormatter();

            // Send the Order to the queue.
            string text = value;
            byte[] result = System.Text.Encoding.UTF8.GetBytes(text);

            Message myMessage = new Message();
            myMessage.Body = result;
            if (priority == 7) {
                myMessage.Priority = MessagePriority.Highest;
            } else if (priority > -1) {
                myMessage.Priority = MessagePriority.Lowest;
            }

            myMessage.Formatter = new BinaryMessageFormatter();

            myQueue.Send(myMessage);

            return;
        }

        //**************************************************
        // Receives a message containing an Order.
        //**************************************************

        public void ReceiveMessage(string queue, int priority)
        {
            // Connect to the a queue on the local computer.
            MessageQueue myQueue = new MessageQueue(@"FormatName:DIRECT=TCP:172.16.155.132\private$\" + queue);

            // Set the formatter to indicate body contains an Order.
            myQueue.Formatter = new BinaryMessageFormatter();
            
            // Obtain message in order of priority
            if (priority > -1)
            {
                myQueue.MessageReadPropertyFilter.Priority = true;
            }
            
            try
            {
                // Receive and format the message.
                Message myMessage = myQueue.Receive();
                byte[] test = (byte[])myMessage.Body;

                // Display message information.
                string hex = BitConverter.ToString(test);
                Console.WriteLine("[+]Retrieved: " + hex);
                Console.WriteLine("[+]Size: " + test.Length.ToString());
            }

            catch (MessageQueueException e)
            {
                // Handle Message Queuing exceptions.
                Console.WriteLine("[!]Error reading queue: " + e.Message);
            }

            // Handle invalid serialization format.
            catch (InvalidOperationException e)
            {
                Console.WriteLine(e.Message);
            }

            // Catch other exceptions as necessary.

            return;
        }
    }
}

Here are some interesting notes I found during testing:

  • Public queues are only available for domain systems, not workgroup systems
  • Private queues can be created in the Computer Management pane
  • You can send messages to a remote private queue anonymously by default
  • You cannot receive (read) messages from a remote queue anonymously unless you enable access via the Computer Management pane
  • Messages are sent via TCP but they are received using MS-RPC on port 135.

Creating a Private Queue

Allowing Anonymous Read Access

On my test system I’ve created two private queues. One called “test” and one is called “aaaa”. They both have given anonymous read access so I can send and receive messages to the queues for testing.

Objects

The most obvious object to look at would be a message object itself, since clearly we can create these and, if we have permission, read them back again.

I first set a breakpoint on the instruction that copies the 0xC byte.

0:005> bp MQQM!CQmPacket::CQmPacket+0x90a

If I then try to send a message to the “test” queue, here’s what happens:

Z:\WindowsShared>MSMQTest2.exe send test AAAAAAAAAAAAAAAA -1 1
[+] Sending message(s)...
Breakpoint 0 hit
MQQM!CQmPacket::CQmPacket+0x90a:
00007ffb`dc9e88aa 48c7030c000000  mov     qword ptr [rbx],0Ch ds:000002b6`7bfd010c=000000000000000c

I hit the breakpoint. The address being written to is 000002b6`7bfd010c. Let’s see what that address is:

0:005> !address 000002b6`7bfd010c

...

Usage:                  MappedFile
Base Address:           000002b6`7bfd0000
End Address:            000002b6`7c3d0000
Region Size:            00000000`00400000 (   4.000 MB)
State:                  00001000          MEM_COMMIT
Protect:                00000004          PAGE_READWRITE
Type:                   00040000          MEM_MAPPED
Allocation Base:        000002b6`7bfd0000
Allocation Protect:     00000004          PAGE_READWRITE
Mapped file name:       \Device\HarddiskVolume3\Windows\System32\msmq\storage\r0000028.mq


Content source: 1 (target), length: 3ffef4

WinDbg says it’s a mapped file. The file being mapped is c:\Windows\System32\msmq\storage\r0000028.mq. Certain message types can persist when the service shuts down or restarts, but the messages I’m sending do not persist. I thought it was interesting that the messages still were mapped to a file. If the MSMQ service restarts, this file is deleted. The next time the service starts, the file is recreated but with the filename incremented by 1.

If I continue execution and then manually break into the app, I can see what the message looks like in memory after processing.

0:015> dqs 000002b6`7bfd010c - 10c L30
000002b6`7bfd0000  00006804`00000200
000002b6`7bfd0008  6477c593`00000000
000002b6`7bfd0010  00000000`00200002
000002b6`7bfd0018  00000000`00000000
000002b6`7bfd0020  10030010`00000000
000002b6`7bfd0028  000001a8`524f494c
000002b6`7bfd0030  7e5a55fc`647d0a89
000002b6`7bfd0038  084847a6`4e4c9b57
000002b6`7bfd0040  00010004`6e381e61
000002b6`7bfd0048  859b10ac`00000000
000002b6`7bfd0050  ffffffff`00000000
000002b6`7bfd0058  000023c8`6477c489
000002b6`7bfd0060  00540042`00201c01
000002b6`7bfd0068  0031003a`00500043
000002b6`7bfd0070  0031002e`00320037
000002b6`7bfd0078  00350031`002e0036
000002b6`7bfd0080  00330031`002e0035
000002b6`7bfd0088  00720070`005c0032
000002b6`7bfd0090  00740061`00760069
000002b6`7bfd0098  0074005c`00240065
000002b6`7bfd00a0  00000074`00730065
000002b6`7bfd00a8  00000000`00000000
000002b6`7bfd00b0  00000000`00000000
000002b6`7bfd00b8  00000000`00000000
000002b6`7bfd00c0  00000000`00000300
000002b6`7bfd00c8  0000002c`0000002c
000002b6`7bfd00d0  0000800e`00000000
000002b6`7bfd00d8  00000000`00006801
000002b6`7bfd00e0  ffffff00`00000100
000002b6`7bfd00e8  00000000`000001ff
000002b6`7bfd00f0  00100000`00010f00
000002b6`7bfd00f8  41414141`41020000
000002b6`7bfd0100  41414141`41414141
000002b6`7bfd0108  0000000c`0b414141
000002b6`7bfd0110  00000012`00000000
000002b6`7bfd0118  00000000`00000094
000002b6`7bfd0120  00000000`00000000
000002b6`7bfd0128  00000000`00000000
000002b6`7bfd0130  00000012`00000000
000002b6`7bfd0138  00000000`00000094

My first thought was to see if there is a way to corrupt this message to make the system think it’s longer than it really is. If this were possible, then maybe I could get the system to send me back too much data when receiving a message. This could lead to a memory leak.

I sent another message with 8 extra bytes in the message.

Z:\WindowsShared>MSMQTest2.exe send test AAAAAAAAAAAAAAAAAAAAAAAA -1 1
[+] Sending message(s)...
0:015> g
Breakpoint 0 hit
MQQM!CQmPacket::CQmPacket+0x90a:
00007ffb`dc9e88aa 48c7030c000000  mov     qword ptr [rbx],0Ch ds:000002b6`7bfd0314=0000009400000012

Let’s see what that one looks like:

0:016> dqs 000002b6`7bfd0314 - 10c - 8 L 30
000002b6`7bfd0200  00006805`00000200
000002b6`7bfd0208  6477c607`00000000
000002b6`7bfd0210  00000000`00200002
000002b6`7bfd0218  00000000`00000000
000002b6`7bfd0220  10030010`00000000
000002b6`7bfd0228  000001b0`524f494c
000002b6`7bfd0230  7e5a55fc`647d0c05
000002b6`7bfd0238  084847a6`4e4c9b57
000002b6`7bfd0240  00010004`6e381e61
000002b6`7bfd0248  859b10ac`00000000
000002b6`7bfd0250  ffffffff`00000000
000002b6`7bfd0258  000023c9`6477c603
000002b6`7bfd0260  00540042`00201c01
000002b6`7bfd0268  0031003a`00500043
000002b6`7bfd0270  0031002e`00320037
000002b6`7bfd0278  00350031`002e0036
000002b6`7bfd0280  00330031`002e0035
000002b6`7bfd0288  00720070`005c0032
000002b6`7bfd0290  00740061`00760069
000002b6`7bfd0298  0074005c`00240065
000002b6`7bfd02a0  00000074`00730065
000002b6`7bfd02a8  00000000`00000000
000002b6`7bfd02b0  00000000`00000000
000002b6`7bfd02b8  00000000`00000000
000002b6`7bfd02c0  00000000`00000300
000002b6`7bfd02c8  00000034`00000034
000002b6`7bfd02d0  0000800e`00000000
000002b6`7bfd02d8  00000000`00006801
000002b6`7bfd02e0  ffffff00`00000100
000002b6`7bfd02e8  00000000`000001ff
000002b6`7bfd02f0  00180000`00010f00
000002b6`7bfd02f8  41414141`41020000
000002b6`7bfd0300  41414141`41414141
000002b6`7bfd0308  41414141`41414141
000002b6`7bfd0310  0000000c`0b414141
000002b6`7bfd0318  00000012`00000000
000002b6`7bfd0320  00000000`00000094

They are mostly identical with a couple of bytes which differ. I spent some time manually overwriting some of these bytes with larger values to see if anything changed. Some of them resulted in problems deserializing the message, but I think I narrowed down the size field to this location:

Message 1:

000002b6`7bfd0028  000001a8

Message 2:

000002b6`7bfd0028  000001b0

You can see how the value changed by 8 bytes, which is the number of extra characters I sent in the second message. I tried changing this value from 0x01b0 to 0x0cb0. The 0x0c value simulates what would happen if I were to overwrite this by exploiting the vulnerability.

0:016> eq 000002b6`7bfd0228
000002b6`7bfd0228 000001b0`524f494c 00000cb0`524f494c
000002b6`7bfd0230 7e5a55fc`647d0c05 
0:016> g
Z:\WindowsShared>MSMQTest2.exe recv test AAAAAAAAAAAAAAAAAAAAAAAA -1 1
[X] Press enter to receive
[+] Receiving message(s)...
[+]Retrieved: 41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41
[+]Size: 24

It received only the same amount of data. At least, it only deserialized that much data. I had a suspicion that maybe the service was sending a lot of data, but the .NET library receiving the message was deserializing that byte array message and stopping when it saw the end. If this were the case, then it was possible that the service was leaking memory back to me but I just wasn’t seeing it because it was being ignored. To try and test this, I turned to Wireshark.

Wireshark

This is where I discovered that although MSMQ sends messages on TCP port 1801, it actually receives remote messages via MS-RPC on TCP port 135. I had to ensure this port was opened on the target machine in order to check the remote queue. Then I used Wireshark to try and see how much data was being sent back when I didn’t modify the message and when I did modify it.

I sent and received three unmodified messages, and two modified messages. Unfortunately, the RPC traffic is encrypted, so I was unable to see the actual data returned. But I could see the size of the data sent back.

Wireshark Screenshot

Wireshark showed that I received three payloads between 598 and 614 bytes in size. I received two payloads of 3414 byes in size. I did some conversions from hex to base 10 in WinDbg:

0:002> ?0cb0
Evaluate expression: 3248 = 00000000`00000cb0

0:002> ?01b0
Evaluate expression: 432 = 00000000`000001b0

The values are pretty close. Likely there are extra bytes included as part of the overall headers or maybe part of RPC. Plus the encryption probably alters the overall size. But the results indicated that most likely the MSMQ service was leaking a bunch of data back to me. My attacking system was just ignoring it either due to some part of my local MSMQ process or due to the deserialization happening as part of .NET.

To be sure, I tried one more time. This time I modified the size DWORD to this:

000c01b0`524f494c

This would be around 786,864 bytes. A huge amount of data to leak. After receiving the message, I saw a bunch of large RPC messages in Wireshark that I hadn’t seen before. It seems it was copying a large amount of data…

Wireshark Large Packets

It looked like a lot of data was leaking, but my client application still only displayed and recognized the actual message data. I suspected this had something to do with how the message was being processed and deserialized. My code was using the BinaryMessageFormatter to format the payload. I figured that the formatter was probably parsing another part of the message to see how long the actual binary data was, and then converting that back into an object. Any extra data at the end was likely being ignored. I wanted to find a way to send raw data without using any formatter at all.

BodyStream

I did some more research and found that this could be done by using Message.BodyStream instead of Message.Body.

using System;
using System.Messaging;
using System.Net.NetworkInformation;
using static System.Net.Mime.MediaTypeNames;
using System.IO;

namespace MSMQTest2
{

    // This class represents an object the following example
    // sends to a queue and receives from a queue.
    public class Order
    {
        public int orderId;
        public DateTime orderTime;
        public string orderName;
    };

    /// <summary>
    /// Provides a container class for the example.
    /// </summary>
    public class MyNewQueue
    {

        //**************************************************
        // Provides an entry point into the application.
        //		
        // This example sends and receives a message from
        // a queue.
        //**************************************************

        public static void Main(string[] args)
        {

            string arg_operation = args[0];             // "send", "recv", or "both"
            string arg_queue = args[1];                 // Queue name
            string arg_data = args[2];                  // String data to send in message
            int arg_priority = Int32.Parse(args[3]);    // -1 = Disable, 0 = LOWEST, 7 = HIGHEST
            int arg_count = Int32.Parse(args[4]);       // Number of duplicate messages to send/recv

            // Create a new instance of the class.
            MyNewQueue myNewQueue = new MyNewQueue();

            // Send a message to a queue.
            if (arg_operation == "send" || arg_operation == "both")
            {
                Console.WriteLine("[+] Sending message(s)...");
                for (int i = 0; i < arg_count; i++)
                {
                    myNewQueue.SendMessage(arg_queue, arg_data, arg_priority);
                }
            }


            if (arg_operation == "recv" || arg_operation == "both")
            {

                Console.WriteLine("[X] Press enter to receive");
                while (Console.ReadKey().Key != ConsoleKey.Enter) { }

                Console.WriteLine("[+] Receiving message(s)...");

                // Receive a message from a queue.
                for (int i = 0; i < arg_count; i++)
                {
                    myNewQueue.ReceiveMessage(arg_queue, arg_priority);
                }
            }

            return;
        }

        //**************************************************
        // Sends an Order to a queue.
        //**************************************************

        public void SendMessage(string queueName, string value, int priority)
        {

            // Connect to a queue on the local computer.
            MessageQueue myQueue = new MessageQueue(@"FormatName:DIRECT=TCP:172.16.155.132\private$\" + queueName);

            // Send the Order to the queue.
            string text = value;
            byte[] result = System.Text.Encoding.UTF8.GetBytes(text);

            Message myMessage = new Message();
            myMessage.BodyStream = new MemoryStream(result);
            if (priority == 7) {
                myMessage.Priority = MessagePriority.Highest;
            } else if (priority > -1) {
                myMessage.Priority = MessagePriority.Lowest;
            }

            myQueue.Send(myMessage);

            return;
        }

        //**************************************************
        // Receives a message containing an Order.
        //**************************************************

        public void ReceiveMessage(string queue, int priority)
        {
            // Connect to the a queue on the local computer.
            MessageQueue myQueue = new MessageQueue(@"FormatName:DIRECT=TCP:172.16.155.132\private$\" + queue);
            
            // Obtain message in order of priority
            if (priority > -1)
            {
                myQueue.MessageReadPropertyFilter.Priority = true;
            }
            
            try
            {
                // Receive and format the message.
                Message myMessage = myQueue.Receive();
                byte[] test = ReadFully(myMessage.BodyStream);

                // Display message information.
                string hex = BitConverter.ToString(test);
                Console.WriteLine("[+]Retrieved: " + hex);
                Console.WriteLine("[+]Size: " + test.Length.ToString());
            }

            catch (MessageQueueException e)
            {
                // Handle Message Queuing exceptions.
                Console.WriteLine("[!]Error reading queue: " + e.Message);
            }

            // Handle invalid serialization format.
            catch (InvalidOperationException e)
            {
                Console.WriteLine(e.Message);
            }

            // Catch other exceptions as necessary.

            return;
        }

        private static byte[] ReadFully(Stream input)
        {
            byte[] buffer = new byte[16 * 1024];
            using (MemoryStream ms = new MemoryStream())
            {
                int read;
                while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
                {
                    ms.Write(buffer, 0, read);
                }
                return ms.ToArray();
            }
        }
    }
}

I tried sending another test message:

Z:\WindowsShared>MSMQTest2.exe send test AAAAAAAAAAAAAAAAAAAAAAAA -1 1

Here’s what it looked like in memory (notations added)

0:019> dqs 000002d1`c15900f8 - f8 L30
000002d1`c1590000  0000a001`000001c0
000002d1`c1590008  64781e44`00000000
000002d1`c1590010  00000000`00200002
000002d1`c1590018  00000000`00000000
000002d1`c1590020  10030010`00000000
000002d1`c1590028  00000194`524f494c    <--- 0x194 is the overall size
000002d1`c1590030  7e5a55fc`647d6441
000002d1`c1590038  084847a6`4e4c9b57
000002d1`c1590040  00010004`6e381e61
000002d1`c1590048  859b10ac`00000000
000002d1`c1590050  ffffffff`00000000
000002d1`c1590058  00006801`64781e41
000002d1`c1590060  00540042`00201c01
000002d1`c1590068  0031003a`00500043
000002d1`c1590070  0031002e`00320037
000002d1`c1590078  00350031`002e0036
000002d1`c1590080  00330031`002e0035
000002d1`c1590088  00720070`005c0032
000002d1`c1590090  00740061`00760069
000002d1`c1590098  0074005c`00240065
000002d1`c15900a0  00000074`00730065
000002d1`c15900a8  00000000`00000000
000002d1`c15900b0  00000000`00000000
000002d1`c15900b8  00000000`00000000
000002d1`c15900c0  00000000`00000000
000002d1`c15900c8  00000018`00000018    <--- 0x18 seems to be the payload size below (A's)
000002d1`c15900d0  0000800e`00000000
000002d1`c15900d8  00000000`00006801
000002d1`c15900e0  41414141`41414141
000002d1`c15900e8  41414141`41414141
000002d1`c15900f0  41414141`41414141
000002d1`c15900f8  00000000`0000000c
000002d1`c1590100  00000094`00000012
000002d1`c1590108  00000000`00000000

I found that I could modify the two marked lines to look like this:

000002d1`c1590028  000c0194`524f494c 
000002d1`c15900c8  00000c18`00000c18

The total message size went from 0x0194 to 0xc0194, which is 786,432 bytes larger. Then I changed the second and third values from 0x18 to 0xc18, which is 3072 bytes larger. I then attempted to receive the message and found:

Z:\WindowsShared>MSMQTest2.exe recv test AAAAAAAAAAAAAAAAAAAAAAAA -1 1
[X] Press enter to receive
[+] Receiving message(s)...
[+]Retrieved: 41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-41-0C-00-00-00-00-00-00-00-12-00-00-00-94-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-20-00-00-00-04-00-01-00-00-00-00-00-AC-10-9B-85-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
[+]Size: 3096

Success! The total payload size was 3096, which is 0xC18 in hex. This proved that if I could exploit the vulnerability to overwrite those three bytes in a message with 0xc characters, it would theoretically be possible to leak memory back to me. This would require three separate writes, though. I think it could be possible to leak the data with only a single write, by overwriting the total message length only. However, I think I would need to be able to see the incoming raw RPC data in order to actually read the leaked memory because the .NET library seems to ignore it. So it might require implementing the RPC message receiving functionality manually.

Next, I had a few questions to answer:

  • Is it possible to overwrite those characters?
  • What interesting/helpful information can we leak?
  • Can we use the leaked information to gain further access?

What Can We Leak?

I started by trying to see what interesting information could possibly be leaked? The way this leak works, you can only leak data that comes right after the corrupted message object. This means that most likely there are going to be other message objects leaked. This could be useful, depending on what messages are stored in the message queue. If you have permission to read from one queue, but not another, you may be able to leak messages from the inaccessible queue and perhaps there is sensitive data in there.

To further develop an exploit, we’d likely want to leak some interesting pointers. We already saw that the message queue memory space is actually a file mapped to 4MB of memory. What comes after that segment? I turned to the WinDbg “!address” command.

0:006> !address 

        BaseAddress      EndAddress+1        RegionSize     Type       State                 Protect             Usage
--------------------------------------------------------------------------------------------------------------------------
+        0`00000000        0`7ffe0000        0`7ffe0000             MEM_FREE    PAGE_NOACCESS                      Free       
+        0`7ffe0000        0`7ffe1000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READONLY                      Other      [User Shared Data]

...

+      290`b7030000      290`b7031000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [................]
+      290`b7031000      290`b7040000        0`0000f000             MEM_FREE    PAGE_NOACCESS                      Free       
+      290`b7040000      290`b7041000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [................]
       290`b7041000      290`b7140000        0`000ff000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
+      290`b7140000      290`b7540000        0`00400000 MEM_MAPPED  MEM_COMMIT  PAGE_READWRITE                     MappedFile "\Device\HarddiskVolume3\Windows\System32\msmq\storage\r0000031.mq"
+      290`b7540000     7df4`d7760000     7b64`20220000             MEM_FREE    PAGE_NOACCESS                      Free       
+     7df4`d7760000     7df4`d7765000        0`00005000 MEM_MAPPED  MEM_COMMIT  PAGE_READONLY                      Other      [Read Only Shared Memory]
      7df4`d7765000     7df4`d7860000        0`000fb000 MEM_MAPPED  MEM_RESERVE                                    MappedFile "PageFile"
+     7df4`d7860000     7df5`d7880000        1`00020000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
+     7df5`d7880000     7df5`d9880000        0`02000000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
      7df5`d9880000     7df5`d9881000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [................]
+     7df5`d9881000     7df5`d9890000        0`0000f000             MEM_FREE    PAGE_NOACCESS                      Free       

...

After a fresh restart of the MSMQ service, it looks like the message queue MappedFile is the last thing loaded in this memory space. After that segment is just a large chunk of free space before we get into anything else that might be interesting. Unfortunately this gap is just too large to realistically be able to leak. Interestingly, after performing the leak, the layout changes and looks like this:

...

+      290`b7030000      290`b7031000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [................]
+      290`b7031000      290`b7040000        0`0000f000             MEM_FREE    PAGE_NOACCESS                      Free       
+      290`b7040000      290`b7041000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     <unknown>  [................]
       290`b7041000      290`b7140000        0`000ff000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
+      290`b7140000      290`b7540000        0`00400000 MEM_MAPPED  MEM_COMMIT  PAGE_READWRITE                     MappedFile "\Device\HarddiskVolume3\Windows\System32\msmq\storage\r0000031.mq"
+      290`b7540000      290`b7541000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     Heap       [ID: 2; Handle: 00000290b6880000; Type: Segment]
       290`b7541000      290`b7600000        0`000bf000 MEM_PRIVATE MEM_RESERVE                                    Heap       [ID: 2; Handle: 00000290b6880000; Type: Segment]
       290`b7600000      290`b7601000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     Heap       [ID: 2; Handle: 00000290b6880000; Type: Segment]
       290`b7601000      290`b773f000        0`0013e000 MEM_PRIVATE MEM_RESERVE                                    Heap       [ID: 2; Handle: 00000290b6880000; Type: Segment]
       290`b773f000      290`b7740000        0`00001000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
+      290`b7740000      290`b7801000        0`000c1000 MEM_PRIVATE MEM_COMMIT  PAGE_READWRITE                     Heap       [ID: 0; Handle: 00000290b65d0000; Type: Segment]
       290`b7801000      290`b783f000        0`0003e000 MEM_PRIVATE MEM_RESERVE                                    Heap       [ID: 0; Handle: 00000290b65d0000; Type: Segment]
       290`b783f000      290`b7840000        0`00001000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
+      290`b7840000     7df4`d7760000     7b64`1ff20000             MEM_FREE    PAGE_NOACCESS                      Free       
+     7df4`d7760000     7df4`d7765000        0`00005000 MEM_MAPPED  MEM_COMMIT  PAGE_READONLY                      Other      [Read Only Shared Memory]
      7df4`d7765000     7df4`d7860000        0`000fb000 MEM_MAPPED  MEM_RESERVE                                    MappedFile "PageFile"
+     7df4`d7860000     7df5`d7880000        1`00020000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  
+     7df5`d7880000     7df5`d9880000        0`02000000 MEM_PRIVATE MEM_RESERVE                                    <unknown>  

...

Some new heap segments were allocated after the mapped file. I’m not sure why this happened. Maybe the application needed to allocate some more heap space to store the message data while it was building RPC packets? Either way, it’s possible to cause the application to allocate some data after the mapped file. I found through testing that if you fill the message queue, the application will map a second file after the first one, with some other heap segments in between. So there are multiple ways to force new heap allocations after the queue.

What’s actually stored in those allocations? I’m not sure yet. It looks like some of them contain copies of the message. One of them is empty. One of them contains a lot of data, which I’m guessing is maybe an RPC packet or the encrypted payload to be sent over RPC, though I haven’t confirmed that yet.

The allocation immediately after the message queue seems to contain a copy of my message, though there is heap info stored before that:

0:021> dqs 290`b7540000 L30
00000290`b7540000  00000000`00000000
00000290`b7540008  01017fc0`5ad6fb01
00000290`b7540010  00000002`ffeeffee
00000290`b7540018  00000290`b6880120
00000290`b7540020  00000290`b6ea0018
00000290`b7540028  00000290`b6880000
00000290`b7540030  00000290`b7540000
00000290`b7540038  00000000`000001ff
00000290`b7540040  00000290`b7540070
00000290`b7540048  00000290`b773f000
00000290`b7540050  00000001`0000013e
00000290`b7540058  00000000`00000000
00000290`b7540060  00000290`b7600fe0
00000290`b7540068  00000290`b7600fe0
00000290`b7540070  00000000`00000000
00000290`b7540078  00017fc7`61df3bf3
00000290`b7540080  00000290`b6880150
00000290`b7540088  00000290`b6ee37f0
00000290`b7540090  4e4c9b57`7e5a55fc
00000290`b7540098  6e381e61`084847a6
00000290`b75400a0  00000000`00010004
00000290`b75400a8  00000000`859b10ac
00000290`b75400b0  6478b72c`ffffffff
00000290`b75400b8  00201c01`00007004
00000290`b75400c0  00500043`00540042
00000290`b75400c8  00320037`0031003a
00000290`b75400d0  002e0036`0031002e
00000290`b75400d8  002e0035`00350031
00000290`b75400e0  005c0032`00330031
00000290`b75400e8  00760069`00720070
00000290`b75400f0  00240065`00740061
00000290`b75400f8  00730065`0074005c
00000290`b7540100  00000000`00000074
00000290`b7540108  00000000`00000000
00000290`b7540110  00000000`00000000
00000290`b7540118  00000000`00000000
00000290`b7540120  00000c18`00000000
00000290`b7540128  00000000`00000c18
00000290`b7540130  00006801`0000800e
00000290`b7540138  41414141`00000000
00000290`b7540140  41414141`41414141
00000290`b7540148  41414141`41414141
00000290`b7540150  0000000c`41414141
00000290`b7540158  00000012`00000000
00000290`b7540160  00000000`00000094
00000290`b7540168  00000000`00000000
00000290`b7540170  00000000`00000000
00000290`b7540178  00000000`00000000

The seventh line contains a pointer back to this heap allocation itself. So at the very least, if I could leak that pointer then I could potentially calculate where the message resides in memory. This could be useful later if I’m able to develop a read or write primitive.

0:021> dqs 290`b7540000 L7
00000290`b7540000  00000000`00000000    <--- Start of allocation
00000290`b7540008  01017fc0`5ad6fb01
00000290`b7540010  00000002`ffeeffee
00000290`b7540018  00000290`b6880120
00000290`b7540020  00000290`b6ea0018
00000290`b7540028  00000290`b6880000
00000290`b7540030  00000290`b7540000    <--- Pointer to start of allocation

This seems like it would be worthwhile to explore, even if only to leak message queue data belonging to other queues.

Is it Possible?

The next question is, “Is it possible to perform the 0xC overwrite on a message that exists in the queue?

Let’s look back at the vulnerable disassembly code:

nxtSec = (CBaseHeader *)
        ((longlong)&nxtSec->versionNumber +
        ((ulonglong)(*(int *)&nxtSec->field_0x14 + 0x1b) & 0xfffffffc));

We have two controlled DWORDs added together. The result is eventually added to the nxtSec pointer. Zoemurmure’s post says that he set these values to 0x40000000 so they would add together to equal 0x80000000 which is a large negative value. This made me think that the nxtSec pointer would end up decreasing and pointing to somewhere way before the message object in memory. However, this isn’t the case. Let’s look out the disassembly from IDA where these operations happen.

.text:000000000003844A mov     [rdi+88h], rbx 
.text:0000000000038451 mov     edx, [rbx+4]    ; One int here
.text:0000000000038454 mov     ecx, [rbx+8]    ; Another int here
.text:0000000000038457 add     ecx, 0Fh
.text:000000000003845A add     edx, ecx        ; 32-bit add
.text:000000000003845C mov     ecx, [r8+2Ch]
.text:0000000000038460 and     rdx, r9
.text:0000000000038463 add     rbx, rdx        ; 64-bit add to the pointer

The two DWORDS are added together using the 32-bit registers. The result is 0x80000000, which would be a negative 32-bit integer if the integer is interpreted as a signed int. However, the last line shows that the final addition is done using the 64-bit registers. So suddenly, the 0x80000000 becomes 0x00000000`80000000. The new value is not sign-extended so this is no longer interpreted as a negative number. This means that we can really only perform the overwrite at a memory location that comes after our malicious message object.

This poses a problem. In my experiments, each message I send gets placed after the previously sent message in the queue. When a message is read, the most recent message (the one at the largest memory address) is read by default. Once it’s been marked as “read”, that space becomes available to be overwritten by a new message. If I sent a malicious message to perform the 1-byte overwrite, it will be at the end of the list. Since I can only overwrite addresses after my message in memory, there won’t be another message there to corrupt.

Abusing Priority

I found that messages can be read out of order if there is a priority set on the message. I experimented with this and tried sending messages in the following sequence:

  1. Low Priority Message
  2. High Priority Message
  3. Low Priority Message

I then performed a receive operation using priority, which meant that the high priority message was received first and marked as read. In theory, this should free up that space in memory, creating a “hole” where I could stick my malicious message. In practice, I found that this wasn’t the case. If I sent a new message, it was still placed at the end of the queue in memory, leaving that hole in place. If I then read the two messages at the end of the queue, it created a continuous “chunk” of read messages and the next message received would be placed in the “hole”. But then it would be the last message in the queue again, leaving me nothing to corrupt.

Filling the Queue

I also tried filling the queue’s heap allocation so it would have to make a second allocation for additional messages. My hope was that if I read messages from the first allocation, it might store new messages there. That was unfortunately not the case. I found that once a new allocation was created, the original one was not used again. I tried reading all messages from the queue, which should have marked every message as “read”. However, new messages were placed at the beginning of the second allocation.

Using Multiple Queues

I tried writing messages to multiple queues but they all end up in order in the same allocation. Reading a message from one queue does not create a “hole” like I was hoping for. This didn’t work out either.

Next Steps

There are a few directions to head from here. I think a good approach is to look further at the unpatched decompilation of MSMQ. I suspect that this single byte overwrite is actually just a small side effect of the real vulnerability. It would make sense to look at what else can happen before or after that byte is written. Maybe extra headers could be applied to the malicious message that would result in some other manipulation of the data at that nxtSec address. It’s more complicated than I hoped, but if I don’t make it any further this was certainly a fun learning exercise.