一台沒人看的排程機, 替別人挖了 356 天的礦
從「沒事做卻很忙」的矛盾出發,五條線索逐步逼出 rootkit、雙幣礦工與後門帳號。全程唯讀,未更動主機任何檔案。
- 業務早已失效的語音 AI 排程機持續單核滿載,最耗 CPU 的行程名稱是假的。
- ss 抓到連往 OVH 機房 3333 埠的 ESTABLISHED 連線,發起程序叫 log,執行檔在 /var/log/log。
- 讀取可疑檔案就 segfault,證實 LD_PRELOAD rootkit;再找到三重自啟與免密碼 sudo 的後門帳號。
本篇目錄
先知道「正常」長什麼樣
在判斷「異常」之前,得先知道這台機器原本該做什麼。這台排程機唯一的業務,是一支處理員工錄音的流程:每 15 分鐘由 crontab 觸發,讀試算表拿到錄音清單、下載原始檔、用 Whisper 轉逐字稿、用 GPT 改寫、用 TTS 合成語音、再回寫試算表。
問題是,這條流程其實已經死了。它依賴的服務帳戶早在數月前被刪除,每次執行都在 log 尾端固定拋出 Invalid JWT Signature。業務面沒有任何產出,CPU 卻長期滿載。
「沒事做卻很忙」。一台該閒置的機器持續高負載,不一定是挖礦,但一定值得追問「這個負載能對應到哪一項合理的工作?」合法報表也會高負載,所以單一指標不能定案,它只是調查的起點。
五條線索,逐步逼出真相
鑑識不是靠掃毒軟體給答案,而是每看到一個不合理,就追問「為什麼」。以下是實際的判讀順序與現場證據。
線索 1:CPU 滿載,但最耗資源的行程「說謊」
USER PID %CPU %MEM TIME COMMAND root 404 25.9 8.3 17613:25 /usr/bin/python3 .../unattended-upgrade-shutdown --wait-for-signal mysql 562 0.3 18.2 218:45 /usr/sbin/mysqld # 一支「關機前套用更新」的程式燒了 17,613 分鐘 CPU?不合理。
線索 2:一條連往境外機房 3333 埠的連線
換個角度:不看行程,看連線。
State Local Address:Port Peer Address:Port Process
ESTAB 10.x.x.x:36708 141.94.96.71:3333 users:(("log",pid=479))
# 141.94.96.71 → OVH SAS / 法國。port 3333 = 礦池 Stratum 常用埠。
# 發起程序名叫 "log",執行檔卻是 /var/log/log。線索 3:檔案一被讀取就 Segmentation fault
想算 /var/log/log 的雜湊去比對惡意程式庫,結果一碰就 core dump。連 /etc/ld.so.preload 都讀不了。
$ sudo sha256sum /var/log/log Segmentation fault /var/log/log (core dumped) $ sudo cat /etc/ld.so.preload Segmentation fault /etc/ld.so.preload (core dumped) # 一般檔案讀取不會 segfault。這是 LD_PRELOAD rootkit 的防偵察反應, # 也解釋了為何 ps 抓不到礦工:它被藏起來了。
線索 4:三重開機自啟
透過 /proc/479/exe(rootkit 攔不到的路徑)確認礦工本體就是 9.4 MB 的 /var/log/log。再往持久化機制找:
$ cat /etc/systemd/system/log.service [Unit] Description=log Plugin Service [Service] ExecStart=/usr/bin/log -sshd Restart=always User=root $ sudo crontab -l @reboot /var/log/log > /dev/null 2>&1 & disown # 偽裝成「log 外掛服務」+ root 的 @reboot,加上 ld.so.preload,三重保險。
線索 5:一個免密碼 sudo 的「系統帳號」
$ grep -vE 'nologin|false' /etc/passwd root:x:0:0:root:/root:/bin/bash ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash systemd:x:1001:1001::/home/systemd:/bin/bash # ← 假的系統帳號 $ sudo grep -r '' /etc/sudoers* systemd ALL=(ALL) NOPASSWD:ALL $ sudo chage -l systemd | head -1 Last password change : Sep 23, 2025 # ← 入侵時間錨點
這次外洩了什麼
這台機器上沒有雲端憑證,所以「外洩」的東西跟一般想像的不一樣:被拿走的是登入能力本身,加上兩把寫死在腳本裡的 API key。
| 服務 | 憑證類型 | 用途 | 存在形式 | 放在哪 | 權限範圍 | 狀態 |
|---|---|---|---|---|---|---|
| 受害主機(Linux 本機帳號) | 攻擊者自建的可登入帳號與密碼 | 攻擊者的長期登入管道,偽裝成系統帳號 | /etc/passwd 條目(有 home 與 shell)+ /etc/shadow 密碼雜湊 | 受害主機本機 | 搭配 sudoers 規則等同 root | 2026-09-15 起主機隔離、保持 stopped,待砍掉重建 |
| 受害主機(sudo 授權) | 免密碼 sudo 規則(等於免憑證提權) | 讓應用程式帳號與後門帳號直接取得 root | 明文 sudoers 檔案中的一行 NOPASSWD:ALL 規則 | 受害主機 /etc/sudoers | 以 root 執行任意指令 | 重建時一併移除 |
| 對外開放的開發用 API | 沒有憑證:端點本身就是入口 | 原為內部測試用途,卻經反向代理對公網開放 | HTTP 請求參數被拼進 shell 指令字串 | 受害主機的 Flask 應用 | 以應用程式帳號執行指令,再經 sudo 到 root | 入侵前 log 已被清除,無法直證是由此進入 |
| AI 服務供應商 API(語音轉文字與文字改寫) | 長期有效的 API key × 2 | 排程流程呼叫轉逐字稿與改寫 | 原始碼常數:明文字串寫死在排程腳本裡 | 受害主機的腳本目錄(root 可讀) | 該供應商帳號的 API 用量與帳單 | 必須視為已外洩並撤銷;是否被盜用需查供應商用量 |
| 同叢集主機的 SSH 金鑰/共用密碼 | 可能被沿用的登入憑證 | 維運人員登入其他主機 | 格式符合 PEM 私鑰或共用密碼,但無法確認這台上是否留有可用的一份 | 未取得完整盤點 | 若共用即等於橫向擴散路徑 | gap:僅能假設有,處置時一併輪替 |
| 攻擊端:礦池收款識別(SupportXMR/C3Pool) | 門羅幣錢包位址+worker 代號 | 把偷來的算力換成錢 | 礦工設定 JSON 的 user/pass 欄,只常駐在行程記憶體 | 攻擊者的 XMRig 設定與 Stratum 登入封包 | 攻擊端 IOC,不是我方憑證 | 已提交礦池;SupportXMR 回覆已將該位址 denylist |
時間軸
- 2025-09-23後門帳號 systemd 建立密碼設定日與惡意檔案時間戳一致。
- 2025-09-24 05:30植入 XMRig、rootkit sshdd.so、第二礦工離線磁碟分析取得檔案時間戳。
- 2025-09-24 00:00 之前入侵前的網站存取 log 全數消失access.log 最早一筆停在 9/24 00:00;攻擊者清過 log。
- 2025-09-24 → 2026-09-15潛伏約 356 天,單核持續替攻擊者挖礦業務失效後無人登入,所以無人發現。
- 2026-09-15偵測 → 活體取證 → 錢包 → 離線鑑識 → 關機封存全程唯讀。
356 天的帳
這台機器的業務早就死了,帳單一天也沒停。把雙方的帳擺在一起看:我們付的是雲端機時,他賺的是門羅幣。
| 項目 | 用量 | 單價 | 期間 | US$ | NT$ | 舉證 |
|---|---|---|---|---|---|---|
| EC2 t2.small(排程機) 估:業務早已失效,這 356 天的機時等於全部白付 | 8,544 小時(356 天 × 24) | US$0.0304/小時 | 2025-09-24 → 2026-09-15 | 259.74 | 8,312 | 期間可證明(後門帳號 chage -l 密碼設定日+離線磁碟檔案時間戳);金額為牌價 × 機時推算,未匯出 Cost Explorer 明細 |
| EBS 根磁碟 gp2 估:文中未載明容量,以常見的 30 GB 根磁碟計 | 30 GB × 11.7 個月 | US$0.12/GB·月 | 同上 | 42.12 | 1,348 | 容量為假設值,無帳單佐證;要證明需 Cost Explorer 的 EBS:VolumeUsage 明細 |
| 公網 IPv4 位址 估:2024-02 起 AWS 對所有公網 IPv4 位址計費 | 8,544 小時 | US$0.005/小時 | 同上 | 42.72 | 1,367 | 主機有對外服務(nginx 對公網開放)可證明位址存在;金額為牌價推算 |
| 對外流量(提交 share) 估:挖礦幾乎不吃頻寬,所以帳單上看不出任何異常——這點比金額重要 | 約 2 GB/年 | 每月首 100 GB 免費,超出 US$0.114/GB | 同上 | 0.00 | 0 | 無 VPC Flow Logs,流量僅由 Stratum 協定特性推估 |
| 我方偵測與判讀人力 估:偵測這件事本身,比這台機器一年的機時還貴 | 約 1 人日:五條線索判讀、活體取證、叢集盤點 | 顧問日費 NT$20,000~35,000(估),取中間值 NT$27,500 | 2026-09-15 | 859.38 | 27,500 | 當日鑑識步驟推估,無正式工時單;日費為假設值 |
| 攻擊者的植入成本 估:他真正的成本只有掃描時間 | 1 次入侵,工具全是現成品 | 公開的 XMRig 與 rootkit,取得成本 0 | 2025-09-24 | 0.00 | 0 | 離線鑑識取得的 binary 為公開版本 XMRig 6.22.2,非自行開發 |
| 攻擊者的聚合 proxy 主機 估:以入門級 VPS 月租推估;同一台同時吃上百台肉雞的算力 | 1 台歐洲機房 VPS × 12 個月 | 約 US$7/月 | 356 天 | 84.00 | 2,688 | proxy 位置可證明(ESTABLISHED 連線至該 IP,OVH SAS);租金為公開牌價推估 |
| 攻擊者從本機賺到的 估/推論:22.19 XMR 是整個 C4 活動的總額,不是本機損失 | 1~2 KH/s ÷ 148 KH/s × 22.19 XMR ≈ 0.15 XMR(取下限) | ≈ US$509/XMR(依下一篇 2026-09-15 匯率換算) | 356 天 | 76.42 | 2,445 | 礦池 API 的活動總額可證明,但單機算力佔比無法從礦池端取得,只能用機型算力推估 |
| 寫死在腳本裡的兩把 API key 被盜用的金額 必須視為已外洩並撤銷 | — | — | — | — | — | 缺口:未取得該供應商同期用量帳單,無法確認有沒有被消費 |
| 這台是否還被拿去做別的事(跳板、掃描、寄信) 只能假設有 | — | — | — | — | — | 缺口:入侵前的 nginx access.log 已被清除(最早一筆停在 2025-09-24 00:00),且無 VPC Flow Logs |
| 合計(僅可證明列) | 0.00 | 0 | ||||
可證明的金額:US$0。 這張表沒有一列是 proven,因為我們手上沒有這段期間的帳單明細——要把它升級成可證明,只要去 Cost Explorer 匯出這台在該期間的 BoxUsage 機時就行。這正是下一篇要講的:損害量化用機時,不是用礦池上的數字。
而推估的部分已經很清楚:他從這台賺到約 US$76,我們為此付出的雲端費用約 US$344(不含工時)。挖礦劫持的經濟學就長這樣:他的收益只是你成本的零頭,因為機時、磁碟、位址、電力、人力全部由你支付。
為什麼會被攻進來
主機上一支開發用的 Flask 對外開了一個端點,把使用者傳入的參數直接拼進 shell 執行,沒有任何驗證或白名單:
# 使用者可控的 param_n、param_data 直接進了指令字串
cmd = "python3 /var/www/app/" + param_n + ".py " + param_data
subprocess.run(cmd, shell=True)
整條路徑短得驚人:公網 → nginx :80 → Flask :5000 → 指令注入 → 以 ubuntu 執行 → ubuntu 有 NOPASSWD:ALL → root。
攻擊者 vs 防守者
- 免驗證 RCE 加免密碼 sudo,從公網到 root 只要兩步,成本趨近於零。
- 業務失效、沒人登入的機器,是最穩定的礦工宿主:沒人會看 top,也沒人會重開機。
- 行程改名、rootkit 藏檔、三重自啟、假系統帳號,每一層都在拉長「被發現」的時間。潛伏 356 天,每一天都是收益。
- worker 代號 C4 暗示這只是他上百台肉雞的其中一台。你不是被鎖定的目標,你是被掃到的。
已經中了怎麼辦
遇到 root 級 rootkit,不要嘗試就地清毒。你無法確認清乾淨,因為你看到的系統狀態本身就不可信。
- 隔離:改 Security Group 切斷對外連線(尤其 egress),但先別關機。關機會失去記憶體裡的跡證。
- 存證:趁礦工活著採集記憶體與封包(下一篇),再對 EBS 建立 snapshot。
- 輪替金鑰:撤銷這台碰過的所有憑證:API key、SSH key、共用密碼。到對應後台檢查是否有異常用量。
- 盤點連動:同一把 SSH pem、同一組密碼是否被其他主機共用?一併輪替。移除 DNS 記錄與健康檢查。
- 銷毀重建:terminate 並確認 EBS 一併刪除、Elastic IP 釋放;從乾淨映像重建,切勿沿用舊 volume。
就算移除了已知的礦工與後門,也無法證明沒有殘留的第二層植入。唯一能回到已知乾淨狀態的方法,就是砍掉重建。
重點學習
- 01load average(閒置卻高載)「沒事做卻很忙」是本案最早、最便宜、也是唯一真正有效的訊號,整條調查從這個數字開始。
- 02ss -tnpps 會被 rootkit 騙,連線表不會;rootkit 在場時這是少數還能相信的介面。
- 03ld.so.preload / LD_PRELOAD檔案一讀就 segfault 的原因,也是「你看到的系統狀態本身不可信」的根源。
- 04/proc/PID/exe行程名稱可以造假,這條符號連結指向真正的執行檔,礦工本體就是靠它認出來的。
- 05Stratum 礦池埠(3333/5555/9443)主機主動外連這些埠,幾乎等同確診;也是最容易設告警的一條防線。
- 06持久化(systemd unit/@reboot/preload)惡意程式很少只有一個啟動點,只清掉找到的那一個,隔天它就回來了。
- 07NOPASSWD:ALL應用程式執行帳號有這一行,公網上的一個請求就能直接變成 root。
- 08shell=True 指令注入本案破口的形狀:使用者參數被拼進指令字串,「資料」就變成了「命令」。
- 09chage -l(密碼變更日)後門帳號的密碼設定日是最可靠的入侵時間錨點,尤其在 log 被清掉之後。
- 10egress 管控你未必擋得住他進來,但可以讓算力送不出去;礦工連不上礦池就沒有收益。
今天就可以來試試
全部唯讀,不改任何設定;做完你會知道自己有沒有同樣的破口。
- 挑一台你說不清楚「現在在忙什麼」的主機,看負載與最耗 CPU 的行程。
uptime ps aux --sort=-%cpu | head -5看到什麼代表什麼:load 接近核心數、但你講不出這個負載對應哪一項業務,就進入下一步;能對上合法工作就先放過。 - 看它主動連去哪裡,排除 SSH 與一般 HTTPS。
sudo ss -tnp | grep ESTAB | grep -vE ':22|:443'看到什麼代表什麼:出現 3333、5555、7777、9443、13333、14444 這類埠,或連往你不認識的境外機房,直接當已感染處理,先別關機。 - 檢查有沒有 preload 型 rootkit 的痕跡。
ls -l /etc/ld.so.preload 2>&1 cat /etc/ld.so.preload 2>&1看到什麼代表什麼:檔案存在而你從沒裝過任何 preload 函式庫,或讀取時直接 segfault,代表 ps/ls/top 的輸出全部不可信。 - 盤點可以登入的帳號,以及誰有免密碼 sudo。
grep -vE 'nologin|false' /etc/passwd sudo grep -rh NOPASSWD /etc/sudoers /etc/sudoers.d 2>/dev/null看到什麼代表什麼:有 home 目錄與 shell 的「系統風格」帳號幾乎必定是後門;應用程式帳號有 NOPASSWD:ALL,等於把提權送給對方。 - 盤點開機自啟點。
ls -la /etc/systemd/system/*.service | tail -20 sudo crontab -l看到什麼代表什麼:服務描述含糊(像是某某 Plugin Service)、或 root 的 @reboot 直接執行 /var、/tmp 下的檔案,都要追到底。 - 看這台實際對外開了哪些埠。
sudo ss -tlnp看到什麼代表什麼:5000、8888、9200 這類開發/除錯埠綁在 0.0.0.0,就是對公網開著的門,與 Security Group 對照後排入本週處理。
M4 主機偵測(12 條獵捕指令)· M5 活體取證與離線鑑識 · 想在自己的環境做一次同樣的盤點,從 自我盤點 開始。
每篇新文章一封:三點摘要、攻擊者/防守者視角、今天能做的一件事。或先 加入會員 記錄你完成的任務。