1

Edit: the problem didn't lie in NtQuerySystemInformation but in the file type (bObjectType) having changed in this new edition of Windows 10 to the value 34. in Creators Update it's 35.

I have been using the following code successfully to retrieve a list of files in use by a given process, but since the Windows 10 "anniversary update" it's no longer working.

Windows 10 version 1607 Build 14393.105

Any idea?

function GetFileNameHandle(hFile: THandle): String;
var lpExitCode: DWORD;
    pThreadParam: TGetFileNameThreadParam;
    hThread: THandle;
    Ret: Cardinal;
begin
  Result := '';
  ZeroMemory(@pThreadParam, SizeOf(TGetFileNameThreadParam));
  pThreadParam.hFile := hFile;
  hThread := CreateThread(nil, 0, @GetFileNameHandleThr, @pThreadParam, 0, {PDWORD(nil)^} Ret);
  if hThread <> 0 then
  try
    case WaitForSingleObject(hThread, 100) of
      WAIT_OBJECT_0: begin
        GetExitCodeThread(hThread, lpExitCode);
        if lpExitCode = STATUS_SUCCESS then
          Result := pThreadParam.FileName;
      end;
      WAIT_TIMEOUT: TerminateThread(hThread, 0);
    end;
  finally
    CloseHandle(hThread);
  end;
end;

procedure DeleteUpToFull(var src: String; UpTo: String);
begin
  Delete(src,1,Pos(Upto,src)+Length(UpTo)-1);
end;

procedure ConvertDevicePath(var dvc: string);
var i: integer;
    root: string;
    device: string;
    buffer: string;
    //drvs: string;
begin
  // much faster without using GetReadyDiskDrives
  setlength(buffer, 1000);
  for i := Ord('a') to Ord('z') do begin
    root := Chr(i) + ':';
    if (QueryDosDevice(PChar(root), pchar(buffer), 1000) <> 0) then begin
      device := pchar(buffer);
      if finds(device+'\',dvc) then begin
        DeleteUpToFull(dvc,device+'\');
        dvc := root[1] + ':\' + dvc;
        Exit;
      end;
    end;
  end;
end;

//get the pid of the process which had open the specified file
function GetHandlesByProcessID(const ProcessID: Integer; Results: TStringList; TranslatePaths: Boolean): Boolean;
var hProcess    : THandle;
    hFile       : THandle;
    ReturnLength: DWORD;
    SystemInformationLength : DWORD;
    Index       : Integer;
    pHandleInfo : PSYSTEM_HANDLE_INFORMATION;
    hQuery      : THandle;
    FileName    : string;
    r: byte;
begin
  Result := False;
  Results.Clear;
  pHandleInfo      := nil;
  ReturnLength     := 1024;
  pHandleInfo      := AllocMem(ReturnLength);
  hQuery           := NTQuerySystemInformation(DWORD(SystemHandleInformation), pHandleInfo, 1024, @ReturnLength);
  r := 0; // loop safe-guard
  While (hQuery = $C0000004) and (r < 10) do begin
    Inc(r);
    FreeMem(pHandleInfo);
    SystemInformationLength := ReturnLength;
    pHandleInfo             := AllocMem(ReturnLength+1024);
    hQuery                  := NTQuerySystemInformation(DWORD(SystemHandleInformation), pHandleInfo, SystemInformationLength, @ReturnLength);//Get the list of handles
  end;
  // if hQuery = 0 then
  //  RaiseLastOSError;

  try
    if (hQuery = STATUS_SUCCESS) then begin
     for Index := 0 to pHandleInfo^.uCount-1 do begin
       // filter to requested process
       if pHandleInfo.Handles[Index].uIdProcess <> ProcessID then Continue;
       // http://www.codeproject.com/Articles/18975/Listing-Used-Files
       // For an object of type file, the value bObjectType in SYSTEM_HANDLE is 28 in Windows XP, Windows 2000, and Window 7; 25 in Windows Vista; and 26 in Windows 2000.
       // XP = 28
       // W7 = 28
       // W8 = 31
       if (pHandleInfo.Handles[Index].ObjectType < 25) or
         (pHandleInfo.Handles[Index].ObjectType > 31) then Continue;

        hProcess := OpenProcess(PROCESS_DUP_HANDLE, FALSE, pHandleInfo.Handles[Index].uIdProcess);
        if(hProcess <> INVALID_HANDLE_VALUE) then begin
          try
           if not DuplicateHandle(hProcess, pHandleInfo.Handles[Index].Handle,
                                  GetCurrentProcess(), @hFile,  0 ,FALSE,
                                  DUPLICATE_SAME_ACCESS) then
            hFile := INVALID_HANDLE_VALUE;
          finally
           CloseHandle(hProcess);
          end;

          if (hFile <> INVALID_HANDLE_VALUE) then begin
            try
              FileName := GetFileNameHandle(hFile);
            finally
              CloseHandle(hFile);
            end;
          end
          else
          FileName := '';

          if FileName <> '' then begin
            if TranslatePaths then begin
                ConvertDevicePath(FileName);
                if not FileExists(Filename) then FileName := '\##\'+Filename; //Continue;
            end;
            Results.Add(FileName);
          end;
        end;
      end;
    end;
  finally
    if pHandleInfo <> nil then FreeMem(pHandleInfo);
  end;
end;
hikari
  • 3,393
  • 1
  • 33
  • 72
  • Please make a MCVE for us – David Heffernan Sep 11 '16 at 20:56
  • hQuery(NTSTATUS) is 0 - this is not error but ok code. and NtQuerySystemInformation(SystemHandleInformation) fine work on 10 version 1607 as well. i checked this – RbMm Sep 11 '16 at 20:59
  • and you must not hardcode object types indexes but got this info from system – RbMm Sep 11 '16 at 21:02
  • Sorry, you are right, the problem lies elsewhere. I'll check again and edit the question. – hikari Sep 11 '16 at 21:26
  • @hikari NtQuerySystemInformation(SystemHandleInformation) must return STATUS_INFO_LENGTH_MISMATCH (c0000004) for 1024 size buffer.. dont know why you got 0. but i claim that SystemHandleInformation and SystemExtendedHandleInformation correct worked on latest win10 build (1607 x64) - i permanent use this calls in self code – RbMm Sep 11 '16 at 21:32
  • @RbMm: Pointing out what's wrong is helpful. If you know, how to properly implement it, provide some more details. It need not necessarily be Delphi code in a comment. Someone fluent in Delphi can probably translate C code easily, and likely compose an answer. – IInspectable Sep 11 '16 at 21:35
  • @IInspectable - yes, ofcourse i know how implement this. however hikari code look like correct for this point. very not optimal, but must work – RbMm Sep 11 '16 at 21:38
  • Found the issue, bObjectType = 34 for this new W10 and I was only including 25-31. – hikari Sep 11 '16 at 22:05
  • @hikari - "Found the issue, bObjectType = 34" - this is wat i say - you need not hardcode this indexes by got in in runtime. i show how in answer – RbMm Sep 11 '16 at 22:12
  • @hikari - also this mean that "hQuery is 0 after .." is not true ? – RbMm Sep 11 '16 at 22:18
  • edit: bObjectType = 35 in W10 Creators Update – hikari May 04 '17 at 02:20

2 Answers2

3

The next code (in C++) works 100% correct on all Windows versions (Win 10 1607 as well). Also, I use SystemExtendedHandleInformation in place of SystemHandleInformation, and advise you to do so, too. It is present from XP onwards. However, the code with SystemHandleInformation also works correctly, I just checked it.

NTSTATUS GetHandlesByProcessID()
{
    union {
        PVOID buf;
        PSYSTEM_HANDLE_INFORMATION_EX pshti;
    };

    NTSTATUS status;
    ULONG ReturnLength = 1024;//not reasonable value for start query,but let be
    ULONG UniqueProcessId = GetCurrentProcessId(); 
    do 
    {
        status = STATUS_INSUFFICIENT_RESOURCES;

        if (buf = new BYTE[ReturnLength])
        {
            if (0 <= (status = ZwQuerySystemInformation(SystemExtendedHandleInformation, buf, ReturnLength, &ReturnLength)))
            {
                if (ULONG_PTR NumberOfHandles = pshti->NumberOfHandles)
                {
                    SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX* Handles = pshti->Handles;
                    do 
                    {
                        if (Handles->UniqueProcessId == UniqueProcessId)
                        {
                            DbgPrint("%u, %p\n", Handles->ObjectTypeIndex, Handles->HandleValue);
                        }
                    } while (Handles++, --NumberOfHandles);
                }
            }

            delete buf;
        }

    } while (status == STATUS_INFO_LENGTH_MISMATCH);

    return status;
}

I think this is like a repeat until in a Delphi loop :)

r := 0; // loop safe-guard - this is not needed.

About the hard-coded ObjectTypeIndex - beginning in Win 8.1, you can exactly get this info from the OS. You need to call ZwQueryObject() with ObjectTypesInformation (in some sources, this is named ObjectAllTypeInformation, see ntifs.h) to get an array of OBJECT_TYPE_INFORMATION structs. Look for the TypeIndex member - it exactly cooresponds to the ObjectTypeIndex from SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX. Before Win 8.1, there also exists ways to get this 'on the fly' by using ObjectAllTypeInformation but it is more complex.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
RbMm
  • 31,280
  • 3
  • 35
  • 56
2

I just tested the code from my blog article "Running multiple instances of Microsoft Lync" on Windows 10 Anniversary Update on it appears to work without any issues.

Here's the code that I tested (takes process name eg foobar.exe as parameter):

program ListHandles;

{$APPTYPE CONSOLE}

uses
  JwaWinBase,
  JwaWinNT,
  JwaWinType,
  JwaNtStatus,
  JwaNative,
  JwaWinsta,
  SysUtils,
  StrUtils;

{$IFDEF RELEASE}
  // Leave out Relocation Table in Release version
  {$SetPEFlags IMAGE_FILE_RELOCS_STRIPPED}
{$ENDIF RELEASE}
{$SetPEOptFlags IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE}

// No need for RTTI
{$WEAKLINKRTTI ON}
 {$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])}

var
  dwPid: DWORD;
  hProcess: THandle;

{$ALIGN 8}
{$MINENUMSIZE 4}
type
  _SYSTEM_HANDLE = record
    ProcessId: ULONG;
    ObjectTypeNumber: Byte;
    Flags: Byte;
    Handle: USHORT;
    _Object: PVOID;
    GrantedAccess: ACCESS_MASK;
  end;
  SYSTEM_HANDLE = _SYSTEM_HANDLE;
  PSYSTEM_HANDLE = ^SYSTEM_HANDLE;

  _SYSTEM_HANDLE_INFORMATION = record
    HandleCount: ULONG;
    Handles: array[0..0] of SYSTEM_HANDLE;
  end;
  SYSTEM_HANDLE_INFORMATION = _SYSTEM_HANDLE_INFORMATION;
  PSYSTEM_HANDLE_INFORMATION = ^SYSTEM_HANDLE_INFORMATION;

  _OBJECT_NAME_INFORMATION = record
    Length: USHORT;
    MaximumLength: USHORT;
    Pad: DWORD;
    Name: array[0..MAX_PATH-1] of Char;
  end;
  OBJECT_NAME_INFORMATION = _OBJECT_NAME_INFORMATION;
  POBJECT_NAME_INFORMATION = ^OBJECT_NAME_INFORMATION;

function GetObjectName(const hObject: THandle): String;
var
  oni: OBJECT_NAME_INFORMATION;
  cbSize: DWORD;
  nts: NTSTATUS;
begin
  Result := '';

  cbSize := SizeOf(oni) - (2 * SizeOf(USHORT));
  oni.Length := 0;
  oni.MaximumLength := cbSize;
  nts := NtQueryObject(hObject, ObjectNameInformation, @oni, cbSize, @cbSize);
  if (nts = STATUS_SUCCESS) and (oni.Length > 0) then
  begin
    Result := oni.Name;
  end;
end;

function GetCurrentSessionId: DWORD;
 asm
   mov     eax,fs:[$00000018];   // Get TEB
   mov     eax,[eax+$30];        // PPEB
   mov     eax,[eax+$1d4];       // PEB.SessionId
 end;

function GetProcessByName(const ProcessName: string): DWORD;
var
  ProcName: PChar;
  Count: Integer;
  tsapi: PTS_ALL_PROCESSES_INFO_ARRAY;
  i: Integer;
  dwSessionId: DWORD;
begin
  Result := 0;
  tsapi := nil;

  if not WinStationGetAllProcesses(SERVERNAME_CURRENT, 0, Count, tsapi) then
    Exit;

  ProcName := PChar(ProcessName);
  dwSessionId := GetCurrentSessionId;

  WriteLn(Format('Looking for Process %s in Session %d',
    [ProcessName, dwSessionId]));

  for i := 0 to Count - 1 do
  begin
    with tsapi^[i], tsapi^[i].pTsProcessInfo^ do
    begin
      if (dwSessionId = SessionId) and (ImageName.Buffer <> nil) and
        (StrIComp(ProcName, ImageName.Buffer) = 0) then
      begin
        Result := UniqueProcessId;
        WriteLn(Format('%s has Pid %d', [ProcessName, Result]));
        Break
      end;
    end;
  end;

  if tsapi <> nil then
    WinStationFreeGAPMemory(0, tsapi, Count);
end;

procedure EnumHandles;
var
  shi: PSYSTEM_HANDLE_INFORMATION;
  cbSize: DWORD;
  cbRet: DWORD;
  nts: NTSTATUS;
  i: Integer;
  hDupHandle: THandle;
  dwErr: DWORD;
  ObjectName: string;
begin
  WriteLn('Enumerating Handles');
  cbSize := $5000;
  GetMem(shi, cbSize);
  repeat
    cbSize := cbSize * 2;
    ReallocMem(shi, cbSize);
    nts := NtQuerySystemInformation(SystemHandleInformation, shi, cbSize, @cbRet);
  until nts <> STATUS_INFO_LENGTH_MISMATCH;

  if nts = STATUS_SUCCESS then
  begin
    for i := 0 to shi^.HandleCount - 1 do
    begin
      if shi^.Handles[i].GrantedAccess <> $0012019f then
      begin
        if shi^.Handles[i].ProcessId = dwPid then
        begin
          nts := NtDuplicateObject(hProcess, shi^.Handles[i].Handle,
            GetCurrentProcess, @hDupHandle, 0, 0, 0);

          if nts = STATUS_SUCCESS then
          begin
            ObjectName := GetObjectName(hDupHandle);
            if (ObjectName <> '') then
            begin
              WriteLn(Format('Handle=%d Name=%s', [shi^.Handles[i].Handle, ObjectName]));
              CloseHandle(hDupHandle);
            end;
          end;
        end;
      end;
    end;
  end
  else begin
    dwErr := RtlNtStatusToDosError(nts);
    WriteLn(Format('Failed to read handles, NtQuerySystemInformation failed with %.8x => %d (%s)', [nts, SysErrorMessage(dwErr)]));
  end;

  FreeMem(shi);
end;


procedure AnyKey;
begin
    WriteLn('Finished');
    WriteLn('Press any key to continue');
    ReadLn;
end;

begin
  try
    dwPid := GetProcessByName(ParamStr(1));
    if dwPid = 0 then
    begin
      WriteLn('Process was not found, exiting.');
      Exit;
    end;

    WriteLn(Format('Opening Process %d with PROCESS_DUP_HANDLE', [dwPid]));
    hProcess := OpenProcess(PROCESS_DUP_HANDLE, False, dwPid);

    if hProcess = 0 then
    begin
      WriteLn(Format('OpenProcess failed with %s', [SysErrorMessage(GetLastError)]));
      Exit;
    end
    else begin
      WriteLn(Format('Process Handle is %d', [hProcess]));
    end;

    EnumHandles;

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
Remko
  • 7,214
  • 2
  • 32
  • 52
  • That asm is dodgy, use `GetCurrentProcessId` and then `ProcessIdToSessionId` – David Heffernan Sep 12 '16 at 09:57
  • We're in the land of undocumented api's so I don't think that asm will hurt (it's documented on MSDN btw) – Remko Sep 12 '16 at 10:00
  • In fact it is documented on MSDN to use `GetCurrentProcessId` and then `ProcessIdToSessionId` https://msdn.microsoft.com/en-us/library/bb432286.aspx – David Heffernan Sep 12 '16 at 10:03
  • Yes I know that link, if you read on you'll see that `ProcessIdToSessionId` is Vista/2008 up (so doesn't work on XP/2003 according to MSDN). That's the reason I originally used this asm. But fair enough for x64 and future win versions might be better to leave it out... – Remko Sep 12 '16 at 10:15