Scapyの環境構築とネットワークプログラミング - 土日の勉強ノート

土日の勉強ノート

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

Scapyの環境構築とネットワークプログラミング

前回 は、hashcat を使って、簡単なパスワードを解析して、hashcat の使い方を詳細に知って、GPU を使って、どれぐらいの時間でパスワードをクラックできるかを考えてみました。

今回は、Python の Scapy を使って、ネットワークプログラミングをやりたいと思います。

それでは、やっていきます。

参考文献

はじめに

「セキュリティ」の記事一覧です。良かったら参考にしてください。

セキュリティの記事一覧
・第1回:Ghidraで始めるリバースエンジニアリング(環境構築編)
・第2回:Ghidraで始めるリバースエンジニアリング(使い方編)
・第3回:VirtualBoxにParrotOS(OVA)をインストールする
・第4回:tcpdumpを理解して出力を正しく見れるようにする
・第5回:nginx(エンジンエックス)を理解する
・第6回:Python+Flask(WSGI+Werkzeug+Jinja2)を動かしてみる
・第7回:Python+FlaskのファイルをCython化してみる
・第8回:shadowファイルを理解してパスワードを解読してみる
・第9回:安全なWebアプリケーションの作り方(徳丸本)の環境構築
・第10回:Vue.jsの2.xと3.xをVue CLIを使って動かしてみる(ビルドも行う)
・第11回:Vue.jsのソースコードを確認する(ビルド後のソースも見てみる)
・第12回:徳丸本:OWASP ZAPの自動脆弱性スキャンをやってみる
・第13回:徳丸本:セッション管理を理解してセッションID漏洩で成りすましを試す
・第14回:OWASP ZAPの自動スキャン結果の分析と対策:パストラバーサル
・第15回:OWASP ZAPの自動スキャン結果の分析と対策:クロスサイトスクリプティング(XSS)
・第16回:OWASP ZAPの自動スキャン結果の分析と対策:SQLインジェクション
・第17回:OWASP ZAPの自動スキャン結果の分析と対策:オープンリダイレクト
・第18回:OWASP ZAPの自動スキャン結果の分析と対策:リスク中すべて
・第19回:CTF初心者向けのCpawCTFをやってみた
・第20回:hashcatの使い方とGPUで実行したときの時間を見積もってみる
・第21回:Scapyの環境構築とネットワークプログラミング ← 今回

Scapy の公式サイトは以下です。

scapy.net

また、公式のドキュメントは以下にあります。

scapy.readthedocs.io

今回は、この公式ドキュメントを参考にして、出来るだけ多くのプロトコルを実装してみたいと思います。

環境は、Windows10 の VirtualBox で、ParrotOS を使います。通信先は、徳丸本の実習環境の wasbook(VirtualBox で起動した Debian)を使います。wasbook のように、軽い通信相手が手元にあると、とても便利だと思います。

環境構築は以下の記事で行いました。

daisuke20240310.hatenablog.com

Scapyの概要と環境構築

Scapy とは、Python で、柔軟にパケットを送受信することが出来るパッケージで、ハッキングのスクリプト(エクスプロイトコード)で、よく使われている印象です。

Scapy にはインタラクティブシェルという、インストールしなくても試せる機能が提供されています。run_scapy というプログラムで、以下の Scapy の GitHub のツリーに含まれています。なお、pip でインストールしたパッケージには run_scapy は含まれていなくて、代わりに scapy というコマンドで、ほぼ同じことが出来ると思います。ここでは、run_scapy を使ってみます。

github.com

クローンして、ディレクトリ移動して、run_scapy を起動するだけです。Scapy の中には、管理者権限がないと実行できないものが存在します(Wiresharkやtcpdumpも管理者権限が必要)ので、sudo を付けて実行します。

なかなか個性的な画面とともに、入力待ち(>>>)になりました。

$ git clone https://github.com/secdev/scapy.git
$ cd scapy/
$ sudo ./run_scapy
INFO: Can't import PyX. Won't be able to use psdump() or pdfdump().

                     aSPY//YASa
             apyyyyCY//////////YCa       |
            sY//////YSpcs  scpCY//Pp     | Welcome to Scapy
 ayp ayyyyyyySCP//Pp           syY//C    | Version 2.6.0rc1.dev79
 AYAsAYYYYYYYY///Ps              cY//S   |
         pCCCCY//p          cSSps y//Y   | https://github.com/secdev/scapy
         SPPPP///a          pP///AC//Y   |
              A//A            cyP////C   | Have fun!
              p///Ac            sC///a   |
              P////YCpc           A//A   | Craft packets like it is your last
       scccccp///pSP///p          p//Y   | day on earth.
      sY/////////y  caa           S//P   |                      -- Lao-Tze
       cayCyayP//Ya              pY/Ya   |
        sY/PsY////YCc          aC//Yp
         sc  sccaCY//PCypaapyCP//YSs
                  spCPY//////YPSps
                       ccaacs
                                       using IPython 8.5.0
>>>

デモにあるように、GitHub に ICMP(ping)アクセスしてみます。

パケットを準備して、sr1(送信して1つの受信を受け取る)を実行します。受信したパケットは、戻り値 r に情報を格納してくれるので、その中を確認できます。ICMP は type で機能が決まります。エコー要求の type は 8、エコー応答の type は 0 です。

>>> p = IP(dst="github.com")/ICMP()
>>> p
<IP  frag=0 proto=icmp dst=Net("github.com/32") |<ICMP  |>>
>>> r = sr1(p)
Begin emission:
Finished sending 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
>>> r
<IP  version=4 ihl=5 tos=0x0 len=28 id=6388 flags= frag=0 ttl=113 proto=icmp chksum=0x5f52 src=20.27.177.113 dst=10.0.2.15 |<ICMP  type=echo-reply code=0 chksum=0xffff id=0x0 seq=0x0 unused=b'' |<Padding  load=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' |>>>
>>> r[IP].src
'20.27.177.113'
>>> r[ICMP].type
0

このときの送受信を Wireshark でキャプチャしました。

Wiresharkのキャプチャ
Wiresharkのキャプチャ

とても直感的にパケットの送受信を行うことが出来ます。自分が設定、変更したいフィールドだけを設定すれば、あとは、適切なデフォルト値を Scapy の方で設定してくれます。これがとても便利です。

インタラクティブシェルを終了するときは、Python と同じで、exit() で終了できます。

Scapyのインストール

まずは、インストールしていきます。インストールは簡単です。

$ pip install scapy
Successfully installed scapy-2.5.0

最新の v2.5.0 が入りました。

Scapyを使ってPythonでネットワークプログラミング

では、Python で書いていきます。

ICMP

まずは、先ほどの ICMP を Python で実装してみます。

やってることは先ほどと同じですが、ICMP の送信先については、コマンドライン引数で渡せるようにしています。

import os, sys
from scapy.all import *

def icmp( host="localhost" ):
    
    pkt = IP(dst=host)/ICMP()
    
    print( f"pkt={pkt}" )
    
    res = sr1( pkt )
    
    print( f"res={res}" )

if __name__ == '__main__':
    
    print( f"sys.argv={sys.argv}" )
    
    icmp( sys.argv[1] )

では、実行してみます。先ほどと同様に、管理者権限で実行します。

$ sudo python scapy_inet.py github.com
sys.argv=['scapy_inet.py', 'github.com']
pkt=IP / ICMP 10.0.2.15 > Net("github.com/32") echo-request 0
Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
res=IP / ICMP 20.27.177.113 > 10.0.2.15 echo-reply 0 / Padding

うまく動いているようです。Wireshark のキャプチャも取ったので貼っておきます。

Wiresharkのキャプチャ(ICMP)
Wiresharkのキャプチャ(ICMP)

パケットの詳細表示

ICMP のプログラムに、パケットを詳細に表示するものを追加してみます。detail 関数を追加しました。見やすいように少し改行を追加しています。

import os, sys
from scapy.all import *

def icmp( host="localhost" ):
    
    pkt = IP(dst=host)/ICMP()
    
    print( f"pkt={pkt}" )
    
    detail( pkt )
    
    res = sr1( pkt )
    
    print( f"res={res}" )
    
    detail( res )

def detail( pkt ):
    
    print()
    
    print( "--- show ---\n" )
    
    pkt.show()
    
    print( "--- ls ---\n" )
    
    ls( pkt )
    
    print( "\n--- hexdump ---\n" )
    
    hexdump( pkt )

if __name__ == '__main__':
    
    print( f"sys.argv={sys.argv}" )
    
    icmp( sys.argv[1] )

では、実行してみます。

$ sudo python scapy_inet.py github.com
sys.argv=['scapy_inet.py', 'github.com']
pkt=IP / ICMP 10.0.2.15 > Net("github.com/32") echo-request 0

--- show ---

###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     =
  frag      = 0
  ttl       = 64
  proto     = icmp
  chksum    = None
  src       = 10.0.2.15
  dst       = Net("github.com/32")
  \options   \
###[ ICMP ]###
     type      = echo-request
     code      = 0
     chksum    = None
     id        = 0x0
     seq       = 0x0
     unused    = ''

--- ls ---

version    : BitField  (4 bits)                  = 4               ('4')
ihl        : BitField  (4 bits)                  = None            ('None')
tos        : XByteField                          = 0               ('0')
len        : ShortField                          = None            ('None')
id         : ShortField                          = 1               ('1')
flags      : FlagsField                          = <Flag 0 ()>     ('<Flag 0 ()>')
frag       : BitField  (13 bits)                 = 0               ('0')
ttl        : ByteField                           = 64              ('64')
proto      : ByteEnumField                       = 1               ('0')
chksum     : XShortField                         = None            ('None')
src        : SourceIPField                       = '10.0.2.15'     ('None')
dst        : DestIPField                         = Net("github.com/32") ('None')
options    : PacketListField                     = []              ('[]')
--
type       : ByteEnumField                       = 8               ('8')
code       : MultiEnumField (Depends on 8)       = 0               ('0')
chksum     : XShortField                         = None            ('None')
id         : XShortField (Cond)                  = 0               ('0')
seq        : XShortField (Cond)                  = 0               ('0')
ts_ori     : ICMPTimeStampField (Cond)           = None            ('31606452')
ts_rx      : ICMPTimeStampField (Cond)           = None            ('31606452')
ts_tx      : ICMPTimeStampField (Cond)           = None            ('31606452')
gw         : IPField (Cond)                      = None            ("'0.0.0.0'")
ptr        : ByteField (Cond)                    = None            ('0')
reserved   : ByteField (Cond)                    = None            ('0')
length     : ByteField (Cond)                    = None            ('0')
addr_mask  : IPField (Cond)                      = None            ("'0.0.0.0'")
nexthopmtu : ShortField (Cond)                   = None            ('0')
unused     : MultipleTypeField (ShortField, IntField, StrFixedLenField) = b''             ("b''")

--- hexdump ---

0000  45 00 00 1C 00 01 00 00 40 01 A9 45 0A 00 02 0F  E.......@..E....
0010  14 1B B1 71 08 00 F7 FF 00 00 00 00              ...q........
Begin emission:
Finished sending 1 packets.
.*
Received 2 packets, got 1 answers, remaining 0 packets
res=IP / ICMP 20.27.177.113 > 10.0.2.15 echo-reply 0 / Padding

--- show ---

###[ IP ]###
  version   = 4
  ihl       = 5
  tos       = 0x0
  len       = 28
  id        = 6529
  flags     =
  frag      = 0
  ttl       = 113
  proto     = icmp
  chksum    = 0x5ec5
  src       = 20.27.177.113
  dst       = 10.0.2.15
  \options   \
###[ ICMP ]###
     type      = echo-reply
     code      = 0
     chksum    = 0xffff
     id        = 0x0
     seq       = 0x0
     unused    = ''
###[ Padding ]###
        load      = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

--- ls ---

version    : BitField  (4 bits)                  = 4               ('4')
ihl        : BitField  (4 bits)                  = 5               ('None')
tos        : XByteField                          = 0               ('0')
len        : ShortField                          = 28              ('None')
id         : ShortField                          = 6529            ('1')
flags      : FlagsField                          = <Flag 0 ()>     ('<Flag 0 ()>')
frag       : BitField  (13 bits)                 = 0               ('0')
ttl        : ByteField                           = 113             ('64')
proto      : ByteEnumField                       = 1               ('0')
chksum     : XShortField                         = 24261           ('None')
src        : SourceIPField                       = '20.27.177.113' ('None')
dst        : DestIPField                         = '10.0.2.15'     ('None')
options    : PacketListField                     = []              ('[]')
--
type       : ByteEnumField                       = 0               ('8')
code       : MultiEnumField (Depends on 0)       = 0               ('0')
chksum     : XShortField                         = 65535           ('None')
id         : XShortField (Cond)                  = 0               ('0')
seq        : XShortField (Cond)                  = 0               ('0')
ts_ori     : ICMPTimeStampField (Cond)           = None            ('31606452')
ts_rx      : ICMPTimeStampField (Cond)           = None            ('31606452')
ts_tx      : ICMPTimeStampField (Cond)           = None            ('31606452')
gw         : IPField (Cond)                      = None            ("'0.0.0.0'")
ptr        : ByteField (Cond)                    = None            ('0')
reserved   : ByteField (Cond)                    = None            ('0')
length     : ByteField (Cond)                    = None            ('0')
addr_mask  : IPField (Cond)                      = None            ("'0.0.0.0'")
nexthopmtu : ShortField (Cond)                   = None            ('0')
unused     : MultipleTypeField (ShortField, IntField, StrFixedLenField) = b''             ("b''")
--
load       : StrField                            = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' ("b''")

--- hexdump ---

0000  45 00 00 1C 19 81 00 00 71 01 5E C5 14 1B B1 71  E.......q.^....q
0010  0A 00 02 0F 00 00 FF FF 00 00 00 00 00 00 00 00  ................
0020  00 00 00 00 00 00 00 00 00 00 00 00 00 00        ..............

それぞれ、だいぶ細かい表示ですね。実装時は、各パケットの特定のフィールドにアクセスすることが多いと思うので、あまり使わないかもしれませんが、Wireshark が使えない環境とかではいいかもです。

情報取得

次は、自分の情報(IPアドレスとか、MACアドレスとか)や、指定したホストの情報を取得してみたいと思います。Scapy には、たくさんの情報を取得できる関数が用意されています。

import os, sys
from scapy.all import *

from scapy_inet import icmp

def myinfo():
    
    lst_if = get_if_list()
    
    print( f"get_if_list()={lst_if}" )
    
    lst_info = []
    
    for iface in lst_if:
        
        dic = { 'if': None, 'ip': None, 'mac': None, }
        
        dic['if']  = iface
        dic['ip']  = get_if_addr( iface )
        dic['mac'] = get_if_hwaddr( iface )
        
        lst_info.append( dic )
    
    for dic in lst_info:
        
        print( f"iface={dic['if']}" )
        print( f"  ip_addr={dic['ip']}" )
        print( f"  mac={dic['mac']}" )

def hostinfo( host ):
    
    res = icmp( host )
    
    mac = getmacbyip( res[IP].src )
    
    print( f"host({host}) mac={mac}" )

if __name__ == '__main__':
    
    print( f"sys.argv={sys.argv}" )
    
    myinfo()
    
    print()
    
    hostinfo( sys.argv[1] )

では、実行してみます。最初は、自分の情報を取得してログ出力し、次は、指定したホスト(wasbook)について、ICMP で、IPアドレスを取得した後、MACアドレスを取得しています。

$ sudo python scapy_info.py example.jp
sys.argv=['scapy_info.py', 'example.jp']
get_if_list()=['lo', 'enp0s3', 'enp0s8']
iface=lo
  ip_addr=127.0.0.1
  mac=00:00:00:00:00:00
iface=enp0s3
  ip_addr=10.0.2.15
  mac=08:00:27:38:2a:ed
iface=enp0s8
  ip_addr=192.168.56.105
  mac=08:00:27:b7:97:75

pkt=IP / ICMP 192.168.56.105 > Net("example.jp/32") echo-request 0
Begin emission:
Finished sending 1 packets.
..*
Received 3 packets, got 1 answers, remaining 0 packets
res=IP / ICMP 192.168.56.101 > 192.168.56.105 echo-reply 0 / Padding
host(example.jp) mac=08:00:27:b3:d8:93

ARP

次は、ARP を送信してみます。

ARP は、Ether の上に乗ってるので、そのように実装します。IPアドレスを送信すると、MACアドレスを返してくれます。

import os, sys
from scapy.all import *

from scapy_common import detail

def arp( ipaddr="127.0.0.1", debug=False ):
    
    pkt = Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst=ipaddr)
    
    print( f"pkt={pkt}" )
    
    if debug: detail( pkt )
    
    res = srp1( pkt )
    
    print( f"res={res}" )
    
    if debug: detail( res )
    
    return res

if __name__ == '__main__':
    
    # 実行例
    # $ sudo python scapy_arp.py 10.0.2.2
    
    print( f"sys.argv={sys.argv}" )
    
    arp( sys.argv[1] )

では、実行してみます。

$ sudo python scapy_arp.py 10.0.2.2
sys.argv=['scapy_arp.py', '10.0.2.2']
pkt=Ether / ARP who has 10.0.2.2 says 10.0.2.15
Begin emission:
Finished sending 1 packets.
*
Received 1 packets, got 1 answers, remaining 0 packets
res=Ether / ARP is at 52:54:00:12:35:02 says 10.0.2.2 / Padding

同じネットワーク内のノードを対象とする必要があるので、VirtualBox が準備してくれるルーターに送信しました。MACアドレスが取得できています。

おわりに

今回は、Scapy を使って、低レイヤなネットワークの実装をしてみました。

必要になったら、DNS、DHCPクライアントなどを実装して、ここに追記したいと思います。HTTP もやってみたかったのですが、Scapy よりも requests を使った方が実装しやすいようなので、別の記事で書きたいと思います。

今回は、Scapy のロゴを使わせていただきました。ありがとうございます。

最後になりましたが、エンジニアグループのランキングに参加中です。

気楽にポチッとよろしくお願いいたします🙇

今回は以上です!

最後までお読みいただき、ありがとうございました。