DoS源码拆解与隔离靶场复现:半连接、慢速连接与防御参数

发布时间:2026/11/6 6:47:56
DoS源码拆解与隔离靶场复现:半连接、慢速连接与防御参数

简介:这份DOS拒绝服务攻击源代码压缩包面向网络安全初学者、课堂实验者与攻防技术研究者,定位于拒绝服务攻击的原理剖析、代码阅读和防御思路学习,适合作为教学演示与自学样本,而非实际攻击工具。包内共6个文件,约11KB,以cpp源代码为核心,配以plg、opt、dsw、dsp、ncb等Visual C++工程、配置与浏览信息文件,便于在旧版开发环境中打开工程、编译并跟踪程序结构。已有517人学习下载,表明这份轻量样本在相关课程与自学场景中有一定参考价值。通过阅读代码,可对照理解SYN Flood、Ping Flood、UDP Flood等攻击如何占用连接、带宽与系统资源,并进一步联系防火墙过滤、限速、流量清洗、补丁更新和服务器配置优化等防护措施;工程文件也有助于梳理入口、模块与调试关系。相关技术仅限合法学习与防御研究,不得用于真实攻击。

1. 从一份 DOS 拒绝服务攻击源代码说起:读懂它比跑通它更值钱

我最早拿到一份 DOS 拒绝服务攻击源代码,第一反应是编译起来跑一遍看效果。结果把实验室出口的网关打到假死半分钟,运维同事的电话比日志先到。那份代码其实没什么魔法,几百行 socket 调用而已,真正难的不是“把包发出去”,而是“哪一个参数让服务端扛不住”。后来我改了个习惯:这类源码一律当教材读,先拆结构,再在隔离靶场跑最小复现,最后落回防御参数。这篇就按这个顺序走,给做网络安全研究的同学一条能照着做的复现路径,也给运维和开发一条从网络攻击代码反推配置的思路。所有操作只在我自建的、有授权的实验环境里做,不碰任何线上系统。

2. 拆解一份 DoS 源码:四层骨架、三条实现路线与环境确认

拿到源码先别急着make。我先做的一件事是把整个仓库的目录铺开,按“数据从哪进、在哪变形、从哪出去”画一条线。这一步花二十分钟,后面能省掉几小时。DoS 拒绝服务的源码看着五花八门,骨架其实高度一致,绝大多数都逃不出四层。认清楚这四层,再去看具体实现,判断速度会快很多。

2.1 从入口参数到发包的四层骨架

第一层,入口与参数解析。通常在main里,getopt或者手写 argv 遍历,把目标地址、端口、并发数、持续时间、源地址伪装开关读进全局结构体。这一层最容易看出作者的工程水平:参数有没有做边界检查、默认值是不是拍脑袋定的、atoi失败之后有没有兜底。我见过一份代码把线程数直接atoi(argv[3]),传个字母进去得到 0,pthread_create循环零次,程序秒退,日志里什么都没有,新手很容易以为“是防火墙拦了”,其实压根没发包。

第二层,目标与并发调度。循环创建线程、或者用select/epoll做单线程事件驱动。这一层决定了攻击的“节奏感”:是一次性把并发拉满,还是按速率匀速注入。速率型的代码里一般能看到一个usleep或者时间轮,匀速型的复现价值更高,因为流量曲线干净,观测端好对照。固定线程池的代码则常常有个坑——线程创建失败不重试,实际并发数远小于设定值,你以为打了 500 并发,实际只起来 200。

第三层,报文构造。分两种。一种是直接调内核 socket,socket()→connect()或send(),代码短,但受协议栈约束,源地址伪装做不了。另一种是自己填 IP/TCP 头,走原始套接字,要自己算校验和、自己维护序列号,代码量翻几倍,但可控性强。判断一份源码属于哪种,看有没有IP_HDRINCL(原始套接字自己填 IP 头)或者SOCK_RAW就够了。

第四层,状态维持与回收。这是最容易被忽略、却最影响“打不打得动”的一层。半连接类代码需要保证连接在一段时间内不被本地内核回收,否则你刚打出去,本地就发 RST 把连接关了,服务端队列立刻释放。应用层慢速类代码需要维持住已建立的连接并周期性发一点数据保活。回收逻辑写不好,复现出来就是“打了几秒就没了”,很多人卡在这,以为代码有问题,其实是本地状态被清掉了。

读源码的时候我会在这四层各标一个文件名,然后重点看第三层和第四层。前两层是工程问题,后两层才是技术核心。

2.2 三条实现路线对比:原始套接字、半连接、应用层慢速

市面上叫“DoS 源码”的东西,实际走的路线差别很大,选哪条路线复现,取决于你想验证什么。我把三条主流路线按我实际用过的感受列一下。

维度原始套接字自构报文TCP 半连接(握手不完成)应用层慢速连接
所需权限root 或CAP_NET_RAW普通用户即可普通用户即可
核心代码量大,需自算校验和、维护序列号小,几十行到两百行最小,逻辑以 sleep 和分片发送为主
对中间设备的可见性畸形包特征明显,易被清洗中,混在正常 SYN 里低,流量和正常用户几乎一致
复现观测难度高,需要抓包工具逐条看中,看队列计数即可低,看连接数和响应时间
主要学习价值理解 IP/TCP 报文结构与校验理解 accept 队列、半连接队列与超时理解 worker 模型与超时配置
常见翻车点校验和算错导致全部丢包本地内核提前回收连接服务端超时太短,连接维持不住

我一般会这么比:如果目的是理解协议栈,走原始套接字,哪怕代码跑不通,把校验和算法吃透也值;如果目的是验证服务端的队列与超时配置,走半连接,最直接,数据也最好看;如果目的是验证应用层并发模型,走慢速连接,因为它的流量特征最温和,不容易被中间设备打断,对照组最干净。

三条路线的“性价比”在不同阶段不一样。刚入门的时候我建议从半连接起步,参数少、观测直观、失败原因单一;原始套接字适合作为第二阶段,用来补协议细节;慢速连接看着最简单,实际上对“节奏控制”要求最高,稍不注意就变成压测而不是复现。

2.3 编译之前先确认三件事:隔离、权限、授权

这一步我踩过坑,所以现在当成固定动作。

隔离。靶场必须放在一个没有外网出口的虚拟网段里,攻击机和靶机在同一台宿主机的 host-only 网络上。我用的是一个192.168.56.0/24的纯内网段,靶机不给默认网关。为什么强调这点——半连接类代码如果目标不可达,本地内核会不断重传 SYN,重传包会顺着默认路由跑出去,打到你不认识的地方。我第一次就是没断外网,重传包顺着默认网关出去了,虽然只持续了半分钟,但足够让运维找上门。

权限。走原始套接字的代码需要 root 或者CAP_NET_RAW;半连接和应用层慢速的代码用普通用户跑就行,不需要提权。看到一份源码要求你sudo跑,先搞清楚它为什么要这个权限,如果只是半连接,那多半是作者图省事。给最小权限,是复现时的基本纪律。

授权。靶机是自己建的、或者书面授权过的测试机,地址写死在代码里而不是从命令行随便传。我把目标地址做成编译期常量而不是运行时参数,就是为了防止手滑输错一个数字。

编译层面还有个细节:依赖libnet、libpcap这类库的代码,先pkg-config --libs确认路径,别直接抄 Makefile 里的-I/usr/local/include,路径不对报的错是“找不到头文件”,新手容易误以为是缺包。

3. 隔离靶场复现:把一次半连接和慢速连接跑出可观测数据

复现的目标不是“把机器打挂”,而是“拿到一条能解释现象的曲线”。所以我在复现时会把并发数压得很低,低到刚好能让队列出现积压、但不至于把靶机拖死。这样观测端能看清每一条连接的去向,参数改一格就能看到对应变化。

3.1 靶场拓扑与三层隔离

拓扑很简单:一台攻击机(虚拟机)、一台靶机(虚拟机),同一宿主机的 host-only 网段,靶机不开默认网关。靶机上跑一个最小 Web 服务,监听 80。

隔离做三层:

  • 网络层:host-only 网段,无外网出口,攻击机的默认路由指向一个不存在的网关,确保任何“跑偏”的包原地消失。
  • 资源层:靶机限制为 1 核 1G,这样队列积压现象在低并发下就能出现,不需要打很大的流量。
  • 观测层:攻击机开抓包,靶机开连接状态采样,两边时间戳对齐。

靶机上先备好观测命令,复现时随时调用:

# 靶机侧:一次采样,记录半连接数、accept 队列溢出、连接总数 ss -ant state syn-recv | wc -l # 当前处于半连接状态的条目数 ss -ant state established | wc -l # 当前已完成握手的连接数 netstat -s | grep -iE "listen|overflow|SYNs" # 队列溢出与 SYN 相关计数

这三条命令的输出,就是后面判断“打到了没有”的唯一依据。我习惯把它们包成一个脚本,每秒落一行到文件,复现结束后直接看曲线。参数说明:ss -ant的-a是所有套接字、-n是数字端口、-t是 TCP;netstat -s里的 overflow 计数是关键,它涨了就说明队列真的满了。

3.2 最小半连接实验:代码与参数逐个解释

下面是用来验证半连接队列行为的最小片段,只对隔离靶场里的固定地址生效,并发上限写死得很低。它不构造原始报文,只是让内核发起连接后不完成握手,目的是观察服务端syn-recv队列的变化。

import socket import time TARGET = ("192.168.56.10", 80) # 写死隔离靶场地址,不接受运行时传参 ROUNDS = 30 # 并发上限,先小后大,观察队列拐点 INTERVAL = 0.05 # 每条之间的间隔,方便抓包端逐条对照 held = [] for i in range(ROUNDS): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setblocking(False) # 非阻塞,避免 connect 卡住整个循环 try: s.connect_ex(TARGET) # 只发出 SYN,不等握手完成就返回 except OSError: pass held.append(s) # 关键:不 close,保持半开,让服务端队列持续占用 time.sleep(INTERVAL) time.sleep(10) # 观察窗口,此时去靶机执行 3.1 里的采样命令 for s in held: # 统一回收,避免残留状态影响下一轮 s.close()

逐项解释一下参数怎么改、改了会怎样:

  • ROUNDS是这轮实验的并发上限。从 30 开始,靶机syn-recv计数应该能看到明显的台阶式上升;如果调到 300 现象不变,说明靶机的半连接队列上限远大于 300,或者中间有设备替你挡了。
  • INTERVAL控制注入节奏。设成 0 就是一波流,队列瞬间冲高,观测端只能看到一个尖峰;设成 0.05 能看出累积过程,更适合对照队列上限。
  • time.sleep(10)是观察窗口。这段时间里靶机的syn-recv应该维持在高位不掉,如果掉得很快,说明服务端的握手超时配得很短(常见是 1 秒级),这也是一个有效结论。
  • s.close()一定要执行。不回收的话,本地文件描述符会耗尽,第二轮实验直接报Too many open files,这是新手最常遇到的“第二轮跑不动”。

这段代码的价值不在于“能打多少并发”,而在于它把“服务端队列是怎么被占住的”这件事变成了可观测的。看懂这个,再回头看原始套接字版本的源码,你就知道它多做的那部分工作(自填 IP/TCP 头、自算校验和)本质是在绕过内核的握手管理,直接往对端塞 SYN,代价是复杂度和被中间设备识别的概率都上去了。

3.3 观测端:怎么确认“打到了”而不是“自己卡了”

复现失败最常见的原因不是靶机太强,而是攻击机自己先卡住了。区分方法很简单:两边同时看。

攻击机侧:

# 攻击机侧:观察本地连接状态分布与重传 ss -ant | awk '{print $1}' | sort | uniq -c # 各类 TCP 状态计数 ss -s # 汇总视图,看是否有大量 SYN-SENT 堆积

如果攻击机上SYN-SENT大量堆积且长时间不减少,说明包出去了但没人应答,问题在链路或目标不可达,不在服务端。这时候去看靶机的收包计数:

# 靶机侧:确认包到底有没有进来 netstat -s | grep -iE "SYNs to LISTEN|overflow"

SYNs to LISTEN计数在涨,说明包到了、服务端也收到了 SYN,只是没完成握手——这才叫“打到了”。如果这个计数纹丝不动,而攻击机SYN-SENT很高,那基本可以判定包在中间被丢了,或者目标地址写错了。

我一般会这么排:先确认三层(攻击机发出 → 链路可达 → 靶机收到),再谈效果。跳过前两步直接调并发,是最常见的浪费时间的做法。

3.4 慢速应用层连接:为什么二十个连接就能占满两百个 worker

半连接验证的是传输层队列,慢速连接验证的是应用层并发模型。这两件事的失效机理完全不同,很多人混在一起谈,结论就不可靠。

慢速连接的原理很朴素:建立完整连接后,把请求头一个字节一个字节地发,或者发完头部不发 body,让服务端一直处于“等数据”的状态。服务端的 worker 是有限的,一个 worker 被一个慢连接占着,就少一个能处理正常请求的 worker。

复现时我控制两个量:连接数和每个连接的发包间隔。

import socket import time TARGET = ("192.168.56.10", 80) CONNS = 20 # 连接数,从个位数开始试,观察服务端活跃连接数 GAP = 5 # 每个字节之间的间隔(秒),必须大于服务端读超时才有意思 held = [] for _ in range(CONNS): s = socket.create_connection(TARGET, timeout=3) s.sendall(b"GET / HTTP/1.1\r\nHost: lab\r\n") # 只发一部分头部,故意不发结束空行 held.append(s) for s in held: try: s.sendall(b"X-Pad: a") # 每次只推一点点,维持连接不被判空闲 except OSError: pass time.sleep(GAP)

参数怎么调:

  • CONNS决定占用多少个 worker。理论上的临界值是“服务端 worker 总数 + 1”,超过这个数,正常请求开始排队或超时。从 20 开始试,靶机如果配的是 200 个 worker,20 个连接不会有明显现象——这时候不是代码不行,是量不够,这一点必须说清楚,否则很多人第一次跑就放弃了。
  • GAP决定连接能维持多久。如果服务端的读超时是 60 秒,GAP必须小于 60,否则连接被服务端主动关掉,占用立刻释放。所以复现之前先摸清靶机的超时配置,这一步比写代码重要。
  • 断开时要逐条close(),否则靶机侧的连接会一直挂到超时,影响下一轮对照。

观测端看两个数:靶机的established连接数是否长时间维持在高位,以及正常请求的响应时间是否被拉长。第一个数涨、第二个数也跟着涨,才算复现成功。只涨第一个数而响应时间不变,说明 worker 还没被占满。

4. 避坑与排查:五条踩出来的经验

下面五条都是我在复现过程中真实撞上的,按“现象 → 原因 → 解决”记下来,省得再走一遍。

第一条:程序秒退,什么日志都没有。现象是编译通过、运行后瞬间回到提示符,靶机侧观测无任何变化。原因多半出在参数解析层——并发数用atoi解析失败得到 0,循环体一次都没执行;或者目标地址格式写错,inet_addr返回INADDR_NONE,代码没有校验就继续走。解决方式很土但有效:在进入主循环前打印一行解析结果,把目标地址、端口、并发数、持续时间全部打出来,对不上就是参数问题,不用往下查。

第二条:第一轮有效果,第二轮完全没反应。现象是同一份代码连跑两次,第一次靶机syn-recv明显上涨,第二次毫无变化。原因是第一轮结束后没有回收 socket,文件描述符耗尽,socket()返回 -1,代码没判错继续往下跑,实际一个包都没发。解决方式是把socket()的返回值判空、把close()放进统一回收流程,另外在复现前后各看一次ulimit -n和当前进程的 fd 数量。

第三条:包发出去了,抓包也看到了,但服务端计数不涨。现象是攻击机上SYN-SENT一堆,靶机抓包能看到 SYN 进来,syn-recv计数却纹丝不动。原因常见于中间有状态防火墙或反向代理替服务端回了 SYN-ACK,真正的后端压根没看到连接。解决方式是绕过中间设备直连后端做一次对照,或者直接看后端进程自己的连接计数。这一条最坑,因为它会让你怀疑代码,实际上代码是对的。

第四条:原始套接字版本全丢包。现象是自构报文的代码跑得飞快,但靶机一个包都没收到。八成的概率是 IP 头校验和或 TCP 校验和算错。解决方式是先算 IP 头校验和、单独验证,再加 TCP 校验和(注意伪首部里的协议号和长度字段)。另外IP_HDRINCL打开后内核不再帮你填某些字段,total length和id都要自己维护,漏一个就整包被丢。

第五条:慢速连接维持不住,几十秒后全部断开。现象是刚建立一批连接就快速消失,靶机established数先涨后掉。原因是服务端读超时短于你的发包间隔,或者中间设备有连接空闲超时。解决方式是把发包间隔压到服务端超时的一半以内,同时确认中间设备的空闲超时。另一个容易忽略的点是 TCP keepalive——它默认两小时才探一次,指望它维持连接不现实。

提示:上面五条里,第三条和第五条最容易被误判成“代码不行”,实际是环境配置问题。复现之前先把靶机的超时参数和中间链路摸清楚,能省一半时间。

5. 从攻击代码反推防御:一份能直接对照改的参数清单

把源码读透、在靶场跑通之后,最有价值的产出其实是防御侧的那张表。攻击代码里的每一个“有效参数”,在防御侧都有对应的一个配置项;你在攻击侧调哪个参数现象变化最明显,防御侧就优先改那一项。

下面这张表是我这几年反复对照下来的,左侧是复现时能观察到的现象,右侧是对应的配置方向和判断依据:

观察到的现象攻击侧起作用的因素防御侧优先改的配置判断依据
syn-recv队列长时间高位半连接占用量半连接队列长度、SYN 超时时间、SYN Cookie队列长度要覆盖正常峰值,超时要短于业务容忍窗口
队列溢出计数持续上涨注入速率超过处理速率队列溢出后的降级策略、限速规则溢出计数是硬指标,涨了就说明容量不够
established高但响应慢worker 被慢连接占满读超时、单连接最小速率限制、worker 上限超时必须小于正常用户的最慢请求耗时
正常请求排队超时应用层并发耗尽连接数上限、排队超时、按来源限并发先限单来源,再调总量,否则容易误伤
攻击机侧SYN-SENT堆积目标不可达或被丢弃不用改,检查链路与清洗策略这一步是排查手段,不是防御点

具体的参数动作,我一般按这个顺序做:

先改超时。半连接的超时从默认的几十秒压到个位数,应用层的读超时压到“正常最慢请求耗时 × 1.5”。这一项改完,同样的攻击量下队列占用时间会成倍下降,是性价比最高的一刀。

再调容量。半连接队列长度按业务峰值的两倍给,同时打开 SYN Cookie 作为溢出后的兜底。注意 Cookie 打开后在极端情况下会丢失部分 TCP 选项(比如窗口缩放),大带宽链路上要评估一下副作用。

最后加限速。按来源 IP 限新建连接速率,按单连接限最小传输速率,超限直接断。这一步放最后,是因为前两步没做的时候,限速规则很容易把正常用户一起限掉。

回过头看,我从一开始“编译跑一遍看效果”的莽撞,到现在“先拆四层、再跑最小复现、最后落一张防御参数表”的固定流程,中间交了不少学费。教训其实就一条:这类代码的价值不在于它能打多大,而在于它把服务端的某一条边界暴露得清清楚楚。你把那条边界找出来、量出来,防御就有依据;找不出来,加多少带宽都是玄学。

从那以后我每次拿到一份新的攻击源码,都强制走一遍“拆结构 → 最小复现 → 对照防御参数”这三步,一步不跳。慢一点,但结论站得住。这份源码和配套的靶场脚本,按这个流程过一遍,你自己也能得出属于你环境的参数结论。希望帮到你。

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