Scapyを使ってDHCPサーバを探してみようと、実装方法を調べてみました。
すると、Scapyの公式ドキュメントにあったり、既に実装している方々がいました。
- Identifying rogue DHCP servers on your LAN | Usage — Scapy 2.3.3-dev documentation
- Python(Scapy)を使って'DHCP DISCOVER' STORMERを作ってみる
- david415/dhcptakeover: DhcpTakeover - written with Python and scapy; a fork of the original author's code.
ただ、自分でも手を動かして理解したかったので、実装してみたときのメモを残します。
なお、DHCPサーバを探すツールは、英語版Wikipediaにまとまっていました。
Rogue DHCP - Wikipedia
目次
環境
ネットワーク構成
+--------+ | switch | +-+-+----+ | | | +----------------+ | | +--------+ Mac OS X | | | | 192.168.10.101 | | | +----------------+ | | | | +----------------+ | +----------+ DHCP1 | | | 192.168.10.1 | | +----------------+ | | +----------------+ +------------+ DHCP2 | | 192.168.10.110 | +----------------+
各機器の構成
- Mac OS X 10.11.6
- DHCP1, DHCP2
- 両方とも無線LANブロードバンドルータ内蔵のDHCPを利用
- IPアドレスのリース範囲は重ならないよう設定
インストール
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.
とあり、ソースコードからインストールすれば良いと書かれていました。
そのため、ソースコードからインストールしてみます。
# インストール用のディレクトリを作成し、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
と、パケットは送信されるものの受信ができませんでした。
そのため、
として実行すると、
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