64位平台上的WinAPI IcmpSendEcho

时间:2018-06-28 16:30:30

标签: c++ winapi visual-c++ windows-10-desktop win64

我正尝试使用Windows提供的IcmpSendEcho功能发送ping请求,如here所述。
在大多数情况下,这似乎很简单,但是我很难理解应如何设置和使用调用方提供的回显请求缓冲区(LPVOID ReplyBufferDWORD ReplySize参数)。该文档指出以下内容:

  

ReplyBuffer
  [...]
  一个缓冲区,用于保存对回显请求的所有答复。返回时,缓冲区包含ICMP_ECHO_REPLY结构的数组,后跟答复的选项和数据。缓冲区应足够大,以容纳至少一个ICMP_ECHO_REPLY结构以及RequestSize字节的数据。
  在64位平台上,返回时,缓冲区包含ICMP_ECHO_REPLY32结构数组,后跟答复的选项和数据。

  

ReplySize
  [...]
  分配的答复缓冲区大小(以字节为单位)。缓冲区应足够大,以容纳至少一个ICMP_ECHO_REPLY结构以及数据的RequestSize字节。在64位平台上,缓冲区应足够大,以容纳至少一个ICMP_ECHO_REPLY32结构以及数据的RequestSize字节。
  此缓冲区还应该足够大,以容纳8个字节以上的数据(ICMP错误消息的大小)。

我正在开发并瞄准64位系统和Windows版本,因此我从文档中了解到,我需要使用ICMP_ECHO_REPLY32来计算缓冲区大小,即:

ReplySize >= sizeof(ICMP_ECHO_REPLY32) + RequestSize + 8

但是,当对此进行测试时,IcmpSendEcho总是立即失败,返回值为0。
对于RequestSize < 4GetLastError()返回ERROR_INVALID_PARAMETER,该文档指出“ ReplySize 参数指定的值小于ICMP_ECHO_REPLY或ICMP_ECHO_REPLY32结构的大小” 。
对于RequestSize >= 4GetLastError返回一个未记录的值,当与GetIpErrorString()进行解析时,该值表示“常规故障”。

我还看到文档中的示例代码排除了根据“ ICMP错误消息”的要求指定的回复缓冲区中的8个字节(并且已经看到speculation that 8 is incorrect for 64-bit platforms)-我不确定这是否会影响问题。

如果我改为使用ICMP_ECHO_REPLY进行ReplySize计算,则IcmpSendEcho不再因任何有效载荷大小而失败,但是仍然不清楚该函数在内部实际使用了什么。也就是说,ReplyBuffer是作为ICMP_ECHO_REPLYICMP_ECHO_REPLY32的数组写的吗?由于曾经必须通过强制转换指针类型来访问缓冲区,因此使用错误的类型可能会导致静默地未对齐并破坏对其的所有操作。

那么应该如何正确设置回复缓冲区?应该使用ICMP_ECHO_REPLY还是ICMP_ECHO_REPLY32

这是我一直在使用的测试代码:

#include <WS2tcpip.h>
#include <Windows.h>
#include <iphlpapi.h>
#include <IcmpAPI.h>

#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib, "Ws2_32.lib")

int main()
{
    // Create the ICMP context.
    HANDLE icmp_handle = IcmpCreateFile();
    if (icmp_handle == INVALID_HANDLE_VALUE) {
        throw;
    }

    // Parse the destination IP address.
    IN_ADDR dest_ip{};
    if (1 != InetPtonA(AF_INET, "<IP here>", &dest_ip)) {
        throw;
    }

    // Payload to send.
    constexpr WORD payload_size = 1;
    unsigned char payload[payload_size]{};

    // Reply buffer for exactly 1 echo reply, payload data, and 8 bytes for ICMP error message.
    constexpr DWORD reply_buf_size = sizeof(ICMP_ECHO_REPLY32) + payload_size + 8;
    unsigned char reply_buf[reply_buf_size]{};

    // Make the echo request.
    DWORD reply_count = IcmpSendEcho(icmp_handle, dest_ip.S_un.S_addr,
        payload, payload_size, NULL, reply_buf, reply_buf_size, 10000);

    // Return value of 0 indicates failure, try to get error info.
    if (reply_count == 0) {
        auto e = GetLastError();

        // Some documented error codes from IcmpSendEcho docs.
        switch (e) {
        case ERROR_INSUFFICIENT_BUFFER:
            throw;
        case ERROR_INVALID_PARAMETER:
            throw;
        case ERROR_NOT_ENOUGH_MEMORY:
            throw;
        case ERROR_NOT_SUPPORTED:
            throw;
        case IP_BUF_TOO_SMALL:
            throw;
        }

        // Try to get an error message for all other error codes.
        DWORD buf_size = 1000;
        WCHAR buf[1000];
        GetIpErrorString(e, buf, &buf_size);
        throw;
    }

    // Close ICMP context.
    IcmpCloseHandle(icmp_handle);
}

1 个答案:

答案 0 :(得分:2)

我认为文档有误。

如果您查看ICMP_ECHO_REPLY32中的实际内容,则会发现一些使用__ptr32属性声明的指针,并且有关于这些家伙here的信息。

但是,当您实际上取消引用其中之一时,它毕竟是真正的64位指针,这与Hans的帖子中所说的不一样。我猜自从他写完这本书以来,情况已经发生了变化(我刚刚在Visual Studio 2017上对其进行了测试)。

无论如何,要弄清楚,但是破坏代码的是,即使是在64位版本中,sizeof对于这样的指针也返回 4 ,这意味着报告的总大小ICMP_ECHO_REPLY32结构的大小不够大(有效载荷很小),无法容纳单个ICMP回复,因此IcmpSendEcho失败,ERROR_INVALID_PARAMETER,如您所见。

因此,要使代码正常工作,您要做的就是在设置请求时使用sizeof (ICMP_ECHO_REPLY),然后将reply_buf强制转换为const ICMP_ECHO_REPLY *以解释结果。

这是我(正在运行的)测试程序的全部代码,并添加了一些日志记录(为简洁起见,我删除了一些错误处理):

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <WS2tcpip.h>
#include <Windows.h>
#include <iphlpapi.h>
#include <IcmpAPI.h>

#include <iostream>

#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib, "Ws2_32.lib")

int main()
{
    // Create the ICMP context.
    HANDLE icmp_handle = IcmpCreateFile();
    if (icmp_handle == INVALID_HANDLE_VALUE) {
        throw;
    }

    // Parse the destination IP address.
    IN_ADDR dest_ip{};
    if (1 != InetPtonA(AF_INET, "89.238.162.170", &dest_ip)) {
        throw;
    }

    // Payload to send.
    constexpr WORD payload_size = 1;
    unsigned char payload[payload_size] { 42 };

    // Reply buffer for exactly 1 echo reply, payload data, and 8 bytes for ICMP error message.
    constexpr DWORD reply_buf_size = sizeof(ICMP_ECHO_REPLY) + payload_size + 8;
    unsigned char reply_buf[reply_buf_size]{};

    // Make the echo request.
    DWORD reply_count = IcmpSendEcho(icmp_handle, dest_ip.S_un.S_addr,
        payload, payload_size, NULL, reply_buf, reply_buf_size, 10000);

    // Return value of 0 indicates failure, try to get error info.
    if (reply_count == 0) {
        auto e = GetLastError();
        DWORD buf_size = 1000;
        WCHAR buf[1000];
        GetIpErrorString(e, buf, &buf_size);
        std::cout << "IcmpSendEcho returned error " << e << " (" << buf << ")" << std::endl;
        return 255;
    }

    const ICMP_ECHO_REPLY *r = (const ICMP_ECHO_REPLY *) reply_buf;
    struct in_addr addr;
    addr.s_addr = r->Address;
    char *s_ip = inet_ntoa (addr);
    std::cout << "Reply from: " << s_ip << ": bytes=" << r->DataSize << " time=" << r->RoundTripTime << "ms TTL=" << (int) r->Options.Ttl << std::endl;

    // Close ICMP context.
    IcmpCloseHandle(icmp_handle);
    return 0;
}

rextester上运行它。 (那是我网站的IP地址,您看到我在那儿做什么了吗::)

给以后的访问者的注意事项:返回r->Status时,此代码不会检查IcmpSendEcho。但它应该做。 0 =成功,请参见documentation