Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include NtQueryInformationProcess function and its associated structures like PROCESS_BASIC_INFORMATION/PEB/RTL_USER_PROCESS_PARAMETERS #123

Closed
Enigmatrix opened this issue May 17, 2020 · 21 comments

Comments

@Enigmatrix
Copy link

Is your feature request related to a problem? Please describe.
I would request that we include NtQueryInformationProcess into Vanara.PInvoke.NTDll, and also put the associated structures for the function in suitable locations.

On a side note, given a UNICODE_STRING from the Vanara library, can I read its buffer contents from another process in an idiomatic way?

@dahall
Copy link
Owner

dahall commented May 17, 2020

On you second question, I believe the implementation of UNICODE_STRING should allow it to work across process boundaries. My only caution would be that if it was created in native code (through a P/Invoke call) then the pointer to the string field may not persist. If you copy the structure first, then you should always be safe.

@dahall
Copy link
Owner

dahall commented May 18, 2020

Will you please reply with a list of the names of the associated structures you'd like?

@Enigmatrix
Copy link
Author

Functions
NtQueryInformationProcess: official, pinvoke.net

Structures
PROCESS_BASIC_INFORMATION: pinvoke.net
PEB: official
RTL_USER_PROCESS_PARAMETERS: official

Enums
PROCESSINFOCLASS: pinvoke.net

My only caution would be that if it was creating in native code (through a P/Invoke call) then the pointer to the string field may not persist.

Well, the UNICODE_STRING is a field of the RTL_USER_PROCESS_PARAMETERS returned when we invoke ReadProcessMemory to read the structure out, so the Buffer field of UNICODE_STRING is pointing to a string in the other process. Is there any way to get the string as IntPtr?

dahall added a commit that referenced this issue May 20, 2020
@dahall
Copy link
Owner

dahall commented May 20, 2020

Thanks for all the links. That saved me some time implementing. It appears that there only a few of the info class values supported so I only included those and not the entire list on pinvoke.net. Please see the unit test for use.

This will go out in the next release, but feel free to pull the interim NuGet package from AppVeyor (see home page for link and details).

@dahall dahall closed this as completed May 20, 2020
@Enigmatrix
Copy link
Author

Not too sure whether I should make a new issue or not, but here I go ...

I've made this method below from the nuget package source, with the package version 3.2.8 linked in the home page

public string GetCommandLine(Kernel32.SafeHPROCESS handle)
{
    var info = NtDll.NtQueryInformationProcess<NtDll.PROCESS_BASIC_INFORMATION>(handle,
        NtDll.PROCESSINFOCLASS.ProcessBasicInformation);

    var pebSz = Marshal.SizeOf<NtDll.PEB>();
    var pebPtr = Marshal.AllocHGlobal(pebSz);
    Kernel32.ReadProcessMemory(handle, info.AsRef().PebBaseAddress, pebPtr, pebSz, out var pebSzRead).CheckValid();
    var peb = Marshal.PtrToStructure<NtDll.PEB>(pebPtr);
    Marshal.FreeHGlobal(pebPtr);

    var rtlUserParamsSz = Marshal.SizeOf<NtDll.RTL_USER_PROCESS_PARAMETERS>();
    var rtlUserParamsPtr = Marshal.AllocHGlobal(rtlUserParamsSz);
    Kernel32.ReadProcessMemory(handle, peb.ProcessParameters, rtlUserParamsPtr, rtlUserParamsSz,
        out var rtlUserParamsRead).CheckValid();
    /* errors out below with ExecutionEngineException*/
    var rtlUserParams = Marshal.PtrToStructure<NtDll.RTL_USER_PROCESS_PARAMETERS>(rtlUserParamsPtr);
    Marshal.FreeHGlobal(rtlUserParamsPtr);

    return rtlUserParams.CommandLine.Buffer; // lets test if this works
}

The last Marshal.PtrToStructure errors out with ExecutionEngineException. After some investigation, I stubled across this, which says that the StructLayout attribute should be present.

Looking at the source of the structure of NtDll.RTL_USER_PROCESS_PARAMETERS with JetBrains decompiler, it shows this:

/// <summary>
/// <para>[This structure may be altered in future versions of Windows.]</para>
/// <para>Contains process parameter information.</para>
/// </summary>
[PInvokeData("winternl.h", MSDNShortId = "e736aefa-9945-4526-84d8-adb6e82b9991")]
public struct RTL_USER_PROCESS_PARAMETERS
{
  /// <summary>Reserved for internal use by the operating system.</summary>
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
  private readonly byte[] Reserved1;
  /// <summary>Reserved for internal use by the operating system.</summary>
  [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
  private readonly IntPtr[] Reserved2;
  /// <summary>The path of the image file for the process.</summary>
  public NtDll.UNICODE_STRING ImagePathName;
  /// <summary>The command-line string passed to the process.</summary>
  public NtDll.UNICODE_STRING CommandLine;
}

it has the StructLayout attribute missing, whereas the source code from the recent commit has this attribute. So somehow, either the attribute gets stripped when compiling in AppVeyor or it is some other runtime issue

For your information, I'm using this code in .NET Core 3.1

@dahall
Copy link
Owner

dahall commented May 21, 2020

I put a link in my last post to the unit test. It gets the command line successfully.

@Enigmatrix
Copy link
Author

But for reading the command line from another process, I have to use ReadProcessMemory using the pointers given in the structure. So from PROCESS_BASIC_INFORMATION->PebBaseAddress to a PEB struct, PEB->ProcessParameters to RTL_USER_PROCESS_PARAMETERS and RTL_USER_PROCESS_PARAMETERS.CommandLine->Buffer to a string, all of these need to be fetched via ReadProcessMemory as the pointers are not pointing to locations in our own process memory.
Using ToStructure on rpbi.PbBaseAddress will just yield ExecutionEngineException, which is why I wrote code like this:

public string GetCommandLine(Kernel32.SafeHPROCESS handle)
{
    var info = NtDll.NtQueryInformationProcess<NtDll.PROCESS_BASIC_INFORMATION>(handle,
        NtDll.PROCESSINFOCLASS.ProcessBasicInformation);

    var pebSz = Marshal.SizeOf<NtDll.PEB>();
    var pebPtr = Marshal.AllocHGlobal(pebSz);
    Kernel32.ReadProcessMemory(handle, info.AsRef().PebBaseAddress, pebPtr, pebSz, out var pebSzRead).CheckValid();
    var peb = Marshal.PtrToStructure<NtDll.PEB>(pebPtr);
    Marshal.FreeHGlobal(pebPtr);

    var rtlUserParamsSz = Marshal.SizeOf<NtDll.RTL_USER_PROCESS_PARAMETERS>();
    var rtlUserParamsPtr = Marshal.AllocHGlobal(rtlUserParamsSz);
    Kernel32.ReadProcessMemory(handle, peb.ProcessParameters, rtlUserParamsPtr, rtlUserParamsSz,
        out var rtlUserParamsRead).CheckValid();
    /* errors out below with ExecutionEngineException*/
    var rtlUserParams = Marshal.PtrToStructure<NtDll.RTL_USER_PROCESS_PARAMETERS>(rtlUserParamsPtr);
    Marshal.FreeHGlobal(rtlUserParamsPtr);

    return rtlUserParams.CommandLine.Buffer; // lets test if this works
}

but like I mentioned in an above reply, gets me ExecutionEngineException for another reason.

@dahall dahall reopened this May 25, 2020
@dahall
Copy link
Owner

dahall commented May 26, 2020

I ran the following test without errors, but with bad strings:

[Test]
public void GetCommandLineTest()
{
   var randProc = System.Diagnostics.Process.GetProcesses().Where(p => p.ProcessName.StartsWith("devenv")).First();
   using var hProc = Kernel32.OpenProcess((uint)(Kernel32.ProcessAccess.PROCESS_QUERY_INFORMATION | Kernel32.ProcessAccess.PROCESS_VM_READ), false, (uint)randProc.Id);
   Assert.That(hProc, ResultIs.ValidHandle);

   NtQueryResult<PROCESS_BASIC_INFORMATION> info = null;
   Assert.That(() => info = NtQueryInformationProcess<PROCESS_BASIC_INFORMATION>(hProc, PROCESSINFOCLASS.ProcessBasicInformation), Throws.Nothing);
   Assert.That(info, Is.Not.Null);
   Assert.That(info.AsRef().PebBaseAddress, Is.Not.EqualTo(IntPtr.Zero));

   using var pebPtr = new SafeHGlobalStruct<PEB>();
   Assert.That(Kernel32.ReadProcessMemory(hProc, info.AsRef().PebBaseAddress, pebPtr, pebPtr.Size, out var pebSzRead), ResultIs.Successful);
   Assert.That(pebSzRead, Is.LessThanOrEqualTo(pebPtr.Size));

   using var rtlUserParamsPtr = new SafeHGlobalStruct<RTL_USER_PROCESS_PARAMETERS>();
   Assert.That(Kernel32.ReadProcessMemory(hProc, pebPtr.Value.ProcessParameters, rtlUserParamsPtr, rtlUserParamsPtr.Size, out var rtlUserParamsRead), ResultIs.Successful);
   Assert.That(rtlUserParamsRead, Is.LessThanOrEqualTo(rtlUserParamsPtr.Size));
   var rtlUser = rtlUserParamsPtr.Value;
   Assert.That(rtlUser.ImagePathName.Length, Is.GreaterThan(0));
   StringAssert.StartsWith("C:\\", rtlUser.ImagePathName.ToString());
   TestContext.WriteLine($"Img: {rtlUser.ImagePathName}; CmdLine: {rtlUser.CommandLine}");
}

I then searched for a reason and found this on StackOverflow. It appears this is not a reliable approach as the memory can change.

@Enigmatrix
Copy link
Author

Hi, can I know what platform that test is run on, and the output of the test? What do you mean by 'bad strings?'

@dahall
Copy link
Owner

dahall commented May 27, 2020

Win10 1903 with Admin privileges. Test compiled as Any CPU. rtlUser.ImagePathName.Length was 266, but the rtlUser.ImagePathName.Buffer returned String.Empty. When I put it into debug mode, I can see that the bytes behind rtlUser.ImagePathName.Buffer were 00 da 34 02 fe 12... (apparently random values and not ASCII or Unicode characters).

@Enigmatrix
Copy link
Author

I believe that rtlUser.ImagePathName.Buffer still points to the foreign process' memory, so when the Marshaller converts that pointer to a string it just points to our own process memory instead.

The Marshaller has no idea that the UNICODE_STRING struct was actually read from other processes' memory. I think we need to set Buffer to type IntPtr and let the ToString() overload in UNICODE_STRING handle the 'normal' cases when Buffer points to our own memory, using Marshal.PtrToStringUni (UNICODE_STRING is meant to hold Windows wide characters). For own case, we need to call one more ReadProcessMemory to read the Buffer to a managed string. This is in line with other cases that I found online such as this one

dahall added a commit that referenced this issue May 28, 2020
@dahall
Copy link
Owner

dahall commented May 28, 2020

Great suggestion. It is now working. See unit test for code.

@dahall dahall closed this as completed May 28, 2020
@ThomasLebrun
Copy link

Hi @dahall, @Enigmatrix !

I'm trying to use the same code as the one in your unit test but I'm always getting a string.Empty result when trying to get the command line:

public static string GetCommandLine(HWND hWnd)
        {
            GetWindowThreadProcessId(hWnd, out var processId);

            using var process = OpenProcess((uint)(ProcessAccess.PROCESS_QUERY_INFORMATION | ProcessAccess.PROCESS_VM_READ), false, processId);
            if (process.IsNull || process.IsInvalid)
            {
                return string.Empty;
            }

            var processInfos = NtQueryInformationProcess<PROCESS_BASIC_INFORMATION>(process, PROCESSINFOCLASS.ProcessBasicInformation);
            if (processInfos == null || processInfos.IsInvalid || processInfos.AsRef().PebBaseAddress == IntPtr.Zero)
            {
                return string.Empty;
            }

            using var pebPtr = new SafeHGlobalStruct<PEB>();
            if (!ReadProcessMemory(process, processInfos.AsRef().PebBaseAddress, pebPtr, pebPtr.Size, out _))
            {
                return string.Empty;
            }

            using var processParameters = new SafeHGlobalStruct<RTL_USER_PROCESS_PARAMETERS>();
            if (!ReadProcessMemory(process, pebPtr.Value.ProcessParameters, processParameters, processParameters.Size, out _))
            {
                return string.Empty;
            }

            var parameters = processParameters.Value;

            var u = GetString(parameters.ImagePathName);
            var v = GetString(parameters.CommandLine);

            // Here, "u" and "v" are emtpy
            return GetString(parameters.CommandLine);

            string GetString(in UNICODE_STRING us)
            {
                using var mem = new SafeCoTaskMemString(us.MaximumLength);
                if (!ReadProcessMemory(process, us.Buffer, mem, mem.Size, out _))
                {
                    return string.Empty;
                }

                return mem;
            }
        }

Do yo have any ideads what could be wrong ? I'm using Win10 20H2, the code is running in AnyCPU / Debug

Thanks !

@dahall
Copy link
Owner

dahall commented Jan 25, 2021

@ThomasLebrun When you debug, which of the four failure conditions is returning string.Empty?

@dahall
Copy link
Owner

dahall commented Jan 25, 2021

@ThomasLebrun I ran the following unit test, which I adapted from your code, with success. I ran this with elevated privileges.

Process proc = null;
try
{
   proc = Process.Start("notepad.exe", TestCaseSources.LogFile);
   System.Threading.Thread.Sleep(500);
   HWND hWnd = proc.MainWindowHandle;

   unsafe
   {
      using var processInfos = NtQueryInformationProcess<PROCESS_BASIC_INFORMATION>(proc.Handle, PROCESSINFOCLASS.ProcessBasicInformation);
      Assert.That(processInfos, ResultIs.ValidHandle);
      Assert.That(((PROCESS_BASIC_INFORMATION*)processInfos)->PebBaseAddress, Is.Not.EqualTo(IntPtr.Zero));

      using var pebPtr = new SafeHGlobalStruct<PEB>();
      Assert.That(Kernel32.ReadProcessMemory(proc.Handle, ((PROCESS_BASIC_INFORMATION*)processInfos)->PebBaseAddress, pebPtr, pebPtr.Size, out _), ResultIs.Successful);

      using var processParameters = new SafeHGlobalStruct<RTL_USER_PROCESS_PARAMETERS>();
      Assert.That(Kernel32.ReadProcessMemory(proc.Handle, pebPtr.Value.ProcessParameters, processParameters, processParameters.Size, out _), ResultIs.Successful);

      var parameters = processParameters.Value;
      Assert.That(GetString(parameters.ImagePathName, proc.Handle), Is.Not.Empty);
      Assert.That(GetString(parameters.CommandLine, proc.Handle), Is.Not.Empty);
   }
}
finally
{
   proc?.CloseMainWindow();
}

@ThomasLebrun
Copy link

@dahall I'm giving a try to your solution => do you have the definition of the GetString method you're using ?

@ThomasLebrun
Copy link

@ThomasLebrun When you debug, which of the four failure conditions is returning string.Empty?

None of the conditions where returning string.Empty: it was the GetString method

@dahall
Copy link
Owner

dahall commented Jan 25, 2021

public static string GetString(in UNICODE_STRING us, HPROCESS hProc)
{
	using var mem = new SafeCoTaskMemString(us.MaximumLength);
	return ReadProcessMemory(hProc, us.Buffer, mem, mem.Size, out _) ? mem : string.Empty;
}

@ThomasLebrun
Copy link

Here is the code I'm using:

`unsafe
{
using var process = OpenProcess((int)(ProcessAccess.PROCESS_QUERY_INFORMATION | ProcessAccess.PROCESS_VM_READ), false, processId);

                using var processInfos = NtQueryInformationProcess<PROCESS_BASIC_INFORMATION>(process, PROCESSINFOCLASS.ProcessBasicInformation);
            
                using var pebPtr = new SafeHGlobalStruct<PEB>();
                ReadProcessMemory(process, ((PROCESS_BASIC_INFORMATION*)processInfos)->PebBaseAddress, pebPtr, pebPtr.Size, out _);
                
                using var processParameters = new SafeHGlobalStruct<RTL_USER_PROCESS_PARAMETERS>();
                ReadProcessMemory(process, pebPtr.Value.ProcessParameters, processParameters, processParameters.Size, out _);
                
                var parameters = processParameters.Value;

                var s = GetString(parameters.ImagePathName, process);
                var s2 = GetString(parameters.CommandLine, process);

                return s2;
            }`

s is string.Empty and s2 is null:
image

@ThomasLebrun
Copy link

ThomasLebrun commented Jan 25, 2021

Using the code available here (https://stackoverflow.com/a/16142791/1438337), I get the correct CommandLine.

I've noticed there is a difference between this code and your: there is an offset when reading PebBaseAddress and when getting UNICODE_STRING_WOW64 / RTL_USER_PROCESS_PARAMETERS (offseet with different values according to x32/x64): maybe that could be the issue ?

I would like, if possible, not having to do the P/Invoke import on my side and rely on your lib instead :)

@dahall dahall reopened this Jan 25, 2021
dahall added a commit that referenced this issue Jan 26, 2021
@dahall
Copy link
Owner

dahall commented Jan 26, 2021

Thank you for the link. After reviewing and trying to fix, I have a stronger desire to avoid NtDll. :) Effectively, the problem comes when running as a 64-bit process and trying to query information from a 32-bit process or running as a 32-bit process and trying to query information from a 64-bit process. If they match, then the code I've published works fine. So, what I have done is added xx_WOW64 structures to address the latter and throw an exception for the former. I've added a function to Vanara.PInvoke.NtDll called NtQueryInformationProcessRequiresWow64Structs that will look at the current process and queried process to tell you if you have to use the WOW64 variants of the structures. So the code would look like:

string imgName = null;
if (NtQueryInformationProcessRequiresWow64Structs(hProcToQuery))
{
   var pfn = NtQueryInformationProcess<UNICODE_STRING_WOW64>(hProc, PROCESSINFOCLASS.ProcessImageFileName);
   imgName = pfn.Value.ToString(hProcToQuery);
}
else
{
   var pfn = NtQueryInformationProcess<UNICODE_STRING>(hProc, PROCESSINFOCLASS.ProcessImageFileName);
   imgName = pfn.Value.ToString(hProcToQuery);
}

This is all the work I'm willing to do on this. These old libs that were written in 32-bit days and forced into the 64-bit world are just a mess. If you decide to do further work, I'm glad to include via a PR.

@dahall dahall closed this as completed Jan 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants