Pythonでスニッファー

BlackHatPython(2nd edition)を読むpython

Black Hat Python, 2nd Edition
Python Programming for Hackers and Pentesters
by Justin Seitz and Tim Arnold
no starch press
April 2021, 216 pp.

 

本記事は『Black Hat Python, 2nd Edition Python Programming for Hackers and Pentesters』の要点整理及び自分の理解度確認を目的として書いたものです。ここで解説する本書中のソースコードは全てno starch pressのサイトから誰でもフリーでダウンロードできます。自分の理解が及ばず誤りが散見される可能性がありますがご了承ください。

 

流石に本書に書いてある文章をそのまま訳してつらつら書くわけにはいかない。

 

ここでは誰でもフリーにダウンロードできる本書中のソースコードにおいて、大切な所や少し難しい所の説明を軽く挟んでいくだけ。

はしょった部分も沢山ある。

 

重要な部分は実際に本書を読まなければ知ることができない

 

本書をすでに購入済みの人がその本片手に参考にするような記事になっている。

 

是非『Black Hat Python, 2nd Edition Python Programming for Hackers and Pentesters』を手に取ってその周辺の重要な説明部分にも目を通すことを勧める。

 

 

今回は最終的な完全なコードを示すことはせず、コードを部分的に説明する。

(まぁ最終的なコードは以降で説明してる部分的なコードを一つにまとるだけだが。)

 

完全版のコードは誰でもno starch pressからダウンロードできる。

スポンサーリンク

スニッファーについて

”スニッファー”とはトラフィックを監視してパケットの解析などを行うソフトウェアのこと。

 

import socket
import os

HOST = '192.168.2.103'

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)
    
    print(sniffer.recvfrom(65565))
        
    if os.name == 'nt':
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

if __name__ == '__main__':
    main()

 

リッスンするホストとして自分のIPアドレスを変数HOSTに設定する。

 

ソケットオブジェクトに与える引数については、正直「こういうものなんだな〜」くらいで書き写してしまっていいと個人的には思う。

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)
sniffer.bind((HOST, 0))

sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)だ

スニッフする時にソケットオブジェクトに与える引数はコレ、IPヘッダー情報欲しいときはコレを引数として与えるって感じで。

 

スニッファーとして”プロミスキャスモード”で動作する。

”プロミスキャスモード”は自分宛でないパケットでも全部受け取るモードのこと。

 

if os.name == 'nt':
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

Widowsでプロミスキャスモードを使用するにはIOCTL値なるものの設定が必要で、それがこの部分。

IPのデコード

やってみるとわかるが、上のスクリプトではレスポンスがバイナリのままで読みにくい。

そこでPythonのstructモジュールを使ってデコードの機能を実装する。

 

IPヘッダの情報を扱うクラスIPを作成。

import ipaddress
import struct

class IP:
    def __init__(self, buff = None):        
        header = struct.unpack('<BBHHHBBH4s4s', buff)
        self.ver = header[0] >> 4
        self.ihl = header[0] & 0xF

        self.tos = header[1]
        self.len = header[2]
        self.id = header[3]
        self.offset = header[4]
        self.ttl = header[5]
        self.protocol_num = header[6]
        self.sum = header[7]
        self.src = header[8]
        self.dst = header[9]

        self.src_address = ipaddress.ip_address(self.src)
        self.dst_addresss = ipaddress.ip_address(self.dst)
        
        self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}

 

変数buffはパケット等データのこと。

 

まずデータbuffを書式指定文字に従ってバイナリからか読みやすいように変換(アンパック)する。

header = struct.unpack('<BBHHHBBH4s4s', buff)

返り値の型はタプルである。

 

書式指定文字についてはstructモジュールのドキュメントを参照。

 

 

そしてそのタプルの各要素がIPヘッダのそれぞれの要素に対応するから、上に示したコードの通りインスタンス変数を設定する。

(IPヘッダの構造)

IPヘッダ構造

引用:https://datatracker.ietf.org/doc/html/rfc791#section-3.1

 

先に出てきた書式指定文字との対応はこんな感じ👇

ipヘッダ(書き込みアリ)

ちょうど書式指定文字のBHと同じ個数のインスタンス変数があることにも注目。

 

self.ver = header[0] >> 4
self.ihl = header[0] & 0xF

IPのバージョンとIPヘッダ長をそれぞれインスタンス変数verihlに入れる。

なぜこの奇妙なコードでバージョンとヘッダ長が得られるのか?は本書に詳しく書いてある。

 

self.ver = header[0] >> 4

これはheader[0]の値を右に4ビットシフトすることを意味する。

 

以下に具体例を示す

>>> a = 0b10101001
>>> bin(a)
'0b10101001'
>>> bin(a >> 4)
'0b1010'

右に4ビットシフトするということは、10101001の左に00000が4つ付く。

 

self.src_address = ipaddress.ip_address(self.src) 
elf.dst_addresss = ipaddress.ip_address(self.dst)

ipaddressモジュールのip_addressモジュールは、IPアドレスオブジェクトを受け取ってそれをIPアドレスとして読みやすいにようにしたものを返す。

ICMPのデコード

ICMPのデコードもIPのそれとほとんど同じコードで実装できる。

 

特に重要なこととしては、UDPパケットを打ってUnreachableが返ってくるということはそのホストがaliveしている証拠という点。

もしどのホストもaliveしていなければ何も返ってこない。

 

ICMPの部分については本書を参照。

ipaddressモジュールについて

忘れないようにipaddressモジュールについてここで簡単にまとめておく。

 

ipaddress.ip_address('192.168.2.105')

ipaddress.ip_addressにはstr型のアドレスを渡せばアドレスオブジェクトが返ってくる。

 

ipaddress.ip_network('192.168.2.0/24')

ipaddress.ip_networkには上のようにサブネットを渡すと、ネットワークオブジェクトを返す。

 

ipaddressモジュールの公式ドキュメント👇

個人的感想

自分のPython理解度が以前に比べだいぶ高くなったことに気付いた。

 

実はこの2ndエディションの前の『サイバーセキュリティプログラミング――Pythonで学ぶハッカーの思考』をやったんだけど、Pythonの基礎知識と浅い理解では太刀打ちできなかった。

 

それから少し経って、だいぶPythonに慣れてその他の知識もついた今はソースコードがきちんと理解できる。

それを実感したなぁ。

タイトルとURLをコピーしました