Linux raw socket的总结
介绍相关结构体和常量, 最后实现ICMP接收和响应

创建套接字

int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);

创建一个IPV4 ICMP原始套接字(IPV4 ICMP raw socket)
AF_INET: 代表IPV4协议
SOCK_RAW: 代表原始套接字
IPPROTO_ICMP: 代表ICMP协议

禁用内核自动附加IP头

int on = 1;
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));

启用IP_HDRINCL选项, 数据包含IP数据头, 发送数据时需要自己构建IP数据头, 内核不再生成

接收数据

char* buf = calloc(1, IPPROTO_MAX);
struct sockaddr in;
socklen_t in_len = sizeof(in);

recvfrom(sock, buf, IPPROTO_MAX, 0, &in, &in_len);

将数据写入buf, 发送方地址将保存在in变量

解析数据段

struct iphdr *ip = (struct iphdr*)buf;
struct icmphdr *icmp = (struct icmphdr*)((char*)ip + 4 * ip->ihl);
printf("data: %s\n", (char*)icmp + 8);

iphdr是ip header的缩写, 表示一个IP数据头结构 image-20230308174939394 IHL(Internet Header Length): IP数据头是不定长的, 所以需要IHL记录IP数据头大小, 以4字节为单位, 所以IP数据头大小等于4 * IHL bytes
Total Length: IP数据包总大小, 包含数据头和数据, 用于计算IP数据段大小:
Total Length - 4 * IHL bytes
Protocol: 协议标识, IP协议主要用于寻址路由, 一般会包含其他协议进行数据交换, 所以这个字段用来标识IP数据段是什么协议, 内核有一组常量定义已知协议
Options: 这个字段保留用作自定义协议, 可配合Protocol字段开发自定义协议
字段详解: https://en.wikipedia.org/wiki/Internet_Protocol_version_4#Header

icmphdr是icmp header的缩写, 代表一个ICMP数据头结构 image-20230308181034216 Type: 定义了一组常量
常量细节及描述: https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Control_messages
Code: 这个字段在Echo类型中总是0(回复和请求)
字段详解: https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol#Header

所以一个IP数据包, 首先把IP头和IP数据段分开
IP数据段是一个ICMP包
一个ICMP数据包又包含ICMP头和ICMP数据段
就这样层层解析

完整代码

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

uint16_t in_cksum(uint16_t *addr, int len)
{
    int nleft = len;
    uint32_t sum = 0;
    uint16_t *w = addr;
    uint16_t answer = 0;

    // Adding 16 bits sequentially in sum
    while (nleft > 1) {
        sum += *w;
        nleft -= 2;
        w++;
    }

    // If an odd byte is left
    if (nleft == 1) {
        *(unsigned char *) (&answer) = *(unsigned char *) w;
        sum += answer;
    }

    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    answer = ~sum;

    return answer;
}

int main() {
    char* buf = calloc(1, IPPROTO_MAX);
    struct sockaddr_in in;
    socklen_t in_len = sizeof(in);
//    int on = 1;

    int sock = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
    if (sock < 0) {
        goto error;
    }

//    /* Set that header is included with data, kernel do not generate the header */
//    if (setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) == -1){
//        goto error;
//    }

    ssize_t n = recvfrom(sock, buf, IPPROTO_MAX, 0, (struct sockaddr*)&in, &in_len);
    if (n == -1){
        goto error;
    }

    struct iphdr *ip = (struct iphdr*)buf;
    struct icmphdr *icmp = (struct icmphdr*)((char*)ip + 4 * ip->ihl);

    uint16_t dataSize = ntohs(ip->tot_len) - 4 * ip->ihl - 8;

    icmp->type = ICMP_ECHOREPLY;
    icmp->checksum = 0;
    icmp->checksum = in_cksum((unsigned short *)icmp, sizeof(struct icmphdr) + dataSize);

    sendto(sock, icmp, sizeof(struct icmphdr) + dataSize, 0, (struct sockaddr*)&in, in_len);

    goto success;

    error:
    free(buf);
    buf = NULL;
    close(sock);
    printf("error: %s\n", strerror(errno));
    exit(EXIT_FAILURE);

    success:
    free(buf);
    buf = NULL;
    close(sock);
    return 0;
}

需要高权限执行, 而且还需要关闭内核的ICMP回复功能
否则会出现异常情况(一个请求两个回答)

禁用内核ICMP回复

临时禁用:

echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all

永久禁用:
/etc/sysctl.conf里添加下面这一行

net.ipv4.icmp_echo_ignore_all=1

更新设置:

sysctl -p

思考

  • IP协议支持自定义协议, 木马是否可以使用自定义协议在网络层(Internet layer)通信
  • IP数据包里的源地址伪造, 是否可以让我们匿名发送数据包(只不过收不到回复)
  • IP数据包源地址伪造后, 是否可以控制目标地址向伪造源地址发送数据包

思考结果

  • IP协议支持自定义协议, 木马是否可以使用自定义协议在网络层(Internet layer)通信 理论可以, 未来做一个POC

  • IP数据包里的源地址伪造, 是否可以让我们匿名发送数据包(只不过收不到回复) 可以, 除去物理层以及链路层, 接下来就是网络层了, 所以网络层数据包伪造可以在网络层匿名发送数据包, 因为数据包能否发送成功无所谓源地址.
    实验开始:
    构造IPV4数据包, 发送一个ICMP请求, 伪造源地址为本地(127.0.0.1) image-20230312002840290 可以看到, 源地址已经被伪造了, 但是由于是局域网, 所以以太网头会有我们的源MAC地址 但是在实际网络中, 我们的MAC地址不会泄露出去(数据包会多次中转) 伪造数据包代码:

use std::net::SocketAddr;
use etherparse::{IcmpEchoHeader, Icmpv4Type, PacketBuilder};
use socket2::{Domain, Protocol, SockAddr, Socket, Type};

fn main() {
    let mut packet_eth = Vec::<u8>::with_capacity(4096);
    let payload = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

    let addr:SocketAddr = "192.168.20.174:80".parse().unwrap();
    let addr = SockAddr::from(addr);

    let conn = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
        .unwrap();

    conn.set_header_included(true).unwrap();

    let packet = PacketBuilder::
    ipv4(
            [127,0,0,1],
            [192,168,20,174],
        64)
        .icmpv4(
            Icmpv4Type::EchoRequest(
                IcmpEchoHeader{ id: 1, seq: 1}));

    packet.write(&mut packet_eth, payload.as_bytes()).unwrap();

    let n = conn.send_to(packet_eth.as_slice(), &addr).unwrap();
    println!("sent {n} bytes.");
}

伪造源地址一个作用是匿名: 攻击者只需要发送, 不需要收到回复的场景
思考: 是否可以绕过IP封锁, 或者防火墙?

  • IP数据包源地址伪造后, 是否可以控制目标地址向伪造源地址发送数据包 可以, 刚刚将源地址伪造成127.0.0.1后, 系统并没有向127.0.0.1发送ICMP回复数据包, 原因不明, 但是如果将源地址伪造成其他局域网地址呢?
    我的实验系统是VMWare虚拟机, 一台Windows, 一台Ubuntu
    IP 192.168.20.1是物理机上的一块虚拟网卡, 我们尝试伪造源地址使其向物理机的虚拟网卡发送ICMP回复数据包 image-20230312003936071 这个也许可以被用做DoS, 发送一个数据包会回复很大数据包的场景