Avatar of npaun
npaun
 asked on

Application exe file path from application window handle

Hi,
I need a method to get the exe file path (full path, not just the file name) of an application to which belongs the window for which you have hWnd. However, the method must work equally well on 32 and 64-bit systems, and to be reasonably fast, not slower then few milliseconds.

I tried a standard method based on GetModuleFileNameExA (example given), but it doesn’t work on 64bit systems. More precise, it work only for 32 applications, but if the window handle belongs to an 64bit app it will return nothing. This is limitation of the GetModuleFileNameExA, which cannot return information for a 64bit application handle if it is used in a 32bit application. If VB6 could be compiled with 64bit compiler it would work fine, but since there is no 64bit version of VB6…

Also, the method MUST NOT be based on WMI, i.e. something like

    Set Processes = GetObject("winmgmts://localhost")
    Set myProcEnum = Processes.ExecQuery("select * from Win32_Process")

as this usually takes about 50 milliseconds per call, and it is way to slow for my need. I need something based on APIs…

Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function OpenProcess Lib "KERNEL32.DLL" (ByVal dwDesiredAccessas As Long, ByVal bInheritHandle As Long, ByVal dwProcId As Long) As Long
Private Declare Function EnumProcessModules Lib "PSAPI.DLL" (ByVal hProcess As Long, ByRef lphModule As Long, ByVal cb As Long, ByRef cbNeeded As Long) As Long
Private Declare Function GetModuleBaseName Lib "PSAPI.DLL" Alias "GetModuleBaseNameA" (ByVal hProcess As Long, ByVal hModule As Long, ByVal lpFileName As String, ByVal nSize As Long) As Long
Private Declare Function GetWindowThreadProcessId Lib "user32" (ByVal hWnd As Long, lpdwProcessId As Long) As Long
Private Declare Function GetModuleFileNameExA Lib "PSAPI.DLL" (ByVal hProcess As Long, ByVal hModule As Long, ByVal ModuleName As String, ByVal nSize As Long) As Long
Private Declare Function CloseHandle Lib "KERNEL32.DLL" (ByVal Handle As Long) As Long

Private Const MAX_PATH = 260
Private Const PROCESS_QUERY_INFORMATION = 1024
Private Const PROCESS_VM_READ = 16

Private Sub Command1_Click()
    Dim hWnd As Long
    Dim Path As String, Name As String
    
    hWnd = FindWindow(vbNullString, "Untitled - Notepad")
    
    If hWnd = 0 Then
        MsgBox "Notepad is not opened!"
        Exit Sub
    End If
    
    Path = ExePath(hWnd)
    Debug.Print Path, "|||"
End Sub

Public Function ExePath(ByVal hWnd As Long) As String
    Dim ThreadID As Long, ProcessID As Long
    Dim lngReturn As Long
    Dim strEXEPath As String
    Dim lngSize As Long
    Dim hProcess As Long
    Dim hMod(0 To 1023) As Long
    Dim cbNeeded As Long
    
    ThreadID = GetWindowThreadProcessId(hWnd, ProcessID)
    
    lngSize = MAX_PATH
    
    'Get a handle to the Process
    hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0, ProcessID)

    lngReturn = EnumProcessModules(hProcess, hMod(0), 1024, cbNeeded)
    
    strEXEPath = String$(lngSize, 0)
    lngReturn = GetModuleFileNameExA(hProcess, hMod(0), strEXEPath, lngSize)
    strEXEPath = Left(strEXEPath, lngReturn)
    
    
    lngReturn = CloseHandle(hProcess)
    
    ExePath = strEXEPath
End Function

Open in new window

Visual Basic Classic

Avatar of undefined
Last Comment
npaun

8/22/2022 - Mon
VBClassicGuy

This is not written in VB6, but could be converted. If not a solution, it may at least give you some ideas:
[DllImport("user32.dll")]
public static extern IntPtr GetWindowThreadProcessId(IntPtr hWnd, IntPtr ProcessId);
 
void GetProcessPathFromWindowHandle(IntPtr hwnd)
{
   uint pid = 0;
   Win32.GetWindowThreadProcessId(hwnd, out pid);
   Process p = Process.GetProcessById((int)pid);
   return p.MainModule.FileName;
}
Mike Tomlinson

@VBClassicGuy...

This part:

    Process p = Process.GetProcessById((int)pid);
    return p.MainModule.FileName;

uses the .Net Process() class which has no equivalent in VB6 so it can't be converted.
VBClassicGuy

Good catch. Oh well...
This is the best money I have ever spent. I cannot not tell you how many times these folks have saved my bacon. I learn so much from the contributors.
rwheeler23
ASKER CERTIFIED SOLUTION
nffvrxqgrcfqvvc

THIS SOLUTION ONLY AVAILABLE TO MEMBERS.
View this solution by signing up for a free trial.
Members can start a 7-Day free trial and enjoy unlimited access to the platform.
See Pricing Options
Start Free Trial
GET A PERSONALIZED SOLUTION
Ask your own question & get feedback from real experts
Find out why thousands trust the EE community with their toughest problems.
npaun

ASKER
Hi egl1044,
Thank you for your suggestion!

I actually tried with GetProcessImageFileName once before, but it was not working quite best. Now I see it was because I used a bad code snippet from internet. Now I rewrote it and it seems to work just fine.

One thing: I was under impression that I will need to use GetProcessImageFileName on XP, but I would need to develop an alternative approach based on QueryFullProcessImageName for Vista/Win7 (based on your comment I supposed that GetProcessImageFileName will not work on Vista/Win7). However, my preliminary test shows that my version with GetProcessImageFileName works just fine on Vista-x64/Win7-x64? I just want to check that there is no hidden catch which would require to use QueryFullProcessImageName too?

Public Function ExePath(ByVal hWnd As Long) As String
    Dim szName As String
    Dim ThreadID As Long
    Dim ProcessID As Long
    Dim hProcess As Long
    Dim lenName As Integer
    Dim DevicePath As String
    Dim p As Integer
    
    ThreadID = GetWindowThreadProcessId(hWnd, ProcessID)
    hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0, ProcessID)
    
    szName = String(MAX_PATH, Chr(0))
    lenName = GetProcessImageFileName(hProcess, szName, MAX_PATH)
    p = InStr(1, szName, Chr(0))
    
    DevicePath = Left$(szName, p - 1)
    
    ExePath = ConvertDevicePathToStandardPath(DevicePath)
End Function

Private Function ConvertDevicePathToStandardPath(ByVal DevFilePath As String) As String
    Dim ret As Long
    Dim k As Integer
    Dim strSave As String
    Dim sl As String
    Dim DevPath As String
    Dim l As Long
    Dim p As Integer
    
    strSave = String(255, Chr$(0))
    ret = GetLogicalDriveStrings(255, strSave)
    
    For k = 1 To 100
        If Left$(strSave, InStr(1, strSave, Chr$(0))) = Chr$(0) Then Exit For
        sl = Left$(strSave, InStr(1, strSave, Chr$(0)) - 2)
        
        DevPath = String(255, Chr$(0))
        l = QueryDosDevice(sl, DevPath, 255)
        DevPath = Left$(DevPath, InStr(1, DevPath, Chr$(0)) - 1)
        
        p = InStr(1, DevFilePath, DevPath)
        If p <> 0 Then
            ConvertDevicePathToStandardPath = sl & Right(DevFilePath, Len(DevFilePath) - Len(DevPath))
            Exit Function
        End If
        
        strSave = Right$(strSave, Len(strSave) - InStr(1, strSave, Chr$(0)))
    Next k
    
End Function

Open in new window

nffvrxqgrcfqvvc

GetProcessImageFileName() is exported on Windows XP and later it will work for later versions but it's best to use the newest functions since they provide some more power and in some rare cases they are updated to fix some issues from previous versions.
Internally they all wrap the ntdll.dll functions... I found an example in my backups that I wrote years ago but I can't remember if it works for x64 systems. I must have written for some purpose and it might be that I wrote it to test if the NT function had the same behavior on x64 systems as GetModuleFileName() for external processes.
You can test the example and let me know ... :)

Option Explicit

Private Const ProcessImageFileName = 27
Private Const STATUS_INFO_LENGTH_MISMATCH = &HC0000004
Private Const PROCESS_QUERY_INFORMATION = &H400&
Private Const PROCESS_VM_READ = &H10&
Private Const HEAP_ZERO_MEMORY = &H8&

Private Type UNICODE_STRING
Length As Integer
MaximumLength As Integer
Buffer As Long
End Type

Private Declare Function GetProcessHeap Lib "kernel32.dll" () As Long
Private Declare Function HeapAlloc Lib "kernel32.dll" (ByVal hHeap As Long, ByVal dwFlags As Long, ByVal dwBytes As Long) As Long
Private Declare Function HeapFree Lib "kernel32.dll" (ByVal hHeap As Long, ByVal dwFlags As Long, ByVal lpMem As Long) As Long
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessid As Long) As Long
Private Declare Function NtQueryInformationProcess Lib "ntdll.dll" (ByVal ProcessHandle As Long, ByVal ProcessInformationClass As Long, ByVal ProcessInformation As Long, ByVal ProcessInformationLength As Long, ByRef ReturnLength As Long) As Long
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As Long) As Long
Private Declare Sub RtlMoveMemory Lib "kernel32.dll" (ByVal lpDest As Long, ByVal lpSource As Long, ByVal cbCopy As Long)

Public Function GetProcessNameByPid(ByVal pid As Long) As String

  Dim uni             As UNICODE_STRING
  Dim Buffer          As Long
  Dim hProcess        As Long
  Dim FileName        As String
  Dim cbNeeded        As Long
  
  hProcess = OpenProcess(PROCESS_QUERY_INFORMATION Or PROCESS_VM_READ, 0, pid)
  If hProcess = 0 Then
    Exit Function
  End If
  ' Get the buffer size needed for the call.
  If NtQueryInformationProcess(hProcess, ProcessImageFileName, VarPtr(uni), 8, cbNeeded) = STATUS_INFO_LENGTH_MISMATCH Then
    ' Allocate the required buffer from the heap.
    Buffer = HeapAlloc(GetProcessHeap, HEAP_ZERO_MEMORY, cbNeeded)
    If NtQueryInformationProcess(hProcess, ProcessImageFileName, Buffer, cbNeeded, cbNeeded) = 0 Then
      ' UNICODE_STRING
      RtlMoveMemory VarPtr(uni), ByVal Buffer&, Len(uni)
      FileName = String$(uni.Length / 2, vbNullChar)
      RtlMoveMemory StrPtr(FileName), ByVal uni.Buffer, uni.Length
      GetProcessNameByPid = FileName
    End If
   HeapFree GetProcessHeap, 0, Buffer
  End If
  CloseHandle hProcess
  
End Function

Private Sub Command1_Click()
  Debug.Print GetProcessNameByPid(1092)
End Sub

Open in new window

nffvrxqgrcfqvvc

In your example above I don't see CloseHandle() call on the process handle. Make sure you call CloseHandle() when your finished or you will get "handle" leak in your application.
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
npaun

ASKER
Hi egl1044,

a)      Thanks for noting missing CloseHandle…

b)      I’ve tested your alternative GetProcessNameByPid, and it seems to be working too! I tested it on XP32, and Vista64/Win7x64 and it seems to work just fine. I don’t have XP64 at my disposal right now, but I don’t expect o have problem there too. Any preferences between these two approaches?

c)      One more question if I can: I’ve noticed that for some applications, it seems I can’t get full app path, no matter what method I’m using, even for those methods that are working perfectly fine on XP32, or any other OSver…For instance, “wow.exe” of “World of Warcraft” is such a case. After some testing with “wow.exe” example, I found that in this case I actually don’t get appropriate handle when calling OpenProcess, just zero! After consulting MSDN, I would say because of the “access control to process objects” for such applications? Anyway, do you have some idea how to get path to exe even for such applications? Is there a way to do that without need for handle to the process? Even the robust WMI approach I mentioned at the beginning is not working for such applications, probably because WMI itself is unsuccessfully using OpenProcess somewhere…
nffvrxqgrcfqvvc

I knew I wrote it for something, I probrably did it to use for all versions but couldn't remember nor did I put any comments in the example that would remind me what I was doing.
<One more question if I can: I’ve noticed that for some applications, it seems I can’t get full app path, no matter what method I’m using >>
Yes some process will change the DACL and deny access in most cases SYSTEM process will be denied but in the case of (wow.exe) it removed VM_READ from the security so you don't get a valid handle for that process, if you would enable SeDebugPrivilege which I think (requires administrator) then you would be able to access the handle and should be able to get the requested output correctly. You will have this issue with "Windows Services".
nffvrxqgrcfqvvc

Make sure you test the code from (User) accounts and not Administrative accounts. This is especially the case under XP where no UAC is implemented. I think this (wow.exe) process might try to change the security but only if it has the required privileges to do so.. If you run under Administrator on XP then it probrably attempts to change the security on its process but if it didn't have the rights to do so it simply fails.
So try running wow.exe from a user account and see what results you get..
Your help has saved me hundreds of hours of internet surfing.
fblack61
npaun

ASKER
ok, I tried the wow.exe on a User account (on XPx32), however the results are the same: OpenProcess returns zero handle for this process…
nffvrxqgrcfqvvc

OK. Well I have Windows 7 and it works fine but they add security when it gets the administrative token so they might always add the security on XP but it doesn't appear they always enable it on Windows 7.
You will have this problem for process that deny PROCESS_QUERY_INFORMATION | PROCESS_VM_READ access on the security DACL.. You can always get a handle for the process if you enable SeDebugPrivilege.
npaun

ASKER
a) I tried wow.exe on XP32 and Win7-64 and it always returns zero handle, regardless of User/Admin-UAC_On/UAC_Off combination, if it means something to you.

b) Nevertheless, SeDebugPrivilege seems to be working. Although, it doesn’t work on Win7 if UAC is enabled, and it doesn’t work on XP/Win7 on User account regardless of UAC. Is that what should be expected?

c) For SeDebugPrivilege I used example from of your other posts
https://www.experts-exchange.com/questions/24106182/Force-Kill-Running-Process-in-vb6.html?sfQueryTermInfo=1+10+30+egl1044+sedebugprivileg

Is there a way to release SeDebugPrivilege after use? Can your code be modified so it can also unset this privilege? My idea is to use this approach only if and when needed, and to return to previous state immediately afterwards. Somehow I feel a little bit uncomfortable to simply enable SeDebugPrivilege at the beginning of my program regardless whether it will be needed or not.

d) This is a little bit of topic but connected: Do you know is there a way to know if user is having UAC enabled or disabled (or even which 1-3 level on Win7)? I know how to check is it Admin, but I haven’t found an example for this. If Yes, then I’ can put this as a new question…
⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
nffvrxqgrcfqvvc

A) I don't know how (wow.exe) works internally but it's changing the security on the DACL and removing specific process access rights.
B) SeDebugPrivilege overrides the security so that an application can always obtain PROCESS_ALL_ACCESS right regardless of the security DACL. Except for system critical processes where they change the critical bit flag ( the processes that always need to run for windows to work).
C) You can disable the privilege in two different ways, if you enable multiple privileges you can use the second parameter (DisableAllPrivileges) set to TRUE removes all privileges and ignores the third parameter. To disable specific privilege use the same code but instead of passing SE_PRIVILEGE_ENABLED specify NONE (0) that will remove the privilege.

See AdjustTokenPrivileges specifially the parameters: (DisableAllPrivileges and NewState)
http://msdn.microsoft.com/en-us/library/aa375202(VS.85).aspx
npaun

ASKER
thanks.
Regarding B)
So SeDebugPrivilege should be able to get handle to process regardless if account is User, or UAC is On? And in this case, inability to get handle for wow.exe when User or UAC=On is just some peculiarity of this particular program?
I’m happy even now (as 95% comps are with Admin rights, and most users turns UAC off), butt I’m just a little concerned with resistance of this wow.exe, and possibility that some other programs might also resist in this way…
nffvrxqgrcfqvvc

No unfortuanetley that is something that requires administrative privileges :(
Just to cheer you up a bit, even the latest Windows Task Manager has the same issues when running with user tokens. Windows 7 Task Manager has another option to "Show Image paths" if you run in normal mode it misses some system processes and so forth but it has an option called "show processes from all users" which has the UAC shield icon basically what that does is request administrative rights and enable SeDebugPrivilege and then you can see that all the paths are available and not missing.
Experts Exchange has (a) saved my job multiple times, (b) saved me hours, days, and even weeks of work, and often (c) makes me look like a superhero! This place is MAGIC!
Walt Forbes
nffvrxqgrcfqvvc

My Bad.
<< So SeDebugPrivilege should be able to get handle to process regardless if account is User, or UAC is On? >>
Yes.
In the previous post my first statment "No unfortuanetley that is something that requires administrative privileges " was refering that you need "Administrative rights" to enable the privilege.
I can show you how exactly this "resistance" is accomplished (Just wrote something the other day to work with changing process privileges) but it's in VB.NET. I can show you how how to change the DACL on your process so that other processes can't obtain access which would be exactly what some processes are doing and that is why OpenProcess() fails.
npaun

ASKER
Well, now you have confused me:) If you need Admin for installing SeDebugPrivilege  then << So SeDebugPrivilege should be able to get handle to process regardless if account is User, or UAC is On? >>=YES have collision, as you can’t get SeDebugPrivilege  on User?

Do you wish to say that:
a) SeDebugPrivilege should be able to get handle to process only on Admin account
b) SeDebugPrivilege should be able to get handle to process on Admin account regardless on UAC
c) something else…

regarding the "resistance" method, it not quite necessary, but if you have it your disposal, it might me useful as information. VB.Net doesn’t matter…
nffvrxqgrcfqvvc

Well I am looking to deep into this I suppose, but just look at from a process token point of view.
Your process could be running on a user account but it would need to request and administrative token to enable the privilege so (yes it can be enabled from a user account) but it doesn't change the fact that in order for it to be enabled your process must request the administrative token first.
I'll just share my class with you if you feel the need to use it for debugging purposes (or to see what some of these process are doing and why your calls sometimes fail.)
The ProcessSecurity Class is still under construction but the basic functionality of adding and removing access rights in the DACL works.
Create new class named ProcessSecurity.vb
Using the class is very easy, in the following example I obtain the the everyoneSid and Deny access to PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE.
If you try and terminate or kill the process using (End Process) from task manager you will get (Access Denied) because we set Deny access for PROCESS_TERMINATE. The same is true for the other two access rights we denied no process(Including our own) will be allowed to query information or rather (get the file path).
Add 2 Buttons to Form then this code.

Imports System.Security.AccessControl
Imports System.Security.Principal
Imports Microsoft.Win32.SafeHandles
Public Class Form1
Dim hProcess As SafeFileHandle = Nothing
Dim pSecurity As ProcessSecurity = Nothing
Dim sid As SecurityIdentifier = Nothing
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
' Deny Button
pSecurity.AddAccessRule(New ProcessAccessRule(sid, ProcessRights.ProcessQueryInformation Or ProcessRights.ProcessVmRead Or ProcessRights.ProcessTerminate, AccessControlType.Deny))
pSecurity.Persist(hProcess)
End Sub
Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
' Allow Button
pSecurity.RemoveAccessRule(New ProcessAccessRule(sid, ProcessRights.ProcessQueryInformation Or ProcessRights.ProcessVmRead Or ProcessRights.ProcessTerminate, AccessControlType.Deny))
pSecurity.Persist(hProcess)
End Sub
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
hProcess = New SafeFileHandle(Process.GetCurrentProcess.Handle, False)
sid = New SecurityIdentifier(WellKnownSidType.WorldSid, Nothing)
pSecurity = New ProcessSecurity(hProcess)
End Sub
End Class

'ProcessSecurity.vb
'egl1044

Imports System.Runtime.InteropServices
Imports System.Security.AccessControl
Imports System.Security.Principal


Public NotInheritable Class ProcessSecurity
    Inherits NativeObjectSecurity
    Public Sub New(ByVal handle As SafeHandle)
        MyBase.New(False, Security.AccessControl.ResourceType.KernelObject, handle, _
                   AccessControlSections.Access Or _
                   AccessControlSections.Group Or _
                   AccessControlSections.Owner)
    End Sub
    Public Overrides Function AccessRuleFactory(ByVal identityReference As IdentityReference, ByVal accessMask As Integer, ByVal isInherited As Boolean, ByVal inheritanceFlags As System.Security.AccessControl.InheritanceFlags, ByVal propagationFlags As System.Security.AccessControl.PropagationFlags, ByVal type As System.Security.AccessControl.AccessControlType) As System.Security.AccessControl.AccessRule
        Return New ProcessAccessRule(identityReference, accessMask, type)
    End Function
    Public Overrides Function AuditRuleFactory(ByVal identityReference As IdentityReference, ByVal accessMask As Integer, ByVal isInherited As Boolean, ByVal inheritanceFlags As System.Security.AccessControl.InheritanceFlags, ByVal propagationFlags As System.Security.AccessControl.PropagationFlags, ByVal flags As System.Security.AccessControl.AuditFlags) As System.Security.AccessControl.AuditRule
        Return Nothing
    End Function
    Public Overloads Sub AddAccessRule(ByVal rule As ProcessAccessRule)
        MyBase.AddAccessRule(rule)
    End Sub
    Public Overloads Function RemoveAccessRule(ByVal rule As ProcessAccessRule) As Boolean
        Return MyBase.RemoveAccessRule(rule)
    End Function
    Public Overloads Sub Persist(ByVal handle As SafeHandle)
        WriteLock()
        Try
            Persist(handle, _
                    AccessControlSections.Access Or _
                    AccessControlSections.Group Or _
                    AccessControlSections.Owner)
        Finally
            WriteUnlock()
        End Try
    End Sub
    Public Overrides ReadOnly Property AccessRightType As System.Type
        Get
            Return GetType(Integer)
        End Get
    End Property
    Public Overrides ReadOnly Property AccessRuleType As System.Type
        Get
            Return GetType(ProcessAccessRule)
        End Get
    End Property
    Public Overrides ReadOnly Property AuditRuleType As System.Type
        Get
            Return Nothing
        End Get
    End Property
End Class
Public NotInheritable Class ProcessAccessRule
    Inherits AccessRule
    Public Sub New(ByVal identity As IdentityReference, ByVal accessRights As ProcessRights, ByVal accessType As AccessControlType)
        MyBase.New(identity, accessRights, False, InheritanceFlags.None, PropagationFlags.None, accessType)
    End Sub
End Class
<Flags()>
Public Enum ProcessRights
    'ProcessAllAccess = 0
    ProcessCreateProcess = &H80
    ProcessCreateThread = &H2
    ProcessDupHandle = &H40
    ProcessQueryInformation = &H400
    ProcessQueryLimitedInformation = &H1000
    ProcessSetInfromation = &H200
    ProcessSetQuota = &H100
    ProcessSuspendResume = &H800
    ProcessTerminate = &H1
    ProcessVmOperation = &H8
    ProcessVmRead = &H10
    ProcessVmWrite = &H20
    ProcessSynchronize = &H100000
End Enum

Open in new window

⚡ FREE TRIAL OFFER
Try out a week of full access for free.
Find out why thousands trust the EE community with their toughest problems.
npaun

ASKER
Ok, I see.
Thank you very much. You were very helpful and thorough!