黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第四章 使用SCAPY掌控网络(3)处理PCAP数据包
文章目录
写在前面
Wireshark和其他工具(如Network Miner)非常适合交互式地探索数据包捕获文件,但有时您会希望使用Python和Scapy分割pcap文件。一种很棒的使用场景是基于捕获的网络流量或者甚至像重放您以前捕获的流量这样简单的东西来生成模糊测试用例。
我们将对此进行稍微不同的分析,并尝试从HTTP流量中分离出图像文件。有了这些图像文件,我们将使用OpenCV(一种计算机视觉工具),尝试检测包含人脸的图像,以便缩小可能感兴趣的图像的范围。我们可以使用前面的ARP投毒脚本生成pcap文件,也可以扩展ARP投毒嗅探器,以便在目标浏览时对图像进行实时面部检测。
这个示例将执行两个分离的任务:从HTTP流量中提取图像,然后检测这些图像中的人脸。为了完成这两个任务,我们将创建两个程序,以便您可以根据手头的工作选择单独使用它们。你也可以按顺序使用这些程序,就像我们在这里所做的那样。第一个程序是recapper.py,用于分析一个pcap文件,并查找pcap文件中包含的数据流中存在的任何图像,然后将这些图像写入磁盘。第二个程序是detector.py,用于分析每个图像文件以确定它是否包含人脸,如果包含,将图像文件重新写入磁盘,并在图像中的每个人脸周围添加一个框框。
从PCAP提取图片
namedtuple
我们从编写执行PCAP分析所需的代码开始,代码中我们将使用namedtuple,一个Python数据结构,具有可通过属性查找访问的字段。标准元组使我们能够存储一系列不可变的值;几乎就跟列表一样,只是不能更改元组的值。标准元组使用数字索引访问其成员:
point = (1.1, 2.5)
print(point[0])
print(point[1])
运行结果如下图所示。
另一方面,namedtuple的行为与常规元组相同,只是它可以通过名称访问字段。这使得代码可读性更高,而且比字典更节省内存。创建namedtuple的语法需要两个参数:元组名称,以及空格分割的属性名称列表。例如,假设需要创建一个名为Point的数据结构,它具有两个属性:x和y。我们可以按照如下定义它:
Point = namedtuple('Point', ['x', 'y'])
然后,我们就可以使用代码“p=Point(35,65)”创建一个名为p的Point对象,并像引用类的属性一样引用p的属性:p.x和p.y引用特定Point namedtuple的x和y属性。这比引用常规元组中某个项的索引的代码更容易阅读。在我们的示例中,假设您使用以下代码创建了一个名为Response的namedtuple:
Response = namedtuple('Response', ['header', 'payload'])
现在,我们可以使用Response来代替引用普通元组的索引:Response.header,或者Response.payload,这将会变得非常容易理解。
recapper.py文件
代码框架
接下来的例子中我们将会使用这些方法。我们将读取一个pcap文件来重新构造我们传输过的所有图像,并将图像写入磁盘。打开recapper.py文件(没有的话就新建一个),并输入以下代码:
from requests import Response
from scapy.all import TCP, rdpcap
import collections
import os
import re
import sys
import zlib
OUTDIR = './pictures'
PCAPS = './ '
Response = collections.namedtuple('Response', ['header', 'payload'])
def get_header(payload):
pass
def extract_content(Response, content_name='image'):
pass
class Recapper:
def __init__(self, fname):
pass
def get_responses(self):
pass
def write(self, content_name):
pass
if __name__ == '__main__':
pfile = os.path.join(PCAPS, ' arper.pcap ')
recapper = Recapper(pfile)
recapper.get_responses()
recapper.write('image')
上面这段代码是整个脚本的主要逻辑框架,稍后我们将添加相关的支撑函数。我们设置了配置信息,指定输出图像的目录位置和要读取的pcap文件的位置。然后,我们定义了一个名为Response的namedtuple,它具有两个属性:数据包头和数据包有效载荷。我们将创建两个支持函数来获取数据包头,和数据包有效载荷;数据包有效载荷将与Recapper类一起使用来重建数据包中的图像。除了__init__,Recaper类还有两个方法:get_responses用于从pcap文件读取响应;write用于将响应中包含的图像文件写入到输出目录。
get_header函数
接下来我们将编写get_header函数来进一步充实上面的代码框架。
def get_header(payload):
try:
header_raw = payload[:payload.index(b'\r\n\r\n' + 2)]
except ValueError:
sys.stdout.write('-')
sys.stdout.flush()
return None
header = dict(re.findall(r'(?P<name>.*?): (?P<value>.*?)\r\n', header_raw.decode()))
if 'Content-Type' not in header:
return None
return header
get_header函数接收原始HTTP流量并分离出http头。我们通过查找从开头开始并以两个“回车符-换行符”对结束的有效载荷部分来提取http头。如果有效载荷与该模式不匹配,程序将返回一个ValueError的异常,这时我们只需将破折号(-)写入控制台并返回。否则,程序将根据解析出的有效载荷创建一个字典(头),用冒号分割,冒号之前是键名,冒号之后是键值。如果http头中没有名为“Content-Type”的键,则返回None,表示http头不包含要提取的数据。
extract_content函数
接下来,我们将实现从响应消息中提取内容的函数:
def extract_content(Response, content_name='image'):
content, content_type = None, None
if content_name in Response.header['Content-Type']:
content_type = Response.header['Content-Type'].split('/')[1]
content = Response.payload[Response.payload.index(b'\r\n\r\n') + 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)
return content, content_type
extract_content函数接收HTTP响应和要提取的Content-Type的名称。Response对象是一个namedtuple,有两个部分:header和payload。
如果内容是用gzip或deflate之类的工具编码的,我们将使用zlib模块解压缩之。对于包含图像的任何响应,http头的Content-Type属性中将包含名称image(例如image/png或image/jpg)。当http头中的Content-Type值包含image时,程序创建一个名为content_type的变量,该变量值由http头中真实的Content-Type设定。然后创建另一个变量content来保存内容本身,它是http头之后的有效载荷中的所有内容。最后,我们返回content和content_type的元组。
Recapper类
完成这两个支撑函数后,我们填充一下Recapper类(原书中用了“方法(methods)”,明显不合适,从上面的代码框架中可以看出Recapper是一个类):
class Recapper:
def __init__(self, fname):
pcap = rdpcap(fname)
self.sessions = pcap.sessions()
self.responses = list()
首先,我们用要读取的pcap文件的名称初始化对象。然后我们利用Scapy的一个炫酷的特性,将每个TCP会话自动分离到一个包含每个完整TCP流的字典中。最后,我们创建一个名为responses的空列表,我们将用pcap文件中的response来填充这个列表。
get_responses方法
在get_responses方法中,我们将遍历数据包以查找每个单独的响应,并将每个响应添加到数据包流的响应列表中:
def get_responses(self):
for session in self.sessions:
payload = b''
for packet in self.sessions[session]:
try:
if packet[TCP].dport == 80 or packet[TCP].sport == 80:
payload += bytes(packet[TCP].payload)
except IndexError:
sys.stdout.write('x')
sys.stdout.flush()
if payload:
header = get_header(payload)
if header is None:
continue
self.responses.append(Response(header=header, payload=payload))
在get_responses方法中,我们遍历会话字典,然后遍历每个会话中的数据包。我们过滤流量,并且只获取目的地或源端口为80的数据包。然后,我们将所有流量的有效载荷连接到一个称为payload的完整的缓冲区中。这实际上与右键单击Wireshark中的数据包并选择Follow TCP Stream相同。如果我们没有成功地附加到payload变量(很可能是因为数据包中没有TCP),我们会将x打印到控制台并继续。
然后,在重新组装HTTP数据之后,如果有效负载字节字符串不为空,我们将其传递给HTTP头解析函数get_header,这使得我们能够单独检查HTTP头。最后,我们将Response附加到响应列表。
write方法
最后,我们遍历响应列表,如果响应包含图像,则使用write方法将图像写入磁盘:
def write(self, content_name):
for i, response in enumerate(self.responses):
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:
f.write(content)
提取工作完成后,write方法只需迭代响应,提取内容,然后将内容写入文件。该文件在输出目录中创建,其名称由enumerate内置函数的计数器和content_type值组成。例如,生成的图像名称可能是ex_2.jpg。当我们运行程序时,将会创建一个Recapper对象,调用它的get_responses方法来查找pcap文件中的所有响应,然后将从这些响应中提取的图像写入磁盘。
从图片识别并标注人脸
本节中,我们将检查每个图像以确定其中是否包含人脸。对于每个包含人脸的图像,我们将把图像重新写入磁盘,并在图像中的人脸周围添加一个框。创建一个名为detector.py的新文件:
import cv2
import os
ROOT = './pictures'
FACES = './faces'
TRAIN = './training'
def detect(srcdir=ROOT, tgtdir=FACES, train_dir=TRAIN):
for fname in os.listdir(srcdir):
if not fname.upper().endswith('.JPG'):
continue
fullname = os.path.join(srcdir, fname)
newname = os.path.join(tgtdir, fname)
img = cv2.imread(fname)
if img is None:
continue
gray = cv2.cvColor(img, cv2.COLOR_BGR2GRAY)
training = os.path.join(train_dir, 'haarcascade_frontalface_alt.xml')
cascade = cv2.CascadeClassifier(training)
rects = cascade.detectMultiScale(gray, 1.3, 5)
try:
if rects.any():
print('Got a face')
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:
cv2.rectangle(img, (x1, y1), (x2, y2), (127, 255, 0), 2)
cv2.imwrite(newname, img)
if __name__ == '__main__':
detect()
检测函数接收源目录、目标目录和训练目录作为输入。它遍历源目录中的JPG文件。(因为我们正在查找人脸,所以这些图像都是图片,很可能保存为.jpg文件。)然后,我们使用OpenCV计算机视觉库cv2读取图像,并加载检测器XML文件,然后创建cv2人脸检测器对象。该检测器是一种分类器,它预先经过训练,以检测面向前方的人脸。OpenCV包含用于侧面人脸检测的分类器,也包括手、水果等一整套其他的对象,大家可以自己尝试。对于找到人脸的图像,分类器将返回与在图像中检测到人脸的位置相对应的矩形坐标。然后,程序将信息打印输出到控制台,在人脸周围绘制一个绿色框,然后将图像写入输出目录。
从检测器返回的rects数据的格式为(x,y,width,height),其中x,y值提供矩形左下角的坐标,而width、height值对应矩形的宽度和高度。
程序使用Python切片语法进行形式转换。我们将返回的rects数据转换为实际坐标:(x1,y1,x1+width,y1+height)或(x1、y1、x2、y2)。这是cv2.rectangle方法期望的输入格式。
上述代码由Chris Fidao慷慨地分享在http://www.fideloper.com/facial-detection/上。此示例对原始示例进行了微小的修改。现在,让我们把这一切都带到你的kali虚拟机里面。
小试牛刀
准备工作
如果您尚未安装OpenCV库,请从Kali VM中的终端运行以下命令(再次感谢,Chris Fidao):
$ sudo apt-get install libopencv-dev python3-opencv python3-numpy python3-scipy
这将会安装所有必要的文件,以便在生成的图像上处理面部检测。我们还需要抓取面部检测训练文件,如下所示:
$ wget http://eclecti.cc/files/2008/03/haarcascade_frontalface_alt.xml
将下载的文件复制到我们在detector.py中的TRAIN变量中指定的目录。现在为输出创建几个目录,放入PCAP,然后运行脚本。这应该类似于以下内容:
#:> mkdir -p ./pictures
#:> mkdir -p ./faces
运行recapper.py脚本
通过如下命令运行脚本:
#:> python recapper.py
但是运行recapper.py脚本的时候,貌似并没有解析出照片,如下图。
难道是我抓的包不够多?修改一下脚本,多抓点试试看(这次抓2000个包),这次抓出来的包明显大了一圈,看来有戏,如下图。
再次运行recapper.py脚本,结果报错了,如下图(忽略Blowfish的算法告警)。
看一下脚本的第18行,对比一下原书的代码,我没有敲错,看来这里有问题。我直接执行原书网站下载的代码试试看,同一个pcap包,解析出了4张图片,并且代码运行到最后也是报错了,如下图所示。
到对应的pictures目录下看了一下,果真解析出了4张图片,如下图。
那就先打开看看抓出来了什么怪物吧,结果凉凉了,所有图片都打不开,提示如下的错误。
原书代码也运行不了,稍等看看是怎么回事。有点奇怪的是,原书也有“header_raw = payload[:payload.index(b’\r\n\r\n’)+2]”但是没有报那个TypeError(其实是我敲代码出了乌龙,读者自己找答案吧,我实在没脸在这里提),直接BeyondCompare比较一下取长补短吧。发现我原来敲入的代码里面有两个错误,一个是上面提到的错误,另一个是有几行代码中的responses被我写成了response,尴尬啊,修改后重新运行代码,如下图。
这次跟原书代码运行结果一致了,不过还是存在两个问题:一是上图中的错误,二是提取的图片仍然打不开,尝试了半天没搞定,暂时放一边,后面再研究
说明:由于这里提取照片没有成功,在下半节中是识别并标注人脸的示例中,我是随便从互联网上下载了一下图片,如有侵权请联系我
附上完整的代码。
from scapy.all import TCP, rdpcap
import collections
import os
import re
import sys
import zlib
OUTDIR = './pictures'
PCAPS = './'
Response = collections.namedtuple('Response', ['header', 'payload'])
def get_header(payload):
try:
header_raw = payload[:payload.index(b'\r\n\r\n')+2]
except ValueError:
sys.stdout.write('-')
sys.stdout.flush()
return None
header = dict(re.findall(r'(?P<name>.*?): (?P<value>.*?)\r\n', header_raw.decode()))
if 'Content-Type' not in header:
return None
return header
def extract_content(Response, content_name='image'):
content, content_type = None, None
if content_name in Response.header['Content-Type']:
content_type = Response.header['Content-Type'].split('/')[1]
content = Response.payload[Response.payload.index(b'\r\n\r\n')+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)
return content, content_type
class Recapper:
def __init__(self, fname):
pcap = rdpcap(fname)
self.sessions = pcap.sessions()
self.responses = list()
def get_responses(self):
for session in self.sessions:
payload = b''
for packet in self.sessions[session]:
try:
if packet[TCP].dport == 80 or packet[TCP].sport == 80:
payload += bytes(packet[TCP].payload)
except IndexError:
sys.stdout.write('x')
sys.stdout.flush()
if payload:
header = get_header(payload)
if header is None:
continue
self.responses.append(Response(header=header, payload=payload))
def write(self, content_name):
for i, response in enumerate(self.responses):
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:
f.write(content)
if __name__ == '__main__':
# pfile = 'arper.pcap'
pfile = os.path.join(PCAPS, 'arper.pcap')
recapper = Recapper(pfile)
recapper.get_responses()
recapper.write('image')
运行detector.py脚本
接下来运行一下人脸识别脚本,这时候会报找不到模块cv2,如下图所示。
通过pip安装一下opencv-python和opencv-contrib-python,然后再次运行指令,报错了,如下图。
后来定位到是有一行代码引用jpeg文件路径的时候,引用错了(对应路径下根本没有那个文件,又是我敲错了。。。),修改后继续运行,有报错了,如下图。
仔细检查,发现引用cv2的cvtColor函数的时候,少打了个t(太丢人了),修改后继续运行,正常运行了,如下图。
有些文件提示木有发现人脸(这个是有可能的,一方面我放了一些动画人脸在里面,另一方图像识别本来就存在误差),通过比对发现木有被识别的图片如下。
太不应该了,我喜欢的小宋佳竟然没有被识别,我猜测主要原因是这些人脸都不够“正”,这不是这本书的重点,以后再研究。
接下来看看对于识别到的人脸,是否都做了标注,如下图。
基本上都被标记出来了,达到预期。
说明:原书的代码中有一点不好合适的地方,如果判断文件名不是以JPG结尾的,直接跳过去了,其实可以优化一下,如果不是JPG结尾,并且也不是JPEG结尾再跳过去,这样能够多识别到许多照片。
读者可能会看到OpenCV生成许多错误消息,这是因为我们输入其中的一些图像可能已损坏或部分下载,或者其格式可能不受支持。
这项技术可以用来确定受害者在看什么类型的内容,以及通过社会工程发现可能感兴趣的内容。当然,我们可以将此示例扩展到其它场景,并将其与后面章节中描述的网络爬虫和解析技术结合使用。
最后,附上完整的可运行的人脸识别代码。
import cv2
import os
from numpy import full
ROOT = '/home/kali/bhp/pictures'
FACES = '/home/kali/bhp/faces'
TRAIN = '/home/kali/bhp/training'
def detect(srcdir=ROOT, tgtdir=FACES, train_dir=TRAIN):
for fname in os.listdir(srcdir):
if not fname.upper().endswith('.JPG'):
if not fname.upper().endswith('.JPEG'):
print(f'not a jpg file :\n {fname}')
continue
fullname = os.path.join(srcdir, fname)
newname = os.path.join(tgtdir, fname)
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')
cascade = cv2.CascadeClassifier(training)
rects = cascade.detectMultiScale(gray, 1.3, 5)
try:
if rects.any():
print('Got a face')
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:
cv2.rectangle(img, (x1, y1), (x2, y2), (127, 255, 0), 2)
cv2.imwrite(newname, img)
if __name__ == '__main__':
detect()