前面, 我们玩过http, 颇有点意思, 在本文中, 我们继续来玩ftp(file transfer protocol). http和ftp都是建立在tcp之上的应用层协议, 无论他们怎么包装, 怎么装bigger, 最终还是基于tcp端到端传输的。本文主要分为两个部分: 一. 用Wireshark抓包来揭开ftp client GG和ftp server MM的勾搭内容。二.用C代码来简要模拟实现Windows自带的ftp client.
说明, 本文中的实验, 我用了两台电脑, 分别是pc1(192.168.1.100)和pc2(192.168.1.102). 其中, pc1做客户端(ftp client GG), pc2做服务端(ftp server MM)。 好吧, 开启两台电脑的电源按钮吧, 来玩ftp。
一. 用Wireshark抓包来揭开ftp client GG和ftp server MM的勾搭内容
1. 在pc2上开启ftp server MM, 设置用户名为1, 密码为1. 我们在pc2的cmd中执行netstat -nao | findstr 21 可以看到, pc2开启了21端口的监听。 在ftp server MM对应的目录下创建a.txt和b.txt两个文件。(ftp server MM的搭建方法请参考我之前的博文:http://blog.csdn.net/stpeace/article/details/38026285)
2. 在pc1上进行wireshark抓包, 并打开cmd, 依次执行如下命令, 得到结果为:
C:\Documents and Settings\Administrator>ftp 192.168.1.102
Connected to 192.168.1.102.
220 欢迎访问 Slyar FTPserver!
User (192.168.1.102:(none)): 1
331 Please specify the password.
Password:
230 Login successful.
ftp>
ftp>
ftp> dir
200 Port command successful.
150 Opening ASCII mode data connection for directory list.
-rwx------ 1 user group 0 Apr 16 23:04 a.txt
-rwx------ 1 user group 0 Apr 16 23:04 b.txt
226 Transfer complete
ftp: 收到 118 字节,用时 0.01Seconds 7.87Kbytes/sec.
ftp>
我们首先执行ftp 192.168.1.102, 此时ftp client GG与ftp server MM建立tcp连接, 然后输入用户名和密码进行认证。 认证通过后, 最后用dir命令来查询ftp server MM中有什么东东, 结果看到了a.txt和b.txt文件
3. 我们将抓包文件保存下来(port.cap), 进行分析, 截图如下:
4. 第1.2个包很简单, 住pc1发的arp广播, 主要是去找192.168.1.102这台电脑(pc2)的mac, 第3个包就表明成功获取了pc2的mac地址, 有了mac才能通信啊。
5. 实际上, 上述的arp操作是由ftp 192.168.1.102触发的, 其实ftp 192.168.1.102的更重要用途是与ftp server MM建立tcp连接通道。第4, 5, 6个包就是传说中的三次握手协议, 太重要太基础, 故不多说。
6. 第7-14个包主要用户ftp client GG与ftp server MM之间的用户名和密码认证, 约妹子, 要获得妹子的验证和认可, 也是理所当然的了。
7. 当我们执行dir命令时,会触发后续所有的包。 第15个包是ftp client GG向ftp server MM发送PORT 192,168,1,100,7,220命令, 并对ftp server MM说: 你待会儿要主动跟我连接我, 建立数据传输的tcp通道(第二个tcp通道), 我监听地址是192.168.1.100上的 (7*256+220) 端口, 并意犹未尽地暗示: 你这个ftp server MM要主动一点来联系我, 我就在刚才的那个ip和port处等你, 不见不散。 此时ftp server GG端开始监听(7*256+220)这个端口, 等待ftp server MM主动上钩。 第16个包是ftp server MM的回应, 意思是在说: 好的, 我一定会去那个地点那个端口找你。
8. 第17个包是ftp client GG发送的dir请求, 意思是询问ftp server MM: 请问你家产有多少?
9. 第18个包是ftp server MM的回应, 仿佛在说: 我已经知道你在问什么问题了, 我待会儿会在我们之前约定的地点告诉你(第二个临时的tcp通道)。
10. 好,到此为止, ftp client GG和ftp server MM的第一轮勾搭暂时告一段落。注意, 是暂时告一段落, 不是终止, 你看看, 根本没有挥手byebye的收据包啊。
11. 好, ftp client GG和ftp server MM的第二轮勾搭正式开始, 只不过, 这次, ftp server MM开始骚动了, 主动到照预约的地点去上钩, 请看第19-21个包, 这就是第二轮勾搭的三次握手协议。
12. 我们来一起仅仅地盯住第22个包, 这次, ftp server MM要在刚刚建立的tcp通道(第二个临时的tcp通道)上来传输数据了, 别忘了, ftp client GG在第一轮勾搭中问过ftp server MM家有多少财产, 这次, ftp server MM该说实话了, 财产肯定会涉及到隐私啊, 所以用新的tcp通道来回馈。 展开第22个包, 我们看到ftp server MM终于不再矜持了, 把自己的家产和盘托出, 通信内容为:
-rwx------ 1 user group 0 Apr 16 23:04 a.txt
-rwx------ 1 user group 0 Apr 16 23:04 b.txt
13. 我们看到, 其实, ftp serve MM家里也没有多少财产, 只有可怜巴巴的a.txt和b.txt两个空空的文件。
14. 后续的23-29个包无非就是一些挂电话的过程(关闭第二个临时的tcp通道), 说byebye了, 可以清晰看到有断开socket的过程。 值得注意的是第28个包, ftp server MM非常贴心地说: 我已经说说完了, 财产就这么多, 接下来你开着办吧。
15. 请注意, 到此为止, ftp client GG和ftp server MM的在第二个tcp通道上已经断开了, 但是在第一个tcp通道上仍然保持着联系。 后续如果ftp client继续执行其他请求命令, 比如探探三围啊, 他自身又会重新开启新的随机端口进行监听, 并把这个随机端口又一次告诉ftp server MM, 让ftp server MM来发起第三次临时tcp连接。 同理, ftp server MM会在第三个tcp通道上吧三围数据告诉给ftp client GG, 随后第三个临时的tcp通道也会被拆除。 不变的是, 第一个tcp通道依然在那里紧紧相连。
总结: 以上就是ftp的主动模式。 第一个“永久”tcp通道主要用来传递请求命令, 是ftp client GG主动去勾引ftp server MM, 后续的第二/三/四...个临时tcp通道主要用来传数据, 而且是ftp server MM主动上钩。所谓的ftp主动模式, 是指ftp server MM主动。
二.用C代码来简要模拟实现Windows自带的ftp client.
到此为止, 对tcp主动模式应该有了比较通透的理解了, 现在我们尝试来用C代码简要模拟一下上述过程, 模拟ftp client GG的代码如下:
// 我花了较长时间调试, 如果要转载, 请注明本博客地址, 尊重版权 // 博客地址:http://blog.csdn.net/stpeace/article/details/45100687 #include <stdio.h> #include <string.h> #include <winsock2.h> // winsock接口 #pragma comment(lib, "ws2_32.lib") // winsock实现库 // 缓冲区长度 #define LEN (1024 + 1) SOCKET g_ctrlSocket = 0; // ftp client端负责在"命令控制tcp通道"上通信的socket SOCKET g_listenSocket = 0; // ftp client端负责监听的socket SOCKET g_dataSocket = 0; // ftp client端负责在"数据传输tcp通道"上通信的socket // 创建"命令控制tcp通道"的通信socket int createCtrlSocket() { g_ctrlSocket = socket(AF_INET, SOCK_STREAM, 0); return 0; } // 获取ftp server 在"命令控制tcp通道"上返回的信息 int getCmdResFromFtpServer() { char szRecvBuf[LEN] = {0}; int nRet = recv(g_ctrlSocket, szRecvBuf, sizeof(szRecvBuf) - 1, 0); if(nRet < 0) { printf("recv error\n"); return -1; } if(0 == nRet) { printf("connection has been closed by ftp server"); return -1; } printf("%s", szRecvBuf); return 0; } // 建立"命令控制tcp通道" int connectFtpServer(const char *pIP, unsigned short port) { struct sockaddr_in ftpServerAddr; ftpServerAddr.sin_family = AF_INET; ftpServerAddr.sin_addr.S_un.S_addr = inet_addr(pIP); ftpServerAddr.sin_port = htons(port); int nRet = connect(g_ctrlSocket, (struct sockaddr *)&ftpServerAddr, sizeof(ftpServerAddr)); if(nRet < 0) { printf("connect error\n"); return -1; } getCmdResFromFtpServer(); return 0; } // 从"命令控制tcp通道"上向ftp server 发起相关命令请求 int requestFtpServer(const char *pPassWord) { char szSendBuf[LEN] = {0}; sprintf(szSendBuf, "%s\r\n", pPassWord); send(g_ctrlSocket, szSendBuf, strlen(szSendBuf) + 1, 0); getCmdResFromFtpServer(); return 0; } // 开启ftp client监听线程,准备接受ftp server请求建立"数据传输tcp通道" DWORD WINAPI createDataSocketThread(LPVOID p) { unsigned int a = 0; unsigned int b = 0; sscanf((const char *)p, "%d:%d", &a, &b); SOCKET g_listenSocket = socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN addrSrv; addrSrv.sin_family = AF_INET; addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.100"); addrSrv.sin_port = htons(a * 256 + b); bind(g_listenSocket,(SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); listen(g_listenSocket, 5); SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); // 等待ftp server主动请求建立"数据传输tcp通道" g_dataSocket = accept(g_listenSocket, (SOCKADDR*)&addrClient, &len); return 0; } // 从"数据传输tcp通道" 上获取ftp server的数据信息 int getDateFromFtpServer() { char szRecvBuf[LEN] = {0}; int nRet = recv(g_dataSocket, szRecvBuf, sizeof(szRecvBuf) - 1, 0); if(nRet < 0) { printf("recv error\n"); return -1; } if(0 == nRet) { printf("closed by ftp server\n"); return -1; } printf("%s", szRecvBuf); return 0; } int main() { // 网络初始化 WSADATA wsaData; WSAStartup(MAKEWORD(1,1), &wsaData); // 创建"命令控制tcp通道"上通信的socket createCtrlSocket(); // 建立"命令控制tcp通道" connectFtpServer("192.168.1.102", 21); // 从"命令控制tcp通道"发送用户名和密码给ftp server进行认证 requestFtpServer("user 1"); requestFtpServer("pass 1"); // 建立"数据传输tcp通道" HANDLE handle = CreateThread(NULL, 0, createDataSocketThread, "12:34", 0, NULL); // 主线程阻塞1s, 确保createDataSocketThread线程拉起监听 Sleep(1000); // 从"命令控制tcp通道" 把ftp client即将监听的ip, port传给ftp server requestFtpServer("PORT 192,168,1,100,12,34"); // 从"命令控制tcp通道" 上把LIST消息请求传递过去 requestFtpServer("LIST"); // 等1s(相当有必要), 等待ftp server的连接, 否则, 如果"数据传输tcp通道"没有创建好, 那还谈什么沟通呢? Sleep(1000); // 从"数据传输tcp通道"上接收数据 getDateFromFtpServer(); // 关闭临时的"数据传输tcp通道". 注意: "命令控制tcp通道"不能关闭 closesocket(g_dataSocket); closesocket(g_listenSocket); // 重复上述请求 handle = CreateThread(NULL, 0, createDataSocketThread, "12:40", 0, NULL); Sleep(1000); requestFtpServer("PORT 192,168,1,100,12,40"); requestFtpServer("LIST"); Sleep(1000); getDateFromFtpServer(); closesocket(g_dataSocket); closesocket(g_listenSocket); while(1); // 阻塞 CloseHandle(handle); closesocket(g_ctrlSocket); WSACleanup(); return 0; }ftp client端的结果为:
220 欢迎访问 Slyar FTPserver!
331 Please specify the password.
230 Login successful.
200 Port command successful.
150 Opening ASCII mode data connection for directory list.
-rwx------ 1 user group 0 Apr 16 23:04 a.txt
-rwx------ 1 user group 0 Apr 16 23:04 b.txt
226 Transfer complete
200 Port command successful.
-rwx------ 1 user group 0 Apr 16 23:04 a.txt
-rwx------ 1 user group 0 Apr 16 23:04 b.txt
我在调试上述程序的时候吃了一些苦头, 有一下几点值得注意:
1. 确保ftp server MM先开启, 然后再运行我上面的程序。
2. 确保ftp server MM的21端口没有被防火墙堵住, 在pc1上用telnet 192.168.1.102 21就可以测试。
3. 确保ftp client GG的随机端口没有被防火墙堵住, 我在调试的时候, 就栽倒在此处, 结果ftp server MM总是不能与ftp client GG建立第二个tcp通道。 典型症状是, 在pc2上, 执行telnet 192.168.1.100 xxx 无法成功, 其中xxx是ftp client GG监听的端口。 后来, 我把pc1上的防火墙放开, 就可以了。
4. 程序中采用线程创建socket是很好的方式, 确保ftp client GG先启动accept.
5. 程序中的两个Sleep比较关键, 在程序中已经有注释, 所以我就不再赘述了。
6. 很多资料说, 在后续的"数据传输tcp通道"中, ftp server MM端的端口是20, 也就是说, 去connect的时候, 她自身socket绑定到了20端口, 刚好, 我们之前在博文中讲过bind的这种应用。 但是, 在实际抓包中我发现,ftp server MM并没有死死守在20端口, 当然, 这并不为错, 主要是具体实现的差异所致。 而且, 我觉得不绑定20号端口更好。
OK . 到此为止, 我们算是对ftp的主动模式有了比较深入的了解了。 那什么是ftp的被动模式呢? 很简单: 第一次tcp通道由ftp client GG请求创建, 后续的所有临时tcp通道也是由ftp client GG请求创建, 也就是说, ftp server MM很被动, 很被动。
我也亲自验证过, Windows上自带的ftp client其实并不支持所谓的被动模式。 网上所说的quote pasv或literal pasv其实并没有pasv请求的功能。 当然, 在这个问题上, 更详细的阐述是:http://blogs.isaserver.org/pouseele/2006/11/09/about-the-microsoft-command-line-ftp-client/ , 有兴趣的可以去瞄瞄。
鉴于ftp被动模式和上面介绍的主动模式大同小异, 我就不再赘述ftp被动模式了, 有兴趣的童鞋可以自己深入学习一下, 找个支持被动模式的ftp client, 抓抓包看看。
好了, 关于ftp的介绍到此为止。