CreateService API Bypasses Service Permissions

First of all I should point out that this isn’t really an exploit or security issue, its just some slightly weird behaviour that I found interesting.

I started looking at this after I read an interesting write up from 0xdf for the Hack The Box machine named Resolute, which you can find here: https://0xdf.gitlab.io/2020/06/01/resolute-more-beyond-root.html

The Problem

Unlike myself and most others, 0xdf didn’t use the DNS Admins group as the path to root on this HTB machine (see my video here if you’re not sure what that path is). Instead he took advantage of the fact that our user account had permission to create new services.

So we should just be able to create a new service, set it to run as Local System, tell it to run some malicious exe as the service executable, then start the service and boom, we have Local System access. Right? Not so fast.

Let’s try and do that using the built in sc.exe program in Windows (after granting my account permission to create new services, like we had on the HTB machine). First of all we create the service:

sc.exe create MyService binPath= "C:\nc.exe etc etc"

This completes successfully, but now when we try to start the service with the following command we get Access Denied:

sc.exe start MyService

This is because the default ACL assigned to new services does not allow regular users to start the service. We can see this if we look at the ACL on our new service with the sdshow option:

If you’re anything like me you probably struggle to read SDDL, so here’s the same service ACL shown in this GUI tool:

The only group in the ACL that a regular user is ever likely to find themselves in is the Interactive group, and that does not have permission to start the service as you can see in the screenshot above.

How You Normally Start Services

So the new service’s ACL says we can’t start the service… however it turns out there is actually a very simple way we can bypass the ACL entirely for our new service and be allowed full access to it.

But first of all let’s take a look at what happens when you use a tool like sc.exe to try and start a service. Essentially any program like that will do the following:

1: Get a handle to the SCM (Service Control Manager) by calling OpenSCManager

2: Get a handle to the service we want to start by calling OpenService and passing in the handle we got in step 1

3: Call StartService and pass in the handle we got in step 2

In each of the steps that involve requesting a handle, we have to specify what level of access we want for the service/SCM and if we don’t have the permissions required then the call will fail with access denied. You can find all of the different permissions here.

So in our example where we can’t start the service, we’re actually failing at step 2 because we’re going to request a handle with a desired access level of “SERVICE_START” and this will fail as we don’t have that permission. You can see in our original error message earlier it does actually mention that it is the call to OpenService that failed:

So we never get a handle to the service and therefore can’t perform step 3 which is passing the handle to the StartService function.

Bypassing The ACL For A New Service

So we know that we can’t create a new service and then get a handle to it that will let us start it, but it turns out when we create a new service we get a handle to the service returned back to us by the CreateService function.

At first glance this doesn’t seem very useful because the CreateService function requires us to specify the level of access this handle should be for just like OpenService does. So you would think if we asked CreateService to return us a handle with permission to start the new service, it would fail because the default service ACL doesn’t allow us to do that… but that is not the case!

We can actually request whatever level of access we like to the new service and CreateService will return a handle with that level of access. So then we can pass that handle to StartService or whatever other service functions we want and it will work perfectly fine without any access denied errors.

I’ve written some example .NET code that demonstrates this (full code with error handling etc can be found on my github here) :

            ' Connect to SCM (Service Control Manager) and get a handle that allows us to create services by specifying SC_MANAGER_CREATE_SERVICE. 
            ' Would fail with access denied here if we don't have permission to create services
            ScmHandle = WinApi.OpenSCManager(Nothing, Nothing, WinApi.SCM_RIGHTS.SC_MANAGER_CREATE_SERVICE)

            ' Create a new service and get back a handle to the service (that can then be used with StartService function and other service functions). 
            ' We specify SERVICE_ALL_ACCESS so that this handle allows us full control over the service (to start it, stop it, delete it, etc) and for some
            ' reason this works. If we try to request this level of access afterwards by using the OpenService function, it fails with access denied
            ServiceHandle = WinApi.CreateService(ScmHandle,
                                                ServiceName,
                                                Nothing,
                                                WinApi.SERVICE_RIGHTS.SERVICE_ALL_ACCESS,
                                                WinApi.SERVICE_WIN32_OWN_PROCESS,
                                                WinApi.SERVICE_DEMAND_START,
                                                WinApi.SERVICE_ERROR_NORMAL,
                                                ExePath,
                                                Nothing, Nothing, Nothing, "NT Authority\System", Nothing)

            ' If we successfully created the service, attempt to start it using the handle we got back from CreateService. 
            If Not ServiceHandle = IntPtr.Zero Then
                WinApi.StartService(ServiceHandle, 0, IntPtr.Zero)
            End If

Line 11 in the code above is the important part really. This is where we specify that we would like a handle that has full control over the service (why stop at just requesting permission to start the service eh). This does indeed give us a handle we can use to do anything we want to the service, even if the ACL doesn’t allow us to do that.

Closing Thoughts

If you think about it, there are a few reasons why this does kind of make sense and it definitely seems like a conscious design choice by MS rather than any kind of bug or oversight.

But it still feels very weird being able to call CreateService and get a handle that allows full control over the service, then one second later call OpenService against that same service and request the same level of access but get an access denied error.

Also just to clarify – even if an attacker didn’t do this and couldn’t manually start the service this way, there are still plenty of ways to get the service started (which is one reason why at the start I said this little trick isn’t really a security issue). The simplest way would be to specify that the service should start automatically on boot up, then just restart the machine or wait for it to be rebooted by the user.

At the end of the day if you’re granting someone permission to create new services, you’re basically giving them Local System regardless of what other permissions are in place.

Using .NET To Get Process Command Lines

No exploits in this one – just some good old fashioned coding tips.

I wanted to show the command line used to launch each process that’s running on the machine (the executable file path along with arguments passed to it, i.e. C:\Windows\system32\svchost.exe -k netsvcs).

Now there’s plenty of examples online showing how to do this from C++ but things get slightly more awkward when doing it from C#.NET or VB.NET, especially when it comes to 64 bit processes. It took me a while to get it all working, so I thought I’d document it here for anyone else wanting to do the same thing.

There’s going to be lots of Win32 API interop here, so buckle up!


The first thing we need to do is call the native Win32 API NtQueryInformationProcess.

The MSDN description states that this function “Retrieves information about the specified process”. So we can call it and get back a PROCESS_BASIC_INFORMATION structure which contains the address of the PEB (Process Environment Block) for the process we specified.

Why do we want the address of the PEB? Well, if we look at the structure of the PEB we can see it contains a member called ProcessParameters which is an RTL_USER_PROCESS_PARAMETERS structure. If we then look at the definition of that structure we see it contains a member called CommandLine which is described as “The command-line string passed to the process”. Sounds like exactly what we are after!

Just to make it a bit clearer, here’s how we’re going to go from calling the API to getting the command line string we want:

So let’s get started by calling the NtQueryInformationProcess function. Due to this being a Win32 API, to use it in our .NET project we need to take the C++ definition we got from MSDN:

__kernel_entry NTSTATUS NtQueryInformationProcess(
  IN HANDLE           ProcessHandle,
  IN PROCESSINFOCLASS ProcessInformationClass,
  OUT PVOID           ProcessInformation,
  IN ULONG            ProcessInformationLength,
  OUT PULONG          ReturnLength
);

and declare it like so in our .NET project (I’m using VB.NET but its very easy to convert this to C#.NET) :

        <DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess", SetLastError:=True)>
        Public Shared Function NtQueryInformationProcess(ByVal ProcessHandle As IntPtr,
                                                         ByVal Processinformationclass As UInteger,
                                                         ByRef ProcessInformation As PROCESS_BASIC_INFORMATION,
                                                         ByVal ProcessInformationLength As Integer,
                                                         ByRef ReturnLength As UInteger) As Integer
        End Function

We also need to take the PROCESS_BASIC_INFORMATION structure definition from MSDN:

typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

and implement that in .NET:

        <StructLayout(LayoutKind.Sequential)> _
        Public Structure PROCESS_BASIC_INFORMATION
            Public Reserved1 As IntPtr
            Public PebBaseAddress As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved2() As IntPtr
            Public UniqueProcessID As IntPtr
            Public Reserved3 As IntPtr
        End Structure

As you can see, we have to convert the C++ definitions into their equivalent .NET types. For example if a member is declared as PVOID in C++ then we declare it as IntPtr in .NET.

NOTE: I’ll do another blog post soon explaining this process in a lot more detail and showing how to get started with marshalling things between native Win32 API and .NET. Maybe I’ll do a video on it as as well.

I’ll add a link here once that post/video is up.

Unfortunately we can’t just read the PebBaseAddress pointer from our PROCESS_BASIC_INFORMATION structure and treat it like a normal pointer though. This is because the pointer is relative to the target process’ memory, not our own process’s memory.

This is true for all of the pointers in the PEB and its members as well.

So if we take one of those pointers and try to grab the data it is pointing to by using something like PtrToStructure then it will fail because the pointer doesn’t make any sense in our own process’ memory address space. What we need to do is use the ReadProcessMemory API to read data from the target process’ memory. With that, we can use these pointers that relate to the target process’ virtual memory.

To make this ReadProcessMemory API easier to use, I wrapped it up in a .NET class called ProcessMemoryReader which you can find below:

Public Class ProcessMemoryReader : Implements IDisposable

    Private _TargetProcess As Process = Nothing
    Private _TargetProcessHandle As IntPtr = IntPtr.Zero

    Public Sub New(ByVal ProcessToRead As Process)
        If ProcessToRead Is Nothing Then
            Throw New ArgumentNullException("ProcessToRead")
        End If
        _TargetProcess = ProcessToRead
        Me.Open()
    End Sub

    Public Function Read(ByVal MemoryAddress As IntPtr, ByVal Count As Integer) As Byte()
        If _TargetProcessHandle = IntPtr.Zero Then
            Open()
        End If
        Dim Bytes(Count - 1) As Byte
        Dim Result As Boolean = WindowsApi.Win32.ReadProcessMemory(_TargetProcessHandle, MemoryAddress, Bytes, CUInt(Count), 0)
        If Result Then
            Return Bytes
        Else
            Return Nothing
        End If
    End Function

    Public Sub Open()
        If _TargetProcess Is Nothing Then
            Throw New ApplicationException("Process not found")
        End If
        If _TargetProcessHandle = IntPtr.Zero Then
            _TargetProcessHandle = WindowsApi.Win32.OpenProcess(WindowsApi.Win32.ProcessAccess.VMRead Or WindowsApi.Win32.ProcessAccess.QueryInformation, True, CUInt(_TargetProcess.Id))
            If _TargetProcessHandle = IntPtr.Zero Then
                Throw New ApplicationException("Unable to open process for memory reading. The last error reported was: " & New System.ComponentModel.Win32Exception().Message)
            End If
        Else
            Throw New ApplicationException("A handle to the process has already been obtained, " & _
                                           "close the existing handle by calling the Close method before calling Open again")
        End If
    End Sub

    Public Sub Close()
        If Not _TargetProcessHandle = IntPtr.Zero Then
            Dim Result As Boolean = WindowsApi.Win32.CloseHandle(_TargetProcessHandle)
            If Not Result Then
                Throw New ApplicationException("Unable to close process handle. The last error reported was: " & _
                                               New System.ComponentModel.Win32Exception().Message)
            End If
            _TargetProcessHandle = IntPtr.Zero
        End If
    End Sub


#Region "IDisposable Support"

    Private disposedValue As Boolean

    Protected Overridable Sub Dispose(ByVal disposing As Boolean)
        If Not Me.disposedValue Then
            If Not _TargetProcessHandle = IntPtr.Zero Then
                Try
                    WindowsApi.Win32.CloseHandle(_TargetProcessHandle)
                Catch ex As Exception
                    Debug.WriteLine("Error closing handle - " & ex.Message)
                End Try
            End If
        End If
        Me.disposedValue = True
    End Sub

    Protected Overrides Sub Finalize()
        Dispose(False)
        MyBase.Finalize()
    End Sub

    Public Sub Dispose() Implements IDisposable.Dispose
        Dispose(True)
        GC.SuppressFinalize(Me)
    End Sub

#End Region


End Class

So now that we have a way to read the target process’ memory and make use of the pointers in the PEB, we can put everything together to do what we want.

In the final code below,you can see I’ve added a few more API definitions to help handle 32 bit processes on 64 bit operating systems, and created definitions for the various structures we need to interact with.

I’ve written a lot of comments in the code so hopefully it all makes sense, but if you prefer to step through it yourself you can download the full VS project on GitHub here: https://github.com/VbScrub/ProcessCommandLineDemo

Imports System.Runtime.InteropServices

Public Class WindowsApi

    Public Shared Function GetCommandLine(ByVal TargetProcess As Process) As String
        'If we're on a 64 bit OS then the target process will have a 64 bit PEB if we are calling this function from a 64 bit process (regardless of
        'whether or not the target process is 32 bit or 64 bit).
        'If we are calling this function from a 32 bit process and the target process is 32 bit then we will get a 32 bit PEB, even on a 64 bit OS. 
        'The one situation we can't handle is if we are calling this function from a 32 bit process and the target process is 64 bit. For that we need to use the
        'undocumented NtWow64QueryInformationProcess64 and NtWow64ReadVirtualMemory64 APIs
        Dim Is64BitPeb As Boolean = False
        If Environment.Is64BitOperatingSystem() Then
            If Is32BitProcessOn64BitOs(Process.GetCurrentProcess) Then
                If Not Is32BitProcessOn64BitOs(TargetProcess) Then
                    'TODO: Use NtWow64ReadVirtualMemory64 to read from 64 bit processes when we are a 32 bit process instead of throwing this exception
                    Throw New InvalidOperationException("This function cannot be used against a 64 bit process when the calling process is 32 bit")
                End If
            Else
                Is64BitPeb = True
            End If
        End If

        'Open the target process for memory reading
        Using MemoryReader As New ProcessMemoryReader(TargetProcess)
            Dim ProcessInfo As Win32.PROCESS_BASIC_INFORMATION = Nothing
            'Get basic information about the process, including the PEB address
            Dim Result As Integer = Win32.NtQueryInformationProcess(TargetProcess.Handle, 0, ProcessInfo, Marshal.SizeOf(ProcessInfo), 0)
            If Not Result = 0 Then
                Throw New System.ComponentModel.Win32Exception(Win32.RtlNtStatusToDosError(Result))
            End If
            'Get pointer from the ProcessParameters member of the PEB (PEB has different structure on x86 vs x64 so different structures needed for each)
            Dim PebLength As Integer
            If Is64BitPeb Then
                PebLength = Marshal.SizeOf(GetType(Win32.PEB_64))
            Else
                PebLength = Marshal.SizeOf(GetType(Win32.PEB_32))
            End If
            'Read the PEB from the PebBaseAddress pointer
            'NOTE: This pointer points to memory in the target process' address space, so Marshal.PtrToStructure won't work. We have to read it with the ReadProcessMemory API 
            Dim PebBytes() As Byte = MemoryReader.Read(ProcessInfo.PebBaseAddress, PebLength)
            'Using GCHandle.Alloc get a pointer to the byte array we read from the target process, so we can use PtrToStructure to convert those bytes to our PEB_32 or PEB_64 structure
            Dim PebBytesPtr As GCHandle = GCHandle.Alloc(PebBytes, GCHandleType.Pinned)
            Try
                Dim ProcParamsPtr As IntPtr
                'Get a pointer to the RTL_USER_PROCESS_PARAMETERS structure (again this pointer refers to the target process' memory)
                If Is64BitPeb Then
                    Dim PEB As Win32.PEB_64 = GcHandleToStruct(Of Win32.PEB_64)(PebBytesPtr.AddrOfPinnedObject)
                    ProcParamsPtr = PEB.ProcessParameters
                Else
                    Dim PEB As Win32.PEB_32 = GcHandleToStruct(Of Win32.PEB_32)(PebBytesPtr.AddrOfPinnedObject)
                    ProcParamsPtr = PEB.ProcessParameters
                End If
                'Now that we've got the pointer from the ProcessParameters member, we read the RTL_USER_PROCESS_PARAMETERS structure that is stored at that location in the target process' memory
                Dim ProcParamsBytes() As Byte = MemoryReader.Read(ProcParamsPtr, Marshal.SizeOf(GetType(Win32.RTL_USER_PROCESS_PARAMETERS)))
                'Again we use GCHandle.Alloc to get a pointer to the byte array we just read
                Dim ProcParamsBytesPtr As GCHandle = GCHandle.Alloc(ProcParamsBytes, GCHandleType.Pinned)
                Try
                    'Convert the byte array to a RTL_USER_PROCESS_PARAMETERS structure
                    Dim ProcParams As Win32.RTL_USER_PROCESS_PARAMETERS = GcHandleToStruct(Of Win32.RTL_USER_PROCESS_PARAMETERS)(ProcParamsBytesPtr.AddrOfPinnedObject)
                    'Get the CommandLine member of the RTL_USER_PROCESS_PARAMETERS structure
                    Dim CmdLineUnicodeString As Win32.UNICODE_STRING = ProcParams.CommandLine
                    'The Buffer member of the UNICODE_STRING structure points to the actual command line string we want, so we read from the location that points to
                    Dim CmdLineBytes() As Byte = MemoryReader.Read(CmdLineUnicodeString.Buffer, CmdLineUnicodeString.Length)
                    'Convert the bytes to a regular .NET String and return it
                    Return System.Text.Encoding.Unicode.GetString(CmdLineBytes)
                Finally
                    'Clean up the GCHandle we created for the RTL_USER_PROCESS_PARAMETERS bytes
                    If ProcParamsBytesPtr.IsAllocated Then
                        ProcParamsBytesPtr.Free()
                    End If
                End Try
            Finally
                'Clean up the GCHandle we created for the PEB bytes
                If PebBytesPtr.IsAllocated Then
                    PebBytesPtr.Free()
                End If
            End Try
        End Using
    End Function

    'Using this generic function just to make the code in the GetCommandLine function easier to read and save some casting 
    Private Shared Function GcHandleToStruct(Of T)(Pointer As IntPtr) As T
        Return DirectCast(Marshal.PtrToStructure(Pointer, GetType(T)), T)
    End Function

    Public Shared Function Is32BitProcessOn64BitOs(ByVal TargetProcess As Process) As Boolean
        Dim IsWow64 As Boolean = False
        If MethodExistsInDll("kernel32.dll", "IsWow64Process") Then
            Win32.IsWow64Process(TargetProcess.Handle, IsWow64)
        End If
        Return IsWow64
    End Function

    Public Shared Function MethodExistsInDll(ByVal ModuleName As String, ByVal MethodName As String) As Boolean
        Dim ModuleHandle As IntPtr = Win32.GetModuleHandle(ModuleName)
        If ModuleHandle = IntPtr.Zero Then
            Return False
        End If
        Return (Win32.GetProcAddress(ModuleHandle, MethodName) <> IntPtr.Zero)
    End Function

    Public Class Win32

        <StructLayout(LayoutKind.Sequential)>
        Public Structure UNICODE_STRING
            Public Length As UInt16
            Public MaximumLength As UInt16
            '64 bit version of this actually has 4 bytes of padding here (after MaximumLength and before Buffer), but the default Pack size for structs handles this for us
            Public Buffer As IntPtr
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure RTL_USER_PROCESS_PARAMETERS
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=16)>
            Public Reserved1() As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=10)>
            Public Reserved2() As IntPtr
            Public ImagePathName As UNICODE_STRING
            Public CommandLine As UNICODE_STRING
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure PEB_32
            <MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved1() As Byte
            Public BeingDebugged As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=1)> _
            Public Reserved2() As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)> _
            Public Reserved3() As IntPtr
            Public Ldr As IntPtr
            Public ProcessParameters As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=3)> _
            Public Reserved4() As IntPtr
            Public AtlThunkSListPtr As IntPtr
            Public Reserved5 As IntPtr
            Public Reserved6 As UInteger
            Public Reserved7 As IntPtr
            Public Reserved8 As UInteger
            Public AtlThunkSListPtr32 As UInteger
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=45)> _
            Public Reserved9() As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=96)> _
            Public Reserved10() As Byte
            Public PostProcessInitRoutine As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=128)> _
            Public Reserved11() As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=1)> _
            Public Reserved12() As IntPtr
            Public SessionId As UInteger
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure PEB_64
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved1() As Byte
            Public BeingDebugged As Byte
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=21)>
            Public Reserved2() As Byte
            Public LoaderData As IntPtr
            Public ProcessParameters As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=520)>
            Public Reserved3() As Byte
            Public PostProcessInitRoutine As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=136)>
            Public Reserved4() As Byte
            Public SessionId As UInteger
        End Structure

        <StructLayout(LayoutKind.Sequential)>
        Public Structure PROCESS_BASIC_INFORMATION
            Public Reserved1 As IntPtr
            Public PebBaseAddress As IntPtr
            <MarshalAs(UnmanagedType.ByValArray, SizeConst:=2)>
            Public Reserved2() As IntPtr
            Public UniqueProcessID As IntPtr
            Public Reserved3 As IntPtr
        End Structure

        <Flags()>
        Public Enum ProcessAccess As UInteger
            AllAccess = CreateThread Or DuplicateHandle Or QueryInformation Or SetInformation Or Terminate Or VMOperation Or VMRead Or VMWrite Or Synchronize
            CreateThread = &H2
            DuplicateHandle = &H40
            QueryInformation = &H400
            QueryLimitedInformation = &H1000
            SetInformation = &H200
            Terminate = &H1
            VMOperation = &H8
            VMRead = &H10
            VMWrite = &H20
            Synchronize = &H100000
        End Enum

        <DllImport("ntdll.dll", EntryPoint:="RtlNtStatusToDosError", SetLastError:=True)> _
        Public Shared Function RtlNtStatusToDosError(NtStatus As Integer) As Integer
        End Function

        <DllImport("kernel32.dll", EntryPoint:="IsWow64Process", SetLastError:=True)> _
        Public Shared Function IsWow64Process(ByVal hProcess As IntPtr, <Out()> ByRef Wow64Process As Boolean) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("kernel32.dll", EntryPoint:="GetModuleHandle", SetLastError:=True)> _
        Public Shared Function GetModuleHandle(ByVal ModuleName As String) As IntPtr
        End Function

        <DllImport("kernel32.dll", EntryPoint:="GetProcAddress", SetLastError:=True)> _
        Public Shared Function GetProcAddress(ByVal hModule As IntPtr, ByVal MethodName As String) As IntPtr
        End Function

        <DllImport("ntdll.dll", EntryPoint:="NtQueryInformationProcess", SetLastError:=True)>
        Public Shared Function NtQueryInformationProcess(ByVal handle As IntPtr,
                                                         ByVal Processinformationclass As UInteger,
                                                         ByRef ProcessInformation As PROCESS_BASIC_INFORMATION,
                                                         ByVal ProcessInformationLength As Integer,
                                                         ByRef ReturnLength As UInteger) As Integer
        End Function

        <DllImport("kernel32.dll", EntryPoint:="ReadProcessMemory", SetLastError:=True)> _
        Public Shared Function ReadProcessMemory(ByVal hProcess As IntPtr,
                                                 ByVal lpBaseAddress As IntPtr,
                                                 <Out()> ByVal lpBuffer As Byte(),
                                                 ByVal nSize As UInteger,
                                                 <Out()> ByRef lpNumberOfBytesRead As UInteger) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function

        <DllImport("kernel32.dll", EntryPoint:="OpenProcess", SetLastError:=True)> _
        Public Shared Function OpenProcess(ByVal dwDesiredAccess As ProcessAccess,
                                           <MarshalAs(UnmanagedType.Bool)> ByVal bInheritHandle As Boolean,
                                           ByVal dwProcessId As UInteger) As IntPtr
        End Function

        <DllImport("kernel32.dll", EntryPoint:="CloseHandle", SetLastError:=True)> _
        Public Shared Function CloseHandle(ByVal Handle As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
        End Function


    End Class




End Class

Hope that helps save someone some time, as it took me longer than I’d like to admit to get some parts of this working.