APIHOOK之在NT系列操作系统里让自己“消失”中3

在NT系列操作系统里让自己“消失”上
2007-03-31 09:02

===================[ 在NT系列操作系统里让自己“消失”]==================
                                                SoBeIt

             作者:Holy_Father <holy_father@phreaker.net>
             版本:1.2 english
             日期:05.08.2003


=====[ 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是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是所给键中键值的索引。KeyValueInformationClass描述信息的类型,保存在KeyValueInformation缓冲区中,缓冲区以字节为大小为KeyValueInformationLength。写入字节的数量返回在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感兴趣。

    
     如果这里没有被转移的索引记载我们就返回错误STATUS_NO_MORE_ENTRIES。

     #define STATUS_NO_MORE_ENTRIES 0x8000001A



=====[ 6. 系统服务和驱动 ]====================================

     系统服务和驱动是通过4个独立的API函数枚举的。它们在每个Windows版本中的联系都不一样。所以我们必须挂钩所有4个函数。

     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 权限 ]=============================================

     首先我们得知道我们至少获得管理员administrator权限来获得进入所有正在运行的进程。最好的可能是将我们的进程当做系统服务来运行,因为它运行与SYSTEM用户权限下。为安装服务我们首先得获取特殊的权限。
     获取SeDebugPrivilege的权限是很有用的,通过调用OpenProcessToken、LookupPrivilegeValue
和AdjustTokenPrivileges来完成。

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


 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值