[Python] scapyでパケットを生成、送信してみる

ネットワーク

はじめに

ネットワークの仕組みを深く理解したい、あるいは特定のネットワーク状況を再現してテストしたいと思ったことはありませんか? 🤔 そんなとき、Pythonの強力なライブラリである Scapy が役立ちます。

Scapyは、ネットワークパケットを自由に作成(forge)、操作、送信、受信できるPythonライブラリです。Wiresharkのようなパケットキャプチャツールで通信を眺めるだけでなく、自分で意図した通りのパケットを作り出し、ネットワークに送り出すことができます。これにより、プロトコルの動作確認、ネットワーク機器のテスト、セキュリティの検証など、様々な用途に活用できます。

この記事では、Scapyの基本的な使い方から、様々なプロトコル(IP, TCP, UDP, ICMPなど)のパケットを生成し、実際にネットワークに送信する手順を、具体的なコード例を交えながら詳しく解説します。ネットワークの学習や実験に役立てていただければ幸いです。🚀

注意: Scapyを使ったパケット送信は、ネットワークや接続先のシステムに予期せぬ影響を与える可能性があります。特に、実験を行う際は、自身の管理下にあるネットワーク環境やテスト用の仮想環境で行うようにしてください。公共のネットワークや許可なく第三者のシステムに対してパケットを送信することは、法律や規約に違反する可能性があるため、絶対に行わないでください。

Scapyのインストール

Scapyを利用するには、まずPython環境にScapyをインストールする必要があります。Pythonのパッケージ管理ツールであるpipを使うのが最も簡単です。

ターミナル(コマンドプロンプト)を開き、以下のコマンドを実行してください。

pip install scapy

多くの場合、Scapyはネットワークインターフェースに直接アクセスしてパケットを送受信するため、管理者権限(Linux/macOSではsudo、Windowsでは管理者としてコマンドプロンプトを実行)が必要になることがあります。

また、プラットフォームによっては、Scapyが依存する他のライブラリ(libpcap/Npcapなど)のインストールが必要になる場合があります。公式ドキュメントや各OSのドキュメントを参照して、必要な依存関係を事前にインストールしておきましょう。

ScapyはPython 3.7以降で動作します(Scapy 2.5.0がPython 2.7をサポートする最後のバージョンでした)。お使いのPythonのバージョンを確認してください。

python --version

インストールが完了したら、ScapyをPythonスクリプトや対話モードで利用できるようになります。✅

Scapyの基本的な使い方

Scapyは、対話モードで使うと非常に便利です。ターミナルでsudo scapy(Windowsの場合は管理者権限でscapy)と入力すると、Scapyの対話シェルが起動します。

sudo scapy

INFO: No IPv6 support in kernel. Removing optional IPv6 routing.
INFO: Can't import package "pyx". Won't be able to use psdump() or pdfdump().
Welcome to Scapy (2.x.x)
>>>
      

この>>>プロンプトで、PythonコードのようにScapyのコマンドを入力して実行できます。

レイヤー構造

Scapyの最大の特徴は、ネットワークパケットをレイヤー(層)の組み合わせとして扱える点です。OSI参照モデルやTCP/IPモデルで説明されるように、ネットワーク通信は複数のプロトコル階層(イーサネット、IP、TCP、UDPなど)で成り立っています。

Scapyでは、各プロトコルに対応するクラス(例: Ether, IP, TCP, UDP, ICMP, DNSなど)が用意されています。パケットを作成するには、これらのクラスのインスタンスを/演算子で結合します。この演算子は、下位レイヤーから上位レイヤーへと積み重ねていくイメージです。


# イーサネットフレームの上にIPパケット、さらにその上にTCPセグメントを載せる
packet = Ether() / IP() / TCP()
      

このように記述するだけで、基本的なイーサネットフレーム、IPパケット、TCPセグメントが組み合わされたパケットオブジェクトが生成されます。💡

フィールドの確認と設定

各レイヤー(プロトコルクラスのインスタンス)には、そのプロトコルで定義されているフィールド(送信元/宛先アドレス、ポート番号、フラグなど)が含まれています。

ls()関数を使うと、特定のプロトコルクラスが持つフィールドの一覧とその型、デフォルト値を確認できます。


>>> ls(IP)
version    : BitField             = (4)
ihl        : BitField             = (None)
tos        : XByteField           = (0)
len        : ShortField           = (None)
id         : ShortField           = (1)
flags      : FlagsField           = (0)
frag       : BitField             = (0)
ttl        : ByteField            = (64)
proto      : ByteEnumField        = (0)
chksum     : XShortField          = (None)
src        : SourceIPField        = (None)
dst        : DestIPField          = (None)
options    : PacketListField      = ([])
      

パケットを生成する際に、これらのフィールド値を指定できます。インスタンス化する際に引数として渡します。


# 宛先IPアドレスとTTLを指定してIPパケットを作成
ip_layer = IP(dst="8.8.8.8", ttl=128)
      

生成済みのパケットオブジェクトのフィールド値にアクセスしたり、変更したりすることも可能です。


>>> ip_layer.dst
'8.8.8.8'
>>> ip_layer.ttl = 64
>>> ip_layer.ttl
64
      

パケットの表示

作成したパケットの内容を確認するには、いくつかの便利な関数があります。

  • show(): パケットの詳細なフィールド情報を分かりやすく表示します。デバッグに非常に役立ちます。
  • summary(): パケットの概要を1行で表示します。
  • show2(): show()と似ていますが、計算されたフィールド(チェックサムなど)も含めて表示します。送信前の確認に有用です。

>>> packet = IP(dst="www.google.com")/TCP(dport=80, flags="S")
>>> packet.summary()
'IP / TCP 192.168.1.10:ftp_data > www.google.com:http S'
>>> packet.show()
###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     =
  frag      = 0
  ttl       = 64
  proto     = tcp
  chksum    = None
  src       = 192.168.1.10 # Scapyが自動で設定してくれる場合がある
  dst       = Net('www.google.com') # ScapyがDNS解決してくれる場合がある
  options   = []
###[ TCP ]###
     sport     = 20
     dport     = 80
     seq       = 0
     ack       = 0
     dataofs   = None
     reserved  = 0
     flags     = S
     window    = 8192
     chksum    = None
     urgptr    = 0
     options   = {}
      

Scapyは、dstにホスト名を指定すると自動的にDNS解決を行ったり、src(送信元IP)やチェックサムなどを自動で計算・設定してくれたりする賢い機能も持っています。✨

様々なパケットの生成例

Scapyを使って、一般的なプロトコルのパケットを生成する例を見ていきましょう。

IPパケット

最も基本的なレイヤー3パケットです。宛先IPアドレスを指定するだけで作成できます。送信元IPアドレスは多くの場合、Scapyが適切なインターフェースから自動で設定してくれますが、明示的に指定することも可能です。


# 宛先IPアドレスを指定
ip_pkt = IP(dst="192.168.1.1")

# 送信元IPアドレスも指定
ip_pkt_with_src = IP(src="192.168.1.100", dst="192.168.1.1")

# TTLを指定
ip_pkt_ttl = IP(dst="8.8.8.8", ttl=30)

ip_pkt.show()
      

TCPパケット

TCPセグメントを作成します。通常はIPレイヤーの上に構築します。宛先ポート(dport)や送信元ポート(sport)、TCPフラグ(flags)などを指定します。

TCPフラグは文字列で指定します:

  • S: SYN (Synchronize)
  • A: ACK (Acknowledge)
  • F: FIN (Finish)
  • R: RST (Reset)
  • P: PSH (Push)
  • U: URG (Urgent)

# WebサーバーへのSYNパケット (HTTP)
tcp_syn = IP(dst="www.example.com") / TCP(dport=80, flags="S")

# SSHサーバーへのSYNパケット
tcp_syn_ssh = IP(dst="192.168.1.5") / TCP(dport=22, flags="S")

# SYN+ACK パケット (応答のシミュレーションなど)
tcp_synack = IP(dst="192.168.1.100") / TCP(dport=12345, sport=80, flags="SA", seq=1000, ack=501)

# RST パケット (接続拒否など)
tcp_rst = IP(dst="192.168.1.100") / TCP(dport=12345, flags="R")

tcp_syn.show()
      

シーケンス番号(seq)やACK番号(ack)も指定できます。

UDPパケット

UDPデータグラムを作成します。TCPと同様に、通常はIPレイヤーの上に構築し、宛先ポート(dport)や送信元ポート(sport)を指定します。


# DNSクエリ (Google Public DNSへ)
udp_dns_query = IP(dst="8.8.8.8") / UDP(dport=53) / DNS(rd=1, qd=DNSQR(qname="www.google.com"))

# NTPリクエスト
udp_ntp = IP(dst="pool.ntp.org") / UDP(dport=123)

# 任意のポートへのUDPパケット
udp_custom = IP(dst="192.168.1.20") / UDP(dport=161) # SNMPなど

udp_dns_query.show()
      

UDPにはTCPのようなフラグはありませんが、ペイロードとしてさらに上位のプロトコル(例: DNS)を載せることができます。

ICMPパケット

ICMPメッセージを作成します。これもIPレイヤーの上に構築します。typeフィールドとcodeフィールドでメッセージの種類を指定します。

一般的なICMPタイプ:

  • 8: Echo Request (Pingリクエスト)
  • 0: Echo Reply (Ping応答)
  • 3: Destination Unreachable
  • 11: Time Exceeded

# Echo Request (Ping)
icmp_ping = IP(dst="8.8.8.8") / ICMP(type=8, code=0) # type=8, code=0 はデフォルトなので ICMP() だけでも良い

# Echo Reply (Ping応答のシミュレーション)
icmp_reply = IP(dst="192.168.1.100") / ICMP(type=0, code=0, id=12345, seq=1)

# Destination Unreachable (Port Unreachable)
icmp_unreach = IP(dst="192.168.1.100") / ICMP(type=3, code=3)

icmp_ping.show()
      

Echo Request/Replyでは、idseq(シーケンス番号)フィールドが対応付けに使われます。

ペイロード(データ部)の追加

TCPやUDPパケットに、特定のプロトコルヘッダではなく、任意のデータ(ペイロード)を追加したい場合、文字列を/で結合します。ScapyはこれをRawレイヤーとして扱います。


# HTTP GETリクエストのようなペイロードを持つTCPパケット
http_get_payload = "GET / HTTP/1.1\r\nHost: www.example.com\r\n\r\n"
tcp_with_payload = IP(dst="www.example.com") / TCP(dport=80, flags="PA") / http_get_payload

# UDPパケットに任意の文字列データを載せる
udp_with_payload = IP(dst="192.168.1.30") / UDP(dport=9999) / "Hello, Scapy!"

tcp_with_payload.show()
udp_with_payload[Raw].load # ペイロード部分の確認
      

パケットの送信

Scapyで作成したパケットをネットワークに送信するには、いくつかの関数が用意されています。主なものはsend()sendp()です。

レイヤー3送信: send()

send()関数は、レイヤー3(IPレベル)でパケットを送信します。つまり、渡されたIPパケット(またはそれ以上のレイヤーを持つパケット)に対して、Scapyが適切な送信元MACアドレス、宛先MACアドレス(ARP解決が必要な場合)、およびイーサネットヘッダを自動的に付与して送信します。ルーティングも考慮されます。

通常、IPパケットやその上位レイヤー(TCP, UDP, ICMPなど)を送信する場合はsend()を使用します。


# ICMP Echo Requestを送信 (Ping)
packet_to_send = IP(dst="8.8.8.8") / ICMP()
send(packet_to_send)

# TCP SYNパケットを送信
tcp_syn_pkt = IP(dst="192.168.1.1") / TCP(dport=80, flags="S")
send(tcp_syn_pkt)

# 複数の宛先に一度に送信
send(IP(dst=["8.8.8.8", "8.8.4.4"]) / ICMP())

# 10回送信する
send(IP(dst="192.168.1.1") / UDP(dport=12345), count=10)

# 1秒間隔でループ送信 (Ctrl+Cで停止)
# send(IP(dst="192.168.1.1")/TCP(dport=22, flags='S'), loop=1, inter=1)
      

send()関数は送信したパケット数を表示しますが、応答は受信しません。

send()関数にはcount引数で送信回数を、inter引数で送信間隔(秒)を指定できます。loop=1とするとCtrl+Cが押されるまで無限に送信し続けます。

レイヤー2送信: sendp()

sendp()関数は、レイヤー2(イーサネットレベル)でパケットを送信します。この関数を使用する場合、Ether()レイヤーを含め、完全なイーサネットフレームを自分で構築する必要があります。ScapyはARP解決やルーティングを行いません。

主に、ARPパケットや、特定のMACアドレス宛に直接フレームを送りたい場合、またはレイヤー2レベルでの特殊な操作(VLANタグ操作など)を行いたい場合に使用します。


# ARPリクエストをブロードキャスト送信 (指定したIPアドレスのMACアドレスを問い合わせる)
arp_request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.1")
sendp(arp_request)

# 特定のMACアドレス宛にIPパケットを送信 (ルーティング無視)
# 送信元MAC、宛先MACを自分で指定する必要がある
custom_frame = Ether(src="00:11:22:33:44:55", dst="AA:BB:CC:DD:EE:FF") / IP(dst="10.0.0.1") / ICMP()
# sendp(custom_frame) # 実行には注意

# 特定のネットワークインターフェースから送信
# sendp(Ether()/IP(dst="192.168.1.254")/ICMP(), iface="eth0")
      

sendp()関数では、iface引数で使用するネットワークインターフェースを明示的に指定することが重要になる場合があります。指定しない場合はScapyが選択したデフォルトインターフェースが使われます。

送受信: sr(), sr1(), srp(), srp1()

パケットを送信し、その応答を受信したい場合は、sr系の関数を使用します。

  • sr(): レイヤー3でパケットを送信し、受信した応答パケットのリストと、応答がなかったパケットのリストをタプルで返します。
  • sr1(): レイヤー3でパケットを送信し、最初に応答があったパケットのみを返します。応答がない場合はNoneを返します。タイムアウト設定(timeout引数)が重要です。
  • srp(): レイヤー2でパケット(フレーム)を送信し、受信した応答フレームのリストと、応答がなかったフレームのリストをタプルで返します。
  • srp1(): レイヤー2でパケット(フレーム)を送信し、最初に応答があったフレームのみを返します。

# Google DNSにPingを送信し、応答を待つ (sr1)
response = sr1(IP(dst="8.8.8.8") / ICMP(), timeout=1, verbose=0) # verbose=0で送信メッセージを抑制
if response:
    print("応答がありました:")
    response.show()
else:
    print("応答がありませんでした。")

# ローカルネットワーク内の複数のホストにARPリクエストを送信し、応答を収集 (srp)
ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.0/24"), timeout=2, verbose=0)
print(f"{len(ans)} 個の応答がありました。")
ans.summary() # 応答の概要を表示

# 特定ポートが開いているかTCP SYNスキャン (sr1)
# 応答がSYN+ACKならポートが開いている可能性、RST+ACKなら閉じている可能性
syn_ack_resp = sr1(IP(dst="scanme.nmap.org")/TCP(dport=80, flags='S'), timeout=1, verbose=0)
if syn_ack_resp and syn_ack_resp.haslayer(TCP):
    if syn_ack_resp.getlayer(TCP).flags == 0x12: # SYN+ACKフラグ
        print("ポート 80 は開いているようです。")
        # 接続を切断するためにRSTを送る
        send(IP(dst="scanme.nmap.org")/TCP(dport=80, sport=syn_ack_resp[TCP].dport, flags='R', seq=syn_ack_resp[TCP].ack), verbose=0)
    elif syn_ack_resp.getlayer(TCP).flags == 0x14: # RST+ACKフラグ
        print("ポート 80 は閉じているようです。")
else:
    print("ポート 80 から応答がないか、フィルタリングされているようです。")

      

これらのsr系の関数は、ネットワーク探索やポートスキャン、プロトコルの応答確認などに非常に強力です。ただし、実行には十分な注意が必要です。⚠️

注意点と免責事項

Scapyは非常に強力なツールですが、その力を正しく理解し、責任を持って使用することが極めて重要です。以下の点に十分注意してください。

  • 権限: Rawソケットを使用してパケットを送受信するため、多くの場合、Scapyの実行には管理者権限(rootまたはAdministrator)が必要です。
  • ネットワークへの影響: 不適切なパケットや大量のパケットを送信すると、ネットワークや接続先のシステムに負荷をかけたり、不安定にさせたりする可能性があります。特にループ送信やブロードキャスト送信には注意が必要です。
  • 法律と倫理: 許可なく第三者のネットワークやシステムに対してスキャン行為やパケット送信を行うことは、不正アクセス禁止法などの法律に抵触する可能性があります。また、倫理的にも問題があります。実験は必ず自身の管理下にある環境、または許可された環境でのみ行ってください。
  • 意図しない動作: Scapyは低レイヤーでの操作を可能にするため、OSの通常のネットワークスタックの動作(例: RSTパケットの自動送信など)と干渉する場合があります。特定のシナリオ(例: TCPセッションの完全な模倣)では、OS側の動作を抑制する設定(ファイアウォールルールなど)が必要になることがあります。
  • 免責事項: 本記事の情報に基づいてScapyを使用した結果、発生したいかなる損害や問題についても、作成者は一切の責任を負いません。自己責任において、十分な注意を払って利用してください。

安全に、そして倫理的にScapyを活用し、ネットワークへの理解を深めていきましょう。🛡️

まとめ

この記事では、Pythonの強力なパケット操作ライブラリであるScapyを使って、ネットワークパケットを生成し、送信する方法について解説しました。

主なポイントは以下の通りです:

  • ScapyはPythonでパケットを自由に構築・操作できるライブラリであること。
  • pip install scapyでインストールできること。
  • パケットはEther() / IP() / TCP()のように/演算子でレイヤーを重ねて構築すること。
  • ls()でプロトコルのフィールドを確認し、インスタンス化時に引数で値を設定できること。
  • show()summary()でパケットの内容を確認できること。
  • IP, TCP, UDP, ICMPなど様々なプロトコルのパケット生成例。
  • レイヤー3送信にはsend()、レイヤー2送信にはsendp()を使用すること。
  • 応答を受信するにはsr1()srp()などの関数を使用すること。
  • Scapyの使用には管理者権限が必要な場合が多く、ネットワークへの影響や法律・倫理に十分注意する必要があること。

Scapyを使いこなせば、ネットワークプロトコルの動作をより深く理解したり、カスタムツールを作成したり、複雑なネットワークテストを実施したりすることが可能になります。ぜひ、安全な環境で色々なパケットを作成・送信してみて、その可能性を探求してみてください。 Happy Packet Crafting! 🎉

参考情報

  • Scapy 公式ドキュメント: Scapyの最も包括的で正確な情報源です。使い方、APIリファレンス、チュートリアルなどが含まれています。
    https://scapy.readthedocs.io/
  • GitHub – secdev/scapy: Scapyのソースコードリポジトリ。最新の開発状況やIssueを確認できます。
    https://github.com/secdev/scapy

コメント

タイトルとURLをコピーしました