本文要分析的是带附件电子邮件的发送。发送邮件有几种方法,可以使用控件,使用MAPI,这些似乎都是基于Outlook客户端的了。但在没有安装Outlook的电脑是上就要使用套接字实现了。简单邮件的发送以前已经有人给出了,对于搞安全的人来说,如果邮件没有附件,其实是没有什么攻击力的,用纯文本来实现攻击的可能性其实非常小(至少也是需要一个网页吧)。所以这篇文章将从协议开始出发,使用套接字编写一个可以发送附件的邮件客户端。有关SMTP(简单邮件协议)的详细相关内容,请参考协议标准。下面来大概看一下这个协议。
SMTP通信模型
在互联网上使用的SMTP通信示意图如图1所示(SMTP所对应的RFC文档为RFC821)。
图1
SMTP是基于C/S模型建立在TCP/IP协议之上的通信协议,其处理的文件是基于ASCII码的,所以命令也是ASCII码。用户发送一封电子邮件的基本过程是:
1) 客户端格式化邮件,以备发送;
2) 客户端使用TCP连接至SMTP服务器;
3) SMTP服务器返回是否就绪状态,如果连接失败,邮件发送终止;
4) 现在许多服务器到要有验证登录了,所以接下来就是验证用户身份,说通俗一点就是验证用户名和密码(就是网页上登录邮箱用的那个了);
5) 服务器返回验证信息,如果验证失败,邮件发送终止;
6) 客户端发出发送请求,并将格式好的邮件传给服务器;
7) 发送完毕退出。
由上面的发送过程可以知道,客户端和服务器的连接其实是使用一问一答的联系方式的。相互之间可以识别的语言就是SMTP邮件命令了。SMTP命令相对比较少,常用的命令有以下几个(实际使用时首先连接服务端会收到欢迎信息):
HELO <domain> <CRLF>:向服务器标识用户身份,注意后面的domain 是必不可少的。
MAIL FROM:<reverse-path> <CRLF>:<reverse-path>为发送者地址,此命令用来初始化邮件传输,即用来对所有的状态和缓冲区进行初始化。
RCPT TO:<forward-path> <CRLF>:<forward-path>用来标志邮件接收者的地址,常用在MAIL FROM后,可以有多个RCPT TO。
DATA <CRLF>:将之后的数据作为数据发送,以<CRLF>.<CRLF>标志数据的结尾。
REST <CRLF>:重置会话,当前传输被取消。
NOOP <CRLF>:要求服务器返回OK应答,一般用作测试。
QUIT <CRLF>:结束会话。
以上命令均不区分大小写,返回2开头的就是命令成功得到响应,5开头表示失败,具体内容请参看有关文档。由于简单邮件协议只处理7位的ASCII文本,对身份认证也不支持,为了支持身份验证和传输二进制文件,标准化组织对简单邮件传输协议进行了扩展,扩展后的协议(MIME标准来支持)已经支持附件发送了。使用的登录命令为:AUTH LOGIN返回334表示成功。验证使用的加密方式不定,不过通常是BSASE64加密,BASE64明显不太安全(其实也没有什么安全可言了,只要截获就可以还原出密码来),所以也有采用MD5加密验证的了。随文已经提供一个用于MD5加密的动态链接库,如果有需要,可以扩展,在本程序里使用base64加密。这个扩展了的协议在使用时需要把附件用Base64进行编码。
下面说说常见电子邮件的格式,为了方便说明,首先使用Windows自带的Outlook编写一封邮件,简单写个内容,添加一个附件(我这里添加一个可执行程序),为便于分析,附件不要太大。另存在某处,用记事本打开这个文件。如果不出意外,得到的应该是类似于如下的内容:
From: "Test" 6252656757@qq.com //这个是发件人的名字,我猜可以伪造,不过估计服务端可以检测出来
To: 37510834727@QQ.COM //这个是收信人的邮箱了
Subject: TEST //邮件主题
Date: Wed, 16 Sep 2009 12:48:20 +0800 //邮件日期
MIME-Version: 1.0 //这个就是扩展协议的内容之一了,要发送带附件的邮件,这是必须的
Content-Type: multipart/mixed;
上面这个表示内容类型是MIME标准,“/”前面的是主类型,后面是子类型。具体含义如表1所示。
内容类型子类型描 述
t e x tp l a i n
r i c h t e x t
e n r i c h e d无格式文本
简单格式文本,如粗体、斜体或下划线等
r i c h t e x t的简化和改进
m u l t i p a r tm i x e d
p a r a l l e l
d i g e s t
a l t e n a t i v e多个正文部分,串行处理
p a r a l l e l 多个正文部分,可并行处理
d i g e s t 一个电子邮件的摘要
a l t e n a t i v e 多个正文部分,具有相同的语义内容
m e s s a g er f c 8 2 2
p a r t i a l
e x t e r n a l - b o d y内容是另一个RFC 822邮件报文
内容是一个邮件报文的片断
内容是指向实际报文的指针
a p p l i c a t i o no c t e t - s t r e a m
p o s t s c r i p t任意二进制数据
一个P o s t S c r i p t程序
v i d e om p e gISO 111 7 2格式
a u d i ob a s i c用8 bit ISDN律格式编码
i m a g ej p e g
g i fISO 10918格式
C o m p u S e r v e的图形交换格式
表1
下面是定义一个分隔符,用于隔开多个部分,对于有多个段的邮件是必须的。
boundary="----=_NextPart_000_0008_01CA36CB.FD2609F0"
下面这个段是Outlook自己加上去的,我们自己发送时可以忽略。X-开头的在标准里属于用户自定义内容,服务器不做检查。
X-Priority: 3
X-MSMail-Priority: Normal
X-Unsent: 1
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.5579
This is a multi-part message in MIME format.
------=_NextPart_000_0008_01CA36CB.FD2609F0
Content-Type: multipart/alternative;
boundary="----=_NextPart_001_0009_01CA36CB.FD2609F0"
------=_NextPart_001_0009_01CA36CB.FD2609F0 //注意和下面一行不要有空格
Content-Type: text/plain;
charset="gb2312"
Content-Transfer-Encoding: base64 //这一句指定Content的加密方式,这里可以不加密了,文本为了方便不加密,把加密方式设置成7bit
VGhpcyBpcyBhIHRlc3QgZmlsZSE= // This is a test file!的base64的加密内容,也就是发送的内容了
------=_NextPart_001_0009_01CA36CB.FD2609F0
Content-Type: text/html;
charset="gb2312"
Content-Transfer-Encoding: base64
……这里省略了一大部分内容,其实是base64的加密内容,这个地方标准里没有规定,估计是Outlook为了方便以网页形式阅读邮件而加上去的吧?
------=_NextPart_001_0009_01CA36CB.FD2609F0--
//下面这个就是附件的邮件体了
------=_NextPart_000_0008_01CA36CB.FD2609F0
Content-Type: application/x-msdownload;
name="FontWindow.exe"
Content-Transfer-Encoding: base64
Content-Disposition: attachment;
filename="FontWindow.exe"
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
……省略,附件base64加密后的内容
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
//结尾了,注意比前面的分隔符在后面多了“—”,这点很重要,否则不能被正确解析
------=_NextPart_000_0008_01CA36CB.FD2609F0--
通过对邮件内容进行分析,可见其格式的主要内容如表2所示。
主邮件头这里的几个域不能省略,可以作假,但估计服务端可以检测出来
邮件分隔符
第一个个内容头这个要紧挨着上面的分隔符,我试过了,只要有一个空行就有问题
第一个邮件内容
邮件分隔符
第二个邮件头注意点同上
第二个邮件体
………………可以有多个邮件体
邮件结尾符格式为“--分隔符—”
表2
从以上分析来看,发送环节相对简单,复杂的是对文件的格式化,这里为了降低编程的复杂度,分成几个类来写,一个类用于格式文本,一个类用于格式附件,第三个类将以上两个格式结果格式化为符合标准的邮件文件,最后一个类用于发送命令等套接字处理。对于Base64的加解密专门写成一个类,这个编码算法就不细说了,感兴趣的可以在网上搜索看看,网上也可以找到开源的代码。
打开VC6.0,新建一个基于对话框的程序,在里面添加所需要的几个类。为了简化发送,文本内容通过读取TXT文件获取,制作的界面如图2所示。下面那个编辑框显示格式化好的邮件内容,为了验证邮件的正确性,可以将里面的内容复制出来另存为XXX.eml,双击后操作系统将会默认调用Outlook来打开,如果显示的内容正确,就说明邮件格式正确了。
图2
理论就是以上这些了,如果还是不清楚,就把协议标准找来结合一封实际的邮件看看。下面来简单的看一下实现代码,知道原理代码就简单了。
格式化邮件文本
主要有两个函数,一个格式化文本邮件头,另一个格式邮件内容。这里为什么要格式文本内容呢?细心的读者估计在看实际邮件时就发现了,Outlook生成的邮件每行只有七十六个字符,所以要提供这个函数,免得超过可以显示的内容。
CString CFormatMainText::Format_Head(CString Conext, CString szParam)
{CString Result;
Result.Format("Content-Type:%s%s\r\n",GetConnectType(),szParam);
Result+="Connect-Transfer-Encoding:7Bit\r\n\r\n";
return Result;
}
CString CFormatMainText::FormatString(CString szInput)
{//这个函数把内容整理为每行不超过70个字符的文本,为了兼容英文单词,主要使用空格来判断
CString szAll=szInput;
CString Result="";
CString Temp=szAll.GetAt(0);
int nlPos=1;
int Seek;
if(szAll.IsEmpty())
return "";
while(nlPos<szAll.GetLength())
{
Temp+=szAll.GetAt(nlPos);
if((nlPos%70)==0)
{
Seek=Temp.GetLength();
while(Temp.GetAt(--Seek)!=' ');
Result+=Temp.Left(Seek);
Result+="\r\n";
Temp=Temp.Right(Temp.GetLength()-Seek);
}
nlPos++;
}
if(nlPos<70)
Result=szInput;
Result+="\r\n";
return Result;
}
简单说一下后面这个函数的算法,首先获得一行70个字符长的文本,如果最后一个不是空格,回溯回去直到找到空格,后面部分放下一行。格式化附件的代码如下,没有什么难度,也就是字符串操作,这个地方似乎还不太完善,如果中英文混合,好像处理不太好,这个问题就留给读者自行解决了。
格式化附件
以下函数用于读取文件并用Base64进行编码。
bool CFormatAttachFile::Attach_File(CString FilePath, CString &Result)
{
CStdioFile hFile;
Result="";
char szBuffer[BYTES_TO_READ+1]={0};
int nByteRead=0;
if(!hFile.Open(FilePath,CFile::modeRead|CFile::shareDenyWrite|CFile::typeBinary))
return false;
CBase64* Encode=new CBase64;
if(Encode==NULL)
return false;
do{
try
{nByteRead=hFile.Read(szBuffer,BYTES_TO_READ);
}
catch(CFileException e)
{OutputDebugString("读文件发生异常\n");
return false;
}
Result+=Encode->Encode(szBuffer,nByteRead);
memset(szBuffer,0,BYTES_TO_READ+1);
Result+="\r\n";
}while(nByteRead==BYTES_TO_READ);
Result+="\r\n";delete Encode;hFile.Close();
return true;
}
}
格式附件头的方法和上面类似,就不贴代码了,可以结合前面的理论看看附件里完整的代码。最后看看发送电子邮件的实现,这个就更简单了,连接服务器,发送命令,验证成功后发送数据就可以了。在获取到套接字后,写个专门发送数据的函数SendCommand(CString Cmd),这个只是简单调用send函数,再写个接收数据函数即可。
bool CSendMessage::RevcData(CString &Result, char* Need)
{
char * Buffer=(char*)malloc(MAX_BUFFER);
memset(Buffer,0,MAX_BUFFER);
recv(m_hSocket,Buffer,MAX_BUFFER,0);
if(strncmp(Buffer,Need,1)==0)
{free(Buffer);
return true;
}
free(Buffer);
return false;
}
后一个参数传进来的是命令正确执行的返回值。前面已经说了,除登录命令以外,返回2开头就正确,5就失败,所以只比较第一字节。
转自藏锋者,地址:http://www.cangfengzhe.com/wangluoanquan/1774.html