tion.hProcess=hProcess; csrmsg.ProcessInformation.hThread=hThread; csrmsg.ProcessInformation.dwProcessId=pid; csrmsg.ProcessInformation.dwThreadId=tid;
csrmsg.PortMessage.MessageSize=0x4c; csrmsg.PortMessage.DataSize=0x34;
csrmsg.CsrssMessage.Opcode=0x10000;
ZwRequestWaitReplyPort(handleIndex,(PORT_MESSAGE*)&csrmsg, (PORT_MESSAGE*)&csrmsg); } }
The solution to the problem was obvious; Switch context to one in which the port is open, e.g. to the context of any win32-process. I inser- ted KeAttachProcess(HelperProcess) before calling Nebbet's InformCsrss, and KeDetachProcess afterwards. The role of the HelperProcess was taken by calc.exe.
When I tried using KeAttachProcess that way I failed though: The con- text was switched (visible using the proc command in SoftICE), but Csr- ClientCallServer returned STATUS_ILLEGAL_FUNCTION. Only Uncle Bill knows what was happening inside CSRSS.
When trying to frame the whole process creation function into KeAttachProcess/KeDetachProcess led to the following error when calling ZwCreateProcess: "Break Due to KeBugCheckEx (Unhandled kernel mode exception) Error=5 (INVALID_PROCESS_ATTACH_ATTEMPT) ... ".
A different way to execute my code in the context of an arbitrary process is APC. The APC may be kmode or user-mode. As long as only kmode APC may overcome nonalertable wait state, all code for process creation must be done in kernel mode. Nebbet's code normally works at IRQL == APC_LEVEL Code execution in the context of a given win32-process by means of APC is implemented in the StartShell() function, in file ShellAPC.cpp.
Interaction with the process =============================
Starting a process isn't all. The Backdoor still needs to communicate with it: It is necessary to redirect it's stdin/stdout/stderr to our driver. We could do this like most "driver+app"-systems: Create a device that is visible from user-mode, open it using ZwOpenFile and pass the handle to the starting process (stdin/stdout/stderr). But a named device is not stealthy, even if we automatically create a random names. This is why I have chosen to use named pipes instead.
Windows NT uses named pipes with names like Win32Pipes.%08x.%08x (here %08x is random 8-digit numbers) for emulation of anonymous pipes. If we create one more such pipe, nobody will notice. Usually, one uses 2 anon- ymous pipes r redirecting I/O of a console application in Win32, but when using a named pipe one will be sufficient as it is bi-directional. The driver must create a bi-directional named pipe, and cmd.exe must use it's handle as stdin/stdout/stderr.
The handle can be opened in both kmode and user-mode. The final ver- sion uses the first variant, but I have also experimented with the second variant -- being able to implement different variants may help evade anti- viruses. Starting a process with redirected I/O has been completely imple- mented in kernel mode in the file NebbetCreateProcess.cpp.
There are two main differences between my and Nebbet's code: The fun- ctions that are not exported from ntoskrnl.exe but from ntdll, are dyn- amically imported (see NtdllDynamicLoader.cpp). The handle to the named pipe is opened with ZwOpenFile() and passed to the starting process with ZwDuplicateObject with DUPLICATE_CLOSE_SOURCE flag.
For opening the named pipe from user mode I inject code into a start- ing process. I attached the patch (NebbetCreateProcess.diff) for edu- cational purposes. It adds a code snippet to a starting process. The patch writes code (generated by a C++ compiler) to a process's stack. For independence that code is a function which accepts a pointer to a struc- ture containing all the necessary data (API addresses etc) as parameter. This structure and a pointer to it are written to the stack together with the code of the function itself. ESP of the starting thread is set 4 bytes bellow the pointer to the parameters of the function, and EIP to it's en- try point. Once the injected code is done executing, it issues a CALL back to the original entry point. This example can be modified to be yet another way of injecting code into a working userland process from kernel mode.
---[ 4.2 - Activation and communication with the remote client
If a listening socket is permanently open (and visible to netstat -an) it is likely to be discovered. Even if one hides the socket from netstat is insufficient as a simple portscan could uncover the port. To remain stealthy a backdoor must not have any open ports visible locally or re- motely. It is necessary to use a special packet, which on the one hand must be unambigously identified by the backdoor as activation signal, yet at the same time must not be so suspicious as to trigger alerts or be fil- tered by firewalls. The activation signal could e.g. be a packet contain- ing a set of packets at any place (header or data) -- all characteristics of the packet (protocol, port etc) should be ignored. This allows for max- imum flexibility to avoid aggressive packet filters.
Obviously, we have to implement some sort of sniffer in order to detect such a special packet. In practice, we have several choices on how to implement the sniffer:
1) NDIS protocol driver (advantage: possibility not only to receive packets, but also to send - thus making covert channel for communication with remote client possible; disadvantage: difficulties with supporting all types of network devices) - applied in ntrootkit;
2) use service provided by IpFilterDriver on w2k and higher (advantages: simple implementation and complete independence from physical layer; disadvantage: receive only);
3) setup filter on 1 of network drivers, through which packets pass through (see [5]);
4) direct appeal to network drivers by some other means for receive and send packets (advantage: can do everything; disadvantage: unexplored area).
I have chosen variant 2 due to it's simplicity and convenience for both described variants of starting a shell. IpFilterDriver used only for activation, further connection is made via TCP by means of TDI.
An example of the usage of IpFilterDriver can be seen in Filtering.cpp and MPFD_main.cpp. InitFiltering() loads the IpFilterDriver if it isn't yet loaded. Then it calls SetupFiltering, which sets a filter with IOCTL_PF_SET_EXTENSION_POINTER IOCTL. PacketFilter() is then called on each IP packet. If a keyword is detected StartShellEvent is set and causes a shell to be started.
The variant using shellcode in an existing process works with the network in user-mode, thus we do not need to describe anything in detail.
A Kernel-mode TCP shell is implemented in NtBackd00r.cpp. When cmd.exe is started from a driver with redirected I/O, the link is maintained by the driver. I took the tcpecho example as base for the communitcation mod- ule in order not to waste time coding a TDI-client from scratch. DriverEntry() initialises TDI, creates a listening socket and an unnamed device for IoQueueWorkItem.
For each conenction an instance of the Session class is created. In it's OnConnect handler a sequence of operations for creating a process. process. As long as this handler is called at IRQL==DISPATCH_LEVEL, it's impossible to do all necessary operations directly in it. It's even impossible to start a thread because PsCreateSystemThread must be called only at PASSIVE_LEVEL according to the DDK. Therefore the OnConnect handler calls IoAllocateWorkItem and IoQueueWorkItem in order to do any further operations accomplished in WorkItem handler (ShellStarter function) at PASSIVE_LEVEL.
ShellStarter calls StartShell() and creates a worker thread (DataPumpThread) and 2 events for notifying it about arriving packets and named pipe I/O completion. Interaction between the WorkItem/thread and Session class was built with taking a possible sudden disconnect and freeing Session into account: syncronisation is accomplished by disabling interrupts (it's equivalent of raise IRQL to highest) and by means of DriverStudio classes (SpinLock inside). The Thread uses a copy of some data that must be available even after instance of Session was deleted.
Initially, DataPumpThread starts one asynchronous read operation (ZwReadFile) from named pipe -- event hPipeEvents[1] notifies about it's completion. The other event hPipeEvents[0] notifies about data arrival from the network. After that ZwWaitForMultipleObjects executed in a loop waits for one of these events. In dependence of what event was signaled, the thread does a read from the named pipe and sends data to client, or does a read read from FIFO and writes to pipe. If the Terminating flag is set, thread closes all handles, terminates the cmd.exe process, and then terminates itself. Data arrival is signaled by the hPipeEvents[0] event in Session::OnReceive and Session::OnReceiveComplete handlers. It also used in conjunction with the Terminating flag to notify the thread about termination.
Data resceived from the network is buffered in pWBytePipe FIFO. DataPumpThread reads data from the FIFO to temporary buffers which are allocated for each I/O operation and writes data asynchronously to the pipe (ZwWriteFile). The buffers are freed asynchronously in the ApcCallback- WriteComplete handler.
Data transfers from the pipe to the network are also ac 上一页 [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] ... 下一页 >> |