您的位置:首页 > 其它

用Raw socket自己构造数据包头部

2015-08-05 10:50 381 查看
一、原始套接字的创建

只有超级用户才能创建原始套接字。

int sockFd;

sockFd = socket(AF_INET, SOCK_RAW, protocol);
其中第3个参数protocol是形如IPPROTO_xxx的某个常值,在<netinet/in.h>头文件中定义,经常不为0。

原始套接字不存在端口号的概念。在原始套接字上调用bind()比较少见,这么做仅仅是设置本地地址。在原始套接字上调用connect()也比较少见,这么做仅仅是设置外地地址。

二、通过原始套接字发送数据

通过raw socket发送数据时,内核会自动对超出外出接口MTU的原始分组执行分片。

普通输出通过sendto()或sendmsg()并指定目的IP地址完成。

如果我们想通过一个原始套接字自己构造IP首部应该怎么做呢?最早原始套接字是不能实现这个目的的,后来某个大牛给了支持traceroute而给出了一个内核补丁,该补丁要求应用进程在调用socket()创建原始套接字时必须指定protocol参数值为IPPROTO_RAW(值为255),它是一个保留值,从不允许作为IP首部中的协议字段出现。时至今日,我们还可以看到很多这样的代码:

if(-1 == (sockFd = socket(AF_INET, SOCK_RAW, 255)))
这种方法需要自行填写IP头部和TCP头部,并自行计算校验和,细节见后文。

不过,后来又有了新的方法,4.3BSD Reno引入了IP_HDRINCL套接字选项,进程可以使用该选项自行构造IPv4首部:

const int on = 1;

if(setsocketopt(sockFd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0){

//error handler;

}
如果IP_HDRINCL选项未开启,则由内核自动构造IP首部并把它置于来自进程的数据之前,进程让内核发送的数据起始地址指的是IP首部之后的第一个字节。

如果IP_HDRINCL选项开启,则进程让内核发送数据的起始地址指的是IP首部的第一个字节,进程调用输出函数写出的数据量必须包括IP首部的大小,整个IP首部由进程构造,不过(a)IPv4标识字段可置为0,表示进程让内核来设置该值(b)IPv4首部校验和字段总是由内核计算并存储(c)IPv4选项字段是可选的。

至于IPv6,不存在IP_HDRINCL这样的选项,大概也不存在可以自行构造首部的内核补丁。IPv6首部的几乎所有字段和所有扩展首部都可以通过套接字选项或辅助数据由应用进程指定或获取。如果一定应用进程一定要读入或写出完整的IPv6数据包,就必须使用链路层访问。

三、通过原始套接字接收数据

如果某个IP datagram以片段形式到达,内核会重组好完整的IP datagram之后再传递给raw socket.

内核向原始套接字交付IP datagram的规则:

1. 内核接收到的UDP分组和TCP分组绝对不会交付给任何原始套接字。如果一个进程想要读取含有TCP/UDP分组的完整IP数据包,就必须在数据链路层读取这些分组。

2. 大多数ICMP分组在内核处理完其中的ICMP消息后传递到原始套接字。源自Berkeley的实现只有回射请求、时间戳请求和地址掩码请求完全由内核处理,此外的所有ICMP分组都会传递给raw socket.

3. 所有IGMP分组在内核处理完其中的IGMP消息后会传递给raw socket.

当内核有一个需要传递到原始套接字的IP datagram时,它将检查所有进程上的所有raw socket以寻找所有匹配的套接字,每个匹配的套接字都会被传递一个该IP datagram的副本。内核检查的规则是:

如果创建这个原始套接字时指定了socket()第3个参数protocol为非0,那么接收到的IP datagram的协议字段必须匹配该值。如果一个原始套接字是以protocol为0值(IPPROTO_IP)来创建的,那么该套接字讲接收可由内核传递到原始套接字的每个原始IP datagram的一个副本。(原始套接字上调用bind()和connect()的情况少见,这里不论)

内核向原始IPv4套接字进程传递的是包括IP首部在内的完整IP datagram,而向原始IPv6套接字传递的是剥去了IPv6首部和所有扩展首部的净荷(payload)。

四、IP头部 & struct iphdr

IP首部结构如下图:



(1) 版本:IPv4用4,IPv6用6

(2) 首部长度:注意单位是32位字长,即4字节。当IP首部长度为1111B时,首部就达到最大长度60字节。还要注意当IP分组的首部长度不是4字节的整数倍时,必须进行填充(选项字段后面还有填充字段),因此数据部分起始地址永远是4字节的整数倍。

(3) 区分服务:以前叫服务类型,没使用过。98年IETF把这个字段改名为DifferentiatedServices,只在使用区分服务时起作用,不管。

(4) 总长度:指首部和数据之和的长度,单位为字节。这个字段16位,因此IP datagram最大长度为2的16次方减1= 65535字节。如果有分片,总长度字段指的是分片后的一个分组的首部和数据之和的长度。分片是内核完成的,对用户进程透明。

(5) Identification字段:每产生1个ip datagram,计数器就加1,并将此值付给identification字段,但是这个identification并不是序号,因为IP是无连接服务,IPdatagram不存在按序接收的问题。这个字段是用于IP datagram分片,分片时原IP datagram的identification字段的值被复制给每个片段的identification字段,相同的标识字段的值使分片后的各个片段最后能够正确地重装为原来的IP
datagram。

(6) Flag字段:3位,目前仅使用两位。a) 最低位叫MF(MoreFragment)。MF == 1表示后面还有分片,MF == 0 表示是最后一个分片。b)中间一位叫DF(Don't Fragment)。DF == 1时才允许分片。

(7) 片偏移:较长的分组在分片后,某片在原分组中相对于用户数据起始地址的偏移地址。

(8) Time To Live(TTL):发出IP datagram的源点设置这个字段,路由器在转发IPdatagram之前就把TTL减1,若TTL减小到0,就丢弃这个IPdatagram不再转发。

(9) Protocol字段:指出此IP datagram携带的数据是使用何种协议,以便使目的主机的IP层知道应该将数据部分向上层交付给哪个处理过程。(所以TCP和UDP的端口号互不影响。所以如果创建raw socket时protocol参数值为非0,那么只能接收到协议字段与protocol参数值匹配的IP datagram)

(10) 首部校验和。这个字段只校验IP datagram的首部,不包括数据部分。这是因为IPdatagram每经过一个router,因为首部一些字段值会发生改变,router需要重新计算首部校验和。只校验首部能减少计算量,为进一步减少计算量,IP首部校验采用如下比较简单的算法而不用CRC:

在发送方,先把IP datagram首部分为若干个16位的序列,校验和字段设为0,用模2运算把所有的16位序列相加后,将得到的和求反,写入首部校验和字段。

在接收方,也将首部的所有16位序列使用模2运算相加1次,将得到的和取反。如果首部未发生变化,此结果必为0,保留该IP datagram,否则认为出错,丢弃该IP datagram。

好,下面来看看struct iphdr的定义。在/usr/src/linux-headers(即linux头文件源代码安装目录)/include/linux/ip.h(是个符号链接)中有如下定义:(Ubuntu 11.10)

struct iphdr{

#if defined(__LITTLE_ENDIAN_BITFIELD)

__u8 ihl:4, //xk> ip header length吧

version:4;

#elif defined(__BIG_ENDIAN_BITFIELD)

__u8 version:4,

ihl:4;

#else

#error "please fix <asm/btyeorder.h>"

#endif

__u8 tos;

__be16 tot_len;

__be16 id;

__b316 frag_off;

__u8 ttl;

__u8 protocol;

__sum16 check;

__be32 saddr;

__be32 daddr;

/*The options start here*/

};
基本与上面IP首部示意图中的字段是对应的,有个3位Frag标志字段在iphdr中没有体现,从后面示例也可以看出,自行构造IP头部并不需要填写iphdr结构的所有字段。

五、TCP头部 & struct tcphdr

TCP头部如下图所示。与IP首部一样,固定长度20字节,最大长度60字节:



(1) 源端口号和目的端口号:端口号占用16位。

(2) 序号:TCP是面向字节流的,在一个TCP连接中传送的每一个字节都按顺序编号,序号增加到2的32次方减1后下一个序号回到0。起始序号在TCP连接建立时设置。TCP报文中序号字段的值指的是本报文段所发送的数据的第一个字节的序号。

(3) ACK序号:指期望收到对方下一个报文段的第一个数据字节的序号。若ACK序号==N,表示到序号N - 1为止的所有数据都已经正确收到。

(4) 首部长度:因为首部中有长度不确定的选项字段,所以首部长度字段是有必要的。和IP首部中的首部长度字段一样,以32位字即4个字节为一个单位,4位能表示的最大值为15,所以首部长度最大不超过15个单位即60字节。并且这意味着,首部长度必须是4字节的倍数,不足的用0填充。

(5) URG(urgent)标志:URG == 1表示此报文段中有紧急数据。发送方TCP会把紧急数据插入到本报文段数据的最前面以优先处理,这时要与首部中Urgent Pointer字段配合使用。

(6) ACK(acknowlegment)标志:ACK == 1时ACK序号字段才有效。TCP规定,在建立连接后所有传送的报文段都必须ACK置1。

(7) PSH(push)标志:一般用于即时交互。如果PSH == 1,发送方立即发送,接收方收到PSH == 1的报文段后立即交付给应用进程,而不再等到接收缓冲区填满后再向上交付。

(8) RST(reset)标志:RST == 1时表示TCP连接中出现严重错误必须释放连接,以便于重新建立连接。RST置1还用来拒绝一个非法的报文段或者拒绝打开一个连接。

(9) SYN(synchronization)标志:在连接建立时用来同步序号。当SYN == 1 && ACK == 0时,表明这是一个连接请求报文段,对方若同意建立连接则响应一个SYN == 1 && ACK == 1的报文段。

(10) FIN(finis)标志:FIN == 1表明发送方的数据已经发送完毕,接收方收到FIN报文后向应用进程交付一个EOF字符。注意TCP是全双工的,可以双向传输数据,发送一个FIN报文并不表示断开连接,而是终止写操作,套接字可能处于半连接状态,应用进程仍然可以从套接字读数据。

(11) 窗口大小:指的是接收窗口的大小而不是发送窗口的大小。窗口大小值告诉对方:接收方目前允许对方发送的数据量。

(12) 校验和:校验的范围包括首部和数据部分。和UDP数据报一样,计算校验和时要在TCP报文段的前面加上12字节的伪首部。接收方收到此报文段后也加上伪首部再进行校验。

伪首部并不真实存在数据包中,只是计算校验和时临时加在UDP数据报或TCP报文段前面。伪首部内容:4字节源IP地址、4字节目的IP地址、1字节填充0、1字节协议号(UDP是17,TCP是6)、2字节总长度。

校验的算法和IP首部校验的计算方法一样。注意IP首部的校验和字段仅对IP首部进行校验,而UDP和TCP的校验和字段是加上伪首部后对首部和数据进行校验。

(13) Urgent Pointer:URG == 1时才有意义。其值为紧急数据的字节数,也为普通数据第一个字节的偏移地址,所以名为指针。自己构造数据包时用不到。

下面看看struct tcphdr,在Ubuntu 11.10系统linux头文件目录/include/linux/tcp.h中有如下定义(以little-endian为例,条件编译分支就不抄了):

struct tcphdr{

__be16 source;

__be16 dest;

__be32 seq;

__be32 ack_seq;

__u16 res1:4, //xk> 注意res后面是数字1不是小写字母l

doff:4,

fin:1,

syn:1,

rst:1,

psh:1,

ack:1,

urg:1,

ece:1, //xk> 这两个不知道啥东西

cwr:1,

__be16 window;

__sum16 check;

__be16 urg_ptr;

};
其中有些成员在自己构造IP头部时用不到。

六、自行构造IP首部

自行构造IP头部时,有些字段可以不管,而有些字段如果不正确设置会导致数据包被直接丢弃,所以下面给出示例,节选自[《拒绝服务攻击》李德全著,电子工业出版社,2007. 由m3lt编写的Land攻击代码]

下面的代码比较老,iphdr和tcphdr结构成员的名称明显与上面讲的不一致,仅供参考。

struct pseudohdr{

struct in_addr saddr;

struct in_addr daddr;

u_char zero;

u_char protocol;

u_short length;

struct tcphdr tcpheader;

}pseudoheader; //计算TCP校验和字段时需要加上伪首部

char buffer[40]; // 注意40个字节,前半段是ip头部,后半段是tcp头部

struct iphdr *ipheader = (struct iphdr *)buffer;
// iphdr在<netinet/ip.h>中定义

struct tcphdr *tcpheader = (struct tcphdr *)(buffer + sizeof(struct iphdr)); // tcphdr在<netinet/tcp.h>中定义

if(-1 == (sockFd = socket(AF_INET, SOCK_RAW, 255))){

// error handler;

}

memset(buffer, 0, sizeof(struct iphdr) + sizeof(struct tcphdr));

ipheader->version = 4;

ipheader->ihl = sizeof(struct iphdr) / 4;

ipheader->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr));

ipheader->id = htons(0xF1C); // 为啥是0xF1C?

ipheader->ttl = 255;

ipheader->protocol = 6; // tcp的协议号是6

ipheader->saddr = inet_addr("sss.sss.sss.sss");
// 源IP地址

ipheader->daddr = inet_addr("ddd.ddd.ddd.ddd");
// 目的IP地址

tcpheader->th_sport = ntons(atoi("ssss")); // 源端口号。若为命令行参数需要从字符串转为数值

tcpheader->th_dport = ntons(dddd);
// 目的端口号

tcpheader->th_seq = htonl(0xF1C); // 貌似随便给1值

tcpheader->th_flags = TH_SYN;

tcpheader->th_off = sizeof(struct tcphdr) / 4;

tcpheader->th_win = htons(2048);

// 计算TCP首部校验和字段,IP首部校验和字段没设

memset(&pseudoheader, 0, 12 + sizeof(struct tcphdr));

pseudoheader.saddr.s_addr = xxx; // 上面的源IP地址

pseudoheader.daddr.s_addr = xxx; // 上面的目的IP地址

pseudoheader.protocol = 6;

pseudoheader.length = htons(sizeof(struct tcphdr));

bcopy((char *)tcpheader, (char *)&pseudoheader.tcpheader, sizeof(struct tcphdr));

tcpheader->th_sum = checksum((u_short *)&pseudoheader, 12 + sizeof(struct tcphdr));

if(-1 == sendto(sockFd, buffer, sizeof(iphdr) + sizeof(tcphdr), 0, (sockaddr *)&sin, sizeof(sockaddr_in))){

// error handler;

}

close(sockFd);

// checksum()定义

u_short checksum(u_short *data, u_short length){

register long value; // register……不用了吧……

u_short i;

for(int i = 0; i < (length >> 1); i++){

value += data[i];

}

if(1 == (length &1)){

value += (data[i] << 8);

}

value = (value & 65535) + (value >> 16);

return (~value);

}
自己构造数据包的IP头部,可以做很多坏事哦~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: