Mac + Python2 + Scapyで、DHCPサーバを探してみた

Scapyを使ってDHCPサーバを探してみようと、実装方法を調べてみました。

すると、Scapyの公式ドキュメントにあったり、既に実装している方々がいました。

 
ただ、自分でも手を動かして理解したかったので、実装してみたときのメモを残します。

なお、DHCPサーバを探すツールは、英語版Wikipediaにまとまっていました。
Rogue DHCP - Wikipedia

 
目次

 

環境

ネットワーク構成
+--------+
| switch |
+-+-+----+
| | |        +----------------+
| | +--------+ Mac OS X       |
| |          | 192.168.10.101 |
| |          +----------------+
| |
| |          +----------------+
| +----------+ DHCP1          |
|            | 192.168.10.1   |
|            +----------------+
|
|            +----------------+
+------------+ DHCP2          |
             | 192.168.10.110 |
             +----------------+

 

各機器の構成

 

インストール

Scapyはvirtualenv環境にインストールします。

# virtualenv環境を作る
$ virtualenv env
...
Installing setuptools, pip, wheel...done.

# virtualenv環境を有効化する
$ source env/bin/activate

# Scapyをインストールする
(env) $ pip install scapy
...
Successfully installed scapy-2.3.3

 
他にも必要なライブラリがあるためインストールしてみましたが、当初うまくいきませんでした。

そのため、NG/OKなインストール例をメモしておきます。

なお、discover_dhcp_server.py というDHCPサーバを見つけるPythonスクリプトは正常に動作するものとします。

 

Scapyで送信できないインストール例

Scapyだけをインストールして実行してみます。

# インストール状況の確認
(env) $ pip list
pip (9.0.1)
scapy (2.3.3)
setuptools (38.4.0)
wheel (0.30.0)

# 実行
(env) $ python discover_dhcp_server.py 
Traceback (most recent call last):
  File "discover_dhcp_server.py", line 3, in <module>
    from scapy.layers.dhcp import DHCP, BOOTP
  File "/path/to/env/lib/python2.7/site-packages/scapy/layers/dhcp.py", line 14, in <module>
    from scapy.ansmachine import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/ansmachine.py", line 14, in <module>
    from scapy.sendrecv import send,sendp,sniff
  File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 14, in <module>
    from scapy.arch.consts import DARWIN, FREEBSD
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/__init__.py", line 79, in <module>
    from scapy.arch.pcapdnet import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 328, in <module>
    import pcapy as pcap
ImportError: No module named pcapy

 
モジュール pcapy が無いようなので、インストールします。

(env) $ pip install pcapy
...
Successfully installed pcapy-0.11.1

 
再度実行します。

(env) $ python discover_dhcp_server.py 
Traceback (most recent call last):
  File "dhcp/discover_dhcp_server.py", line 3, in <module>
    from scapy.layers.dhcp import DHCP, BOOTP
  File "/path/to/env/lib/python2.7/site-packages/scapy/layers/dhcp.py", line 14, in <module>
    from scapy.ansmachine import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/ansmachine.py", line 14, in <module>
    from scapy.sendrecv import send,sendp,sniff
  File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 14, in <module>
    from scapy.arch.consts import DARWIN, FREEBSD
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/__init__.py", line 79, in <module>
    from scapy.arch.pcapdnet import *
  File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 471, in <module>
    import dumbnet as dnet
ImportError: No module named dumbnet

別のモジュール dumbnet が無いようです。

 
PyPIで探しますが、それらしいライブラリはありませんでした。
https://pypi.python.org/pypi?%3Aaction=search&term=dumbnet&submit=search

調べてみると、stackoverflowに

dumbnet is also known as libdnet.

python - 'ImportError: No module named dumbnet' when trying to run a script that leverages scapy on OS X - Stack Overflow

とあり、ソースコードからインストールすれば良いと書かれていました。

そのため、ソースコードからインストールしてみます。

# インストール用のディレクトリを作成し、git clone
(env) $ mkdir install_libdnet
(env) $ cd install_libdnet/
(env) install_libdnet $ git clone https://github.com/dugsong/libdnet.git
...
Resolving deltas: 100% (2709/2709), done.

# インストール
(env) install_libdnet $ cd libdnet
(env) libdnet $ ./configure && make
...
(env) libdnet $ cd python
(env) python $ python setup.py install
...
Writing /path/to/env/lib/python2.7/site-packages/dnet-1.12-py2.7.egg-info

 
もう一度実行してみます。

# ディレクトリを戻す
(env) $ cd ../../../

# 実行
(env) $ python discover_dhcp_server.py 
Begin emission:
Finished to send 1 packets.
.**Traceback (most recent call last):
File "discover_dhcp_server.py", line 66, in <module>
  discover()
File "discover_dhcp_server.py", line 58, in discover
  answers, _ = srp(discover_packet, iface=USB_INTERFACE_NAME, multi=True)
File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 378, in srp
  a,b=sndrcv(s ,x,*args,**kargs)
File "/path/to/env/lib/python2.7/site-packages/scapy/sendrecv.py", line 133, in sndrcv
  r = pks.nonblock_recv()
File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 672, in nonblock_recv
  p = self.recv(MTU)
File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 653, in recv
  pkt = self.ins.next()
File "/path/to/env/lib/python2.7/site-packages/scapy/arch/pcapdnet.py", line 390, in next
  s,us = h.getts()
AttributeError: 'NoneType' object has no attribute 'getts'

また別のエラーが出ました。今度はモジュール以外のエラーです。

 
事例を調べたところ、ScapyのIssueに情報がありました。

In a bash shell, i ran: sudo pip install pypcap Instead of sudo pip install pcap.

Sniff function Is not Functioning on OS X · Issue #97 · secdev/scapy

pypcapをインストールして試してみます。

# pypcapのインストール
(env) $ pip install pypcap
...
Successfully installed pypcap-1.2.0

# 実行
(env) $ python discover_dhcp_server.py 
Begin emission:
Finished to send 1 packets.
.**.....

送受信が成功しました。

 

GitHubの最新コミットを利用する場合 (2017/1/9追記)

GitHubの最新コミットを使えば、

  • pypcap
  • libdnet

がなくても動作するようです。

試してみた内容を以下の記事にしました。
最新のScapy(2.3.3.dev957)では、Macにpcapやdnetモジュールのインストールが不要っぽい - メモ的な思考的な

 

Scapyで送信できるインストール例

上記より、

  • pipでインストール
    • Scapy
    • pypcap
  • ソースコードからインストール
    • libdnet

のセットアップで良さそうです。

# Pythonバージョンを確認
$ python --version
Python 2.7.14

# virtualenv環境を作って有効化
$ virtualenv env
...
Installing setuptools, pip, wheel...done.
$ source env/bin/activate

# pipでScapyとpypcapをインストール
(env) $ pip install scapy pypcap
...
Successfully installed pypcap-1.2.0 scapy-2.3.3

# ソースコードからlibdnetをインストール
(env) $ mkdir install_libdnet
(env) $ cd install_libdnet/
(env) $ git clone https://github.com/dugsong/libdnet.git
(env) $ ./configure && make
(env) $ cd python/
(env) $ python setup.py install
...
Writing /path/to/env/lib/python2.7/site-packages/dnet-1.12-py2.7.egg-info

# インストール後の状況を確認
(env) $ pip list
dnet (1.12)
pip (9.0.1)
pypcap (1.2.0)
scapy (2.3.3)
setuptools (38.4.0)
wheel (0.30.0)

# 動作確認
(env) $ cd ../../../
(env) $ python discover_dhcp_server.py 
Begin emission:
Finished to send 1 packets.
.**....

動作しました。

 

野良DHCPサーバを探す実装

処理の流れ

公式ドキュメントや他の実装のコードを読みつつ、実装していきます。

主な処理の流れとしては、

  • Ether, IP, UDP, BOOTP, DHCP の各レイヤを作る
  • 公式ドキュメントに従い、送信前に conf.checkIPaddr = False とセット
  • L2パケットとして、srp(discover_packet, iface=USB_INTERFACE_NAME, multi=True) として送信
    • multi=True は公式ドキュメント通り

です。

 

BOOTPオブジェクトについて

BOOTPレイヤを作るため、 scapy.layers.dhcp.BOOTP クラスのインスタンスを作成します。

コンストラクタ引数の chaddr にはクライアントのMACアドレスを渡します。今回は野良DHCPサーバを探すため、実際のインタフェースのMACアドレスを渡すのではなく、適当な値を渡すことにします。

 
また、引数のMACアドレスの形式については、

  • MACアドレスをそのまま渡す
  • 何かしらの変換をして渡す

の2つの記事がありました。ソースコードのdocstringには何も記載されていませんでした。

そこで、RFC2131を見てみましたが、

chaddr 16 クライアントのハードウェアアドレス。

http://srgia.com/docs/rfc2131j.html#tbl1 (原文) https://www.ietf.org/rfc/rfc2131.txt

としか書かれておらず、形式については触れられていませんでした。

 
試しに両方で実装してみると、

# MACアドレスをそのまま渡すように実装した場合の結果
Begin emission:
Finished to send 1 packets.
.*****.........................^C
Received 31 packets, got 5 answers, remaining 0 packets
DHCP Server - MAC: bc:xx:xx:xx:xx:xx, IP: 192.168.10.110
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

# MACアドレスを変換した場合の結果
Begin emission:
Finished to send 1 packets.
.**.................................^C
Received 36 packets, got 2 answers, remaining 0 packets
DHCP Server - MAC: bc:xx:xx:xx:xx:xx, IP: 192.168.10.110
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

と、MACアドレスそのものを渡した場合にはうまく動作していないように見えました。

 
では、どんなロジックで変換すればよいか、参考にしたソースコードを見てみると

# http://netbuffalo.doorblog.jp/archives/4278096.html
chaddr = ''.join([chr(int(x,16)) for x in mac.split(':')])

# https://github.com/david415/dhcptakeover/blob/master/dhcptakeover.py
fam,hw = scapy.all.get_if_raw_hwaddr(scapy.all.conf.iface)
scapy.all.BOOTP(chaddr=hw)

となっていました。

get_if_raw_hwaddr()の実装をIntelliJ IDEAで追ってみると、

# https://github.com/secdev/scapy/blob/v2.3.3/scapy/arch/pcapdnet.py#L509
mac = mac2str(str(link_addr))

# https://github.com/secdev/scapy/blob/v2.3.3/scapy/utils.py#L294
def mac2str(mac):
    return "".join(map(lambda x: chr(int(x,16)), mac.split(":")))

となっていました。

参考にしたソースコード間でロジックが一致したため、 scapy.utils.mac2str() を使えば良さそうでした。

 

ソースコード全体

discover_dhcp_server.py

# -*- coding: utf-8 -*-
from scapy.config import conf
from scapy.layers.dhcp import DHCP, BOOTP
from scapy.layers.inet import IP, UDP
from scapy.layers.l2 import Ether
from scapy.sendrecv import srp
from scapy.utils import mac2str

DHCP_CLIENT_MAC_ADDRESS = '00:00:00:00:00:04'
BROADCAST_MAC_ADDRESS = 'FF:FF:FF:FF:FF:FF'

LIMITED_BROADCAST_IP_ADDRESS = '255.255.255.255'
DHCP_DISCOVER_HOST_IP_ADDRESS = '0.0.0.0'

DHCP_SERVER_PORT = 67
DHCP_CLIENT_PORT = 68

USB_INTERFACE_NAME = 'en4'


def discover():
    ether_layer = Ether(
        src=DHCP_CLIENT_MAC_ADDRESS,
        dst=BROADCAST_MAC_ADDRESS,
    )

    ip_layer = IP(
        src=DHCP_DISCOVER_HOST_IP_ADDRESS,
        dst=LIMITED_BROADCAST_IP_ADDRESS,
    )

    udp_layer = UDP(
        sport=DHCP_CLIENT_PORT,
        dport=DHCP_SERVER_PORT,
    )

    chaddr = mac2str(DHCP_CLIENT_MAC_ADDRESS)
    bootp_layer = BOOTP(chaddr=chaddr)

    dhcp_layer = DHCP(options=[('message-type', 'discover'), 'end'])

    discover_packet = ether_layer / ip_layer / udp_layer / bootp_layer / dhcp_layer

    # 作成したパケットを念のため確認
    discover_packet.show()

    # Scapyのドキュメント通りに設定して送信
    # http://scapy.readthedocs.io/en/latest/usage.html#identifying-rogue-dhcp-servers-on-your-lan
    conf.checkIPaddr = False
    answers, _ = srp(discover_packet, iface=USB_INTERFACE_NAME, multi=True)

    for send_packet, recv_packet in answers:
        print 'DHCP Server - MAC: {}, IP: {}'.format(
            recv_packet[Ether].src, recv_packet[IP].src)


if __name__ == '__main__':
    discover()

 

実行

Macで実行する時の注意点

手元の環境が

  • OS X 10.11.6
  • pyenvのPython2を使う
  • virtualenvに入れたScapyを使う

という環境のせいかもしれませんが、単に実行しただけでは

(env) $ python discover_dhcp_server.py
Begin emission:
Finished to send 1 packets.
..................(略)...^C
Received 272 packets, got 0 answers, remaining 1 packets

と、パケットは送信されるものの受信ができませんでした。

そのため、

  • WiFiではなく、外付けUSB有線LANアダプタ(LUA4-U3-AGT)からパケットを送信
  • Wiresharkを起動し、外付けUSB有線LANアダプタ(LUA4-U3-AGT)をモニタ

として実行すると、

Begin emission:
Finished to send 1 packets.
.*....*........................^C
Received 31 packets, got 2 answers, remaining 0 packets

受信もできました。 * の部分が、送受信が成功したところです。

 
ちなみに、実行時のログ全体は以下の通りです。

なお、 Ctrl + C で停止しない限り、パケットを送信し続けます。

$ python dhcp/discover_dhcp_server.py 
###[ Ethernet ]### 
  dst       = FF:FF:FF:FF:FF:FF
  src       = 00:00:00:00:00:04
  type      = 0x800
###[ IP ]### 
     version   = 4
     ihl       = None
     tos       = 0x0
     len       = None
     id        = 1
     flags     = 
     frag      = 0
     ttl       = 64
     proto     = udp
     chksum    = None
     src       = 0.0.0.0
     dst       = 255.255.255.255
     \options   \
###[ UDP ]### 
        sport     = bootpc
        dport     = bootps
        len       = None
        chksum    = None
###[ BOOTP ]### 
           op        = BOOTREQUEST
           htype     = 1
           hlen      = 6
           hops      = 0
           xid       = 0
           secs      = 0
           flags     = 
           ciaddr    = 0.0.0.0
           yiaddr    = 0.0.0.0
           siaddr    = 0.0.0.0
           giaddr    = 0.0.0.0
           chaddr    = '\x00\x00\x00\x00\x00\x04'
           sname     = ''
           file      = ''
           options   = 'c\x82Sc'
###[ DHCP options ]### 
              options   = [message-type='discover' end]

Begin emission:
Finished to send 1 packets.
.*....*........................^C
Received 31 packets, got 2 answers, remaining 0 packets
DHCP Server - MAC: bc:xx:xx:xx:xx:xx, IP: 192.168.10.110
DHCP Server - MAC: a4:xx:xx:xx:xx:xx, IP: 192.168.10.1

 

ソースコード

GitHubに上げました。 dhcp/discover_dhcp_server.py が今回のファイルです。
https://github.com/thinkAmi-sandbox/scapy-sample