scapy でソケット通信
最近の高級な言語を使ったネットワークプログラミングではソケットすら意識しなくてもプログラムができてしまいますが、実際にどんな仕組みで通信されているかを知ることは重要です。
そこで scapy という任意のパケットを生成するソフトを使ってパケットを一つ一つ作成し、echo サーバとTCP で通信してみたいと思います。
scapy のインストール
scapy は Ubuntu ではパッケージになっているので apt で入りますが、CentOS6 ではパッケージになっていなかったので自分で取ってきて入れました。
# wget http://www.secdev.org/projects/scapy/files/scapy-latest.tar.gz # tar xvf scapy-latest.tar.gz # cd scapy-2.1.0 # ./setup.py install
scapy の簡単な使い方
scapy は python のモジュールですが、インストールすると専用コマンドも一緒にインストールされます。
# scapy Welcome to Scapy (2.1.0) >>>
試しに www.google.com 宛の ICMP Echo リクエスト (ping) のパケットを作ってみます。
>>> ping=IP(dst='www.google.com')/ICMP() >>> ping.show() ###[ IP ]### version= 4 ihl= None tos= 0x0 len= None id= 1 flags= frag= 0 ttl= 64 proto= icmp chksum= None src= 192.168.11.76 dst= Net('www.google.com') \options\ ###[ ICMP ]### type= echo-request code= 0 chksum= None id= 0x0 seq= 0x0
IP() や ICMP() でそれぞれのレイヤを作成でき、それを "/" で繋ぐとパケットになります。
パケットの中身は show() で見ることができます。
パラメータで指定しなかった chksum や ttl などの部分はうまく作ってくれるので非常に楽です。
それではこの ping パケットを実際に送信してみます。
>>> reply = sr1(ping) Begin emission: .......................Finished to send 1 packets. ...............................* Received 55 packets, got 1 answers, remaining 0 packets <IP version=4L ihl=5L tos=0x0 len=28 id=51106 flags= frag=0L ttl=44 proto=icmp chksum=0x2f8 src=64.233.183.105 dst=192.168.11.7> >>> reply.show() ###[ IP ]### version= 4L ihl= 5L tos= 0x0 len= 28 id= 51106 flags= frag= 0L ttl= 44 proto= icmp chksum= 0x2f8 src= 64.233.183.105 dst= 192.168.11.76 \options\ ###[ ICMP ]### type= echo-reply code= 0 chksum= 0x0 id= 0x0 seq= 0x0
sr1() はパケットを送信し、戻ってきたパケットを返すメソッドです。
返ってきたパケットを見ると、確かに echo-reply が戻ってきていることがわかります。
echo サーバと TCP 通信
今回は echo サーバとソケットをオープンしてデータを送信し、ソケットをクローズするところまでやってみます。
TCP の状態遷移図は以下のページに載っています。
@IT:連載 基礎から学ぶWindowsネットワーク 第16回 信頼性のある通信を実現するTCPプロトコル(3) 2.TCPの状態遷移図
サーバ側の準備
サーバ側は netcat を使ってポート 30000 を開いて待っておきます。
# nc -l 30000
このサーバに対して、scapy を使ってパケットを送りつけることでソケット通信を行います。
今回の環境ではサーバの IP は 192.168.11.1 です。
クライアント側の準備
実際にパケットを送る前にまずは以下のコマンドで、RST パケットの送信を抑止しておきます。
クライアント側は実際にはソケットがないため、サーバからのリプライに対して OS が RST を送ってしまうためです。
# iptables -A OUTPUT -p tcp --tcp-flags RST RST -d <サーバのIP> -j DROP
今回の環境ではクライアント側の IP は 192.168.11.76 です。
ソケットのオープン
まずは 3way ハンドシェイクでセッションを確立します。
SYN を送って返ってきた SYN+ACK に対して ACK を送ります。
>>> ip=IP(dst='192.168.11.1') >>> tcp=TCP(sport=50000,dport=30000,seq=100)
dst にはサーバの IP を指定し、dport には nc で待ち受けているポートを指定します。
sport, seq は適当です。
>>> tcp.flags = 'S' >>> SYN=ip/tcp >>> SYNACK=sr1(SYN) Begin emission: ..............................Finished to send 1 packets. ...* Received 34 packets, got 1 answers, remaining 0 packets
SYN フラグを立てたパケットを送信して SYN+ACK を受け取ります。
この時点でのサーバの状態は TCP 状態遷移図でいうところの SYN_RECV です。
(サーバで netstat をすると状態が確認できます。)
>>> tcp.seq += 1 >>> tcp.ack = SYNACK.seq + 1 >>> tcp.flags = 'A' >>> send(ip/tcp) . Sent 1 packets.
返ってきた SYN+ACK に対して ACK を返します。
これでサーバの状態は ESTABLISHED になり、セッションが確立できました。
データ送信
セッションができたので実際のデータを送信します。
>>> payload="Hello world!" >>> DATA=ip/tcp/payload >>> ACK=sr1(DATA) Begin emission: ...............................Finished to send 1 packets. ..* Received 34 packets, got 1 answers, remaining 0 packets >>> tcp.seq += len(payload)
サーバの nc を実行したプロンプトを見てみると、"Hello world!" が表示されているはずです。
送信したデータのサイズ分シーケンス番号を進めておきます。
ソケットのクローズ
ソケットをクローズするために FIN フラグを立てたパケットを送ります。
>>> tcp.flags = 'FA' >>> FINACK=sr1(ip/tcp) Begin emission: ............................Finished to send 1 packets. ..* Received 31 packets, got 1 answers, remaining 0 packets >>> FINACK.show() ###[ IP ]### version= 4L ihl= 5L tos= 0x0 len= 40 id= 65164 flags= DF frag= 0L ttl= 64 proto= tcp chksum= 0xa4a5 src= 192.168.11.1 dst= 192.168.11.76 \options\ ###[ TCP ]### sport= 30000 dport= 50000 seq= 788872184 ack= 114 dataofs= 5L reserved= 0L flags= FA window= 5840 chksum= 0x5d75 urgptr= 0 options= {}
サーバは何も送信するデータがないため、即時 FIN を返してきます。
サーバの状態は LAST_ACK です。
>>> tcp.seq += 1 >>> tcp.ack = FINACK.seq + 1 >>> tcp.flags = 'A' >>> send(ip/tcp)
最後に ACK を返してやることでソケットは正常にクローズされます。
これでひと通りの TCP 通信ができました。
scapy を使うと色々なことができそうなのでもっと使ってみようと思います。