概要

今回はC++とWinSock API (Windows Sockets) を使って、指定したIPアドレスとポートが開いているかを確認する「ポートスキャナ」を作成しました。

TCPの接続試行 (connect) の仕組みを通じて、ネットワークプログラミングの基礎とソケット通信を学ぶことが目的です。

セキュリティ分野では、ポートスキャンは攻撃者が攻撃対象のサーバーで稼働しているサービス(開いているポート)を偵察するために使用する最も基本的な手法の一つです。この仕組みを理解することは、ファイアウォールの設定や侵入検知システムの重要性を理解する上で不可欠です。

thumbnail

ポートスキャナとは

ポートスキャナ(Port Scanner)は、ネットワーク上のコンピュータに対し、どのTCP/UDPポートが通信可能(開いている)状態にあるかを調査するプログラムのことです。サーバーがどのようなサービス(例:Webサーバーならポート80や443、SSHならポート22)を提供しているかを特定するために使われます。

システム管理者による正当なネットワーク管理や脆弱性診断で使われる一方、攻撃者による不正アクセスの準備段階(偵察)でも悪用されます。

実装の仕組み

ポートスキャンには「TCP SYNスキャン」や「TCP Connectスキャン」など様々な手法がありますが、今回は最もシンプルで実装が容易な「TCP Connectスキャン」を採用します。

これは、Windowsが提供するネットワーク通信機能「WinSock (Windows Sockets)」を利用して実装します。

TCP Connectスキャン

TCP Connectスキャンは、通常のTCP通信と同じように、対象のポートに対して完全な接続(3ウェイ・ハンドシェイク)を試みる手法です。

  1. クライアント(スキャナ)がソケットを作成します (socket 関数)。
  2. 対象のIPアドレスとポート番号に接続要求を送信します (connect 関数)。
  3. 成功した場合: connect 関数が成功し、OSが3ウェイ・ハンドシェイクを完了させたことを意味します。このポートは「開いている (OPEN)」と判断できます。
  4. 失敗した場合: connect 関数が失敗し、サーバーから応答がない(またはRSTパケットが返る)場合、このポートは「閉じている (CLOSED)」またはファイアウォールなどで「フィルタリングされている (Filtered)」と判断できます。

C++によるコード解説

メイン関数 (main) - 初期化と引数解析

プログラムの起点です。まず WSAStartup でWinSockライブラリを使用するための初期化処理を行います。次に、argc (引数の数) と argv (引数の配列) を使って、コマンドラインから -ip <IP>-p <PORT> の形式で引数を受け取ります。

std::stoi は、文字列で受け取ったポート番号を数値(int)に変換するために使用しています。最後に、引数が正しく指定されているかを確認し、スキャン処理を実行します。

int main(int argc, char* argv[]) {

    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (result != 0) {
        std::cerr << "WSAStartup failed: " << result << std::endl;
        return 1;
    }

    std::string targetIP;
    int targetPort = 0; // 0で初期化

    // --- コマンドライン引数の解析 ---
    for (int i = 1; i < argc; ++i) { // argv[0] (プログラム名) はスキップ
        std::string arg = argv[i];

        if (arg == "-ip") {
            if (i + 1 < argc) { // -ip の後にIPアドレスがあるかチェック
                targetIP = argv[i + 1]; // 次の引数をIPとして取得
                i++; // IPアドレスの分もインデックスを進める
            }
        } else if (arg == "-p") {
            if (i + 1 < argc) { // -p の後にポート番号があるかチェック
                try {
                    // 次の引数(文字列)を std::stoi で int に変換
                    targetPort = std::stoi(argv[i + 1]);
                    i++; // ポート番号の分もインデックスを進める
                } catch (const std::exception&) {
                    // stoi が失敗した場合 (例: "-p abc" など)
                    std::cerr << "Error: Invalid port number." << std::endl;
                    WSACleanup();
                    return 1;
                }
            }
        }
    }

    // IPとポートが正しく設定されたかチェック
    if (targetIP.empty() || targetPort == 0) {
        std::cerr << "Usage: " << (argc > 0 ? argv[0] : "program.exe") << " -ip  -p " << std::endl;
        WSACleanup();
        return 1;
    }

    // --- スキャン処理 ---
    // (省略) ...

    WSACleanup(); // プログラム終了時にWinSockをクリーンアップ
    return 0;
}

ポートスキャン関数 (isPortOpen)

ここがスキャン処理の核心部です。

  • socket(AF_INET, SOCK_STREAM, 0): インターネット(AF_INET)用のTCP(SOCK_STREAM)ソケットを作成します。
  • sockaddr_in: 接続先(ターゲット)の情報を格納する構造体です。sin_family (AF_INET), sin_port (ポート番号), sin_addr (IPアドレス) を設定します。
  • inet_pton(...): "127.0.0.1" のような文字列形式のIPアドレスを、ネットワークが理解できるバイナリ形式に変換し、構造体に格納します。
  • connect(...): 実際にターゲットへの接続を試行します。この関数が SOCKET_ERROR を返さなければ接続成功(ポートOPEN)とみなします。
  • closesocket(sock): 接続の成否にかかわらず、使用したソケットを必ず閉じてリソースを解放します。
bool isPortOpen(const std::string& ip, int port) {
    SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == INVALID_SOCKET) {
        return false;
    }

    sockaddr_in server_addr;
    ZeroMemory(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(port); // ポート番号をネットワークバイトオーダーに変換

    // IPアドレス文字列をバイナリ形式に変換
    if (inet_pton(AF_INET, ip.c_str(), &server_addr.sin_addr) <= 0) {
        closesocket(sock);
        return false;
    }

    bool isOpen = false;
    // 接続を試行
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) != SOCKET_ERROR) {
        isOpen = true; // 接続成功
    }

    closesocket(sock); // ソケットを閉じる
    return isOpen;
}

倫理的考察

このコードは、ネットワークプログラミングとセキュリティの基礎を学ぶための教育目的で作成したものです。

他人が管理するネットワークやサーバーに対して許可なくポートスキャンを実行する行為は、多くの場合、利用規約違反や不正アクセス行為(またはその準備行為)とみなされます。攻撃的な偵察行為と受け取られ、法的措置やアクセス禁止措置の対象となる可能性があるため、実行する際はローカルホスト(127.0.0.1)や自身が管理する検証環境でのみ行ってください。

ポートスキャンによる脅威

攻撃者はポートスキャンを「攻撃の入り口探し」に利用します。開いているポートを見つけると、次はそのポートで稼働しているサービス(例: Apache 2.4.41, OpenSSH 8.0)のバージョンを特定しようとします。

もし、その特定のバージョンに既知の脆弱性(セキュリティホール)が見つかっていれば、攻撃者はそこを突いてシステムに侵入しようとします。ポートスキャンは、こうした一連の攻撃の最初のステップとなります。

対策・対処

ポートスキャンへの対策は、サーバー防御の基本です。

  • ファイアウォール: 最も基本的な対策です。外部に公開する必要のないポートは、ファイアウォール(Windows Defender ファイアウォール、ネットワーク機器など)ですべて閉じる(通信をブロックする)ことが重要です。
  • 最小限のサービス: サーバーで稼働させるサービス(開けるポート)を必要最小限に絞ります。
  • IDS/IPS (侵入検知・防御システム): 短時間のうちに大量のポートへ接続試行する(スキャンする)行為は異常な通信パターンです。IDS/IPSはこれを検知し、スキャン元のIPアドレスからの通信を自動的にブロックすることができます。
  • 今回の「TCP Connectスキャン」は、対象サーバーのログに接続試行の記録が残りやすいため、検知されやすいスキャン手法でもあります。