Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでポートスキャナーを作成する方法【TCP通信の基礎から実装まで】

なぜポートスキャナーを自作したいと思うのでしょうか?

ポートスキャナーの作成は、TCPプロトコルの基礎を学ぶ絶好の方法です。TCPは、HTTPやSSHを含むほとんどのインターネットプロトコルが利用するトランスポート層(伝送層)です。

また、Rubyのネットワークプログラミングの仕組みを深く理解するための良い演習にもなります。

それでは、ポートについての説明から始めましょう!

ポートとは何か?

「ポート」というとき、私たちは実際には何を指しているのでしょうか?OS(オペレーティングシステム)レベルにおけるポートとは、単純に言えばプロセスに関連付けられた「ファイルディスクリプタ」のことです。

ファイルディスクリプタとは、stdout(標準出力)、ネットワークソケット、ファイルといった開いているI/Oチャネルを参照するために使われる番号です。

OSがTCP/IPパケットを受信すると、宛先ポートを確認し、そのポートで待ち受けている(リッスンしている)プロセスを探します。

リッスン中のプロセスが存在すれば、パケットはそのプロセスへと配信されます。

ポートの範囲

利用可能なポートは合計65,535個ありますが、実際にはすべてが日常的に使われているわけではありません。

ポートは大きく3つのグループに分けられます。

範囲 名称
1-1023 ウェルノウンポート 22 (SSH)、80 (HTTP)、443 (HTTPS)
1024-49151 登録済みポート 3306 (MySQL)、5432 (PostgreSQL)
49152-65535 エフェメラルポート Chrome、Firefox

2つ目の範囲のポートは、IANA(Internet Assigned Numbers Authority:インターネット割り当て番号管理機関)によって割り当てられています。

3つ目の範囲は「ダイナミック」または「エフェメラル」ポートと呼ばれ、接続のクライアント側がサーバーからデータを受信する際に使用されるポートです。

TCP通信の基礎

これでポートについては理解が深まりましたが、まだポートスキャナーを書く準備は整っていません。

まず、「ポートが開いている」とはどういう意味なのかを確認する必要があります。そのうえで、開いているポートと閉じているポートがネットワークレベルでどのように振る舞うかを調べ、両者を区別できるようにしましょう。

ポートが開いているかどうかは、どうすればわかるのでしょうか?

ポートが開いているとは、相手側でアプリケーションがリッスンしており、かつアクセス可能である(ファイアウォールでブロックされていない)ことを意味します。

それでは、新しいTCP接続がどのように開始されるのか見てみましょう。

Rubyでポートスキャナーを作成する方法【TCP通信の基礎から実装まで】

新しいTCP接続はSYNパケットから始まります。このパケットは、新規接続の開始を表すものです。

起こりうる結果は3つあります

  • サーバーがSYN/ACKで応答する ― 接続を確立する準備ができていることを意味します
  • サーバーがRSTパケットで応答する ― 接続が拒否されたことを意味します
  • サーバーがまったく応答しない ― 一部のファイアウォールでは、DROPポリシーとしてこの挙動を採用しています

クライアントがSYN/ACKパケットを受信すると、今度はACKパケットを送信することで接続の確立を完了できます。

これが「スリーウェイハンドシェイク(3ウェイハンドシェイク)」と呼ばれる仕組みです。

補足:このやり取りを実際に観察するには、tcpdumpやWiresharkといったネットワークツールを使うのがおすすめです。これらのツールを使えば、システムに出入りするすべてのパケットを目視で確認できます。

以下はtshark(Wiresharkのコマンドラインインターフェース)による接続の例です。

Rubyでポートスキャナーを作成する方法【TCP通信の基礎から実装まで】

TCP接続がどのように確立されるのか、基本を理解できたところで、シンプルなポートスキャナーを書いてみましょう。

ポートスキャナーを書いてみよう!

最もシンプルな実装方法は、TCPSocketで新しいTCP接続を開き、接続が拒否された場合にErrno::ECONNREFUSED例外が発生するという性質を利用することです。

コードはこちらです

require 'socket'

PORT = ARGV[0] || 22
HOST = ARGV[1] || 'localhost'

begin
  socket = TCPSocket.new(HOST, PORT)
  status = "open"
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT
  status = "closed"
end

puts "Port #{PORT} is #{status}."

ただし、このコードにはいくつか制限があります。

まず、一度にスキャンできるのは1つのポートだけです。特定のホストでどのようなサービスが公開されているかを把握するには、通常はポートの範囲全体をスキャンしたくなるでしょう。

もう1つの問題は、応答を返さないポート(先述の3つの結果のうち3番目)に遭遇した場合、接続がタイムアウトするまで約20秒も待たされてしまうことです。

この問題は、connect_nonblockIO.select、そしてSocket(オブジェクト生成時に即座に接続を試みてしまうTCPSocketの代わり)を組み合わせることで解決できます。

実装例

require 'socket'

TIMEOUT = 2

def scan_port(port)
  socket      = Socket.new(:INET, :STREAM)
  remote_addr = Socket.sockaddr_in(port, 'www.example.com')

  begin
    socket.connect_nonblock(remote_addr)
  rescue Errno::EINPROGRESS
  end

  _, sockets, _ = IO.select(nil, [socket], nil, TIMEOUT)

  if sockets
    p "Port #{port} is open"
  else
    # ポートは閉じている
  end
end

PORT_LIST = [21,22,23,25,53,80,443,3306,8080]
threads   = []

PORT_LIST.each { |i| threads << Thread.new { scan_port(i) } }

threads.each(&:join)

IO.selectの呼び出しは、ソケットがデータを受信できる状態になるまで(つまりポートが開いている場合)、あるいはTIMEOUTで指定した時間が経過するまで待機します。タイムアウトした場合は、そのポートが閉じているか、接続要求を無視していると判断できます。

学習目的としては十分ですが、本格的なポートスキャナーが必要な場合は、nmapのような専用ツールを使うのが賢明です。

Nmapはオープンソースで、15年以上にわたり活発に開発が続けられている実績のあるツールです。

nmapの出力は次のようになります。

Rubyでポートスキャナーを作成する方法【TCP通信の基礎から実装まで】

情報量が多く感じられたかもしれません。記憶に定着させたい方は、この記事から学んだことを2つ、ぜひコメントで共有してみてください🙂

まとめ

この記事では、ポートとは何か、ポートの種類と範囲、TCP接続の確立方法(スリーウェイハンドシェイク)、そしてRubyを使った基本的なポートスキャナーの実装方法について学びました。

この記事が役に立ったと感じたら、より多くの人に届くようぜひシェアしてください🙂


  1. RubyのStructとOpenStructの使い方を徹底解説!値オブジェクトを簡単に作る方法

    Rubyには、関連する属性をまとめて保持する「値オブジェクト」を手軽に作れるStructという組み込みクラスが用意されています。本記事では、Structの基本的な使い方から注意点、そしてOpenStructとの違いまで詳しく解説します。 そもそもStructとは何か? StructはRubyに組み込まれたクラスで、新しいクラスを動的に生成し、値オブジェクト(Value Object)を作成するために使われます。値オブジェクトとは、関連性のある複数の属性をひとまとめにして扱うためのオブジェクトです。 具体例を挙げてみましょう。 2つの座標(xとy)を持つPointというデータを表したい場合、この

  2. Rubyでファイルを読み書きする方法を実例つきで徹底解説

    Rubyでファイルの読み書きができるようになると、ファイルからデータを抽出したり、新しいファイルを作成したり、必要な情報を素早く見つけたりできるようになります。この記事では、Rubyにおけるファイル操作の基本を、実際のコード例とともにわかりやすく解説します。 この記事で学べること Rubyでファイルを読み込む方法 Rubyでファイルに書き込む方法 覚えておきたいFileクラスのメソッド ディレクトリ操作の基礎 FileUtilsモジュールの使い方 まとめ それでは始めましょう! Rubyでファイルを読み込む方法 Rubyでファイルを読み込む基本的な流れは、次の3ステップです。 op