=====[ 2. 介绍 ]==================================================
这篇文档是在Windows NT操作系统下隐藏对象、文件、服务、进程等的技术。这种方法是基于Windows API函数的挂钩。
这篇文章中所描述的技术都是从我写rootkit的研究成果,所以它能写rootkit更有效果并且更简单。这里也同样包括了我的实践。
在这篇文档中隐藏对象意味着改变某些用来命名这些对象的系统函数,使它们将忽略这些对象的名字。这样一来我们改动的那些函数的返回值表示这些对象根本就不存在。
最基本的方法(除去少数不同的)是我们用原始的参数调用原始的函数,然后我们改变它们的输出。
在这篇文章里将描述隐藏文件、进程、注册表键和键值、系统服务和驱动、分配的内存还有句柄。
=====[ 3. 文件 ]========================================
在有很多种隐藏文件使系统无法发现的可能。我们只使用改变API的方法,而没使用那些比如涉及到文件系统的技术。这样会更容易些因为我们无法知道文件系统工作的独特性。
=====[ 3.1 NtQueryDirectoryFile ]=============================
在WINNT里在某些目录中寻找某个文件的方法是枚举它里面所有的文件和它的子目录下的所有文件。文件的枚举是使用NtQueryDirectoryFile函数。
NTSTATUS NtQueryDirectoryFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN FILE_INFORMATION_CLASS FileInformationClass,
IN BOOLEAN ReturnSingleEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartScan
);
对我们来说重要的参数是FileHandle,FileInformation和FileInformationClass。FileHandle是从NtOpenFile获得的目录对象句柄。FileInformation是一个指针,指向函数要写入需要的数据的已分配内存。FileInformationClass决定写入FileImformation的记录的类型。
FileInformationClass是一个变化的枚举类型,我们只需要其中4个值来枚举目录内容:
#define FileDirectoryInformation 1
#define FileFullDirectoryInformation 2
#define FileBothDirectoryInformation 3
#define FileNamesInformation 12
要写入FileInformation的FileDirecoryInformation记录的结构:
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
FileFullDirectoryInformation:
typedef struct _FILE_FULL_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
WCHAR FileName[1];
} FILE_FULL_DIRECTORY_INFORMATION, *PFILE_FULL_DIRECTORY_INFORMATION;
FileBothDirectoryInformation:
typedef struct _FILE_BOTH_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
UCHAR AlternateNameLength;
WCHAR AlternateName[12];
WCHAR FileName[1];
} FILE_BOTH_DIRECTORY_INFORMATION, *PFILE_BOTH_DIRECTORY_INFORMATION;
FileNamesInformation:
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
这个函数在FileInformation中写入这些结构的一个列表。对我们来说在这些结构类型中只有3个变量是重要的。
NextEntryOffset是这个列表中项的偏移地址。第一个项在地址FileInformation+0处,所以第二个项在地址是FileInformation+第一个项的NextEntryOffset。最后一个项的NextEntryOffset是0。
FileName是文件全名。
FileNameLength是文件名长度。
如果我们想要隐藏一个文件,我们需要分别通知这4种类型,对每种类型的返回记录我们需要和我们打算隐藏的文件比较名字。如果我们打算隐藏第一个记录,我们可以把后面的结构向前移动,移动长度为第一个结构的长度,这样会导致第一个记录被改写。如果我们想要隐藏其它任何一个,只需要很容易的改变上一个记录的NextEntryOffset的值就行。如果我们要隐藏最后一个记录就把它的NextEntryOffset改为0,否则NextEntryOffset的值应为我们想要隐藏的那个记录和前一个的NextEntryOffset值的和。然后修改前一个记录的Unknown变量的值,它是下一次搜索的索引。把要隐藏的记录之前一个记录的Unknown变量的值改为我们要隐藏的那个记录的Unkown变量的值即可。
如果没有原本应该可见的记录被找到,我们就返回STATUS_NO_SUCH_FILE。
#define STATUS_NO_SUCH_FILE 0xC000000F
=====[ 3.2 NtVdmControl ]========================================
不知什么原因DOS的枚举NTVDM能够通过函数NtVdmControl也能获得文件的列表。
NTSTATUS NtVdmControl(
IN ULONG ControlCode,
IN PVOID ControlData
);
ConcrolCode标明了在缓冲区ControlData中申请数据的子函数。如果ControlCode为VdmDiretoryFile那么这个函数的功能将和FileInformation设置为FileBothDirectoryInformation的函数NtQueryDirectoryFile功能一样。
#define VdmDirectoryFile 6
这时的ControlData的用法就和FileInformation一样。这里唯一的不同就是我们不知道缓冲区的长度。所以我们需要手动来计算它的长度。我们把所有记录的NextEntryOffset和最后一个记录的FileNameLength还有0X5E(最后一个记录除去文件名的长度)。隐藏的方法和前面提到的使用NtQueryDirectoryFile的方法一样。
=====[ 4. 进程 ]========================================
各种进程信息是通过NtQuerySystemInformation获取的。
NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
SystemInformationClass标明了我们想要获得的信息的类别,SystemInformation是一个指向函数输出缓冲区的指针,SystemInformationLength是这个缓冲区的长度,ReturnLength是写入字节的数目。
对于正在运行的进程的枚举我们使用设置为SystemProcessesAndThreadsInformation的SystemInformationClass。
#define SystemInformationClass 5
在SystemInformation的缓冲区中返回的数据结构是:
typedef struct _SYSTEM_PROCESSES {
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved1[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters; // Windows 2000特有的
SYSTEM_THREADS Threads[1];
} SYSTEM_PROCESSES, *PSYSTEM_PROCESSES;
隐藏进程和隐藏文件方法基本一样,就是改动我们需要隐藏的记录的前一个记录的NextEntryDelta。通常我们不用隐藏第一个记录,因为它是空闲进程(Idle process)。
=====[ 5. 注册表 ]========================================
Windows的注册表是一个很大的树形数据结构,对我们来说里面有两种重要的记录类型需要隐藏。一种类型是注册表键,另一种是键值。因为注册表的结构,隐藏注册表键不象隐藏文件或进程那么麻烦。
=====[ 5.1 NtEnumerateKey ]===============================
因为注册表的结构我们不能请求某个指定部分所有键