君は春の中にいる、かけがえのない春の中にいる.

你驻足于春色中,于那独一无二的春色之中.

SMTP协议附件数据流抓取与还原

SMTP作为一种广泛使用的文件传输协议,已经和POP等协议一同构成了电子邮件系统传输的基石,针对于协议的分析也有很多书籍和博文可以参考,但是在这当中,对于MIME附件的数据流还原能找到的参考不多,所以在这里记下实践心得。

0x00 基础知识

1.数据包的层级关系

SMTP通过TCP协议进行传输,在这之前不得不再复习一下数据包的构成关系。

一个典型的SMTP包将会由一下几部分构成:

Ethernet{首部,数据部(IP{首部,数据部(TCP{首部,数据部})})}

从上面的结构可以看出数据链路层、网络层、传输层上面的协议是嵌套关系,高层次的完整数据包作为底层次协议中的数据部。

打个比方,如果我们要发送“Hello World!”的信息,正文信息将会在编码后加上TCP头成为TCP包,而TCP包会作为IP包的数据部,和IP头组成IP包,以此类推,以太网包就是首部加上IP包。

不同的层次被赋予了不同的职责,我们要传送的实体信息是一切的基石,TCP头则负责告知收发双方的端口号并通过序号和校验保证数据的顺序和完整性;IP头负责告知收发双方的IP地址;eth头负责告知收发双发的MAC地址。

2.SMTP协议

SMTP协议的具体含义网上可以很轻易的得到。

这里插一句,早期的SMTP实现过程是这样的,发送方写好一份邮件存在自己的硬盘上,通过TCP连接直接将邮件转到接收者的硬盘中,如果发送成功,发送者的硬盘就会删除邮件。否则,将会等待重发。

很明显,上述实现过程在现代复杂网络中已经显得十分简陋,其他的先不说,这种机制首先必须要求收发方都要在线,这大大制约了电子邮件的发送。因此,邮件服务器就应运而生,二十四小时持续运转的邮件服务器可以使得邮件的发送不用在收到时间的限制。电子邮件系统雏形也就这样诞生了。

由于需要与邮件服务器交流,便产生了相应的操作指令和状态码,详细的操作指令和状态码也可以很容易的从网络获得。今天,当我们发送邮件时,邮件客户端帮我们完成了SMTP指令的交互,我们只需要关系邮件实体的相关内容即可。

3.MIME格式

最初的邮件只能传输文本,这显然无法满足我们的交流需求。MIME的出现就是为了丰富邮件传输的类型,与此同时,我们可以在邮件附件中添加图片、动画、声音和程序。

MIME主要由首部和正文组成,通过“Content-Type”确定发送富文本的类型,常用的类型有:

text/plain  纯文本
mutipart/mixed  多部分组成
application/octet-stream  二进制数据
image/jpeg  jpeg图像
video/mpeg  mpeg动画

当声明为多部分组成时,通过“boundary”来进行分割。

0x01 邮件发送

为了便于我们进行分析,我们在本地的另一台Ubuntu主机上搭建SMTP服务器,通过Python程序在本机上发送邮件到Ubuntu主机上,这样就能实现一个可控的操作环境。

在Ubuntu上安装Postfix和SMTP服务

apt-get install postfix
apt-get install mailutils

完成之后,我们可以在本机上使用

telnet Ubuntu的IP 25

来验证是否搭建成功

搭建完成后我们来编写一个可以传输MIME附件的Pyhton程序,关键代码如下:
需要的Python库

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from email.header import Header

先初始化发送邮箱,邮件服务器,邮箱认证账号密码

def init():  
    sender = 
    smtpserver = 
    username = 
    password = 
    return sender, smtpserver, username, password

发送部分调用

msg = MIMEMultipart('alternative')

在msg中添加邮件来源、主题、正文内容。附件部分这样处理

att = MIMEText(open('1.jpg', 'rb').read(), 'base64', 'utf-8')
att["Content-Type"] = 'application/octet-stream'
att["Content-Disposition"] = 'attachment;filename="1.jpg"'
msg.attach(att)

信息添加完整后,调用smtp来发送

smtp = smtplib.SMTP()
smtp.connect('邮件服务器地址')
smtp.login(username, password)
smtp.sendmail(sender, receiver, msg.as_string())
smtp.quit()

整个实践的基础环境就可用了。

0x02 附件数据流抓取与解析

同样使用Python来编写(╮(╯▽╰)╭,Python用的熟),主要从网卡抓取到流量,先还原为完整的数据包,再做编码转换,将一个完整的邮件发送过程中的命令交互和邮件内容分别还原出来。

程序监听网卡后,过滤出端口为25的tcp协议,过滤后的数据包大体结构如下所示:

Ethernet(dst='xxx', src='xxx', data=IP(len=78, id=44381, off=16384, p=6, sum=52034, src='xxx', dst='xxx', opts='', data=TCP(sport=25, dport=45675, seq=175475907, ack=2692538927L, flags=24, win=229, sum=40326, opts='', data='220 localhost ESMTP Postfix (Ubuntu)\r\n')))

和我们在基础知识中提到的数据包结构一样,这里完整的获得了从Ethernet到TCP层的数据。这一条抓取到是Ubuntu邮件服务器发送给我们的欢迎信息。当然,大概了解了抓取到的包解构后,略过其他重要信息,我们直奔主题,去寻找数据包中附件的部分。

为了便于展示,这里使用Wireshark重复再抓取一次电子邮件的包,可以从图中看到,这些被分割成相同的1440bytes大小的数据包就是我们所传的图片附件,而在我们的程序中,它是接下来这个样子的,数据较长,这里截取一部分。

Content-Type: multipart/alternative; 

boundary="===============0786123174=="\r\n

MIME-Version: 1.0\r\n

From: \r\n

Subject: \r\n\r\n
--===============0786123174==\r\n
Content-Type: text/plain; 
charset="us-ascii"\r\n
MIME-Version: 1.0\r\n
Content-Transfer-Encoding: 7bit\r\n\r\n123456\r\n
--===============0786123174==\r\n
Content-Type: image/png\r\n
MIME-Version: 1.0\r\n
Content-Transfer-Encoding: base64\r\n
Content-ID: application/octet-stream\r\n


iVBORw0KGgoAAAANSUhEUgAAAKoAAA
FHCAYAAAAx29dqAAAgAElEQVR4nKy9d5T
c2XXfeSt0d3XO\r\nOQNooLuR02ByYpjhcEi
KSTYlS5S0+4dE7ZElK9iWLC+8Pmd3La20K1
nBa3ltr5UlUiaXEidxhhPA\r\nmUEaAING6Eb
onFN1d3VVdcW9n/v7vUIDA1IkjwqnUNW/
+oX37vvefN97gf/9f/2/89PT01JdXSWV\r\n
lZWytLwsHR0dsh5dk83NTVldXZWmpiZpa
GiQYDAonDs2NiadnZ2yrOc+9NBDcvvWL
akor5BUKmXn\r\nXbly
.............
.............
.............
l7wVOj83I4AAAAASUVORK5CYII=\r\n
--===============0786123174==--\r\n
.\r\n

简单分析一下,在这里,我们传递的真是多MIME的邮件,第一个分隔符以上说明是多部分合在一起,第一段内是邮件的发送者信息和标题,第二段是我们传递的邮件正文,第三段是附件。重点再看一下第三部分,第三部分首先声明了附件类型、编码方式,紧接着就是文件二进制的base64编码。

通过上述分析,为了能够还原出附件,我们大致需要进行以下步骤:

  1. 准确分离出一封完整的邮件
  2. 准确分离出附件段
  3. 从附件段分离出附件的base64编码正文
  4. 将base64解码为二进制文件
  5. 获取当前文件类型,保存二进制文件为当前文件类型

一次完整的发送过程包括若干次完整的包,我们需要保存这一次发送中的若干个包,这就需要我们在捕捉到的多次发送包中先匹配出邮件开始和结束的标志,从上面可以看到,邮件结束时都使用一个单行的“.”来标志。我们匹配出完整的邮件包后,根据边界标志来匹配出附件部分的文件类型声明和编码正文,得到正文后我们使用Python的base64模块进行解码,将解码后的文件存成相应类型,就基本完成了附件的还原。

0x03 存在的问题

上面对于一次单一的发送行为做了附件还原,但其实还有很多问题有待解决。

  1. TCP发送过程中存在重复发送和乱序的问题,需要一定的机制去做判断,暂时想到的方法是使用TCP包中的Seq和Ack参数按照包编号和大小去逐个递增查找
  2. 多个发送行为存在的时候,如何在可能交叉的发送包中匹配出一次完整的发送包
  3. 当发送程序使用不同的编码时,还需要获取到编码方式再对应进行解码

接下来将会侧重于这部分问题解决方法的探索~~