Query metadata for NTFS special files as unprivileged user?

Viewed 633

From an unprivileged user context, how can I query the size of the NTFS special files?

The size is the most important piece of metadata for me, but if I could get everything that is typically found in WIN32_FIND_DATA I would not mind it.

The NTFS special files I mean are (among others): $Mft, $MftMirr, $LogFile, $BadClus et cetera.

In order to open the MFT, I'd have to acquire certain privileges, open the volume and then parse the MFT. So that's out.

Also it seems to be impossible to open these files by name (for the majority of them), which precludes NtQueryInformationFile() and GetFileInformationByHandle(). Or maybe there is a combination of flags I haven't tried and it is possible to open them somehow for querying the file information?

Last but not least I don't get these files returned when using the respective Win32 APIs (FindFirstFile() et. al.), nor with NtQueryDirectoryFile() nor by using IRP_MN_QUERY_DIRECTORY directly.


Yes, I understand that I can effectively get the size of the MFT using FSCTL_GET_NTFS_VOLUME_DATA, but that's just one of these special files.

2 Answers

on ntfs volume we can enumerate all file records with FSCTL_GET_NTFS_FILE_RECORD. unfortunatelly format of FileRecordBuffer is undocumented/undeclared in windows headers. but this is common ntfs structs. buffer begin with NTFS_RECORD_HEADER (base class) after which will be several NTFS_ATTRIBUTE records. partial and custom definitions:

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
    };
};

the code of enumeration all files can look like:

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;
}

some NTFS System Files, but this list already old, exist more system files. if want concrete system file query need assign it number to NTFS_FILE_RECORD_INPUT_BUFFER. little changed code for query sys files only:

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;
}

with it i got next result:

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

Yes, it is possible to use DeviceIoControl / FSCTL_GET_NTFS_FILE_RECORD to read the $MFT without elevation. With the help of this page, I have worked out the minimum settings. Note the last few paragraphs at the bottom of that page.

  1. in Group Policy gpedit.msc, add the (non-elevated) user account you'll be running under to the following to the following policies:

    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. So far, I haven't needed the following, but make a note of it in case you need to come back to it:

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

  3. Run gpupdate.exe from a Windows command prompt, or wait about 15 minutes for the group policy changes to take effect.

  4. Those changes allow your user account to acquire the privileges. As a one-time step every time your app starts, you'll have to explicitly adjust your token. Here's a standalone version of the Win32 API AdjustTokenPrivileges:

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. Give the current user account the "SeManageVolumePrivilege" privilege when the app starts up by calling the AdjPriv.SetPrivilege utility function shown above. Call it once for each additional privilege you may also want to add.
static MyProgram()
{
    if (!AdjPriv.SetPrivilege("SeManageVolumePrivilege"))
        throw new SecurityException();

    /// etc...
}
  1. And now for the code. I won't go into detail with the p/Invoke since everyone has their own way of doing it. I'll just show the exact flags and constants values passed into the two critical two API calls in order to work without prompting for elevation.
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. And then finally...
/// <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

Notes:

It works! Well at least for me on Windows 10, Version 10.0.18362.387. Please notice the backslash ...\ at the end of the Volume Guid path in CreateFileW. The code will not work without it.

Related