以非特权用户身份查询 NTFS 特殊文件的元数据?

Query metadata for NTFS special files as unprivileged user?

如何从非特权用户上下文查询 NTFS 特殊文件的大小?

大小对我来说是最重要的元数据,但如果我能得到通常在 WIN32_FIND_DATA 中找到的所有内容,我不会介意。

我指的 NTFS 特殊文件是(除其他外):$Mft$MftMirr$LogFile$BadClus 等等。

为了打开 MFT,我必须获得某些权限,打开卷然后解析 MFT。这样就结束了。

而且似乎无法通过名称打开这些文件(对于其中的大多数),这排除了 NtQueryInformationFile()GetFileInformationByHandle()。或者可能有我没有尝试过的标志组合,是否可以以某种方式打开它们以查询文件信息?

最后但并非最不重要的一点是,在使用相应的 Win32 API(FindFirstFile() 等)、NtQueryDirectoryFile()IRP_MN_QUERY_DIRECTORY 时,我都不会返回这些文件直接。


是的,我知道我可以使用 FSCTL_GET_NTFS_VOLUME_DATA 有效地获取 MFT 的大小,但这只是这些特殊文件之一。

ntfs 卷上,我们可以枚举所有文件记录 FSCTL_GET_NTFS_FILE_RECORD. unfortunatelly format of FileRecordBuffer is undocumented/undeclared in windows headers。但这是常见的 ntfs 结构。缓冲区以 NTFS_RECORD_HEADER(基数 class)开头,之后是几条 NTFS_ATTRIBUTE 记录。部分和自定义定义:

union NTFS_FILE_ID 
{
    LONGLONG IndexNumber;

    struct  
    {
        LONGLONG MftRecordIndex : 48;
        LONGLONG SequenceNumber : 16;
    };
};

struct NTFS_RECORD_HEADER 
{
    enum {
        FILE = 'ELIF',
        INDX = 'XDNI',
        BAAD = 'DAAB',
        HOLE = 'ELOH',
        CHKD = 'DKHC'
    } Type;
    USHORT UsaOffset;
    USHORT UsaCount;
    USN Usn;
};

struct NTFS_FILE_RECORD_HEADER : public NTFS_RECORD_HEADER
{
    USHORT SequenceNumber;
    USHORT LinkCount;
    USHORT AttributesOffset;
    USHORT Flags;
    ULONG BytesInUse;
    ULONG BytesAllocated;
    ULONGLONG BaseFileRecord;
    USHORT NextAttributeNumber;

    enum{
        flgInUse = 1, flgDirectory = 2
    };
};

struct NTFS_ATTRIBUTE 
{
    enum ATTRIBUTE_TYPE {
        StandardInformation = 0x10,
        AttributeList = 0x20,
        FileName = 0x30,
        ObjectId = 0x40,
        SecurityDescriptor = 0x50,
        VolumeName = 0x60,
        VolumeInformation = 0x70,
        Data = 0x80,
        IndexRoot = 0x90,
        IndexAllocation = 0xa0,
        Bitmap = 0xb0,
        ReparsePoint = 0xc0,
        EAInformation = 0xd0,
        EA = 0xe0,
        PropertySet = 0xf0,
        LoggedUtilityStream = 0x100,
        StopTag = MAXDWORD
    } Type;
    ULONG Length;
    BOOLEAN Nonresident;
    UCHAR NameLength;
    USHORT NameOffset;
    USHORT Flags;// 1 = Compresed
    USHORT AttributeNumber;
};

struct NTFS_RESIDENT_ATTRIBUTE : public NTFS_ATTRIBUTE 
{
    ULONG ValueLength;
    USHORT ValueOffset;
    USHORT Flags;
};

struct NTFS_NONRESIDENT_ATTRIBUTE : public NTFS_ATTRIBUTE 
{
    LONGLONG LowVcn;
    LONGLONG HighVcn;
    USHORT RunArrayOffset;
    UCHAR CompressionUnit;
    UCHAR Unknown[5];
    LONGLONG AllocationSize;
    LONGLONG DataSize;
    LONGLONG InitializedSize;
    LONGLONG CompressedSize;
};

struct NTFS_ATTRIBUTE_LIST
{
    NTFS_ATTRIBUTE::ATTRIBUTE_TYPE Type;
    USHORT Length;
    UCHAR NameLength;
    UCHAR NameOffset;
    LONGLONG LowVcn;
    LONGLONG FileReferenceNumber : 48;
    LONGLONG FileReferenceNumber2 : 16;
    USHORT AttributeNumber;
    USHORT Unknown[3];
};

struct NTFS_STANDARD_ATTRIBUTE 
{
    LONGLONG CreationTime;
    LONGLONG ChangeTime;
    LONGLONG LastWriteTime;
    LONGLONG LastAccessTime;
    ULONG FileAttributes;
    ULONG Unknown[3];
    ULONG QuotaId;
    ULONG SecurityId;
    ULONGLONG QuotaChange;
    USN Usn;
};

struct NTFS_FILENAME_ATTRIBUTE
{
    NTFS_FILE_ID DirectoryId;
    LONGLONG CreationTime;
    LONGLONG ChangeTime;
    LONGLONG LastWriteTime;
    LONGLONG LastAccessTime;
    LONGLONG AllocationSize;
    LONGLONG DataSize;
    ULONG FileAttributes;
    ULONG EaSize;
    UCHAR FileNameLength;// in symbols !!
    UCHAR NameType;
    WCHAR FileName[];

    enum {
        systemName , longName, shortName, systemName2
    };
};

枚举所有文件的代码如下所示:

inline ULONG BOOL_TO_ERROR(BOOL f)
{
    return f ? NOERROR : GetLastError();
}

ULONG QFMD(PCWSTR szVolumeName)
{
    HANDLE hVolume = CreateFile(szVolumeName, FILE_GENERIC_READ, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);

    if (hVolume == INVALID_HANDLE_VALUE)
    {
        return GetLastError();
    }

    ULONG cb, BytesReturned;
    NTFS_VOLUME_DATA_BUFFER nvdb;

    ULONG err = BOOL_TO_ERROR(DeviceIoControl(hVolume, FSCTL_GET_NTFS_VOLUME_DATA, 0, 0, &nvdb, sizeof(nvdb), &BytesReturned, 0));

    if (err == NOERROR)
    {
        NTFS_FILE_RECORD_INPUT_BUFFER nfrib;

        cb = FIELD_OFFSET(NTFS_FILE_RECORD_OUTPUT_BUFFER, FileRecordBuffer[nvdb.BytesPerFileRecordSegment]);

        PNTFS_FILE_RECORD_OUTPUT_BUFFER pnfrob = (PNTFS_FILE_RECORD_OUTPUT_BUFFER)alloca(cb);

        // search for maximum valid FileReferenceNumber
        LONG a = 0, b = MAXLONG, o;
        do 
        {
            nfrib.FileReferenceNumber.QuadPart = o = (a + b) >> 1;

            err = BOOL_TO_ERROR(DeviceIoControl(hVolume, FSCTL_GET_NTFS_FILE_RECORD, 
                &nfrib, sizeof nfrib, pnfrob, cb, &BytesReturned, 0));

            err ? b = o : a = o + 1;

        } while(a < b);

        nfrib.FileReferenceNumber.QuadPart--;

        DbgPrint("MftRecordCount=%u\n", nfrib.FileReferenceNumber.LowPart);


        union {
            PVOID FileRecordBuffer;
            PBYTE pb;
            NTFS_RECORD_HEADER* pnrh;
            NTFS_FILE_RECORD_HEADER* pnfrh;
            NTFS_ATTRIBUTE* pna;
            NTFS_RESIDENT_ATTRIBUTE* pnra;
            NTFS_NONRESIDENT_ATTRIBUTE* pnaa;
        };

        NTFS_FILE_ID nfi;
        UNICODE_STRING us = { sizeof (nfi), sizeof (nfi), (PWSTR)&nfi };
        OBJECT_ATTRIBUTES oa = { sizeof(oa), hVolume, &us };

        do 
        {
            FileRecordBuffer = pnfrob->FileRecordBuffer;

            if (err = BOOL_TO_ERROR(DeviceIoControl(hVolume, FSCTL_GET_NTFS_FILE_RECORD, 
                &nfrib, sizeof nfrib, pnfrob, cb, &BytesReturned, 0)))
            {
                break;
            }

            // are really file
            if (
                pnrh->Type != NTFS_RECORD_HEADER::FILE ||
                !(pnfrh->Flags & NTFS_FILE_RECORD_HEADER::flgInUse) ||
                pnfrh->BaseFileRecord
                )
            {
                continue;
            }

            ULONG FileAttributes = INVALID_FILE_ATTRIBUTES;
            ULONGLONG FileSize = 0;

            nfi.MftRecordIndex = pnfrob->FileReferenceNumber.QuadPart;
            nfi.SequenceNumber = pnfrh->SequenceNumber;

            pb += pnfrh->AttributesOffset;

            for( ; ; ) 
            {
                NTFS_FILENAME_ATTRIBUTE* pnfa;
                NTFS_STANDARD_ATTRIBUTE* pnsa;

                switch (pna->Type)
                {
                case NTFS_ATTRIBUTE::StopTag:
                    goto __end;

                case NTFS_ATTRIBUTE::FileName:

                    pnfa = (NTFS_FILENAME_ATTRIBUTE*)RtlOffsetToPointer(pnra, pnra->ValueOffset);

                    if (pnfa->NameType == NTFS_FILENAME_ATTRIBUTE::longName)
                    {
                        //DbgPrint("<< %.*S\n", pnfa->FileNameLength, pnfa->FileName);
                    }
                    break;

                case NTFS_ATTRIBUTE::StandardInformation:

                    pnsa = (NTFS_STANDARD_ATTRIBUTE*)RtlOffsetToPointer(pnra, pnra->ValueOffset);
                    FileAttributes = pnsa->FileAttributes;
                    break;

                case NTFS_ATTRIBUTE::Data:
                    FileSize += pna->Nonresident ? pnaa->DataSize : pnra->ValueLength;
                    break;
                }

                pb += pna->Length;
            }

__end:;

            //HANDLE hFile;
            //IO_STATUS_BLOCK iosb;

            //NTSTATUS status = NtOpenFile(&hFile, FILE_READ_ATTRIBUTES, &oa, &iosb, FILE_SHARE_VALID_FLAGS,
            //  FILE_OPEN_REPARSE_POINT| FILE_OPEN_BY_FILE_ID | FILE_OPEN_FOR_BACKUP_INTENT);

            //if (0 <= status)
            //{
            //  NtClose(hFile);
            //}


        } while (0 <= (nfrib.FileReferenceNumber.QuadPart = pnfrob->FileReferenceNumber.QuadPart - 1));

    }

    CloseHandle(hVolume);

    return err;
}

一些NTFS System Files,但是这个列表已经很旧了,存在更多的系统文件。如果要查询具体的系统文件需要将其编号分配给NTFS_FILE_RECORD_INPUT_BUFFER。仅查询 sys 文件的代码变化不大:

ULONG QFMD(PCWSTR szVolumeName)
{
    HANDLE hVolume = CreateFile(szVolumeName, FILE_GENERIC_READ, FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);

    if (hVolume == INVALID_HANDLE_VALUE)
    {
        return GetLastError();
    }

    ULONG cb, BytesReturned;
    NTFS_VOLUME_DATA_BUFFER nvdb;

    ULONG err = BOOL_TO_ERROR(DeviceIoControl(hVolume, FSCTL_GET_NTFS_VOLUME_DATA, 0, 0, &nvdb, sizeof(nvdb), &BytesReturned, 0));

    if (err == NOERROR)
    {
        NTFS_FILE_RECORD_INPUT_BUFFER nfrib;

        nfrib.FileReferenceNumber.QuadPart = 0x30;

        cb = FIELD_OFFSET(NTFS_FILE_RECORD_OUTPUT_BUFFER, FileRecordBuffer[nvdb.BytesPerFileRecordSegment]);

        PNTFS_FILE_RECORD_OUTPUT_BUFFER pnfrob = (PNTFS_FILE_RECORD_OUTPUT_BUFFER)alloca(cb);

        union {
            PVOID FileRecordBuffer;
            PBYTE pb;
            NTFS_RECORD_HEADER* pnrh;
            NTFS_FILE_RECORD_HEADER* pnfrh;
            NTFS_ATTRIBUTE* pna;
            NTFS_RESIDENT_ATTRIBUTE* pnra;
            NTFS_NONRESIDENT_ATTRIBUTE* pnaa;
        };

        NTFS_FILE_ID nfi;
        UNICODE_STRING us = { sizeof (nfi), sizeof (nfi), (PWSTR)&nfi };
        OBJECT_ATTRIBUTES oa = { sizeof(oa), hVolume, &us };

        do 
        {
            FileRecordBuffer = pnfrob->FileRecordBuffer;

            if (err = BOOL_TO_ERROR(DeviceIoControl(hVolume, FSCTL_GET_NTFS_FILE_RECORD, 
                &nfrib, sizeof nfrib, pnfrob, cb, &BytesReturned, 0)))
            {
                break;
            }

            // are really file
            if (
                pnrh->Type != NTFS_RECORD_HEADER::FILE ||
                !(pnfrh->Flags & NTFS_FILE_RECORD_HEADER::flgInUse) ||
                pnfrh->BaseFileRecord
                )
            {
                continue;
            }

            ULONG FileAttributes = INVALID_FILE_ATTRIBUTES;
            ULONGLONG FileSize = 0;
            PCWSTR ShortName = 0, LongName = 0, SystemName = 0;
            UCHAR ShortNameLength = 0, LongNameLength = 0, SystemNameLength = 0;

            nfi.MftRecordIndex = pnfrob->FileReferenceNumber.QuadPart;
            nfi.SequenceNumber = pnfrh->SequenceNumber;

            pb += pnfrh->AttributesOffset;

            BOOL bSysFile = FALSE;

            for( ; ; ) 
            {
                union {
                    NTFS_FILENAME_ATTRIBUTE* pnfa;
                    NTFS_STANDARD_ATTRIBUTE* pnsa;
                };

                switch (pna->Type)
                {
                case NTFS_ATTRIBUTE::StopTag:
                    goto __end;

                case NTFS_ATTRIBUTE::FileName:

                    pnfa = (NTFS_FILENAME_ATTRIBUTE*)RtlOffsetToPointer(pnra, pnra->ValueOffset);

                    switch (pnfa->NameType)
                    {
                    case NTFS_FILENAME_ATTRIBUTE::systemName:
                    case NTFS_FILENAME_ATTRIBUTE::systemName2:
                        bSysFile = TRUE;
                        SystemName = pnfa->FileName, SystemNameLength = pnfa->FileNameLength;
                        break;
                    case NTFS_FILENAME_ATTRIBUTE::longName:
                        LongName = pnfa->FileName, LongNameLength = pnfa->FileNameLength;
                        break;
                    case NTFS_FILENAME_ATTRIBUTE::shortName:
                        ShortName = pnfa->FileName, ShortNameLength = pnfa->FileNameLength;
                        break;
                    }
                    break;

                case NTFS_ATTRIBUTE::StandardInformation:

                    pnsa = (NTFS_STANDARD_ATTRIBUTE*)RtlOffsetToPointer(pnra, pnra->ValueOffset);
                    FileAttributes = pnsa->FileAttributes;
                    break;

                case NTFS_ATTRIBUTE::Data:
                    FileSize += pna->Nonresident ? pnaa->DataSize : pnra->ValueLength;
                    break;
                }

                pb += pna->Length;
            }

__end:;

            if (bSysFile)
            {
                HANDLE hFile;
                IO_STATUS_BLOCK iosb;

                NTSTATUS status = NtOpenFile(&hFile, FILE_READ_ATTRIBUTES, &oa, &iosb, FILE_SHARE_VALID_FLAGS,
                    FILE_OPEN_REPARSE_POINT| FILE_OPEN_BY_FILE_ID | FILE_OPEN_FOR_BACKUP_INTENT);

                if (0 <= status)
                {
                    NtClose(hFile);
                }

                char sz[32];
                StrFormatByteSize64A(FileSize, sz, RTL_NUMBER_OF(sz));
                DbgPrint("%I64u: %08x %s [%x] %.*S\n", pnfrob->FileReferenceNumber.QuadPart, 
                    FileAttributes, sz, status, SystemNameLength, SystemName);
            }


        } while (0 <= (nfrib.FileReferenceNumber.QuadPart = pnfrob->FileReferenceNumber.QuadPart - 1));

    }

    CloseHandle(hVolume);

    return err;
}

有了它我得到了下一个结果:

38: 10000006 0 bytes [0] $Deleted
34: 00000020 10.0 MB [0] $TxfLogContainer00000000000000000002
33: 00000020 10.0 MB [0] $TxfLogContainer00000000000000000001
32: 00000020 64.0 KB [0] $TxfLog.blf
31: 00000026 1.00 MB [0] $Tops
30: 80000006 0 bytes [0] $Txf
29: 00000006 0 bytes [0] $TxfLog
28: 00000026 27.0 MB [0] $Repair
27: 00000006 0 bytes [0] $RmMetadata
26: 20000026 0 bytes [c0000034] $Reparse
25: 20000026 0 bytes [c0000034] $ObjId
24: 20000026 0 bytes [c0000034] $Quota
11: 00000006 0 bytes [0] $Extend
10: 00000006 128 KB [0] $UpCase
9: 20000006 0 bytes [c0000034] $Secure
8: 00000006 237 GB [c0000022] $BadClus
7: 00000006 8.00 KB [c0000022] $Boot
6: 00000006 7.42 MB [c0000022] $Bitmap
5: 00000806 0 bytes [0] .
4: 00000006 2.50 KB [0] $AttrDef
3: 00000006 0 bytes [0] $Volume
2: 00000006 64.0 MB [c0000022] $LogFile
1: 00000006 4.00 KB [0] $MFTMirr
0: 00000006 212 MB [0] $MFT

是的,可以使用 DeviceIoControl / FSCTL_GET_NTFS_FILE_RECORD 阅读 $MFT 而无需提升。在 this page 的帮助下,我已经制定了最低设置。请注意该页底部的最后几段。

  1. 组策略gpedit.msc中,将您运行所在的(非提升的)用户帐户添加到遵循以下政策:

    Windows Settings/Security Settings/Local Policies/User Rights Assignment/...
    Perform Volume Maintenance tasks     (definitely needed)

    Windows Settings/Security Settings/Local Policies/User Rights Assignment/...
    Back up files and directories     (not sure if this is essential)

  2. 到目前为止,我不需要以下内容,但请记下它以备您需要返回时使用:

    Windows Settings/Security Settings/Local Policies/User Rights Assignment/...
    Restore files and directories

  3. 运行 gpupdate.exe 从 Windows 命令提示符,或等待大约 15 分钟让组策略更改生效。

  4. 这些更改允许您的用户帐户获得权限。作为一次性步骤,每次您的应用程序启动时,您都必须明确调整您的令牌。这是 Win32 API AdjustTokenPrivileges:

  5. 的独立版本
using System;
using System.Runtime.InteropServices;
using System.Security;
using System.ComponentModel;

[SuppressUnmanagedCodeSecurity]
public static class AdjPriv
{
    [DllImport("kernel32.dll")]
    static extern IntPtr GetCurrentProcess();

    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool OpenProcessToken(IntPtr h, int acc, out IntPtr phtok);

    [DllImport("advapi32.dll", SetLastError = true, ExactSpelling = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool LookupPrivilegeValueW(IntPtr host, [MarshalAs(UnmanagedType.LPWStr)] String name, out long pluid);

    [DllImport("advapi32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, in TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);

    const int
        SE_PRIVILEGE_ENABLED    /**/ = 0x00000002,
        TOKEN_QUERY             /**/ = 0x00000008,
        TOKEN_ADJUST_PRIVILEGES /**/ = 0x00000020,
        ERROR_NOT_ALL_ASSIGNED  /**/ = 0x00000514;

    [StructLayout(LayoutKind.Sequential, Pack = 4)]
    struct TokPriv1Luid
    {
        public int Count;
        public long Luid;
        public int Attr;
    };

    public static bool SetPrivilege(String szSe)
    {
        if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out IntPtr htok))
            goto _error;

        var tp = new TokPriv1Luid { Count = 1, Attr = SE_PRIVILEGE_ENABLED };

        if (!LookupPrivilegeValueW(IntPtr.Zero, szSe, out tp.Luid))
            goto _error;

        if (!AdjustTokenPrivileges(htok, false, in tp, 0, IntPtr.Zero, IntPtr.Zero))
            goto _error;

        return Marshal.GetLastWin32Error() != ERROR_NOT_ALL_ASSIGNED;

    _error:
        throw new Win32Exception();
    }
};
  1. 通过调用上面显示的 AdjPriv.SetPrivilege 实用函数,在应用程序启动时为当前用户帐户授予 "SeManageVolumePrivilege" 权限。为您可能还想添加的每个额外权限调用一次。
static MyProgram()
{
    if (!AdjPriv.SetPrivilege("SeManageVolumePrivilege"))
        throw new SecurityException();

    /// etc...
}
  1. 现在是代码。我不会详细介绍 p/Invoke 因为每个人都有自己的方式。我将只显示传递给两个关键的两个 API 调用的确切标志和常量值,以便在不提示提升的情况下工作。
IntPtr h = CreateFileW(@"\?\Volume{c2655473-adc2-4fe3-99a0-77d5bb1b809f}\",
                       FILE_ACCESS_READ_CONTROL,   // 0x00020000
                       FILE_SHARE_ANY,             // 7
                       IntPtr.Zero,
                       CREATE_MODE_OPEN_EXISTING,  // 3
                       FILE_FLAG_BACKUP_SEMANTICS, // 0x02000000
                       IntPtr.Zero);
  1. 然后终于...
/// <summary>
/// Given a 48-bit MFT index 'frn', recover the current "sequence number" of the file, which
/// can be used as the upper 16-bits to complete a usable FILE_REFERENCE (NTFS FileId):
/// </summary>
var frn = (FILE_REFERENCE)0x_0000_000000000218;
                    //       ^^^^----- ???

// lookup proceeds downwards, so set the sought-after upper 16 bits to max. seq. value
frn.Seq = 0xFFFF;

if (!DeviceIoControl(h, FSCTL_GET_NTFS_FILE_RECORD, in frn, out FILE_RECORD_OUTPUT_BUFFER rec) ||
    frn.Index != rec.file_ref.Index)
    throw new Win32Exception();

frn.Seq = rec.frh.SequenceNumber;
                                          //    vvvv--- !!!
Console.WriteLine($"0x{frn:X16}");        // 0x_0092_000000000218

备注:

有效!好吧,至少对我来说 Windows 10,版本 10.0.18362.387。请注意 CreateFileWVolume Guid 路径 末尾的反斜杠 ...\。没有它,代码将无法工作。