查询系统所有句柄可以使用NtQuerySystemInformation的SystemExtendedHandleInformation枚举,查到系统句柄表后使用OpenProcess和DuplicateHandle获取本地句柄,最后用NtQueryObject的ObjectNameInformation枚举得到句柄的各种详细信息,这是Windows资源监视器使用的技术路线。注意 Windows SDK 中未公开关于系统句柄信息的查询参数、枚举结构等,需要使用PHNT提供的头文件,将返回缓冲区解释为SYSTEM_HANDLE_INFORMATION_EX结构。
此方法存在一个问题,就是NtQueryObject在查询某些类型的文件句柄时会永久挂起不返回。目前已知对于管道类型的文件必定会挂起,但实际上还不限于此,有很多难以确认的原因都会导致此调用挂起。因此资源监视器对文件句柄会单开一个异步线程执行NtQueryObject,如果没有在限时内返回,就放弃查询此句柄。此方法会造成大量被废弃的线程,性能较低。但在一般用户权限下,这是唯一的解决方案。
但是,如果我们可以将进程提升到管理员权限,Sysinternals Handle为我们提供了另一种解决方案,就是使用ProcExp152.sys驱动。
ProcExp152.sys是Windows自带的内核驱动,可以用来查询系统句柄信息。要调用此驱动,需要使用CreateFileW打开"\??\PROCEXP152"设备,然后使用DeviceIoControl获取信息。DeviceIoControl是对各种设备通用的API,对不同的驱动和控制码具有不同的输入参数,全部放在InBuffer结构体中。DeviceIoControl将InBuffer定义为LPVOID,但对于一个具体的设备的具体的控制码,此参数的结构由驱动程序提供方定义。OutBuffer也是同理,由驱动程序提供方定义输出缓冲区的结构。
0x83350048是ProcExp152定义的,用于查询句柄名称和访问权限的控制码。对于文件句柄,这个名称就是文件路径(但不使用盘符作为根,而是使用HarddiskVolume#,用QueryDosDeviceW可以转换为盘符)。
但是,此控制码的InBuffer和OutBuffer结构定义没有公开的文档。因此作者用IDA和 API Monitor 等逆向工具推导得到:
#pragma pack(push,8)
struct ProcExp_0x83350048_InBuffer
{
const DWORD CurrentProcessId = CPI;
PVOID Object;
bool IsFile;
HANDLE DuplicatedHandle;
protected:
static const DWORD CPI;
};
#pragma pack(pop)
const DWORD ProcExp_0x83350048_InBuffer::CPI = GetCurrentProcessId();
struct ProcExp_0x83350048_OutBuffer
{
ULONG ShareAccess;
wchar_t* 文件名()const noexcept { return (wchar_t*)(this + 1); }
};
InBuffer是一个8字节对齐的结构体,有4个成员。第1个是本进程的ID,第2个是NtQuerySystemInformation返回的系统句柄表中提供的句柄对象指针(不是句柄值HandleValue),第3个是此句柄是否是文件类型的逻辑值,第4个是DuplicateHandle得到的本地句柄值。
OutBuffer长度不固定。第1个固定成员ShareAccess,此值与CreateFileW的ShareAccess参数含义相同。第2个变长成员即为UTF16编码的句柄文件名,采用C样式0结尾字符串。
此方法的缺点在于需要提权,但优点是不会挂起,一定会返回。
使用DuplicateHandle的DUPLICATE_CLOSE_SOURCE选项可以直接关闭原进程持有的该句柄,这可以用于在不结束先占进程的情况下抢夺其对文件(广义的文件,包括设备、串口、端口等)的占用(当然此操作需要提权)。但如果对系统关键进程使用句柄抢夺可能造成系统崩溃。