📘 第十一章 Web的攻击技术
📄 一句话概括

四个连上就不说话的连接,把单线程服务钉住:正常请求从 15 毫秒变成 4027 毫秒, 多等 4.0 秒。压短读超时降到 816 毫秒,改成多线程只剩 2 毫秒。 这一招叫 Slowloris,几乎不花带宽。

📝 本节和前面几节的关系

前面十几节都是「拿到点什么」。DoS 是唯一一种「什么都不要,只要你别干活」。

一、它要的不是数据,是可用性

DoS 是一个来源把服务打瘫。手法有几种:占满带宽、占满连接、占满 CPU、让请求队列排到永远处理不完。 DDoS 是很多来源一起打(肉鸡、被入侵的摄像头和路由器)。 它解决的是「单个 IP 被封掉」这个问题,所以规模比手法重要。 这一类的目标写在名字里:让服务不可用。所以「数据没丢」不代表没被攻击——机密性和可用性是两件事。 最省资源的一种就是 Demo 测的 Slowloris:不占带宽、不占 CPU,只占连接。请求头发一半,然后不动了。

二、四个连接为什么能拖死一个服务

单线程服务端的模型很朴素:接一个连接、读一个请求、回一个响应,处理完才轮到下一个。 一个连接只发了半行请求头就不发了,服务端就在读上等着——不是在等数据,是在等超时。 这一等,后面所有连接全排队。 Demo 里超时设 1 秒、挂四个慢连接,正常请求要等 4027 毫秒,差不多就是四个超时叠起来。 所以这不是代码有 bug,是「一次只能处理一个」这个设计选择。并发模型直接决定抗打击能力。

三、两种缓解,通常一起上

压短读超时:Demo 里超时从 1 秒降到 0.2 秒,正常请求从 4027 毫秒降到 816 毫秒。 它没有消灭问题,只是把代价按比例压小——超时乘连接数,就是这个量级。 每个连接一个线程(或事件循环):Demo 里四个慢连接同时挂着,正常请求只花 2 毫秒。 慢连接再多,也只占内存,不占处理能力。 真实系统一般两个都上:超时防「一个连接无限挂着」,并发防「一个挂住连累所有人」。 外面再套一层反向代理或流量清洗,把明显异常的连接挡在门外。 还有一条容易漏的:限制单个 IP 的并发连接数。 Slowloris 靠的就是「一个 IP 开很多半开连接」,把这条限住,成本立刻上去。

四、动手 Demo:四个不说话的连接,多等 4 秒

本机起一个单线程服务端,先量一次正常请求要多久。 再开四个连接、每个只发半行请求头就不发了,重新量一次正常请求。 然后把读超时压短、再改成多线程,各量一次。

def hang(n):
    for _ in range(n):
        s = socket.create_connection(ADDR)
        s.sendall(b'GET / HTTP/1.1\r\nHost: x\r\n')   # 发一半就停
        SLOW.append(s)

t0 = time.perf_counter()
st, hd, bd = H.recv_response(socket.create_connection(ADDR), REQ)
print(u'%d 毫秒' % ((time.perf_counter() - t0) * 1000))

实跑输出:

--------------------------------------------------------------------
DoS:四个不说话的连接能拖垮一个服务
--------------------------------------------------------------------
  场景                                 正常请求       说明
  单线程,没有慢连接                          15 毫秒      基准
  单线程,4 个慢连接(超时 1 秒)                 4027 毫秒    被拖住
  单线程,4 个慢连接(超时 0.2 秒)               816 毫秒     拖得少些
  多线程,4 个慢连接                         2 毫秒       几乎没影响

  正常请求被拖住             多等了 4.0 秒(4027 毫秒 对 15 毫秒)
  这四个慢连接什么坏事都没干:它们只是**连上了却不把请求发完**。
  单线程服务端一次只能处理一个连接,所以它被第一个慢连接钉住,
  后面排队的全得等——包括那些正常的请求。

  这一招叫 Slowloris。它的可怕之处在于**不需要带宽**:四个连接、
  每秒几个字节,就能让一个单线程服务形同虚设。

  两种缓解都在这张表里:把每连接的读超时压短(C),
  或者干脆每个连接一个线程(D)。真实系统一般两个都上,

四行数字:15、4027、816、2 毫秒。基准 15,四个慢连接挂上之后 4027,多等 4.0 秒。 第三行说明超时确实有用:0.2 秒超时下只多等了 0.8 秒。 但注意它是按比例压小的——1 秒超时配 4 个连接,就是 4 秒。 想彻底解决得靠并发,不是靠把超时调到很小(那会误杀正常用户)。 第四行是多线程版:慢连接照样挂着,正常请求 2 毫秒。 差别不在「线程更快」,在「它不用等前面那一个」。 所以这一节该记的不是 4027 这个数,是 15 对 4027 这个比例:打垮服务的是排队,不是计算。

30 秒实验

  1. 本机起一个 python -m http.server,用几个 telnet 连着不发请求,看它卡不卡。
  2. 想一想:为什么「不限单 IP 并发」等于给这类攻击帮忙?
  3. 查一个你用的云服务的连接数上限,想想它为什么要有这个数。

常见误区

  1. 「带宽够大就不怕」——Slowloris 每秒只发几个字节。
  2. 「上了 HTTPS 更安全」——握手更贵,这一类攻击反而更省对手的力气。
  3. 「数据没丢就不算被攻击」——它打的是可用性,不是机密性。
自测三问(点开对答案) 1. Slowloris 占的是什么资源? —— 连接和读超时,几乎不占带宽,也不占 CPU。 2. 为什么单线程服务特别怕它? —— 一次只能处理一个连接,前面那个挂住,后面全排队。 3. 两种缓解各解决哪一半? —— 超时缩短「单个连接挂多久」,并发解决「一个挂住是否连累别人」。

最后一种最不像攻击——因为它可能就藏在自家代码里。

相关笔记