【网络编程】检查数据的有效性:缓冲区处理

数据无效的问题我在做项目的过程中也发生了,导致了服务端和客户端的崩溃。

我在测试的过程中,让客户端发送多次数据,数据的长短不一,服务端接受短数据的时候,后面总会连着一些长数据末尾的几个字符,最后才发现是结束符的问题。

一个设计良好的网络程序,应该可以在随机输入的情况下表现稳定。不仅是这样,随着互联网的发展,网络安全也愈发重要,我们编写的网络程序能不能在黑客的刻意攻击之下表现稳定,也是一个重要考量因素。

很多黑客程序,会针对性地构建出一定格式的网络协议包,导致网络程序产生诸如缓冲区溢出、指针异常的后果,影响程序的服务能力,严重的甚至可以夺取服务器端的控制权,随心所欲地进行破坏活动,比如著名的 SQL 注入,就是通过针对性地构造出 SQL 语句,完成对数据库敏感信息的窃取。所以,在网络程序的编写过程中,我们需要时时刻刻提醒自己面对的是各种复杂异常的场景,甚至是别有用心的攻击者,保持“防人之心不可无”的警惕。

下面就通过几个例子来说明一些典型的漏洞:

Example1

这个例子就包含了我遇到的问题,代码如下:

char Response[] = "it's OK";
char buffer[128];

while (1) {
    int nBytes = recv(connfd, buffer, sizeof(buffer), 0);
    if (nBytes == -1) {
        error(1, errno, "error read message");
    } else if (nBytes == 0) {
        error(1, 0, "client closed \n");
    }

    buffer[nBytes] = '\0';
    if (strcmp(buffer, "quit") == 0) {
        printf("client quit\n");
        send(socket, Response, sizeof(Response), 0);
    }

    printf("received %d bytes: %s\n", nBytes, buffer);
}

这段代码从连接套接字中获取字节流,并且判断了出差和 EOF 情况,如果对端发送来的字符是“quit”就回应“it’s OK”的字符流,乍看上去一切正常。

但仔细看一下,这段代码很有可能会产生下面的结果。

char buffer[128];
buffer[128] = '\0';

通过 recv 读取的字符数为 128 时,就会这样的结果。因为 buffer 的大小只有 128 字节,最后的赋值环节,产生了缓冲区溢出的问题。

所谓缓冲区溢出,是指计算机程序中出现的一种内存违规操作。本质是计算机程序向缓冲区填充的数据,超出了原本缓冲区设置的大小限制,导致了数据覆盖了内存栈空间的其他合法数据。

可以对这个程序稍加修改,主要的想法是留下 buffer 里的一个字节,以容纳后面的’\0’。

int nBytes = recv(connfd, buffer, sizeof(buffer)-1, 0);

这个例子里面,还昭示了一个有趣的现象。你会发现我们发送过去的字符串,调用的是sizeof,那也就意味着,Response 字符串中的’\0’是被发送出去的,而我们在接收字符时,则假设没有’\0’字符的存在。

为了统一,我们可以改成如下的方式,使用 strlen 的方式忽略最后一个’\0’字符。

send(socket, Response, strlen(Response), 0);

我遇到的情况就是,因为我用的std::string类型,直接用了str.length()来指定发送长度,导致没有发送结束符,而接收方又没有加"\0",所以出现错误;建议采取不发送结束符,在接收方加的方式,避免一些麻烦的事情发生。

Example2

在对变长报文解析中,涉及到了两种方法,一个是使用特殊的边界符号,例如 HTTP 使用的回车换行符;另一个是将报文信息的长度编码进入消息。

这里主要是针对将报文信息的长度编码进入消息的情况,将报文长度编码进消息,可以让接收方明确需要接收到消息体的长度,但也要保持警惕。比如下面的代码:

size_t read_message(int fd, char *buffer, size_t length) {
    u_int32_t msg_length;
    u_int32_t msg_type;
    int rc;

    rc = readn(fd, (char *) &msg_length, sizeof(u_int32_t));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;
    msg_length = ntohl(msg_length);

    rc = readn(fd, (char *) &msg_type, sizeof(msg_type));
    if (rc != sizeof(u_int32_t))
        return rc < 0 ? -1 : 0;

    if (msg_length > length) {
        return -1;
    }

    /* Retrieve the record itself */
    rc = readn(fd, buffer, msg_length);
    if (rc != msg_length)
        return rc < 0 ? -1 : 0;
    return rc;
}

在进行报文解析时,第 15 行对实际的报文长度msg_length和应用程序分配的缓冲区大小进行了比较,如果报文长度过大,导致缓冲区容纳不下,直接返回 -1 表示出错。

千万不要小看这部分的判断,试想如果没有这个判断,对方程序发送出来的消息体,可能构建出一个非常大的msg_length,而实际发送的报文本体长度却没有这么大,这样后面的读取操作就不会成功,如果应用程序实际缓冲区大小比msg_length小,也产生了缓冲区溢出的问题。

struct {
    u_int32_t message_length;
    u_int32_t message_type;
    char data[128];
} message;

int n = 65535;
message.message_length = htonl(n);
message.message_type = 1;
char buf[128] = "just for fun\0";
strncpy(message.data, buf, strlen(buf));
if (send(socket_fd, (char *) &message,
         sizeof(message.message_length) + sizeof(message.message_type) + strlen(message.data), 0) < 0)
    error(1, errno, "send failure");

就是这样一段发送端“不小心”构造的一个程序,消息的长度“不小心”被设置为 65535 长度,实际发送的报文数据为“just for fun”。在去掉实际的报文长度msg_length和应用程序分配的缓冲区大小做比较之后,服务器端一直阻塞在 read 调用上,这是因为服务器端误认为需要接收 65535 大小的字节(也就是前面所说的读取操作不会成功)。

总结

在网络编程中,需要多注意异常边界的检测问题,因为这些问题决定了程序运行的稳定性,不要动不动就崩了。要时刻提醒自己做好应对各种复杂情况的准备,这里的异常情况包括缓冲区溢出、指针错误、连接超时检测等。

问题一:在读数据的时候,一般都需要给应用程序最终缓冲区分配大小,这个大小咋确定?

最终缓冲区的大小应该比预计接收的数据大小大一些,预防缓冲区溢出。

问题二:这里的缓冲区是否可以换成动态分配?

可以的,但是要记得在return前释放缓冲区。我项目程序中写到的就是根据接收数据的大小来分配,当时是直接new的,没有考虑到释放(后来缓冲区出错了,字符读取错乱,才注意到)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值