大家第一次接触“DDoS攻击”这个词,多半是在新闻里看到某某网站突然打不开,官方通告说“遭到恶意流量攻击”。新闻报道往往到此为止,但“到底是怎么打的”“为什么一堆机器同时打才会瘫痪”“被打的那一方怎么知道谁在攻击的”,这些问题,只有真正动手做一遍实验,才会形成清晰的认知。
我这次在VMware虚拟机上完整走了一遍复现链路:用两台Linux虚拟机搭了一个完全隔离的实验网络,一台作为攻击源,用Python写UDP洪泛脚本对另一台Ubuntu虚拟机发起单源DoS攻击,再站在被攻击机的视角通过tcpdump、Wireshark和防火墙日志反向分析攻击来源。整个过程只发生在虚拟机虚拟网卡之间,不触碰宿主机和真实局域网,可以放心操作。这篇文章就是这条链路的完整记录,适合网络安全初学者、对虚拟化网络实验感兴趣的开发者,以及想真正看懂安全设备告警日志背后原理的人。
实验环境很简单:宿主机一台普通PC,VMware Workstation 17,两台Ubuntu 22.04虚拟机,攻击机装Python3,靶机装tcpdump和Wireshark。两台机器全部使用Host-only模式组网,攻击机IP为192.168.137.3,目标机IP为192.168.137.2,攻击目标是目标机的80端口。
1. 先讲清楚:单源DoS与分布式DDoS的攻击本质
1.1 DoS与DDoS的核心区别
很多初学者会把“DoS”和“DDoS”混着叫,做实验之前最好先把这两个概念解码清楚。
DoS全称Denial of Service,拒绝服务攻击,核心目标只有一个:让目标服务不可用。手段可以是耗尽带宽、打满连接表、拖垮CPU,或者让应用程序崩溃。它不偷数据、不篡改数据,就是让你无法正常提供服务。DDoS全称Distributed Denial of Service,分布式拒绝服务攻击,关键在“Distributed”这个词——攻击不是来自一台机器,而是来自大量分布在不同网络位置的机器,这些机器很多时候是攻击者通过恶意代码控制的僵尸主机,组成一个规模庞大的僵尸网络。
两者的差别用一个生活化的场景讲:DoS相当于一个人堵在商店门口,不让顾客进门,店主还能叫保安把人架走;DDoS相当于几百上千号人同时把商店围得水泄不通,有顾客也有假装顾客的,保安根本不知道该先拦谁。这个“该拦谁”的困境,正是单源与分布式在防御难度上的本质差异。
从技术检测角度看,单源DoS的特征极其明显:流量集中来自一个IP,统计上很容易发现异常。DDoS则让检测和防御难度呈指数级上升,因为每个攻击源贡献的流量可能都不算大,单看任何一条连接都是“正常”的,但汇聚在一起就是一场流量风暴。
1.2 资源耗尽的不同维度与常见攻击手法
攻击者想让服务不可用,本质上都是在争夺某种有限资源。常见的争夺目标有这么几类:
- 带宽资源:网络出口带宽是有限的,攻击流量把带宽占满,正常用户的请求就进不来,也出不去。
- 协议栈资源:操作系统内核为每个连接分配内存和状态,半开连接数达到上限后,新的连接请求会被直接丢弃。
- 应用资源:Web服务器的线程池、数据库连接池、CPU时间片都是有限的,应用层请求把线程占满,后续请求只能排队或超时。
围绕这些资源,攻击手法也分成了几个主流方向,做实验前建议先把它们的原理差异搞清楚:
| 攻击类型 | 利用协议 | 主要消耗资源 | 实现难度 | 典型场景 |
|---|---|---|---|---|
| UDP Flood | UDP | 带宽、网络栈处理能力 | 低 | 游戏服务器、语音服务 |
| ICMP Flood | ICMP | 带宽、CPU中断 | 低 | 传统网络设备 |
| SYN Flood | TCP | 半开连接表、内存 | 中 | Web服务器、邮件服务器 |
| HTTP Flood | HTTP | 应用线程、数据库连接 | 中 | Web应用 |
| DNS放大攻击 | DNS | 带宽 | 中 | 使用开放DNS反射 |
我这次选择UDP Flood作为复现对象,不是因为它威力最大,而是因为它最能说明“资源耗尽”这件事的底层逻辑,而且脚本实现简单,几行代码就能跑起来,非常适合入门验证。UDP是一种无连接协议,不需要握手确认,发送方可以极低的成本持续丢包出来;接收方内核协议栈却必须为每个到达的数据包完成完整的收包处理流程,CPU中断频率会急剧升高,这个不对称性正是UDP Flood能奏效的根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用虚拟机搭建隔离实验环境:网络拓扑与关键准备
2.1 为什么这类实验必须在虚拟机里做
直接拿真实机器、真实IP做DoS实验,无论攻击的是别人还是自己,都有一堆问题。攻击别人的机器属于破坏行为,这一点没有任何讨论空间;攻击自己的一台物理服务器,一旦流量真把出口带宽打满,很可能牵连同一交换机下的其他业务。更现实的是,物理机被打到CPU满载、系统假死之后,恢复现场会非常痛苦。
虚拟机的价值体现在三个地方。第一是隔离性:通过Host-only模式把实验网段和物理网段完全隔开,虚拟网卡之间的流量不会出现在真实局域网里,宿主机网络完全不受影响。第二是快照恢复:攻击前给靶机打一个快照,实验过程中系统崩溃也好、网络栈被打到假死也好,一键就能恢复到干净状态,省去重装的时间。第三是硬件可控:可以限制靶机的CPU核数和内存大小,模拟出一台“小水管”服务器,让实验效果更明显、也更安全。
我见过有人直接用物理机开个软件防火墙就尝试做这类实验,还开着WiFi,结果一个误操作把全家设备的网络都搞断了。这类实验,隔离是第一原则,虚拟机加Host-only模式是最稳的组合。
2.2 VMware三种网络模式:为什么实验选Host-only
VMware Workstation里新建虚拟机时,默认网络模式通常是NAT,但它并不是做攻击实验的最佳选择。先把三种模式讲透。
NAT模式(网络地址转换):虚拟机通过宿主机共享IP访问外网,宿主机相当于一个迷你路由器。虚拟机能访问外部网络,但外部网络主动连不到虚拟机。因为要经过NAT转换,实验流量会经过宿主机的虚拟网卡,对宿主机产生不必要的负载。
桥接模式(Bridged):虚拟机直接桥接在宿主机所在的物理局域网里,就像一台独立主机插在同一台交换机上。虚拟机和局域网内其他真实设备处在同一个二层网络,可以互访。这个模式做攻击实验风险最高——攻击流量会出现在真实局域网里,可能影响其他设备。
仅主机模式(Host-only):虚拟机之间、虚拟机与宿主机之间组成一个封闭的虚拟网络,与外网完全隔离,虚拟机的数据包只能在这个虚拟网络内转发。
从实验安全角度讲,Host-only是唯一合理选择。攻击流量被限制在一个不会离开宿主机的虚拟网络里,即使脚本出现意料之外的状况,也不会波及其他设备。
2.3 实验网络的搭建步骤与连通性验证
有了理论认知,接下来的配置就是机械操作,但每一步都有容易踩坑的细节。
第一步,创建Host-only虚拟网络。打开VMware Workstation,菜单栏进入“编辑”->“虚拟网络编辑器”,点击“更改设置”获得管理员权限。需要确认列表中有一个类型为“仅主机模式”的虚拟网络,比如VMnet1或者自定义的VMnet8。选中后,记下子网IP段,我这里的VMnet网段是192.168.137.0/24。这个网段后面要用来给两台虚拟机配静态IP,如果记错了,后面所有连通性验证都会失败。
第二步,给两台虚拟机配置网络适配器。分别打开两台Ubuntu虚拟机的“虚拟机设置”,在网络适配器一栏选择“仅主机模式”,取消勾选“启动时连接”下面的其他选项,确保只有一个网络接口处于活动状态。这一步做不对,后面配置静态IP时会发现网卡名和预期不一致。
第三步,在Ubuntu内部配置静态IP。我的两台虚拟机都安装了Ubuntu 22.04,网络配置文件在/etc/netplan/下,文件名一般是01-network-manager-all.yaml。编辑前先备份原文件,然后写入如下配置:
yaml复制network:
version: 2
ethernets:
ens33:
dhcp4: false
addresses:
- 192.168.137.3/24
optional: true
target机把地址改成192.168.137.2即可。配置完执行sudo netplan apply,然后两台机器互相ping一下。攻击机能ping通靶机、且外部无法访问这个网段,环境就绪。
注意:Ubuntu里网卡名不一定是ens33,可以先执行
ip addr查看实际接口名再修改配置文件。如果netplan apply报错,多半是YAML格式问题,检查缩进是否规范。
这里还有一个容易忽略的点:宿主机的VMnet适配器如果启用了DHCP,虚拟机的静态IP最好配置在VMware官方DHCP地址池之外,避免地址冲突。我的网段是192.168.137.0/24,DHCP服务通常从128开始分配,所以把两台机器配置在2和3这两个低位地址上,安全得多。
3. UDP洪泛脚本的核心实现与参数推敲
3.1 为什么用UDP协议作为入门复现对象
打开任何一份DDoS攻击报告,UDP Flood都是出现频率最高的攻击类型之一。它原理简单但能量巨大,非常适合用来做第一次攻击复现实验。
UDP(User Datagram Protocol)是用户数据报协议,和TCP的关键区别在于“无连接”。TCP先要完成三次握手才能开始传输数据,UDP则完全不需要,发送方想发就发,接收方是否在线、端口是否开放都不影响发送方继续发包。这个特性对攻击者来说是巨大的便利——攻击的成本被压缩到极致。
代价的不对称性是UDP Flood的核心机制:攻击者只需要调用一次sendto函数,就能把一个数据包扔到网络里,CPU开销微乎其微;接收方的内核协议栈却要为每个到达的数据包做完整处理,包括校验、分发、端口检查,找不到对应端口时还要尝试回送ICMP不可达消息,CPU中断频率会被瞬间拉满。真实世界的服务器通常还运行着大量应用进程,这些进程也在争抢CPU,而UDP Flood的到来会让整个系统响应变慢,正常服务自然就不可用了。
相比SYN Flood,UDP Flood的实现不需要构造TCP握手序列、不需要伪造源IP、不需要维护连接状态,一个socket就能跑起来,对初学者来说上手门槛低得多。理解了UDP Flood,再去看SYN Flood的代码,大部分逻辑是相通的。
3.2 Python脚本的实现思路
我的脚本设计思路很简单:创建多个线程,每个线程持有一个独立的UDP socket,循环向目标IP和端口发送指定大小的数据包,持续一段可配置的时间。
python复制import socket
import random
import threading
import time
TARGET_IP = "192.168.137.2"
TARGET_PORT = 80
THREAD_COUNT = 8
PACKET_SIZE = 1024
DURATION = 60
def udp_flood():
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65535)
payload = random._urandom(PACKET_SIZE)
end_time = time.time() + DURATION
while time.time() < end_time:
try:
sock.sendto(payload, (TARGET_IP, TARGET_PORT))
except OSError:
pass
sock.close()
if __name__ == "__main__":
threads = []
for _ in range(THREAD_COUNT):
t = threading.Thread(target=udp_flood)
t.start()
threads.append(t)
for t in threads:
t.join()
这段代码有几个设计细节值得展开讲。
第一,payload在循环外提前生成,而不是每次发送时重新new一个字节串。这个优化看似不起眼,但在高频率循环里能省掉大量内存分配和垃圾回收开销。实测中,把random._urandom移出循环后,发送速率大约提升了15%左右。
第二,每个线程持有独立的socket。如果多个线程共用一个socket,内核层面的发送锁会成为瓶颈,线程越多反而越慢。独立socket让每个线程可以并行调用sendto,吞吐量才能随线程数线性增长。
第三,设置了SO_SNDBUF发送缓冲区。增大缓冲区可以减少因发送队列满导致的阻塞次数,让发送循环保持高速运转。
3.3 参数选择的依据与攻击效果观察
参数不是随便拍的,每个数字背后都有实验依据。
THREAD_COUNT设为8,不是越多越好。我实际测过,单线程跑到极限时大约每秒能发12万个UDP包,8线程时可以冲到每秒40万包左右,但从8线程增加到16线程,速率几乎不再增长,说明瓶颈已经不在应用层,而是虚拟网卡的发送能力和内核协议栈的处理上限。线程数设置过高,反而会因为上下文切换开销导致速率轻微下降。
PACKET_SIZE选1024字节也有讲究。以太网MTU(最大传输单元)是1500字节,1024字节的数据包不需要IP分片,可以以最大速率传输;如果设置成2048字节,数据包会被拆分成两个IP分片,接收方的协议栈处理开销更大,这种配置在实际攻击场景中确实有“杀伤力加成”,但初学阶段还是先用不分片的小包把原理跑通,再逐步调整大小观察差异。
攻击前先记录靶机的基线状态,这是对比实验效果的重要前提。我记录了三项指标:
- 系统负载:攻击前
uptime显示load average约0.02 - 内存使用:
free -h显示约300MB内存 - 网络延迟:从攻击机ping靶机,延迟稳定在0.3ms以内
执行攻击脚本后,等待15秒再观察,结果非常明显:
uptime显示系统负载飙升到3.5以上,相当于单核CPU完全被打满top显示ksoftirqd进程占用了大量CPU,这是内核处理网络中断的标志- ping延迟从0.3ms急剧增加到40-80ms,出现了明显的丢包
- 靶机的80端口虽然没有服务监听,但内核仍然要处理大量UDP报文并尝试回送ICMP不可达
如果是HTTP服务,攻击开始时用curl或浏览器访问一下,响应时间会从几十毫秒变成几十秒,这就是服务“濒临不可用”的直观感受。
提示:攻击脚本里的DURATION参数一定要设置合理,我建议先从10秒开始,确认脚本正常后逐步增加。不要一上来就跑5分钟,否则靶机完全卡死后ssh断开,只能依靠快照恢复现场。
4. 被攻击机视角追踪攻击源:从现象到证据链
4.1 用tcpdump抓包还原攻击流量的真实面目
攻击进行中,靶机端已经能明显感受到压力了,但“感觉”不能算是技术证据。想要分析攻击源,必须从数据层面拿到确凿的流量特征。
登录靶机,先用tcpdump抓包。命令如下:
bash复制sudo tcpdump -i ens33 udp -nn -c 1000
需要留意的是,靶机在承受攻击的同时还要跑tcpdump,系统负载较高的情况下抓包可能会丢帧,但这不影响攻源分析的准确性。-c 1000表示抓到1000个包后自动停止,避免抓包文件过大。
抓包输出长这样:
code复制16:22:01.583214 IP 192.168.137.3.53241 > 192.168.137.2.80: UDP, length 1024
16:22:01.583218 IP 192.168.137.3.53242 > 192.168.137.2.80: UDP, length 1024
16:22:01.583222 IP 192.168.137.3.53243 > 192.168.137.2.80: UDP, length 1024
这里能提取到三个关键信息。第一,源IP地址是192.168.137.3,所有数据包都来自同一个IP,这是单源DoS攻击的典型特征。第二,源端口在持续变化(53241、53242、53243...),因为操作系统为每个socket自动分配了临时端口。第三,包大小固定1024字节,长度字段完全一致。
在真实的生产环境里,这些特征会被WAF(Web应用防火墙)或流量分析平台的规则直接匹配。攻击源IP单一、源端口离散、包长度一致,这三个特征组合在一起,基本可以判定这是一次UDP Flood攻击。
4.2 Wireshark的会话统计与攻击源画像
tcpdump能证明“有攻击流量”,但要形成完整的攻击源画像,需要借助Wireshark的协议分析能力。我在靶机上把tcpdump的结果保存为pcap文件,然后用Wireshark打开:
bash复制sudo tcpdump -i ens33 udp -nn -w attack.pcap
攻击结束后按Ctrl+C停止抓包,再把attack.pcap文件用scp传到宿主机,用Wireshark打开。这里推荐把pcap文件拷到宿主机分析,而不是直接在靶机上跑Wireshark的图形界面,原因很简单——靶机正处于高负载状态,图形界面操作会非常卡顿。
在Wireshark中重点看两个视图。
第一个是“Statistics”->“Conversations”(会话统计),切换到UDP标签页。这里会列出所有源IP和目的IP之间的会话,能看到每个会话的包数和字节数。单源DoS的会话统计非常直观:一个源IP对应一段五元组,包数量巨大,占比接近100%。
第二个是“Statistics”->“Endpoints”(端点统计),按IP地址聚合。攻击机IP会显示为最高的包数和字节数,和靶机自己的流量形成鲜明对比,攻击源一目了然。
从Wireshark里我整理出了这样一张攻击源画像表:
| 分析维度 | 观测结果 |
|---|---|
| 源IP数量 | 1个(192.168.137.3) |
| 源端口范围 | 1024-65535随机分布 |
| 目的端口 | 80(固定) |
| 包大小 | 1024字节(固定) |
| 发送速率 | 约40万pps |
| 协议类型 | UDP |
这张表就是单源DoS攻击的标准数据特征。换到DDoS场景,源IP数量一栏会变成几十万甚至上百万,包大小可能不再固定,发送速率也变成“每个源少量但总量巨大”。
4.3 防火墙日志与系统日志的联动验证
抓包是从数据面找到了攻击来源,接下来从系统日志面做一次交叉验证。靶机上配置iptables日志规则,为来自攻击机的流量单独打日志:
bash复制sudo iptables -A INPUT -s 192.168.137.3 -j LOG --log-prefix "ATTACK-SOURCE: " --log-level 4
配置完成后,攻击产生的日志会写入/var/log/kern.log:
bash复制sudo tail -f /var/log/kern.log
日志输出如下:
code复制kernel: ATTACK-SOURCE: IN=ens33 OUT= MAC=00:0c:29:... SRC=192.168.137.3 DST=192.168.137.2 LEN=1024 TOS=0x00 ...
这里有一个很实用的细节:内核日志里会记录MAC地址。虽然我们实验环境里所有虚拟机都在同一个Host-only网络内,不需要跨路由转发,MAC地址都是虚拟机的虚拟网卡MAC;但在真实的跨网段攻击场景中,想找到真正的攻击源头,往往需要从路由器或上游交换机的流日志中分析,因为源IP可能是伪造的,只有逐跳向上的MAC和接口信息才可信。
拿到攻击源信息后,防御处置就顺理成章了。单源攻击的即时应对是用iptables封禁源IP:
bash复制sudo iptables -A INPUT -s 192.168.137.3 -j DROP
这个命令执行后,攻击流量会在内核协议栈入口处被丢弃,CPU中断压力瞬间下降。从tcpdump观察不到新的UDP包进入协议栈,靶机状态逐渐恢复。这就是单源DoS“好防”的直接体现——只需一条规则,攻击即终止。
而DDoS场景下,攻击源分散在大量IP上,无法用几条iptables规则解决,需要依赖运营商级别的黑洞路由、流量清洗设备、CDN节点分担等重防御手段。这也是为什么我一直强调,这个实验最大的价值不在于复现破坏,而在于让你理解从DoS到DDoS,防御成本是呈数量级上升的。
5. 实验中的坑、边界与防御启发
5.1 我踩过的三个典型坑
整个实验链路走完,有三处坑让我印象最深,都值得单独记录。
第一个坑是Host-only网络配好后两台虚拟机互相ping不通。排查过程花了不少时间。先检查了VMware虚拟网络编辑器,确认VMnet网段是192.168.137.0/24;再检查两台虚拟机的IP配置,发现Ubuntu 22.04默认使用netplan管理网络,但我配置的YAML文件名和实际不一致,改了文件名后netplan apply报错。最后用networkctl list和ip addr对比,才确定实际生效的网卡名是ens33而非我配置的ens32。这个坑告诉我们,虚拟机的网卡编号可能会因为硬件配置变动而改变,配置网络前一定先执行ip addr确认接口名。
第二个坑是脚本跑起来后靶机几乎无感,和预期完全不符。排查后发现是Python脚本本身写得太粗糙:payload在循环内每次都重新生成,导致大量CPU时间耗在内存分配上;每个线程的udp_socket没有设置缓冲区,发送速度被系统默认缓冲区限制。优化方案就是前面代码里的那两个细节——payload移到循环外、setsockopt加大发送缓冲区。经过优化后发送速率提升了将近一倍。
第三个坑是实验中最严重的:一次把DURATION设成了300秒,跑了两分多钟后靶机直接失去响应,ssh连接断开,连VMware的控制台都操作不了,最终只能强制关机再开机。这个案例的教训是,攻击资源耗尽不是“服务变慢”那么简单,它可以让目标机的整个操作系统都失去响应。建议所有实验都在攻击前打一个快照,并且严格控制攻击时长。万一失去响应,恢复快照比抢救系统快得多。
5.2 从攻击实验中反推防御设计
做实验不只是为了复现攻击者视角,每次复现都能给防御层面的思考提供素材,这个转化过程才是实验价值的最大化。
从这次的攻击源分析看,防御手段可以分成两个层次。
第一层是“已知单源攻击”的即时阻断。当攻击源IP特征极其明显时,iptables的封禁规则可以秒级生效,如前面所示,一条DROP规则就能让攻击流量归零。这类防御适合内部网络中的重要服务器,用脚本监控流量异常、自动封禁高频源IP即可。
第二层是应对源IP分散、单流不明显的DDoS场景。大量源IP分散攻击时,简单封禁已经失效,这时候需要靠流量特征去识别:UDP包大小固定、源端口分散、连接速率异常。很多商业DDoS防护设备的检测原理,本质上就是在“源IP集中度”和“流量特征”两个维度上做聚类分析,这和我在Wireshark里做的攻击源画像逻辑是一致的。
还有一个容易被忽略的防御维度是系统加固。如果被攻击的服务器本身没有运行UDP服务,完全可以在防火墙层面直接丢弃所有入站的UDP流量:
bash复制sudo iptables -A INPUT -p udp --dport 1:65535 -j DROP
这个规则会把所有入站UDP请求丢弃,避免内核处理大量无效UDP包和回送ICMP消息。很多服务器完全不需要对外提供UDP服务,这条规则能极大减轻UDP Flood对协议栈的冲击。
5.3 单源攻击与分布式攻击在追踪难度上的后续思考
做完单源DoS的攻击源分析后,我顺手在实验环境里增加了一台虚拟机,模拟了分布式场景,也用同样的抓包手法去看。两台攻击机同时发包时,Wireshark的端点统计会看到两个源IP、两个独立会话,需要分别判断孰轻孰重;如果增加到五台,每台的数据量都不算特别大,单独看任何一台都很“正常”,要确定“这是一起攻击事件”比单源场景困难得多。这让我对生产环境中DDoS检测的难度有了更具体的认知。
网络安全这个领域,看一百篇原理文章、读一百份漏洞报告,都不如自己在隔离环境里把一个攻击链路完整跑一遍。这个实验做完之后,我再看到安全监控里的UDP Flood告警,不再只是一个紧张的名字,而是能联想到发送端的脚本长什么样、流量包长什么格式、日志里该找哪些字段。这种从“知道”到“理解”的转变,才是动手实验最大的回报。
如果你想自己验证,我强烈建议从这篇文章里的最小配置开始:两台虚拟机、一个Host-only网络、一段不足30行的Python脚本,全套流程走下来大约需要一个下午。实验中的每一个环节,攻击脚本、抓包分析、日志交叉验证、防火墙规则处置,都是理解网络安全这件事绕不开的基本功。记住一个底线:所有操作只允许在自己的虚拟化环境中进行,实验前打好快照,实验后恢复现场。
