BindView Security Advisory -------- Various security vulnerabilities with LPC ports Issue Date: October 3, 2000 Contact: Todd Sabin Topic: LPC ports Overview: There are various flaws in the implementation of LPC ports. Affected Systems: Windows NT4 up to and including SP6a Window 2000 up to and including SP1 Impact: Denial of service to possible local promotion. Background: 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 [1] and [2]. 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 [1] and [2]. [1] has better examples, but [2] 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. Details: 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. [I don't as yet understand the exact circumstances under which a normal server will call NtReplyWaitReplyPort. If someone has more info on this, please drop me some mail.] 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] Background 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 Workarounds: none known. Recommendations: Install the hotfix(es) from Microsoft. Limit local logon rights. References: Microsoft's security bulletin: http://www.microsoft.com/technet/security/bulletin/MS00-070.asp Microsoft's Hotfix: NT4: http://www.microsoft.com/Downloads/Release.asp?ReleaseID=24650 W2K: http://www.microsoft.com/Downloads/Release.asp?ReleaseID=24649 Microsoft's Knowledge Base article: http://www.microsoft.com/technet/support/kb.asp?ID=266433 (may take a couple days to appear) [1] Undocumented Windows NT http://www.amazon.com/exec/obidos/ASIN/0764545698/ [2] Windows NT/2000 Native API Reference http://www.amazon.com/exec/obidos/ASIN/1578701996 [3] BindView Security Advisory on NtImpersonateClientOfPort http://razor.bindview.com/publish/advisories/adv_NTPromotion.html