`
bailingeye
  • 浏览: 21389 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论
阅读更多

转帖From:郁金香的csdn博客http://blog.csdn.net/kkksi13996362600/archive/2009/03/05/3959312.aspx

 

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


=====[ 7.2 全局挂钩 ]=======================================

枚举进程通过前面提到的API函数NtQuerySystemInformation来完成。因为系统中还有一些内部native进程,所以使用重 写函数第一个指令的方法来挂钩。对每个正在运行的进程我们需要做的都一样。首先在目标进程里分配一部分内存用来写入我们用来挂钩函数的新代码,然后把每个 函数开始的5个字节改为跳转指令(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。我们要 在ClientId结构里设置UniqueProcess为目标进程的PID,UniqueThread应该为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_COMMIT之外再加上MEM_TOP_DOWN因为内存要在接近DLL地址的尽可能高的地址分配。

#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+0C处是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+01C处是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或者LoadLibrary)。然后把这个ImageBase和InInitializationOrderModuleList的ImageBase比较。
现 在我们已经为挂钩准备就绪。因为我们是挂钩正在运行的进程,所以可能我们正在改写代码的同时代码被执行,这时就会导致错误。所以首先我们就得停止目标进程 里的所有线程。它的所有线程列表可以通过设置了SystemProcessAndThreadInformation的 NtQuerySystemInformation来获得。有关这个函数的描述参考第4节。但是还得加入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之后被调用。
唯一的问题在于,这个函数并不只在新进程被创建时调用。但我们能很容易解决这点。NtQueryInformationThread能给我们指定线程是属于哪个进程的信息。最后我们要做的就是检查进程是否已经被挂钩了。这通过读取我们要挂钩的函数的开始5个字节来完成。

NTSTATUS NtQueryInformationThread(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
OUT PVOID ThreadInformation,
IN ULONG ThreadInformationLength,
OUT PULONG ReturnLength OPTIONAL
);

ThreadInformationClass是信息分类,在这里它被设置为ThreadBasicInformation。ThreadInformation是保存结果的缓冲区,大小按字节计算为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,它保存模块名字。当调用成功后pResultInstance保存模块地址。
我们首先调用原始的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中我们就需要在缓冲区中改变它的一些字节。
如果有人在我们分配的内存中查询字节我们就返回空的缓冲区和错误STATUS_PARTIAL_COPY。这个值用来表示被请求的字节并没有完全被拷贝到缓冲区中,它也同样被用在当请求了未分配的内存时。这时ReturnLength应该被设为0。

#define STATUS_PARTIAL_COPY 0x8000000D

如果有人查询被挂钩的函数开始的字节我们就调用原始代码并拷贝原始代码里开始的那些字节到缓冲区中。
现在新进程已无法通过读取它的内存来检测是否被挂钩了。同样如果你调试被挂钩的进程调试器也会用问题,它会显示原始代码,但却执行我们的代码。

为了使隐藏更完美,我们还要挂钩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标明了返回数据的类别。我们对开始的2种类型感兴趣。

#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位标明了区段地址,低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分组的。这意味着一个来自同一个进程中的所有句柄都在一块儿。对于一个进程变量Handle的数量是不断增加的。
现在回想 一下这个函数(NtQuerySystemInformation)使用SystemProcessAndThreadsInformation类来调用 时返回的结构_SYSTEM_PROCESSES。这里我们能够看到每个进程都有它自己的句柄的数量在HandleCount中。如果我们想要做得更完美 我们就应该修改HandleCount,因为用SystemProcessesAndThreadsInformation类调用这个函数时隐藏了不少句 柄。但校正是非常浪费时间的。在系统正常运行的一小段时间里就会有很多句柄正在打开或关上。所以在对这个函数两次紧挨着的调用句柄的数量被更改是很正常 的,所以我们根本不需要改变HandleCount。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics