技术版:文件被占用无法删除怎么办?
共 13900字,需浏览 28分钟
·
2021-01-14 20:37
技术版:文件被占用无法删除怎么办?了解一点操作系统知识的同学们应该都知道,文件占用无法删除,是因为某些进程正在使用该文件。
要删除这样的文件,就需要让那些进程关闭文件,然后自然可以删除。
一句话的事,那究竟要怎么用代码来实现这个功能呢?
打开和关闭文件
还记得上大学第一门语言课-C语言,迄今为止还依然活跃并被一直使用的语言。
比汇编容易理解,又更接近底层,所以Windows操作系统内核大部分代码都是用C语言来编写的。
在C的课程里,我们学过通过FILE来操作使用文件,比如:
FILE *fp;
fp = fopen("c:\\temp\\test.txt", "r")
通过读的方式打开一个文件,使用非常简单,后续通过fp这个结构体指针操作文件即可。
其实fopen并不接近操作系统,他是对win32 API CreateFile的封装。
也就是前者是标准库接口,在Windows、linux、unix等都是通用接口。
而后者才是和操作系统关联紧密,由微软自己提供的API。
要更好的理解进程如何使用文件的,我们还得看看CreateFile这个API接口。
HANDLE CreateFileA(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
这是msdn对CreateFile的定义,简单来看我们可以只关注lpFileName和返回值,lpFileName传递你要打开的文件,返回值是操作系统给你的一个代表文件的句柄(handle)。
HANDLE hFile = CreateFileA("c:\\temp\\test.txt", ...);
要对文件进行读、写等操作都需要这个句柄,也就是说这个句柄至关重要,它表示文件正在被使用。
然后什么时候结束使用呢,我们需要看另一个API CloseHandle.
BOOL CloseHandle(
HANDLE hObject
);
CloseHandle用于关闭一个正在被使用的文件,通过句柄来关闭。
现在明白过来了吗,只要我们让进程调用CloseHandle这个API,关闭被占用的文件句柄,那么该文件也就被解除占用了。
哈哈,是不是很简单。
枚举占用文件的进程
那么我就想问同学们一个问题,怎么知道哪些进程在使用我们想删除的文件呢?怎么去查找?
带着这个问题,我们继续往下看。
我们来想一个问题,操作系统给调用CreateFile的用户返回了一个句柄,然后通过句柄来操作文件,那操作系统是如何知道句柄代表哪个文件呢?
我们简单思考一下,我们要达到这个目的有没有什么方法,比如我用一个数组来存用户打开的文件路径,而数组序号就返回给用户,下次用户就只需要把序号给我,我就知道要操作什么问题了。
演示代码,忽略细节
LPWSTR FileTable[100] = {0};
HANDLE CreateFileA(
LPCSTR lpFileName,
...)
{
for(int i = 0; i < 100; i ++) {
if(FileTable[i] == NULL) { //还有空位
FileTable[i] = lpFileName; //保存路径
return (HANDLE)i; //返回句柄
}
}
return NULL;
}
BOOL CloseHandle(
HANDLE hObject
) {
if((int)hObject < 100) {
if(FileTable[hObject]) {
FileTable[hObject] = NULL;//找到文件路径
return TRUE;
}
}
return FALSE;
}
上面简单的代码演示了一下我们粗略考略的文件和句柄的关系以及句柄的管理,那操作系统是不是这么做的呢?其实也差不多。
任意进程,只要每打开一个对象(包括文件、进程、线程等等),就会获得一个句柄。
这个句柄用来标志对某个对象的一次打开,通过句柄,可以直接找到对应的内核对象。
每个进程都有一个句柄表,用来记录本进程打开的所有内核对象。
句柄表可以简单看做为一个一维数组,每个表项就是一个句柄,一个结构体,一个句柄描述符。
struct _HANDLE_TABLE_ENTRY //句柄描述符
struct _HANDLE_TABLE //句柄表描述符
好,更加细节的句柄表的原理我们不用再深究,我们只需要知道每个进程都有一个句柄表,通过句柄表就可以找到打开的文件。
这就是我们的目的,我们需要查到进程是不是打开了我们要删除的文件,我们需要查句柄表。
那怎么查呢?
操作系统给用户提供了一个接口ZwQuerySystemInformation。
NTSTATUS WINAPI ZwQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);
它可以获取系统非常多的信息,包括进程、模块、处理器、内存等等各种信息。
而SystemHandleInformation = 16就能获取到系统所有的句柄信息。
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
USHORT UniqueProcessId;//所属进程
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue; //句柄
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG NumberOfHandles;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
既然知道了方法,下面就开始枚举所有句柄,找到我们被占用的文件的进程信息。
Status = ZwQuerySystemInformation(SystemHandleInformation,
Information,
Length,
&ReturnLength);
for (i = 0; i < Information->NumberOfHandles; i++) {
if (Information->Handles[i].UniqueProcessId != CurrentProcessId) {//不是当前进程
Status = ZwQueryObject(TargetHandle, ObjectTypeInformation, &TypeInfo, sizeof(TypeInfo), NULL);
RtlInitUnicodeString(&TargetType, L"File");
if (!RtlEqualUnicodeString(&TypeInfo.Info.TypeName, &TargetType, FALSE)) {
goto __next;
}
Status = ZwQueryObject(TargetHandle, ObjectNameInformation, &NameInfo, sizeof(NameInfo), NULL);
if (RtlEqualUnicodeString(&NameInfo.Info.Name, &FileName, FALSE)) {
printf("在进程(%d)发现文件占用:(%x) %wZ\n",
ProcessId,
Information->Handles[i].HandleValue,
&NameInfo.Info.Name);
}
}
}
ZwQuerySystemInformation获取到所有句柄信息,通过循环枚举Information->Handles,找到句柄类型属于File,路径是目标文件的进程。
ZwQueryObject传入ObjectTypeInformation可以获取句柄类型,ZwQueryObject传入ObjectNameInformation可以获取文件路径。
如此两个条件的对比,就能让我们找到占用文件的进程了。
是不是感觉还挺简单,不复杂嘛。
坑一:ZwQueryObject
前面提到,每个进程都有自己的句柄表,所以ZwQuerySystemInformation枚举拿到的句柄并不能直接使用,还需要复制一份到本进程才有效。
系统也提供了API叫做DuplicateHandle:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
LPHANDLE lpTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions
);
DuplicateHandle(hSrcProc, Information->Handles[i].HandleValue, hCurProc, TargetHandle, ...);
上面我们使用的TargetHandle就是通过复制获取的。
这个地方并不是坑,而是在通过ZwQueryObject获取句柄对应的文件路径时,会发生阻塞,导致程序卡死无法继续运行。
0: kd> kv
# ChildEBP RetAddr Args to Child
00 d7fdb7cc 828aacda 00000000 00000000 a7d73040 nt!KiSwapContext+0x19 (FPO: [Uses EBP] [1,0,4])
01 d7fdb86c 828aa358 d7fdb930 a7d73120 a7d73040 nt!KiSwapThread+0x4aa (FPO: [Non-Fpo])
02 d7fdb8c8 828a9d67 00000000 00000000 00000000 nt!KiCommitThreadWait+0x128 (FPO: [Non-Fpo])
03 d7fdb978 829298a3 8ff18afc 00000000 a7d73300 nt!KeWaitForSingleObject+0x1f7 (FPO: [Non-Fpo])
04 d7fdb9a4 82c0759f 88c0e801 d7fdba18 8ff18ab0 nt!IopWaitForLockAlertable+0x3f (FPO: [Non-Fpo])
05 d7fdb9cc 82d3f75c 88c0e800 a7d733f8 d7fdb9ef nt!IopWaitAndAcquireFileObjectLock+0x41 (FPO: [Non-Fpo])
06 d7fdba1c 82bed31a 000001ee d7fdbb01 9a651dc0 nt!IopQueryXxxInformation+0x150f3e
07 d7fdba9c 82becf65 00000000 007af7a4 00000210 nt!IopQueryNameInternal+0x31a (FPO: [Non-Fpo])
08 d7fdbab8 82bece25 8ff18ab0 87ff2400 007af7a4 nt!IopQueryName+0x1b (FPO: [Non-Fpo])
09 d7fdbb40 82bec6a6 00000210 d7fdbc04 d7fdbb01 nt!ObQueryNameStringMode+0x495 (FPO: [Non-Fpo])
0a d7fdbbf8 829cce6b 8ff18ab0 00000000 007af7a4 nt!NtQueryObject+0x186 (FPO: [SEH])
0b d7fdbbf8 77cd5ef0 8ff18ab0 00000000 007af7a4 nt!KiSystemServicePostCall (FPO: [0,3] TrapFrame @ d7fdbc14)
经过一些简单的分析,如果文件被是同步(SYNCHRONIZE)打开的,内核会等待一下锁,等其他线程操作完成,本线程才能拿到所有权。
//
// Make a special check here to determine whether this is a synchronous
// I/O operation. If it is, then wait here until the file is owned by
// the current thread. If this is not a (serialized) synchronous I/O
// operation, then initialize the local event.
//
if (FileObject->Flags & FO_SYNCHRONOUS_IO) {
BOOLEAN interrupted;
if (!IopAcquireFastLock( FileObject )) {
status = IopAcquireFileObjectLock( FileObject,
Mode,
(BOOLEAN) ((FileObject->Flags & FO_ALERTABLE_IO) != 0),
&interrupted );
if (interrupted) {
ObDereferenceObject( FileObject );
return status;
}
}
KeClearEvent( &FileObject->Event );
synchronousIo = TRUE;
}
所以这里我们就需要通过线程和超时的方式来调用ZwQueryObject,让程序可以不阻塞正常运行。
void
QueryThread(
IN PQUERY_CONTEXT Context
) {
Status = ZwQueryObject(TargetHandle, ObjectNameInformation, &NameInfo, sizeof(NameInfo), NULL);
}
ThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)QueryThread, Context, 0, NULL);
Result = WaitForSingleObject(ThreadHandle, 1000); //等待1秒超时,线程退出
TerminateThread(ThreadHandle, 0);
CloseHandle(ThreadHandle);
坑二:文件Map
解决上面的问题之后,我们基本就解决了文件占用的问题,大部分情况下,我们可以正常删除文件了。
可是…
某些时候,我们要删除的文件并不是普通文件,可能是一个DLL、或者其他特殊文件。
关闭所有占用的句柄后,依然无法删除文件,还是提示占用。
这可怎么办?
类似于DLL这种文件,进程在使用中,操作系统会映射一份内存到进程空间,此时并没有句柄与之对应。
但是它却关联了文件的内核对象,专业术语说增加了一次文件对象的引用。
我们要知道,为了能够安全删除一个文件,操作系统需要保证该文件的内核对象在两种引用计数上清零。
一个是句柄引用计数,一个是对象引用计数。
前面我们通过枚举句柄,将句柄引用计数清零。
但是因为共享内存的原因,对象引用计数仍未清零,所以无法删除文件。
0: kd> !handle 48
PROCESS fffffa801b7c6060
SessionId: 1 Cid: 0b70 Peb: 7efdf000 ParentCid: 0588
DirBase: 1bfea000 ObjectTable: fffff8a0029f27e0 HandleCount: 157.
Image: procexp.exe
Handle table at fffff8a0029f27e0 with 157 entries in use
0004: Object: fffffa801bdcca10 GrantedAccess: 00000003 Entry: fffff8a0020cc010
Object: fffffa801bdcca10 Type: (fffffa8018dcfa30) File
ObjectHeader: fffffa801bdcc9e0 (new version)
HandleCount: 0//句柄引用计数 PointerCount: 1 //对象引用计数
我们通过!vad俩看看内存map。
0: kd> !vad fffffa8019d34e00
VAD Level Start End Commit
fffffa8019d34e00 0 1000 12ce 0 Mapped READONLY \Windows\Globalization\Sorting\SortDefault.nls
0: kd> dt _mmvad fffffa8019d34e00
nt!_MMVAD
+0x000 u1 :
+0x008 LeftChild : (null)
+0x010 RightChild : (null)
+0x018 StartingVpn : 0x1000
+0x020 EndingVpn : 0x12ce
+0x028 u :
+0x030 PushLock : _EX_PUSH_LOCK
+0x038 u5 :
+0x040 u2 :
+0x048 Subsection : 0xfffffa80`1b56ef90 _SUBSECTION
+0x048 MappedSubsection : 0xfffffa80`1b56ef90 _MSUBSECTION
+0x050 FirstPrototypePte : 0xfffff8a0`00b02000 _MMPTE
+0x058 LastContiguousPte : 0xfffff8a0`00b03670 _MMPTE
+0x060 ViewLinks : _LIST_ENTRY [ 0xfffffa80`18ec81c0 - 0xfffffa80`18fcd190 ]
+0x070 VadsProcess : 0xfffffa80`1b7c6061 _EPROCESS
0: kd> dt 0xfffffa80`1b56ef90 _SUBSECTION
nt!_SUBSECTION
+0x000 ControlArea : 0xfffffa80`1b56ef10 _CONTROL_AREA
+0x008 SubsectionBase : 0xfffff8a0`00b02000 _MMPTE
+0x010 NextSubsection : 0xfffffa80`193a0a60 _SUBSECTION
+0x018 PtesInSubsection : 0x2cf
+0x020 UnusedPtes : 0
+0x020 GlobalPerSessionHead : (null)
+0x028 u :
+0x02c StartingSector : 0
+0x030 NumberOfFullSectors : 0x2cf
0: kd> dt 0xfffffa80`1b56ef10 _CONTROL_AREA
nt!_CONTROL_AREA
+0x000 Segment : 0xfffff8a0`03b31fd0 _SEGMENT
+0x008 DereferenceList : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]
+0x018 NumberOfSectionReferences : 0
+0x020 NumberOfPfnReferences : 0x101
+0x028 NumberOfMappedViews : 0x2a
+0x030 NumberOfUserReferences : 0x2a
+0x038 u :
+0x03c FlushInProgressCount : 0
+0x040 FilePointer : _EX_FAST_REF
+0x048 ControlAreaLock : 0n0
+0x04c ModifiedWriteCount : 0
+0x04c StartingFrame : 0
+0x050 WaitList : (null)
+0x058 u2 :
+0x068 LockedPages : 1
+0x070 ViewList : _LIST_ENTRY [ 0xfffffa80`1be91570 - 0xfffffa80`1abbe690 ]
0: kd> dx -id 0,0,fffffa801b7c6060 -r1 (*((ntkrnlmp!_EX_FAST_REF *)0xfffffa801b56ef50))
(*((ntkrnlmp!_EX_FAST_REF *)0xfffffa801b56ef50)) [Type: _EX_FAST_REF]
[+0x000] Object : 0xfffffa801b61fa14 [Type: void *]
[+0x000 ( 3: 0)] RefCnt : 0x4 [Type: unsigned __int64]
[+0x000] Value : 0xfffffa801b61fa14 [Type: unsigned __int64]
0: kd> !object 0xfffffa801b61fa10
Object: fffffa801b61fa10 Type: (fffffa8018dcfa30) File
ObjectHeader: fffffa801b61f9e0 (new version)
HandleCount: 0 PointerCount: 5
Directory Object: 00000000 Name: \Windows\Globalization\Sorting\SortDefault.nls {HarddiskVolume2}
0: kd> dt _file_object 0xfffffa801b61fa10
nt!_FILE_OBJECT
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xfffffa80`19e9d530 _DEVICE_OBJECT
+0x010 Vpb : 0xfffffa80`19eca270 _VPB
+0x018 FsContext : 0xfffff8a0`00ad0140 Void
+0x020 FsContext2 : 0xfffff8a0`00ad0330 Void
+0x028 SectionObjectPointer : 0xfffffa80`1b61f808 _SECTION_OBJECT_POINTERS
0: kd> dx -id 0,0,fffffa801b7c6060 -r1 ((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xfffffa801b61f808)
((ntkrnlmp!_SECTION_OBJECT_POINTERS *)0xfffffa801b61f808) : 0xfffffa801b61f808 [Type: _SECTION_OBJECT_POINTERS *]
[+0x000] DataSectionObject : 0xfffffa801b56ef10 [Type: void *] //其实就是前面的_mmvad->Subsection->ControlArea
[+0x008] SharedCacheMap : 0x0 [Type: void *]
[+0x010] ImageSectionObject : 0x0 [Type: void *]
SortDefault.nls是被映射到了进程中,通过_mmvad->Subsection->ControlArea->FilePointer我们可以一步步定位到它引用的文件对象。
!object 0xfffffa801b61fa10
看到确实是该文件,也可以通过fileobject->SectionObjectPointer->DataSectionObject找到对应的映射内存。
如此我们初步理解了文件map导致文件占用无法删除文件的原理。
下面我们就需要找到方法怎么解决这个问题。
首先,需要枚举进程的虚拟内存,找到是否有我们需要查找的文件的map,然后对该进程有两种操作:
非常暴力但是简单的方法,那就是直接关闭进程
或者unmap这块内存,解除对象引用计数(经过测试,未成功)
如何枚举虚拟内存呢,使用ZwQueryVirtualMemory.
NTSTATUS ZwQueryVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
_Out_ PVOID MemoryInformation,
_In_ SIZE_T MemoryInformationLength,
_Out_opt_ PSIZE_T ReturnLength
);
//MemoryBasicInformation
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
USHORT PartitionId;
SIZE_T RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
Type
The type of pages in the region. The following types are defined.
MEM_IMAGE 0x1000000 Indicates that the memory pages within the region are mapped into the view of an image section.
MEM_MAPPED 0x40000 Indicates that the memory pages within the region are mapped into the view of a section.
MEM_PRIVATE 0x20000 Indicates that the memory pages within the region are private (that is, not shared by other processes).
从0地址开始,每次加一个页,获取内存信息,如果内存的type是MEM_IMAGE或者MEM_MAPPED,那么就是文件map,然后获取虚拟内存对应名字,判断是不是目标文件。
for (;;) {
Status = ZwQueryVirtualMemory(ProcessHandle, BaseAddress, MemoryBasicInformation,
&MemoryInfo, sizeof(MemoryInfo), NULL);
if (MemoryInfo.Type == MEM_IMAGE || //image
MemoryInfo.Type == MEM_MAPPED) { //data
Status = ZwQueryVirtualMemory(ProcessHandle, BaseAddress, MemoryMappedFilenameInformation, &Name, sizeof(Name), NULL);
if (RtlEqualUnicodeString(&Name.u, &TargetName, TRUE)) {
//找到目标文件
break;
}
}
}
}
找到目标进程后,关闭进程,轻松删除文件。
(完)
如果觉得内容还不错,请不吝点赞或在看,谢谢。
欢迎关注公众号:Anhkgg日记本