☰
拆解DoS源码:连接型攻击的队列观测与限速兜底
2026/11/2 6:53:00 网站建设 项目流程

简介:这份DOS拒绝服务攻击源代码压缩包面向网络安全学习者与攻防实验人员,聚焦拒绝服务攻击的实现机制与防御思路,可作为理解SYN Flood、Ping Flood、UDP Flood等典型手法原理的参考样本。包内共6个文件,约11KB,以cpp源程序为核心,辅以Visual Studio工程配置类的plg、opt、dsw、dsp文件及ncb浏览数据库,构成可直接在旧版VC环境中打开的工程结构。已有517人学习下载。读者可借此观察攻击端发包逻辑的组织方式、参数设置与代码骨架,进而对照防火墙过滤、限速、流量清洗等防护策略理解攻防对应关系。需注意,代码仅供理论分析与安全教学使用,任何实际攻击行为均属违法,应在合规环境中开展学习。

1. 看到「DOS拒绝服务攻击源代码」,先弄清它到底在写什么

某公司内网一台测试机忽然连不上,运维第一反应是“被人打了”,查到最后发现是自己人从网上抄了段脚本在跑。公开渠道流传的这类源码,八成残缺、掺了后门,或者干脆拿别的项目改个名字。真正有价值的不是那几百行代码,而是它的结构:怎么选目标、怎么组织发包、怎么控制节奏、怎么管理自己的状态。看懂这四件事,你才知道自己的服务会先在哪一层塌掉。这篇面向要在隔离环境里做验证的安全工程师和运维,从结构拆到本地复现、流量识别和限速兜底。

2. 拆开一份DoS源码:发包循环、目标选择、节奏控制和状态管理

2.1 四层结构:为什么“能打出去”和“打得久”是两件事

一份能跑起来的实现,剥掉混淆和外壳,基本都归到四层:发包层、目标层、调度层、控制层。发包层按协议栈要求把报文拼出来,或者直接调 socket 走完三次握手;目标层管地址列表、端口、URL 路径、请求头模板;调度层决定并发模型是多进程、多线程还是事件循环,以及速率、重试和退避;控制层负责启停、进度上报和异常重连。公开流传的版本,八成只把前两层做完了。

“能打出去”和“打得久”完全是两件事。发包层写对了,第一秒就有流量;调度层没写对,跑十分钟自己的进程先被文件描述符耗尽、内存涨爆或者被本机内核限速,效果断崖式下跌。最典型的翻车是每发一个包就新建一个线程,跑到八百并发时进程直接 OOM。这不是目标扛不住,是发起端自己先崩了。

从防守视角看这个结构反而很有用:你能大致判断对方能力停在哪一层。只会拼报文的,通常一次性的多;带速率控制和掉线重连的,才值得认真对待。我见过的大部分“打挂线上”的案例,靠的不是多强的代码,而是目标侧队列配得太保守。

2.2 三类攻击面:带宽型、连接型、语义型,为什么连接型最常见

带宽型拼的是流量体积,需要资源,普通人玩不动。连接型拼的是状态表项:SYN 半连接、慢速请求、大量空闲 keep-alive,都能用极小的成本占住服务端的队列槽位。语义型最阴,单个请求就能触发昂贵的后端计算,比如复杂查询、大图缩放、正则回溯,一个 QPS 顶别人一千。

连接型之所以最常见,原因很朴素:门槛低。一份几十行的脚本,一台普通机器,就能把默认队列不长的服务打到溢出。要理解这个,得先看清两套队列。内核收到 SYN 后放进半连接队列(受tcp_max_syn_backlog约束),三次握手完成后移进 accept 队列(受 listen backlog 和somaxconn共同约束),应用再调用 accept 取走。半连接不占文件描述符,但占队列槽位;accept 队列满了,内核默认丢包或者回 RST,客户端看到的就是“连不上”和“连上就断”混着来。

慢速请求走的是另一条路:连接已经建立,请求头一点一点发,服务端读缓冲一直挂着,worker 就被这条连接占着。这类手法对并发模型是“每个连接一个线程”的服务杀伤力最大,对事件循环模型则没那么致命,差别全在架构上。

2.3 把源码参数和实际效果对上:六个必看的量

源码里的参数常见写法主要影响观测指标调大的后果
并发连接数100~2000 线程/协程发起端 fd 与内存进程 RSS、ss -s的 orphan发起端先 OOM
建连间隔sleep 0~10ms服务端 SYN 到达速率TcpExtListenOverflows本机被限速、丢包
目标端口80/443/8080命中哪条 listen 队列ss -lnt的 Recv-Q换端口即失效
连接保持时长0~60s 不主动关accept 队列占用ss -ant state established被 idle timeout 清掉
请求头长度几百字节到几十 KB服务端读缓冲worker 内存曲线触发 header 上限被拒
重试次数3~10冲击的持续性单源包速率被源限速拦掉

这张表是给防守方反推用的。你在流量里看到 SYN 到达速率突然抬到平时的几十倍、连接保持时间固定在某个整数附近、源端口按固定步长递增,基本能倒推出对方脚本里那几个参数是怎么写的,也就知道该在哪一层加限制。

提示:默认 listen backlog 在很多发行版上并不大,服务启动参数里不显式设置的话,队列长度取决于内核somaxconn和程序传入值取小。这一点在压测前先确认,否则你测出来的“服务很脆”其实是配置问题。

3. 在隔离环境里复现:本机回环上的最小实验

3.1 先把环境关起来:网络命名空间加回环

别在办公网里“小试一下”,哪怕只跑十秒。最省事的隔离方式是建一对网络命名空间,中间只连一根 veth,两端都没有默认路由,出不去也进不来。

# 建一对隔离的网络命名空间,只靠一根 veth 相连,外部无法路由进来 ip netns add lab-a ip netns add lab-b ip link add veth-a type veth peer name veth-b ip link set veth-a netns lab-a ip link set veth-b netns lab-b # 选一个和你现有网段不冲突的私有段 ip netns exec lab-a ip addr add 10.200.0.1/24 dev veth-a ip netns exec lab-b ip addr add 10.200.0.2/24 dev veth-b ip netns exec lab-a ip link set veth-a up ip netns exec lab-b ip link set veth-b up ip netns exec lab-a ip link set lo up ip netns exec lab-b ip link set lo up # 验证:a 能到 b,b 出不去 ip netns exec lab-a ping -c 2 10.200.0.2 ip netns exec lab-b ip route

逻辑说明:lab-a当发起端,lab-b当服务端,两边各有一个回环口给本机进程用。地址段只是举例,换成你自己环境里没被占用的 /24 就行,关键是别和办公网、容器网段撞上,否则路由一乱,你以为的隔离其实是假的。

参数说明:veth是一对虚拟网卡,删掉一端另一端自动消失,清理时ip netns del lab-a就够。ip route在lab-b里应该只能看到 10.200.0.0/24 这一条直连路由,没有 default,这就是隔离成立的证据。

3.2 一条最小的连接压力脚本:只打回环,带速率上限

下面这段不是攻击工具,是把“连接型手法”的循环结构抽出来做的事实验证:目标硬编码在回环地址,速率和时长都可控,跑完自己就停。

import socket, time, threading TARGET, PORT = "127.0.0.1", 8080 # 只打回环;换成别的地址前先拿到书面授权 RATE, HOLD, DURATION = 200, 5, 20 # 每秒新建连接数 / 每条保持秒数 / 总时长 # 稳态下:新建速率 ≈ 并发数 / 保持时长,所以并发数 = RATE * HOLD CONCURRENCY = RATE * HOLD ok = 0 lock = threading.Lock() stop_at = time.time() + DURATION def worker(): global ok while time.time() < stop_at: try: s = socket.create_connection((TARGET, PORT), timeout=1) except OSError: time.sleep(0.02) # 建连失败让出 CPU,避免发起端空转把自己打满 continue with lock: ok += 1 time.sleep(HOLD) # 占住 accept 队列槽位,模拟长连接 s.close() ts = [threading.Thread(target=worker, daemon=True) for _ in range(CONCURRENCY)] [t.start() for t in ts] [t.join() for t in ts] print(f"成功建连 {ok} 次,名义速率 {RATE}/s,实际 {ok / DURATION:.0f}/s")

逻辑说明:线程数等于RATE × HOLD,每条连接活HOLD秒再关,稳态下每秒正好补齐RATE条新建连接。这是“连接保持”这类手法最核心的算式,看懂它,你就知道为什么对方用几百个并发就能压出几千的建立速率。

参数说明:RATE决定服务端 SYN 的到达速率,HOLD决定队列被占用的时长,两个一起调才能覆盖不同的队列行为。timeout=1是防止服务端已经丢包时脚本卡死。失败分支里的sleep(0.02)很关键,少了它,建连失败会变成忙等,CPU 先打满的是发起端。

跑之前先确认本机 fd 上限够用,ulimit -n至少要大于CONCURRENCY加余量,否则你会看到一堆Too many open files,误以为是服务端拒绝了你。

ulimit -n 8192 ip netns exec lab-a python3 pressure.py

注意:这段代码只对回环地址生效。真要对任何非自有环境做压力验证,先拿书面授权、定好时间窗和回滚方案,否则就是事故,不是测试。

3.3 用 ss 和 tcpdump 看半连接堆在哪

发起端跑起来之后,服务端那边要同时开两个观测窗口。

# 每 1 秒打印一次连接汇总和 8080 的 listen 队列 ip netns exec lab-b bash -c \ 'while :; do ss -s | head -3; ss -lnt "( sport = :8080 )"; sleep 1; done' # 只抓与 8080 相关的握手包,看 SYN 之后有没有 ACK 回来 ip netns exec lab-b tcpdump -ni veth-b \ 'tcp port 8080 and (tcp[tcpflags] & (tcp-syn|tcp-ack)) != 0' -c 40

逻辑说明:ss -s给出全局的连接状态汇总,ss -lnt里的Recv-Q就是当前 listen 队列里排着的连接数。tcpdump 抓到的包序列最能说明问题:正常握手是 SYN、SYN-ACK、ACK 三个来回;队列溢出时你只看到 SYN 进来,服务端不回 SYN-ACK,或者回了但客户端没收到 ACK。

参数说明:过滤器里tcp[tcpflags] & (tcp-syn|tcp-ack) != 0的意思是“只要带 SYN 或 ACK 标志位的包”,这样能把纯数据包过滤掉,减少噪音。-c 40抓够 40 个就停,避免文件写太大。

再看内核计数器的增量,这是比ss更硬的证据:

ip netns exec lab-b nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtTCPReqQFullDrop

三个计数器分别对应:accept 队列满导致的溢出、listen 阶段的总丢包、半连接队列满导致的丢弃。这三个数在空闲时应该完全不涨,只要开始增长,就说明队列已经被打满,而不是应用处理慢。

3.4 三个观测点对齐:连接数、队列溢出、应用延迟

单看任何一个指标都会误判。连接数涨可能只是业务高峰,队列溢出涨可能是配置太保守,应用延迟涨可能是后端数据库慢。要把三者放在同一根时间轴上。

具体做法是每 2 秒采样一次,记成三列:发起端看到的 established 连接数、服务端ss -lnt的Recv-Q、用curl -o /dev/null -s -w '%{time_total}'打一次业务接口拿到的耗时。然后看三列拐点是否落在同一秒。如果队列先满、延迟紧跟、连接数还在涨,那是标准的队列打满;如果延迟先涨、队列没动,问题在后端,不在网络层。

这套对齐方法我用得最多,它能把“玄学”变成可复现的曲线。压测报告里贴三条曲线,比写十段文字描述有用。

4. 从流量里认出DoS:三个可量化的判定信号

4.1 信号一:SYN_RECV 的增长斜率,而不是绝对值

绝对值的坑在于基线不清楚:一台常年挂着几千个 TIME_WAIT 的服务器,你看到数字大不代表被打了。真正有意义的是斜率。

# 每 2 秒记录一次半连接数和溢出计数 for i in $(seq 1 60); do ts=$(date +%s) syn=$(ss -ant state syn-recv | wc -l) ovf=$(nstat -az TcpExtListenOverflows | awk 'NR==2{print $2}') echo "$ts $syn $ovf" sleep 2 done > /tmp/syn.log # 算溢出计数的增长速度 awk 'NR==1{t0=$1; o0=$3} END{ d = ($3 - o0) / ($1 - t0); printf "溢出速率 %.2f 次/秒, 末次半连接数 %d\n", d, $2 }' /tmp/syn.log

逻辑说明:先记下空闲期十分钟的基线,正常情况下溢出速率为 0。压测开始后这个值会在几秒内从 0 抬起来,抬起的快慢就是斜率,斜率直接反映对方的建连速率。

参数说明:2 秒的采样间隔是折中,太密会自己制造负载,太稀会漏掉短时脉冲。awk 'NR==2{print $2}'取的是nstat输出第二行的第二个字段,也就是计数器的值,不同版本字段位置可能不同,先手动跑一次确认再写进脚本。

4.2 信号二:源地址与源端口的分布熵

单个 IP 的连接数高不一定是攻击,NAT 出口后面挂一整栋楼也是常态。要看分布形状。

from scapy.all import rdpcap, IP, TCP from collections import Counter import math pkts = rdpcap("/tmp/lab.pcap") # 只取带 SYN、不带 ACK 的包,排除握手后续 syn = [p for p in pkts if IP in p and TCP in p and p[TCP].flags & 0x02 and not p[TCP].flags & 0x10] srcs = Counter(p[IP].src for p in syn) sports = Counter(p[TCP].sport for p in syn) def entropy(c): n = sum(c.values()) return -sum((v / n) * math.log2(v / n) for v in c.values()) print("SYN 总数", len(syn)) print("独立源地址", len(srcs), "独立源端口", len(sports)) print("源地址熵 %.2f 源端口熵 %.2f" % (entropy(srcs), entropy(sports))) print("Top5 源", srcs.most_common(5))

逻辑说明:熵衡量的是分布的均匀程度。真实业务的源地址熵高但增长平缓,源端口也接近均匀;单机伪造源地址的手法,常常出现“源地址熵很高、源端口却集中在某个递增区间”这种不匹配。两个熵一起看,比单看哪一个都准。

参数说明:TCP.flags & 0x02判断 SYN 位,0x10是 ACK 位,组合起来就是“只留首次握手包”。阈值没有通用数字,必须在自己的业务上跑一周拿到基线,再看偏离多少个标准差。

4.3 信号三:QPS 与并发连接数的背离

这是最强的一个信号,因为它把应用层和网络层放在一起看。正常业务里,流量涨则并发连接涨,QPS 和连接数同向;被连接型手法压住的时候,连接数暴涨而 QPS 不涨甚至下跌,请求耗时同步飙升。

先让访问日志带上请求耗时:

log_format timed '$remote_addr - [$time_local] "$request" ' '$status $body_bytes_sent rt=$request_time'; access_log /var/log/app/access.log timed;

再按 10 秒窗口聚合:

# 假设 rt= 出现在最后一个字段之前,取出请求耗时做窗口平均 awk '{ split($4, a, ":"); # $4 形如 [10/Oct/2025:13:45:22 win = a[2] ":" int(a[3]/10)*10; # 按 10 秒取整做窗口 n[win]++; if (match($0, /rt=[0-9.]+/)) { rt = substr($0, RSTART+3, RLENGTH-3) + 0; s[win] += rt; } } END { for (w in n) printf "%s 请求=%d 平均耗时=%.3f\n", w, n[w], s[w]/n[w]; }' /var/log/app/access.log | sort

逻辑说明:窗口内的请求数近似 QPS×10,平均耗时是这一窗的整体响应水平。把这张表和ss -ant state established | wc -l的采样放在一起看,背离一眼就能看出来。

参数说明:$4的位置取决于日志格式,改成自己的字段索引;int(a[3]/10)*10是窗口取整,想要 1 分钟粒度就把 10 改成 60。这套聚合只在单机日志上成立,多机要先按实例分开算再汇总,混在一起算会掩盖单点异常。

注意:三个信号要联合判断。只看熵容易把 CDN 回源流量误判成攻击,只看 QPS 背离容易把慢 SQL 当成外部压力。任何一个信号异常,都先去对应层拿第二个证据。

5. 避坑与排查:五条踩过的血泪经验

5.1 测量与环境上的坑

坑一:在办公网里“小试一下”。现象是同事的 CI 忽然大面积超时,排查半天找不到原因。原因是脚本虽然只跑了几十秒,但 SYN 打满的是共用的网关会话表,影响面远大于你以为的那台测试机。解决办法很直接:任何压力验证只在网络命名空间或 host-only 虚拟机里做,做完ip netns del清干净,别留后门进程。

坑二:只在一台机器上压,结论完全反了。现象是脚本跑到一半报Too many open files,目标端毫无反应,于是得出“服务很抗打”的结论。原因是默认 fd 上限通常只有 1024,而RATE × HOLD轻易就超过这个数,发起端先趴下了。解决办法是压测前先ulimit -n调到并发数的两倍以上,并且在脚本里统计“成功建连数”而不是“发起次数”,用成功数算速率。

坑三:看到大量 ESTAB 就判定被攻击。现象是ss -ant state established | wc -l常年几千,值班同学每天报警一次。原因是连接池和 keep-alive 会让长连接常驻,这属于正常形态;真正的异常在新建速率和队列溢出上。解决办法是把监控口径从“连接总数”换成“每秒新建连接数”加“Recv-Q占 backlog 的比例”,前者看趋势,后者看水位。

5.2 配置与处置上的坑

坑四:把 backlog 调到 65535 就以为万事大吉。现象是参数调完,TcpExtListenOverflows还在涨。原因是队列只是缓冲,应用 accept 的速度没变,队列再长也只是把雪崩时间往后推几秒;而且内核somaxconn不跟着调,程序传进去的大值会被截断。解决办法是三个值一起改:net.core.somaxconn、net.ipv4.tcp_max_syn_backlog、应用侧的 listen backlog,改完重启服务并确认ss -lnt的Send-Q真的变成了新值。

坑五:只加限速不看业务,上线当天误伤一片。现象是限速规则生效后,某个地区的用户集中反馈登录失败。原因是按源地址限速在 NAT 场景下会把一整个出口的用户算成一个人,rate=10r/s对个人够用,对一栋楼就是灾难。解决办法是分级:静态资源按 IP 限,接口按登录态或设备标识限,并且配置burst让正常突发能排队通过,而不是直接 503。

这五条里,前三条是测量方法的问题,后两条是处置策略的问题。测量错了,后面所有判断都是空中楼阁;处置激进,防线自己就成了故障源。我在两个方向上都翻过车,代价都是半夜被叫起来。

6. 把队列、限速和超时调成一组:一次能复现的回归验证

单独调一个参数意义不大,队列、限速、超时必须成组调,否则你会看到“队列变长了但延迟更高”这种反向结果。

参数位置起始值目标值作用
net.core.somaxconn内核1284096listen 队列硬上限
net.ipv4.tcp_max_syn_backlog内核1282048半连接队列上限
worker_connections应用5124096单 worker 并发连接
limit_req rate应用未设10r/s单源请求速率
proxy_read_timeout应用60s10s慢连接占用时长

限速部分按维度分开配,别一把梭:

# 两个维度:按源地址限请求速率,按源地址限并发连接数 limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s; limit_conn_zone $binary_remote_addr zone=connip:10m; server { location /api/ { limit_req zone=perip burst=20 nodelay; # 允许 20 个突发,超出直接拒 limit_conn connip 50; # 同源最多 50 条并发连接 proxy_read_timeout 10s; # 慢连接 10 秒内没动静就断 } }

burst=20 nodelay的意思是允许 20 个突发请求立即通过,再多就拒;不加nodelay会变成排队,排队本身又会占连接,反而帮了对手。limit_conn是兜底,防的是“请求很慢但连接很多”这一类。

改完必须做一次回归,方法就是把 3.2 的脚本再跑一遍,对比三个数:成功建连数、TcpExtListenOverflows增量、业务接口的 p95 耗时。调优前溢出计数器每秒涨几十,调优后应该稳定在 0,同时 p95 不因为限速而变差;如果 p95 涨了,说明burst给太小,正常突发被拒后客户端在重试。

我现在的习惯是:任何一次参数变更,都先把这三个数抄在变更单上,改完再抄一遍,对不上就不上生产。这个习惯是从一次“调完 backlog 反而更慢”的事故里换来的,没有后悔药,只能靠流程。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询