在NT系列操作系统里让自己消失=====[ 1. 内容 ]============================================
1. 内容
2. 介绍
3. 文件
3.1 NtQueryDirectoryFile
3.2 NtVdmControl
4. 进程
5. 注册表
5.1 NtEnumerateKey
5.2 NtEnumerateValueKey
6. 系统服务和驱动
7. 挂钩和扩展
7.1 权限
7.2 全局挂钩
7.3 新进程
7.4 DLL
8. 内存
9. 句柄
9.1 命名句柄并获得类型
10. 端口
10.1 Netstat, OpPorts和FPortWinXP下
10.2 OpPorts在Win2k和NT4下, FPort在Win2k下
11. 结束
=====[ 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 ]===============================
因为注册表的结构我们不能请求某个指定部分所有键的列表。我们只能在注册表某个部分通过查询指定键的索引以获得它的信息。这里提供了NtEnumerateKey。
NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass,
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);
KeyHandle是已经用索引标明我们想要从中获取信息的子键的句柄。KeyInformationClass标明了返回信息类型。数据最后写入KeyInformaiton缓冲区,缓冲区长度为KeyInformationLength。写入的字节数由ResultLength返回。
我们需要意识到的最重要的东西是如果我们隐藏了某个键,在这个键之后的所有键的索引都会改变。因为我们是通过高位的索引来获取键的信息,并通过低位的索引来请求这个键。所以我们必须记录之前有多少个记录被隐藏,然后返回正确的值。
让我们来看个例子。假设我们在注册表中有一些键名字是A,B,C,D,E和F。它们的索引从0开始,也就是说索引4对应键E。现在我们如果想要隐藏键B,被挂钩过的应用程序用索引4调用NtEnumerateKey时我们应该返回F键的信息因为有一个索引改变了。现在问题是我们不知道是否会有索引被改变。如果我们不注意索引的改变而对于索引4的请求仍然返回键E而不是键F的话,很有可能在我们用索引1请求时什么都返回不了或者返回键C。这两种情况都会导致错误。这就是为什么我们要注意索引的改变。
现在如果我们通过用索引0到Index重新调用函数来记录转移我们可能会等待一段时间(在1GHz处理器上普通的注册表就得等10秒种那么长的时间)。所以我们不得不想出一种更加巧妙的方法。
我们知道键是按字母排序的(除了引用外)。如果我们忽略引用(我们不需要隐藏)我们能使用以下方法记录改变。我们通过字母排序列出我们想要隐藏的键名的列表(使用RtlCompareUnicodeString),然后当应用程序调用NtEnumerateKey时我们不需要用不可变的变量重新调用它,而能够找到用索引标明的记录的名字。
NTSTATUS RtlCompareUnicodeString(
IN PUNICODE_STRING String1,
IN PUNICODE_STRING String2,
IN BOOLEAN CaseInSensitive
);
String1和String2是将要比较的字符串,CaseInSensitive在不忽略大小写时被设置为True。
函数结果描述String1和String2的关系:
result > 0: String1 > String2
result = 0: String1 = String2
result < 0: String1 < String2
现在我们需要找到一个边缘项。我们在列表中对用索引标明的键按字母比较名字。边缘项是在我们列表中最后一个较短的名字。我们知道转移最多是我们列表中边缘项的数量。但并不是所有我们列表中的项都是注册表中有效的键。所以我们不得不请求我们列表中达到边缘项的所有的在注册表中这个部分的项。这些通过调用NtOpenKey来完成。
NTSTATUS NtOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
KeyHandle是高位的键的句柄,我们使用NtEnumerateKey的这个值。DesaireAccess是访问权力。KEY_ENUMERATE_SUB_KEYS是它的正确的值。ObjectAttributes描述了我们要打开的子键(包括了它的名字)。
#define KEY_ENUMERATE_SUB_KEYS 8
如果NtOpenKey返回0表示打开成功,意味着这个来自我们列表中的键是存在的。被打开的键通过NtClose来关闭。
NTSTATUS NtClose(
IN HANDLE Handle
);
对每次NtEnumareteKey的调用我们要计算的改变,数量上等同于我们列表中存在于注册表指定部分的键的数量。然后我们把改变的数量加到变量Index,最后调用原始的NtEnumerateKey。
我们使用KeyInformationClass的KeyBasicInformation来获得用索引标明的键的名字。
#define KeyBasicInformation 0
NtEnumerateKey在KeyInformation缓冲区中返回这个结构:
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
这里我们只需要的东西是Name和它的长度NameLength。
如果没有被转移的索引的记载我们就返回错误STATUS_EA_LIST_INCONSISTENT。
#define STATUS_EA_LIST_INCONSISTENT 0x80000014
=====[ 5.2 NtEnumerateValueKey ]============================
注册表键值不是按字母分类的。幸运的是在一个键里键值的数目比较少,所以我们可以通过重调的方法来获得改变的数目。用来获取一个键值信息的API是NtEnu