【转载】初识Scapy--Python的Scapy/Kamene模块学习之路

原文链接:初识Scapy–Python的Scapy/Kamene模块学习之路

前言

虽说自己是网络专业,但对于协议这一块,一直没有捋的很清楚,像 arp 欺骗之类的攻击方式,有所耳闻,也略知原理,但还达不到张口就来的水平,作为网络专业的学生对此真的是惭愧。

所以接下来要好好补习下网络协议的知识。把协议什么的弄清楚。之前了解到 python 的 scapy 模块很强大,正好可以跟学习协议的过程结合起来,一起学习下这个模块。

在搜索有关 scapy 这个模块入门文章时,看到了这篇,感觉写的还挺清晰的。本来想看完之后自己在总结一遍,但写了几百字之后,感觉没有这篇写的清晰。所以就直接把这篇文章转载过来吧。

Scapy/Kamene是什么?

Scapy是基于Python语言的网络报文处理程序,它可以让用户发送、嗅探、解析、以及伪造网络报文,运用Scapy可以进行网路侦测、端口扫描、路由追踪、以及网络攻击。Kamene是Scapy的分支,一开始的Scapy并不支持Python 3,为了支持Python 3就诞生了Scapy3k这个分支,为了方便区分就把Scapy3k更名为了Kamene。注意:最新的Scapy是支持Python 3的(Scapy 2.4.0+)。

Scapy 官网: https://scapy.net/)

Scapy Pypi包主页: https://pypi.org/project/scapy/

注意: 本系列文章默认你有Python基础

如何选择Python和Scapy的版本?

这是Scapy版本的兼容列表:

img

现在(2019年4月)有相当一部分Linux发行版都不预装Python 2了而改为Python 3 (Fedora 29, Ubuntu 19.04),而且Python 2也将在2020年停止维护,所以本系列文章完全使用Python 3(外加我也没有怎么学过Python 2)。

我的实验环境:

  • 运行Scapy的主机: Ubuntu 19.04 / Kali linux 2019.1a(不需要额外安装 Scapy)
  • 端口扫描测试靶机: Metasploittable2-Linux

它们都是VMware虚拟机,通过桥接模式连接到本地网络,安装VMware虚拟机的方法这里就不介绍了,通过搜索引擎就可以查找到很多资料。当然实验环境可以跟我不一样,但推荐用Linux系统运行Scapy,其他系统的安装方法可以参考官方的文档(英文)。

安装Scapy/Kamene:

安装Scapy之前需要先安装Python 3 ,有些Linux发行版已经安装好了Python 3 (Ubuntu, Kali)。安装 Python 3 网上也有很多资料了,只要输入你的操作系统加上 Python 3就可以找到。

安装完Python 3之后再建议把pip的源改为国内的镜像(如果你不在国内的话那就不用改了),改pip源可以参考这篇文章: https://www.cnblogs.com/ZhangRuoXu/p/6370107.html

编辑配置文件:

mkdir ~/.pip

vi ~/.pip/pip.conf

然后输入一下内容,把pip源设为豆瓣的镜像:

1
2
3
[global]
timeout = 60
index-url = https://pypi.doubanio.com/simple

安装Scapy:

sudo pip3 install scapy

执行完毕后输入sudo scapy验证安装是否成功,出现以下界面就表示安装成功了(注意: Scapy某些功能需用root权限):

sudo scapy

img

上面白色的警告提示说: 无法导入matplotlib和PyX模块,没有IPv6路由,没有IPython shell。我们依次安装这几个模块。目前来说我们用不到IPv6,所以不用管它。

这就是一个Python shell,所以可以用exit()退出它。

exit()

安装matplotlib:

sudo pip3 install matplotlib

安装PyX:

sudo pip3 install PyX

安装IPython:

sudo pip3 install IPython

实际上matplotlib和PyX不安装也可以,后面一般不用,要用到时我会另外再说。但是IPython有必要安装一下,安装IPython之后你会发现Python Shell明显流畅了很多,而且自动补全功能也更加完善。

安装完Scapy后我们就开始使用它吧。

Scapy shell的使用

进入Scapy shell:

sudo scapy

进入Scapy你可能发现了这和Python的shell有点像,但是颜色又有点不对。事实上这就是一个Python Shell只是更改了颜色选项,然后引入了Scapy模块。

知道这原理后我们只需要一行代码就可以构造一个Ping的请求包。

ping = IP(dst=”http://www.baidu.com“) / ICMP()

看到这个你可能会问: “你这一行代码这么就构造一个数据包数据了啊!”,先别急我们慢慢来看这个数据包是这么构成的。

我们想要灵活使用Scapy需要知道一定的网络知识,下面我们来简单了解一下这些知识。

现在的网络数据都是分层传输的,以TCP/IP协议族为例网络可分为4层,下图就是TCP/IP协议族的层次图。

img

最上面的应用层(Application Layer),然后传输层(Transport Layer),然后网络(Internet Layer),最下面一层是数据链路层(Link/Physical Layer)。

比如说QQ这个应用它处在应用层,我们发送一条消息时QQ应用会把消息放入自己定义的一个应用协议中,然后交给它的下一层传输层。传输层协议例如UDP(TCP会有一点不一样),会把自己的报文头加在数据之前,然后再交给下一层网络层。网络层的协议例如IP协议也会把自己的报文头加在数据之前,然后再交给数据数据链路层。数据链路层的协议例如以太网协议(Ethernet)还是会把自己的报文头加在数据之前(以太网协议不仅有头还有尾),最后再通过网卡发送Bit流到网络中。这个过程就就叫封装,当然有封装就有解封装,解封装就是把封装的过程倒过来。下图就是数据传输过程的图片:

img

知道了数据传输的过程,那些TCP, UDP, IP, Ethernet是什么东西啊?它们都是网络协议,网络协议就是一些标准,如果你不遵守协议或者协议和别人不一样的话那你就没办法和别人通讯。就像说不同语言的两个人没办法正常沟通一样。

我这里只是笼统概括了一下如果想深入学习的话可以看这篇文章: https://wizardforcel.gitbooks.io/network-basic/content/0.html,也可以买一些相关书籍来看。

Scapy的主要的功能就是操作各种协议的报文,所以我们接下来会学习到一些协议,但是我不会一下子讲太多协议,而是我们需要什么协议就学什么。

这里提一下报文是什么,报文就是按照一定规则组成的数据,方便构造和解析。报文头就是数据最前面的标识信息,报文 = 报文头 + 数据

好了了解这些基础知识我们继续看刚刚构造的数据包

ping = IP(dst=”www.baidu.com") / ICMP()

Scapy中的斜杠’ / ‘就是用来区分网络层次的,一般来说把底层的协议写在左边把高层的协议写在右边,就像下面这样:

数据链路层协议 / 网络层协议 / 传输层协议 / 应用层协议及数据

下面是TCP/IP协议族关系表:

img

一般来说我们不需要关心数据链路层的协议Scapy会为我们自动配置。

Scapy定义了很多函数和类,通过这些函数和类我们就可以构造数据包。这里的IP可以用来生成一个IP的报文头。

IP()

img

show()方法获取报文字段信息(在Python shell中下划线’_’表示上条语句执行结果)

_.show()

img

这里简单介绍一下IP协议。IP协议就是通过IP地址来标识互联网上的主机,我们通过IP地址就可以访问到互联网上的主机(非NAT)。IP协议有2个版本IPv4和IPv6,目前主流的还是IPv4,但是已经有很多地方在使用IPv6了,可能过个几年IPv6就是主流了。

长度(len),校验和(chksum)、协议(proto)等字段Scapy都会自动计算,所以我们只有3个字段需要注意一下,ttl、src、dst。

  • ttl: 生存时间值(Time To Live),当数据包每进行进行一次转发时TLL值就会减一,TTL为0时数据包就会被丢弃,TTL的主要作用是避免IP包在网络中的无限循环和收发,节省了网络资源,并能使IP包的发送者能收到告警消息(路由追踪原理)。
  • src: 源地址(Source Address)数据发送者的地址
  • dst: 目的地址(Destination Address)数据接收者的地址

所以这段代码就是设置目标地址:

IP(dst=”www.baidu.com")

有基础的朋友可能会说:“这明明是域名,不是IP地址!“。对,这确实是域名,但是Scapy功能非常强大,当你把dst赋值为域名时Scapy会自动调用一个名为Net的函数来解析域名。

img

还可以指定掩码位数:

list(Net(‘www.baidu.com/30'))

img

然后我们再来看看ICMP协议:

ICMP(互联网控制消息协议)是互联网协议族的核心协议之一。它用于TCP/IP网络中发送控制消息,提供可能发生在通信环境中的各种问题反馈,通过这些信息,使管理者可以对所发生的问题作出诊断,然后采取适当的措施解决(维基百科)。ping就是基于ICMP协议的。

生成一个ICMP报文头:

ICMP()

img

ICMP只有5个字段这些字段分别的意思是(维基百科):

  • type:ICMP的类型,标识生成的错误报文。
  • code:进一步划分ICMP的类型,该字段用来查找产生错误的原因。例如,ICMP的目标不可达类型可以把这个位设为1至15等来表示不同的意思。
  • chksum:校验码部分,这个字段包含有从ICMP报头和数据部分计算得来的,用于检查错误的数据,其中此校验码字段的值视为0。
  • id:这个字段包含了ID值,在Echo Reply类型的消息中要返回这个字段。
  • Seq:这个字段包含一个序号,同样要在Echo Reply类型的消息中要返回这个字段。

以下是ICMP type code的对应表:

img

一般来说只用记住这2个,type为8、code为0是ping的请求,type为0、code为0是ping的响应。

知道这些之后我们就可以构造一个ICMP请求报文头。注意Scapy会为每个报文设定默认值,调用ICMP函数默认就是构造一个ICMP请求,所以下面两条是等价的:

ICMP()

ICMP(type=8, code=0)

也可以查看对象的type,code属性值

img

通过ls函数就可以查看默认值

ls(<协议名>)

img

也可以同时查看实际值和默认值

ls(IP(dst=”http://www.baidu.com“))

没有括号的是实际值,有括号的是默认值

img

好了现在我们再回头看刚刚构造的ping请求包就应该可以明白其中的意思了:

ping = IP(dst=”192.168.40.1”) / ICMP()

注意: 为了方便演示我把地址换成了自己的网关

ICMP函数首先构造一个ping请求报文头,然后IP函数构造一个IP报文头设置目标为’192.168.40.1’,最后赋值给变量ping。

构造完数据包之后那我们就来发送数据包。

resultpkt = sr1(ping)

sr1函数发送一个数据包,再接收一个数据包,然后返回响应的数据包。发送成功会显示形如下面信息。

img

Received 3 packets意思是: 收到了3个包

got 1 answers意思是: 得到了一个应答。也就是我们需要的包。

rermaining 0 packets意思是: 剩余没有应答得包。

直接在命令行输入变量名就可以输出接收到的包信息了。

img

好了得到回显数据之后就可以通过一些方法来访问其中的数据了。

通过下标访问每个报文数据:

img

这里可能有一点难理解,简单解释一下就是:对于IP协议来说ICMP报文头和Padding实际上就是一堆数据,当我们获取IP报文时它会把IP报文头和数据一起输出。对于获取ICMP报文也是如此,输出ICMP报文头和Padding数据(报文实际上就是报文头加数据)

通过属性的形式获取字段

resultpkt[0].src

img

resultpkt[1].type

img

可以加下标也可以不加,但是推荐加上。例如我们想访问ICMP的校验和(chksum),不加标访问的是IP的校验和加下标[1]才可以访问ICMP的校验和

img

通过fields属性以字典的形式获取报文头

获取IP报文头:

resultpkt[0].fields

img

获取ICMP报文头 :

resultpkt[0].fields

img

获取Padding数据:

resultpkt[2].fields

img

既然是字典就可以通过键获取对应的值。

获取IP报文头的源地址:

resultpkt[0].fields[‘src’]

img

获取IP报文头的目标地址:

resultpkt[0].fields[‘dst’]

img

获取ICMP报文头的type值和code值:

resultpkt[0].fields[‘type’]

resultpkt[0].fields[‘code’]

img

还有一种高级的方法,通过协议下标来访问报文头:

img

就是把数字下标改为了对应协议的下标功能和数字下标基本一样,但是用协议下标可以增加程序的灵活性和可读性。

好了这就是Scapy shell的基本用法并,接下来我们讲讲Scapy数据包收发机制。

Scapy数据包收发机制

Scapy数据包发送函数:

  • sr1: 发送一个数据包并接收一个相匹配的数据包
  • srp1: 与sr1一样但是关注数据链路层(以后讲ARP协议时会用)
  • sr: 发送数据包并接收相匹配的数据包
  • srp: 与sr一样但是关注数据链路层(以后讲ARP协议时会用)
  • send: 只发送数据包不接收数据包
  • sendp: 与send一样但是关注数据链路层(以后讲ARP协议时会用)

上面提到了匹配这个概念接下来会讲解。

下图就是Scapy sr函数发收流程图:

img

解释一下就是: Scapy sr函数会把数据包批量发送到网络,再获取接收到的所有包(包括其他应用的数据包),再用发送的包和接收包的进行匹配(通过IP,端口等)把发送的数据包和它相匹配的应答包组成一个元组放入一个列表中(和列表很相似的对象),再把没有应答的包放入一个列表中(发送了数据包但是没有配到对应的接收包),最后把两个列表合成一个元组。(没有匹配到的数据包丢弃)

通过实例理解一下

生成一组数据包然后通过sr函数发送,timeout参数设置超时时间默认单位为秒,这里设置超时时间为10秒钟。

  1. result = sr(IP(dst=”10.72.1.0/24”) / ICMP(), timeout=10)

img

直接输入变量名或用type函数就可以看到返回的数据是个元组

img

通过下标访问results中的数据

img

上图通过result[0],获取有响应的包的列表,用len函数获取长度,用display方法来展示数据。

result[0]里的每个素都是元组,每个元组都是由发送的包和它相匹配的应答包组成。

img

分别访问发送的包和接收的包

img

对于单个数据包的操作就和就前面的一样了

通过result[1]访问没有相应的请求数据表列表,里面的元素就是单个数据包。

img

清楚理解Scapy的发收包机制很重要,以后的学习都是基于这个。

接下来带来一个干货。

开发一个ping扫描工具

下面代码的Git仓库地址: https://github.com/starunity/Scapy-instance

直接亮代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python3

from scapy.all import *

def pingscan(ip):
"""
pingscan(ip)
Ping the incoming IP.
ip: Pass in an IP like 192.168.1.0 or 192.168.1.0/24
"""
answer, uanswer = sr( \
IP(dst=ip) / ICMP(), \
timeout=10, verbose=False \
)

alive = []
for send, recv in answer:
if recv[ICMP].type == recv[ICMP].code == 0:
alive.append(recv[IP].src)

return alive


if __name__ == '__main__':
ip = input("Enter IP address:")
result = pingscan(ip)
for i in result:
print("{} is alive.".format(i))

第13行的sr函数里面的verbose参数是用来开启或关闭发收包详情的

默认开启的效果:

img

关闭的效果:

img

执行结果如下:

img

把原文的 ping 扫描工具改写了一下,附上自己的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from scapy.all import *
import sys
import time

def pingscan(host):
for i in range(0,4):
try:
a = time.time()
answer, uanswer = sr(IP(dst=host) / ICMP(), timeout=3, verbose=False)
b = time.time()
except OSError:
print('Ping 请求找不到主机'+host+'。请检查该名称,然后重试。')
exit()
delay = (b-a)*1000
for send, recv in answer:
if recv[ICMP].type == recv[ICMP].code == 0:
print('来自 '+str(recv[IP].src)+' 的回复:字节='+str(recv[IP].len)+' 时间='+str(int(delay))+'ms TTL='+str(recv[IP].ttl))
for send in uanswer:
if send[IP].dst == host:
print('请求超时。')

if __name__ == '__main__':
ip = sys.argv[1]
pingscan(ip)