Kernel TCPIP Support for Your Rootkit Using TDI

Kernel TCP/IP Support for Your Rootkit Using TDI

All this talk about TCP/IP naturally leads us to some code. In a Microsoft Windows environment, you basically have two modes in which to write networking code: user mode and kernel mode. The advantage of user mode is that it's easier, but a downside is that it's more visible. With kernel mode, the advantage is more stealth, but the downside is complexity. In the kernel, you don't have as many built-in functions available to you and you must do more stuff "from scratch." In this section, we focus primarily on the kernel-mode approach.

In a kernel-mode approach, the two major interfaces are TDI and NDIS. TDI has the advantage of using the existing TCP/IP stack on the machine. This makes using TCP/IP easier, because you don't have to write your own stack.

On the other hand, a desktop firewall can detect a TCP/IP-embedded communication. With NDIS, you can read and write raw packets to the network and can bypass some firewalls, but on the downside you will need to implement your own TCP/IP stack if you want to use the protocol.

Build the Address Structure

Your rootkit lives in a networked world, so naturally, it should be able to communicate with the network. Unfortunately, the kernel doesn't offer easy-to-use TCP/IP sockets. Libraries are available, but these are commercial packages that cost money. They might also be traceable. You don't need these expensive packages to use TCP/IP in the kernel, of course, but they may be the easiest solutions.

For the do-it-yourself programmer, there is a kernel library that supports TCP/IP functionality, and you can work with it from a kernel-mode device driver. Device drivers can call functions in other drivers; this how you can use TCP/IP from your rootkit.

The TCP/IP services are available from a driver which exposes several devices that have names like /device/tcp and /device/udp. Sound interesting? It is if you need a sockets-like interface from kernel mode.

The Transport Data Interface (TDI) is a specification for talking to a TDI-compliant driver. We are concerned with the TDI-compliant driver in the Windows kernel that exposes TCP/IP functionality. Unfortunately, as of this writing there is no decent example code or documentation you can download to illustrate how to use this TCP/IP functionality. One problem with TDI is that it's so flexible and generic that most documentation on the subject is broad and confusing.

In our discussion focusing on TCP/IP, we have created an example that will ease you into TDI programming.

The first step in programming a TDI client is to build an address structure. The address structure is very much like the structures used in user-mode socket programming. In our example, we make a request to the TDI driver to build this structure for us. If the request is successful, we are returned a handle to the structure. This technique is very common in the driver world: Instead of allocating the structure ourselves, we make a request to another driver, which then builds the structure for us and returns a handle (pointer) to the structure.

To build an address structure, we open a file handle to /device/tcp, and we pass some special parameters to it in the open call. The kernel function we use is called ZwCreateFile. The most important argument to this call is the extended attributes (EA).[12] Within the extended attributes, we pass important and unique information to the driver (see Figure 9-2).

[12] Extended attributes are used mostly by file-system drivers.

Figure 9-2. Driver A makes request to Driver B via the ZwCreateFile call. The extended attributes structure contains the details of the request. The returned file handle is actually a handle to an object built by the lower-level driver.

This is where some documentation can be helpful. The use of the extended attributes argument is unique and specific to the driver in question. In this case, we are to pass information about the IP address and TCP port we want to use for covert communication. The Microsoft DDK documents this, although the documentation isn't very straightforward, and there is no example code.

The extended-attribute argument is a pointer to a structure. The structure is of type FILE_FULL_EA_INFORMATION. This structure is documented in the DDK.

Create a Local Address Object

Now it's time to create an address object. The address object is associated with an endpoint so that communication can begin. The address object is constructed using the extended attributes field of the ZwCreateFile call. The filename used in this call is \Device\Tcp:

Next we initialize the object attributes structure. The most important part of this structure is the transport-device name. We also specify that the string should be treated as case-insensitive. If the target system is Windows 2000 or greater, we should also specify OBJ_KERNEL_HANDLE.

It is always good practice to ASSERT the required IRQ level for the call you're making. This allows your debug version of the driver to throw an assertion if you have not managed your IRQ levels properly.

Next we encounter the extended attributes structure. We specify a buffer large enough to hold the structure plus the TDI address. The structure has a NextEntryOffset field, which we set to zero to indicate that we are sending only one structure in the request. There is also a field called EaName, which we set to the constant TDI_TRANSPORT_ADDRESS. This constant is defined as the string "TransportAddress" in TDI.h.

The EaNameLength field receives the TDI_TRANSPORT_ADDRESS_LENGTH constant. This is the length of the TransportAddress string minus the NULL terminator. We are sure to copy the entire string, including the NULL terminator, when we initialize the EaName field:

The EaValue is a TA_TRANSPORT_ADDRESS structure that contains the local host IP address and the local TCP port to be used for the connection. It contains one or more TDI_ADDRESS_IP structures. If you are familiar with user-mode socket programming, you can think of the TDI_ADDRESS_IP structure as the kernel equivalent of the sockaddr_in structure.

It is best to let the underlying driver choose a local TCP port for you. This way, you never have to manage determining which ports are already in use. The only time the source port needs to be controlled is when connecting over a firewall that has filtering rules that can be defeated using a specific source port (port 80, 25, or 53).

We perform some pointer arithmetic to point to the EaValue location so that we can write the data. The pSin pointer makes it easy for us. We must be sure to set the EaValueLength field to the correct size.

Note: In order to get the underlying driver to choose a source port for us, we supply a desired source port of zero. Be sure to close your ports when you are done with them, or the system will eventually run out of ports! We also set the source address to 0.0.0.0 so that the underlying driver will fill in the local host IP address for us:

That was a lot of code for such a simple operation. However, once you get used to it, the process becomes routine.

The next sections show how to associate the address object with an endpoint and then to finally connect to a server.

Create a TDI Endpoint with Context

Creating a TDI endpoint requires another call to ZwCreateFile. The only change we make to our call is the location pointed to in our "magic" EA_Buffer. You can see that most of the arguments are passed in the EA structure. Our EA buffer should contain a pointer to a user-supplied structure known as the context structure. In our example, we set the context to a dummy value, because we aren't using it.

The connection context is a user-supplied pointer. It can point to anything. This is typically used by driver developers to track the state associated with the connection. CONNECTION_CONTEXT is a pointer to a user-supplied structure. You can put whatever you want in your context structure.

Since we are dealing with only a single connection in our example, we don't need to keep track of anything, so we set the context to a dummy value:

pEA_Buffer->EaValueLength = sizeof(CONNECTION_CONTEXT);

Pay close attention to the very detailed pointer arithmetic in this statement:

Now that we have created an endpoint object, we must associate it with a local address. We have already created a local address object, so now we simply associate it with the new endpoint.

Associate an Endpoint with a Local Address

Having created both an endpoint object and a local address object, our next step is to associate them. An endpoint is worthless without an associated address. The address tells the system which local port and IP address you wish to use. In our example, we have configured the address so that the system will choose a local port for us (similar to the way you expect a socket to work).

Communication with the underlying driver will take place using IOCTL IRPs from this point forward. For each function we wish to call, we must first craft an IRP, fill it with arguments and data, and then pass it down to the next-lowest driver via the IoCallDriver() routine. After we pass each IRP, we must wait for it to complete. To do this, we use a completion routine. An event shared between the completion routine and the rest of our code allows us to wait for processing to complete.

Connect to a Remote Server (Send the TCP Handshake)

Now that a local address is associated with the endpoint, we can create a connection to a remote address. The remote address is the IP address and port to which we want to connect. In our example, we connect to port 80 on IP address 192.168.0.10. Again, we use the completion routine to wait for the IRP to complete. When we call the lower driver, we should expect to see a TCP three-way handshake on the network. We can verify this with a packet sniffer.

It should be noted that the TCP connection can take some time to complete. Since we might be waiting on our completion event for a long while, and we should never block the thread when we are in DriverEntry, our example would be unsuitable for use in an actual rootkit. In the real world, you will need to rearchitect the driver so that a worker thread handles the TCP activity.

Send Data to a Remote Server

To complete the example, we will create instructions to send some data to the remote server. Again, this is performed using an IRP and a wait event. We first allocate some memory for the data to be sent to the remote server. We also lock this memory so that it will not be paged to disk.

Again, the data-sending operation may take time to complete, so in a real-world driver, you would not want to block in the DriverEntry routine.

At this point, we've incorporated kernel support into our rootkit using TDI. This method is useful since the TDI layer handles the TCP/IP protocol for us. The downside is that it cannot easily evade desktop firewalls. It also doesn't allow us to perform low-level manipulation of packets. In the next section, we discuss strategies for raw packet manipulation.