ScapyによるTCP通信


はじめに

現在に至るまでの世界中の人々の努力により、世の中には非常に便利で協力なツールが溢れている。その恩恵を享受することで、我々は自分のプロジェクトに専念することができる。例えば何らかのWebサービスを作ろうとしたとき、サーバーフレームワークから作ろうとはしないはずである。世界には、Apacheなり、IISなり、あるいはRailsやDjangoといったツールが揃っている。
同じ理由で、通信に関する部分もフレームワークなりOSなりに任せていることが通常である。特にクラウド等の利用によるIaaSやPaaSが当たり前になっている状態において、クエリに対しての処理を記述することはあっても、「このパケットのペイロードにデータを格納して〜」とか「MACアドレスとIPの変換は〜」とか考えることはなかなかない。

しかし、何事にも例外は存在し、先人たちのツールに介入したい場合などが考えられる。例えば、自社内の独立したネットワーク上のシステムをイメージする。ネットワークに属した計算機が相互に通信を行い、一つの巨大システムを構築する。そのうちの一つの計算機が、トラブったとしよう。原因究明のためにログを漁ることはもちろんであるが、「そのシステムがトラブルを起こす前後の状況をすべて再現したい」とも考えるはずだ。そんなとき、これまでは先人たちのツールに任せていた通信を"手ずから構築・実行する"必要にかられる。

今回はそんなイレギュラーなケースを対象に、通信を行う方法を記す。なお、今回目標とするのはTCP通信の3hand shakeである。

手段の検討

ネットワーク上にアクセスするためには、まずはツールの選択が重要である。

pcap

pcap(packet capture)は、ネットワーク上でスニファリング(要は盗聴)するためのAPIである。UNIX系のシステムではlibpcapとして実装されており、それをWindowsに移植したのがWinPcapである。後述のWiresharkにも組み込まれているなど、ネットワークに対してなにか行おうと考えたときのベースとなる。

Wireshark

ネットワーク解析において非常に強力かつ有名なツールがWiresharkである。管理者権限として実行することで、NICからパケットを直接取得でき

  • パケットの可視化を即時実行
  • パケットのデコードも可能
  • 特定のフィルタリングも可能
  • 取得したパケットを定期的にファイル出力可能
  • 出力したファイル(.pcap)を読み込んで解析することも可能

と非常に便利なツールである。とりあえず、今回は通信内容を監視するために使用する。

Colasoft Packet Builder

Colasoft Packet Builderは、パケットの生成・編集・送信することが可能なツールである。Wiresharkなどで取得したpcapファイルを読み込むことも可能で、手軽にパケットを送信するだけであれば十分な機能を有している。しかし、Windows環境でしか動かないことや、GUIを基本とした使い方であることから、大量のデータを編集して送信するなどには向かない。

tcpreplay

tcpreplayはパケットのキャプチャ・編集・送信を可能なツール群である。tcpreplayを中心に、キャプチャ用のコマンドや編集用のコマンドが揃っており、ある時点での通信を再現する操作が一通り可能である。しかし、

  • パケットキャプチャに関してはWiresharkが非常に強力であること
  • 今回はTCP通信の3hand shakeから実行したいため、パケットの生成と送信をより直感的に行いたい
  • 将来的にパケット送信タイミングの制御/再現を目指しており、tcpreplay以外のツールが必要になること

から、今回はtcpreplayは使用しない。
なお、有志の方による非公式日本語サイトが存在する。

Scapy

Scapyは、Pythonによって記述されたpacket manipulation toolである。パケットのキャプチャ・生成・編集・送信が可能なツールであり、Pythonコードの中から実行することも可能なため、他の処理と合わせてPythonコード内で完結することができる。また、Pythonであることからある程度OSや環境を自由に構築することができる。
難点としては、Pythonであるがために、例えばpacket送信タイミングの時間精度が気になるところであるが、今回はこだわらないものとする。

OSによる介入

Scapyの使い方について述べる前に、先にぶつかるであろう問題について述べておく。今回実現したいことはTCP通信を実現することであるが、TCPはOSが適切に管理している。そのため、「プログラムがTCP通信を実行」しようとしても、それは「OSから見たら異常な操作」となり、介入を受けてしまう。
その内容について解説する。

基本的なTCP通信

TCP通信の基本的な接続を下記に示す。

OSによって介入される例

3hand shakeにて、TCP通信を結ぶときを例に説明する。説明の都合上、Client側をOSとプログラムに分けて記述する。ClientからServerにSYNを送ると、ServerはSYN-ACKを返してくる。しかし、ClientのOSは「TCP通信をかけていない」と認識しているため、SYN-ACKを受け取れず、RSTを返してTCP通信をリセットする。それに遅れてClientのプログラムがACKを返すが、すでにTCP通信はリセット済みである。

初めてScapyに触れたときは、勝手にRSTが送られる現象をバグと認識してしまうことがあるが、極めて当たり前の動作をしているだけである。

Scapyに限らずプログラムからTCP通信を実現するには、OSがRSTを送るのをブロックするしかない。同じことに悩んでいた人は、arp spoofingを使った方法日本語版)(ARPプロトコルの応答を偽装して、ネットワーク上でなりすましを行う方法)で回避したようだが、結局arpspoofでなりすましされる端末が必要になる。今回は余分な端末もないので、直接RSTを阻害する方法をとる。

MacでのRST阻害方法

pf(packet filter)を利用する。Mac OSXに搭載されているpfはOpenBSD由来のファイアウォール機能で、ネットワークの操作を行える。今回はこちらのStackExchangeの投稿こちらのQiitaの投稿を参考にした。なお、192.168.179.9は今回の通信相手(Sever)である。

$ sudo cp /etc/pf.conf /etc/pf.conf.disable_rst
$ sudo echo 'block drop proto tcp from any to 192.168.179.9 flags R/R' >> /etc/pf.conf.disable_rst
$ sudo pfctl -f /etc/pf.conf.disable_rst
pfctl: Use of -f option, could result in flushing of rules
present in the main ruleset added by the system at startup.
See /etc/pf.conf for further details.

No ALTQ support in kernel
ALTQ related functions disabled
$ sudo pfctl -e
No ALTQ support in kernel
ALTQ related functions disabled
pfctl: pf already enabled

LinuxでのRST阻害方法

今回のClient環境がMacであったため、その検証しかしていない。Linuxの場合は、iptablesによって対応するらしい。参考までにstackoverflowの投稿と、そこに記述されたコマンドを転載しておく。

iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.1.20 -j DROP

Scapyの使い方

インストール

pipでインストールあるいはGitHubから最新版をインストール可能である。

$ pip install scapy

使い方

スーパーユーザーにて実行する。

$ sudo scapy

で、IPythonが起動し、Scapyの関数が一通り実行できる。
あるいは

$ sudo python
>>> from scapy.all import *

でもOKである。

Scapyにおけるパケットの生成は、実際のパケット構成と似た直感的な書式で記述可能である。具体例と合わせて、示していく。

例:ICMPのパケット送信

# 192.168.1.1へのICMPパケットの生成
# なお、こちらのIPは192.168.179.127である。
# IPのかわりに、www.google.comのようなURLでもOK
>>> pkt = IP(dst = '192.168.179.1') / ICMP()
# パケットの中身を確認
>>> pkt.show()
###[ IP ]###
  version= 4
  ihl= None
  tos= 0x0
  len= None
  id= 1
  flags=
  frag= 0
  ttl= 64
  proto= icmp
  chksum= None
  src= 192.168.179.127
  dst= 192.168.179.1
  \options\
###[ ICMP ]###
     type= echo-request
     code= 0
     chksum= None
     id= 0x0
     seq= 0x0

# パケットのsend & receive 1 packet
>>> sr1(pkt)
Begin emission:
..Finished sending 1 packets.
.*
Received 4 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x0 len=28 id=51974 flags= frag=0 ttl=64 proto=icmp chksum=0xc808 src=192.168.179.1 dst=192.168.179.127 options=[] |<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 |>>
>>>

例:SYNスキャン

TCPにSYNを詰めて相手先ポートに送信するSYNスキャンも、1コマンドとなる。

>>> sr1(IP(dst = '192.168.179.1') / TCP(dport = 80, flags = 'S'))
Begin emission:
..Finished sending 1 packets.
.*
Received 4 packets, got 1 answers, remaining 0 packets
<IP  version=4 ihl=5 tos=0x0 len=44 id=0 flags=DF frag=0 ttl=64 proto=tcp chksum=0x52fa src=192.168.179.1 dst=192.168.179.127 options=[] |<TCP  sport=http dport=ftp_data seq=3668123710 ack=1 dataofs=6 reserved=0 flags=SA window=29200 chksum=0x42ed urgptr=0 options=[('MSS', 1460)] |>>

TCP通信をScapyにて実現

繰り返しになるが、今回のゴールは3hand shakeを行ってTCP通信を確立することである。将来的にはインターフェースやポートを変えて複数の通信を管理できるようにしたいので、TCP通信自体をクラスとして実装した。

クラスとしての実装

具体的なコードを以下に示す。今回は簡単のため、TCP接続を行ったら、あとは接続断するだけであり、一切のデータの通信を行わない。

from scapy.all import *


class TCP_CONNECT:

    def __init__(self,
                 src = '127.0.0.1',
                 dst = '127.0.0.1',
                 sport = 60000,
                 dport = 60000):
        self.src = src
        self.dst = dst
        self.sport = sport
        self.dport = dport
        self.ip = IP(dst = self.dst)
        self.tcp = TCP(sport = self.sport, dport = self.dport, flags = 'S', seq = 100)


    def synchronize(self):
        ''' request for TCP connection
        '''
        # send sync & get ack
        syn = self.ip / self.tcp
        self.syn_ack = sr1(syn)
        # send sync
        self.tcp.seq += 1
        self.tcp.ack = self.syn_ack.seq + 1
        self.tcp.flags = 'A'
        ack = self.ip / self.tcp
        send(ack)
        return syn, self.syn_ack, ack


    def fin(self):
        ''' finish for TCP connection
        '''
        # send FIN packet
        self.tcp.seq
        fin = self.ip / TCP(sport = self.sport,
                            dport = self.dport,
                            flags = 'FA',
                            seq = self.syn_ack.ack,
                            ack = self.syn_ack.seq + 1)
        self.fin_ack = sr1(fin)

        # return final ACK
        lastack = self.ip / TCP(sport = self.sport,
                                dport = self.dport,
                                flags = 'A',
                                seq = self.fin_ack.ack,
                                ack = self.fin_ack.seq + 1)
        send(lastack)


    def __del__(self):
        self.fin()

クラスを使ったTCP接続

上記クラスを使用して通信を実現してみた。今回、自分のIPを192.168.179.127でポートを54321、通信相手にはWeb Serverを用意し、IPを192.168.179.9でポートを80にした。
なお、上記のクラスは、pypacian.mainをimportすることで読み込んでいる。

$ sudo python
Password:
Python 3.6.1 (default, Aug 27 2017, 16:38:38)
[GCC 4.2.1 Compatible Clang 3.9.1 (tags/RELEASE_391/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from pypacian.main import *  # クラスの読み込み
>>> tcp_connect = TCP_CONNECT(src = '192.168.179.127', dst = '192.168.179.9', sport = 54321, dport = 80)  # TCP通信クラスの定義
>>> tcp_connect.synchronize()  # 3hand shakeを実行
Begin emission:
...........................Finished sending 1 packets.
........................................................................................................................................................*
Received 180 packets, got 1 answers, remaining 0 packets
.
Sent 1 packets.
(<IP  frag=0 proto=tcp dst=192.168.179.9 |<TCP  sport=54321 dport=http seq=100 flags=S |>>, <IP  version=4 ihl=5 tos=0x0 len=44 id=0 flags=DF frag=0 ttl=64 proto=tcp chksum=0x52f2 src=192.168.179.9 dst=192.168.179.127 options=[] |<TCP  sport=http dport=54321 seq=1413997991 ack=101 dataofs=6 reserved=0 flags=SA window=29200 chksum=0x2f56 urgptr=0 options=[('MSS', 1460)] |<Padding  load='\x00\x00' |>>>, <IP  frag=0 proto=tcp dst=192.168.179.9 |<TCP  sport=54321 dport=http seq=101 ack=1413997992 flags=A |>>)
>>> tcp_connect.fin()
Begin emission:
..................................................................Finished sending 1 packets.
.................................................*
Received 116 packets, got 1 answers, remaining 0 packets
.
Sent 1 packets.
>>>

このあと、クラスのデコンストラクタの動きがおかしかった気がするが、最低限のTCP通信を確立することができた。

最後に、このときのWiresharkの様子を記しておく。

ちゃんと3hand shakeが実現できていることがわかる。今後は、今回確立したTCP通信の中で情報のやり取りを行う。