Ian Boyd
Ian Boyd

Reputation: 256711

How to call NtOpenFile?

I'm trying to call NtOpenFile, and it's failing with error:

STATUS_OBJECT_PATH_SYNTAX_BAD = NTSTATUS($C000003B);

Object Path Component was not a directory object.

The basic gist is:

//The file we'll test with
filename: UnicodeString := 'C:\Windows\Explorer.exe'; //23 characters

//Convert the filename to counted UNICODE_STRING
cs: UNICODE_STRING;
cs.Length        := Length(filename) * sizeof(WideChar); //46 bytes
cs.MaximumLength := cs.Length + 2;  //48 bytes
cs.Buffer        := PWideChar(Filename);  //"C:\Windows\Explorer.exe"

//Define the OBJECT_ATTRIBUTES
oa: OBJECT_ATTRIBUTES := Default(OBJECT_ATTRIBUTES);
oa.Length := sizeof(OBJECT_ATTRIBUTES); //24 bytes
oa.Attributes := OBJ_CASE_INSENSITIVE;
oa.ObjectName := @cs; //UNICODE_STRING

//Open the file (by Object Attributes) and get a file handle
hFile: THandle;
iosb: IO_STATUS_BLOCK;

status: NTSTATUS := NtOpenFile(@hFile, FILE_READ_ATTRIBUTES, @oa, @iosb, FILE_SHARE_READ, 0);

What am I doing wrong?

Basic gist (C#-style psuedocode)

//The file we'll test with
UnicodeString filename = "C:\Windows\Explorer.exe"; //23 characters

//Convert the filename to counted UNICODE_STRING
UNICODE_STRING cs;
cs.Length        = Length(filename) * sizeof(WideChar); //46 bytes
cs.MaximumLength = cs.Length + 2;  //48 bytes
cs.Buffer        = Filename;  //"C:\Windows\Explorer.exe"

//Define the OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTES oa = Default(OBJECT_ATTRIBUTES);
oa.Length = sizeof(OBJECT_ATTRIBUTES); //24 bytes
oa.Attributes = OBJ_CASE_INSENSITIVE;
oa.ObjectName = cs; //UNICODE_STRING

//Open the file (by Object Attributes) and get a file handle
THandle hFile;
IO_STATUS_BLOCK iosb;

NTSTATUS status = NtOpenFile(out hFile, FILE_READ_ATTRIBUTES, ref oa, out iosb, FILE_SHARE_READ, 0);

Other styles of filenames

Filename Result Description
"C:\Windows\Explorer.exe" STATUS_OBJECT_PATH_SYNTAX_BAD Object Path Component
was not a directory object.
"\global??\C:\Windows\Explorer.exe" 0xC0000033 Object Name invalid
"\??\C:\Windows\Explorer.exe" 0xC0000033 Object Name invalid

OBJECT_ATTRIBUTES raw memory dump

Because there can be issues of memory layout, alignment, padding, and missing members, lets dump the raw oa memory:

18 00 00 00                 ;Length. 0x00000018 = 24 bytes (sizeof)
00 00 00 00                 ;RootDirectory. 0x00000000 = NULL
28 FF 19 00                 ;ObjectName. PUNICODE_STRING 0x0019FF28
40 00 00 00                 ;Attributes. (0x00000040 = OBJ_CASE_INSENSITIVE)
00 00 00 00                 ;SecurityDescriptor 0x0000000 = NULL
00 00 00 00                 ;SecurityQualityOfService 0x00000000 = NULL

**0x0019FF28:**

    3C FF 19 00             ;PUNICODE_STRING 0x0019FF3C

**0x0019FF3C**

36 00                      ; String length in bytes 0x0036 
38 00                      ; Buffer size in bytes 0x0038
E8 B6 4E 00                ; PWideChar 0x004EB6E8 ==> "C:\Windows\Explorer.exe"

Which, now I see my problem: PUNICODE_STRING -> PUNICODE_STRING.

CMRE

program NtOpenFileDemo;

{$APPTYPE CONSOLE}

{$R *.res}
{$ALIGN 8}
{$MINENUMSIZE 4}

uses
  SysUtils, Windows, ComObj;

type
    NTSTATUS = Cardinal;
const
    STATUS_SUCCESS = 0;

type
    UNICODE_STRING = packed record
        Length: Word;         // Length of the string, in bytes (not including the null terminator)
        MaximumLength: Word;  // Size of the buffer, in bytes
        Buffer: PWideChar;    //really a PWideChar
    end;
    PUNICODE_STRING = ^UNICODE_STRING;

type
    IO_STATUS_BLOCK = packed record
        Status: NTSTATUS;
        Information: Pointer;
    end;
    PIO_STATUS_BLOCK = ^IO_STATUS_BLOCK;


    OBJECT_ATTRIBUTES = packed record
        Length: Cardinal;
        RootDirectory: THandle;
        ObjectName: PUNICODE_STRING;
        Attributes: Cardinal;
        SecurityDescriptor: Pointer;
        SecurityQualityOfService: Pointer;
    end;
    POBJECT_ATTRIBUTES = ^OBJECT_ATTRIBUTES;

const
// Define share access rights to files and directories
    FILE_SHARE_READ         = $00000001;
    FILE_SHARE_WRITE            = $00000002;
    FILE_SHARE_DELETE       = $00000004;
    FILE_SHARE_VALID_FLAGS  = $00000007;


// Valid values for the Attributes field
const
    OBJ_INHERIT          = $00000002;
    OBJ_PERMANENT        = $00000010;
    OBJ_EXCLUSIVE        = $00000020;
    OBJ_CASE_INSENSITIVE = $00000040;
    OBJ_OPENIF           = $00000080;
    OBJ_OPENLINK         = $00000100;
    OBJ_KERNEL_HANDLE    = $00000200;
    OBJ_VALID_ATTRIBUTES = $000003F2;


function NtOpenFile(FileHandle: PHandle; DesiredAccess: ACCESS_MASK; ObjectAttributes: POBJECT_ATTRIBUTES;
     IoStatusBlock: PIO_STATUS_BLOCK; ShareAccess: DWORD; OpenOptions: DWORD): NTSTATUS; stdcall; external 'ntdll.dll';

function  NtClose(Handle: THandle): NTSTATUS; stdcall; external 'ntdll.dll';


function FormatNTStatusMessage(const NTStatusMessage: NTSTATUS): string;
var
    Buffer: PChar;
    Len: Integer;
    hMod: HMODULE;

    function MAKELANGID(p, s: WORD): WORD;
    begin
      Result := WORD(s shl 10) or p;
    end;
begin
    {
        KB259693: How to translate NTSTATUS error codes to message strings
        Let the OS initialize the Buffer variable. Need to LocalFree it afterward.
    }
    hMod := SafeLoadLibrary('ntdll.dll');

    Len := FormatMessage(
            FORMAT_MESSAGE_ALLOCATE_BUFFER or
            FORMAT_MESSAGE_FROM_SYSTEM or
//          FORMAT_MESSAGE_IGNORE_INSERTS or
//          FORMAT_MESSAGE_ARGUMENT_ARRAY or
            FORMAT_MESSAGE_FROM_HMODULE,
            Pointer(hMod),
            NTStatusMessage,
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
            @Buffer, 0, nil);
    try
        //Remove the undesired line breaks and '.' char
        while (Len > 0) and (CharInSet(Buffer[Len - 1], [#0..#32, '.'])) do Dec(Len);
        //Convert to Delphi string
        SetString(Result, Buffer, Len);
    finally
        //Free the OS allocated memory block
        LocalFree(HLOCAL(Buffer));
    end;
    FreeLibrary(hMod);
end;


procedure TestCase;
var
    filename: UnicodeString;
    cs: PUNICODE_STRING;
    hFile: THandle;
    oa: OBJECT_ATTRIBUTES;
    iosb: IO_STATUS_BLOCK;
    status: NTSTATUS;
begin
    filename := 'C:\Windows\Explorer.exe'; //23 characters

    {
        Convert the filename to an "Object Attributes" structure

            OBJECT_ATTRIBUTES.Length <-- 24
            OBJECT_ATTRIBUTES.Attributes <-- OBJ_CASE_INSENSITIVE
            OBJECT_ATTRIBUTES.ObjectName.Length <-- 46
            OBJECT_ATTRIBUTES.ObjectName.MaximumLength <-- 48
            OBJECT_ATTRIBUTES.ObjectName.Buffer <-- "C:\Windows\Explorer.exe"
    }
    cs.Length := Length(Filename) * sizeof(WideChar);
    cs.MaximumLength := cs.Length + 2; //the null terminator
    cs.Buffer := PWideChar(Filename);

    oa := Default(OBJECT_ATTRIBUTES);
    oa.Length := sizeof(OBJECT_ATTRIBUTES);
    oa.Attributes := OBJ_CASE_INSENSITIVE;
    oa.ObjectName := @cs;

    //Open the file (by Object Attributes) and get a file handle
    status := NtOpenFile(@hFile, FILE_READ_ATTRIBUTES, @oa, @iosb, FILE_SHARE_READ, 0);
    if status <> STATUS_SUCCESS  then
    begin
        WriteLn('Error opening file "'+Filename+'": '+FormatNTStatusMessage(status)+' (0x'+IntToHex(status, 8)+')');
        Exit;
    end;
    try
        WriteLn('Successfully opened file');
    finally
        NtClose(hFile);
    end;
end;



begin
    try
        TestCase;
        WriteLn('Press enter to close...');
        ReadLn;
    except
        on E: Exception do
            Writeln(E.ClassName, ': ', E.Message);
    end;
end.

Bonus Reading

Upvotes: 6

Views: 2454

Answers (1)

Ian Boyd
Ian Boyd

Reputation: 256711

Found it.

Two things:

  1. I didn't know "NT Paths" are different from "DOS Paths"

"C:\Windows\Notepad.exe""\??\C:\Windows\Notepad.exe"

  1. To OBJECT_ATTRIBUTES.ObjectName I was assigning @PUNICODE_STRING, rather than @UNICODE_STRING

Use Case - Processes using file

I was using NtOpenFile in order to get the Process IDs that are using a file.

The short version is:

  • call NtOpenFile to open the file you're interested in
  • call NtQueryInformationFile with the FileProcessIdsUsingFileInformation constant
  • iterate each returned Process ID (pid)
  • call OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid) to get the process name
function GetProcessIDsUsingFile(Filename: UnicodeString; out ProcessIDs: array of DWORD): string;
var
    hFile: THandle;
    oa: OBJECT_ATTRIBUTES;
    iosb: TIOStatusBlock;
    status: NTSTATUS;
    fi: FILE_PROCESS_IDS_USING_FILE_INFORMATION;
    i: Integer;
    pid: DWORD;
    hProcess: THandle;
    s: string;
    dw: DWORD;
    cs: UNICODE_STRING;
    ntFilename: UnicodeString;
const
    PROCESS_QUERY_LIMITED_INFORMATION = $1000;
begin
    Result := '';

(*
    Decription

        Get list of processes that are using a file.

    Sample usage

        GetProcessIDsUsingFile('C:\Windows\Notepad.exe', {out}pids);

    Returns

        Result: a text description of the processes using the file

            "PID:12345 (Explorer.exe)"

        ProcessIDs: an array of process IDs (PIDs)

            [12345]
*)


    ntFilename := Filename;
{
    Convert the "DOS path" into an "NT Path". NT Paths are not the same as DOS paths.

        DOS Path:   C:\Windows\Explorer.exe
        NT Path:    \??\C:\Windows\Explorer.exe

    The short version is:

        \DosDevices         is a symbolic link for    \??
        \??                 is a special value understood by the object manager's parser to mean the global \DosDevices directory
        \GLOBAL??           is a folder that is home to global device.

    And in the end all 3 (eventually) end up in the same place.

    But you want to use "??" rather than "\GLOBAL??" because the former understands per-user (i.e. local) drive mappings.

    Bonus Reading:
        - Google Project Zero: The Definitive Guide on Win32 to NT Path Conversion (Google Project Zero)
                    https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html
        - Google Project Zero: Windows Drivers are True’ly Tricky
                    https://googleprojectzero.blogspot.com/2015/10/windows-drivers-are-truely-tricky.html
        - Nynaeve: The kernel object namespace and Win32, part 3
                    http://www.nynaeve.net/?p=92

    Apparently you can also use some undocumented functions (exported by name) to convert a DOS path to an NT path; there are like 7 variations

            - RtlGetFullPathName_U
            - RtlDosPathNameToRelativeNtPathName_U
            - RtlDosPathNameToNtPathName_U_WithStatus

    But i'll just prefix it with "\??\"
}
    if Copy(ntFilename, 1, 4) <> '\??\' then
        ntFilename := '\??\'+Filename;

{
    Convert the filename to an "Object Attributes" structure

        OBJECT_ATTRIBUTES.Length <-- 24
        OBJECT_ATTRIBUTES.Attributes <-- OBJ_CASE_INSENSITIVE
        OBJECT_ATTRIBUTES.ObjectName.Length <-- 46
        OBJECT_ATTRIBUTES.ObjectName.MaximumLength <-- 48
        OBJECT_ATTRIBUTES.ObjectName.Buffer <-- "C:\Windows\Explorer.exe"
}
    cs.Length := Length(ntFilename) * sizeof(WideChar);
    cs.MaximumLength := cs.Length + 2; //the null terminator
    cs.Buffer := PWideChar(ntFilename);

    oa := Default(OBJECT_ATTRIBUTES);
    oa.Length := sizeof(OBJECT_ATTRIBUTES);
    oa.Attributes := OBJ_CASE_INSENSITIVE;
    oa.ObjectName := @cs;

    //Open the file (by Object Attributes) and get a file handle
    status := NtOpenFile(@hFile, FILE_READ_ATTRIBUTES, @oa, @iosb, FILE_SHARE_READ, 0);
    if status <> STATUS_SUCCESS  then
        raise EOleSysError.Create('Error opening file "'+Filename+'": '+FormatNTStatusMessage(status)+' (0x'+IntToHex(status, 8)+')', status, 0);
    try
        //Query for information about the file (by handle)
        status := NtQueryInformationFile(hFile, @iosb, @fi, sizeof(fi), FileProcessIdsUsingFileInformation);
        if status <> STATUS_SUCCESS  then
            raise EOleSysError.Create('Error querying file "'+Filename+'" information: '+FormatNTStatusMessage(status), status, 0);

        for i := 0 to fi.NumberOfProcessIdsInList-1 do
        begin
            pid := fi.ProcessIdList[i];

            if Result <> '' then
                Result := Result+#13#10;
            Result := Result+'PID: '+IntToStr(pid);

            hProcess := OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid);
            if hProcess = 0 then
                RaiseLastOSError;
            try
                SetLength(s, 32767);
                dw := GetModuleFileNameEx(hProcess, 0, PChar(s), Length(s));
                if dw <= 0 then
                    RaiseLastOSError;
                SetLength(s, dw); //returns characters (not including null terminator)
                Result := Result+' ('+s+')';
            finally
                CloseHandle(hProcess);
            end;
        end;
    finally
        NtClose(hFile);
    end;

end;

Upvotes: 6

Related Questions