如何在Windows NT中隐藏自己

  CVC电脑病毒论坛 (http://www.retcvc.com/cgi-bin/leobbs.cgi)
-- 病毒技术研究 (http://www.retcvc.com/cgi-bin/forums.cgi?forum=1)
--- 病毒技术翻译专版 (http://www.retcvc.com/cgi-bin/forums.cgi?forum=19)
---- Invisibility on NT boxes (http://www.retcvc.com/cgi-bin/topic.cgi?forum=19&topic=81)


-- 作者: pker
-- 发布时间: 2004/10/21 00:02pm

=============================[ 在NT“盒子”里消失 ]=============================

                         如何在Windows NT中隐藏自己
                         --------------------------
                         
                          作者:Holy_Father <holy_father@phreaker.net>
                          版本:1.2 英语
                          日期:05.08.2003
                         
                          翻译:pker / CVC翻译小组
                         
                         
=====[ 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 WinXP的Netstart,OpPorts,WinXP的FPort
       10.2 Win2k和NT4的OpPorts,Win2k的FPort
11. 结束语


=====[2. 介绍 ]=================================================================

这篇文档是关于在Windows NT中隐藏对象、文件、服务和进程等的技术。这些方法是建立在
挂钩Windows API的基础上的,具体描述见我的“挂钩Windows API”。

所有的这些都是我在编写rootkit代码时自己研究出来的,所有我在写这篇文章时的效率很高,
而且很容易就写成了。这要归功于我的付出。

在这篇文档中所提到的对任意对象的隐藏是指通过改变命名对象的系统过程使之跳过对这个
对象的命名过程。这样这个对象就只是这个过程的返回值,好象它不存在一样。

基本方法(不包括描述上的区别)是我们使用原始调用和原始函数然后我们改变它的输出。

在这个版本的文档中我们讲述如何隐藏文件、进程、关键字和注册表键值,系统服务和驱动,
分配的内存和句柄。


=====[ 3. 文件 ]================================================================

有很多隐藏文件而使其对系统不可见的可能。我们只针对改变API的技术而不涉及那些修改文
件系统的技术。这也更加简单因为我们不需要知道很多实际的文件系统是如何工作的。

=====[ 3.1 NtQueryDirectoryFile ]===============================================

Windows NT中,在目录中寻找文件是通过在这个目录和它所有的子目录中寻找得到的。因为
枚举文件要用到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。File-
Handle是一个可以从NtOpenFile得到的目录对象的句柄。FileInformation是一个指向一块
已分配内存的指针,函数向这里写入用户想要得到的信息。FileInformationClass决定在
FileInformation中写入的记录类型。

FileInformationClass是一个可变的枚举类型,但是我们只需要其中的四个值,这四个值用
来枚举目录的内容。

   #define FileDirectoryInformation        1
   #define FileFullDirectoryInformation    2
   #define FileBothDirectoryInformation    3
   #define FileNamesInformation            12
   
对于FileDirectoryInformation写入FileInformation的记录结构是:

   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写入一个这些结构的列表。在这些结构类型中只有三个变量对我
们很重要。

NextEntryOffset是详细列表项的长度。第一项可以在地址FileInformation + 0处找到。所
以第二项就是在第一项的偏移FileInformation + NextEntryOffset处。最后一项的Next-
EntryOffset字段为0。

FileName是文件的完成文件名。

FileNameLength是文件名的长度。

如果我们想要隐藏一个文件,我们要分辨出这四个类型的结构然后对每一个返回的记录我们
需要把其中的文件名与我们要隐藏的文件名进行比较。如果我们要隐藏第一个记录,我们就
要根据第一个结构的大小移动后面的结构。这就导致第一个记录被重写。如果我们要隐藏另
一个记录,我们可以简单的改写前一个记录的NextEntryOffset字段。如果我们想隐藏最后一
个记录,那么它前面一个记录的NextEntryOffset字段应该置为0,否则这个字段的值应该是
我们要隐藏的记录和前一记录的NextEntryOffset字段的和。然后我们要改写前一记录的
Unknown字段的值,这个值是下一个记录的索引号。前一记录的Unknown值应该写为我们要隐
藏的记录的Unknown字段值。

如果没有找到可见的记录,我们会得到一个表示错误的返回值STATUS_NO_SUCH_FILE。

   #define STATUS_NO_SUCH_FILE 0xC000000F
   
   
=====[ 3.2 NtVdmControl ]=======================================================

出于一些原因,DOS模拟器NTVDM可以用过NtVdmControl调用获得文件列表。

   NTSTATUS NtVdmControl(        
       IN ULONG ControlCode,
       IN PVOID ControlData
   );

ControlCode指定向ControlData缓冲中提供数据的子功能。如果ControlCode等于VdmDirect-
oryFile,那么这个函数与FileInformationClass字段填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是写入字节数。

我们可以通过把SystemInformationClass字段设置为SystemProcessAndThreadsInformation
来枚举运行中的进程。

   #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;

隐藏进程和隐藏文件类似。我们需要改变要隐藏进程的前一记录的NextEntryData字段。通常
我们不会隐藏第一个记录,因为那通常是Idle进程。


=====[ 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是我们要通过Index索引的子键的的句柄,我们要从这个子键中获得信息。返回信
息的类型由KeyInformationClass指定。数据被写入KeyInformation缓冲,其大小由Key-
InformationLength指定。写入的字节数返回到ResultLength中。

最主要的我们要注意到的是如果我们隐藏了一个键,所有的键的索引都会改变。并且因为我
们可以通过一个低索引的记录得到一个高索引的记录,所以我们通常要计算在这个记录之前
我们隐藏了多少个记录然后返回一个正确的值。

让我们看一个例子。假定我们的注册表中有一些键叫做A,B,C,D,E和F。从0开始为它们编
号,也就是说键E的索引为4。现在如果我们想要隐藏键B然后当被挂钩的NtEnumerateKey函数
以Index为4被调用时我们应该返回F,因为这里要进行索引的改变。问题是我们并不知道这里
需要进行改变。并且如果我们不管这个改变并当请求索引为4的键时我们返回了E而不是F,那
么当查询索引为1的键的时候我们会什么都不返回或者返回C。这两种情况都是错误的。这就
是为什么我们要考虑索引的改变。

现在如果我们通过重新调用函数为每个键计算偏移我们有时会等很长时间(在1G赫兹处理器
上对于标准的注册表这会占用10秒钟的时间)。所以我们必须想一些奇特的方法。

我们知道键是根据字母表排序的(引用除外)。如果我们忽略引用(这个我们也不想要隐藏)
我们可以用下面的方法计算偏移。我们把我们要隐藏的键的名字按字母表排序(可以用Rtl-
CompareUnicodeString函数),然后当应用程序调用NtEnumerateKey我们不用以不变的参数
重新调用函数而是找到Index指定的记录的名字。

   NTSTATUS RtlCompareUnicodeString(      
       IN PUNICODE_STRING String1,
       IN PUNICODE_STRING String2,
       IN BOOLEAN  CaseInSensitive  
   );

String1和String2是需要比较的字符串,如果我们要忽略字符的大小写可以把CaseInSensi-
tive置为真。

函数返回值描述了String1和String2的关系:

返回值 > 0:  String1 > String2
返回值 = 0:  String1 = String2
返回值 < 0:  String1 < String2

现在我们需要找到一个边界。我们要对由Index指定的键名和我们列表里的键名进行字母比
较。我们知道,偏移的最大值就是我们的列表中键的数量。但是并不是我们列表中的所有项
对应注册表的其中一部分都有效。所以我们要看我们列表中的每一项是否在注册表的这个部
分里。我们可以用NtOpenKey。

   NTSTATUS NtOpenKey(
       OUT PHANDLE KeyHandle,
       IN ACCESS_MASK DesiredAccess,
       IN POBJECT_ATTRIBUTES ObjectAttributes
   );

KeyHandle是一个主键的句柄。我们可以使用NtEnumerateKey返回的值。DesiredAccess是访
问权限。应该用KEY_ENUMERATE_SUB_KEYS来填写这个字段。ObjectAttributes描述了我们要
打开的子键(包括它的名字)。

   #define KEY_ENUMERATE_SUB_KEYS 8
   
如果NtOpenKey返回的结果是0说明打开成功,表示我们列表中的这个键存在。打开的键要通
过NtClose关闭。

   NTSTATUS NtClose(
       IN HANDLE Handle
   );
   
对于每个NtEnumerateKey调用我们都要计算相对列表中的存在于注册表给定区域的键的偏
移。然后我们把这个偏移加到Index参数上然后调用原始的NtEnumerateKey函数。

为了得到Index指定的键的名字,我们可以使用KeyBasicInformation作为KeyInformation-
Class的值。

   #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。

如果没有属于偏移后的Index对应的入口,我们要返回一个错误码STATUS_EA_LIST_INCONSIS-
TENT。

   #define STATUS_EA_LIST_INCONSISTENT 0x80000014
   
   
=====[ 5.2 NtEnumerateValueKey ]================================================

注册表的键值不是按字母表排序的。幸运的是一个键下的键值不是很多,所以我们可以通过
重新调用的方法得到偏移。获得键值的API是NtEnumerateValueKey.。

   NTSTATUS NtEnumerateValueKey(
       IN HANDLE KeyHandle,
       IN ULONG Index,
       IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
       OUT PVOID KeyValueInformation,
       IN ULONG KeyValueInformationLength,
       OUT PULONG ResultLength
   );
   
KeyHandle还是主键的句柄。Index是一个给定键的键值列表中的索引。KeyValueInformation-
Class描述了要存入KeyValyeInformate缓冲的信息的类型,其长度由KeyValueInformation-
Length指定。写入的字节数返回到ResultLength中。

我们要再一次计算偏移,但是这次是根据一个键下的键值数量然后从0序号到Index重新调用
这个函数。当KeyValueInformationClass被设置成KeyValueBasicInformation时可以得到键
值的名字。

   #define KeyValueBasicInformation 0
   
然后我们在KeyValueInformation缓冲中得到如下结构:

   typedef struct _KEY_VALUE_BASIC_INFORMATION {
       ULONG TitleIndex;
       ULONG Type;
       ULONG NameLength;
       WCHAR Name[1];
   } KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;
   
再一次,我们只关心Name和Namelength字段。

如果没有属于偏移后的Index对应的入口,我们要返回一个错误码STATUS_NO_MORE_ENTRIES。

   #define STATUS_NO_MORE_ENTRIES 0x8000001A
   
   
=====[ 6. 系统服务和驱动 ]======================================================

系统服务和驱动可以通过四个独立的API枚举。他们的联系在每个不同版本的Windows系统中
都不同。所以我们必须挂钩这四个函数。

   BOOL EnumServicesStatusA(
       SC_HANDLE hSCManager,
       DWORD dwServiceType,
       DWORD dwServiceState,
       LPENUM_SERVICE_STATUS lpServices,
       DWORD cbBufSize,
       LPDWORD pcbBytesNeeded,
       LPDWORD lpServicesReturned,
       LPDWORD lpResumeHandle
   );

   BOOL EnumServiceGroupW(
       SC_HANDLE hSCManager,
       DWORD dwServiceType,
       DWORD dwServiceState,
       LPBYTE lpServices,
       DWORD cbBufSize,
       LPDWORD pcbBytesNeeded,
       LPDWORD lpServicesReturned,
       LPDWORD lpResumeHandle,
       DWORD dwUnknown
   );

   BOOL EnumServicesStatusExA(
       SC_HANDLE hSCManager,
       SC_ENUM_TYPE InfoLevel,
       DWORD dwServiceType,
       DWORD dwServiceState,
       LPBYTE lpServices,
       DWORD cbBufSize,
       LPDWORD pcbBytesNeeded,
       LPDWORD lpServicesReturned,
       LPDWORD lpResumeHandle,
       LPCTSTR pszGroupName
   );

   BOOL EnumServicesStatusExW(
       SC_HANDLE hSCManager,
       SC_ENUM_TYPE InfoLevel,
       DWORD dwServiceType,
       DWORD dwServiceState,
       LPBYTE lpServices,
       DWORD cbBufSize,
       LPDWORD pcbBytesNeeded,
       LPDWORD lpServicesReturned,
       LPDWORD lpResumeHandle,
       LPCTSTR pszGroupName
   );
   
这里最重要的是lpService,它指向将存放服务列表的缓冲。同时,lpServicesReturned指向
记录个数,也很重要。输出到缓冲的数据结构要依赖于不同的功能。对于EnumServicesStatusA
和EnumServicesGroupW将返回如下结构:

   typedef struct _ENUM_SERVICE_STATUS {
       LPTSTR lpServiceName;
       LPTSTR lpDisplayName;
       SERVICE_STATUS ServiceStatus;
   } ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS;

   typedef struct _SERVICE_STATUS {
       DWORD dwServiceType;
       DWORD dwCurrentState;
       DWORD dwControlsAccepted;
       DWORD dwWin32ExitCode;
       DWORD dwServiceSpecificExitCode;
       DWORD dwCheckPoint;
       DWORD dwWaitHint;
   } SERVICE_STATUS, *LPSERVICE_STATUS;
   
对于EnumServicesStatusExA和EnumServicesStatusExW是:

   typedef struct _ENUM_SERVICE_STATUS_PROCESS {
       LPTSTR lpServiceName;
       LPTSTR lpDisplayName;
       SERVICE_STATUS_PROCESS ServiceStatusProcess;
   } ENUM_SERVICE_STATUS_PROCESS, *LPENUM_SERVICE_STATUS_PROCESS;

   typedef struct _SERVICE_STATUS_PROCESS {
       DWORD dwServiceType;
       DWORD dwCurrentState;
       DWORD dwControlsAccepted;
       DWORD dwWin32ExitCode;
       DWORD dwServiceSpecificExitCode;
       DWORD dwCheckPoint;
       DWORD dwWaitHint;
       DWORD dwProcessId;
       DWORD dwServiceFlags;
   } SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS;
   
我们指关心lpServiceName,这个是系统服务的名字。记录有一个静态的大小,所以如果我们
想要隐藏一个记录,我们就要根据记录的大小移动后面所有的记录。这里我们必须要区分清
SERVICE_STATUS和SERVICE_STATUS_PROCESS的大小。


=====[ 7. 挂钩和展开 ]==========================================================

为了达到我们想要的效果,我们必须挂钩所有运行进程和所有将产生的进程。新进程必须在
它运行它自己的第一条指令前被挂钩,否则在它被挂钩前它就能够看到我们隐藏的对象。


=====[ 7.1 权限 ]================================================================

首先,应该先知道我们至少需要管理员权限来得到所有运行进程的访问权。最好的办法就是
以系统服务的方式、以SYSTEM的身份来运行我们的进程。要安装服务,我们同样需要特殊的
权限。

同时,得到SeDebugPrivilege是很有用的。这个可以通过OpenProcessToken,LookupPrivilege-
Value和AdjustTokenPrivileges这些API来达到。

   BOOL OpenProcessToken(
       HANDLE ProcessHandle,
       DWORD DesiredAccess,
       PHANDLE TokenHandle
   );

   BOOL LookupPrivilegeValue(
       LPCTSTR lpSystemName,
       LPCTSTR lpName,
       PLUID lpLuid
   );

   BOOL AdjustTokenPrivileges(
       HANDLE TokenHandle,
       BOOL DisableAllPrivileges,
       PTOKEN_PRIVILEGES NewState,
       DWORD BufferLength,
       PTOKEN_PRIVILEGES PreviousState,
       PDWORD ReturnLength
   );
   
忽略错误,这个代码可以写成下面这样:

   #define SE_PRIVILEGE_ENABLED        0x0002
   #define TOKEN_QUERY                 0x0008
   #define TOKEN_ADJUST_PRIVILEGES     0x0020

   HANDLE hToken;
   LUID DebugNameValue;
   TOKEN_PRIVILEGES Privileges;
   DWORD dwRet;

   OpenProcessToken(GetCurrentProcess(),
                    TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,hToken);
   LookupPrivilegeValue(NULL,"SeDebugPrivilege",&DebugNameValue);
   Privileges.PrivilegeCount=1;
   Privileges.Privileges[0].Luid=DebugNameValue;
   Privileges.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
   AdjustTokenPrivileges(hToken,FALSE,&Privileges,sizeof(Privileges),
                         NULL,&dwRet);
   CloseHandle(hToken);
   

=====[ 7.2 全局钩子 ]============================================================

枚举进程的问题在前面讲NtQuerySystemInformation这个API的时候已经解决了。系统中有一
些本地进程,所以我们可以通过改写函数的第一条指令的方法来挂钩他们。对于每一个运行
程我们都必须要这么做。我们要在目标进程中分配一块内存,在这里写入我们要挂钩的函数
的新的代码。然后我们用jmp指令来改写这些函数的第一个指令。这个跳转把执行重定位到
我们的代码。所以当被挂钩的函数被调用的时候这个jmp指令会被立即执行。我们必须要保
存每个函数的被改写的第一条指令。我们需要它们去调用原始的被挂钩的函数。保存指令在
我的“挂钩Windows API”一文的第3.2.3节中有描述。

首先我们要通过NtOpenProcess打开目标进程并且得到句柄。如果我们没有足够的权限这会
失败。

   NTSTATUS NtOpenProcess(
       OUT PHANDLE ProcessHandle,
       IN ACCESS_MASK DesiredAccess,
       IN POBJECT_ATTRIBUTES ObjectAttributes,
       IN PCLIENT_ID ClientId OPTIONAL
   );

ProcessHandle是一个指向返回句柄的指针。DesiredAccess应该被设置成PROCESS_ALL_
ACCESS。我们可以把目标进程的PID设置成ClientId结构的UniqueProcess值,Unique-
Thread应该为0。打开的句柄总是可以通过NtClose函数关闭。

   #define PROCESS_ALL_ACCESS 0x001F0FFF
   
现在我们要为我们的代码分配内存。这个可以通过NtAllocateVirtualMemory实现。

   NTSTATUS NtAllocateVirtualMemory(
       IN HANDLE ProcessHandle,
       IN OUT PVOID BaseAddress,
       IN ULONG ZeroBits,
       IN OUT PULONG AllocationSize,
       IN ULONG AllocationType,
       IN ULONG Protect
   );
   
ProcessHandle就是NtOpenProcess返回的句柄。BaseAddress是一个指向内存开始处的指针。
这里存放着分配的内存的地址。输入值可以为NULL。AllocationSize是一个指向我们想要申
请的内存大小的指针。同时,它还用来返回分配内存的实际大小。最好把AllocationType设
置成MEM_TOP_DOWN和MEM_COMMIT,因为这样会分配到尽可能靠近动态链接库的高地址。

   #define MEM_COMMIT      0x00001000
   #define MEM_TOP_DOWN    0x00100000
   
然后我们可以用NtWriteVirtualMemory把我们的代码写进去。

   NTSTATUS NtWriteVirtualMemory(
       IN HANDLE ProcessHandle,
       IN PVOID BaseAddress,
       IN PVOID Buffer,
       IN ULONG BufferLength,
       OUT PULONG ReturnLength OPTIONAL
   );

BaseAddress就是NtAllocateVirtualMemory返回的地址。Buffer指向了函数写入的字节数,
BufferLength是我们要写入的字节数。

现在我们要挂钩单一函数。只有ntdll.dll是每个进程都要加载的。所以我们我们检查我们
要挂钩的ntdll.dll中的函数是不是被进程引入了。但是这个函数(在其他DLL中)在内存中
放置的位置是可分配的,所以在这个地址上改写很容易在目标进程中引发错误。这就是为什
么我们要检查这个库(存放我们要挂钩的函数的地方)是否被目标进程加载了。

我们要通过NtQueryInformationProcess得到目标进程的PEB(进程环境块)。

   NTSTATUS NtQueryInformationProcess(
       IN HANDLE ProcessHandle,
       IN PROCESSINFOCLASS ProcessInformationClass,
       OUT PVOID ProcessInformation,
       IN ULONG ProcessInformationLength,
       OUT PULONG ReturnLength OPTIONAL
   );

我们要把ProcessInformationClass设置成ProcessBasicInformation。然后PROCESS_BASIC_
INFORMATION结构返回到ProcessInformation缓冲,其大小有ProcessInformationLength指定。

   #define ProcessBasicInformation 0

   typedef struct _PROCESS_BASIC_INFORMATION {
       NTSTATUS ExitStatus;
       PPEB PebBaseAddress;
       KAFFINITY AffinityMask;
       KPRIORITY BasePriority;
       ULONG UniqueProcessId;
       ULONG InheritedFromUniqueProcessId;
   } PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
   
PebBaseAddress是我们想要的。在PebBaseAddress + 0x0c的地方是PPEB_LDR_DATA的地址。
这个可以通过NtReadVirtualMemory调用得到。

   NTSTATUS NtReadVirtualMemory(
       IN HANDLE ProcessHandle,
       IN PVOID BaseAddress,
       OUT PVOID Buffer,
       IN ULONG BufferLength,
       OUT PULONG ReturnLength OPTIONAL
   );

参数和NtWriteVirtualMemory函数相似。

在PPEB_LDR_DATA + 0x1c的地方是InInitializationOrderModuleList的地址。这是进程加载
的链接库的列表。我们只关心这个结构的一部分。

   typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST {
       PVOID Next,
       PVOID Prev,
       DWORD ImageBase,
       DWORD ImageEntry,
       DWORD ImageSize,
       ...
   );
   
Next是一个指向下一个记录的指针,Prev指向前一个记录,最后一个记录指向第一个。
ImageBase是模块在内存中的地址,ImageEntry是模块的入口,ImageSize是其大小。

对于所有我们有挂钩的库我们都要得到它的ImageBase(比如用GetModuleHandle或者Load-
Library)。我们用这个ImageBase和InInitializationOrderModuleList中的每个入口进行
比较。

现在我们已经为挂钩做好准备了。因为我们要挂钩运行中的进程,有一个可能是我们的代码
可能在被改写的同时被执行。这会发生错误,所以首先我们要停止目标进程中的所有线程。
可以通过在第四节中描述的NtQuerySystemInformation的SystemProcessAndThreadsInformation
类型得到线程列表。但是我们要描述一下用来存放线程信息的SYSTEM_THREADS结构。

   typedef struct _SYSTEM_THREADS {
       LARGE_INTEGER KernelTime;
       LARGE_INTEGER UserTime;
       LARGE_INTEGER CreateTime;
       ULONG WaitTime;
       PVOID StartAddress;
       CLIENT_ID ClientId;
       KPRIORITY Priority;
       KPRIORITY BasePriority;
       ULONG ContextSwitchCount;
       THREAD_STATE State;
       KWAIT_REASON WaitReason;
   } SYSTEM_THREADS, *PSYSTEM_THREADS;
   
对每个线程我们要通过NtOpenThread得到其句柄。我们要对它使用ClientId。

   NTSTATUS NtOpenThread(
       OUT PHANDLE ThreadHandle,
       IN ACCESS_MASK DesiredAccess,
       IN POBJECT_ATTRIBUTES ObjectAttributes,
       IN PCLIENT_ID ClientId
   )
   
我们要获得的句柄存储在ThreadHandle中。我们要把DesiredAccess设置成THREAD_SUSPEND_
RESUME。

   #define THREAD_SUSPEND_RESUME 2
   
ThreadHandle用来调用NtSuspendThread。

   NTSTATUS NtSuspendThread(
       IN HANDLE ThreadHandle,
       OUT PULONG PreviousSuspendCount OPTIONAL
   );

挂起的线程就可以准备改写了。我们像“挂钩Windows API”中第3.2.2节中讲述的那样进
行。唯一的不同是这次我们是对其他进程使用。

挂钩完毕后我们通过NtResumeThread唤醒进程的所有线程。

   NTSTATUS NtResumeThread(
       IN HANDLE ThreadHandle,
       OUT PULONG PreviousSuspendCount OPTIONAL
   );
   
   
=====[ 7.3 新进程 ]==============================================================

对所有运行线程的感染不会影响之后运行的进程。我们可以获得进程列表然后过一会儿再获
得一次然后比较他们并感染那些出现在第二个列表而没有出现在第一个列表中的进程。但是
这种方法很不可靠。

更好一点的办法是挂钩那些当新进程开始时总会被调用的函数。因为我们挂钩了系统中的所
有进程,所以用这种方法我们不会漏掉任何一个新进程。我们可以挂钩NtCreateThread但这
不是最早的办法。我们可以挂钩NtResumeThread,这个函数在每当有一个新进程被创建的时
候也会被调用。它在NtCreateThread之后被调用。

NtResumeThread唯一的问题是不仅仅是创建新进程时会被调用。但是我们可以很容易地克服
它。NtQueryInformationThread会给我们一个关于哪个进程所有一个指定线程的信息。我们
要做的最后一件事是检查这个进程是否已经被挂钩了。这个可以通过读取我们要挂钩的任意
函数来完成。

   NTSTATUS NtQueryInformationThread(
       IN HANDLE ThreadHandle,
       IN THREADINFOCLASS ThreadInformationClass,
       OUT PVOID ThreadInformation,
       IN ULONG ThreadInformationLength,
       OUT PULONG ReturnLength OPTIONAL
   );
   
ThreadInformationClass是信息类,我们要把它设置为ThreadBasicInformation。Thread-
Information是返回结果的缓冲,其大小为ThreadInformationLength。

   #define ThreadBasicInformation 0

对于ThreadBasicInformation,返回如下结构:

   typedef struct _THREAD_BASIC_INFORMATION {
       NTSTATUS ExitStatus;
       PNT_TIB TebBaseAddress;
       CLIENT_ID ClientId;
       KAFFINITY AffinityMask;
       KPRIORITY Priority;
       KPRIORITY BasePriority;
   } THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;
   
ClientId就是拥有这个线程的PID。现在我们要感染一个新进程。问题是这个新进程在内存
中只有ntdll.dll。其他模块是在调用完NtResumeThread后立即加载的。可以有几种方法来
解决这个问题。比如,我们可以挂钩LdrInitializeThunk这个API,它在进程初始化时被调
用。

   NTSTATUS LdrInitializeThunk(
       DWORD Unknown1,
       DWORD Unknown2,
       DWORD Unknown3
   );
   
首先我们运行原始的代码,然后我们挂钩新进程中所有我们要挂钩的函数。但是最好解除对
LdrInitializeThunk的挂钩因为这个函数以后还有被调用很多次我们不希望重新挂钩所有的
函数。在被挂钩的程序执行第一条之前我们已经完成了所有的事。这就是为什么它没有机会
在被挂钩之前调用被挂钩的函数。

挂钩自身和挂钩执行进程是一样的。这里我们不考虑执行线程。


=====[ 7.4 DLL ]=================================================================

系统中每一个进程都有一个ntdll.dll的拷贝。这就意味着我们可以在进程初始化时挂钩这个
模块中的任何函数。但是其他的来自kernel32.dll或者advapi32.dll中的函数呢?而且有一
些进程只有ntdll.dll。其他的库都可以在进程被挂钩后在代码中动态地加载。这就是为什么
我们要挂钩LdrLoadDll,它用来加载新模块。

   NTSTATUS LdrLoadDll(
       PWSTR szcwPath,
       PDWORD pdwLdrErr,      
       PUNICODE_STRING pUniModuleName,
       PHINSTANCE pResultInstance
   );

这里对我们最重要的是pUniModuleName,这里存放了模块的名字。如果调用成功pResultIn-
stance被填充为它的内存地址。

我们可以调用原始的LdrLoadDll然后挂钩所有的被加载模块中的函数。


=====[ 8. 内存 ]================================================================

当我们挂钩一个函数时我们改变它的前几个字节。通过调用函数NtReadVirtualMemory,我们
可以检测一个函数是否被挂钩了。所以我们还要挂钩NtReadVirtualMemory以防止被检测出。

   NTSTATUS NtReadVirtualMemory(
       IN HANDLE ProcessHandle,
       IN PVOID BaseAddress,
       OUT PVOID Buffer,
       IN ULONG BufferLength,
       OUT PULONG ReturnLength OPTIONAL
   );
   
我们已经改变了我们要挂钩函数的前几个字节,并为我们的代码申请了内存。我们应该检查
调用者是否读了这些字节。如果在BaseAddress到BaseAddress+BufferLength的范围内有我们
的代码,我们就必须改变Buffer中的字节。

如果用户请求读取我们申请的内存中的数据我们要返回空和一个STATUS_PARTIAL_COPY错误
码。这个值说明不是所有请求的内存都被复制到了Buffer中。这个值同时用来描述请求未分
配空间。这种情况ReturnLength应设置为0。

   #define STATUS_PARTIAL_COPY 0x8000000D
   
如果用户请求读取被挂钩函数的前几个字节,我们要调用原始代码并且要把原始字节复制到
Buffer中(这几个字节我们为了调用原始函数而保存)。

现在进程已经无法通过读取自己的内存检测到它已经被挂钩了。而且如果调式被挂钩的进程
时也会遇到困难。它呈现在你面前的是原始代码但却执行我们的代码。

为了能更完美地隐藏,我们还可以挂钩NtQueryVirtualMemory。这个函数用来获得关于虚拟
内存的信息。我们可以挂钩它来防止我们申请的内存被检测到。

   NTSTATUS NtQueryVirtualMemory(
       IN HANDLE ProcessHandle,
       IN PVOID BaseAddress,
       IN MEMORY_INFORMATION_CLASS MemoryInformationClass,
       OUT PVOID MemoryInformation,
       IN ULONG MemoryInformationLength,
       OUT PULONG ReturnLength OPTIONAL
   );

MemoryInformationClass指定了返回的数据类。我们对前面的两个类型感兴趣。

   #define MemoryBasicInformation 0
   #define MemoryWorkingSetList 1
   
对于MemoryBasicInformation类返回如下结构:

   typedef struct _MEMORY_BASIC_INFORMATION {
       PVOID BaseAddress;
       PVOID AllocationBase;
       ULONG AllocationProtect;
       ULONG RegionSize;
       ULONG State;
       ULONG Protect;
       ULONG Type;
   } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
   
每一个内存单元区间都有自己的RegionSize和自己的类型Type。空闲内存的类型为MEM_FREE。

   #define MEM_FREE 0x10000
   
如果我们之前的区域为MEM_FREE,我们应该把我们的代码区间的大小加到它的RegionSize上。
如果我们后面的区域为MEM_FREE,我们应该再把这个区间的大小再加到前面的RegionSize上。

如果我们前面的区间为别的类型,我们对我们的区间返回MEM_FREE。它的大小同样要根据后
面的区间属性来确定。

对于MemoryWorkingSetList类,返回如下结构:

   typedef struct _MEMORY_WORKING_SET_LIST {
       ULONG NumberOfPages;
       ULONG WorkingSetList[1];
   } MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;
   
NumberOfPages是WorkingSetList中的项数。这个数应该被减小。我们要在WorkingSetList
中找到我们的区间然后用后面的记录覆盖我们的。WorkingSetList是一个DWORD数组,其中
高20位指定了区间的高20位地址,底12位指定为标志。


=====[ 9. 句柄 ]================================================================

通过SystemHandleInformation类调用NtQuerySystemInformation我们可以得到一个所有打开
的句柄,它存放在_SYSTEM_HANDLE_INFORMATION_EX结构中。

   #define SystemHandleInformation 0x10

   typedef struct _SYSTEM_HANDLE_INFORMATION {
       ULONG ProcessId;
       UCHAR ObjectTypeNumber;
       UCHAR Flags;
       USHORT Handle;
       PVOID Object;
       ACCESS_MASK GrantedAccess;
   } SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;

   typedef struct _SYSTEM_HANDLE_INFORMATION_EX {
       ULONG NumberOfHandles;
       SYSTEM_HANDLE_INFORMATION Information[1];
   } SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;
   
ProcessId指定了拥有该句柄的进程。ObjectTypeNumber是句柄类型。NumberOfHandles存放
的是Information数组中的记录个数。隐藏一个句柄很简单。我们不得不把后面的所有记录
提前并减小NumberOfHandles的值。必须要把后面的所有项提前,因为这个数组是一个由
ProcessId组成的组。这就意味着一个进程的所有句柄都是在一起的。并且对于一个进程,
其句柄的个数是在不断增长的。

现在回想一下这个函数由SystemProcessesAndThreadsInformation类返回的_SYSTEM_PROCESS
结构。这里我们可以看到每一个进程都有一个关于其句柄个数的值存放在HandleCount中。
如果我们要完美地隐藏,我们还要在以SystemProcessesAndThreadsInformation类调用这个
函数时根据我们隐藏的句柄个数改写HandleCount。但是这个改变对时间的要求是很高的。
系统正常运行时,在很短的时间内会有很多的句柄打开和关闭。所以经常发生这样的情况,
在这个函数的调用中间句柄的个数改变了,但我们不需要改变HandleCount的值。


=====[ 9.1 命名句柄并获得类型 ]=================================================

隐藏句柄很简单,但找到想要隐藏的句柄要难一些。比如如果我们隐藏了一个进程我们就要
隐藏它的所有句柄以及所有指向它的句柄。隐藏这个进程的句柄也很简单。我们只需要比较
句柄的ProcessId和我们的进程的PID,当它们相等的时候我们就隐藏它。但是对于其他进程
的句柄,它首先必须是命名的然后我们才可以进行比较。系统中的句柄数通常很多,所以我
们最好在比较命名前先比较句柄的类型。命名类型可以为我们省去很多搜索我们不关心的句
柄的时间。

命名句柄和命名类型可以通过调用NtQueryObject得到。

   NTSTATUS ZwQueryObject(
       IN HANDLE ObjectHandle,
       IN OBJECT_INFORMATION_CLASS ObjectInformationClass,
       OUT PVOID ObjectInformation,
       IN ULONG ObjectInformationLength,
       OUT PULONG ReturnLength OPTIONAL
   );
   
ObjectHandle是一个我们要获得信息的句柄,ObjectInformationClass是将被存储在Object-
Information缓冲中的信息的类型,其长度为ObjectInformationLength字节长。

   #define ObjectNameInformation 1
   #define ObjectAllTypesInformation 3

   typedef struct _OBJECT_NAME_INFORMATION {
       UNICODE_STRING Name;
   } OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;
   
Name字段指明了句柄的名称。

   typedef struct _OBJECT_TYPE_INFORMATION {
       UNICODE_STRING Name;
       ULONG ObjectCount;
       ULONG HandleCount;
       ULONG Reserved1[4];
       ULONG PeakObjectCount;
       ULONG PeakHandleCount;
       ULONG Reserved2[4];
       ULONG InvalidAttributes;
       GENERIC_MAPPING GenericMapping;
       ULONG ValidAccess;
       UCHAR Unknown;
       BOOLEAN MaintainHandleDatabase;
       POOL_TYPE PoolType;
       ULONG PagedPoolUsage;
       ULONG NonPagedPoolUsage;
   } OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;

   typedef struct _OBJECT_ALL_TYPES_INFORMATION {
       ULONG NumberOfTypes;
       OBJECT_TYPE_INFORMATION TypeInformation;
   } OBJECT_ALL_TYPES_INFORMATION, *POBJECT_ALL_TYPES_INFORMATION;

Name字段指明了紧后面的每个OBJECT_TYPE_INFORMATION结构的对象类型名。下一个OBJECT_
TYPE_INFORMATION结构也是这个名字,在开始的四字节边界处。

SYSTEM_HANDLE_INFORMATION结构中的ObjectTypeNumber是一个TypeInformation数组的索引。

难的是寻找别的进程的句柄。有两种可能的方法去命名它。第一是把这个句柄通过NtDupli-
cateObject复制到我们的进程然后命名它。这个方法对一些特定类型的句柄不起作用。但是
这只是少数情况,所以我们可以不必紧张。

   NtDuplicateObject(
       IN HANDLE SourceProcessHandle,
       IN HANDLE SourceHandle,
       IN HANDLE TargetProcessHandle,
       OUT PHANDLE TargetHandle OPTIONAL,
       IN ACCESS_MASK DesiredAccess,
       IN ULONG Attributes,
       IN ULONG Options
   );
   
SourceProessHandle是一个拥有SourceHandle的进程的句柄,SourceHandle就是我们要复制
的句柄。TargetProcessHandle是要复制到的进程的句柄。在我们使用的情况下这就是我们的
进程句柄。TargetHandle是一个指向原始句柄副本的指针。DesiredAccess应该设置为PROCESS
_QUERY_INFORMATION,Attributes和Options应为0。

第二种命名方法是使用系统驱动,它可以对任何句柄其作用。这个的源代码在OpHandle项目
中涉及,可以在我的网站http://rootkit.host.sk找到。


=====[ 10. 端口 ]===============================================================

最简单的枚举打开句柄的方法是使用AllocateAndGetTcpTableFromStack和AllocateAndGet-
UdpTableFromStack调用,或者调用iphlpapi.dll中的AllocateAndGetTcpExTableFromStack
和AllocateAndGetUdpExTableFromStack。Ex函数从XP后才有效。

   typedef struct _MIB_TCPROW {
       DWORD dwState;
       DWORD dwLocalAddr;
       DWORD dwLocalPort;
       DWORD dwRemoteAddr;
       DWORD dwRemotePort;
   } MIB_TCPROW, *PMIB_TCPROW;

   typedef struct _MIB_TCPTABLE {
       DWORD dwNumEntries;
       MIB_TCPROW table[ANY_SIZE];
   } MIB_TCPTABLE, *PMIB_TCPTABLE;

   typedef struct _MIB_UDPROW {
       DWORD dwLocalAddr;
       DWORD dwLocalPort;
   } MIB_UDPROW, *PMIB_UDPROW;

   typedef struct _MIB_UDPTABLE {
       DWORD dwNumEntries;
       MIB_UDPROW table[ANY_SIZE];
   } MIB_UDPTABLE, *PMIB_UDPTABLE;

   typedef struct _MIB_TCPROW_EX
   {
       DWORD dwState;
       DWORD dwLocalAddr;
       DWORD dwLocalPort;
       DWORD dwRemoteAddr;
       DWORD dwRemotePort;
       DWORD dwProcessId;
   } MIB_TCPROW_EX, *PMIB_TCPROW_EX;

   typedef struct _MIB_TCPTABLE_EX
   {
       DWORD dwNumEntries;
       MIB_TCPROW_EX table[ANY_SIZE];
   } MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX;

   typedef struct _MIB_UDPROW_EX
   {
       DWORD dwLocalAddr;
       DWORD dwLocalPort;
       DWORD dwProcessId;
   } MIB_UDPROW_EX, *PMIB_UDPROW_EX;

   typedef struct _MIB_UDPTABLE_EX
   {
       DWORD dwNumEntries;
       MIB_UDPROW_EX table[ANY_SIZE];
   } MIB_UDPTABLE_EX, *PMIB_UDPTABLE_EX;

   DWORD WINAPI AllocateAndGetTcpTableFromStack(
       OUT PMIB_TCPTABLE *pTcpTable,
       IN BOOL bOrder,
       IN HANDLE hAllocHeap,
       IN DWORD dwAllocFlags,
       IN DWORD dwProtocolVersion;
   );

   DWORD WINAPI AllocateAndGetUdpTableFromStack(
       OUT PMIB_UDPTABLE *pUdpTable,
       IN BOOL bOrder,
       IN HANDLE hAllocHeap,
       IN DWORD dwAllocFlags,
       IN DWORD dwProtocolVersion;
   );

   DWORD WINAPI AllocateAndGetTcpExTableFromStack(
       OUT PMIB_TCPTABLE_EX *pTcpTableEx,
       IN BOOL bOrder,
       IN HANDLE hAllocHeap,
       IN DWORD dwAllocFlags,
       IN DWORD dwProtocolVersion;
   );

   DWORD WINAPI AllocateAndGetUdpExTableFromStack(
       OUT PMIB_UDPTABLE_EX *pUdpTableEx,
       IN BOOL bOrder,
       IN HANDLE hAllocHeap,
       IN DWORD dwAllocFlags,
       IN DWORD dwProtocolVersion;
   );
   
还有一个办法来做这件事。当一个程序创建了一个套接字并且开始监听,它一定会得到一个
句柄用来打开端口。我们可以枚举系统中的所有打开句柄并通过NtDeviceIoControlFile向
它们发送特殊的缓冲字段来检测这个句柄是否是用来打开端口的。这还可以给我们关于这个
端口的信息。因为有很多打开的句柄,我们只需要检测类型为File并且名字是/Device/Tcp
或者/Device/Udp的。打开的端口只有这个类型和名字。

当我们查看iphlpapi.dll中上面几个函数的代码时我们可以得知这些函数同样调用了函数
NtDeviceIoControlFile并且发送了一个特殊的缓冲字段以获得系统中所有打开端口的列
表。这就意味着唯一需要我们挂钩的函数只有NtDeviceIoControlFile。

   NTSTATUS NtDeviceIoControlFile(
       IN HANDLE FileHandle
       IN HANDLE Event OPTIONAL,
       IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
       IN PVOID ApcContext OPTIONAL,
       OUT PIO_STATUS_BLOCK IoStatusBlock,
       IN ULONG IoControlCode,
       IN PVOID InputBuffer OPTIONAL,
       IN ULONG InputBufferLength,
       OUT PVOID OutputBuffer OPTIONAL,
       IN ULONG OutputBufferLength
   );
   
我们感兴趣的参数是指定与之通信的设备句柄FileHandle,指向接收完成状态和请求操作信
息的IoStatusBlock,指定设备类型、方法、访问和一个函数的IoControlCode。InputBuffer
包含了InputBufferLength大小的输入数据,这个和OutputBuffer和OutputBufferLength类
似。


=====[ 10.1 WinXP的Netstart,OpPorts,WinXP的FPort ]============================

第一种得到所有打开端口列表的方法是通过Windows XP的OpPorts和FPort,同时还有Windows
XP的Netstat。

这里程序两次通过IoControlCode=0x000120003调用NtDeviceIoControlFile。OutputBuffer
在第二次调用后被填充。FileHandle的名字这里一直为/Device/Tcp。InputBuffer根据调用
类型的不同而不同。

1) 为获得MIB_TCPROW数组,InputBuffer应为:

第一次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

第二次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

2) 获得MIB_UDPROW数组:

第一次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

第二次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

3) 获得MIB_TCPROW_EX数组:

第一次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

第二次调用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

4) 获得MIB_UDPROW_EX数组:

第一次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

第二次调用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00

你可以看到,缓冲中的数据只有几个字节的区别。我们可以明确地概括如下:

调用要求InputBuffer[1]为0x04并且多数时候InputBuffer[17]为0x01。只有这样我们才可
以在OutputBuffer中得到我们希望的列表。如果我们要得到关于TCP端口的信息,我们要把
InputBuffer[0]设置为0x00,如果要得到关于UDP的信息则要设置为0x01。如果我们要得到
扩展输出列表(MIB_TCPROW_EX或者MIB_UDPROW_EX),我们在第二次调用时把InputBuffer
[16]设置为0x02。

如果我们搞明白这些参数我们就可以改变输出缓冲。想要得到输出缓冲中的行数我们可以简
单地用IoStatusBlock中的Information除以行的大小。隐藏一行很简单。只要用后面的行覆
盖它并删除最后一行即可。别忘了改变OutputBufferLength和IoStatusBlock。


=====[ 10.2 Win2k和NT4的OpPorts,Win2k的FPort ]=================================

我们通过IoControlCode=0x00210012调用NtDeviceIoControlFile来决定一个类型为File,名
字为/Device/Tcp或者/Device/Udp的句柄是否为一个打开的端口句柄。

所以首先我们要比较IoControlCode然后是类型和名字。如果还关心其他的,我们可以比较输
入缓冲的大小,它应该等于TDI_CONNECTION_IN结构的大小。这个长度为0x18。OutputBuffer
是TDI_CONNECTION_OUT。

   typedef struct _TDI_CONNETION_IN
   {
       ULONG UserDataLength,
       PVOID UserData,
       ULONG OptionsLength,
       PVOID Options,
       ULONG RemoteAddressLength,
       PVOID RemoteAddress
   } TDI_CONNETION_IN, *PTDI_CONNETION_IN;

   typedef struct _TDI_CONNETION_OUT
   {
       ULONG State,
       ULONG Event,
       ULONG TransmittedTsdus,
       ULONG ReceivedTsdus,
       ULONG TransmissionErrors,
       ULONG ReceiveErrors,
       LARGE_INTEGER Throughput
       LARGE_INTEGER Delay,
       ULONG SendBufferSize,
       ULONG ReceiveBufferSize,
       ULONG Unreliable,
       ULONG Unknown1[5],
       USHORT Unknown2
   } TDI_CONNETION_OUT, *PTDI_CONNETION_OUT;
   
确定一个句柄是否为打开端口的具体实现可以在OpPorts的代码中找到,它在http://rookit.
host.sk上。我们现在对隐藏某个指定端口感兴趣。我们比较了InputBufferLength和IoCon-
trolCode。还比较了RemoteAddressLength。这个值对于打开端口通常为3或4。最后我们要
做的是比较OutputBuffer中的ReceivedTsdus,它包含网络中的断口和我们想要隐藏的端口
列表。TCP和UDP可以通过句柄的名字来区分。通过删除OutputBuffer中一些值,改变Io-
StatusBlock并返回STATUS_INVALID_ADDRESS我们可以隐藏这个端口。


=====[ 11. 结束语 ]=============================================================

上面描述的技术的具体实现可以在Hacker Defender Rootkit的1.0.0版本中找到,它的主页
是http://rootkit.host.sk和http://www.rootkit.com。

将来可能我还会加入一些其他关于在Windows NT下的隐藏技术。这篇文档的新版本将包含上
述技术的改进和一些新的观点。

特别感谢Ratter,他告诉了我很多知识来帮助我完成这篇文档以及完成Hacker Defender项
目的编写。

如果有什么意见可以发邮件到holy_father@phreaker.net或者到http://rootkit.host.sk的
留言板留言。

====================================[ 完 ]======================================

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值