注:本文已发表在2008年第10期《黑客防线》,转载请注明来源。
对于编程实现过滤文件传输这个目标,我们初步将其分为两种情况:本地传输和网络传输。
本地传输即禁止将受保护文件复制到U盘、移动硬盘等可移动设备,该任务使用API HOOK即可完成;而通过网络传输文件的方法太多,我们仅考虑三种典型情况:Web上传、Email附件、FTP上传,只要试图使用这三种方式将文件传送出去,就要将其内容进行拦截并对相关数据加密。本文只讲解网络传输文件的过滤方法。
由于进度要求,我们采用了SPI,即编写一个分层服务提供者(LSP)DLL,将其安装到系统协议链中,这样即可拦截到所有基于Winsock API的应用程序数据。
在此DLL中我们导出了31个WSP系列函数,不过大多数函数都无需进行任何处理,直接将其转发给协议链的下一个结点即可,但是我们必须导出WSPStartup,在这个函数的末尾给出了其他WSP函数的入口地址分派表。
如果我们已经将自己的LSP安装到了系统协议链中,则凡是基于Winsock API的调用最终都会被我们所拦截,例如send和WSASend函数就会被我们的WSPSend函数拦截,这样我们就在数据被发送出本机前得到了一个过滤的机会。
由于send或WSASend函数中的数据是应用层数据,因此我们可以省掉繁琐的数据报解析工作,这也算是一个好处吧。
为了过滤HTTP、SMTP、FTP数据流中可能存在的文件传输,我们必须理解它们各自是怎样实现文件传输的,然后才能对症下药。
FTP原理
把FTP放在最开始是因为它最为简单,因为FTP本身就是为文件传输而生。
思路:FTP会首先建立一条控制连接,在进行数据传输前会通过发送PORT命令协商一个IP(通常为发送者自己的IP,但FTP也支持第三方传输)和端口建立一条数据连接,数据连接的功能通常有三种:上传文件、下载文件、列举目录。
当数据连接建立好后客户端就可以发送命令来确定是要执行何种动作,例如客户端发送了STOR或STOU命令,则服务端即知道客户端要请求上传文件,如果服务端同意了此请求,它们双方就会通过刚才建立的数据连接进行文件传输,当一个文件传输完毕后该连接会关闭,然后协商重新建立一条新的数据连接。
因此我们可以通过设置一个static全局变量记录每次检测到的双方协商IP和端口,并检查其后续发送的命令,如果是STOR或STOU,则刚刚修改过的全局变量就记录了双方要进行文件传输的连接IP和端口;有了该IP和端口就可以在WSPConnect或WSPAccept来记录其对应的套接字SOCKET,并将该套接字对应的所有内容进行加密。
HTTP原理
思路:在HTTP首部中,如果Content-Type字段的值为“multipart/form-dat”则说明可能有文件上传,此时它的格式通常比较固定,如“Content-Type: multipart/form-data;boundary=----------------------------7d429871607fe”;此时boundary=后面的值“---------------------------7d429871607fe”给出了在数据传输过程中至关重要的临界值,在正文中就是通过该临界值将不同内容分隔开的。
下面是一段典型的正在上传文件的数据包:
-----------------------------7d429871607fe
Content-Disposition: form-data; name="file1";filename="C:/homepage.txt"
Content-Type: text/plain
一天到晚游泳的鱼
-----------------------------7d429871607fe
Content-Disposition: form-data; name="filename"
filename
-----------------------------7d429871607fe
通过上面内容很容易可以看出,正在传送的文件本地路径是“C:/homepage.txt”,且其内容是文本格式,其内容为“一天到晚游泳的鱼”(蓝色粗体部分),文件内容遇到下一个临界符前结束。第二段内容只是个普通表单,并非文件,所以它后面不会有“Content-Type: text/plain”字段。该字段主要有两种情况:当上传的为文本文件时它的值为text/plain,其他文件时其值为“application/octet-stream”,即“Content-Type: application/octet-stream”。
不过需要注意的是,IE与Firefox发送数据包的方式不大相同,在Firefox的数据包中仅有文件名,如“homepage.txt”,而不像IE中会给出完整的本地路径;IE发送的数据包较为集中,通常如果内容不是很多的话一个包就可以发送完毕,而在Firefox中,它会首先发送HTTP首部,然后把正文中的内容一段一段发送出去。
SMTP原理
原理:使用SMTP发送附件时要用到MIME,它上传文件的过程与HTTP很相似,都是在正文中通过临界值将不同的段内容分隔开,boundary字段给出了临界值。
Content-Type:确定了每一部分内容的类型,它的值为text/plain时说明为文本,application/octet-stream说明为二进制内容;如果它后面出现了“Content-Disposition: attachment;”说明该段的内容是附件内容,紧接着“Content-Disposition”就会给出文件名。然后一个空行,后面就是附件的内容了,不过是经过编码的。编码的方式可以通过“Content-Disposition: attachment;”前面一句 “Content-Transfer-Encoding:”来确定,通常如果附件是二进制文件,它的值是“base64”,文本文件的值则为“quoted-printable”。附件的内容也是直到遇到临界值就会结束,与HTTP比较类似。
下面是一段典型的SMTP发送附件的数据包:
Message-ID: <008201c8f15e$6d63b550$c5a512de@1FB978629C104D4>
From: "grayfox" <peiyaoqiang@126.com>
To: <peiyaoqiang@yahoo.com.cn>
Subject: test
Date: Tue, 29 Jul 2008 17:35:22 +0800
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----=_NextPart_000_007E_01C8F1A1.7A9876A0"
X-Priority: 3
X-MSMail-Priority: Normal
X-Mailer: Microsoft Outlook Express 6.00.2900.3138
X-MimeOLE: Produced By Microsoft MimeOLE V6.00.2900.3198
This is a multi-part message in MIME format.
// 这中间的内容无关紧要,又大量浪费版面,所以我将其删除了
------=_NextPart_000_007E_01C8F1A1.7A9876A0
Content-Type: application/octet-stream;
name="encode.cpp"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="encode.cpp"
abcdefghijklmnopqrstuvwxyz
------=_NextPart_000_007E_01C8F1A1.7A9876A0--
通过上面的内容可以看出,正在传输的文件属于二进制文件,文件名为“encode.cpp”,编码方式是“quoted-printable”,属于附件内容,文件内容为“abcdefghijklmnopqrstuvwxyz”。另外如果我们细心观察可以发现,在HTTP中每一段的开始与结束临界符都是一样的,但在SMTP中,结束临界符却比开始临界符多了两个“--”。
关于这三种协议上传文件的原理就简单介绍到这里,下面开始让代码说话。
首先看WSPSend函数,所有要发送出的数据都要经过这里,因此最好的方法就是在这里进行检查和判断。
int WSPAPI WSPSend(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine,
LPWSATHREADID lpThreadId,
LPINT lpErrno)
{
char *p;
if((bIsFind==FALSE)&&(p=m_strstr(lpBuffers->buf,"boundary=/"----=_NextPart_",
lpBuffers->len))) // SMTP上传
{
datasock=s;
bIsFind=TRUE;
char temp[50];
sscanf(p,"boundary=/"%[^/"]",temp); //提取标志
sprintf(CriticalSymbol,"--%s",temp);
strcpy(FinalSymbol,CriticalSymbol);
strcat(CriticalSymbol,"/r/n");
strcat(FinalSymbol,"--/r/n");
strcpy(EndSymbol,CriticalSymbol);
CriticalLength = strlen(CriticalSymbol);
iProto=PROTO_SMTP;
}
if((bIsFind==FALSE)&&(p=m_strstr(lpBuffers->buf,"boundary=-----------------------",
lpBuffers->len))) // HTTP上传
{
datasock=s;
bIsFind=TRUE;
char temp[50];
sscanf(p,"boundary=%s/r/n",temp); //提取标志
sprintf(CriticalSymbol,"--%s",temp);
strcpy(FinalSymbol,CriticalSymbol);
strcat(CriticalSymbol,"/r/n");
strcat(FinalSymbol,"--/r/n");
strcpy(EndSymbol,CriticalSymbol);
CriticalLength = strlen(CriticalSymbol);
iProto=PROTO_HTTP;
}
if((bIsFind==FALSE)&&(strstr(lpBuffers->buf,"STOR"))) // FTP上传
{
datasock=s;
bIsFind=TRUE;
iProto=PROTO_FTP;
}
switch(iProto)
{
case PROTO_SMTP:
{
if(s==datasock)
SMTPFilter(lpBuffers->buf,lpBuffers->len);
else
{
datasock=0;
bIsFind=FALSE;
iProto=0;
}
}break;
case PROTO_HTTP:
{
if(s==datasock)
HTTPFilter(lpBuffers->buf,lpBuffers->len);
else
{
datasock=0;
bIsFind=FALSE;
iProto=0;
}
}break;
case PROTO_FTP:
if(s!=datasock)
{
Ftphost_datasock=s;
FTPFilter(lpBuffers->buf,lpBuffers->len);
}
break;
}
// 最后要把数据递交给协议链的下个结点
return g_NextProcTable.lpWSPSend(s,lpBuffers,dwBufferCount,
lpNumberOfBytesSent,dwFlags,lpOverlapped,lpCompletionRoutine,lpThreadId,lpErrno);
}
在该函数中的主要任务就是检查是否有文件上传动作,它主要是通过检查关键字发现的,如果有文件上传动作则调用相关函数进行过滤。
//FTP上传文件数据过滤
int FTPFilter(char *buf,int len)
{
DataCipher(buf,len);
return 0;
}
FTP的函数最为简单,因为它的文件传输是专门通过一条数据连接完成的,它传输的内容里面除了文件内容没有其他多余的东西。
下面我们来看HTTP上传文件的过滤函数。
//WEB上传数据过滤
int HTTPFilter(char *buf,int len)
{
char *start,*end;
int buflen=0;
if((bIsFind_start==FALSE)&&(start=m_strstr(buf,CriticalSymbol,len)))
{
if(start=m_strstr(start,"Content-Type: ",len-(start-buf)))
{
bIsFind_end=FALSE;
bIsFind_start=TRUE;
char temp[50];
sscanf(start,"Content-Type: %s/r/n/r/n",temp);
start+=strlen("Content-Type: /r/n/r/n")+strlen(temp); //找到文件开头
int buflen=len-(start-buf);
if(end=m_strstr(start,EndSymbol,buflen)) //找到文件结尾(开头和结尾在同一个包内)
{
bIsFind_start=FALSE;
bIsFind_end=TRUE;
buflen=end-2-start;
}
else if(end=m_strstr(start,FinalSymbol,buflen))
{
bIsFind_end=FALSE;
bIsFind_start=FALSE;
bIsFind=FALSE;
iProto=0;
datasock=0;
buflen=end-2-start;
if(buflen>0)
{
DataCipher(start,buflen);
}
return 0;
}
if(buflen>0)
{
DataCipher(start,buflen);
}
if(bIsFind_end==TRUE) HTTPFilter(end,len-(end-buf));
}
}
else if(bIsFind_start == TRUE)
{
if(end=m_strstr(buf,EndSymbol,len)) //找到文件结尾(开头和结尾不在同一个包内)
{
bIsFind_start=FALSE;
bIsFind_end=TRUE;
buflen=end-2-buf; //数据部分的长度
}
else if(end=m_strstr(buf,FinalSymbol,len))
{
bIsFind_end=FALSE;
bIsFind_start=FALSE;
bIsFind=FALSE;
iProto=0;
datasock=0;
buflen=end-2-buf;
if(buflen>0)
{
DataCipher(buf,buflen);
}
return 0;
}
else if(bIsFind_end == FALSE) //包内全部是文件数据
{
buflen=len;
}
if(buflen>0)
{
DataCipher(buf,buflen);
}
if(bIsFind_end==TRUE) HTTPFilter(end,len-buflen-2);
}
else if(end=m_strstr(buf,FinalSymbol,len))
{
bIsFind_end=FALSE;
bIsFind_start=FALSE;
bIsFind=FALSE;
iProto=0;
datasock=0;
return 0;
}
return 0;
}
这个函数相对来说就复杂的多了,因为它要分多种情况。不仅要考虑同一个包内可能有多个文件正在上传的情况,还要考虑如果文件内容太大时开始临界符和结束临界符可能不在同一个包内的情况。这个说起来比较复杂,需要自己多抓包测试才能真正理解。
还有需要注意的一个问题,这里我们不能使用常规的strstr函数来查找字符串,因为如果正在上传的文件是二进制文件时,它其中可能存在着许多的0x00字符(不信你用十六进制编辑器打开一个RAR文件看看),这样会使得我们只能找到文件开头,但找不到结尾,也找不到结束临界符。只有自己写一个字符串查找函数,如下所示:
char* m_strstr(char* buf,char* str,int buf_len)
{
char* p=buf;
char* result=NULL;
int len=strlen(str);
while(buf_len>=len)
{
if(memcmp(p,str,len)==0)
{
result=p; //下标值
break;
}
else
{
p++;
buf_len--;
}
}
return result;
}
SMTP过滤函数SMTPFilter与HTTPFilter十分相似,因为它们的工作原理就比较接近,不同的是在SMTP中得到的文件内容并非原文,而是经过base64或quoted-printable编码后的内容,因此我们需要先解码、再加密、最后再将其编码。base64编码解码的代码我就不贴了,网上很多,下面给出quoted-printable解码的函数。
int DecodeQuote(const char *pSrc, unsigned char *pDst, int nSrcLen)
{
int nDstLen = 0; // 输出字符计数
int i = 0;
while (i < nSrcLen)
{
if (strncmp(pSrc, "=/r/n", 3) == 0) // 软回车,跳过
{
pSrc += 3;
i += 3;
}
else
{
if (*pSrc == '=') // 是编码字节
{
sscanf(pSrc, "=%02X", pDst);
pDst++;
pSrc += 3;
i += 3;
}
else // 非编码字节
{
*pDst++ = (unsigned char)*pSrc++;
i++;
}
nDstLen++;
}
}
*pDst = '/0';
return nDstLen;
}
文章写到这里,重要的地方也算基本讲解完毕了,要把代码完全贴出来是不可能的,单一个LSP.cpp文件就上千行,再加上其他文件的话就更多了。
本文写的比较仓促,因此也不敢指望能教会各位多少东西,只要其中的内容能让各位大侠有个思路和印象,就算达到目的了。