Search code examples
c#winapipinvokecreateprocess

Access violation calling CreateProcess in C#


I am trying to write C# code that, running in an elevated process, creates a non-elevated process. The (only) answer to the SO question How to call CreateProcess() with STARTUPINFOEX from C# and re-parent the child contains ready-to-use code. I copied this code, but instead of creating a process, it throws an AccessViolationException. The exception text Attempted to read or write protected memory. This is often an indication that other memory is corrupt. does not help to identify the culprit, however from low-level debugging I can see that the processor is trying to read from memory at a very small address like 0x00000004 which, of course, goes wrong.

The code, briefly explained, retrieves a handle of the desktop process, then calls InitializeProcThreadAttributeList and UpdateProcThreadAttribute to initialize the respective fields of a STARTUPINFOEX struct which is then passed into the CreateProcess Windows API function. CreateProcess should then create the new process as a child of the desktop process.

This function expects in its 9th parameter a pointer to either a STARTUPINFO or a STARTUPINFOEX (which contains a STATUPINFO at its begin). If it's a STARTUPINFOEX, the 6th parameter should contain the EXTENDED_STARTUPINFO_PRESENT flag.

When I don't pass the EXTENDED_STARTUPINFO_PRESENT flag so that CreateProcess think it's being passed just a STARTUPINFO, all works fine except that the created process is elevated (as is the calling process). However, as soon as I add this flag, the access violation occurs. For hours I have tried modifying parameter attributes etc., but the problem persists. I have the essentially same code running in C++, so I know it can work. What am I doing wrong?

The code below doesn't contain elaborated error checking, but it should compile and demonstrate the problem out of the box.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace RunNonElevatedCSharp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");
            StartNonElevated(@"C:\WINDOWS\system32\cmd.exe", "");
        }


        public static int StartNonElevated(string strAppPath, string strCommandLine)
        {
            bool bSuccess = false;
            IntPtr hShellProcess = IntPtr.Zero;
            var pInfo = new PROCESS_INFORMATION();
            var sInfoEx = new STARTUPINFOEX();
            sInfoEx.StartupInfo.cb = Marshal.SizeOf(sInfoEx);

            try
            {
                IntPtr hShellWnd = GetShellWindow();
                if (hShellWnd == IntPtr.Zero)
                {
                    return 0;
                }

                UInt32 pid;
                if (GetWindowThreadProcessId(hShellWnd, out pid) == 0)
                {
                    return 0;
                }

                hShellProcess = OpenProcess(PROCESS_CREATE_PROCESS, FALSE, pid);
                if (hShellProcess == IntPtr.Zero)
                {
                    return 0;
                }

                IntPtr nBufferSize = IntPtr.Zero;
                InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref nBufferSize);
                if (nBufferSize == IntPtr.Zero)
                {
                    return 0;
                }

                sInfoEx.lpAttributeList = Marshal.AllocHGlobal(nBufferSize);
                if (sInfoEx.lpAttributeList == IntPtr.Zero)
                {
                    return 0;
                }
                if (!InitializeProcThreadAttributeList(sInfoEx.lpAttributeList, 1, 0, ref nBufferSize))
                {
                    return 0;
                }

                if (!UpdateProcThreadAttribute(sInfoEx.lpAttributeList, 0, (IntPtr)PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, hShellProcess, (IntPtr)IntPtr.Size, IntPtr.Zero, IntPtr.Zero))
                {
                    return 0;
                }

                // s1 and s2 may not be required
                string s1 = "" + strAppPath + "";
                string s2 = "";

                // The next line causes an access violation unless you remove the 'EXTENDED_STARTUPINFO_PRESENT' flag
                if (!CreateProcess(s1, s2, IntPtr.Zero, IntPtr.Zero, false, CREATE_NEW_CONSOLE | EXTENDED_STARTUPINFO_PRESENT | 0,
                                   IntPtr.Zero, null, ref sInfoEx, out pInfo))
                {
                    return 0;
                }


                bSuccess = true;
                CloseHandle(pInfo.hThread);
                CloseHandle(pInfo.hProcess);
                return pInfo.dwProcessId;
            }
            finally
            {
                if (!bSuccess)
                {
                    var lastError = Marshal.GetLastWin32Error();
                    Debug.WriteLine("Error: " + lastError.ToString());
                }
                if (sInfoEx.lpAttributeList != IntPtr.Zero)
                {
                    DeleteProcThreadAttributeList(sInfoEx.lpAttributeList);
                    Marshal.FreeHGlobal(sInfoEx.lpAttributeList);
                }
                if (hShellProcess != IntPtr.Zero)
                    CloseHandle(hShellProcess);
            }
        }

        [DllImport("User32.dll", SetLastError = true)]
        public static extern IntPtr GetShellWindow();

        [DllImport("User32.dll", SetLastError = true)]
        public static extern UInt32 GetWindowThreadProcessId(IntPtr hWnd, out UInt32 lpdwProcessId);

        [DllImport("Kernel32.dll", SetLastError = true)]
        public static extern IntPtr OpenProcess(UInt32 dwDesiredAccess, int bInheritHandle, UInt32 dwProcessId);

        [DllImport("kernel32.dll", SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool InitializeProcThreadAttributeList(IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize);

        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UpdateProcThreadAttribute(IntPtr lpAttributeList, uint dwFlags, IntPtr Attribute, IntPtr lpValue, IntPtr cbSize,
                                                     IntPtr lpPreviousValue, IntPtr lpReturnSize);

        [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateProcessW", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool CreateProcess(string lpApplicationName, string lpCommandLine,
                                 [In, Optional] IntPtr lpProcessAttributes, [In, Optional] IntPtr lpThreadAttributes,
                                 bool bInheritHandles, uint dwCreationFlags,
                                 [In, Optional] IntPtr lpEnvironment, [In, Optional] string lpCurrentDirectory,
                                 [In] ref STARTUPINFOEX lpStartupInfo, [Out] out PROCESS_INFORMATION lpProcessInformation);

        [DllImport("Kernel32.dll", SetLastError = true)]
        public static extern int CloseHandle(IntPtr hObject);

        [DllImport("kernel32.dll")]
        private static extern void DeleteProcThreadAttributeList(IntPtr lpAttributeList);

        public const UInt32 PROCESS_CREATE_PROCESS = 0x0080;
        public const int FALSE = 0;
        public const int PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x00020000;
        public const uint CREATE_NEW_CONSOLE = 0x00000010;
        public const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public int dwProcessId;
        public int dwThreadId;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct STARTUPINFOEX
    {
        public STARTUPINFO StartupInfo;
        public IntPtr lpAttributeList;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    public struct STARTUPINFO
    {
        public Int32 cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public Int32 dwX;
        public Int32 dwY;
        public Int32 dwXSize;
        public Int32 dwYSize;
        public Int32 dwXCountChars;
        public Int32 dwYCountChars;
        public Int32 dwFillAttribute;
        public Int32 dwFlags;
        public Int16 wShowWindow;
        public Int16 cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }

}

When the access violation occurs, the program cannot continue; the "finally" block isn't executed.

Looking forward to any replies...

Hans


Solution

  • Your call to UpdateProcThreadAttribute() is wrong. The 4th parameter needs the address of hShellProcess, not the value. This is even stated as much in this answer to the other question you linked to (the code in the other question has the same bug):

    Second, the lpValue parameter of the UpdateProcThreadAttribute function must be a pointer to the attribute value (in your case, parentHandle), not the value itself.

    The documentation for PROC_THREAD_ATTRIBUTE_PARENT_PROCESS says:

    The lpValue parameter is a pointer to a handle to a process to use instead of the calling process as the parent for the process being created. The process to use must have the PROCESS_CREATE_PROCESS access right.

    You said that you copied the other answer's code, but your code does not look like the other answer's code, and certainly does not contain the fix that the other answer had provided.

    In your code, change this:

    if (!UpdateProcThreadAttribute(..., hShellProcess, ...))
    

    To this:

    IntPtr lpValue = IntPtr.Zero;
    ...
    lpValue = Marshal.AllocHGlobal(IntPtr.Size);
    Marshal.WriteIntPtr(lpValue, hShellProcess);
    if (!UpdateProcThreadAttribute(..., lpValue, ...))
    ...
    Marshal.FreeHGlobal(lpValue);