342 lines
11 KiB
Python
342 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
import ctypes
|
|
import os
|
|
from ctypes import wintypes
|
|
from dataclasses import dataclass
|
|
|
|
|
|
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
ntdll = ctypes.WinDLL("ntdll", use_last_error=True)
|
|
advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)
|
|
|
|
|
|
PROCESS_DUP_HANDLE = 0x0040
|
|
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
DUPLICATE_SAME_ACCESS = 0x00000002
|
|
DUPLICATE_CLOSE_SOURCE = 0x00000001
|
|
|
|
GENERIC_READ = 0x80000000
|
|
FILE_SHARE_READ = 0x00000001
|
|
FILE_SHARE_WRITE = 0x00000002
|
|
FILE_SHARE_DELETE = 0x00000004
|
|
OPEN_EXISTING = 3
|
|
FILE_ATTRIBUTE_NORMAL = 0x00000080
|
|
|
|
|
|
SystemExtendedHandleInformation = 64
|
|
|
|
|
|
class SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX(ctypes.Structure):
|
|
_fields_ = [
|
|
("Object", ctypes.c_void_p),
|
|
("UniqueProcessId", ctypes.c_void_p),
|
|
("HandleValue", ctypes.c_void_p),
|
|
("GrantedAccess", wintypes.ULONG),
|
|
("CreatorBackTraceIndex", wintypes.USHORT),
|
|
("ObjectTypeIndex", wintypes.USHORT),
|
|
("HandleAttributes", wintypes.ULONG),
|
|
("Reserved", wintypes.ULONG),
|
|
]
|
|
|
|
|
|
class SYSTEM_HANDLE_INFORMATION_EX(ctypes.Structure):
|
|
_fields_ = [
|
|
("NumberOfHandles", ctypes.c_void_p),
|
|
("Reserved", ctypes.c_void_p),
|
|
("Handles", SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX * 1),
|
|
]
|
|
|
|
|
|
NtQuerySystemInformation = ntdll.NtQuerySystemInformation
|
|
NtQuerySystemInformation.argtypes = [
|
|
wintypes.ULONG,
|
|
wintypes.LPVOID,
|
|
wintypes.ULONG,
|
|
wintypes.PULONG,
|
|
]
|
|
NtQuerySystemInformation.restype = wintypes.LONG
|
|
|
|
|
|
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
|
|
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
|
|
kernel32.DuplicateHandle.argtypes = [
|
|
wintypes.HANDLE,
|
|
wintypes.HANDLE,
|
|
wintypes.HANDLE,
|
|
ctypes.POINTER(wintypes.HANDLE),
|
|
wintypes.DWORD,
|
|
wintypes.BOOL,
|
|
wintypes.DWORD,
|
|
]
|
|
kernel32.DuplicateHandle.restype = wintypes.BOOL
|
|
|
|
kernel32.GetCurrentProcess.argtypes = []
|
|
kernel32.GetCurrentProcess.restype = wintypes.HANDLE
|
|
|
|
kernel32.GetFinalPathNameByHandleW.argtypes = [
|
|
wintypes.HANDLE,
|
|
wintypes.LPWSTR,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
]
|
|
kernel32.GetFinalPathNameByHandleW.restype = wintypes.DWORD
|
|
|
|
kernel32.GetCurrentProcessId.argtypes = []
|
|
kernel32.GetCurrentProcessId.restype = wintypes.DWORD
|
|
|
|
kernel32.CreateFileW.argtypes = [
|
|
wintypes.LPCWSTR,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
wintypes.LPVOID,
|
|
wintypes.DWORD,
|
|
wintypes.DWORD,
|
|
wintypes.HANDLE,
|
|
]
|
|
kernel32.CreateFileW.restype = wintypes.HANDLE
|
|
|
|
|
|
def _normalize_path(p: str) -> str:
|
|
p = os.path.abspath(p)
|
|
p = os.path.normcase(p)
|
|
return p
|
|
|
|
|
|
def _normalize_final_path(p: str) -> str:
|
|
# GetFinalPathNameByHandleW 常见返回:\\?\C:\path\file 或 \\?\UNC\server\share\path
|
|
if p.startswith("\\\\?\\UNC\\"):
|
|
p = "\\\\" + p[len("\\\\?\\UNC\\") :]
|
|
elif p.startswith("\\\\?\\"):
|
|
p = p[len("\\\\?\\") :]
|
|
return _normalize_path(p)
|
|
|
|
|
|
def _try_enable_debug_privilege() -> None:
|
|
# best-effort:没有也能工作,只是会漏掉部分系统进程
|
|
TOKEN_ADJUST_PRIVILEGES = 0x0020
|
|
TOKEN_QUERY = 0x0008
|
|
SE_PRIVILEGE_ENABLED = 0x0002
|
|
|
|
class LUID(ctypes.Structure):
|
|
_fields_ = [("LowPart", wintypes.DWORD), ("HighPart", wintypes.LONG)]
|
|
|
|
class LUID_AND_ATTRIBUTES(ctypes.Structure):
|
|
_fields_ = [("Luid", LUID), ("Attributes", wintypes.DWORD)]
|
|
|
|
class TOKEN_PRIVILEGES(ctypes.Structure):
|
|
_fields_ = [("PrivilegeCount", wintypes.DWORD), ("Privileges", LUID_AND_ATTRIBUTES * 1)]
|
|
|
|
OpenProcessToken = advapi32.OpenProcessToken
|
|
OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)]
|
|
OpenProcessToken.restype = wintypes.BOOL
|
|
|
|
LookupPrivilegeValueW = advapi32.LookupPrivilegeValueW
|
|
LookupPrivilegeValueW.argtypes = [wintypes.LPCWSTR, wintypes.LPCWSTR, ctypes.POINTER(LUID)]
|
|
LookupPrivilegeValueW.restype = wintypes.BOOL
|
|
|
|
AdjustTokenPrivileges = advapi32.AdjustTokenPrivileges
|
|
AdjustTokenPrivileges.argtypes = [
|
|
wintypes.HANDLE,
|
|
wintypes.BOOL,
|
|
ctypes.POINTER(TOKEN_PRIVILEGES),
|
|
wintypes.DWORD,
|
|
wintypes.PVOID,
|
|
wintypes.PVOID,
|
|
]
|
|
AdjustTokenPrivileges.restype = wintypes.BOOL
|
|
|
|
token = wintypes.HANDLE()
|
|
if not OpenProcessToken(kernel32.GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ctypes.byref(token)):
|
|
return
|
|
try:
|
|
luid = LUID()
|
|
if not LookupPrivilegeValueW(None, "SeDebugPrivilege", ctypes.byref(luid)):
|
|
return
|
|
tp = TOKEN_PRIVILEGES()
|
|
tp.PrivilegeCount = 1
|
|
tp.Privileges[0].Luid = luid
|
|
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
|
|
AdjustTokenPrivileges(token, False, ctypes.byref(tp), 0, None, None)
|
|
finally:
|
|
kernel32.CloseHandle(token)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FileLock:
|
|
pid: int
|
|
handle: int
|
|
path: str
|
|
|
|
|
|
def _query_system_handles(max_size: int = 1 << 28) -> tuple[ctypes.Array, int] | tuple[None, int]:
|
|
"""返回 (buffer, handle_count);失败返回 (None, 0)。"""
|
|
size = 1 << 20
|
|
while True:
|
|
buf = ctypes.create_string_buffer(size)
|
|
ret_len = wintypes.ULONG(0)
|
|
status = NtQuerySystemInformation(
|
|
SystemExtendedHandleInformation, buf, size, ctypes.byref(ret_len)
|
|
)
|
|
if status == 0: # STATUS_SUCCESS
|
|
info = ctypes.cast(buf, ctypes.POINTER(SYSTEM_HANDLE_INFORMATION_EX)).contents
|
|
n = int(info.NumberOfHandles)
|
|
return buf, n
|
|
# STATUS_INFO_LENGTH_MISMATCH = 0xC0000004 (signed: -1073741820)
|
|
if status in (-1073741820, 0xC0000004):
|
|
size = max(size * 2, int(ret_len.value) + 0x10000)
|
|
if size > max_size:
|
|
return None, 0
|
|
continue
|
|
return None, 0
|
|
|
|
|
|
def _get_file_object_type_index(buf: ctypes.Array, handle_count: int, sample_path: str) -> int | None:
|
|
"""
|
|
通过“本进程打开一个文件句柄”,再在系统句柄表中找到它对应的 ObjectTypeIndex。
|
|
这样可以只扫描 File 类型句柄,速度会快很多。
|
|
"""
|
|
try:
|
|
h = kernel32.CreateFileW(
|
|
sample_path,
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
None,
|
|
OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
None,
|
|
)
|
|
if not h or h == wintypes.HANDLE(-1).value:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
try:
|
|
cur_pid = int(kernel32.GetCurrentProcessId())
|
|
hv = int(ctypes.cast(h, ctypes.c_size_t).value)
|
|
|
|
n = int(handle_count)
|
|
if n <= 0:
|
|
return None
|
|
array_type = SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX * n
|
|
info = ctypes.cast(buf, ctypes.POINTER(SYSTEM_HANDLE_INFORMATION_EX)).contents
|
|
handles = ctypes.cast(ctypes.addressof(info.Handles), ctypes.POINTER(array_type)).contents
|
|
|
|
for ent in handles:
|
|
pid = int(ctypes.cast(ent.UniqueProcessId, ctypes.c_size_t).value)
|
|
if pid != cur_pid:
|
|
continue
|
|
val = int(ctypes.cast(ent.HandleValue, ctypes.c_size_t).value)
|
|
if val == hv:
|
|
return int(ent.ObjectTypeIndex)
|
|
return None
|
|
finally:
|
|
try:
|
|
kernel32.CloseHandle(h)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def find_file_locks(target_path: str, *, max_handles: int = 200000) -> list[FileLock]:
|
|
"""
|
|
返回当前系统中占用 target_path 的 (pid, handle) 列表。
|
|
- 基于 SystemExtendedHandleInformation + DuplicateHandle + GetFinalPathNameByHandleW
|
|
"""
|
|
_try_enable_debug_privilege()
|
|
target_norm = _normalize_path(target_path)
|
|
|
|
buf, n = _query_system_handles()
|
|
if buf is None or n <= 0:
|
|
return []
|
|
|
|
if n <= 0:
|
|
return []
|
|
if n > max_handles:
|
|
n = max_handles
|
|
|
|
# 重新按实际数量解释数组
|
|
array_type = SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX * n
|
|
info = ctypes.cast(buf, ctypes.POINTER(SYSTEM_HANDLE_INFORMATION_EX)).contents
|
|
handles = ctypes.cast(ctypes.addressof(info.Handles), ctypes.POINTER(array_type)).contents
|
|
|
|
cur_proc = kernel32.GetCurrentProcess()
|
|
locks: list[FileLock] = []
|
|
|
|
file_type_index = _get_file_object_type_index(buf, int(info.NumberOfHandles), target_path)
|
|
# 进程句柄缓存:避免每个 handle 都 OpenProcess/CloseHandle
|
|
proc_cache: dict[int, wintypes.HANDLE] = {}
|
|
|
|
for h in handles:
|
|
pid = int(ctypes.cast(h.UniqueProcessId, ctypes.c_size_t).value)
|
|
hv = int(ctypes.cast(h.HandleValue, ctypes.c_size_t).value)
|
|
if pid <= 0 or hv <= 0:
|
|
continue
|
|
if file_type_index is not None and int(h.ObjectTypeIndex) != int(file_type_index):
|
|
continue
|
|
|
|
ph = proc_cache.get(pid)
|
|
if not ph:
|
|
ph = kernel32.OpenProcess(
|
|
PROCESS_DUP_HANDLE | PROCESS_QUERY_LIMITED_INFORMATION, False, pid
|
|
)
|
|
if not ph:
|
|
continue
|
|
proc_cache[pid] = ph
|
|
try:
|
|
dup = wintypes.HANDLE()
|
|
if not kernel32.DuplicateHandle(ph, wintypes.HANDLE(hv), cur_proc, ctypes.byref(dup), 0, False, DUPLICATE_SAME_ACCESS):
|
|
continue
|
|
try:
|
|
# 获取路径
|
|
bufw = ctypes.create_unicode_buffer(4096)
|
|
got = kernel32.GetFinalPathNameByHandleW(dup, bufw, len(bufw), 0)
|
|
if got == 0 or got >= len(bufw):
|
|
continue
|
|
final_norm = _normalize_final_path(bufw.value)
|
|
if final_norm == target_norm:
|
|
locks.append(FileLock(pid=pid, handle=hv, path=final_norm))
|
|
finally:
|
|
kernel32.CloseHandle(dup)
|
|
finally:
|
|
pass
|
|
|
|
for ph in proc_cache.values():
|
|
try:
|
|
kernel32.CloseHandle(ph)
|
|
except Exception:
|
|
pass
|
|
|
|
return locks
|
|
|
|
|
|
def close_remote_handle(pid: int, handle_value: int) -> bool:
|
|
"""
|
|
关闭目标进程中指定 handle(不结束进程)。
|
|
需要足够权限(通常管理员 + SeDebugPrivilege 才更稳)。
|
|
"""
|
|
_try_enable_debug_privilege()
|
|
ph = kernel32.OpenProcess(PROCESS_DUP_HANDLE, False, int(pid))
|
|
if not ph:
|
|
return False
|
|
try:
|
|
dup = wintypes.HANDLE()
|
|
ok = kernel32.DuplicateHandle(
|
|
ph,
|
|
wintypes.HANDLE(int(handle_value)),
|
|
kernel32.GetCurrentProcess(),
|
|
ctypes.byref(dup),
|
|
0,
|
|
False,
|
|
DUPLICATE_CLOSE_SOURCE,
|
|
)
|
|
if ok and dup:
|
|
kernel32.CloseHandle(dup)
|
|
return bool(ok)
|
|
finally:
|
|
kernel32.CloseHandle(ph)
|
|
|