Exploring Windows API with PowerShell & C#

6 minute read

Recently, I subscribed to Ruben Boonen (b33f) Patreon becuase I thought this would be a great oppurtunity to learn some new stuff! Every now and then b33f released a live session where he chatted through a particular topic, one of these was how you can use the Windows API in PowerShell.

I always thought this was a rather complex topic, and veered away from programming but it’s actually really interesting and quite fun.

Anyway moving on there are a few ways of achieving this but I’m going to stick with inline C#.

Using inline C# is pretty simple but can be troublesome and I failed a lot. It uses the Add-Type cmdlet in PowerShell which essentially allows you to add a C# class to PowerShell.

Microsoft has a great document for this - https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-type?view=powershell-7.1

Here is an example of calling a Windows API method using this approach:

Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;
using System.Diagnostics;

public static class WindowsAPI {

    [DllImport("user32.dll")]
    public static extern bool ShowWindowAsync
    (
        IntPtr hWnd,
        int nCmdShow
    );
};
"@

[IntPtr]$handle = (Get-Process -Pid $PID).MainWindowHandle
[WindowsAPI]::ShowWindowAsync($handle,0)

In the example above, you can see we’re invoking Add-Type cmdlet with the here string containing the inline C#, we’re using the class name WindowsAPI, and using the ShowWindowAsync function, I use PInvoke for the signatures great - https://www.pinvoke.net/default.aspx/user32.showwindowasync, but Googling the function name + “MSDN” or “DLLImport” will help.

Within the function you may have to work change the type from C++ to C#, I’ve have included some type conversion here: https://raw.githubusercontent.com/ben0/PS-WinAPI/master/Types.md

After we’ve declared the signature, we get a handle to our current processes main window, then we can call the class and method with the variables $handle, and 0 to hide the current window! Great for hiding a PowerShell window!

Using this technique we can do much more, for example using the CreateProcessWithToken function.

function CreateProcessWithToken {

    <#
    .SYNOPSIS
    Achieve CreateProcessWithTokenW

    .DESCRIPTION

    .PARAMETER processname

    .PARAMETER spawn

    .EXAMPLE

    .INPUTS
    System.String

    .OUTPUTS
    None

    .NOTES
    The following five token flags are required, otherwise the error is access denied:
        TOKEN_ASSIGN_PRIMARY
        TOKEN_DUPLICATE
        TOKEN_QUERY
        TOKEN_ADJUST_DEFAULT
        TOKEN_ADJUST_SESSIONID


    .LINK

    #>
    [CmdletBinding()]
    [OutputType([String])]
    param(
        [Parameter(Mandatory=$true,Position=0)]
        [string]$processname,
        [Parameter(Mandatory=$true,Position=1)]
        [string]$spawn
    )
    Begin {
        Add-Type -TypeDefinition @"
        using System;
        using System.Diagnostics;
        using System.Runtime.InteropServices;
        using System.Security.Principal;

        public enum ProcessAccess
        {
            PROCESS_ALL_ACCESS = (PROCESS_CREATE_PROCESS | PROCESS_CREATE_THREAD | PROCESS_DUP_HANDLE | PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_SET_INFORMATION | PROCESS_SET_QUOTA | PROCESS_SUSPEND_RESUME | PROCESS_TERMINATE | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE | SYNCHRONIZE),
            PROCESS_CREATE_PROCESS = 0x0080,
            PROCESS_CREATE_THREAD = 0x0002,
            PROCESS_DUP_HANDLE = 0x0040,
            PROCESS_QUERY_INFORMATION = 0x0400,
            PROCESS_QUERY_LIMITED_INFORMATION = 0x1000,
            PROCESS_SET_INFORMATION = 0x0200,
            PROCESS_SET_QUOTA = 0x0100,
            PROCESS_SUSPEND_RESUME = 0x0800,
            PROCESS_TERMINATE = 0x0001,
            PROCESS_VM_OPERATION = 0x0008,
            PROCESS_VM_READ = 0x0010,
            PROCESS_VM_WRITE = 0x0020,
            SYNCHRONIZE = 0x00100000
        };
        
        [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;
        };

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

        [StructLayout(LayoutKind.Sequential)]
        public struct SECURITY_ATTRIBUTES
        {
            public int nLength;
            public IntPtr lpSecurityDescriptor;
            public int bInheritHandle;
        };

        public enum LogonFlags
        {
            LOGON_WITH_PROFILE = 0x00000001,
            LOGON_NETCREDENTIALS_ONLY = 0x00000002
        };

        public enum CreationFlags
        {
            CREATE_SUSPENDED = 0x00000004,
            CREATE_NEW_CONSOLE = 0x00000010,
            CREATE_NEW_PROCESS_GROUP = 0x00000200,
            CREATE_UNICODE_ENVIRONMENT = 0x00000400,
            CREATE_SEPARATE_WOW_VDM = 0x00000800,
            CREATE_DEFAULT_ERROR_MODE = 0x04000000,
            CREATE_NO_WINDOW = 0x08000000
        };

        public enum TOKEN_TYPE 
        {
            TokenPrimary = 1,
            TokenImpersonation
        };

        public enum SECURITY_IMPERSONATION_LEVEL 
        {
            SecurityAnonymous,
            SecurityIdentification,
            SecurityImpersonation,
            SecurityDelegation
        };

        public static class WindowsAPICreateProcess
        {
            public const UInt32 STANDARD_RIGHTS_REQUIRED = 0x000F0000;
            public const UInt32 STANDARD_RIGHTS_READ = 0x00020000;
            public const UInt32 TOKEN_ASSIGN_PRIMARY = 0x0001;
            public const UInt32 TOKEN_DUPLICATE = 0x0002;
            public const UInt32 TOKEN_IMPERSONATE = 0x0004;
            public const UInt32 TOKEN_QUERY = 0x0008;
            public const UInt32 TOKEN_QUERY_SOURCE = 0x0010;
            public const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020;
            public const UInt32 TOKEN_ADJUST_GROUPS = 0x0040;
            public const UInt32 TOKEN_ADJUST_DEFAULT = 0x0080;
            public const UInt32 TOKEN_ADJUST_SESSIONID = 0x0100;
            public const UInt32 TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY);
            public const UInt32 TOKEN_CREATEPROCESSWITHTOKEN = (TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID);
            public const UInt32 TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID);

            [DllImport("kernel32.dll", SetLastError=true)]
            public static extern IntPtr OpenProcess
            (
                UInt32 processAccess,
                bool bInheritHandle,
                int processId
            );

            [DllImport("advapi32.dll", SetLastError=true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool OpenProcessToken
            (
                IntPtr ProcessHandle, 
                UInt32 DesiredAccess,
                out IntPtr TokenHandle
            );
            
            [DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
            public extern static bool DuplicateTokenEx
            (
                IntPtr hExistingToken,
                uint dwDesiredAccess,
                ref SECURITY_ATTRIBUTES lpTokenAttributes,
                SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
                TOKEN_TYPE TokenType,
                out IntPtr phNewToken
            );

            [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)]
            public static extern bool CreateProcessWithTokenW
            (
                IntPtr hToken, 
                LogonFlags dwLogonFlags,
                string lpApplicationName, 
                string lpCommandLine, 
                CreationFlags dwCreationFlags, 
                IntPtr lpEnvironment, 
                string lpCurrentDirectory, 
                [In] ref STARTUPINFO lpStartupInfo, 
                out PROCESS_INFORMATION lpProcessInformation
            );
            
        }
"@
    }
    Process {
        Write-Host -ForegroundColor Black -BackgroundColor Yellow "CreateProcessWithTokenW PoC:"

        # Retrieve the Process ID of $processname
        #
        $ProcessID = (Get-Process -Name ${processname} -ErrorAction SilentlyContinue).Id
        if(!$ProcessID)
        {
            Write-Host -Foregroundcolor Red "[!] Could not find ${processname} running..."
            Break
        } elseif ($ProcessID -is [array]) {
            $ProcessID = $ProcessID[0]
        }
        Write-Host -Foregroundcolor White -NoNewLine "[+] Process ID of ${processname}: "
        Write-Host -Foregroundcolor Green $ProcessID

        
        # Open the remote process
        # PROCESS_ALL_ACCESS = 0x1F0FFF
        # https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-openprocess
        #
        [IntPtr]$handleProcess = 0
        $handleProcess = [WindowsAPICreateProcess]::OpenProcess([ProcessAccess]::PROCESS_ALL_ACCESS, $false, $ProcessID); $LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
        Write-Host -Foregroundcolor White -NoNewLine "[+] Handle to process ${processname}/$ProcessID is: "
        if($handleProcess -eq 0)
        {
            Write-Host -Foregroundcolor White -NoNewline "[!] Failed with Error code: "
            Write-Host -Foregroundcolor Green "$LastError"
            Break
        } else {
            Write-Host -Foregroundcolor Green $handleProcess
        }

        # Open the process token
        # Access: TOKEN_DUPLICATE
        # https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocesstoken
        #
        # Declare our Int for handle to the token
        [IntPtr]$handleToken = [IntPtr]::Zero
        $returnValue = [WindowsAPICreateProcess]::OpenProcessToken($handleProcess, [WindowsAPICreateProcess]::TOKEN_DUPLICATE, [ref]$handleToken); $LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
        Write-Host -Foregroundcolor White -NoNewLine "[+] Handle to token ${processname}/$ProcessID is: "
        if($handleToken -eq 0)
        {
            Write-Host -Foregroundcolor White -NoNewline "[!] Failed with Error code: "
            Write-Host -Foregroundcolor Green "$LastError"
            Break
        } else {
            Write-Host -Foregroundcolor Green $handleToken
        }

        # Define and populate the Startupinfo struct, including the struct size
        $SECURITY_ATTRIBUTES = New-Object -Typename SECURITY_ATTRIBUTES
            
        # Duplicate the process token
        # Access: 
        # https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-duplicatetokenex
        #
        # Delcare our Int for handle to the token
        [IntPtr]$duplicateToken = [IntPtr]::Zero
        $DuplicateTokenResult = [WindowsAPICreateProcess]::DuplicateTokenEx($handleToken, [WindowsAPICreateProcess]::TOKEN_ALL_ACCESS, [ref]$SECURITY_ATTRIBUTES, [SECURITY_IMPERSONATION_LEVEL]::SecurityImpersonation, [TOKEN_TYPE]::TokenImpersonation, [ref]$duplicateToken); $LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
        Write-Host -Foregroundcolor White -NoNewLine "[+] Handle to duplicate token is: "
        if($duplicateToken -eq 0)
        {
            Write-Host -Foregroundcolor White -NoNewline "[!] Failed with Error code: "
            Write-Host -Foregroundcolor Green "$LastError"
            Break
        } else {
            Write-Host -Foregroundcolor Green $duplicateToken
        }

        # Define and populate the Startupinfo struct, including the struct size
        $STARTUPINFO = New-Object -Typename STARTUPINFO
        $STARTUPINFO.cb = [System.Runtime.InteropServices.Marshal]::SizeOf($STARTUPINFO)

        # Define and populate the Startupinfo struct, including the struct size
        $PROCESS_INFORMATION = New-Object -Typename PROCESS_INFORMATION

        # Call the function
        $APICallResult = [WindowsAPICreateProcess]::CreateProcessWithTokenW($duplicateToken, [LogonFlags]::LOGON_WITH_PROFILE, $spawn, $spawn, [CreationFlags]::CREATE_NEW_CONSOLE, 0, "C:\\", [ref]$STARTUPINFO, [ref]$PROCESS_INFORMATION); $LastError = [ComponentModel.Win32Exception][Runtime.InteropServices.Marshal]::GetLastWin32Error()
        Write-Host -Foregroundcolor White -NoNewLine "[+] Calling function CreateProcessWithTokenW: "

        # GetLastError courtesy of Exploit Monday
        # http://www.exploit-monday.com/2016/01/properly-retrieving-win32-api-error.html
        if($LastError -eq 1314)
        {
            Write-Host -Foregroundcolor White -NoNewline "[!] Failed with Error code: "
            Write-Host -Foregroundcolor Red "$LastError ERROR_PRIVILEGE_NOT_HELD"
        } elseif ($LastError -eq 0) 
        {
            Write-Host -Foregroundcolor White -NoNewline "[!] Failed with Error code: "
            Write-Host -Foregroundcolor Red "$LastError"
        } elseif ($LastError -ne 0) {
            Write-Host -Foregroundcolor White -NoNewline "[!] Success with Error code: "
            Write-Host -Foregroundcolor Green "$LastError"
        }
    }

    End {
    }
}

With this inline C# it’s possible to get the token of another process, duplicate it and then create a new process with the privileges of that token. Neat!

Check out my Github repo for more examples https://github.com/ben0/PS-WinAPI

Updated: