第3章:原始套接字和流量嗅探

发布于 2021-09-10  212 次阅读


写在前面

  这一章主要是仿写Nmap这样的嗅探工具,熟悉socket库的使用以及使用Python去解码数据包报文。本章设计到的三方库是netaddr库、struct库和ctypes库,涉及的内容还是比较多的,看着量来吧,剩下的会另外开文章再写出来。
  还有一点就是,目前来说流量嗅探工具在网上已经相对很完善了,所以说本章的内容更多的是学习意义大于实际使用意义,不过,在极端的情况,在某些主机上现场编程这些内容还是比较令人尴尬的。

脚本代码

  首先,上本章主体代码,这个是完全体,什么基础的嗅探,ICMP、IP报文解码都在里面了。

import os
import socket
from ctypes import *
import struct
import time
import threading
from netaddr import IPNetwork,IPAddress

# 监听主机
host = "192.168.217.1"

# 扫描的目标网段
subnet = "192.168.217.0/24"

# 自定义响应字段
magic_message = "PYTHONRULES!"

# 批量发送UDP数据包
def udp_sender(subnet,magic_message):
    time.sleep(1)
    sender = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
    for ip in IPNetwork(subnet):
        try:
            sender.sendto(magic_message.encode(),("%s" % ip,65212))
        except Exception as e:
            print(e)

# IP头定义
class IP(Structure):
    _fields_ = [
        ("ihl", c_ubyte, 4),
        ("version", c_byte, 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):  # 喜闻乐见的复写
        # 协议字段与协议名称对应
        self.protocol_map = {1: "ICMP", 6: "TCP", 17: "UDP"}
        # 可读性更强的IP地址
        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)

# ICMP结构体
class ICMP(Structure):
    _fields_ = [
        ("type",    c_ubyte),
        ("code",    c_ubyte),
        ("checksum",c_short),
        ("unused",  c_ushort),
        ("next_hop_mtu",c_ushort)
    ]
    def __new__(self, socket_buffer):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer):
        pass

# 创建原始套接字,然后绑定在公开接口上
if os.name == "nt":
    socket_protocl = socket.IPPROTO_IP
else:
    socket_protocl = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET,socket.SOCK_RAW,socket_protocl)
sniffer.bind((host,0))

# 设置在捕获得数据包中包含IP头
sniffer.setsockopt(socket.IPPROTO_IP,socket.IP_HDRINCL,1)

# 在Windows平台上,设置IOCTL以启用混杂模式
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL,socket.RCVALL_ON)

# 发送探测数据包
t = threading.Thread(target=udp_sender,args=(subnet,magic_message))
t.start()

# 读取单个数据包   v1中的输出单个数据包
# print(sniffer.recvfrom(65565))

# v2对数据包进行部分解析
try:
    while True:
        # 读取数据包
        raw_buffer = sniffer.recvfrom(65565)[0]
        # 将缓冲区前20个字节按IP头进行解析
        ip_header = IP(raw_buffer[0:20])
        # 输出
        # print("Protocol: %s src:%s -> dst %s" % (ip_header.protocol,ip_header.src_address,ip_header.dst_address))

        # v3加入ICMP报文解析
        # 判断是否为ICMP报文
        try:
            if ip_header.protocol == "ICMP":
                # ICMP包的起始位置,hl为IP报文中的header length,代表IP报头的长度,长度单位为 4 个字节,即本区域值 = IP 头部长度(单位为字节)/ 长度单位(4 个字节)
                offset = ip_header.ihl * 4
                buf = raw_buffer[offset:offset + sizeof(ICMP)]
                # 解析ICMP数据
                icmp_header = ICMP(buf)
                # print("ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.code))
                # v4探测
                if icmp_header.code == 3 and icmp_header.type == 3:
                    if IPAddress(ip_header.src_address) in IPNetwork(subnet):
                        # print(raw_buffer)
                        if magic_message.encode() in raw_buffer:
                            print("Host is up: %s" % ip_header.src_address)
        except Exception as e:
            print(e)
except KeyboardInterrupt:          # 处理CTRL-C
    # 在Windows平台上关闭混杂模式
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

# 备注:
# 脚本运行需要管理员权限
# 目前改脚本只能探测出本机,还没有遇到能够探测处多台主机的内网,原因不明

  对于这个脚本,在备注中写的也比较清楚,几处版本的更迭什么的都还比较容易理解。

关于这个脚本

  首先说一些这个脚本相关的东西,C语言 + Python的混合编程——struct库+ctypes库

struct{
    u_char  ip_hl:4;    // header length(包头长度)
    u_char  ip_v:4;     // version(版本号)
    u_char  ip_tos;     // type of service(服务类型)
    u_short ip_len;     // total length(包总长)
    u_short ip_id;      // identifier(数据报ID)
    u_short ip_off;     // fragment offset(片偏移)
    u_char  ip_ttl;     // time to live(生存时间)
    u_char  ip_p;       // protocol(协议)
    u_short ip_sum;     // header checksum(头部校验)
    u_long  ip_src;     // Source Addresses(源地址)
    u_long  ip_dst;     // Destination Addresses(目的地址)
}

  C语言中的冒号":"

  有些信息在存储时,并不需要一个完整的字节而只需要占几个或一个二进制位,为了这种需求,在C语言中需要用到”位域“或者称之为”位段“
  所谓“位域”就是把一个字节中的二进制位划分为几个不同的区域,并说明每个区域的为数,每个域都有一个域名,允许在程序中按域名进行操作
  形式:struct 位域结构名{
位域列表
}
  位域列表的形式:类型说明符 位域符:位域长度
  同样的在Python中的位域结构需要struct库的支持,与C语言结构体结构类似
    位域结构名 = [
        ("位域符",数据类型,位域类型)
    ]
  这个结构算是struct库按照指定格式将字节流转换为Python指定的数据类型,针对于数据包报文

struct库

  struct库提供了用于在自己字符串和Python原生数据类型之间转换的函数,比如数字和字符串,该模块作用主要是用来完成Python数值和C语言结构体的Python字符串型时间的转换,可以用来处理存储在文件中或从网络连接中存储的二进制数据,以及其他数据源。

用处

  1. 按照制定格式将Python数据转换为字符串,该字符串为字节流,如网络传输时,不能传输int类型,此时现将int类型转化为字节流,然后再发送
  2. 按照制定格式将字节流转换为Python制定的数据类型
  3. 处理二进制数据,如果用struct来处理文件的话,需要用'wb','rb'以二进制(字节流)写,读的方式来处理文
  4. 处理C语言中的结构体

struct库中的部分函数

函数 return explain
pack(fmt,v1,v2…) string 按照给定的格式(fmt),把数据转换成字符串(字节流),并将该字符串返回.
pack_into(fmt,buffer,offset,v1,v2…) None 按照给定的格式(fmt),将数据转换成字符串(字节流),并将字节流写入以offset开始的buffer中.(buffer为可写的缓冲区,可用array模块)
unpack(fmt,v1,v2…..) tuple 按照给定的格式(fmt)解析字节流,并返回解析结果
pack_from(fmt,buffer,offset) tuple 按照给定的格式(fmt)解析以offset开始的缓冲区,并返回解析结果
calcsize(fmt) size of fmt 计算给定的格式(fmt)占用多少字节的内存,注意对齐方式

struct模块——封包和解包

bytes、str

  首先是Python版本之间的差别,bytes是Python3.X版本中新增加的数据类型,而在Python2.x中是呗合并在str中的,即Python3.X的数据类型为str和bytes,而Python2.X的数据类型为unicode和str。
  bytes是byte的序列,而str是unicode的序列。
  bytes通过decode()方法转换为str类型,str通过encode()方法转换为bytes类型
  互联网通信过程中是以二进制进行传输的,所以需要将str类型编码成bytes,而接收是需要接码成所需要的数据类型。

bytes()

  bytes()是Python3.X的内置函数,将其他数据类型强制转换为bytes类型。
  对于bytes类型,Python没有专门处理字节的数据类型,但由于b'str_boj'可以表示为bytes_obj,所以->字节数组<=>二进制str
  举个栗子:

b1 = 89
b2 = 83
b3 = 68
b4 = 58
bs = bytes([b1,b2,b3,b4])
print(bs)
bs = bs.decode("utf-8")
print(bs)

# output:
# b'YSD:'
# YSD:

  这里显示为b'YSD:'是因为IDE回显结果会自动进行解码操作,当然也可以使用十六进制的ASCII值进行测试,同样会返回相同的结果
  在C语言中,可以使用struct和union来处理字节,以及字节和int、float的的转换
  所以说Python中这样的操作吃力不讨好,而且这种处理方式对于float类型是无能为力的

struct库

  在Python中,"一切皆对象",基本数据类型也不例外,这里写一点C语言和Python的底层内存存储区别。
  C语言的数据存储的是真正的值,Python的列表存储的是元素的指针,这就造成了"列表元素不连续存储",在Python中列表的数据可能不会呗存储为连续的字节块,如果要处理他们就需要把Python值转换成C语言结构体,即打包成连续的数据字节,或者将一个连续的字节分解成Python对象。
  那么struct库就是执行Python值和以Python bytes表示的C语言结构体之间的转换,这样就可以处理存储在文件中或来自网络连接以及其他源的二进制数据;Python使用一定格式的字符串作为C语言结构体布局的简单描述以及Python值的预期转换

两个函数——pack()、unpack()

 struct库最重要的两个函数就是pack()、unpack()
  打包函数:
    struct.pack(fmt,v1,v2,v3,…)
  解包函数:
    struct.unpack(fmt,buffer)
 其中fmt是格式化字符(format)
  struct模块支持的格式化字符如下:

Format C Type Python 字节数
x pad byte(填充字节) no value 1
c char string of length 1 1
b signed char integer 1
B unsigned char integer 1
? _Bool bool 1
h short integer 2
H unsigned short integer 2
i int integer 4
I unsigned int integer or long 4
l long integer 4
L unsigned long long 4
q long long long 8
Q unsigned long long long 8
f float float 4
d double float 8
s char[] string 1
p char[] string 1
P void * long

struct.pack(fmt,v1,v2,v3,…)

  返回一个字节对象,该对象包含根据格式字符串打包的值v1、v2,…。参数必须与格式要求的值完全匹配。
  第一个参数是后面所有参数的格式字符串,后面的每个参数必须与格式字符串描述一致。
  举个栗子:
    h表示short,l表示long; 'hhl'表示后面有三个参数,依次是short,short,long类型
    c表示 char,bytes of length 1(长度的byte数组),i表示integer 整数;'ci'表示后面有两个个参数,依次是char,integer 类型

from struct import *

test1 = pack('hhl', 1, 2, 3)
test2 = pack('ci', b'*', 0x12131415)
print(test1,'\n',test2)

test3 = unpack('ci',test2)
print('test3->',test3)

# output:
# test1-> b'\x01\x00\x02\x00\x03\x00\x00\x00' 
# test2-> b'*\x00\x00\x00\x15\x14\x13\x12'
# test3-> (b'*', 303240213)

自己的一些理解:

这个函数就是将Python值转换成一个连续存储的、保留数据类型的字节块或者说是字节串,在内存中占用一块连续的存储空间。
转换完成后,对这个整体的值进行相应的操作。
struct库的最后:
在struct库的功能介绍中有可以处理结构体的功能,但是搜索很多资料之后很多都是只有pack()和unpack()及其相关的格式化字符串的功能,可能对于结构体的操作也仅仅是对于字符串的格式化操作,之后如果能找到具体的材料这部分还会进行更新。

ctypes库

  此部分参考连接:聊聊Python ctypes 模块
  ctypes库是Python内建的用于调用动态链接库函数的功能模块,一定程度上可以用于Python与其他语言的回合编程。动态链接库也就是DLL文件。由于编写动态链接库,使用C/C++是最常见的方式,故ctypes最常用于Python与C/C++混合编程之中。
  因为我这边是没有看过ctypes的源码所以这边仅仅是记录一下各平台使用ctypes库所调用的函数,应该说是Python调用不同平台的动态链接库最中所使用的函数:

  Windows平台下:
    最终调用的是Windows API中LoadLibrary函数和GetProcAddress函数
  Linux和Mac OS X平台下:
    最终调用的是Posix标准中的dlopen和dlsym函数

  ctypes实现了一系列的类型转换方法,Python的数据类型会包装或直接推算为C类型,作为函数的调用参数;函数的返回值也经过一系列的包装成为Python类型。

Python和C语言混合编程

  这个作用是ctypes库最为直接的一个作用,在没有合适的编辑器的情况下,特别是遇到第三方库提供动态连接库和调用文件时,Python + ctypes是一个很好的解决方案,当然这仅仅只是在中轻量级的混合编程下。
  当然,对于某种需求,在Python自身功能能够实现的情况下,应该有限使用Python本身的功能而不要使用操作系统提供的相关API接口,这样的程序将会丧失跨平台的特性。
  这里也引用一个参考文章里的例子:

//great_module.c
#include <nmmintrin.h>

#ifdef _MSC_VER
    #define DLL_EXPORT __declspec( dllexport ) 
#else
    #define DLL_EXPORT
#endif

DLL_EXPORT int great_function(unsigned int n) {
    return _mm_popcnt_u32(n);
}

  当然,你可能看不懂这段C语言,简单解释一下,源文件中只有一个函数:great_function,他会调用Intel SSE4.2指令集中的POPCNT指令,该指令封装在_mm_popcnt_u32中,作用是计算一个无符号整数的二进制表示中“1”的个数。
  调用_mm_popcnt_u32需要包含Intel指令集的头文件nmmintrin.h,就像Python需要使用第三方库中的方法时需要import一样。
  中间的ifdef……else……endif,在MSVC下,动态链接库导出的函数必须加 __declspec( dllexport ) 进行修饰。
  然后就是C语言编译成动态链接库,因为我这边没有C语言环境,所以这边只粘贴一些命令了。

Windows MSVC 下编译命令:(启动Visual Studio命令提示)
  cl /LD great_module.c /o great_module.dll
Windows GCC、Linux、Mac OS X下编译命令相同:
  gcc -fPIC -shared -msse4.2 great_module.c -o great_module.dll

  然后写一个Python调用进行测试,最终程序可以正常运行,说明Python和C语言在ctypes的支持下可以进行混合编程。

from ctypes import *

great_module = cdll.LoadLibrary('./great_module.dll')
print(great_module.great_function(13))

# output:
# 3
# 整数13的二进制为1101,所以输出为3

类型映射:基本类型

  这部分就是书中所应用的部分了,对于数字和字符串等基本类型,ctypes采用“中间类型”的方式在Python和C之间搭建桥梁,对于C类型Tc,均有ctypes类型Tm,将其转换为Python类型Tp。简单理解一下就是C type(Tc) <--> ctypes(Tm) <--> Python type(Tp),具体来说就是某动态链接库中的函数要求参数具有C类型Tc,那么在Python ctypes 调用它的时候,就给予对应的ctypes类型Tm。Tm的值可以通过构造函数的方式传递对应的Python类型Tp,或者,使用它的可修改成员Tm.value。
  Tm(ctypes type)、Tc(C type)、Tp (Python type) 之对应关系

ctypes 类型 C 类型 Python 数据类型
c_bool _Bool bool (1)
c_char char 单字符字节串对象
c_wchar wchar_t 单字符字符串
c_byte char int
c_ubyte unsigned char int
c_short short int
c_ushort unsigned short int
c_int int int
c_uint unsigned int int
c_long long int
c_ulong unsigned long int
c_longlong __int64long long int
c_ulonglong unsigned __int64unsigned long long int
c_size_t size_t int
c_ssize_t ssize_tPy_ssize_t int
c_float float float
c_double double float
c_longdouble long doubl float
c_char_p char * (NUL terminated) 字节串对象或 None
c_wchar_p wchar_t * (NUL terminated) 字符串或 None
c_void_p void * int 或 None

  多说无益,不如来个例子,最易理解的就是print相关的例子。简单说一下C语言中的输出为printf,在C语言中printf在C标准库中,在C代码中调用是标准化的,但是,C标准库的实现不是标准化的。在Windows中,printf函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于/usr/lib/libc.dylib,在Linux中,一般位于/usr/lib/libc.so.6

from ctypes import *
from platfrom import *

clib = cdll.LoadLibrary(cdll_name[system()])
clib.printf(c_char_p("Helllo %d %f"),c_int(5),c_double(2.3))

  重点在最后一行,printf的原型为int printf(fonst char * format,...),这里第一个参数用c_char_p创建一个C字符串,兵役构造函数的方式用一个Python字符串进行初始化,接下来给printf一个int型和一个double型的变量,相应的,就需要将Python值转换成C类型变量,这里就用到了c_int和c_double
  当然,还有另外一种形式,使用value成员,下面代码是等价于clib.printf(c_char_p("Hello %d %f"),c_int(15),c_double(2.3)),在具体的开发环境下,不同的实现方法要按照具体情况来,不能说哪种代码量少、哪种方法简单就使用什么样的实现方法,更多的要注意程序运行的稳定性和代码的易读性。

from ctypes import *
from platform import *

str_format = c_char_p()
int_val = c_int()
double_val = c_double()

str_format.value = "Hello %d %f"
int_val.value = 15
double_val.value = 2.3
clib.printf(str_format,int_val,double_val)

  另外,一些C库函数接受指针并修改指针所指向的值。这种情况下相当于数据从C函数流回Python。仍然使用value成员获取值。

from ctypes import *
from platform import *

cdll_names = {
            'Darwin' : 'libc.dylib',
            'Linux'  : 'libc.so.6',
            'Windows': 'msvcrt.dll'
        }

clib = cdll.LoadLibrary(cdll_names[system()])
s1 = c_char_p('a')
s2 = c_char_p('b')
s3 = clib.strcat(s1,s2)
print s1.value #ab

  最后就是当ctypes可以判断类型对应关系时可以直接将Python类型赋予C函数。ctypes 会进行隐式类型转换,反之会触发异常,为了程序的稳定性,所以说要尽量少的去使用隐式类型转换。

# 隐式类型转换
s1 = c_char_p('a')
s3 = clib.strcat(s1,'b') # 等价于 s3 = clib.strcat(s1,c_char_p('b'))
print s1.value #ab

# 触发异常
clib.printf(c_char_p("Hello %d %f"),15,2.3)

到这里呢,ctypes关于书中的部分基本上已经结束了,但是这样就算完了嘛?当然不!后面还有ctypes的高级类型映射的数组、简单类型指针、函数指针三个部分,之后这部分会重新进行整理写在另一篇中。

写在最后

  这一章基本上到这里就结束了,有人该说了:“三个库呢!还有一个呢!”,咳咳,这个目前来说内容比较多了,netaddr库的内容后续会再更新到这篇里,当然时间不会长,三四天吧。
  另外就是这章的内容,切记就是学习意义大于实际使用意义,所以重点在代码编写和三方库的学习上,不用过多纠结于诸如"这脚本为什么不能用啊?"此类的问题。