author: anhkgg
date: 2021-01-14 11:17:27
了解一点操作系统知识的同学们应该都知道,文件占用无法删除,是因为某些进程正在使用该文件。
要删除这样的文件,就需要让那些进程关闭文件,然后自然可以删除。
一句话的事,那究竟要怎么用代码来实现这个功能呢?
打开和关闭文件
还记得上大学第一门语言课-C语言,迄今为止还依然活跃并被一直使用的语言。
比汇编容易理解,又更接近底层,所以Windows操作系统内核大部分代码都是用C语言来编写的。
在C的课程里,我们学过通过FILE来操作使用文件,比如:
1 | FILE *fp; |
通过读的方式打开一个文件,使用非常简单,后续通过fp这个结构体指针操作文件即可。
其实fopen并不接近操作系统,他是对win32 API CreateFile的封装。
也就是前者是标准库接口,在Windows、linux、unix等都是通用接口。
而后者才是和操作系统关联紧密,由微软自己提供的API。
要更好的理解进程如何使用文件的,我们还得看看CreateFile这个API接口。
1 | HANDLE CreateFileA( |
这是msdn对CreateFile的定义,简单来看我们可以只关注lpFileName和返回值,lpFileName传递你要打开的文件,返回值是操作系统给你的一个代表文件的句柄(handle)。
1 | HANDLE hFile = CreateFileA("c:\\temp\\test.txt", ...); |
要对文件进行读、写等操作都需要这个句柄,也就是说这个句柄至关重要,它表示文件正在被使用。
然后什么时候结束使用呢,我们需要看另一个API CloseHandle.
1 | BOOL CloseHandle( |
CloseHandle用于关闭一个正在被使用的文件,通过句柄来关闭。
现在明白过来了吗,只要我们让进程调用CloseHandle这个API,关闭被占用的文件句柄,那么该文件也就被解除占用了。
哈哈,是不是很简单。
枚举占用文件的进程
那么我就想问同学们一个问题,怎么知道哪些进程在使用我们想删除的文件呢?怎么去查找?
带着这个问题,我们继续往下看。
我们来想一个问题,操作系统给调用CreateFile的用户返回了一个句柄,然后通过句柄来操作文件,那操作系统是如何知道句柄代表哪个文件呢?
我们简单思考一下,我们要做到这个目的有没有什么方法,比如我用一个数组来存用户打开的文件路径,而数组序号就返回给用户,下次用户就只需要把序号给我,我就知道要操作什么问题了。
1 | 演示代码,忽略细节 |
上面简单的代码演示了一下我们粗略考略的文件和句柄的关系以及句柄的管理,那操作系统是不是这么做的呢?其实也差不多。
//https://www.cnblogs.com/lsh123/p/8329989.html
任意进程,只要每打开一个对象(包括文件、进程、线程等等),就会获得一个句柄。
这个句柄用来标志对某个对象的一次打开,通过句柄,可以直接找到对应的内核对象。
每个进程都有一个句柄表,用来记录本进程打开的所有内核对象。
句柄表可以简单看做为一个一维数组,每个表项就是一个句柄,一个结构体,一个句柄描述符。
1 | struct _HANDLE_TABLE_ENTRY //句柄描述符 |
好,更加细节的句柄表的原理我们不用再深究,我们只需要知道每个进程都有一个句柄表,通过句柄表就可以找到打开的文件。
这就是我们的目的,我们需要查到进程是不是打开了我们要删除的文件,我们需要查句柄表。
那怎么查呢?
操作系统给用户提供了一个接口ZwQuerySystemInformation。
1 | NTSTATUS WINAPI ZwQuerySystemInformation( |
它可以获取系统非常多的信息,包括进程、模块、处理器、内存等等各种信息。
而SystemHandleInformation = 16就能获取到系统所有的句柄信息。
1 | typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO |
既然知道了方法,下面就开始枚举所有句柄,找到我们被占用的文件的进程信息。
1 | Status = ZwQuerySystemInformation(SystemHandleInformation, |
ZwQuerySystemInformation获取到所有句柄信息,通过循环枚举Information->Handles,找到句柄类型属于File,路径是目标文件的进程。
ZwQueryObject传入ObjectTypeInformation可以获取句柄类型,ZwQueryObject传入ObjectNameInformation可以获取文件路径。
如此两个条件的对比,就能让我们找到占用文件的进程了。
是不是感觉还挺简单,不复杂嘛。
坑一:ZwQueryObject
前面提到,每个进程都有自己的句柄表,所以ZwQuerySystemInformation枚举拿到的句柄并不能直接使用,还需要复制一份到本进程才有效。
系统也提供了API叫做DuplicateHandle:
1 | BOOL DuplicateHandle( |
上面我们使用的TargetHandle就是通过复制获取的。
这个地方并不是坑,而是在通过ZwQueryObject获取句柄对应的文件路径时,会发生阻塞,导致程序卡死无法继续运行。
1 | 0: kd> kv |
经过一些简单的分析,如果文件被是同步(SYNCHRONIZE)打开的,内核会等待一下锁,等其他线程操作完成,本线程才能拿到所有权。
1 | // |
所以这里我们就需要通过线程和超时的方式来调用ZwQueryObject,让程序可以不阻塞正常运行。
1 | void |
坑二:文件Map
解决上面的问题之后,我们基本就解决了文件占用的问题,大部分情况下,我们可以正常删除文件了。
可是…
某些时候,我们要删除的文件并不是普通文件,可能是一个DLL、或者其他特殊文件。
关闭所有占用的句柄后,依然无法删除文件,还是提示占用。
这可怎么办?
类似于DLL这种文件,进程在使用中,操作系统会映射一份内存到进程空间,此时并没有句柄与之对应。
但是它却关联了文件的内核对象,专业术语说增加了一次文件对象的引用。
我们要知道,为了能够安全删除一个文件,操作系统需要保证该文件的内核对象在两种引用计数上清零。
一个是句柄引用计数,一个是对象引用计数。
前面我们通过枚举句柄,将句柄引用计数清零。
但是因为共享内存的原因,对象引用计数仍未清零,所以无法删除文件。
1 | 0: kd> !handle 48 |
我们通过!vad俩看看内存map。
1 | 0: kd> !vad fffffa8019d34e00 |
SortDefault.nls是被映射到了进程中,通过_mmvad->Subsection->ControlArea->FilePointer我们可以一步步定位到它引用的文件对象。
!object 0xfffffa801b61fa10
看到确实是该文件,也可以通过fileobject->SectionObjectPointer->DataSectionObject找到对应的映射内存。
如此我们初步理解了文件map导致文件占用无法删除文件的原理。
下面我们就需要找到方法怎么解决这个问题。
首先,需要枚举进程的虚拟内存,找到是否有我们需要查找的文件的map,然后对该进程有两种操作:
- 非常暴力但是简单的方法,那就是直接关闭进程
- 或者unmap这块内存,解除对象引用计数(经过测试,未成功,待深入研究,也请大佬指教)
如何枚举虚拟内存呢,使用ZwQueryVirtualMemory).
1 | NTSTATUS ZwQueryVirtualMemory( |
从0地址开始,每次加一个页,获取内存信息,如果内存的type是MEM_IMAGE或者MEM_MAPPED,那么就是文件map,然后获取虚拟内存对应名字,判断是不是目标文件。
1 | for (;;) { |
找到目标进程后,关闭进程,轻松删除文件。
代码都在环3完成。工具在此:FileLock
(完)
转载请注明出处:https://anhkgg.github.io/unlockfile
欢迎关注技术公众号:汉客儿