打開網(wǎng)易新聞 查看精彩圖片

在這個爬蟲橫行的時代,越來越多開發(fā)者深受其害:有人怒斥 OpenAI 的爬蟲瘋狂“偷”數(shù)據(jù),;也有人被爬蟲逼到極限,最后只好。但本文作者卻走了一條完全不同的路——他靠一己之力,用一臺每月僅需 6 美元的小破服務(wù)器,成功扛下了 Hacker News 熱榜帶來的流量海嘯,還順手反制了那些惡意爬蟲,用“zip 炸彈”讓它們原地爆炸。而他是怎么做到的,我們不妨來看看。

原文:https://idiallo.com/blog/surviving-the-hug-of-death

https://idiallo.com/blog/zipbomb-protection

作者 |Ibrahim Diallo 編譯| 蘇宓

出品 | CSDN(ID:CSDNnews)

打開網(wǎng)易新聞 查看精彩圖片

背景

我是 Ibrahim Diallo,一名住在加州的軟件開發(fā)者。從 1992 年起,我就開始“折騰電腦”:破解系統(tǒng)、寫代碼,一轉(zhuǎn)眼搞了幾十年,到現(xiàn)在也還沒停下。

平時我會寫點博客,分享技術(shù)觀察和一些個人思考。誰知多年前剛開始寫沒多久,就因為一篇意外“火出圈”的文章,遭遇了前所未有的流量暴擊——直接沖上了 Hacker News 和 Reddit 熱榜。

打開網(wǎng)易新聞 查看精彩圖片

文章登上 HN 首頁的那一刻,我那臺小服務(wù)器瞬間崩潰了。請求像潮水一樣涌來,Apache 服務(wù)器吃力地運轉(zhuǎn),我坐在電腦前一次次重啟它,就像拿個噴水槍去滅森林大火——完全招架不住。這種“被熱度壓垮”的場面,在互聯(lián)網(wǎng)圈子里還有個形象的說法:“死亡之擁”(Hug of Death)。

到底訪問量有多猛、服務(wù)器壓力有多大?說實話,還真挺難用語言形容。

直到今年二月,我又寫了一篇文章,它沒多久后又登上了 Hacker News 第一名。不過這次我有備而來:提前保存了服務(wù)器的日志,還專門做了一段可視化動畫,展示那臺每月只花 6 美元的小服務(wù)器,是怎么扛住這波流量洪水的。

上面這個動畫里,每一個網(wǎng)頁請求都用一個小圓點表示,朝服務(wù)器移動。右下角還有圖例說明:

  • 機器人 vs 真實用戶:通過 user-agent 判斷。帶有 “bot” 的一般是正規(guī)機器人,其他的就靠一些啟發(fā)式規(guī)則來判斷。

  • 響應(yīng)類型:

- 200 OK(是一種HTTP 狀態(tài)碼,表示網(wǎng)頁請求成功了,服務(wù)器已經(jīng)正常返回了你要的內(nèi)容):請求成功

- 重定向:用晃動的小圓點表示

- 404 Not Found:紅點會掉出屏幕

- zip Bombs:這個后面再解釋

服務(wù)器配置

隨著文章爆火,盡管現(xiàn)場一度混亂,我那臺每月只花 6 美元的小服務(wù)器卻始終堅挺。它的配置非常簡陋:只有 1GB 內(nèi)存,跑著 Apache 2 和一個簡單的 MySQL 數(shù)據(jù)庫。沒有云服務(wù)、沒有自動擴容、也沒有負(fù)載均衡器,全靠輕量化配置和合理的緩存策略“硬扛”流量洪峰。

服務(wù)器配置如下:

  • 主機服務(wù)商:DigitalOcean(1GB 內(nèi)存)

  • Web 服務(wù)器:Apache 2

  • 系統(tǒng)環(huán)境:Ubuntu + PHP

  • 數(shù)據(jù)庫:MySQL

  • 月費用:$6

我的博客是基于一個自建框架,大部分頁面內(nèi)容都提前緩存到了 memcached 中。數(shù)據(jù)庫只在每小時查詢一次頁面,大大減輕了負(fù)擔(dān)。這套方案在過去幾次流量高峰時也都撐住了。

來看一下這次文章爆火的時間線:

  • 下午 4:43(PST)— 文章提交到 Hacker News

  • 下午 4:53(PST)— 登上首頁,機器人蜂擁而至

  • 下午 5:17(PST)— 成為 Hacker News 第一,洪水般的訪問量涌入

  • 晚上 8:00(PST)— 管理員改了標(biāo)題(原因不明),流量斷崖式下跌

  • 凌晨 3:56(PST)— 一只機器人掃描了 300 個 URL,試圖找漏洞

  • 上午 9:00(PST)— 來自 Mastodon 網(wǎng)絡(luò)的新一波流量暴增

  • 上午 9:32(PST)— 遭遇大規(guī)模垃圾攻擊:一分鐘內(nèi)接收約 4000 個請求,幾乎全是廣告

  • 下午 4:00(PST)— 24 小時內(nèi),服務(wù)器共處理 46,000 個請求

關(guān)鍵點來了:服務(wù)器從頭到尾都沒宕機,甚至 CPU 使用率連 16% 都沒到!

不過你在動畫中可能注意到一個奇怪現(xiàn)象:明明是 1GB 內(nèi)存的小服務(wù)器,怎么內(nèi)存始終被占了一半左右?原因其實很簡單——MySQL在“背鍋”。

當(dāng)年博客剛上線時,我曾雄心勃勃地記錄下每一條請求,把它們寫入數(shù)據(jù)庫日志表,用來追蹤哪些文章最受歡迎。這招在當(dāng)時確實挺實用,但 12 年過去,數(shù)據(jù)越堆越多,想從中查點東西反而變成了高成本操作。

這次流量沖擊之后,我備份了日志表,然后徹底刪掉它。也是時候跟那段歷史告?zhèn)€別了。

對了,你可能在可視化動畫里看到一些“小爆炸”效果,現(xiàn)在就來解釋下那是怎么回事…

有一天,我偶然發(fā)現(xiàn)有個網(wǎng)站在實時偷我博客的內(nèi)容:每次有人訪問他們的頁面,他們就立刻爬取我最新的文章,刪掉我的名字和品牌標(biāo)識,然后假裝是他們自己寫的。

一開始,我試著“手動反擊”——故意喂他們一些假數(shù)據(jù),讓他們搬錯內(nèi)容。但沒過多久,我就覺得這種方式太麻煩,干脆拿出了我的秘密武器:“zip 炸彈”。

這個“炸彈”的工作原理是這樣的:當(dāng)他們的爬蟲訪問我的網(wǎng)站時,我就返回一個看起來沒什么問題的小壓縮文件。他們的服務(wù)器會乖乖下載并嘗試解壓。結(jié)果呢?幾 GB 的“垃圾”文件瞬間釋放,系統(tǒng)直接崩了。

就這樣,游戲結(jié)束。

什么是“zip 炸彈”?為什么能“反殺”爬蟲?

這些年,“zip 炸彈”成了我對付各種惡意爬蟲(比如偷內(nèi)容、探測漏洞、發(fā)垃圾信息的自動程序)最有效的工具。

要知道,互聯(lián)網(wǎng)上的流量,其實大部分都來自這些機器人。有的是“友好”的,比如搜索引擎、RSS 訂閱器,或者現(xiàn)在很流行的大模型訓(xùn)練用爬蟲。但也有很多是惡意的:像發(fā)送廣告、偷文章,甚至入侵網(wǎng)站的腳本。我曾經(jīng)在工作中就遇到過:有個爬蟲發(fā)現(xiàn)我們 WordPress 的漏洞,在服務(wù)器里偷偷植入惡意代碼,讓網(wǎng)站變成黑客控制的攻擊工具;還有一次,我的網(wǎng)站被爬蟲刷滿垃圾內(nèi)容,結(jié)果直接被 Google 搜索下架。

從那以后我就意識到,必須對這些爬蟲進行“反擊”,zip 炸彈就是我找到的最佳方案之一。

簡單說,zip 炸彈是一種超小的壓縮文件,解壓之后卻會變成一個巨大的文件,能讓處理它的系統(tǒng)崩潰。

在互聯(lián)網(wǎng)早期,gzip 壓縮是很早就被引入的一個功能。由于當(dāng)時網(wǎng)速慢、信息密度高,人們希望盡可能壓縮數(shù)據(jù)再傳輸。比如一個 50KB 的 HTML 文件,經(jīng)過 gzip 壓縮后可能只剩 10KB,能節(jié)省 40KB 的傳輸量。在撥號上網(wǎng)時代,這意味著頁面加載時間從 12 秒減少到 3 秒。

這種壓縮技術(shù)同樣可以用于傳輸 CSS、JavaScript 甚至圖像文件。Gzip 的優(yōu)勢在于它快速、簡單,而且能顯著提升瀏覽體驗。當(dāng)瀏覽器發(fā)出請求時,會在請求頭中表明自己支持壓縮。如果服務(wù)器也支持,就會返回經(jīng)過壓縮的數(shù)據(jù)版本。

Accept-Encoding: gzip, deflate

而爬蟲程序也會這么做,它們一樣想節(jié)省資源。我正是利用這一點“反制”它們

反擊過程:用壓縮包“讓你解壓到崩潰”

在我的博客上,經(jīng)常會遇到一些機器人在掃描安全漏洞,通常我不會理會。但當(dāng)我發(fā)現(xiàn)它們試圖注入惡意攻擊,或是在試探服務(wù)器響應(yīng)時,我就會返回一個看起來正常的響應(yīng)(比如 200 OK),并提供一個經(jīng)過 gzip 壓縮的文件。

我會提供一個從 1MB 到 10MB 不等的文件,它們也會欣然接受。通常一旦它們接收了這個文件,我就再也沒見過它們出現(xiàn)。為什么?因為它們在解壓文件后直接崩潰了。

Content-Encoding: deflate, gzip

具體發(fā)生的事情是這樣的:機器人收到文件后,會讀取文件頭,發(fā)現(xiàn)這是一份壓縮文件。于是它們嘗試解壓這個 1MB 的文件,以查找其中的內(nèi)容。但文件開始不斷擴展、擴展、再擴展,直到內(nèi)存耗盡、服務(wù)器崩潰為止。這個 1MB 的壓縮文件實際上會被解壓為 1GB,大多數(shù)機器人在這一過程中就會崩潰。如果遇到那些死纏爛打的腳本,我就給它們發(fā)送 10MB 的版本,它會解壓成 10GB,直接“秒殺”。

在告訴你如何制作 zip Bomb 之前,我得提醒一句:這確實可能會導(dǎo)致你自己的設(shè)備崩潰甚至損毀,繼續(xù)操作需自擔(dān)風(fēng)險。

下面是創(chuàng)建 zip Bomb 的方法:

dd if=/dev/zero bs=1G count=10 | gzip -c > 10GB.gz

這個命令的作用如下:

  • dd:用于拷貝或轉(zhuǎn)換數(shù)據(jù);

  • if:指定輸入文件,這里是 /dev/zero,一個會不斷生成零字節(jié)數(shù)據(jù)的特殊設(shè)備;

  • bs=1G:設(shè)置塊大小為 1GB,意味著每次處理 1GB 數(shù)據(jù);

  • count=10:處理 10 個這樣的塊,總共生成 10GB 的零數(shù)據(jù);

  • 然后我們將這些數(shù)據(jù)通過管道傳給 gzip,生成一個壓縮文件 10GB.gz,壓縮后大小大約是 10MB。

在我的服務(wù)器上,我寫了一個中間件程序,用來自動識別哪些請求是惡意的。

我維護了一個黑名單 IP 列表,專門記錄那些反復(fù)掃描整個網(wǎng)站的地址。同時還有一些啟發(fā)式策略來檢測垃圾信息發(fā)送者。很多垃圾腳本會在發(fā)完內(nèi)容后再次訪問頁面,看內(nèi)容是否成功插入,我正是利用這個行為模式來識別它們。

當(dāng)系統(tǒng)判斷出請求是惡意的,就觸發(fā)如下邏輯:

if (ipIsBlackListed() || isMalicious()) {
    header("Content-Encoding: deflate, gzip");
    header("Content-Length: " . filesize(ZIP_BOMB_FILE_10G)); // 10MB
    readfile(ZIP_BOMB_FILE_10G);
    exit;
}

就是這么簡單。我只需要“送出”一個 10MB 的文件,就可能讓對方服務(wù)器宕機。如果哪天我的文章突然大火、訪問量激增,我就把炸彈換成 1MB 的“輕量版”,對付低端爬蟲也完全足夠。

Zip Bomb 并不是萬能的

最后再補充一點:zip 炸彈并不是萬能的。

高級一點的爬蟲可以識別壓縮文件,提前做限制,比如只讀取一部分或禁止解壓,這樣就能繞過炸彈。不過那些“低級無腦亂爬”的腳本,就非常容易中招了。

而我需要的,就是一個成本低、效果好的防御手段。對于這種級別的威脅來說,“zip 炸彈”已經(jīng)綽綽有余。