技術(shù)版:文件被占用無法刪除怎么辦?
技術(shù)版:文件被占用無法刪除怎么辦?了解一點(diǎn)操作系統(tǒng)知識(shí)的同學(xué)們應(yīng)該都知道,文件占用無法刪除,是因?yàn)槟承┻M(jìn)程正在使用該文件。

要?jiǎng)h除這樣的文件,就需要讓那些進(jìn)程關(guān)閉文件,然后自然可以刪除。
一句話的事,那究竟要怎么用代碼來實(shí)現(xiàn)這個(gè)功能呢?
打開和關(guān)閉文件
還記得上大學(xué)第一門語(yǔ)言課-C語(yǔ)言,迄今為止還依然活躍并被一直使用的語(yǔ)言。
比匯編容易理解,又更接近底層,所以Windows操作系統(tǒng)內(nèi)核大部分代碼都是用C語(yǔ)言來編寫的。
在C的課程里,我們學(xué)過通過FILE來操作使用文件,比如:
FILE *fp;
fp = fopen("c:\\temp\\test.txt", "r")通過讀的方式打開一個(gè)文件,使用非常簡(jiǎn)單,后續(xù)通過fp這個(gè)結(jié)構(gòu)體指針操作文件即可。
其實(shí)fopen并不接近操作系統(tǒng),他是對(duì)win32 API CreateFile的封裝。
也就是前者是標(biāo)準(zhǔn)庫(kù)接口,在Windows、linux、unix等都是通用接口。
而后者才是和操作系統(tǒng)關(guān)聯(lián)緊密,由微軟自己提供的API。
要更好的理解進(jìn)程如何使用文件的,我們還得看看CreateFile這個(gè)API接口。
HANDLE CreateFileA(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);這是msdn對(duì)CreateFile的定義,簡(jiǎn)單來看我們可以只關(guān)注lpFileName和返回值,lpFileName傳遞你要打開的文件,返回值是操作系統(tǒng)給你的一個(gè)代表文件的句柄(handle)。
HANDLE hFile = CreateFileA("c:\\temp\\test.txt", ...);要對(duì)文件進(jìn)行讀、寫等操作都需要這個(gè)句柄,也就是說這個(gè)句柄至關(guān)重要,它表示文件正在被使用。
然后什么時(shí)候結(jié)束使用呢,我們需要看另一個(gè)API CloseHandle.
BOOL CloseHandle(
HANDLE hObject
);CloseHandle用于關(guān)閉一個(gè)正在被使用的文件,通過句柄來關(guān)閉。
現(xiàn)在明白過來了嗎,只要我們讓進(jìn)程調(diào)用CloseHandle這個(gè)API,關(guān)閉被占用的文件句柄,那么該文件也就被解除占用了。
哈哈,是不是很簡(jiǎn)單。
枚舉占用文件的進(jìn)程
那么我就想問同學(xué)們一個(gè)問題,怎么知道哪些進(jìn)程在使用我們想刪除的文件呢?怎么去查找?
帶著這個(gè)問題,我們繼續(xù)往下看。
我們來想一個(gè)問題,操作系統(tǒng)給調(diào)用CreateFile的用戶返回了一個(gè)句柄,然后通過句柄來操作文件,那操作系統(tǒng)是如何知道句柄代表哪個(gè)文件呢?
我們簡(jiǎn)單思考一下,我們要達(dá)到這個(gè)目的有沒有什么方法,比如我用一個(gè)數(shù)組來存用戶打開的文件路徑,而數(shù)組序號(hào)就返回給用戶,下次用戶就只需要把序號(hào)給我,我就知道要操作什么問題了。
演示代碼,忽略細(xì)節(jié)
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;
}上面簡(jiǎn)單的代碼演示了一下我們粗略考略的文件和句柄的關(guān)系以及句柄的管理,那操作系統(tǒng)是不是這么做的呢?其實(shí)也差不多。
任意進(jìn)程,只要每打開一個(gè)對(duì)象(包括文件、進(jìn)程、線程等等),就會(huì)獲得一個(gè)句柄。
這個(gè)句柄用來標(biāo)志對(duì)某個(gè)對(duì)象的一次打開,通過句柄,可以直接找到對(duì)應(yīng)的內(nèi)核對(duì)象。
每個(gè)進(jìn)程都有一個(gè)句柄表,用來記錄本進(jìn)程打開的所有內(nèi)核對(duì)象。
句柄表可以簡(jiǎn)單看做為一個(gè)一維數(shù)組,每個(gè)表項(xiàng)就是一個(gè)句柄,一個(gè)結(jié)構(gòu)體,一個(gè)句柄描述符。
struct _HANDLE_TABLE_ENTRY //句柄描述符
struct _HANDLE_TABLE //句柄表描述符好,更加細(xì)節(jié)的句柄表的原理我們不用再深究,我們只需要知道每個(gè)進(jìn)程都有一個(gè)句柄表,通過句柄表就可以找到打開的文件。
這就是我們的目的,我們需要查到進(jìn)程是不是打開了我們要?jiǎng)h除的文件,我們需要查句柄表。
那怎么查呢?
操作系統(tǒng)給用戶提供了一個(gè)接口ZwQuerySystemInformation。
NTSTATUS WINAPI ZwQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);它可以獲取系統(tǒng)非常多的信息,包括進(jìn)程、模塊、處理器、內(nèi)存等等各種信息。
而SystemHandleInformation = 16就能獲取到系統(tǒng)所有的句柄信息。
typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
USHORT UniqueProcessId;//所屬進(jìn)程
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;既然知道了方法,下面就開始枚舉所有句柄,找到我們被占用的文件的進(jìn)程信息。
Status = ZwQuerySystemInformation(SystemHandleInformation,
Information,
Length,
&ReturnLength);
for (i = 0; i < Information->NumberOfHandles; i++) {
if (Information->Handles[i].UniqueProcessId != CurrentProcessId) {//不是當(dāng)前進(jìn)程
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("在進(jìn)程(%d)發(fā)現(xiàn)文件占用:(%x) %wZ\n",
ProcessId,
Information->Handles[i].HandleValue,
&NameInfo.Info.Name);
}
}
}ZwQuerySystemInformation獲取到所有句柄信息,通過循環(huán)枚舉Information->Handles,找到句柄類型屬于File,路徑是目標(biāo)文件的進(jìn)程。
ZwQueryObject傳入ObjectTypeInformation可以獲取句柄類型,ZwQueryObject傳入ObjectNameInformation可以獲取文件路徑。
如此兩個(gè)條件的對(duì)比,就能讓我們找到占用文件的進(jìn)程了。
是不是感覺還挺簡(jiǎn)單,不復(fù)雜嘛。
坑一:ZwQueryObject
前面提到,每個(gè)進(jìn)程都有自己的句柄表,所以ZwQuerySystemInformation枚舉拿到的句柄并不能直接使用,還需要復(fù)制一份到本進(jìn)程才有效。
系統(tǒng)也提供了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就是通過復(fù)制獲取的。
這個(gè)地方并不是坑,而是在通過ZwQueryObject獲取句柄對(duì)應(yīng)的文件路徑時(shí),會(huì)發(fā)生阻塞,導(dǎo)致程序卡死無法繼續(xù)運(yùn)行。
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)經(jīng)過一些簡(jiǎn)單的分析,如果文件被是同步(SYNCHRONIZE)打開的,內(nèi)核會(huì)等待一下鎖,等其他線程操作完成,本線程才能拿到所有權(quán)。
//
// 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;
}所以這里我們就需要通過線程和超時(shí)的方式來調(diào)用ZwQueryObject,讓程序可以不阻塞正常運(yùn)行。
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秒超時(shí),線程退出
TerminateThread(ThreadHandle, 0);
CloseHandle(ThreadHandle);坑二:文件Map
解決上面的問題之后,我們基本就解決了文件占用的問題,大部分情況下,我們可以正常刪除文件了。
可是…
某些時(shí)候,我們要?jiǎng)h除的文件并不是普通文件,可能是一個(gè)DLL、或者其他特殊文件。
關(guān)閉所有占用的句柄后,依然無法刪除文件,還是提示占用。

這可怎么辦?
類似于DLL這種文件,進(jìn)程在使用中,操作系統(tǒng)會(huì)映射一份內(nèi)存到進(jìn)程空間,此時(shí)并沒有句柄與之對(duì)應(yīng)。
但是它卻關(guān)聯(lián)了文件的內(nèi)核對(duì)象,專業(yè)術(shù)語(yǔ)說增加了一次文件對(duì)象的引用。
我們要知道,為了能夠安全刪除一個(gè)文件,操作系統(tǒng)需要保證該文件的內(nèi)核對(duì)象在兩種引用計(jì)數(shù)上清零。
一個(gè)是句柄引用計(jì)數(shù),一個(gè)是對(duì)象引用計(jì)數(shù)。
前面我們通過枚舉句柄,將句柄引用計(jì)數(shù)清零。
但是因?yàn)楣蚕韮?nèi)存的原因,對(duì)象引用計(jì)數(shù)仍未清零,所以無法刪除文件。
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//句柄引用計(jì)數(shù) PointerCount: 1 //對(duì)象引用計(jì)數(shù)我們通過!vad倆看看內(nèi)存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 *] //其實(shí)就是前面的_mmvad->Subsection->ControlArea
[+0x008] SharedCacheMap : 0x0 [Type: void *]
[+0x010] ImageSectionObject : 0x0 [Type: void *]SortDefault.nls是被映射到了進(jìn)程中,通過_mmvad->Subsection->ControlArea->FilePointer我們可以一步步定位到它引用的文件對(duì)象。
!object 0xfffffa801b61fa10看到確實(shí)是該文件,也可以通過fileobject->SectionObjectPointer->DataSectionObject找到對(duì)應(yīng)的映射內(nèi)存。
如此我們初步理解了文件map導(dǎo)致文件占用無法刪除文件的原理。
下面我們就需要找到方法怎么解決這個(gè)問題。
首先,需要枚舉進(jìn)程的虛擬內(nèi)存,找到是否有我們需要查找的文件的map,然后對(duì)該進(jìn)程有兩種操作:
非常暴力但是簡(jiǎn)單的方法,那就是直接關(guān)閉進(jìn)程
或者unmap這塊內(nèi)存,解除對(duì)象引用計(jì)數(shù)(經(jīng)過測(cè)試,未成功)
如何枚舉虛擬內(nèi)存呢,使用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地址開始,每次加一個(gè)頁(yè),獲取內(nèi)存信息,如果內(nèi)存的type是MEM_IMAGE或者M(jìn)EM_MAPPED,那么就是文件map,然后獲取虛擬內(nèi)存對(duì)應(yīng)名字,判斷是不是目標(biāo)文件。
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)) {
//找到目標(biāo)文件
break;
}
}
}
}找到目標(biāo)進(jìn)程后,關(guān)閉進(jìn)程,輕松刪除文件。
(完)
如果覺得內(nèi)容還不錯(cuò),請(qǐng)不吝點(diǎn)贊或在看,謝謝。
歡迎關(guān)注公眾號(hào):Anhkgg日記本
