ポートスキャンだけで落ちるTCPサービスを作る

2020/05時点、一部の界隈で、ポートスキャンだけで落ちるサービスってどうよ、という感じのプチ流行があったので無理やりポートスキャンだけで落ちるサービスをC#/.NET Frameworkで作ってみた。

# ポートスキャンの仕組み

とりあえず

- TCPの3Way-Handshakeができたらポートオープン。
- できなければ、ポートクローズまたはポートフィルタリング

というのが、ポートスキャン

3Way-Handshakeまで進まなくても、

- Synパケットを送って、Syn-Ackが戻ってきたらポートオープン。
- Syn-Ackが戻ってこなければ、ポートクローズまたはポートフィルタリング

でもよい。

今回のプログラムは、3Way-Handshakeするポートスキャンで落ちるタイプを作ってみた。

# ポートスキャンするプログラム

nmapでもよいけど、netcatで十分。

`c:\> nc.exe -nvv -z -w 2 IPアドレス ポート`

StreamRelay.NET.exeでもできるよ。
`StreamRelay.NET.exe -Localport 0 -ZeroIO -RemotePort ポート -RemoteHost IPアドレス`

[netcat の代替としての StreamRelay.NET.exe → ポートスキャンとしての netcatの代替](https://qiita.com/tomoki0sanaki/items/d2523a2c2b02419af194#%E3%83%9D%E3%83%BC%E3%83%88%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE-netcat%E3%81%AE%E4%BB%A3%E6%9B%BF)

とか

[netcat の代替としての StreamRelay.jar → ポートスキャンとしての netcatの代替](https://qiita.com/tomoki0sanaki/items/c8794253f715e651c879#%E3%83%9D%E3%83%BC%E3%83%88%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AE-netcat%E3%81%AE%E4%BB%A3%E6%9B%BF)

`StreamRelay.bat -LocalPort 0 -RemoteHost IPアドレス -RemotePort ポート -ZeroIO`

こっちは、Java版のStreamRelay.jarの場合ね。

# ポートスキャンで落ちる仕組み

3Way-Handshakeまでしてしまうと、接続要求はアプリケーションにまで渡されるので、アプリケーションが想定しているプロトコルとは異なるデータ(または即時切断という想定しているプロトコルとしては想定外の挙動)によって、プロセス全体が落ちるような場面がある。
ということ。

しかしながら、普通はこういうふうにプログラムは書かないとは思うけど・・・なので、一般的ケースというよりは稀なケース。

# ソース

ポートスキャンで落ちるTCPサービスのソースはこんな感じ

```csharp:VulnSrv.cs
/* Copyright (c) 2020, "Tomoki Sanaki"<sanaki@cc.rim.or.jp,active@window.goukaku.com>
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, 
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, 
  this list of conditions and the following disclaimer in the documentation 
  and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the names of its contributors 
  may be used to endorse or promote products derived from this software 
  without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
    class VulnSrv{
        static void Main(string[] args){
            UInt16 port = 99;
            if (0 < args.Length) {
                try{
                    port = UInt16.Parse(args[0]);
                }catch {
                    Console.Error.WriteLine("VulnSrv.exe <<port>> (default:99)");
                }
            }
            IPEndPoint ipEndPoint = new IPEndPoint(IPAddress.Any, (int)port);
            TcpListener listener = new TcpListener(ipEndPoint);
            listener.Start();
            while (true){
                TcpClient client = listener.AcceptTcpClient();
                Byte[] hako;
                Recieve(client, out hako);
                Console.WriteLine(Encoding.Default.GetString(hako));
            }
        }
        static void Recieve(TcpClient client, out Byte[] recv) {
            recv = null;
            Byte[] hako = new Byte[1024];
            int len = client.GetStream().Read(hako, 0, 1024);
            if (0 < len) { 
                recv = new Byte[len];
                Array.Copy(hako, recv, len);
            }
        }
    }
```

*(ライセンス表記は邪魔かな)*

ポイントは、

- TCPの接続を受信したら、子スレッド/子タスクを起動して、そちらに処理を移譲するべきところを親の待ち受けスレッドで処理してしまっているところ。
- そして、無理やり例外を出すようにした。

例えば、特殊なプロトコルのTCPサービスを想定していて、

- 接続後にクライアントから数バイト必ず送信される(そういうプロトコル)
- なので、その数バイトは受信するという前提で、親スレッド上の処理としてしまった。
- その数バイトは受信するという前提なので、受信しないとか、別のデータを受信したとかいう例外処理を記述するのを忘れてしまった。

という事態がありうるのかなぁ～、、、と。
(優秀なプログラマさんは、こんなことはしないと思うけど・・・)

# ソースのコンパイル

最近のWindowsには標準でコンパイラ(C#コンパイラ)が付属しているので、こんな感じでコンパイルする

```console:
c:\zzz>dir
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は A474-BE95 です

 c:\zzz のディレクトリ

2020/05/29  18:17    <DIR>          .
2020/05/29  18:17    <DIR>          ..
2020/05/29  18:01             1,212 VulnSrv.cs
               1 個のファイル               1,212 バイト
               2 個のディレクトリ  81,723,904,000 バイトの空き領域

c:\zzz>c:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:exe /out:VulnSrv.exe VulnSrv.cs
Microsoft (R) Visual C# Compiler version 4.8.3752.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240


c:\zzz>dir
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は A474-BE95 です

 c:\zzz のディレクトリ

2020/05/29  18:17    <DIR>          .
2020/05/29  18:17    <DIR>          ..
2020/05/29  18:01             1,212 VulnSrv.cs
2020/05/29  18:17             4,608 VulnSrv.exe
               2 個のファイル               5,820 バイト
               2 個のディレクトリ  81,723,891,712 バイトの空き領域

c:\zzz>
```

# テスト(再現)、または検証

## 実際にテストしてみる1

サーバ側

```console:
c:\zzz>VulnSrv.exe

ハンドルされていない例外: System.ArgumentNullException: 配列は null にはできません。
パラメーター名:bytes
   場所 System.Text.Encoding.GetString(Byte[] bytes)
   場所 VulnSrv.Main(String[] args)

c:\zzz>
```

クライアント側

```console:
E:\c>netstat -an -p tcp | find ":99"
  TCP         0.0.0.0:99             0.0.0.0:0              LISTENING
  TCP         0.0.0.0:9930           0.0.0.0:0              LISTENING
  TCP         127.0.0.1:9983         0.0.0.0:0              LISTENING

E:\c>nc.exe -nvv -z -w 2 127.0.0.1 99
(UNKNOWN) [127.0.0.1] 99 (?) open
sent 0, rcvd 0

E:\c>netstat -an -p tcp | find ":99"
  TCP         0.0.0.0:9930           0.0.0.0:0              LISTENING
  TCP         127.0.0.1:9983         0.0.0.0:0              LISTENING

E:\c>
```

99/tcpが落ちている。・・・当然だけど・・・

## 実際にテストしてみる2

netcatで強制切断(CTRL-C)してみる。
RSTパケットが飛ぶんだっけ!?


サーバ側

```console:
c:\zzz>VulnSrv.exe

ハンドルされていない例外: System.IO.IOException: 転送接続からデータを読み取れません: 既存の接続はリモート ホストに強制的に切断されました。。 ---> System.Net.Sockets.SocketException: 既存の接続はリモート ホストに強制的に切断されました。
   場所 System.Net.Sockets.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 size)
   --- 内部例外スタック トレースの終わり ---
   場所 System.Net.Sockets.NetworkStream.Read(Byte[] buffer, Int32 offset, Int32 size)
   場所 VulnSrv.Recieve(TcpClient client, Byte[]& recv)
   場所 VulnSrv.Main(String[] args)

c:\zzz>
```

クライアント側

```console:
E:\c>netstat -an -p tcp | find ":99"
  TCP         0.0.0.0:99             0.0.0.0:0              LISTENING
  TCP         0.0.0.0:9930           0.0.0.0:0              LISTENING
  TCP         127.0.0.1:9983         0.0.0.0:0              LISTENING

E:\c>nc.exe -nvv 127.0.0.1 99
(UNKNOWN) [127.0.0.1] 99 (?) open
^C
E:\c>netstat -an -p tcp | find ":99"
  TCP         0.0.0.0:9930           0.0.0.0:0              LISTENING
  TCP         127.0.0.1:9983         0.0.0.0:0              LISTENING

E:\c>
```

99/tcpが落ちている。パート2。・・・当然だけど・・・

## 後始末

場合によっては、Windows Firewallで「**VulnSrv.exe**」や**ポート**がオープンになっているかもしれない。
テストした後に閉じることは忘れないように。

# 脆弱性診断で想定できるのか・・・

時々「ポートスキャンで落ちるかもしれないって想定していないとはどういうことか!!」とお怒りになるお客様がいるのだけど、こういうプログラムの書き方は普通はしないので、そんな稀なケースのプログラムが動作しているなんて、想定できないです。

# まとめ

当たり前のことだけど・・・

- TCPサーバで、接続を受信したら、すぐに子スレッド/子タスクに処理を移譲して、親スレッドは新規受信処理だけに専念させよう
- 自分が想定しているプロトコルとは異なるデータが来るかもしれないという例外処理はしっかり書こう
- そしてその例外処理で全体が落ちる事はないようにしよう

# 思い出ばなし

[Zebedee](https://sourceforge.net/projects/zebedee/) のDoS脆弱性{[CVE-2005-2904](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2005-2904)}を発見した際も(発見者の補助をしました)、パケットの先頭から数バイト目の値が0x00の場合、不正プロトコルという事で、(その接続だけ切断すればいいものを)全体を停止していたなぁ、と。

確かAssertionが発動していたという記憶。
Assertionなので、上記のテストプログラム(VulnSrv.cs)のようなテキトーなプログラミングではなくて、むしろ適切に入力値チェック(Assert)をしたうえでAssertionが発動してしまったと記憶している。

# 以上