磁盘上有个目录,是 Linux 备份数据时生成的,现在不用了要删除,屡屡失败,开始以为是权限问题,最后发现是目录/文件名中存在半角英文冒号导致的。由于冒号在 Windows 下,在路径中通常用于盘符识别,所以出现在目录或者文件名中是非法的。但在命令提示符下,或者资源管理器里,只是无法操作,而看上去都很正常,难道没有什么办法迂回一下?
既然是 Linux 的锅,那在 Windows 下的第一反应显然是 WSL。到 WSL 下一看,它连原生的 Windows 环境都不如,连是个目录还是文件都辨识不出来,属性列里一串问号。很明显,它作为一个子系统,向核心系统检索信息就没能够收到正常反馈。
接下来把希望寄托在 native 层面的接口上。Windows 层的 API,删除一个空目录是 RemoveDirectory
,多年不在 Windows 环境下编程了,一开始想当然存在一个 NtRemoveDirectory
的 native 方法,然而并没有。于是把目光转向 NtDeleteFile
,为了短平快,在网上搜到以下代码(略有修改):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
#include <windows.h> #include <stdio.h> // typedef ULONG * ULONG_PTR; typedef LONG NTSTATUS, * PNTSTATUS; typedef enum _FILE_INFORMATION_CLASS { FileDirectoryInformation = 1, FileFullDirectoryInformation, FileBothDirectoryInformation, FileBasicInformation, FileStandardInformation, FileInternalInformation, FileEaInformation, FileAccessInformation, FileNameInformation, FileRenameInformation, FileLinkInformation, FileNamesInformation, FileDispositionInformation, FilePositionInformation, FileFullEaInformation, FileModeInformation, FileAlignmentInformation, FileAllInformation, FileAllocationInformation, FileEndOfFileInformation, FileAlternateNameInformation, FileStreamInformation, FilePipeInformation, FilePipeLocalInformation, FilePipeRemoteInformation, FileMailslotQueryInformation, FileMailslotSetInformation, FileCompressionInformation, FileObjectIdInformation, FileCompletionInformation, FileMoveClusterInformation, FileQuotaInformation, FileReparsePointInformation, FileNetworkOpenInformation, FileAttributeTagInformation, FileTrackingInformation, FileIdBothDirectoryInformation, FileIdFullDirectoryInformation, FileValidDataLengthInformation, FileShortNameInformation, FileIoCompletionNotificationInformation, FileIoStatusBlockRangeInformation, FileIoPriorityHintInformation, FileSfioReserveInformation, FileSfioVolumeInformation, FileHardLinkInformation, FileProcessIdsUsingFileInformation, FileNormalizedNameInformation, FileNetworkPhysicalNameInformation, FileIdGlobalTxDirectoryInformation, FileIsRemoteDeviceInformation, FileAttributeCacheInformation, FileNumaNodeInformation, FileStandardLinkInformation, FileRemoteProtocolInformation, FileMaximumInformation } FILE_INFORMATION_CLASS, * PFILE_INFORMATION_CLASS; typedef struct _IO_STATUS_BLOCK { union { NTSTATUS Status; PVOID Pointer; }; ULONG_PTR Information; } IO_STATUS_BLOCK, * PIO_STATUS_BLOCK; typedef struct _FILE_DISPOSITION_INFORMATION { BOOLEAN DeleteFile; } FILE_DISPOSITION_INFORMATION, * PFILE_DISPOSITION_INFORMATION; typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, * PUNICODE_STRING; // // Valid values for the Attributes field // #define OBJ_INHERIT 0x00000002L #define OBJ_PERMANENT 0x00000010L #define OBJ_EXCLUSIVE 0x00000020L #define OBJ_CASE_INSENSITIVE 0x00000040L #define OBJ_OPENIF 0x00000080L #define OBJ_OPENLINK 0x00000100L #define OBJ_KERNEL_HANDLE 0x00000200L #define OBJ_FORCE_ACCESS_CHECK 0x00000400L #define OBJ_VALID_ATTRIBUTES 0x000007F2L typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; PUNICODE_STRING ObjectName; ULONG Attributes; PVOID SecurityDescriptor; PVOID SecurityQualityOfService; } OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES; #define InitializeObjectAttributes( p, n, a, r, s ) { \ (p)->Length = sizeof( OBJECT_ATTRIBUTES ); \ (p)->RootDirectory = r; \ (p)->Attributes = a; \ (p)->ObjectName = n; \ (p)->SecurityDescriptor = s; \ (p)->SecurityQualityOfService = NULL; \ } NTSTATUS(__stdcall* pf_NtSetInformationFile)( HANDLE FileHandle, PIO_STATUS_BLOCK IoStatusBlock, PVOID FileInformation, ULONG Length, FILE_INFORMATION_CLASS FileInformationClass ); NTSTATUS(__stdcall* pf_NtDeleteFile)( POBJECT_ATTRIBUTES ObjectAttributes ); VOID(__stdcall* pf_RtlInitUnicodeString)( PUNICODE_STRING DestinationString, PCWSTR SourceString ); int main(int argc, char** argv) { FILE_DISPOSITION_INFORMATION fi = { 1 }; IO_STATUS_BLOCK bs = { 0 }; OBJECT_ATTRIBUTES ob; UNICODE_STRING str; HANDLE hfile; pf_NtSetInformationFile = (NTSTATUS(__stdcall*)( HANDLE, PIO_STATUS_BLOCK, PVOID, ULONG, FILE_INFORMATION_CLASS))GetProcAddress(LoadLibrary(L"ntdll.dll"), "NtSetInformationFile"); pf_NtDeleteFile = (NTSTATUS(__stdcall*)(POBJECT_ATTRIBUTES)) GetProcAddress(LoadLibrary(L"ntdll.dll"), "NtDeleteFile"); pf_RtlInitUnicodeString = (VOID(__stdcall*)(PUNICODE_STRING, PCWSTR)) GetProcAddress(LoadLibrary(L"ntdll.dll"), "RtlInitUnicodeString"); pf_RtlInitUnicodeString(&str, L"C:\\UserName\\...\\.Fabric\\com.crashlytics.sdk.android:answers"); InitializeObjectAttributes(&ob, &str, OBJ_CASE_INSENSITIVE, NULL, NULL); NTSTATUS s = pf_NtDeleteFile(&ob); // delete a file or an empty directory printf("0x%x", s); // 0xC0000033 - STATUS_OBJECT_NAME_INVALID; if "\\\\?\\" or "\\\\.\\" prefixed // 0xC0000034 - STATUS_OBJECT_NAME_NOT_FOUND; if "\\??\\" prefixed // 0xC000003B - STATUS_OBJECT_PATH_SYNTAX_BAD; if nothing prefixed return 0; } |
上述代码使用 Visual Studio 2022 编译为 64 位程序通过。首先用一个正常文件和一个正常目录,证实了 NtDeleteFile
确实可以工作于这两者,然后才针对哪个带冒号的目录进行操作,答案是“不行”,当然,在代码中,唯一正确的路径格式是加上“\??\”前缀的那个。
最后的尝试,是使用工具打开硬盘,找到该目录的元信息所在,把冒号改称为其他合法字符,然后尝试删除。运行 HxD,时间关系,没有再去学习如何在磁盘上定位分析 $MFT
文件,启用暴力搜索,找到了位置,但再改后无法保存,提示访问拒绝,熟悉的错误码 5。
这几个目录的后续命运,随缘。如果我哪天启动到了 Linux 而又恰巧想起了这个事,我就会顺手把它们删掉,如果它们运气好,就还可以拖延一阵,以待我在 Windows 下看还有没有别的什么办法来尝试对付它们。