【Ruby】ICMPを使ったネットワークプログラミング


今回はRubyでルートスキャナーを作ります。

Rubyでのプログラミングには慣れていませんが、前回ポートスキャナーを作った時の記憶が残っているのでそれを頼りに実装していきたいと思います。

ルートスキャナーについては以前に記事を書きました。



必要なこと


tarcerouteを実装する場合いろいろ方法はありますが、
前回記事にした時と同様にicmpエコー要求を使う方向で実装したいと思います。

そのために必要なことをまとめます。

ソケット


ポートスキャンと違いルートをトレースするためにipパケットのttlフィールドをこちらで設定します。
ですのでカーネルに「ipパケットもこちらでつくるよ」ということを伝える必要があります。

    @soc = Socket::open(Socket::AF_INET,Socket::SOCK_RAW,Socket::IPPROTO_ICMP)
    @soc.setsockopt(Socket::IPPROTO_IP,Socket::IP_HDRINCL,1)


icmpのソケットを作成し、setsockoptでipヘッダーも作成するよう設定します。

パケット


今回はipパケットとicmpパケットを作成します。

ipパケットを作成する関数です。
パケット送信時にttlを指定します。

  # ipパケット作成
  def make_ip(ver: 4,hl: 5,tos: 0,len: 28,id: 1,off: 0,ttl: 64,p: Socket::IPPROTO_ICMP,src: "127.0.0.1",dst: "127.0.0.1")
    ver_hl = (ver << 4) + 5
    saddr,daddr = IPAddr.new(src).to_i,IPAddr.new(dst).to_i

    cksum = 0

    data = [ver_hl,tos,len,id,off,ttl,p,cksum,saddr,daddr]
    ip_packet = data.pack('C2n3C2n1N2')
    cksum = checksum(ip_packet)
    data = [ver_hl,tos,len,id,off,ttl,p,cksum,saddr,daddr]
    data.pack('C2n3C2n1N2')
  end


icmpパケットを作成する関数です。
icmpエコー要求パケットを送信しますので、タイプに8を指定します。

  # icmpパケット作成
  def make_icmp_echo()
    type = 8
    code = 0
    seq = 1
    id = 1
    cksum = 0

    data = [type, code, cksum, id, seq]
    cksum = checksum(data.pack('C2n3'))
    data = [type, code, cksum, id, seq]
    data.pack('C2n3')
  end


以上が今回作るツールの核になる部分です。

ソースコード全体


RouteScannerというクラスを作成しました。

ttlに設定する値として1からスタートし255までインクリメントしていきます。

readyの待ち時間を過ぎた場合は初回の込みで3回まで再送信します。

パケットを受信したら中身を確認します。
icmpのタイプがecho_replyだった場合はターゲットに到達したと判断してループを終了します。
time_exceededだった場合は再送信ループを抜けてttlをインクリメントして送信処理を続行します。

以上がこのクラスがやってくれる処理の大まかな流れです。

require 'socket'
require 'ipaddr'

class RouteScanner
  def initialize()
    @soc = Socket::open(Socket::AF_INET,Socket::SOCK_RAW,Socket::IPPROTO_ICMP)
    @soc.setsockopt(Socket::IPPROTO_IP,Socket::IP_HDRINCL,1)
  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 make_ip(ver: 4,hl: 5,tos: 0,len: 28,id: 1,off: 0,ttl: 64,p: Socket::IPPROTO_ICMP,src: "127.0.0.1",dst: "127.0.0.1")
    ver_hl = (ver << 4) + 5
    saddr,daddr = IPAddr.new(src).to_i,IPAddr.new(dst).to_i

    cksum = 0

    data = [ver_hl,tos,len,id,off,ttl,p,cksum,saddr,daddr]
    ip_packet = data.pack('C2n3C2n1N2')
    cksum = checksum(ip_packet)
    data = [ver_hl,tos,len,id,off,ttl,p,cksum,saddr,daddr]
    data.pack('C2n3C2n1N2')
  end

  # icmpパケット作成
  def make_icmp_echo()
    type = 8
    code = 0
    seq = 1
    id = 1
    cksum = 0

    data = [type, code, cksum, id, seq]
    cksum = checksum(data.pack('C2n3'))
    data = [type, code, cksum, id, seq]
    data.pack('C2n3')
  end

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

  # 一応受信したパケットを確認
  def analyze(packet)
    # エコー応答
    icmp_echo_reply = 0
    # 時間超過
    icmp_timex = 11

    icmp = packet.unpack('C2n3')
    type = icmp[0]

    if type == icmp_echo_reply
      return 1
    elsif type == icmp_timex
      return 0
    end
    2
  end

  # スキャン開始
  def scan(target_ip)
    puts "Scanning to #{target_ip} ..."

    # 到達したかどうか
    reach = 0

    # 1~255までttlをインクリメントする
    for i in 1..256
      packet = make_ip(src: "0.0.0.0", dst: target_ip, ttl: i)+make_icmp_echo()
      addr = Socket.pack_sockaddr_in(0, target_ip)

      # 3回繰り返す
      for _ in 0..3

        @soc.send(packet, 0, addr)
        sel = IO.select([@soc], nil, nil, 10)
        if sel != nil && sel_soc = sel[0]
          packet, sockaddr = sel_soc[0].recvfrom(1024)
          ip = extract_ip(packet)
          # protocol
          if ip[6] == Socket::IPPROTO_ICMP
            sockaddr = Socket.unpack_sockaddr_in(sockaddr)
            start = (ip[0] & 0x0f) * 4
            res = analyze(packet[start...start+8])

            # 1なら到達 0なら時間超過
            if res == 1
              puts "- Reach time to live #{i} from #{sockaddr[1]}"
              reach = 1
              break
            elsif res == 0
              puts " - time to live #{i} from #{sockaddr[1]}"
              break
            end
          end
        end
      end
      # 到達したらループ終了
      if reach == 1
        break
      end
    end
  end
end


このクラスは以下のように使います。

target_ip = {ターゲットのipアドレス}
scanner = RouteScanner.new()
scanner.scan(target_ip)


パケットを受信したルーターによっては時間超過を知らせるパケットを送ってくれないこともあるようです。

感想


以前Rustとc言語でも作りましたが、やはりスクリプト言語なのか結構軽めにかけた気がします。
この調子でがむしゃらにいろいろ作っていきたいと思います。

ポートスキャンの時もそうでしたがPythonでも作ることになりそうな予感がしてます。