驱动中获取进程完整路径名


2009-07-23

在OSR上无意中看到一篇文章,关于获取进程完整路径的。贴过来,最后有一点小调整。

原文地址:http://www.osronline.com/article.cfm?id=472

Over the years developers have needed or wanted to know the name of the image executing in a given process. Traditionally, this was often done using PsGetProcessImageFile Name, which returns the contents of a field in the EPROCESS structure used by the Windows OS to maintain per-process state information. As we can see from the information in the local debugger session (See Figure 1) the process image file is little more than a field within the EPROCESS structure. Notice that the EPROCESS address is in EBP+8, making it the first - and only - parameter to this function.

Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE Symbol search path is: srv*c:\symbols\websymbols*http://msdl.microsoft.com/download/symbols Executable search path is:


WARNING: Local kernel debugging requires booting with /debug to work optimally.


Windows XP Kernel Version 2600 (Service Pack 2) MP (2 procs) Free x86 compatible Product: winNt, suite: Terminalserver SingleUserTS Built by: 2600.xpsp_sp2_gdr.050301-1519 Kernel base = 0x804d7000 PsLoadedModuleList = 0x805624a0 Debug session time: Mon Aug 7 13:29:53.486 2006 (GMT-4) System Uptime: 2 days 11:08:38.140 lkd> .reload Connected to Windows XP 2600 x86 compatible target, ptr64 FALSE Loading Kernel Symbols …………………………………………………………………… Loading User Symbols …………………………………………………………………… Loading unloaded module list …………………..*** ERROR: Symbol file could not be found. Defaulted to export symbols for C lkd> u nt!PsGetProcessImageFileName Nt!PsGetProcessImageFileName: 8050a14a 8bff mov edi,edi 8050a14c 55 push ebp 8050a14d 8bec mov ebp,esp 8050a14f 8b4508 mov eax,dword ptr [ebp+8] 8050a152 0574010000 add eax,174h 8050a157 5d pop ebp 8050a158 c20400 ret 4 8050a15b 8bce mov ecx,esi

Figure 1 - Local Debug Session of PsGetProcessImageFileName

Unfortunately, there are some issues with this approach:

Though well-known, this function is undocumented. More seriously, the information contained in this field is severely limited. It contains only the first 16 (ASCII) characters of the image file name. It is actually the second issue that often creates problems for programmers because the name of the image file means essentially nothing. For example, we’ve seen kernel-mode drivers in the past that validate the name of their service by checking this field. The most egregious case we’ve seen is when the service was called svchost.exe, which is a common name that is often spoofed.

The Proposal We suggest a different model for acquiring this information, as shown in Figure 2.

> typedef NTSTATUS (*QUERY_INFO_PROCESS) (
>     __in HANDLE ProcessHandle,
>     __in PROCESSINFOCLASS ProcessInformationClass,
>     __out_bcount(ProcessInformationLength) PVOID ProcessInformation,
>     __in ULONG ProcessInformationLength,
>     __out_opt PULONG ReturnLength
>     );
> 
> QUERY_INFO_PROCESS ZwQueryInformationProcess;
> 
> NTSTATUS GetProcessImageName(PUNICODE_STRING ProcessImageName)
> {
>     NTSTATUS status;
>     ULONG returnedLength;
>     ULONG bufferLength;
>     PVOID buffer;
>     PUNICODE_STRING imageName;
>     
>     PAGED_CODE(); // this eliminates the possibility of the IDLE Thread/Process
> 
>     if (NULL == ZwQueryInformationProcess) {
> 
>         UNICODE_STRING routineName;
> 
>         RtlInitUnicodeString(&routineName, L"ZwQueryInformationProcess");
> 
>         ZwQueryInformationProcess = 
>                (QUERY_INFO_PROCESS) MmGetSystemRoutineAddress(&routineName);
> 
>         if (NULL == ZwQueryInformationProcess) {
>             DbgPrint("Cannot resolve ZwQueryInformationProcess\n");
>         }
>     }
>     //
>     // Step one - get the size we need
>     //
>     status = ZwQueryInformationProcess( NtCurrentProcess(), 
>                                         ProcessImageFileName,
>                                         NULL, // buffer
>                                         0, // buffer size
>                                         &returnedLength);
> 
>     if (STATUS_INFO_LENGTH_MISMATCH != status) {
> 
>         return status;
> 
>     }
> 
>     //
>     // Is the passed-in buffer going to be big enough for us?  
>     // This function returns a single contguous buffer model...
>     //
>     bufferLength = returnedLength - sizeof(UNICODE_STRING);
>     
>     if (ProcessImageName->MaximumLength < bufferLength) {
> 
>         ProcessImageName->Length = (USHORT) bufferLength;
> 
>         return STATUS_BUFFER_OVERFLOW;
>         
>     }
> 
>     //
>     // If we get here, the buffer IS going to be big enough for us, so 
>     // let's allocate some storage.
>     //
>     buffer = ExAllocatePoolWithTag(PagedPool, returnedLength, 'ipgD');
> 
>     if (NULL == buffer) {
> 
>         return STATUS_INSUFFICIENT_RESOURCES;
>         
>     }
> 
>     //
>     // Now lets go get the data
>     //
>     status = ZwQueryInformationProcess( NtCurrentProcess(), 
>                                         ProcessImageFileName,
>                                         buffer,
>                                         returnedLength,
>                                         &returnedLength);
> 
>     if (NT_SUCCESS(status)) {
>         //
>         // Ah, we got what we needed
>         //
>         imageName = (PUNICODE_STRING) buffer;
> 
>         RtlCopyUnicodeString(ProcessImageName, imageName);
>         
>     }
> 
>     //
>     // free our buffer
>     //
>     ExFreePool(buffer);
> 
>     //
>     // And tell the caller what happened.
>     //    
>     return status;
>     
> }
> 
Figure 2 - A New Proposal

The function itself is fairly straight-forward. It does rely on use of a single undocumented function (ZwQueryInformationProcess), but note that its counterpart (NtQueryInformationProcess) is documented. We need to use the Zw variant in order to use a kernel memory buffer.

The key element is that Windows has always stored the full path name to the executable image in order to provide this information in the auditing subsystem. This API exploits that existing stored path name.

Other Process Names This function has been implemented to extract the process name for the current process. However, you can use one of the two methods listed below to obtain the process name for a different process:

  1. If you have a handle for the process, you can use that value instead of the NtCurrentProcess() macro. (Note that in our experience, we usually have a process object and not a process handle - the two are not interchangeable).

  2. If you have an EPROCESS address, you can use KeStackAttachProcess/KeUnstackDetachProcess to attach to the process. This technique is rather heavy-weight, so it may be a good idea to cache the information if you need to perform this operation regularly.

When using the second technique, it is important to note that the name that is returned is a cached name. This cache is not updated if the name of the original file changes after the name is first cached. In other words, if the executable image is renamed, which is typically allowed for a running executable, the name returned will be the name of the original file.

This issue is not unique. We have also observed that some file systems return the original name even after a rename (e.g., the CIFS client implementation does this on Windows XP). Thus, it may require additional processing such as through a file system filter driver to protect against similar events. For example, you may encounter a security product that relies on knowing the specific name of the image.Alternatives? There are other options that a driver could also pursue such as registering for process creation/teardown events (PsSetCreateProcessNotifyRoutine) or image loading (PsSetLoadImageNotifyRoutine).

PsSetCreateProcessNotifyRoutine has limitations on the number of drivers that can register using this API. Since there is a fixed size table in the Windows OS, it is possible for this call to fail. When this occurs, a driver needs to ensure it can handle such a failure. PsSetLoadImageNotifyRoutine has the same limitation (fixed size table), but is called for all image loads, not just the original process image. Therefore, it includes drivers, DLLs, executables, etc.

Summary The bottom line is that all of these approaches provide a useful name because they include the full path name. This is vastly superior to using the short ASCII eye-catching name that is stored in the EPROCESS structure. A word of caution - if you decide to use the debug level name, use it for nothing more than debugging since it is not reliable and cannot be relied on for any sort of security check.

We chose to use the proposed technique because it works in all circumstances and does not rely upon a registration that might potentially fail. In your own driver you might implement both this mechanism and a cache-based mechanism tied to the process creation logic.[/quote]

文章里对于如何获取其他进程,提供了两种方法,但是都不太方便,在我的驱动里,我是在PsSetCreateProcessNotifyRoutine的回调函数里获取进程路径,能得到的进程信息是PID,于是动手改了下上面的代码,如下:

NTSTATUS
    GetProcessImagePath(
        IN  HANDLE   hProcessId,
        OUT PUNICODE_STRING ProcessImagePath
    )
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    HANDLE hProcess = NULL;
    PEPROCESS pEprocess = NULL;
    ULONG returnedLength = 0;
    ULONG bufferLength = 0;
    PVOID buffer = NULL;
    PUNICODE_STRING imageName = NULL;
    static QUERY_INFO_PROCESS ZwQueryInformationProcess = NULL;

    //PAGED_CODE; // this eliminates the possibility of the IDLE Thread/Process

    if (NULL == ZwQueryInformationProcess)
    {
        UNICODE_STRING routineName;

        RtlInitUnicodeString(&routineName, L"ZwQueryInformationProcess");

        ZwQueryInformationProcess =
               (QUERY_INFO_PROCESS) MmGetSystemRoutineAddress(&routineName);

        if (NULL == ZwQueryInformationProcess) {
            KdPrint(("[%s?] %4u %s: Cannot get 'ZwQueryInformationProcess'.\n", CODE_ID));
            goto __FINALLY;
        }
    }

    Status = PsLookupProcessByProcessId(hProcessId, &pEprocess);
    if (!NT_SUCCESS(Status))
    {
        KdPrint(("[%s?] %4u %s: PsLookupProcessByProcessId failed(%s).\n",
                CODE_ID, StatusStr(Status)));
        goto __FINALLY;
    }

    Status = ObOpenObjectByPointer(pEprocess,           // Object
                                   OBJ_KERNEL_HANDLE,   //HandleAttributes
                                   NULL,                // PassedAccessState OPTIONAL
                                   GENERIC_READ,        // DesiredAccess
                                   *PsProcessType,      // ObjectType
                                   KernelMode,          // AccessMode
                                   &hProcess);
    if (!NT_SUCCESS(Status))
    {
        KdPrint(("[%s?] %4u %s: ObOpenObjectByPointer failed(%s).\n",
                CODE_ID, StatusStr(Status)));
        goto __FINALLY;
    }

    //
    // Step one - get the size we need
    //
    Status = ZwQueryInformationProcess( hProcess,
                                        ProcessImageFileName,
                                        NULL, // buffer
                                        0, // buffer size
                                        &returnedLength);


    if (STATUS_INFO_LENGTH_MISMATCH != Status)
    {
        KdPrint(("[%s?] %4u %s: ZwQueryInformationProcess@1 failed(%s).\n",
                CODE_ID, StatusStr(Status)));
        goto __FINALLY;
    }

    //
    // Is the passed-in buffer going to be big enough for us?
    // This function returns a single contguous buffer model...
    //
    bufferLength = returnedLength - sizeof(UNICODE_STRING);

    if (ProcessImagePath->MaximumLength < bufferLength)
    {
        ProcessImagePath->Length = (USHORT) bufferLength;
//        KdPrint(("[%s] %4u %s: STATUS_BUFFER_OVERFLOW \n", CODE_ID));
        Status = STATUS_BUFFER_OVERFLOW;
        goto __FINALLY;
    }

    //
    // If we get here, the buffer IS going to be big enough for us, so
    // let's allocate some storage.
    //
    buffer = ExAllocatePoolWithTag(PagedPool, returnedLength, 'ipgD');
    if (NULL == buffer)
    {
        KdPrint(("[%s?] %4u %s: STATUS_INSUFFICIENT_RESOURCES\n", CODE_ID));
        Status = STATUS_INSUFFICIENT_RESOURCES;
        goto __FINALLY;
    }

    //
    // Now lets go get the data
    //
    Status = ZwQueryInformationProcess( hProcess,
                                        ProcessImageFileName,
                                        buffer,
                                        returnedLength,
                                        &returnedLength);

    if (NT_SUCCESS(Status)) {
        //
        // Ah, we got what we needed
        //
        imageName = (PUNICODE_STRING) buffer;

        RtlCopyUnicodeString(ProcessImagePath, imageName);

    }
    else
    {
        KdPrint(("[%s?] %4u %s: ZwQueryInformationProcess@1 failed(%s).\n",
                CODE_ID, StatusStr(Status)));
    }

__FINALLY:

    if (NULL != pEprocess)
    {
        ObDereferenceObject(pEprocess);
    }

    if (NULL != hProcess)
    {
        ZwClose(hProcess);
    }

    //
    // free our buffer
    //
    if (NULL != buffer)
    {
        ExFreePool(buffer);
    }

    //
    // And tell the caller what happened.
    //
    return Status;
}

更新历史: 2012-4-5,修正错误结束分支中的资源泄漏bug