CVE-2024-38257
An information disclosure vulnerability exists in the AllJoyn Router Service in Microsoft Windows 10 version 10.0.19041.4170 and prior. During the initiation of an ARDP session, the service can send a reset packet that includes information from the address space of the process. An attacker can send an unauthenticated packet to trigger this vulnerability.
The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.
Microsoft Windows 10 MSAJApi.dll 10.0.19041.4170
Windows 10 - https://www.microsoft.com/en-us/windows
5.3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CWE-201 - Information Exposure Through Sent Data
Windows 10 is a pretty popular operating system.
AllJoyn is an open-source software framework that was designed to allow compatible devices in a heterogeneous network environment to perform discovery, communication, and collaboration regardless of the connection, product type, or brand. It was originally promoted by the AllSeen Alliance to allow interoperability between the various IoT devices that were being developed at the time. The AllJoyn framework facilitates these capabilities by providing a D-Bus message bus to clients and servers which can then be used by devices to organize themselves into one cohesive system. Microsoft maintains an implementation of the AllJoyn framework as a runtime client and routing node service that was introduced with Windows 10. Additionally, Microsoft has added support for a device system bridge in order to allow interoperability with devices using protocols such as Z-Wave or BACnet as part of an AllJoyn network.
Microsoft’s implementation of the AllJoyn routing node resides within the “AllJoyn Router Service” which can be found with the name “AJRouter” on Windows 10. The “AJRouter” service is generally not started by default, but can be activated by accessing the “\PIPE\ProtectedPrefix\LocalService\MSAJPipe
” named pipe. Once the service has been activated, it will bind to both TCP and a number of UDP ports. The TCP ports provide authenticated services for devices participating within the AllJoyn network. Similar services are provided using UDP, but with the addition of another protocol being used as the transport for devices to communicate over. This protocol is referred to as ARDP, or the AllJoyn Reliable Datagram Protocol.
The ARDP protocol aims to provide a messaging interface that allows for packet fragmentation and fragment reordering. Establishing a connection with the ARDP protocol requires a device to perform a handshake that is not dissimilar from the handshake used by the TCP protocol. In the handshake, the first packet sent by a participant is used to synchronize the sequence and acknowledgement numbers for the protocol, which will then be acknowledged by the receiver. Upon being acknowledged, the participant and receiver can then freely exchange packets which will be assembled before extracting the AllJoyn messages being transported. Upon completion or failure, the connection will be reset by transmitting a final packet. During the process of resetting a connection that is being authenticated, the AllJoyn framework will fail to initialize the buffer being sent to the device initiating the connection. This results in leaking 0x24 bytes from the stack of the process to the original sender.
The following code shows the main loop of the thread that is used to handle all traffic related to the UDP protocol. Upon the incoming socket being signalled, the method at [1] will be used to enter the main function that is responsible for handling the ARDP protocol, ARDP_Run
.
alljoyn_core/router/UDPTransport.cc:8391-8774
void* UDPTransport::Run(void* arg)
{
QCC_UNUSED(arg);
...
for (;;) {
IncrementPerfCounter(PERF_COUNTER_UDP_TRANSPORT_RUN_OUTER_LOOP);
for (vector<Event*>::iterator i = signaledEvents.begin(); i != signaledEvents.end(); ++i) {
...
m_ardpLock.Lock(MUTEX_CONTEXT);
if (socketReady) {
ardpStatus = ARDP_Run(m_handle, (*i)->GetFD(), readReady, writeReady, socketIsAccepting, &ms);
...
}
m_ardpLock.Unlock(MUTEX_CONTEXT);
...
}
}
return (void*) status;
}
The ARDP_Run
function first allocates a 64k buffer onto the stack. This is intended to contain the entirety of the datagram being fed into the ARDP protocol. At [2], the datagram is read from the socket into this buffer with the resulting size being written to the nbytes
variable. Once the packet has been read, two 16-bit integers will be extracted from the header of the packet at [3]. These integers represent the sender and receiver that the packet is associated with. During the initiation of the ARDP handshake, the local connection number is set to 0, as the connection has not been initialized yet. Afterwards, at [4], the InitconnRecord
function will be used to initialize the necessary information used for tracking the connection if there are enough slots available. When the function returns, the newly constructed connection will then be appended to the end of a linked list before being accepted at [5].
alljoyn_core/router/ArdpProtocol.cc:3428-3538
QStatus ARDP_Run(ArdpHandle* handle, qcc::SocketFd sock, bool sockRead, bool sockWrite, bool sockAccepts, uint32_t* ms)
{
const size_t bufferSize = 65536;
uint32_t buf32[bufferSize >> 2];
uint8_t* buf = reinterpret_cast<uint8_t*>(buf32);
qcc::IPAddress address;
uint16_t port = 0;
size_t nbytes;
QStatus status = ER_OK;
...
if (sockRead) {
while ((status = qcc::RecvFrom(sock, address, port, buf, bufferSize, nbytes)) == ER_OK) { // [2] Read entire datagram from socket
...
if (nbytes > 0 && nbytes < 65536) {
uint16_t local, foreign;
ProtocolDemux(buf, nbytes, &local, &foreign); // [3] Extract the local and foreign connection numbers
if (local == 0) {
if (sockAccepts && handle->accepting && handle->cb.AcceptCb) {
if (!IsDuplicateConnRequest(handle, foreign, address)) {
ArdpConnRecord* conn = NewConnRecord();
status = InitConnRecord(handle, conn, sock, address, port, foreign); // [4] Initialize a connection record
if (status == ER_OK) {
EnList(handle->conns.bwd, (ListNode*)conn);
status = Accept(handle, conn, buf, nbytes); // [5] Accept the connection
}
...
}
...
}
if (status != ER_OK) {
QCC_LogError(status, ("Failed to accept incoming connection request from %s (ARDP port %u)", address.ToString().c_str(), foreign));
SendRst(handle, sock, address, port, local, foreign); // [11] Reset the ARDP connection
}
...
}
...
}
}
...
return status;
}
The following code shows the implementation of the Accept
function. At [6], the function will verify that both the flags and version within the ARDP header are as expected. Afterwards, at [7], the Receive
function will be called. At [8], both the header length and data length will be validated before entering the main function containing the state machine for the ARDP protocol at [9]. If any of these tests fail, a QStatus
error will be returned to the caller, ARDP_Run
.
alljoyn_core/router/ArdpProtocol.cc:3395-3414
QStatus Accept(ArdpHandle* handle, ArdpConnRecord* conn, uint8_t* rxbuf, uint16_t len)
{
...
if (!(rxbuf[FLAGS_OFFSET] & ARDP_FLAG_SYN) || (rxbuf[FLAGS_OFFSET] & ARDP_FLAG_RST)) { // [6] Verify the flags in the header
return ER_ARDP_INVALID_CONNECTION;
}
if ((rxbuf[FLAGS_OFFSET] & ARDP_VERSION_BITS) != ARDP_FLAG_VER) { // [6] Verify the version in the header
QCC_DbgHLPrintf(("Accept(): Unsupported protocol version 0x%x",
rxbuf[FLAGS_OFFSET] & ARDP_VERSION_BITS));
return ER_ARDP_VERSION_NOT_SUPPORTED;
}
...
return Receive(handle, conn, rxbuf, len); // [7] Execute the function for processing the received packet
}
\
alljoyn_core/router/ArdpProtocol.cc:3307-3393
static QStatus Receive(ArdpHandle* handle, ArdpConnRecord* conn, uint8_t* rxbuf, uint16_t len)
{
...
hdrSz = (seg.FLG & ARDP_FLAG_SYN) ? ARDP_SYN_HEADER_SIZE : ARDP_FIXED_HEADER_LEN; // [8] Check sizes in header
/* Perform length validation checks */
if (((seg.HLEN * 2) < hdrSz) || (len < hdrSz) || (seg.DLEN + (seg.HLEN * 2)) != len) { // [8] Check sizes in header
...
return ER_ARDP_INVALID_RESPONSE;
}
...
conn->state = LISTEN; // [9] Initialize connection state as "LISTEN"
conn->passive = true;
ArdpMachine(handle, conn, &seg, rxbuf, len); // [9] Enter state machine
return ER_OK;
}
Once the Accept
functions returns, its result will be assigned to the status
variable at [10]. Due to an error being encountered, the ARDP_Run
function will need to reset the connection. After assigning a result to the status
variable, the ARDP_Run
function will check it against ER_OK
to determine whether the connection was successfully accepted. If it wasn’t, the SendRst
function at [11] will be used to transmit a reset packet back to the initiator of the connection.
alljoyn_core/router/ArdpProtocol.cc:3428-3538
QStatus ARDP_Run(ArdpHandle* handle, qcc::SocketFd sock, bool sockRead, bool sockWrite, bool sockAccepts, uint32_t* ms)
{
const size_t bufferSize = 65536;
uint32_t buf32[bufferSize >> 2];
uint8_t* buf = reinterpret_cast<uint8_t*>(buf32);
...
if (local == 0) {
if (sockAccepts && handle->accepting && handle->cb.AcceptCb) {
if (!IsDuplicateConnRequest(handle, foreign, address)) {
ArdpConnRecord* conn = NewConnRecord();
status = InitConnRecord(handle, conn, sock, address, port, foreign);
if (status == ER_OK) {
EnList(handle->conns.bwd, (ListNode*)conn);
status = Accept(handle, conn, buf, nbytes); // [10] Accept the connection
}
...
}
...
}
if (status != ER_OK) {
QCC_LogError(status, ("Failed to accept incoming connection request from %s (ARDP port %u)", address.ToString().c_str(), foreign));
SendRst(handle, sock, address, port, local, foreign); // [11] Reset the ARDP connection
}
...
}
...
return status;
}
The following code is the implementation of the SendRst
function and is responsible for sending a reset packet on behalf of the ARDP_Run
function. This function contains a number of issues and is directly related to the vulnerability described by this document. In order to send a reset packet, the SendRst
functions allocates 3 variables, h
as an ArdpHeader
, buf32
as a 9-element array of uint32_t
, and txbuf
which is used as a pointer into buf32
. At [12], the buf32
array is allocated on the stack using the total size of the ARDP header (36). Afterwards, at [13], the txbuf
pointer is assigned the address of buf32
with the intention that it is used to set the necessary fields within the reset packet that is to be sent. Once txbuf
has been assigned, the memset
at [14] is used to initialize buf32
using the correct size. After buf32
has been initialized, at [15] the txbuf
pointer is used to assign the “flags”, “source”, and “destination” fields inside the space allocated for the buf32
array. It is at this point that both the txbuf
and buf32
variables are not used anymore. At [16], the h
variable is then written to the socket. Due to both the buf32
and txbuf
variables being unused before going out of scope, the memset
at [14] and the assignments at [15] are optimized out by the compiler leaving only the two calls to htons
in case they have any side effects. When the h
variable is written to the socket, it has not yet been written to resulting in leaking 36 bytes from the stack to the initiator of the connection.
alljoyn_core/router/ArdpProtocol.cc:2002-2036
static QStatus SendRst(ArdpHandle* handle, qcc::SocketFd sock, qcc::IPAddress ipAddr, uint16_t ipPort, uint16_t local, uint16_t foreign)
{
QCC_DbgTrace(("SendRst(handle=%p, sock=%d., ipAddr=\"%s\", ipPort=%d., local=%d., foreign=%d.)",
handle, sock, ipAddr.ToString().c_str(), ipPort, local, foreign));
ArdpHeader h;
uint32_t buf32[ARDP_FIXED_HEADER_LEN >> 2]; // [12] Allocate 9 uint32_t on stack for packet
uint8_t* txbuf = reinterpret_cast<uint8_t*>(buf32); // [13] Cast packet from a uint32_t pointer to a uint8_t pointer
memset(buf32, 0, ARDP_FIXED_HEADER_LEN); // [14] Attempt to initialize "buf32", despite being unused.
*(txbuf + FLAGS_OFFSET) = ARDP_FLAG_RST | ARDP_FLAG_VER; // [15] Set the required fields (flags) for the RST packet
*(txbuf + HLEN_OFFSET) = ARDP_FIXED_HEADER_LEN >> 1; // [15] Set the required fields (hlen) for the RST packet
*reinterpret_cast<uint16_t*>(txbuf + SRC_OFFSET) = htons(local); // [15] Set the required fields (src) for the RST packet
*reinterpret_cast<uint16_t*>(txbuf + DST_OFFSET) = htons(foreign); // [15] Set the required fields (dst) for the RST packet
...
size_t sent;
return qcc::SendTo(sock, ipAddr, ipPort, &h, ARDP_FIXED_HEADER_LEN, sent); // [16] Send the packet to the socket using the wrong variable
}
The definition of the ArdpHeader
structure has the following layout. Each of these fields, other than “reserve”, are uninitialized before being written to the socket.
alljoyn_core/router/ArdpProtocol.h:88-103
#pragma pack(push, 1)
typedef struct {
uint8_t flags; /**< See Control flag definitions above */
uint8_t hlen; /**< Length of the header in units of two octets (number of uint16_t) */
uint16_t src; /**< Used to distinguish between multiple connections on the local side. */
uint16_t dst; /**< Used to distinguish between multiple connections on the foreign side. */
uint16_t dlen; /**< The length of the data in the current segment. Does not include the header size. */
uint32_t seq; /**< The sequence number of the current segment. */
uint32_t ack; /**< The number of the segment that the sender of this segment last received correctly and in sequence. */
uint32_t ttl; /**< Time-to-live. Zero means forever. */
uint32_t lcs; /**< Last "in-order" consumed segment. */
uint32_t acknxt; /**< First unexpired segment, TTL accounting */
uint32_t som; /**< Start sequence number for fragmented message */
uint16_t fcnt; /**< Number of segments comprising fragmented message */
uint16_t reserve; /**< Reserved for future use */
} ArdpHeader;
In the following we will demonstrate the vulnerability by observing the service behavior from within a debugger.
The AllJoyn Router Service is run by Microsoft Windows super-server daemon, svchost.exe
. After the service has been started, it can be attached to with a debugger.
0:033> lm m msajapi
Browse full module list
start end module name
50a40000 50cae000 MSAJApi (deferred)
The reset packet is sent by the ajn::SendRst
function. The following commands set a breakpoint when the ajn:SendRst
function is entered.
0:033> bp msajapi!ajn::SendRst
0:033> g
At this point, the proof-of-concept can be used which will result in the debugger interrupting execution at the entrypoint of the ajn::SendRst
function.
Breakpoint 0 hit
eax=00000001 ebx=044f8b28 ecx=044f8b28 edx=00000f10 esi=05aef75a edi=05aef6ca
eip=50bc0e87 esp=05aef6b4 ebp=05aff780 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
MSAJApi!ajn::SendRst:
50bc0e87 8bff mov edi,edi
This function allocates 0x8c bytes for its frame. If we continue execution through the prologue of the function, a stack canary will be placed at the end of the frame before the prologue completes.
0:026> g 180e8f+msajapi
eax=00000001 ebx=044f8b28 ecx=044f8b28 edx=00000f10 esi=05aef75a edi=05aef6ca
eip=50bc0e8f esp=05aef6b0 ebp=05aef6b0 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
MSAJApi!ajn::SendRst+0x8:
50bc0e8f 81ec8c000000 sub esp,8Ch
The following command continues execution until the prologue completes. At this point, the local variables for the function have been allocated.
0:026> g 180ea3+msajapi
eax=a0bc30e4 ebx=044f8b28 ecx=044f8b28 edx=00000f10 esi=05aef75a edi=05aef6ca
eip=50bc0ea3 esp=05aef624 ebp=05aef6b0 iopl=0 nv up ei ng nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000286
MSAJApi!ajn::SendRst+0x1c:
50bc0ea3 0fb7451c movzx eax,word ptr [ebp+1Ch] ss:0023:05aef6cc=26e3
The buffer that will be sent has been allocated at -0x28(%ebp)
. The following commands dump out the contents of the buffer before it has been initialized. After dumping out the contents of the buffer, two hardware breakpoints are set to confirm that this buffer is not written to during execution.
0:026> db @ebp-28 L0n36
05aef688 28 c0 3f 05 b1 3b 75 75-7f 3b 75 75 53 59 15 05 (.?..;uu.;uuSY..
05aef698 10 c1 a6 50 28 c0 3f 05-28 8b 4f 04 d0 1f 78 75 ...P(.?.(.O...xu
05aef6a8 27 7f 3a 75 '.:u
0:026> ba w4 @$exp
0:026> ba w4 @$exp+4
After the hardware breakpoints have been set, the following command resumes execution until encountering the first call to the htons
function.
0:026> g 180ffe+msajapi
eax=00000001 ebx=044f8b28 ecx=000007d4 edx=00000000 esi=00000000 edi=053fc028
eip=50bc0ffe esp=05aef614 ebp=05aef6b0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
MSAJApi!ajn::SendRst+0x177:
50bc0ffe ff157cc0c850 call dword ptr [MSAJApi!_imp__htons (50c8c07c)] ds:0023:50c8c07c={WS2_32!htons (774356e0)}
Disassembling the instructions that follow the current instruction pointer shows that the results from both calls to htons
are discarded. Despite these functions being called, their result is not written to anywhere inside the buffer.
0:026> u .-4 L5
MSAJApi!ajn::SendRst+0x173:
50bc0ffa ff74241c push dword ptr [esp+1Ch]
50bc0ffe ff157cc0c850 call dword ptr [MSAJApi!_imp__htons (50c8c07c)]
50bc1004 ff742410 push dword ptr [esp+10h]
50bc1008 ff157cc0c850 call dword ptr [MSAJApi!_imp__htons (50c8c07c)]
50bc100e 8d442454 lea eax,[esp+54h]
After the results from both calls to htons
have been discarded, we resume execution until we encounter the qcc::SendTo
function. This function essentially writes some number of bytes from a pointer to a socket, resulting in a packet being transmitted from the application.
0:026> g 181179+msajapi
eax=05aef688 ebx=044f8b28 ecx=00000f10 edx=05aef6b8 esi=00000000 edi=50b9e5b0
eip=50bc1179 esp=05aef600 ebp=05aef6b0 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
MSAJApi!ajn::SendRst+0x2f2:
50bc1179 e8078ff0ff call MSAJApi!qcc::SendTo (50aca085)
The qcc::SendTo
function takes 7 parameters using the __fastcall
calling convention. The following annotated snippet describes each parameter and its purpose. Relevant to this vulnerability are the 3rd and 4th parameters, representing the buffer
and length
to be sent.
0:026> ub . Lb
MSAJApi!ajn::SendRst+0x2d9:
50bc1160 8d442418 lea eax,[esp+18h]
50bc1164 51 push ecx ; flags
50bc1165 8b4c2418 mov ecx,dword ptr [esp+18h] ; socket
50bc1169 8d5508 lea edx,[ebp+8] ;
50bc116c 50 push eax ; number of bytes sent
50bc116d 6a24 push 24h ; length
50bc116f 8d44247c lea eax,[esp+7Ch] ; &buffer[0]
50bc1173 50 push eax ; buffer
50bc1174 56 push esi ; scope
50bc1175 ff742420 push dword ptr [esp+20h] ; remote port
50bc1179 e8078ff0ff call MSAJApi!qcc::SendTo (50aca085)
Examining the contents of the buffer being sent as a parameter shows that it retains the same contents from when it was allocated during the execution of the function prologue.
0:026> db poi(@esp+4*2) L(dwo(@esp+4*3))
05aef688 28 c0 3f 05 b1 3b 75 75-7f 3b 75 75 53 59 15 05 (.?..;uu.;uuSY..
05aef698 10 c1 a6 50 28 c0 3f 05-28 8b 4f 04 d0 1f 78 75 ...P(.?.(.O...xu
05aef6a8 27 7f 3a 75 '.:u
If we step over the call to qcc::SendTo
, the uninitialized contents of this buffer will be sent to the initiator of the ARDP session.
0:026> p
eax=00000000 ebx=044f8b28 ecx=a512c6c0 edx=00000000 esi=00000000 edi=50b9e5b0
eip=50bc117e esp=05aef618 ebp=05aef6b0 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
MSAJApi!ajn::SendRst+0x2f7:
50bc117e 8b8c2494000000 mov ecx,dword ptr [esp+94h] ss:0023:05aef6ac=a0bc30e4
None of the hardware breakpoints that were set (after the function prologue has allocated space for the buffer) have been triggered. As the buffer has been untampered with, this will always result in leaking 0x24 bytes from the stack.
0:026> g
The proof-of-concept requires Python3 and can be run remotely or locally. If run against localhost, the proof-of-concept will attempt to activate the service before triggering the vulnerability.
$ python poc.py3.zip $ADDRESS
The proof of concept connects to UDP port 9955 of a host and initiates an ARDP connection. In the packet that initiates the session, the version field is specified incorrectly in order to trigger the vulnerability. The following description shows the header of the packet being sent.
<class ardp.ArdpHeader> 'Header'
[0] <instance c(pb(ardp.ArdpHeader._flags)) 'flags'> {bits=8,partial=True} (0x01,8) :> SYN
[1] <instance be(pint.uint8_t) 'hlen'> 0x0e (14)
[2] <instance be(pint.uint16_t) 'src'> 0xb4ae (46254)
[4] <instance be(pint.uint16_t) 'dst'> 0x0000 (0)
[6] <instance be(pint.uint16_t) 'dlen'> 0x00a0 (160)
[8] <instance be(pint.uint32_t) 'seq'> 0x04ccb123 (80523555)
[c] <instance be(pint.uint32_t) 'ack'> 0x5a0c3863 (1510750307)
The following description shows the header of a leaked ARDP packet. The first byte at offset 0 contains the flags.
>>> packet['header']
<class ardp.ArdpHeader> 'Header'
[0] <instance c(pb(ardp.ArdpHeader._flags)) 'flags'> {bits=8,partial=True} (0x01,8) :> SYN
[1] <instance be(pint.uint8_t) 'hlen'> 0x0e (14)
[2] <instance be(pint.uint16_t) 'src'> 0x2768 (10088)
[4] <instance be(pint.uint16_t) 'dst'> 0x0000 (0)
[6] <instance be(pint.uint16_t) 'dlen'> 0x00a0 (160)
[8] <instance be(pint.uint32_t) 'seq'> 0xde678615 (3731326485)
[c] <instance be(pint.uint32_t) 'ack'> 0x79b79195 (2042073493)
The error being triggered in this instance is due to the AllJoyn framework checking that the 2-bit version in the flags is always set to 1. In this case, it has been set to 0.
>>> packet['header']['flags']
<class c(pb(ardp.ArdpHeader._flags))> 'flags' {bits=8,partial=True}
[0.0] <instance c(pbinary.integer) 'VER'> (0x0,2)
[0.4] <instance c(pbinary.integer) 'unused'> (0x0,1)
[0.6] <instance c(pbinary.integer) 'NUL'> (0x0,1)
[0.8] <instance c(pbinary.integer) 'RST'> (0x0,1)
[0.a] <instance c(pbinary.integer) 'EACK'> (0x0,1)
[0.c] <instance c(pbinary.integer) 'ACK'> (0x0,1)
[0.e] <instance c(pbinary.integer) 'SYN'> (0x1,1)
Despite only the version being set incorrectly in this proof-of-concept, any situation where the ARDP session may be reset triggers the vulnerability. This includes the data or header length being incorrect, the wrong flags being set, or the sequence and acknowledgement numbers being out-of-order after a session has been initiated.
https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-38257
2024-05-06 - Vendor Disclosure
2024-09-10 - Vendor Patch Release
2024-09-11 - Public Release
Discovered by a member of Cisco Talos.