Engineering Note

プログラミングなどの技術的なメモ

3.3 IPレイヤーのデコード (サイバーセキュリティプログラミング Pythonで学ぶハッカーの思考)

本記事は、オライリージャパンから発行されている「サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考(原題:Black Hat Python)」の学習メモとして、書籍ではPython2で書かれていますが、自分なりに解釈した上でPython3に書き直しをしています。

今回はIPヘッダの各フィールドをデコードし、より詳細で分かりやすいパケットの解析方法について学んでいきます。

 

 

IPについて

IPは、OSI参照モデルでいうネットワーク層に位置するプロトコルです。

インターネットなどでは、パケットのルーティングを行うルーターが、このIPのヘッダに含まれている情報(宛先IPアドレスなど)を参照することで、どこのネットワークに送信すべきか判断します。

 

IPヘッダについて

IPは、現在ではまだIPv4が主流となっているため、ここではIPv4ヘッダについて確認していきます。

以下がRFC791で定義されているヘッダフォーマットです。

 

ip_header

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 */
};

 

netinet/ip.h Sourceより参照

 

ライブラリのインポート

今回は以下のライブラリを使用します。

 

# 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のサイトから確認することができます。

Protocol Numbers

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メッセージの詳細なデコードについて学んでいきます。

 

参考書籍

サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考