PowerShellでnc(netcat)を書いてみる

多くのLinuxディストリビューションでは標準でncコマンドが入っており、リモートホストへの接続あるいはローカルポートでの待ち受けを行い、端末からネットワークソケットに対し読み書きすることができた。 一方Windowsの場合、前者はtelnetコマンドで代用することができるが、後者はNetcat for WindowsCygwin版のnc、何らかのスクリプトなどが必要となる。 PowerShellスクリプトによるnetcat実装としてはpowercatがあるが、あらかじめ環境にスクリプトをロードしておく使い方が想定されており、コマンドプロンプトからスクリプトとして実行するにはやや使いづらい。 そこで、ここではpowercatの実装を参考にncのように使えるPowerShellスクリプトを書き、コマンドプロンプトからスクリプトとして実行してみる。

環境

Windows 8.1 Pro 64 bit版、PowerShell 4.0

>systeminfo
OS 名:                  Microsoft Windows 8.1 Pro
OS バージョン:          6.3.9600 N/A ビルド 9600
OS ビルドの種類:        Multiprocessor Free
システムの種類:         x64-based PC
プロセッサ:             1 プロセッサインストール済みです。
                        [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz

>powershell -c "$PSVERSIONTABLE"

Name                           Value
----                           -----
PSVersion                      4.0
WSManStackVersion              3.0
SerializationVersion           1.1.0.1
CLRVersion                     4.0.30319.34014
BuildVersion                   6.3.9600.17090
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0}
PSRemotingProtocolVersion      2.2

nc相当のPowerShellスクリプト

実際にコードを書くと次のようになる。

# nc.ps1
Param(
    [string]$addr,
    [int]$port,
    [alias("l")][int]$lport,
    [alias("v")][switch]$verbose
)

$ErrorActionPreference = "Stop"
if ($verbose) {
    $VerbosePreference = "continue"
}

function interact($client) {
    $stream = $client.GetStream()
    $buffer = New-Object System.Byte[] $client.ReceiveBufferSize
    $enc = New-Object System.Text.AsciiEncoding

    try {
        $ar = $stream.BeginRead($buffer, 0, $buffer.length, $NULL, $NULL)
        while ($TRUE) {
            if ($ar.IsCompleted) {
                $bytes = $stream.EndRead($ar)
                if ($bytes -eq 0) {
                    break
                }
                Write-Host -n $enc.GetString($buffer, 0, $bytes)
                $ar = $stream.BeginRead($buffer, 0, $buffer.length, $NULL, $NULL)
            }
            if ($Host.UI.RawUI.KeyAvailable) {
                $data = $enc.GetBytes((Read-Host) + "`n")
                $stream.Write($data, 0, $data.length)
            }
        }
    } catch [System.IO.IOException] {
        # ignore exception at $stream.BeginRead()
    } finally {
        $stream.Close()
    }
}

if ($lport) {
    $endpoint = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, $lport)
    $listener = New-Object System.Net.Sockets.TcpListener $endpoint
    $listener.Start()
    Write-Verbose "Listening on [0.0.0.0] (family 0, port $($lport))"

    $handle = $listener.BeginAcceptTcpClient($null, $null)
    while (!$handle.IsCompleted) {
        Start-Sleep -m 100
    }
    $client = $listener.EndAcceptTcpClient($handle)
    $remote = $client.Client.RemoteEndPoint
    Write-Verbose "Connection from [$($remote.Address)] port $($lport) [tcp/*] accepted (family 2, sport $($remote.Port))"

    interact $client

    $client.Close()
    $listener.Stop()
} elseif ($addr -and $port) {
    $client = New-Object System.Net.Sockets.TcpClient ($addr, $port)
    Write-Verbose "Connection to $($addr) $($port) port [tcp/*] succeeded!"

    interact $client

    $client.Close()
}

PowerShellでのソケット通信は、System.Net.Sockets.TcpClient/TcpListenerによって行うことができる。 また、受信したデータを適宜処理する方法には、$stream.DataAvailableをチェックして読めるだけ読む同期処理による方法と、上のように$stream.BeginRead/EndReadを利用して非同期処理を行う方法がある。 後者の方法ではリモートからの切断を$stream.EndReadの戻り値として返される受信バイト数が0かどうかで判定できるため、上のスクリプトではpowercatにならい後者の方法で行っている。 接続してくるコネクションの待ち受け方法も同期版のAcceptTcpClientと非同期版のBeginAcceptTcpClient/EndAcceptTcpClientがあるが、前者の場合待ち受け中にCtrl+Cで終了することができなくなってしまうため、これについても後者の非同期版で行っている。

PowerShellはシェルであることもあり、コマンド実行で例外エラーが発生した場合も後続のコマンドは続けて実行されていく。 bashにおけるset -eのように初回の例外エラーで終了させるには、$ErrorActionPreference = "Stop"とすればよい。 また、文字列出力コマンドとしてWrite-Hostの代わりにWrite-Verboseを用いると、$VerbosePreferenceの値によって出力の有無を切り替えることができる。

bashgetoptsなどに対応するコマンドライン引数処理の補助機能として、PowerShellにもParamがある。 Paramは指定した変数に実際の値がセットされるようにすることができ、エイリアス作成や種々のバリデーションも行うことができる。

PowerShellにおいて1行コメントは#複数行コメントは<# #>である。 また、エスケープ文字にはバックスラッシュ(\n)ではなくチルダ(`n)を用いる。

コマンドプロンプトを二つ開き、それぞれで待ち受け、接続を行うと次のようになる。

>powershell -ex remotesigned -c ".\nc -v -l 4444"
詳細: Listening on [0.0.0.0] (family 0, port 4444)
詳細: Connection from [127.0.0.1] port 4444 [tcp/*] accepted (family 2, sport 50355)
server => client
client => server
>powershell -ex remotesigned -c ".\nc -v localhost 4444"
詳細: Connection to localhost 4444 port [tcp/*] succeeded!
server => client
client => server

前のエントリ正規表現フィルタでは-fオプションを用いてスクリプトを直接実行したが、ここでは-cオプションでコマンドとして指定することでスクリプトに引数を与えて実行している。 また、同一ディレクトリにあるスクリプトを実行するため、スクリプト名の前に.\をつける必要がある。

関連リンク