【Ruby】RawSocketを使ったネットワークプログラム


RubyでRawSocketを使いポートスキャンツールを作ります。

Pythonにはscapyという便利なライブラリーがあります。
Rubyにそのようなものがあるのか調べてませんしわからないので、スクラッチで作ることになります。


ソケット作成


synスキャンを行うためrawsocketを使う必要があります。
Rubyでは以下のようにしてraw socketを作成します。
c言語どほとんど変わりはありませんでした。
アドレスファミリー、ソケットタイプ、プロトコルを指定します。

  def make_socket()
    soc = Socket::open(Socket::AF_INET,Socket::SOCK_RAW,Socket::IPPROTO_TCP)
    soc
  end

tcpヘッダを自分で編集するための設定として
ソケットタイプにSOCK_RAW
プロトコルにIPPROTO_TCP
を指定します。


TCPヘッダーの作成


ヘッダーの作成方法に関しては僕が知らないだけでもっといいやり方があるかもしれません。

今回は以下の方法で作成しました。

TCPはパケットの内容が壊れていないかを判定するためチェックサムを計算しますが、その時に擬似ipヘッダーというものを加えます。
これは正しい通信相手からのパケットかどうかを判断するためだです。

諸々のデータを決定してpackで文字列にします。
packに渡している「n2N2C2n3」は値の型と数を表しています。
「n2」は2byteの値を2個、
「N2」は4byteの値を2個、
「C2」は1byteの値を1個、
「n3」は2byteの値を3個といった具合に配列の値を文字列に変換してくれます。
ですのでtcpヘッダーの構成を考えると「n2N2C2n3」をpackに渡すことになります。

特に難しいことはしていません。

  def make_tcp(sport: 9999,dport: 31337,seq: 0,ackseq: 0,doff:5,
               fin: 0,syn: 0,rst: 0,psh: 0,ack: 0,urg: 0,window:300,urp:0,
               src: "127.0.0.1",dst: "127.0.0.1",dlen: 20)

    # 擬似ヘッダ
    pseudo = [IPAddr.new(src).to_i,IPAddr.new(dst).to_i,0,Socket::IPPROTO_TCP,dlen]
    pseudo_packet = pseudo.pack("N2C2n1")

    flags = fin + (syn << 1) + (rst << 2) + (psh << 3) + (ack << 4) + (urg << 5)
    doff = (doff << 4)
    cksum = 0
    data = [sport,dport,seq,ackseq,doff,flags,window,cksum,urp]
    tcp_packet = data.pack("n2N2C2n3")

    cksum = checksum(pseudo_packet+tcp_packet)
    data = [sport,dport,seq,ackseq,doff,flags,window,cksum,urp]
    data.pack("n2N2C2n3")

  end

チェックサムの計算


パケットのチェックサムを計算する関数です。

2byte単位で計算していきます

  # チェックサム計算
  def checksum(data)
    len = data.size / 2 * 2
    sum = 0

    0.step(len-1,2) do |i|
     sum += (data[i+1].ord << 8) + data[i].ord
    end

    if data.size % 2 != 0
      sum += data[-1].ord
    end

    while (sum >> 16) != 0
      sum = (sum >> 16) + (sum & 0xffff)
    end

    sum = sum >> 8 | (sum << 8 & 0xff00)
    return ~sum & 0xffff
  end

スキャン


1から65535までのポートを対象にスキャンを開始します。
ひとつのポートにつき10回までスキャンを試みます。

以下の終了条件を満たすまでパケットを送り続けます。

  1. 10回パケットの送受信をする
  2. syn,ackブラグがたったパケットを受信する
  3. フラグ上記の②以外の状態のパケットを受信する


項目として十分ではないかもしれませんが、ポートスキャナーとして最低限の動きはしてくれると思います。

  def scan(target_ip)
    puts "target host: #{target_ip}"

    for i in 1..65536
      packet = make_tcp(sport:9999, dport: i, src: {自分のip}, dst: target_ip, syn: 1)
      addr = Socket.pack_sockaddr_in(i, target_ip)
      for _ in 0..10
        @soc.send(packet, 0, addr)
          sel = IO.select([@soc],nil,nil,10)
          if sel != nil && sel_soc = sel[0]
            packet = sel_soc[0].recv(1024)

            # パケットを解析
            ip_data = extract_ip(packet[0...20])
            hl = ip_data[0] & 0x0f
            start = hl*4
            tcp_data = extract_tcp(packet[start...start+20])

            # ターゲットからのパケットか確認
            if ip_data[8] == IPAddr.new(target_ip).to_i
              # ターゲットポートからのデータ && syn,ackフラグが立っている
              if tcp_data[0] == i
                if  ( (1 << 1) + (1 << 4) ) == tcp_data[5]
                  puts "- port: #{i} open"
                end
                break
              end
            end
          end
        end
    end
  end

パケットからヘッダーを抽出


ターゲットからパケットを受信したあと、そこからipヘッダーとtcpヘッダーを取得します。

パケットを送信する際にpackを使用して文字列にして送信しました。
今回は文字列に対しunpackを使用して配列を取得します。

  # 受信パケットからipヘッダ取得
  def extract_ip(packet)
    packet.unpack("C2n3C2n1N2")
  end

  # 受信パケットからtcpヘッダ取得
  def extract_tcp(packet)
    packet.unpack("n2N2C2n3")
  end

全体


ソースコード全体はこうなりました。

PortScannerとしてクラスを作成しました。

require 'socket'
require 'ipaddr'

class PortScanner
  def initialize()
    @soc = make_socket()
  end

  # ソケット作成 rawsocket
  def make_socket()
    Socket::open(Socket::AF_INET,Socket::SOCK_RAW,Socket::IPPROTO_TCP)
  end

  # チェックサム計算
  def checksum(data)
    len = data.size / 2 * 2
    sum = 0

    0.step(len-1,2) do |i|
     sum += (data[i+1].ord << 8) + data[i].ord
    end

    if data.size % 2 != 0
      sum += data[-1].ord
    end

    while (sum >> 16) != 0
      sum = (sum >> 16) + (sum & 0xffff)
    end

    sum = sum >> 8 | (sum << 8 & 0xff00)
    return ~sum & 0xffff
  end

  # 受信パケットからipヘッダ取得
  def extract_ip(packet)
    packet.unpack("C2n3C2n1N2")
  end

  # 受信パケットからtcpヘッダ取得
  def extract_tcp(packet)
    packet.unpack("n2N2C2n3")
  end

  # スキャン開始
  def scan(target_ip)
    puts "target host: #{target_ip}"

    for i in 1..65535
      packet = make_tcp(sport:9999, dport: i, src: {自分のip}, dst: target_ip, syn: 1)
      addr = Socket.pack_sockaddr_in(i, target_ip)
      for _ in 0..10
        @soc.send(packet, 0, addr)
          sel = IO.select([@soc],nil,nil,10)
          if sel != nil && sel_soc = sel[0]
            packet = sel_soc[0].recv(1024)

            # パケットを解析
            ip_data = extract_ip(packet[0...20])
            hl = ip_data[0] & 0x0f
            start = hl*4
            tcp_data = extract_tcp(packet[start...start+20])

            # ターゲットからのパケットか確認
            if ip_data[8] == IPAddr.new(target_ip).to_i
              # ターゲットポートからのデータ && syn,ackフラグが立っている
              if tcp_data[0] == i
                if  ( (1 << 1) + (1 << 4) ) == tcp_data[5]
                  puts "- port: #{i} open"
                else
                  break
                end
              end
            end
          end
        end
    end
  end

  # tcpヘッダ作成
  def make_tcp(sport: 9999,dport: 31337,seq: 0,ackseq: 0,doff:5,
               fin: 0,syn: 0,rst: 0,psh: 0,ack: 0,urg: 0,window:300,urp:0,
               src: "127.0.0.1",dst: "127.0.0.1",dlen: 20)

    # 擬似ヘッダ
    pseudo = [IPAddr.new(src).to_i,IPAddr.new(dst).to_i,0,Socket::IPPROTO_TCP,dlen]
    pseudo_packet = pseudo.pack("N2C2n1")

    flags = fin + (syn << 1) + (rst << 2) + (psh << 3) + (ack << 4) + (urg << 5)
    doff = (doff << 4)
    cksum = 0
    data = [sport,dport,seq,ackseq,doff,flags,window,cksum,urp]
    tcp_packet = data.pack("n2N2C2n3")

    cksum = checksum(pseudo_packet+tcp_packet)
    data = [sport,dport,seq,ackseq,doff,flags,window,cksum,urp]
    data.pack("n2N2C2n3")

  end
end

動作確認


実行してみましょう

target_ip = ARGV[0]

scanner = PortScanner.new()
scanner.scan(target_ip)


実行するときのコマンドと結果はこうなります。

sudo ruby port_scan.rb 192.168.11.38
target host: 192.168.11.38
- port: 21 open
- port: 22 open
- port: 23 open
- port: 25 open
- port: 53 open
- port: 80 open
- port: 111 open
- port: 139 open
- port: 445 open
- port: 512 open
- port: 513 open
- port: 514 open
- port: 1099 open
- port: 1524 open
- port: 2049 open
- port: 2121 open
- port: 3306 open
- port: 3632 open
- port: 5432 open
- port: 5900 open
- port: 6000 open
- port: 6667 open
- port: 6697 open
- port: 8009 open
- port: 8180 open
- port: 8787 open
- port: 33774 open
- port: 43909 open
- port: 54497 open
- port: 58138 open