本記事は、オライリージャパンから発行されている「サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考(原題:Black Hat Python)」の学習メモとして、書籍ではPython2で書かれていますが、自分なりに解釈した上でPython3に書き直しをしています。
今回はIPヘッダの各フィールドをデコードし、より詳細で分かりやすいパケットの解析方法について学んでいきます。
IPについて
IPは、OSI参照モデルでいうネットワーク層に位置するプロトコルです。
インターネットなどでは、パケットのルーティングを行うルーターが、このIPのヘッダに含まれている情報(宛先IPアドレスなど)を参照することで、どこのネットワークに送信すべきか判断します。
IPヘッダについて
IPは、現在ではまだIPv4が主流となっているため、ここではIPv4ヘッダについて確認していきます。
以下がRFC791で定義されているヘッダフォーマットです。
上記は幅4バイト(32ビット)が6段に並んでいるため、合計で24バイト(192ビット)ですが、最後の段にあるオプションとパディングはあまり使用されないため、無視し、IPv4のヘッダは全体で20バイトのサイズになります。
また、上記のオプション(パディングも)は可変長のため、オプションが追加されれば、最大で60バイトのヘッダサイズとなります。
IPヘッダのデコード
PythonでIPヘッダのデコードをする場合、ctypesライブラリのStructureクラスを使用すると簡単にデコードができます。
これは、C言語の構造体をPythonで体現しているもので、IPヘッダの各フィールド名と型名を落とし込むことで、容易にデコードすることができます。
C言語では"netinet/ip.h"に以下の構造体として定義されています。
/* netinet_ip.h */ struct ip { #if BYTE_ORDER == LITTLE_ENDIAN u_char ip_hl:4, /* header length */ ip_v:4; /* version */ #endif #if BYTE_ORDER == BIG_ENDIAN u_char ip_v:4, /* version */ ip_hl:4; /* header length */ #endif u_char ip_tos; /* type of service */ short ip_len; /* total length */ u_short ip_id; /* identification */ short ip_off; /* fragment offset field */ #define IP_DF 0x4000 /* dont fragment flag */ #define IP_MF 0x2000 /* more fragments flag */ u_char ip_ttl; /* time to live */ u_char ip_p; /* protocol */ u_short ip_sum; /* checksum */ struct in_addr ip_src,ip_dst; /* source and dest address */ };
ライブラリのインポート
今回は以下のライブラリを使用します。
# sniffer_ip_header_decode.py import socket import struct import os from ctypes import *
IPクラスの作成
PythonのctypesライブラリからStructureクラスを継承したIPクラスを作成します。
class IP(Structure): _fields_ = [ ("ihl", c_uint8, 4), ("version", c_uint8, 4), ("tos", c_uint8), ("len", c_uint16), ("id", c_uint16), ("offset", c_uint16), ("ttl", c_uint8), ("protocol_num", c_uint8), ("sum", c_uint16), ("src", c_uint32), ("dst", c_uint32) ] def __new__(self, socket_buffer=None): return self.from_buffer_copy(socket_buffer) def __init__(self, socket_buffer=None): self.protocol_map = {1: "ICMP", 6:"TCP", 17:"UDP"} self.src_address = socket.inet_ntoa(struct.pack("<L", self.src)) self.dst_address = socket.inet_ntoa(struct.pack("<L", self.dst)) try: self.protocol = self.protocol_map[self.protocol_num] except: self.protocol = str(self.protocol_num)
2~14行目は、前述のC言語スタイルの構造体を定義しています。
16行目で、ctypesのインスタンスを生成します。
20行目の初期化では、"protocol_map"でICMPとTCPおよびUDPを定義します。
各プロトコル番号は以下のIANAのサイトから確認することができます。
20~21行目では送信元IPアドレスおよび宛先IPアドレスを読みやすく加工した形式で格納します。
24行目以降は、前述の"protocol_map"で定義したプロトコル名を格納し、それ以外はただの番号として格納します。
メイン関数の作成
def main(): if os.name == "nt": socket_protocol = socket.IPPROTO_IP else: socket_protocol = socket.IPPROTO_ICMP sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol) sniffer.bind((host, 0)) sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) if os.name == "nt": sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) try: while True: raw_buffer = sniffer.recvfrom(65565)[0] ip_header = IP(raw_buffer[0:20]) print("Protocol: {} {} -> {}".format(ip_header.protocol, ip_header.src_address, ip_header.dst_address)) except KeyboardInterrupt: if os.name == "nt": sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) if __name__ == '__main__': host = "192.168.2.1" main()
メイン関数の処理は、前回作成したスニッファーと変わらないですが、ループ処理を施し、キャプチャしたパケットの20バイト目までをIPクラスの初期化処理用に渡しています。
最終的には「プロトコル名:送信元IPアドレス→宛先IPアドレス」の形で出力します。
動作確認
それでは、上記で作成したスクリプトを起動してみます。
なお、Windowsの場合はプロミスキャスモードを使うには管理者権限が必要なため、コマンドプロンプト(もしくはPowerShell)を管理者権限で起動します。
> python sniffer_ip_header_decode.py Protocol: ICMP 192.168.2.1 -> 192.168.2.1 Protocol: UDP 192.168.2.1 -> 224.0.0.251
試しに自分宛に送ったpingやマルチキャストDNSのパケットが表示されています。
最後に
今回は、Pythonの ctypesライブラリのStructureクラスを使用することで、簡単にIPヘッダの各フィールドにアクセスすることができました。
次回はICMPメッセージの詳細なデコードについて学んでいきます。