【问题标题】:How to securely read data from a packet in C?如何安全地从 C 中的数据包中读取数据?
【发布时间】:2014-05-10 12:07:08
【问题描述】:

如何从 C 中的数据包中读取数据并将其转换为结构?我的意思是,有一个类似的结构

|=======================================================================
|0123456701234567012345670123456701234567012345670123456701234567.......
|  type  |             length            |    MSG HDR    |    data

像结构一样

struct msg {
  char type;
  size_t length;
  int hdr;
  struct data * data;
};

下面的代码可以吗?

bool parse_packet(char * packet, size_t packet_len, struct msg * result) {
    if(packet_len < 5) return false;
    result->type = *packet++;
    result->length = ntohl(*(int*)packet);
    packet+=4;
    if(result->length + 4 + 5 > packet_len)
      return false;
    if(result->length < 2)
      return false;
    result->hdr = ntohs(*(short*)packet);
    packet+=2;
    return parse_data(result, packet);
}

【问题讨论】:

  • 您的应用程序到底在做什么?
  • 您的安全问题是什么?
  • 我不希望缓冲区溢出/读取溢出(例如 heartbleed)
  • @user3623227:这不是“安全的”。这只是“正确”,无论如何都是暗示的。
  • 您的演员表会产生未定义的行为。

标签: c security operating-system


【解决方案1】:

检查packetresult 是否为非空通常是个好习惯。

当标头为 7 个字节时,为什么要检查 packet_len &lt; 5?为什么不只是确保数据包至少有 7 个字节并完成它呢?还是hdr 不存在于某些type 中?

我不确定你想要达到什么目的

if(result->length + 4 + 5 > packet_len)
    result->hdr = ntohs(*(short*)packet);
packet+=2;

如果声明的消息长度加 9 大于接收到的消息长度,则从消息中读取另外两个字节。然后,不管数据的长度如何,您将两个添加到指针并尝试从中解析出一些东西。如果packet_len 是 5 而result-&gt;length 是 4294967295 会怎样?您将读取缓冲区的末尾,就像在 Heartbleed 中一样。您需要始终验证您的读取是否在界限内,并且永远不要相信数据包中声明的大小。

【讨论】:

  • 检查已损坏 - 已修复。
  • 您如何保证在致电parse_packet 之前已阅读整封邮件?
【解决方案2】:

你有一个完全标准的情况。这里没有什么深刻或令人惊讶的。

规范开始。您可以为此使用伪代码或实际的 C 类型,但这意味着数据在线路上被打包成字节:

struct Message  // wire format, pseudo code
{
    uint8_t    type;
    uint32_t   length;      // big-endian on the wire
    uint8_t    header[2];
    uint8_t    data[length];
};

现在开始解析:

// parses a Message from (buf, size)
// precondition: "buf" points to "size" bytes of data; "msg" points to Message
// returns true on success
// msg->data is malloc()ed and contains the data on success
bool parse_message(unsigned char * buf, std::size_t size, Message * msg)
{
    if (size < 7) { return false; }

    // parse length
    uint32_t n;
    memcpy(&n, buf + 1, 4);
    n = ntohl(n);            // convert big-endian (wire) to native

    if (n > SIZE_MAX - 7)
    {
        // this is an implementation limit!
        return false;
    }

    if (size != 7 + n) { return false; }

    // copy data
    unsigned char * p = malloc(n);
    if (!p) { return false; }
    memcpy(p, buf + 7, n);

    // populate result
    msg->type = buf[0];
    msg->length = n;
    msg->header[0] = buf[5];
    msg->header[1] = buf[6];
    msg->data = p;

    return true;
}

另一种解析长度的方法是这样的,直接:

uint32_t n = (buf[1] << 24) + (buf[2] << 16) + (buf[1] << 8) + (buf[0]);

此代码假定buf 包含恰好一条消息。如果您要从流中取出消息,则需要修改代码(即if (size != 7 + n))以检查是否有至少所需的可用数据量,并返回也消费了数据,因此调用者可以相应地推进他们的流位置。 (在这种情况下,调用者可以计算被解析为 msg-&gt;length + 7 的数据量,但依赖它是不可扩展的。)

注意:正如@user 指出的那样,如果您的size_t 不比uint32_t 宽,那么此实现将错误地拒绝非常大的消息。具体来说,7 + n &gt; n 不正确的消息将被拒绝。我对这种(不太可能的)情况进行了动态检查。

【讨论】:

  • 7 + n 可能会溢出(我认为这是未定义的)。检查size - 7 != n 更安全,因为我们已经检查过size 至少为7。但是+1 以获得详细答案。
  • @user3553031: 有趣,但无符号算术定义良好且标准化,因此相等性检查 size != 7 + n 有效,不等式 (size &gt;= 7 + n) 也可以——如果有溢出,检查只会失败。这是一个实现限制:如果您的size_tuint32_t 具有相同的宽度,那么您不能将此解析器用于大消息,您需要设置部分解析器。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-08
  • 1970-01-01
  • 1970-01-01
  • 2023-03-06
  • 2011-05-08
  • 1970-01-01
相关资源
最近更新 更多