dev.nlited.com

>>

NetMon: Process Information

<<<< prev
next >>>>

2015-03-22 19:28:03 chip Page 1219 📢 PUBLIC

March 22 2015

NetMon has become quite stable recently and I am trying to break out of the VS cocoon, running it from the command line. This has naturally introduced a number of new problems.

First, I want to be able to run the user-mode component without requiring admin privileges. The problem of loading the driver has been solved, for the most part, by creating the "autoload" service that loads NetMon.sys at boot. The problem is that NetMonUI.exe is launched and run as the current user. I am currently querying process information in the NetMonUI process, external and after-the-fact with respect to the actual operation. When NetMonUI is a non-admin process, attempting to query information about other processes fail with "access denied".

I see two solutions to this problem:

  1. User-mode Service: A user-mode process is created as a service that automatically launches at startup under admin privileges. This process then loads the driver as needed and queries process information on behalf of the UI program (which is running as the current user). This is the preferred, standard method. It will require a significant rewrite of the NetMon project to migrate code out of NetMonUI.exe to NetMonSvc.exe and the creation of an inter-process communications channel (named pipe?) between them.
  2. Kernel-mode Queries: Determine the process information in NetMon.sys and relay it to NetMonUI through an internal IOCTL command. This would require minimal changes to the existing project. However, the functions normally used to do this (ZwQueryInformationProcess) have been deprecated in Windows 8. While they still exist as undocumented functions, there is a good chance trying to use them in Windows10 will cause problems.

I think the most expeditious route is #2, using the well-(un)documented ZwQueryProcessInformation() and PEB methods. The code should not be huge and can easily be removed later. This will get me to the goal of a easily installed demo disk the quickest.

Eventually I will need a user-mode service, for this and many other reasons. But this is a significant task and I have other tasks that are higher priority.

=====

ZwQueryProcessInformation()

ntndis.com has a great thread about using ZwQueryProcessInformation().

It appears ZwQueryProcessInformation() has been completely expunged from the Windows 8.0 WDK.

Another thread about this exact problem, dated December 10 2013. Leaked source from both Kaspersky and eSet show they are still relying on ZwQueryProcessInformation().

eSet: #include typedef NTSTATUS (__stdcall *QUERY_INFO_PROCESS)( __in HANDLE ProcessHandle, __in PROCESSINFOCLASS ProcessInformationClass, __out PVOID ProcessInformation, __in ULONG ProcessInformationLength, __out_opt PULONG ReturnLength ); QUERY_INFO_PROCESS ZwQueryInformationProcess = NULL; NTSTATUS GetProcessImageName(HANDLE ProcessHandle, PUNICODE_STRING ProcessImageName) { NTSTATUS status = STATUS_ACCESS_DENIED; PUNICODE_STRING imageName = NULL; ULONG returnedLength = 0; ULONG bufferLength = 0; PVOID buffer = NULL; if(ZwQueryInformationProcess == NULL) { UNICODE_STRING routineName; RtlInitUnicodeString(&routineName, L"ZwQueryInformationProcess"); ZwQueryInformationProcess = (QUERY_INFO_PROCESS) MmGetSystemRoutineAddress(&routineName); if (NULL == ZwQueryInformationProcess) { return STATUS_INSUFFICIENT_RESOURCES; } } status = ZwQueryInformationProcess(ProcessHandle, ProcessImageFileName, NULL, 0, &returnedLength); if(STATUS_INFO_LENGTH_MISMATCH != status) { return status; } bufferLength = returnedLength - sizeof(UNICODE_STRING); if(ProcessImageName->MaximumLength < bufferLength) { ProcessImageName->Length = (USHORT) bufferLength; return STATUS_BUFFER_OVERFLOW; } buffer = ExAllocatePoolWithTag(PagedPool, returnedLength, 'ipgD'); if(NULL == buffer) { return STATUS_INSUFFICIENT_RESOURCES; } status = ZwQueryInformationProcess(ProcessHandle, ProcessImageFileName, buffer, returnedLength, &returnedLength); if(NT_SUCCESS(status)) { imageName = (PUNICODE_STRING) buffer; RtlCopyUnicodeString(ProcessImageName, imageName); } ExFreePool(buffer); return status; } BOOLEAN RetrieveProcessNameByID(HANDLE ProcessId, PUNICODE_STRING pusImageFileName) { UNICODE_STRING ProcImgName = {0}; HANDLE hProcessHandle = NULL; NTSTATUS status = STATUS_ACCESS_DENIED; PEPROCESS eProcess = NULL; int iEntryIndex = -1; status = PsLookupProcessByProcessId(ProcessId, &eProcess); if((!NT_SUCCESS(status)) || (!eProcess)) { return FALSE; } status = ObOpenObjectByPointer(eProcess, 0, NULL, 0, 0, KernelMode, &hProcessHandle); if((!NT_SUCCESS(status)) || (!hProcessHandle)) { ObDereferenceObject(eProcess); return FALSE; } ProcImgName.Length = 0; ProcImgName.MaximumLength = 1024; ProcImgName.Buffer = ExAllocatePoolWithTag(NonPagedPool, ProcImgName.MaximumLength, '2leN'); if(ProcImgName.Buffer == NULL) { ZwClose(hProcessHandle); ObDereferenceObject(eProcess); return FALSE; } RtlZeroMemory( ProcImgName.Buffer, ProcImgName.MaximumLength ) ; status = GetProcessImageName(hProcessHandle, &ProcImgName); if(!NT_SUCCESS(status)) { DbgPrint("[NotifyProcessCreate] GetProcessImageName failed (0x%08x)\n", status); ExFreePoolWithTag(ProcImgName.Buffer, '2leN'); ZwClose(hProcessHandle); ObDereferenceObject(eProcess); return FALSE; } if(pusImageFileName) { RtlCopyUnicodeString(pusImageFileName, &ProcImgName); } ExFreePoolWithTag(ProcImgName.Buffer, '2leN'); ZwClose(hProcessHandle); ObDereferenceObject(eProcess); return TRUE; }

Well, that didn't work:
NetMon|ERR WRN[-27]: ProcFindFunction: System function 'ZwQueryProcessInformation' not found.

Oops... I transposed the name. It should be "ZwQueryInformationProcess" not "ZwQueryProcessInformation". It is working now!

This task took about four hours, three of which were consumed by my confusion around UNICODE_STRING. It still confuses me, as the usage seems to be inconsistent. This is the definition of UNICODE_STRING:
typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING;
From this I would assume I need to set Buffer to point to the allocated memory, which may or may not follow the UNICODE_STRING header. However, when I try to use it this way the system blows up with a BSoD. So I tried to allocate a buffer for the entire UNICODE_STRING, header followed by data in a contiguous block. This blows up as well. (I may need to still set Buffer to point to the beginning of the string?)

Then there is confusion around ZwQueryInformationProcess(). This is the MSDN reference:

When the ProcessInformationClass parameter is ProcessImageFileName, the buffer pointed to by the ProcessInformation parameter should be large enough to hold a UNICODE_STRING structure as well as the string itself. The string stored in the Buffer member is the name of the image file.

If the buffer is too small, the function fails with the STATUS_INFO_LENGTH_MISMATCH error code and the ReturnLength parameter is set to the required buffer size.

So I allocated a block of memory of sizeof(UNICODE_STRING)+(MAX_FNAME+1)*2 bytes and initialize the UNICODE_STRING header. This BugChecks with "BAD_POOL_CALLER" with a subcode of "double-free".

Eventually, I use the eSet code (almost) exactly as-is. Finally, something works. It turns out that the UNICODE_STRING header will be set inside ZwQueryInformationProcess(), all I need to do is provide the raw memory buffer. So now I have something that has been running for about four hours.

Here is the final working version:

Process.cpp: /*************************************************************************/ /** Process.cpp: Query process information. **/ /** (C)2015 nlited systems inc, Chip Doran **/ /*************************************************************************/ #include <ntifs.h> #include <ntstrsafe.h> #include <WinDef.h> #include "StdTypes.h" #include "Errors.h" #include "VerID.h" #include "OS.h" #include "Globals.h" typedef NTSTATUS (__stdcall *ZWQUERYINFORMATIONPROCESS)( __in HANDLE ProcessHandle, __in PROCESSINFOCLASS ProcessInformationClass, __out PVOID ProcessInformation, __in ULONG ProcessInformationLength, __out_opt PULONG ReturnLength ); static ZWQUERYINFORMATIONPROCESS _ZwQueryInformationProcess= 0; static void *ProcFindFunction(const WCHAR *Name); /*************************************************************************/ /** Public interface **/ /*************************************************************************/ int ProcGetImagePath(UINT64 ProcID, WCHAR *pDst, UINT DstSz) { int Err= ERR_OK; NTSTATUS Status; PEPROCESS eProc=0; HANDLE hProc=0; UNICODE_STRING *pName=0; ULONG MaxLength= 1024; //Buffer length (bytes) if(!_ZwQueryInformationProcess) _ZwQueryInformationProcess= (ZWQUERYINFORMATIONPROCESS)ProcFindFunction(L"ZwQueryInformationProcess"); if(!_ZwQueryInformationProcess) { Err= WarnA(ERR_NOT_SUPPORTED,FUNCA "Bummer, ZwQueryInformationProcess not found."); } else if(!NT_SUCCESS(Status= PsLookupProcessByProcessId((HANDLE)ProcID,&eProc))) { Err= WarnA(ERR_NOT_FOUND,FUNCA"ProcID %X not found.",ProcID); } else if(!NT_SUCCESS(Status= ObOpenObjectByPointer(eProc,0,0,0,0,KernelMode,&hProc)) || !hProc) { Err= WarnA(ERR_NOT_FOUND,FUNCA"Unable to transform eProc[%X] to hProc.",eProc); } else if(!(pName= (UNICODE_STRING*)ExAllocatePoolWithTag(NonPagedPool,MaxLength,'ipgD'))) { Err= WarnA(ERR_NO_MEM,FUNCA"Unable to alloc %d.",MaxLength); } else { RtlZeroMemory(pName,MaxLength); //pName will be initialized by ZwQuery() if(!NT_SUCCESS(Status= _ZwQueryInformationProcess(hProc,ProcessImageFileName,pName,MaxLength,&MaxLength))) { Err= WarnA(ERR_FAILED,FUNCA"ZwQueryInformationProcess failed. [%X]",Status); } else if(!pName->Length) { Err= WarnA(ERR_NOT_FOUND,FUNCA"No info for ProcID[%X]",ProcID); } else { PrintA(PRINT_DEBUG,FUNCA"ProcID[%X] is '%wZ'",ProcID,pName); RtlStringCchCopyUnicodeString(pDst,DstSz,pName); } ExFreePoolWithTag(pName,'ipgD'); } if(hProc) ZwClose(hProc); if(eProc) ObDereferenceObject(eProc); return(Err); } /*************************************************************************/ /** Public internals **/ /*************************************************************************/ static void *ProcFindFunction(const WCHAR *Name) { UNICODE_STRING NameU; void *pFunc; RtlInitUnicodeString(&NameU,Name); pFunc= MmGetSystemRoutineAddress(&NameU); if(!pFunc) WarnA(ERR_NOT_SUPPORTED,FUNCA" System function '%S' not found.",Name); return(pFunc); }

I am also trying to figure out how to elevate my security rights when NetMonUI is started as a normal user: RunAs



WebV7 (C)2018 nlited | Rendered by tikope in 35.037ms | 44.220.184.63