--[ 4 - My variant: The thorny path
----[ 4.1 - Shell
I originally intended to do something similarily simple as standard
user-mode code: Just pass a socket handle for stdin/stdout/stderr to the
newly created cmd.exe process. I did not find a way to open a useful
socket from a driver though, as the interface with the AFD driver (kmode
core of winsock) is undocumented. Reverse-engineering it's usage was not
an option either as due to changes between versions my technique would be
unreliable. I had to find a different way.
First variant
=============
We could start our code in the context of some process, using a shell-
code quite similar to that used in exploits. The code could wait for a TCP
connection and start cmd.exe with redirected I/O.
I chose this way when I tired of trying to start a full-fledged win32
process from a driver. The shellcode is position-independent, searches for
kernel32.dll in memory and loads the winsock library. All that needs to be
done is injecting the shellcode into the address space of a process and
pass control to the entry point of the shellcode. However, in the process
of doing this the normal work of the process must not be interrupted, be-
cause a failure in a critical system process will lead to a failure of the
whole system.
So we need to allocate memory, write shellcode there, and create a
thread with EIP = entry point of the shellcode. Code to do this can be
found in the attached file shell.cpp. Unfortunately, when CreateProcess
is called from the thread started in this way it failed, most probably
because something that CreateProcess relies upon was not initialized pro-
poerly in the context of our thread. We thus need to call CreateProcess
from a thread context which has everything that CreateProcess needs ini-
tialized -- we're going to take a thread which belongs to the process we
are intruding into (I used SetThreadContext for that). One needs to re-
store the state of the thread prior to the interruption so it can contiue
it's normal operation.
So we need to: Save thread context via GetThreadContext, set the EIP
to our context via SetThreadContext, wait for the code to complete, and
then restore the original cont again. The rest is just a usual shellcode
for Windows NT (full code in dummy4.asm).
One unsolved problem remains: If the thread is in waiting state, it
will not run until it wakes up. Using ZwAlertThread does not yield any re-
sult if the thread is in a nonalertable wait state. Fortunately, the
thread in services.exe worked without a problem -- this does not imply it
will stay like this in the future though, so I continued my research:
Second variant
==============
Things are not as easy as [4] makes them sound. Creating a full-
fledged win32-process requires it's registration in the CSRSS subsystem.
This is accomplished by using CsrClientCallServer(), which receives all
necessary information about the process (handles, TID, PID, flags). The
functions calls ZwRequestWaitReplyPort, which receives a handle of a pre-
viously opened port for connection with CSRSS.
This port is not open in the SYSTEM process context. Opening it never
succeeded (ZwConnectPort returned STATUS_PORT_CONNECTION_REFUSED). Play-
ing with SECURITY_QUALITY_OF_SERVICE didn't help. While disassembling
ntdll.dll I saw that ZwConnectPort calls were preceded by ZwCreateSection.
But there was no time and no desire to play with sections. Here is the
code that didn't work:
VOID InformCsrss(HANDLE hProcess,HANDLE hThread,ULONG pid,ULONG tid)
{
CSRMSG csrmsg;
HANDLE hCurProcess;
HANDLE handleIndex;
PVOID p;
_asm int 3;
UNICODE_STRING PortName;
RtlInitUnicodeString(&PortName,L"\\Windows\\ApiPor t");
static SECURITY_QUALITY_OF_SERVICE QoS =
{sizeof(QoS), SecurityAnonymous, 0, 0};
/*static SECURITY_QUALITY_OF_SERVICE QoS =
{0x77DC0260,
(_SECURITY_IMPERSONATION_LEVEL)2, 0x120101, 0x10000};*/
DWORD ret=ZwConnectPort(&handleIndex,&PortName,&QoS,NULL ,
NULL,NULL,NULL,NULL);
if (!ret) {
RtlZeroMemory(&csrmsg,sizeof(CSRMSG));
csrmsg.ProcessInformation.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 accomplished through
temporary buffers that are allocated before ZwReadFile and freed in
Session::OnSendComplete.
Paths of data streams and temporary buffers handling algorithm:
NamedPipe -(new send_buf; ZwReadFile)-> temporary buffer
send_buf -(send)-> Network -> OnSendComplete{delete send_buf}
Network -(OnReceive)-> pWBytePipe -(new rcv_buf)-> temporary
buffer rcv_buf -(ZwWriteFile)-> NamedPipe ->
ApcCallbackWriteComplete{delete rcv_buf}
In Session::OnReceive handler data is written to the FIFO and the
DataPumpThread is notified about it's arrival. If the transport has more
data available than indicated another buffer is allocated to read the
rest. When the transport is done - asynchronously - OnReceiveComplete()
handler is called, which does the same as OnReceive.
----[ 4.3 - Stealth on disk
I've implemented simple demo module (file Intercept.cpp) which hooks
dispatch functions of a given filesystem diver to hide the first N bytes of
a given file. To hook FSD call e.g. Intercept(L"\\FileSystem\\Fastfat").
There is only 2 FSDs that may be necessary to hook: Fastfat ant Ntfs,
because NT can boot from these filesystems.
Intercept() replaces some driver dispatch functions
(pDriverObject->MajorFunction[...], pDriverObject->FastIoDispatch->...).
When hooked driver handles IRPs and FastIo calls the corresponding hook
functions modifies file size and current file offset. Thus all user-mode
programs see file N bytes smaller than original, containing bytes N to
last. It allows to implement trick described in part 3

Rest In Part III