【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回までスキャンを試みます。
以下の終了条件を満たすまでパケットを送り続けます。
- 10回パケットの送受信をする
- syn,ackブラグがたったパケットを受信する
- フラグ上記の②以外の状態のパケットを受信する
項目として十分ではないかもしれませんが、ポートスキャナーとして最低限の動きはしてくれると思います。
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