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.
Once installed, there will be a new service entry called “Message Queuing”.
You can also view and manage queues from the “Computer Management” control pane.
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.
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 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…
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:
- Low Priority Message
- High Priority Message
- 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.