はじめに
現在に至るまでの世界中の人々の努力により、世の中には非常に便利で協力なツールが溢れている。その恩恵を享受することで、我々は自分のプロジェクトに専念することができる。例えば何らかの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通信の中で情報のやり取りを行う。