Windows I/O系统提供给应用程序的I/O操作的目标对象是文件对象(File Object)。文件对象代表了设备对象的已打开实例,也就是说,内核或应用程序每打开(open)一个设备对象,就将得到一个文件对象。文件对象也是一个内核对象,在用户模式下可以通过句柄来引用文件对象。文件对象表达的是文件的已打开实例,而并非文件本身,所以它并不承担设备对象的数据存储和状态变迁的能力。真正的文件数据位于设备对象而非文件对象中。
文件对象也是对象管理器中的对象,其类型为IoFileObjectType。下面先看一下Windows中对文件对象的定义:
typedefstruct_FILE_OBJECT {
CSHORTType;
CSHORTSize;
PDEVICE_OBJECTDeviceObject; // 指向文件所在的设备对象
PVPBVpb; // 指向文件对象所在卷的卷参数块(VPB)
PVOIDFsContext; // 指向驱动程序为该文件对象维护的状态信息
PVOIDFsContext2; // 指向驱动程序为该文件对象维护的额外状态信息
PSECTION_OBJECT_POINTERSSectionObjectPointer; // 文件对象的内存区对象指针
PVOIDPrivateCacheMap; // 文件对象的私有缓存表
NTSTATUSFinalStatus; // 文件对象I/O请求的最终状态
struct_FILE_OBJECT *RelatedFileObject; // 相关的文件对象
BOOLEANLockOperation; // 是否已在文件对象上执行了锁(Lock)操作
BOOLEANDeletePending; // 正在执行一个删除与文件对象关联的文件的操作
BOOLEANReadAccess; // 以读访问方式打开该文件
BOOLEANWriteAccess; // 以写访问方式打开该文件
BOOLEANDeleteAccess; // 以删除访问方式打开该文件
BOOLEANSharedRead; // 以读共享访问方式打开该文件
BOOLEANSharedWrite; // 以写共享访问方式打开该文件
BOOLEANSharedDelete; // 以删除共享访问方式打开该文件
ULONGFlags; // 标志,以FO_作为前缀定义的一组常量,可以组合
UNICODE_STRINGFileName; // 文件名,仅在IRP_MJ_CREATE请求中有效
LARGE_INTEGERCurrentByteOffset; // 文件中的当前偏移位置,以字节为单位
ULONGWaiters; // 有多少个线程在等待该文件对象,以进行同步访问
ULONGBusy; // 当前是否有线程在以同步方式访问该文件对象
PVOIDLastLock; // 指向上一个应用在该文件对象上的字节范围锁
KEVENTLock; // 文件对象锁,用于同步访问该文件对象
KEVENTEvent; // 文件对象锁,用于I/O请求的完成通知
PIO_COMPLETION_CONTEXTCompletionContext; // 指向与文件对象关联的完成端口信息
} FILE_OBJECT;
FILE_OBJECT包含了指向设备对象的指针、由驱动程序为文件对象维护的状态环境、当前位置信息、访问方式和文件对象标志,以及当多个线程访问一个文件对象时所需要的各种锁。
下图(引用自Microsoft.Press.Windows.Internals)演示了当一个文件被打开时发生的情形:
一个C程序调用运行时库函数fopen,该函数依次调用Windows的CreateFile函数。然后调用Ntdll.dll中的原生函数,在WINDBG工具可以看出,Ntdll.dll中的ZwCreateFile和NtCreateFile都有相同的函数主体,也就是说,它们是同一个函数。
0:001> u ntdll!ZwCreateFile
ntdll!ZwCreateFile:
7c92d090 b825000000 mov eax,25h
7c92d095 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d09a ff12 call dword ptr [edx]
7c92d09c c22c00 ret 2Ch
7c92d09f 90 nop
0:001> u ntdll!NtCreateFile
ntdll!ZwCreateFile:
7c92d090 b825000000 mov eax,25h
7c92d095 ba0003fe7f mov edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c92d09a ff12 call dword ptr [edx]
7c92d09c c22c00 ret 2Ch
7c92d09f 90 nop
其中,25h是系统服务号(也就是SSDT中的索引号),地址7ffe0300处存放的是函数的地址,使用命令dd 7ffe0300可以得知,函数的地址是7c92e4f0,
0:001> dd 7ffe0300
7ffe0300 7c92e4f0 7c92e4f4 00000000 00000000
使用命令u 7c92e4f0可以看出,这个函数就是函数KiFastSystemCall
0:001> u 7c92e4f0
ntdll!KiFastSystemCall:
7c92e4f0 8bd4 mov edx,esp
7c92e4f2 0f34 sysenter
至此,执行线程切换到内核模式中,并进入系统服务分发。系统服务分发根据系统服务号,定位SDT(Service Descriptor Table,服务描述符表)中的对应的系统服务项。其中25h(十进制37)对应的系统服务是ntkrnlpa模块中的函数NtCreateFile。
以上就是用户层利用ntdll!ZwCreateFile来实现对系统服务的调用。在ntkrnalpa模块中也有一个ZwCreateFile函数,下面是该函数的声明:
NTSTATUS
NTAPI
ZwCreateFile (
__outPHANDLEFileHandle, // 指向句柄变量的指针。如果这个函数调用返回成成功(STATUS_SUCCESS),句柄变量接收文件句柄
__inACCESS_MASKDesiredAccess, // 指定一个access_mask值确定所请求的访问对象。除了为所有类型的对象定义的访问权限外,调用者还可以指定下列任何访问权限,这些权限都是针对文件的
__inPOBJECT_ATTRIBUTESObjectAttributes, // 指向OBJECT_ATTRIBUTES结构的指针
__outPIO_STATUS_BLOCKIoStatusBlock, // IoStatusBlock也是一个结构。这个结构在内核开发中经常使用。它往往用于表示一个操作的结果
__in_optPLARGE_INTEGERAllocationSize, // 这个参数很少使用,请设置为NULL
__inULONGFileAttributes, // 这个参数控制新建立的文件的属性。一般的说,设置为FILE_ATTRIBUTE_NORMAL即可
__inULONGShareAccess, // 共享访问。一共有三种共享标记可以设置:FILE_SHARE_READ、FILE_SHARE_WRITE、FILE_SHARE_DELETE。
__inULONGCreateDisposition, // 指定当文件确实或不存在时所要执行的动作
__inULONGCreateOptions, // 指定当驱动程序创建或打开文件时应用的选项
__in_bcount_opt(EaLength) PVOIDEaBuffer, // 对于设备和中间驱动程序,此参数必须是空指针
__inULONGEaLength // 对于设备和中间驱动程序,此参数必须为零
);
在Windbg中看看这个函数的代码:
lkd> u nt!ZwCreateFile
nt!ZwCreateFile:
80501010 b825000000 mov eax,25h
80501015 8d542404 lea edx,[esp+4]
80501019 9c pushfd
8050101a 6a08 push 8
8050101c e830140400 call nt!KiSystemService (80542451)
80501021 c22c00 ret 2Ch
可以看出,它也是通过系统服务号来实现对系统服务的调用。在内核编程中,经常使用这个函数来完成对文件的操作。
综上所述,不管是ntdll!ZwCreateFile还是nt!ZwCreateFile都是通过调用系统服务的方式来实现的,也就是最后都会调用内核函数NtCreateFile。下面是函数NtCreateFile的实现:
NTSTATUS
NtCreateFile (
__outPHANDLEFileHandle,
__inACCESS_MASKDesiredAccess,
__inPOBJECT_ATTRIBUTESObjectAttributes,
__outPIO_STATUS_BLOCKIoStatusBlock,
__in_optPLARGE_INTEGERAllocationSize,
__inULONGFileAttributes,
__inULONGShareAccess,
__inULONGCreateDisposition,
__inULONGCreateOptions,
__in_bcount_opt(EaLength) PVOIDEaBuffer,
__inULONGEaLength
)
{
PAGED_CODE();
returnIoCreateFile( FileHandle,
DesiredAccess,
ObjectAttributes,
IoStatusBlock,
AllocationSize,
FileAttributes,
ShareAccess,
CreateDisposition,
CreateOptions,
EaBuffer,
EaLength,
CreateFileTypeNone,
(PVOID)NULL,
0 );
}
函数NtCreateFile的调用参数与函数ZwCreateFile完全一样。基本上什么都没干,然后又原封不动地调用了函数IoCreateFile,下面是函数IoCreateFile的定义:
NTSTATUS
IoCreateFile(
OUTPHANDLEFileHandle,
INACCESS_MASKDesiredAccess,
INPOBJECT_ATTRIBUTESObjectAttributes,
OUTPIO_STATUS_BLOCKIoStatusBlock,
INPLARGE_INTEGERAllocationSizeOPTIONAL,
INULONGFileAttributes,
INULONGShareAccess,
INULONGDisposition,
INULONGCreateOptions,
INPVOIDEaBufferOPTIONAL,
INULONGEaLength,
INCREATE_FILE_TYPECreateFileType,
INPVOIDExtraCreateParametersOPTIONAL,
INULONGOptions
)
注:除了NtCreateFile,NtCreateNamedPipeFile和NtCreateMailslotFile也是调用IoCreateFile函数来创建命名管道和邮件槽对象。
IoCreateFile函数又进一步调用IopCreateFile函数,下面是函数IopCreateFile的定义:
NTSTATUS
IopCreateFile(
OUTPHANDLEFileHandle,
INACCESS_MASKDesiredAccess,
INPOBJECT_ATTRIBUTESObjectAttributes,
OUTPIO_STATUS_BLOCKIoStatusBlock,
INPLARGE_INTEGERAllocationSizeOPTIONAL,
INULONGFileAttributes,
INULONGShareAccess,
INULONGDisposition,
INULONGCreateOptions,
INPVOIDEaBufferOPTIONAL,
INULONGEaLength,
INCREATE_FILE_TYPECreateFileType,
INPVOIDExtraCreateParametersOPTIONAL,
INULONGOptions,
INULONGInternalFlags,
INPVOIDDeviceObject
)
IopCreateFile函数的执行主要有以下几个部分
1、获取之前的操作模式
requestorMode = KeGetPreviousMode();
if (Options & IO_NO_PARAMETER_CHECKING) {
requestorMode = KernelMode;
}
2、为OpenPacket分配内存。
openPacket = IopAllocateOpenPacket();
if (openPacket == NULL) {
returnSTATUS_INSUFFICIENT_RESOURCES;
}
3、针对不同的模式进行不同的参数和标志的检查。
4、填充OpenPacket (OP)
openPacket->Type = IO_TYPE_OPEN_PACKET;
openPacket->Size = sizeof( OPEN_PACKET );
openPacket->ParseCheck = 0L;
openPacket->AllocationSize = initialAllocationSize;
openPacket->CreateOptions = CreateOptions;
openPacket->FileAttributes = (USHORT) FileAttributes;
openPacket->ShareAccess = (USHORT) ShareAccess;
openPacket->Disposition = Disposition;
openPacket->Override = FALSE;
openPacket->QueryOnly = FALSE;
openPacket->DeleteOnly = FALSE;
openPacket->Options = Options;
openPacket->RelatedFileObject = (PFILE_OBJECT) NULL;
openPacket->CreateFileType = CreateFileType;
openPacket->ExtraCreateParameters = ExtraCreateParameters;
openPacket->TraversedMountPoint = FALSE;
openPacket->InternalFlags = InternalFlags;
openPacket->TopDeviceObjectHint = DeviceObject;
openPacket->FinalStatus = STATUS_SUCCESS;
openPacket->FileObject = (PFILE_OBJECT) NULL;
5、调用ObOpenObjectByName函数来创建文件对象,它是对象管理器的函数,下面是该函数的声明:
NTSTATUS
ObOpenObjectByName (
__inPOBJECT_ATTRIBUTESObjectAttributes,
__in_optPOBJECT_TYPEObjectType,
__inKPROCESSOR_MODEAccessMode,
__inout_optPACCESS_STATEAccessState,
__in_optACCESS_MASKDesiredAccess,
__inout_optPVOIDParseContext,
__outPHANDLEHandle
)
ObOpenObjectByName函数执行的结果是一个指向所创建对象的句柄,只要ObOpenObjectByName返回成功,IopCreateFile函数即成功返回。
ObOpenObjectByName是对象管理器的函数,它实际又是利用对象管理器的另一个函数ObpLookupObjectName来打开一个对象,ObpLookupObjectName从指定的根目录或者系统全局根目录开始,调用ObpLookupDirectoryEntry函数,一层层地进入子目录,直至解析完成,或者碰到实现了Parse方法的对象,从而把余下的路径名称交给该对象进一步解析。