Python 从 Black Hat Python 书中嗅探

Python Sniffing from Black Hat Python book

import socket
import os
import struct
import sys
from ctypes import *

# host to listen on
host   = sys.argv[1]

class IP(Structure):

    _fields_ = [
        ("ihl",           c_ubyte, 4),
        ("version",       c_ubyte, 4),
        ("tos",           c_ubyte),
        ("len",           c_ushort),
        ("id",            c_ushort),
        ("offset",        c_ushort),
        ("ttl",           c_ubyte),
        ("protocol_num",  c_ubyte),
        ("sum",           c_ushort),
        ("src",           c_ulong),
        ("dst",           c_ulong)
    ]

    def __new__(self, socket_buffer=None):
        return self.from_buffer_copy(socket_buffer)    

    def __init__(self, socket_buffer=None):

        # map protocol constants to their names
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # human readable IP addresses
        self.src_address = socket.inet_ntoa(struct.pack("<L",self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L",self.dst))

        # human readable protocol
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)

# create a raw socket and bind it to the public interface
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))

# we want the IP headers included in the capture
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# if we're on Windows we need to send some ioctls
# to setup promiscuous mode
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

try:
    while True:

        # read in a single packet
        raw_buffer = sniffer.recvfrom(65565)[0]

        # create an IP header from the first 20 bytes of the buffer
        ip_header = IP(raw_buffer[0:20])

        print "Protocol: %s %s -> %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

except KeyboardInterrupt:
    # if we're on Windows turn off promiscuous mode
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

这是 Black Hat Python 一书中的代码。这段代码应该嗅探原始套接字并显示来自 IP header 的信息。它对我来说在 Windows 上运行良好(使用 Windows 8.1 64 位)。当我在 linux (Kali linux 1.1.0-amd64) 上尝试 运行 时,出现以下错误

ValueError: Buffer size too small (20 instead of at least 32 bytes)

为了解决这个问题,我像这样向缓冲区添加了 12 个空格

ip_header = IP(raw_buffer[0:20]+' '*12)

当我这样做时,出现以下错误

struct.error: 'L' format requires 0 <= number <= 4294967295

这发生在线上

self.src_address = socket.inet_ntoa(struct.pack("<L",self.src))

我试过将 L 之前的符号更改为 > 和 !我只用 L 尝试过所有这些都给了我同样的问题。我也试过像这样 self.src 在 ntohs 中包装

self.src_address = socket.inet_ntoa(struct.pack("<L",socket.ntohs(self.src)))

我认为这与字节顺序有关,但我不确定。任何帮助将不胜感激。

注意:在 windows 上,您必须 运行 作为管理员,而在 linux 上,由于原始套接字,您必须 运行 作为超级用户。如果你在 linux 上 运行 打开另一个终端并 ping www.google.com 这样你就可以生成一些 ICMP 数据包供它捕获。

编辑:我也尝试用

反转缓冲区
ip_header = IP(raw_buffer[0:20][::-1]+' '*12)

编辑 2:在执行此处列出的任何其他项目之前,我确实在下面的行中同时尝试了 65535 和 65534。

raw_buffer = sniffer.recvfrom(65565)[0]

编辑 3:这在 ubuntu 机器 运行ning python 2.7.6 上工作,我的 kali 发行版是 2.7.3,所以我决定获取最新版本的 python 在我的 kali 盒子上,恰好是 2.7.9。仍然没有运气。

我将以下代码放入我的结构中的 new 函数中以查看缓冲区大小

print sizeof(self)

在我的 Ubuntu 和 windows 机器上它是 20 但是在我的 kali 机器上它是 32

#raw_buffer = sniffer.recvfrom(65565)[0]
raw_buffer = sniffer.recvfrom(65535)[0]

IP 数据包大小为 (2^16) - 1

问题出在 32 位系统和 64 位系统上。
ip_header = IP(raw_buffer[:20]) 适用于 x86 Ubuntu。
ip_header = IP(raw_buffer[:32]) 适用于 amd64 CentOS 6.6 Python 2.6.6
ip_header = IP(raw_buffer) 两者都适用。

你必须改变这些,

("src",           c_ulong),
("dst",           c_ulong)  

self.src_address = socket.inet_ntoa(struct.pack("<L",self.src))
self.dst_address = socket.inet_ntoa(struct.pack("<L",self.dst))

进入

("src",           c_uint32),
("dst",           c_uint32)  

self.src_address = socket.inet_ntoa(struct.pack("@I",self.src))
self.dst_address = socket.inet_ntoa(struct.pack("@I",self.dst))

'@I' 是本机顺序的 unsigned int。 因为 c_ulong 在 i386 中是 4 个字节,在 amd64 中是 8 个字节。检查以下内容,

struct.calcsize('@BBHHHBBHLL')  

在 i386 中为 20,在 amd64 中为 32,大小为 _fields_。实际上它是 amd64 中的 28 个字节加上为字对齐填充的 4 个字节。

ip_header = IP(raw_buffer[:20]) 现在可以独立于平台正常工作。

所以这是一个 64/32 位的问题。它需要 32 个字节而不是 20 个字节的事实意味着该结构没有正确打包。 "c_ulong" 是 64 位 linux 中的 64 位,并在 "IP" class.

中以这种方式映射

IP header 是 20 个字节 + 可选字段。源和目标 IP 地址以字节 20 结尾,这是当前 IP 结构正在拾取的内容。 (如果你想要这些选项,你将不得不手动解析它们)。

我查了UDP位域,直接设置到class"IP"。查看 ctypes 文档,可以映射整数类型以限制位数。

class IP(Structure):

    _fields_ = [
        ("ihl",           c_ubyte, 4),
        ("version",       c_ubyte, 4),
        ("tos",           c_ubyte, 8),
        ("len",           c_ushort, 16),
        ("id",            c_ushort, 16),
        ("offset",        c_ushort, 16),
        ("ttl",           c_ubyte, 8),
        ("protocol_num",  c_ubyte, 8),
        ("sum",           c_ushort, 16),
        ("src",           c_uint, 32),
        ("dst",           c_uint, 32),
    ]

如果将位偏移相加,它们总和为 160。160/8 = 20 字节,这就是 ctypes 将此结构打包到的内容。

运行 在 ping 上会产生一些看起来可以接受的东西。

Protocol: ICMP 127.0.0.1 -> 127.0.0.1
Protocol: ICMP 127.0.0.1 -> 127.0.0.1
Protocol: ICMP 127.0.0.1 -> 127.0.0.1
Protocol: ICMP 127.0.0.1 -> 127.0.0.1

此外,数据包大小是 MTU(或最大传输单元)的函数,因此如果您计划在以太网上使用 运行,则限制因素是帧的 MTU。较大的数据包在被推出以太网端口之前会在 tcp/ip 堆栈中分片。

$ ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 00:00:00:ff:ff:ff  
          UP BROADCAST MULTICAST  MTU:1500  Metric:1

此外,这个问题应该有助于澄清为什么某些平台具有不同大小的整数和长整数的问题:

What is the bit size of long on 64-bit Windows?

作为替代方案,我发现 dpkt 是 decoding/encoding ip 数据包的一个相当不错的库,除非您特别需要使用或想要 ctypes。

https://code.google.com/p/dpkt/