Powershell ネットワーク

【Powershell】TCPクライアントの作り方

脆弱性診断やペネトレーションテストの業務を行う上ではどうやってサイバー攻撃をするかの知識が重要になってきます。こういった業務では検証などを行う中で裏で通信を行うようなプログラムを送り込んで疑似的に端末をマルウェアに感染させた状態を作ってみる、というのはよくある基本的な手法であり特にセキュリティ関連業務に従事する人は是非理解しておきたい内容です。というわけで今日はPowershellを使った基本的なネットワークプログラミングを解説していきたいと思います。もちろん他の言語でも実装は可能ですがPowershellはWindows環境に標準搭載されており実行環境をダウンロードする必要もないので、どんな環境でも動いてくれるのが大きいメリットです。

実行環境

・Windows10 開発環境:Powershell ISE

Powershellのソースコード

それでは最終的にどんなソースコードを書くのか、まずは全体を確認してみましょう。ゼロから作るのは少々めんどくさいので先人が残してくれたプログラムを参考にしながら解説していこうと思います。ただデータを送るだけではなく接続を確立した後はデータの受信も行いたいので、その機能も組み入れていきます。ちなみに先頭に連続で番号が振ってありますがこれは解説をわかりやすくするためのものであり、実際に入力するとエラーになるのでコピペする際は先頭の数字を削除してください。

1 try {
2     try {
3         $socket = New-Object System.Net.Sockets.TcpClient('192.168.11.5', 7777)
4     } catch {
5         Write-Host "Failed to connect the server."
6     }
7 
8     $stream = $socket.GetStream()
9     $writer = New-Object System.IO.StreamWriter($stream)
10    $buffer = New-Object System.Byte[] 1024
11    $encoding = New-Object System.Text.AsciiEncoding
12    
13    $writer.WriteLine("Established!")
14    $writer.Flush()
15    
16    	while($true) {
17		    start-sleep -m 100
18		    while($stream.DataAvailable) {
19			    $read = $stream.Read($buffer, 0, 1024)
20			    $data = $encoding.GetString($buffer, 0, $read)
21		    }
22            Write-Host $data
23            $message = Read-Host "Input a message "
24            $writer.WriteLine($message)
25	      $writer.Flush()
26	    }
27
28} finally {
29    $writer.Close()
30    $stream.Close()
31}

TCPClientクラスを利用しコネクションを確立する

1行目、28行目~31行目:TCPConnectionを確立した後予期せぬエラーが起きてコネクションが残ってしまう可能性があるため、try-finally文でもしエラーが起きた際はリソースを解放するようにしています。

2行目~6行目:3行目でTCPクライアントクラスのオブジェクトを生成しますが、IPとPORTの指定が間違っていた場合に備え、エラーが起きた際はcatchに移行しWrite-Hostを用いてコンソールにエラーメッセージを表示します。例では私の環境のIPとポートを指定してますが、ここは環境に合わせて適宜読み変えてください。

8行目:3行目で生成したTcpClientクラスのオブジェクトである$socketのGetStreamメソッドを利用してネットワークの送受信に使用するストリームを取得します。ここでいうストリームとはデータの入出力を扱う通路のようなものだと捉えておけば良いでしょう。

9行目:System.IO.StreamWriterクラスの引数に$streamを渡し、ネットワークストリームにデータを書き込むためのオブジェクトを生成します。データを送る際はこのクラスを利用して書き込みを行います。

10行目:送られてきたデータを読み込む際、そのデータを1024個の要素を持つByte型の配列に書き込みます。PowershellではByte型の配列を宣言するにはこのようにNew-Objectコマンドレットを利用します。

11行目:System.Text.AsciiEncodingクラスのオブジェクトを生成します。後ほどこのクラスのGetString関数を使って受信したバイト列のデータを文字列にエンコードします。

13~14行目:System.IO.StreamWriterクラスのWriteLineメソッドを使って、指定したIPとポートのネットワークストリームにEstablieshed!(コネクションを確立しました!)というメッセージを書き込みます。似たようなメソッドにWriteというメソッドがありますがこちらは行末に改行が入らないのが特徴です。flush()関数は英単語そのままですがストリームに書き貯めたデータを流し込みます(送信します)。

データの受信及び送信とバイト列を文字列に変換

17行目:待機時間を入れずに無限ループを実行すると負荷がかかるので、start-sleepコマンドレットを利用してループの間に小休止をいれてあげます。引数に-mを指定してますがこれはMilliSecondsを表しており、今回の場合0.1秒プログラムを一時停止するようにします。

18行目:8行目のGetstream()を実行したことでNetworkStreamクラスのオブジェクトが$streamに入っていますが、NetworkStreamクラスにDataAvailableというプロパティがあり、これはストリームに読み取り可能なデータがあるかどうかを判定するためのプロパティになります。これを使って送られてきたデータがないか確認します。

https://learn.microsoft.com/ja-jp/dotnet/api/system.net.sockets.networkstream.dataavailable?view=net-6.0#system-net-sockets-networkstream-dataavailable

19行目:NetworkStreamクラスのReadメソッドを利用してストリームに送られてきたデータを$read変数に代入します。引数にはデータの格納先であるバイト列、データの書き込みをどこから開始するか示すオフセット、どれだけのデータを読み込むのかを表す数値をそれぞれ指定します。第一引数には送られてくるデータがByte型であるため、10行目で用意しておいたByte型の配列を指定します。第二引数にはデータの書き込みを行う位置の指定はないので0、第三引数には1024を指定します。ネットワークの読み込みを行う際のバイト列の長さの指定はハードウェアとの互換性を踏まえ、1024や4096といった2の累乗が推奨されています(基本的にコンピュータは2進数で動いているので2の累乗の方が扱いやすい)。データの読み込みが行われると戻り値として$readにデータの長さが代入されます。

20行目:System.Text.AsciiEncodingクラスのGetStringメソッドを利用して、読み込まれてきたデータが格納されている$buffer変数の中身を人間が読める形の文字列にデコードします。引数には先頭から順にデコード対象のバイト型文字列、デコードの開始位置、デコードするバイト数を指定します。今回デコードするのは$buffer変数で、開始位置は先頭から、$readには読み込んできたデータのバイト数がはいっているので$buffer,0,$readの順に指定します。

22行目~25行目:Write-Hostコマンドレットで受信したデータを確認できるようコンソールに表示します。続いてRead-Hostコマンドレットでコンソールから入力を受け付けその結果を$messageに代入し、WriteLineでストリームに書き込みます。その後flushで書き込んだデータを送信します。

-Powershell, ネットワーク