PythonでRawSocketとTCPを使ったネットワークプログラム


今回はPythonでポートスキャンツールを作成したいと思います。

以前はRubyで作りました。


PythonとRubyは似ているため同じような手順で作成することができます。
細かい説明は以前の記事でまとめたので、今回はソースを書きつつさくっと進めていきたいと思います。


ソケット作成


Pythonでrawsocketを使用する時は以下のように記述します。

    # tcpヘッダーを自分で作る
    def make_socket():
        soc = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_TCP)
        return soc


これでtcpパケットを自分で修正することができます。
受信はipパケットが対象になります。

ちなみにRubyだとこんな感じです。

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

TCPヘッダー作成


こちらもRubyで作った時と同じように作成できます。
細かい部分で違いはありますが、やっていることはRuby版と同じです。
特にpackの部分などがそうですね。
tcpヘッダー構成に合わせて第一引数を指定します。

    def make_tcp(src='127.0.0.1', dst='127.0.0.1', sport=9999, dport=31337, seq=1, ack=1, doff=5, flags=1<<1, win=300, check=0, urg=0):
        tcp_header = struct.pack("!HHLLBBHHH", sport, dport, seq, ack, doff<<4, flags, win, check, urg)

        # 擬似ipヘッダー
        pip_saddr = socket.inet_aton(src)
        pip_daddr = socket.inet_aton(dst)
        pip_reserved = 0
        pip_protocol = socket.IPPROTO_TCP
        pip_len = 20
        pip_header = struct.pack("!4s4sBBH",pip_saddr,pip_daddr,pip_reserved,pip_protocol,pip_len)

        # チェックサム取得
        check = checksum(pip_header+tcp_header)
        tcp_header = struct.pack("!HHLLBBHHH",sport,dport,seq,ack,doff<<4,flags,win,check,urg)
        return tcp_header

チェックサム


これに関しては単にPythonで書きなおしただけですね。
Rubyとの違いはendがあるかないかですかね。

    #チェックサム計算
    def checksum(data):
        data_len = len(data) // 2*2
        sum = 0
        for i in range(0,data_len,2):
            sum += (ord(data[i+1]) << 8) + ord(data[i])
        if len(data) % 2 != 0:
            sum += ord(data[-1])
        while sum >> 16:
            sum = (sum >> 16) + (sum & 0xffff)
        sum = sum >> 8 | (sum << 8 & 0xff00)
        return ~sum&0xffff

スキャン


スキャン処理についても流れは同じです。
Python版でもI/Oのreadyの確認にselectを使っています。

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

        # port 1~65535
        for i in range(1,65536):
           # srcは自分のipアドレス
            packet = make_tcp(src='192.168.11.6', dst=target_ip, dport=i)
            $ 10回ずつ
            for _ in range(10):
                soc.sendto(packet,(target_ip,0))
                sel_res = select.select([soc],[],[],10)

                if len(sel_res[0]) > 0:
                    packet = sel_res[0][0].recv(1024)

                    ip = extract_ip(packet[0:20])
                    offset = (ip[0]&0x0f)*4
                    tcp = extract_tcp(packet[offset:offset+20])

                    # 送信元ip,ポート,フラグ確認
                    if ip[8] == socket.inet_aton(target_ip):
                        if tcp[0] == i:
                            if ( (1<<1) + (1<<4) ) == tcp[5]:
                                print("- port {} open".format(i))
                            break


受信したパケットらヘッダーを取得する処理も似ていて、 unpack関数を使って文字列を配列に変換しています。

    # ipヘッダーを取得
    def extract_ip(packet):
        ip = struct.unpack('!BBHHHBBH4s4s', packet)
        return ip

    # tcpヘッダーを取得
    def extract_tcp(packet):
        tcp = struct.unpack('!HHLLBBHHH', packet)
        return tcp

全体


Rubyで作成した時と同様に今回もPortScannerクラスを作成します。

# -*- coding: utf-8 -*-
import socket
import struct
import sys
import select

class PortScanner:
    def __init__(self):
        self.soc = self.make_socket()

    def __del__(self):
        self.soc.close()

    # tcpヘッダーを自分で作る
    def make_socket(self):
        soc = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket.IPPROTO_TCP)
        return soc

    # ヘッダー作成
    def make_tcp(self, src='127.0.0.1', dst='127.0.0.1', sport=9999, dport=31337, seq=1, ack=1, doff=5, flags=1<<1, win=300, check=0, urg=0):
        tcp_header = struct.pack("!HHLLBBHHH", sport, dport, seq, ack, doff<<4, flags, win, check, urg)

        # 擬似ipヘッダー
        pip_saddr = socket.inet_aton(src)
        pip_daddr = socket.inet_aton(dst)
        pip_reserved = 0
        pip_protocol = socket.IPPROTO_TCP
        pip_len = 20
        pip_header = struct.pack("!4s4sBBH",pip_saddr,pip_daddr,pip_reserved,pip_protocol,pip_len)

        # チェックサム 擬似ip+tcpヘッダー
        check = self.checksum(pip_header+tcp_header)
        tcp_header = struct.pack("!HHLLBBHHH",sport,dport,seq,ack,doff<<4,flags,win,check,urg)
        return tcp_header

    def checksum(self, data):
        data_len = len(data) // 2*2
        sum = 0
        for i in range(0,data_len,2):
            sum += (ord(data[i+1]) << 8) + ord(data[i])
            #Python3
            #sum += (ord(chr(data[i+1])) << 8) + ord(chr(data[i]))
        if len(data) % 2 != 0:
            sum += ord(data[-1])
            #Python3
            #sum += ord(chr(data[-1]))
        while sum >> 16:
            sum = (sum >> 16) + (sum & 0xffff)
        sum = sum >> 8 | (sum << 8 & 0xff00)
        return ~sum&0xffff

    # ipヘッダー取得
    def extract_ip(self, packet):
        #packで配列に変換 第一引数はipヘッダーに合うように指定
        ip = struct.unpack('!BBHHHBBH4s4s', packet)
        return ip

    # tcpヘッダー取得
    def extract_tcp(self, packet):
        #packで配列に変換 第一引数はtcpヘッダーに合うように指定
        tcp = struct.unpack('!HHLLBBHHH', packet)
        return tcp

    def scan(self, target_ip):
        print("target host: {}".format(target_ip))
        # port 1~65535
        for i in range(1,65535):
            # srcは自分のipをいい具合に指定
            packet = self.make_tcp(src='192.168.11.6', dst=target_ip, dport=i)
            # 10回ずつ
            for _ in range(10):
                self.soc.sendto(packet,(target_ip,0))
                sel_res = select.select([self.soc],[],[],10)

                if len(sel_res[0]) > 0:
                    packet = sel_res[0][0].recv(1024)

                    ip = self.extract_ip(packet[0:20])
                    offset = (ip[0]&0x0f)*4
                    tcp = self.extract_tcp(packet[offset:offset+20])

                    # 送信元ip,ポート,フラグを確認
                    if ip[8] == socket.inet_aton(target_ip):
                        if tcp[0] == i:
                            if ( (1<<1) + (1<<4) ) == tcp[5]:
                                print("- port {} open".format(i))
                            else:
                                break


以下のように使用します。

target_ip = sys.argv[1]
scanner = PortScanner()
scanner.scan(target_ip)


実行するときのコマンドと実行結果です。

$ sudo python port_scan.py 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

感想


もうPythonとRubyはひとつになってしまってもいい気がします。
お互いのライブラリーなどの資産を使えるようにしたらみんな幸せになれると思います。