Powershellで別のユーザーでプロセスを実行する

### 行ったこと
Windowsサーバーのログイン中のユーザーで、とある処理をシステム権限で実行する必要があったので、Powershellを利用して別のユーザーでプロセスを実行する処理を作成してみました。

イメージ図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/0836e9ef-53fc-c7a2-72b3-a3f0071f1f1c.png)


### psexec
今回行ったことはpsexecを使用すれば簡単に実装できますが、どうしてもそれを利用できない場合の代替手段として利用しました。

### 利用した機能
Powershellの標準的な機能では、別のユーザーのセッション内にアプリケーションを起動させることができないため、Windows APIを利用しました。

#### Add-Type
Powershellで他のソース C#やVBなどのコードを呼び出すために使用するコマンドレットです。今回はAdd-Typeを使用して、C#のコードでWindows APIを利用するための関数の定義を行いました。

```
Add-Type @"
    using System;
    using System.Runtime.InteropServices;

    public class Win32API
    {
        ここにWindowsAPIを定義
    }
"@
```


#### CreateProcessAsUser
今回使用したメインのAPI関数です。指定したユーザーセッションでアプリケーションを実行することができます。起動時にいくつかパラメータが必要です。STARTUPINFOは構造体で渡してやる必要があります。

dwCreationFlagsに1024(CREATE_UNICODE_ENVIRONMENT)を指定しています。私の使用した環境では環境変数を読み込ませるためにUnicodeの指定が必要でした。これを指定しないとパラメータエラーとなってしまっていました。

```
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CreateProcessAsUser(
    IntPtr hToken,
    string lpApplicationName,
    string lpCommandLine,
    ref SECURITY_ATTRIBUTES lpProcessAttributes,
    ref SECURITY_ATTRIBUTES lpThreadAttributes,
    bool bInheritHandles,
    uint dwCreationFlags,
    IntPtr lpEnvironment,
    string lpCurrentDirectory,
    ref STARTUPINFO lpStartupInfo,
    out PROCESS_INFORMATION lpProcessInformation);
```

構造体定義部分（一部）
```
    public struct STARTUPINFO
    {
        public int cb;
        public string lpReserved;
        public string lpDesktop;
        public string lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public ushort wShowWindow;
        public ushort cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }
```


#### WTSQueryUserToken
query session等で取得したセッションIDをもとに、ユーザーセッションのハンドルを取得します。これには特殊な権限が必要なため、Local Systemで実行しています。普通のAdministratorでは権限がないため、下図のようなエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/4e0e208c-a394-331a-8b90-d67cb5ca9a70.png)

```
[DllImport("wtsapi32.dll", SetLastError = true)]
public static extern bool WTSQueryUserToken(UInt32 sessionId, out IntPtr TokenHandle);
```


#### CreateEnvironmentBlock
起動したプロセスに環境変数を渡すために利用します。サンプルコードのメモ帳は不要ですが、ほとんどのアプリケーションは環境変数が必要になると思います。

```
[DllImport("userenv.dll", SetLastError = true)]
public static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);
```


#### DestroyEnvironmentBlock
CreateEnvironmentBlockは、環境変数をメモリ上の別の領域に確保するようなので、処理の終わりに開放します。

```
[DllImport("userenv.dll", SetLastError = true)]
public static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment);
```

### 指定したアプリケーションを起動
以下の部分で、起動するアプリケーションや引数、作業ディレクトリを指定しています。

```    
    $si = New-Object System.Diagnostics.ProcessStartInfo
    $si.FileName = "C:\Windows\System32\notepad.exe"
    $si.Arguments = "C:\temp\test.txt"
    $si.WorkingDirectory = "C:\Windows"
```

### 使用するときは、Local Systemで実行
Windows タスクスケジューラーを使用しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/77aaf6ed-fefc-f95e-871e-8cfdae8bff36.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3636209/832dee40-76b2-54d3-3a5a-274d0ac0af91.png)


### 完成版
完成版のコードはここにおいています。
https://github.com/mirutaro/cpau_sample/blob/main/sample.ps1

### まとめ
今回はPowershellを使用して、ユーザーセッション内でアプリケーションをシステム権限で実行することをやってみました。PowershellでWindowsAPIを操作できることを初めて知りましたが、非常に協力なツールであると思います。WindowsAPI自体がかなり奥が深く、使いこなせるといろいろなことが出来てしまうんだろうなと思います。しかし、使い方を間違えるとセキュリティ上の問題になるなど、使用にあたっては注意が必要かなと思いました。