PowerShellの基礎構文と自動化スクリプトの実践ガイド

PowerShellは.NETフレームワーク(.NET Frameworkおよび.NET Core/PowerShell 6以降)上に構築された、強力なタスク自動化および構成管理ツールです。従来のコマンドプロンプトやVBScriptと比較して、.NETクラスライブラリへの直接アクセスが可能であり、複雑なロジックを簡潔に記述できます。

セキュリティの観点からは、その強力な実行能力ゆえに、ファイルレス攻撃やAPT(先進的持続的脅威)において悪用されるケースも少なくありません。メモリ上で直接コードを実行できる特性は、AV(アンチウイルス)やEDR(エンドポイント検出と対応)による静的解析を回避する手段として研究されています。一方で、システム管理者にとっては、PythonのようにシンプルかつOS標準で利用可能な不可欠な自動化ツールです。

基本構文とデータ型

変数とコメント

変数は$で始まり、コメントは#(単行)または<# #>(ブロック)で記述します。

# 単行コメント
<#
  複数行にわたる
  ブロックコメント
#>

$counter = 0                  # 基本的な変数宣言
[string]$userName = "Admin"   # 型制約の付与
[ValidateRange(1, 100)][int]$score = 85 # 値の範囲検証
${Special-Var} = "Data"       # 特殊文字を含む変数名には中括弧を使用

自動変数

PowerShellには、システムによって自動的に値が設定される組み込み変数が存在します。

  • $?: 直前のコマンドが成功したかどうかを示す真偽値。
  • $_ (または $PSItem): パイプラインから渡された現在のオブジェクトを表す。ForEach-Object や Where-Object で頻繁に使用されます。
  • $args: 関数やスクリプトに渡された引数の配列。
  • $PSScriptRoot: 現在実行中のスクリプトが格納されているディレクトリのパス。

文字列操作

文字列はシングルクォート(リテラル)とダブルクォート(展開可能)で区別されます。

$version = "5.1"
$literal = 'PowerShell version is $version'   # 変数は展開されない
$expanded = "PowerShell version is $version"  # 変数が展開される

# Here-String (複数行文字列)
$multiLine = @"
これは複数行の文字列です。
変数も展開されます: $version
"@

# 文字列の結合とフォーマット
$combined = $literal + " / " + $expanded
$formatted = "Version: {0}, Status: {1}" -f $version, "Active"

配列とハッシュテーブル

# 配列
$numbers = @(10, 20, 30, 40, 50)
$numbers += 60             # 要素の追加
$subset = $numbers[1..3]   # スライス (20, 30, 40)
$lastItem = $numbers[-1]   # 末尾の要素 (60)

# ハッシュテーブル (辞書)
$config = @{
    HostName = "Server01"
    Port     = 8080
    IsSecure = $true
}
$config.Add("Timeout", 30) # 要素の追加

比較演算子

PowerShellでは、記号の代わりにハイフンで始まる演算子を使用します。

  • -eq (等しい), -ne (等しくない)
  • -gt (より大きい), -ge (以上)
  • -lt (より小さい), -le (以下)
  • -like (ワイルドカード一致), -match (正規表現一致)

制御構文と関数定義

ループと条件分岐

$items = Get-ChildItem -Path "C:\Logs" -Filter "*.log"

foreach ($item in $items) {
    if ($item.Length -gt 1MB) {
        Write-Host "Large file found: $($item.Name)" -ForegroundColor Yellow
    } elseif ($item.Length -eq 0) {
        Remove-Item $item.FullName -Force
    } else {
        Write-Host "Normal file: $($item.Name)"
    }
}

関数の定義

通常の関数に加え、[CmdletBinding()] 属性を付与することで、組み込みコマンドレットと同様の機能(詳細メッセージ、エラー処理、パラメータ検証など)を持つ「高度な関数」を定義できます。

function Get-SystemUptime {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]$ComputerName
    )

    begin {
        Write-Verbose "Connecting to $ComputerName..."
    }

    process {
        try {
            $os = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop
            $uptime = (Get-Date) - $os.LastBootUpTime
            [PSCustomObject]@{
                Computer = $ComputerName
                Uptime   = $uptime
            }
        }
        catch {
            Write-Error "Failed to query $ComputerName. $_"
        }
    }

    end {
        Write-Verbose "Operation completed."
    }
}

実践的な自動化スクリプトの事例

古いログファイルのアーカイブと整理

指定されたディレクトリ内にある特定の日数より古いファイルを特定し、日付プレフィックスを付けてアーカイブフォルダに移動するスクリプトです。

$targetDir = "C:\Application\Logs"
$archiveDir = "C:\Application\Archive"
$daysToKeep = 7

if (-not (Test-Path $archiveDir)) {
    New-Item -ItemType Directory -Path $archiveDir | Out-Null
}

$cutoffDate = (Get-Date).AddDays(-$daysToKeep)
$oldFiles = Get-ChildItem -Path $targetDir -Filter "*.log" -File | 
            Where-Object { $_.CreationTime -lt $cutoffDate }

foreach ($file in $oldFiles) {
    $datePrefix = $file.CreationTime.ToString("yyyyMMdd")
    $newName = "{0}_{1}" -f $datePrefix, $file.Name
    $destination = Join-Path -Path $archiveDir -ChildPath $newName
    
    Move-Item -Path $file.FullName -Destination $destination -Force
    Write-Host "Archived: $($file.Name) -> $newName"
}

CIMを用いたシステム情報の収集とエクスポート

WMIの後継であるCIM(Common Information Model)を使用して、実行中のサービス情報を収集し、JSON形式で出力します。

$services = Get-CimInstance -ClassName Win32_Service | Where-Object { $_.State -eq 'Running' }

$serviceDetails = foreach ($svc in $services) {
    [PSCustomObject]@{
        Name        = $svc.Name
        DisplayName = $svc.DisplayName
        ProcessId   = $svc.ProcessId
        StartMode   = $svc.StartMode
        PathName    = $svc.PathName
    }
}

$outputPath = Join-Path -Path $env:TEMP -ChildPath "RunningServices.json"
$serviceDetails | ConvertTo-Json -Depth 3 | Out-File -FilePath $outputPath -Encoding utf8
Write-Host "Data exported to $outputPath"

Webからのファイルダウンロードと検証

Invoke-WebRequest を使用してリモートからファイルをダウンロードし、ステータスコードを確認する堅牢なダウンロード処理です。

$uri = "https://raw.githubusercontent.com/PowerShell/PowerShell/master/README.md"
$outFile = Join-Path -Path $env:TEMP -ChildPath "PS_Readme.md"

try {
    $response = Invoke-WebRequest -Uri $uri -OutFile $outFile -PassThru -ErrorAction Stop
    if ($response.StatusCode -eq 200) {
        Write-Host "Download successful: $outFile" -ForegroundColor Green
    } else {
        Write-Warning "Unexpected status code: $($response.StatusCode)"
    }
}
catch {
    Write-Error "Failed to download file. Error: $($_.Exception.Message)"
}

タスクスケジューラーへの定期ジョブ登録

毎週日曜日の深夜にtemporaryフォルダをクリーンアップするタスクをシステム権限で登録します。

$actionArgs = "-NoProfile -ExecutionPolicy Bypass -Command `"Remove-Item -Path '$env:TEMP\*' -Recurse -Force -ErrorAction SilentlyContinue`""
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgs
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Sunday -At 02:00AM
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

Register-ScheduledTask -TaskName "WeeklyTempCleanup" `
                       -Action $action `
                       -Trigger $trigger `
                       -Settings $settings `
                       -Principal $principal `
                       -Description "Clears temporary files weekly."

動的コード実行と難読化の概念

Invoke-Expression (エイリアス: iex) は、文字列として渡されたコマンドを動的に評価・実行するコマンドレットです。この機能は柔軟なスクリプト作成を可能にしますが、セキュリティツールによる検出を回避するためにコードを難読化する手法としても頻繁に利用されます。

以下は、文字列反転を用いた簡単な難読化と、Invoke-Expression による動的実行のデモンストレーションです。

# 難読化された文字列 (元のコマンド: Get-Process | Select-Object -First 5 | Format-Table)
$obfuscatedString = "elbat-etaMroF | 5 tsriF -tcejbO-tceleS | ssecorP-teG"

# 文字列を反転させて元のコマンドを復元
$charArray = $obfuscatedString.ToCharArray()
[Array]::Reverse($charArray)
$originalCommand = -join $charArray

# 復元したコマンドを動的に実行
Invoke-Expression $originalCommand

このように、実行時にコードをメモリ上で復元して実行する手法は、ファイルシステム上に悪意のあるスクリプトを残さない「ファイルレス」な実行環境を構築する基礎技術となります。

タグ: PowerShell WindowsAutomation CIM TaskScheduler InvokeExpression

9月24日 12:33 投稿