ポートスキャナは、ネットワーク上のホストでどのポートが開いているか (= どんなサービスが動いているか) を調べる古典的なネットワークツール。本記事では C++ + WinSock API を使って、最もシンプルな TCP Connect スキャンを自作する。ソケット作成 → IP/ポート設定 → connect 試行 → 成否判定という TCP 接続の基本を 1 つの小さなプログラムで体感し、nmap が水面下で何をしているかを理解する。同時に、攻撃者にとってのポートスキャンが何を意味するかと、防御側の対策を整理する。
他人が管理するネットワーク / サーバに対して 許可なくポートスキャンを実行する行為は、多くの場合 利用規約違反 / 不正アクセス禁止法 (の準備行為) / 攻撃的偵察と見なされ、ISP 側で接続切断・刑事問題化することがある。「ping を打っただけ」「ポートを叩いただけ」でも記録されてアラートが上がる時代。実行する際は ローカルホスト (127.0.0.1) / 自分の VM / Hack The Box などの合法な練習環境に限定する。クラウド事業者 (AWS / Azure / GCP) のリソースをスキャンする場合も、事前申請が必要 (AWS は 2019 以降原則申請不要だが、規約は確認)。
ポートスキャナとは #
ポートスキャナ (Port Scanner) は、ネットワーク上のコンピュータに対し、どの TCP/UDP ポートが通信可能 (開いている) 状態にあるかを調査するプログラム。サーバーがどのようなサービス (例: Web サーバーならポート 80 や 443、SSH ならポート 22) を提供しているかを特定するために使われる。
システム管理者による正当なネットワーク管理や脆弱性診断で使われる一方、攻撃者による不正アクセスの準備段階 (偵察) でも悪用される。
1 軒の家には 玄関、勝手口、窓、天窓など多くの出入り口があり、家の住人は 「玄関は鍵、窓は閉めっぱなし」などと使い分けている。ポートスキャナは 「全 65535 個の窓を 1 つずつノックして、開いているものをリストアップする」ツール。攻撃者にとっては 「侵入経路の発見」、防御側にとっては 「自分のサーバは余計な窓を開けっぱなしにしていないか」を確認するセルフチェックの手段、両側面で使われる。
実装の仕組み — TCP Connect スキャン #
ポートスキャンには TCP SYN スキャンや TCP Connect スキャンなど様々な手法があるが、今回は最もシンプルで実装が容易な TCP Connect スキャンを採用する。
これは、Windows が提供するネットワーク通信機能 WinSock (Windows Sockets) を利用して実装する。
TCP Connect スキャンの動作 #
TCP Connect スキャンは、通常の TCP 通信と同じように、対象のポートに対して 完全な接続 (3 ウェイ・ハンドシェイク) を試みる手法。
socket() でソケットを作成。connect() で接続要求を送信。connect() 成功 = OS が 3 ウェイ・ハンドシェイクを完了。このポートは 「開いている (OPEN)」と判断。connect() 失敗 / 応答なし / RST 受信の場合、ポートは 「閉じている (CLOSED)」または FW などで 「フィルタされている (Filtered)」と判断。Connect スキャン (今回実装) は 3 ウェイハンドシェイクを完全に確立するため、相手のサーバのログに「接続試行」が記録され、検知されやすい。SYN スキャン (nmap -sS) は SYN を送って SYN/ACK が返ってきたら即 RST で接続を打ち切るため、ログに残りにくい (= ステルススキャン)。SYN スキャンには root / Administrator 権限が必要で、生のパケットを作るため WinSock の Raw Socket か npcap が要る。本記事の Connect スキャンは権限不要で書ける入門サイズ。
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;
// --- コマンドライン引数の解析 ---
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "-ip") {
if (i + 1 < argc) {
targetIP = argv[i + 1];
i++;
}
} else if (arg == "-p") {
if (i + 1 < argc) {
try {
targetPort = std::stoi(argv[i + 1]);
i++;
} catch (const std::exception&) {
std::cerr << "Error: Invalid port number." << std::endl;
WSACleanup();
return 1;
}
}
}
}
if (targetIP.empty() || targetPort == 0) {
std::cerr << "Usage: " << (argc > 0 ? argv[0] : "program.exe")
<< " -ip <ip_address> -p <port>" << std::endl;
WSACleanup();
return 1;
}
// --- スキャン処理 ---
if (isPortOpen(targetIP, targetPort)) {
std::cout << targetIP << ":" << targetPort << " is OPEN" << std::endl;
} else {
std::cout << targetIP << ":" << targetPort << " is CLOSED or FILTERED" << std::endl;
}
WSACleanup();
return 0;
}
ポートスキャン関数 (isPortOpen) #
ここがスキャン処理の核心部。
| API | 役割 |
|---|---|
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;
}
このコードは 1 ホスト 1 ポートを試すだけのミニマム実装。「1〜1024 まで全部スキャン」のように拡張するなら、並列化 (スレッド / 非同期 I/O) + タイムアウト設定 (デフォルトの connect は数十秒待つ) + レート制限が必須。Connect スキャンを全ポートで撃つと、サーバ側のログに大量の SYN_RECV / ESTABLISHED が記録され、すぐに検知される。学習用に拡張するなら、必ず自分の VM / ローカルホスト限定で。
脅威 — ポートスキャンによる偵察 #
攻撃者はポートスキャンを **「攻撃の入り口探し」**に利用する。
攻撃の流れ #
nmap -sV でポートで稼働しているサービスのバージョン (例: Apache 2.4.41, OpenSSH 8.0) を特定。searchsploit / CVE Details で攻撃コードを取得。ポートスキャンは、こうした一連の攻撃の最初のステップとなる。
Shodan / Censys という「常時スキャナ」 #
近年は Shodan や Censys といったサービスが、世界中のインターネットを常時スキャンして公開ホストのデータベース化を行っている。攻撃者はこれらのサービスで「特定のバージョンの脆弱なサーバ」を検索するだけで、ターゲットリストを瞬時に手に入れられる。
自宅のグローバル IP を Shodan に入れると、「世界からどう見えているか」が一発で分かる。ルータの管理画面が露出していたり、IP カメラがデフォルトパスワードで公開されていたり — 想像以上に 「攻撃者にとっての餌」が放置されている家庭は多い。「自分の家を Shodan で検索する」のは、家庭のセキュリティチェックとして最も手軽で効果的。
対策・対処 #
ポートスキャンへの対策は、サーバー防御の基本。
| 対策 | 内容 |
|---|---|
| ファイアウォール | 最も基本的な対策。外部に公開する必要のないポートは、ファイアウォール (Windows Defender ファイアウォール、ネットワーク機器など) ですべて閉じる |
| 最小限のサービス | サーバーで稼働させるサービス (開けるポート) を必要最小限に絞る |
| IDS / IPS | 短時間のうちに大量のポートへ接続試行する行為は異常な通信パターン。IDS / IPS はこれを検知し、スキャン元 IP を自動ブロックできる |
| ポートノッキング | 特定のポートを順番に叩いた後でないと本来の SSH ポートが開かない、という設定。スクリプトキディ対策に有効 |
| クラウド側の保護 | AWS Security Group / Azure NSG で「許可した IP からしかアクセスできない」設定にする |
| fail2ban | 短時間で多数の接続試行を行う IP を一時的にブロック |
今回の TCP Connect スキャンは、対象サーバーのログに接続試行の記録が残りやすいため、検知されやすいスキャン手法でもある。
ポートスキャンを自作したことの最大の収穫は 「攻撃者の目線で自分のサーバを見られるようになる」こと。攻撃者は 常に外からスキャンを撃っている前提で、「自分のグローバル IP に対して nmap / Shodan を月 1 回流す」を運用習慣にすると、想定外の開放ポート / 古いサービス / 設定ミスを早期に発見できる。Web サーバなら ALB の Security Group、家庭ルータなら UPnP / NAT の設定、SSH ポートを 22 のままで公開していないか — 外から見たときに開いていて当然な穴だけが開いている状態を維持するのが、攻撃者の偵察を「無力化」する最も効率の良い方法。
COMMENTS 1