nigakyのブログ

雑多なメモです。

scapy でソケット通信

最近の高級な言語を使ったネットワークプログラミングではソケットすら意識しなくてもプログラムができてしまいますが、実際にどんな仕組みで通信されているかを知ることは重要です。

そこで scapy という任意のパケットを生成するソフトを使ってパケットを一つ一つ作成し、echo サーバとTCP で通信してみたいと思います。

Scapy

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 を使うと色々なことができそうなのでもっと使ってみようと思います。