python raw socket 介绍

因为要使用 python 底层发包模块,也就是 raw socket 发包模式,所以在此深入了解一下 python socket 通信。

涉及到的函数:

import socket
socket()
setsockopt()
sendto()
recvfrom()

因为使用的是原始套接字,所以我们不使用bind/connect函数,参照《unix 网络编程》

bind 函数仅仅设置本地地址。就输出而言,调用bind函数设置的是将用于从这个原始套接字发送的所有数据报的源IP地址。如果不调用bind,内核就吧源IP地址设置为外出接口的主IP地址。

connect函数仅仅设置外地地址,同样因为原始套接字不存在端口号的概念。就输出而言,调用connect之后我们可以把sendto调用改为write或者send调用,因为目的IP地址已经指定了。

顺便说一句,connect函数也是三次握手的发生过程,参见链接

套接字参数

官网介绍:

socket.socket([family[, type[, proto]]])

参数说明:

family:协议簇/地址簇。

最常用的就是 socket.AF_INET 了,TCP/UDP 通信均属于此类型。

PS:有时我们看到的协议常量名为 AF_xxx,有时又是 PF_xxx,可以理解为 address family 和 protocol family,实际使用中是没有区别的。一般在区分协议的时候习惯使用 PF ,而在区分地址的时候习惯使用AF。可以参照这个链接的解释:

Yes. AF_foo means address family foo, and PF_foo means protocol family foo. In Linux, they are always been the same values, I believe.

Traditionally, the PF_foo constants were used for socket(), but AF_foo in the struct sockaddr structure.

According to man 2 socket, even the (historical) BSD 4.x man page states that "The protocol family generally is the same as the address family", and subsequent standards use AF_* everywhere.

Thus, today, there really should be no difference between AF_foo and PF_foo.

除此之外常用的还有 AF_UNIX/AF_LOCAL ,代表UNIX域协议,属于IPC(进程间通信)的一种方式;AF_INET6 ,IPv6 通信。

  • socket.AF_UNIX 只能够用于单一的Unix系统进程间通信
  • socket.AF_INET 服务器之间网络通信
  • socket.AF_INET6 IPv6

type:socket的类型

官网给出的列表如下:

  • socket.SOCK_STREAM
  • socket.SOCK_DGRAM
  • socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以;其次,SOCK_RAW也可以处理特殊的IPv4报文;此外,利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头。

还有两种就是 socket.SOCK_RDM 与 socket.SOCK_SEQPACKET,基本没见过用

前两种分别代表 面向流(TCP)和面向数据报(UDP)的socket通信。

(我的理解是:SOCK_RAW = 协议头部我也自己发)

proto: 协议类型

常见的为

IPPROTO_ICMP = 1
IPPROTO_IP = 0
IPPROTO_RAW = 255
IPPROTO_TCP = 6
IPPROTO_UDP = 17

设置套接字选项

setsockopt:设置套接字选项

socket.setsockopt(level, optname, value)

具体参数查看链接

level:参数作用范围,常见的包括:

SOL_SOCKET SOL应该是指的 SOck Level ,意为套接字层选项,常见的有 SO_REUSEADDR ,可以服用处于 Time_wait 状态的端口。

IPPROTO_IP IP数据包选项,一个将要用到的是 IP_HDRINCL ,如果是TRUE,IP头就会随即将发送的数据一起提交,并从读取的数据中返回。

还有 IPPROTO_TCP 等,此处不多做介绍。

SOCKET通信例子

TCP/UDP 通信

先放两个简单的例子

UDP Server

import socket  
  
address = ('127.0.0.1', 31500)  
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  
s.bind(address)  
  
while True:  
    data, addr = s.recvfrom(2048)  
    if not data:  
        print "client has exist"  
        break  
    print "received:", data, "from", addr  
  
s.close()

UDP Client

import socket  
  
address = ('127.0.0.1', 31500)  
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  
  
while True:  
    msg = raw_input()  
    if not msg:  
        break  
    s.sendto(msg, address)  
  
s.close()  

TCP Server

import socket  
  
address = ('127.0.0.1', 31500)  
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # s = socket.socket()  
s.bind(address)  
s.listen(5)  
  
ss, addr = s.accept()  
print 'got connected from',addr  
  
ss.send('byebye')  
ra = ss.recv(512)  
print ra  
  
ss.close()  
s.close()

TCP Client

import socket  
  
address = ('127.0.0.1', 31500)  
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  
s.connect(address)  
  
data = s.recv(512)  
print 'the data received is',data  
  
s.send('hihi')  
  
s.close()  

RAW SOCKET 通信

接下来我们看一个raw socket通信的例子

import sys
import socket
from impacket import ImpactDecoder, ImpactPacket
 
def main():
 
    if len(sys.argv) < 3:
        print "Use: %s <src ip> <dst ip>" % sys.argv[0]
        print "Use: %s <src ip> <dst ip> <cnt>" % sys.argv[0]
        sys.exit(1)
    elif len(sys.argv) == 3:
        src = sys.argv[1]
        dst = sys.argv[2]
        cnt = 1
    elif len(sys.argv) ==4:
        src = sys.argv[1]
        dst = sys.argv[2]
        cnt = sys.argv[3]
    else:
        print "Input error!"
        sys.exit(1)
#print src, dst
    ip = ImpactPacket.IP()
    ip.set_ip_src(src)
    ip.set_ip_dst(dst)
 
    tcp = ImpactPacket.TCP()
    tcp.set_th_sport(55968)
    tcp.set_th_dport(80)
    tcp.set_th_seq(1)
    tcp.set_th_ack(1)
    tcp.set_th_flags(0x18)
    tcp.set_th_win(64)
 
    tcp.contains( ImpactPacket.Data("GET /att/DIYLife/41264/528 HTTP/1.1\r\nHost: 192.168.111.1\r\nAccept-Encoding: identity\r\n\r\n"))
 
    ip.contains(tcp)
 
    # Open a raw socket. Special permissions are usually required.
    s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
    s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)  // 此选项设置使用我们自己构造的IP头部
    seq_id = 0
    while cnt >= 1:
        # Calculate its checksum.
        seq_id = seq_id + 1
        tcp.set_th_seq(seq_id)
        tcp.calculate_checksum()
 
        # Send it to the target host.
        s.sendto(ip.get_packet(), (dst,80))
        cnt= cnt -1
 
if __name__ == '__main__':
    main()

看完这几个例子之后,说一下我的看法。

Socket 的作用就是封装了各种不同的底层协议,为我们提供一个统一的操作接口。使用socket通信的时候,我们只需要根据协议类型来初始化相应的socket,然后将我们需要写入的数据传入该socket即可。

因此,在初始化之后,socket为我们做了这么几件事情:

  1. 对于面向流的连接如TCP,可以帮助我们自动完成三次握手(connect函数)和四次挥手(close函数)的过程
  2. 在我们每次发送数据的时候,将我们要发送的数据根据默认或者你设置的选项包裹好包头,将其交给网卡的发送缓冲区
  3. 接受数据的时候,帮助我们去掉包头

由于不同协议都可以使用同样的接口进行发送和接受数据,因此,区分不同包头的过程都是在socket()函数中完成的。

总结

包结构图

创建四层以上的套接字

直接使用 socket.socket(socket.AF_INET,socket.SOCK_STREAM/socket.SOCK_DGRAM , socket.IPPROTO_TCP)即可,proto 可以自动推断(等价于IPPROTO_IP),也可以直接简写为s = socket.socket()

意味着我们需要填充的内容仅仅是包结构图中的 [ 数据 ] 部分的内容

创建三层套接字

  1. 自行填充TCP头/UDP头,IP头部交给内核填充

意味着我们需要填充的是包结构图中的 [ TCP包头 | 数据 ]

此时由于四层协议头部需要由我们自己填充,就有一个问题:如果是四层以上套接字的话,我们是不用告诉socket协议名的,程序会自动根据你的端口号来区分应用层协议。但是如果你填充四层协议头的话,socket就必须提前知道是什么协议,用来填充IP头部的协议字段,也就是说协议字段不能为IPPROTO_IP。

因此,我们就需要传入 socket 函数的第三个参数。例如我们要自己构造TCP包,可以用 socket.socket(socket.AF_INET,socket.SOCK_RAW , socket.IPPROTO_TCP )

  1. 自行填充 四层协议头部和IP头部(限定是IP协议)

意味着我们需要填充的是包结构图中的 [ IP包头 | TCP包头 | 数据 ] 的内容。

这个和上面那个差不多,只不过我们可以修改IP头部,一种方式是:

s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)  # 设置 IP 头部自己发送

另外一种方式是:

s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)

这两种方式应该都是仅仅限于发送IP协议,所以 Ethernet 头部的协议字段不用我们填充~

创建二层套接字

方式1:

socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))

自行填充 以太网包头

意味着我们需要填充的是上图中的 [ MAC包头 | IP包头 | TCP包头 | 数据 ] 的内容。

方式2:

socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))

使用SOCK_RAW发送的数据必须包含链路层的协议头,接受得到的数据包,包含链路层协议头。而使用SOCK_DGRAM则都不含链路层的协议头。

也即是说,需要填充的是上图中的 [ IP包头 | TCP包头 | 数据 ] 的内容。

posted @ 2018-08-16 15:15 四度 阅读(...) 评论(...) 编辑 收藏