COMMAND
LPC ports
SYSTEMS AFFECTED
WinNT, 2000
PROBLEM
Following is based on a BindView Security Advisory by Todd Sabin.
There are various flaws in the implementation of LPC ports.
LPC ports are a mostly undocumented client/server interprocess
communication mechanism which are used by NT system components.
Recently, they have been fairly well documented by third parties
in:
http://www.amazon.com/exec/obidos/ASIN/0764545698/
http://www.amazon.com/exec/obidos/ASIN/1578701996
The main method of communication with ports is by passing messages
from client to server and back. These messages are of the form:
typedef struct lpc_msg {
unsigned short data_len;
unsigned short msg_len; /* normally data_len + sizeof (struct lpc_msg) */
unsigned short msg_type;
unsigned short address_range_offset;
unsigned long pid; /* process id of client */
unsigned long tid; /* thread id of client */
unsigned long mid; /* message id for this message */
unsigned long callback_id; /* callback id for this message */
/* unsigned char buff[0]; data_len bytes of data for this message */
} LPC_MSG;
One common usage goes something like this:
Server ()
{
HANDLE hPort;
NtCreatePort (&hPort, "MyPort");
while (1) {
NtReplyWaitReceivePort (hPort, NULL, msg_receive);
if (msg_receive->type == connection_request) {
NtAcceptConnectPort ();
NtCompleteConnectPort ();
} else {
...
NtReplyPort (hPort);
}
}
}
Client ()
{
HANDLE hPort;
NtConnectPort (&hPort, "MyPort");
while (1) {
...
NtRequestWaitReplyPort (hPort, msg_in, msg_out);
...
}
}
For complete examples, see URLs above. First one has better
examples, but second one has more complete documentation of the
individual calls. Also, there will be a proof of concept utility
available at
http://razor.bindview.com/tools
which can be used for reproducing the vulnerabilities.
The interesting thing about the way ports are used is that
although a server receives a new handle from NtAcceptConnectPort
for each client that connects, it usually doesn't use that handle
when communicating with its clients. Instead, it uses the
original handle it got from the NtCreatePort call. How then does
the kernel know for which client a particular reply is intended?
It uses the pid, tid, and mid from the message to figure out where
the message should end up.
There are several problems with the LPC ports implementation.
1. Connection Stealing [NT4 only]
===================================
It is possible for any process to call NtAcceptConnectPort and
hijack port connections. The NtAcceptConnectPort api doesn't
require an existing port handle to call. All that's required is
an LPC_MSG with the correct triple of pid, tid, mid. If the
correct triple are specified for an outstanding connection
request, the call succeeds and the process is given a handle to
the port. It can then process requests from that client.
Presumably, it could also impersonate the client with
NtImpersonateClientOfPort.
Note: Win2k has a new API NtSecureConnectPort which allows a
client to verify that the port's server is running with a
particular SID. Also, the Win2k version of NtAcceptConnectPort
verifies that the calling process is the same as the process that
created the server port, which prevents this attack.
Repro:
start porttool -s \BaseNamedObjects\Foo
start porttool -c \BaseNamedObjects\Foo
porttool -s1
(enter pid, tid, and mid printed by porttool -s ...)
2. Denial of Service -- BSOD [NT4 only]
=========================================
As described above, when a server process receives a connection
request from a client, it is supposed to call NtAcceptConnectPort
and NtCompleteConnectPort to complete the connection. However,
if, upon receipt of a connection message, the server calls
NtReplyPort() instead of NtAcceptConnectPort(), then a kernel
exception will be triggered, resulting in a BSOD.
Repro:
start porttool -s2 \BaseNamedObjects\Foo
porttool -c \BaseNamedObjects\Foo
3. Spoofed Replies [W2K, NT4]
==============================
Using NtReplyPort (or any of the NtReply...Port calls), anyone
can reply to a waiting client of any server, provided the attacker
can supply the correct triple of pid, tid, and mid.
Aside from the obvious denial of service problems, there are also
potential methods of exploiting this to gain privilege. One
possibility: the ncalrpc RPC protocol sequence uses LPC as the
transport mechanism. When an RPC client connects to a server
using this transport, it first resolves which LPC port the server
is listening on by contacting the RPC portmapper, which is
listening on the well-known endpoint "\RPC Control\epmapper". By
spoofing a reply to a portmapper request, an attacker could fake
a client into connecting to a port which he is listening on, and
then impersonate the client.
Repro:
start porttool -s \BaseNamedObjects\Foo
start porttool -c \BaseNamedObjects\Foo
porttool -s3 \BaseNamedObjects\Foo2
(enter pid, tid, mid from porttool -s ...)
4. Impersonation of (somewhat) arbitrary processes [NT4, W2K (harder)]
=======================================================================
As earlier reported in [3], NT4 was vulnerable to an attack which
let anyone impersonate any other process on the machine by calling
NtImpersonateClientOfPort with the target's pid and tid, and a mid
of 0. This worked because if a thread is not making an LPC
request, its recorded mid will be 0. Apparently the patch which
fixes this adds a check to be sure that the mid is not 0. This
still allows anyone to impersonate any process, provided that
process is currently making an LPC request, and the attacker can
supply the proper mid.
W2K has an added check which makes this attack more difficult, but
still possible under some circumstances. The attack will still
work on an LPC server, provided that server is in the middle of
an NtReplyWaitReplyPort call, and the attacker can provide the
proper pid, tid, mid, and cid.
Repro on NT4:
start porttool -s \BaseNamedObjects\Foo
start porttool -c \BaseNamedObjects\Foo
porttool -s4 \BaseNamedObjects\Foo2
[in another window] porttool -c \BaseNamedObjects\Foo2
(enter pid, tid, mid, cid from porttool -s)
Repro on W2K:
start porttool -s4b \BaseNamedObjects\Foo
start porttool -c \BaseNamedObjects\Foo
porttool -s4 \BaseNamedObjects\Foo2
[in another window] porttool -c \BaseNamedObjects\Foo2
enter pid, tid, mid, and cid port porttool -s4b.
Note that the pid and tid in this case are the ones that belong
to porttool -s4b itself, not the ones it prints from the lpc
message.
5. Reading and writing other processes' address space [W2K, NT4]
=================================================================
Besides the normal method of passing request/reply data in the
message itself, there is another means by which LPC clients and
servers can pass data. NtReadRequestData and NtWriteRequestData
allow an LPC server to read and write to certain parts of the
client's address space, as specified by the client in the request
message. To enable this, a client fills out a struct like this:
struct addr_ranges {
unsigned long num_entries;
struct addr_entry {
void *addr;
size_t len;
} addrs[N]; /* where N is num_entries */
};
The client then puts this structure within the data portion of its
message, and sets the address_range_offset of the LPC_MSG to be
the offset of the struct in the message, and then makes a
NtRequestWaitReplyPort () call as usual. Now the server can read
or write to the specified address ranges with
Nt{Read,Write}RequestData.
5a. Reading/writing portions of other clients' address spaces
==============================================================
The previous description is how things are supposed to work.
However, due to a bug, it is possible for one client of a port to
use these calls as if it were the server, and access areas of
memory in a second client, provided that the second client is
making a call using the address_range_offset feature, and again
assuming the attacker can provide the proper pid, tid, mid.
Repro:
start porttool -s5a \BaseNamedObjects\Foo
start porttool -c5a-1 \BaseNamedObjects\Foo
porttool -c5a-2 \BaseNamedObjects\Foo
(enter pid, tid, mid, cid from porttool -s5)
5b. Reading/writing arbitrary areas of other processes
=======================================================
Through a more elaborate attack, it is also possible for a server
to read and write arbitrary areas of other processes address
spaces, whether or not those processes are clients of the server
or not.
When a client make a call using the address_range_offset feature,
the kernel keeps a copy of the request on a list inside the
server's port object, and then removes it when the matching reply
is sent. The list is searched based on the mid and callback_id
of the LPC_MSG.
The way Nt{Read,Write}RequestData work is the kernel takes the
LPC_MSG passed in, looks up the thread referenced by the pid and
tid, and verifies that the thread is currently making a lpc call
that matches the mid the server specified. Then, it verifies
that the server port actually has an outstanding request with a
matching mid and callback_id. It does this is by looking for the
MSG on the list mentioned earlier.
Now, since the target thread's mid must match some mid on the
server's outstanding request list, in theory, the server should
only be able to access its clients' address spaces. However, the
mid is only a 32 bit integer, so there are only 2^32 possible
mids. Once they've all been used, they will start being reused,
meaning that there's the potential for collisions, provided a
server doesn't send replies to its clients during the time it
takes to wrap the mid counter. Testing shows that it takes about
21.5 hours on a PII 300 to run through 2^32 mids using a client
and server that do nothing but request/reply in a tight loop.
So, the attack goes like this: A server starts and listens on a
port. Then, a client connects to it and send requests to it
using the address_range_offset feature. The address ranges
specified here are what the server will be able to access later,
so they need to be known up front. To increase the odds of a
successful collision, the client can send several thousand
requests to the server. Now, this would normally require several
thousand threads since the server needs to have these as
_outstanding_ requests later in the attack, it can't reply to
them.
However, the outstanding request list is searched by mid and
callback_id, as mentioned previously, but replies are sent based
only on mid, so it's actually possible for the server to send a
reply to the client without having it removed from its outstanding
request list. Before replying, the server just changes the
callback_id so it won't match; the reply is sent as usual, but it
stays on the outstanding request list.
Now, once the server has lots of outstanding requests for later
use, the attacker does requests/replies in a tight loop until the
mids roll back to the point where the server has some matching
mids. Finally, the server specifies the target's pid and tid, and
tries every mid that it has stored. If it misses, it can try
again the next time around until it succeeds.
Repro:
start porttool -s5b \BaseNamedObjects\Foo
start porttool -s5b-2 \BaseNamedObjects\Foo2
porttool -c5b \BaseNamedObjects\Foo \BaseNamedObjects\Foo2
(wait until mids wrap around)
start porttool -s \BaseNamedObjects\Foo3
porttool -c \BaseNamedObjects\Foo3
(in window for porttool -s5b)
enter pid, tid, mid, cid from porttool -s
6. Consuming kernel memory. [W2K, NT4]
=========================================
As mentioned in 5, the kernel keeps copies of all outstanding LPC
messages in kernel memory. It allocates memory from an LPC 'zone'
for these messages. When a message is finished, it's memory is
returned to the zone and can be reused. If is no memory left in
the zone when a new request comes in, then additional kernel
memory will be allocated and added to the zone. Once added to
the LPC zone, the memory is never released.
Using the trick about mismatched callback ids from number 5, it's
possible for a rogue server to arrange that none of the messages
which are sent to it get freed back to the lpc zone. This will
result in the size of the lpc zone growing continually. Once the
server exits (or is killed), the memory will finally be returned
to the lpc zone, but the memory in the zone will never be freed
back to the general kernel memory pool.
Repro:
start porttool -s6 \BaseNamedObjects\Foo
porttool -c6 \BaseNamedObject\Foo
7. Fragile LPC servers -- BSOD [NT4 only]
=============================================
If a client connects to either of \DbgSsApiPort or \DbgUiApiPort
and sends a garbage request, the machine will BSOD.
Repro:
porttool -c \DbgSsApiPort
or
porttool -c \DbgUiApiPort
SOLUTION
Install the hotfix(es) from Microsoft or limit local logon rights.
Microsoft's Hotfix:
NT4: http://www.microsoft.com/Downloads/Release.asp?ReleaseID=24650
W2K: http://www.microsoft.com/Downloads/Release.asp?ReleaseID=24649