LFSR 保護 C2 通訊,UPnP 與 Epoll 創造跳板!
摘要
1. 簡介
僵屍網路經營者過去一貫依賴寫死 (hardcoded) 的 IP 位址或網域產生演算法 (Domain Generation Algorithm, DGA) 來維持命令與控制 (C2) 的持續性,以因應查緝壓力。Dysphoria 代表了此模式的進一步演化:它融合了兩個既有的 Mirai 衍生家族
jackskid
與
fbot
的程式碼血緣,同時引入了一個錨定於以太坊名稱服務 (Ethereum Name Service, ENS) 與 Solana 名稱服務 (Solana Name Service, SNS) 網域的 C2 解析層。與集中式註冊的 DNS 名稱不同,使用區塊鏈的命名記錄無法被註冊商扣押,也無法被單一司法管轄區下令下線,此特性先前已被分析為區塊鏈命名生態系統中惡意程式防禦者所面臨的結構性挑戰。
[2]
類似的設計選擇——使用網際網路電腦協定 (Internet Computer Protocol) 的容器 (canister) 而非 ENS/SNS 記錄作為抗竄改的 C2 儲存空間——已在
CanisterWorm 攻擊活動分析
中被記錄,這顯示去中心化帳本的 C2 儲存方式正逐漸成為跨不同惡意程式家族中反覆出現的架構主題,而非孤立的實驗。
讓 Dysphoria 更為突出的特點在於,它將其 bot 群體劃分為兩個功能性角色:傳統的 DDoS 攻擊代理,以及一個專門用途的中繼/Proxy 變體(於 2026 年 6 月下旬首次被觀察到),該變體移除了所有攻擊程式碼,並將受感染的主機轉換為透明的網路中繼。這種分工——將流量產生功能與位址隱藏功能分開——是下方第 4 與第 5 節的重點。
2. 字串混淆:結合 LCG 與 LFSR 強化的修改版 RC4
DDoS 樣本的字串解密常式在兩個方面不同於標準的 RC4。首先,在標準的金鑰排程演算法 (Key-Scheduling Algorithm, KSA) 步驟之後,第二個 KSA 階段使用一個乘法器為
0x41C64E6D
的 LCG 反覆擾亂 S-box,總共執行五個完整的 256 次重覆週期。其次,偽隨機產生演算法 ( Pseudo-Random Generation Algorithm, PRGA) 階段引入了一個反饋多項式常數為
0xD800A4
的 LFSR,其輸出位元組在進行最終的位元旋轉和二次 XOR 之前,會先與 S-box 查詢結果進行 XOR,以衍生出每個金鑰流位元組。這種分層機制意味著,要還原明文,不僅需要複製 RC4 的置換狀態,還需要完全複製 LCG 和 LFSR 的位元步進順序,這相較於標準的 RC4 實作提高了靜態分析的難度,同時在資源受限的 IoT 硬體上執行時,其計算開銷仍然微不足道。
- KEY = (
- 0x08, 0x45, 0x3C, 0xD1, 0xF9, 0x5D, 0x5C, 0x27,
- 0x61, 0x21, 0x1C, 0xBD, 0xE3, 0x0F, 0x3B, 0x9C,
- )
- def decrypt(data: bytes) -> bytes:
- s = list(range(256))
- # --- KSA stage 1: standard RC4 key scheduling ---
- j = 0
- for i in range(256):
- j = (j + s[i] + KEY[i & 15]) & 0xff
- s[i], s[j] = s[j], s[i]
- # --- KSA stage 2: LCG-driven S-box scrambling (5 full passes) ---
- # This extra stage is NOT part of classic RC4; it defeats
- # generic RC4 keystream recovery tools that only replay KSA 1.
- lcg = 0xE0A4CBD6
- for _ in range(5):
- for i in range(256):
- lcg = (lcg * 0x41C64E6D + 12345) & 0xffffffff
- r = (lcg >> 16) & 0xff
- s[i], s[r] = s[r], s[i]
- out = bytearray(data)
- # --- PRGA stage: RC4 stream generation + LFSR keystream masking ---
- i, j, k = 0, 0, 0
- lfsr = 0xF31C8B2E # 32-bit LFSR state, feedback poly 0xD800A4
- for p in range(len(out)):
- i = (i + 1) & 255
- si = s[i]
- j = (j + si) & 255
- sj = s[j]
- s[i], s[j] = sj, si # Swap 1 (classic RC4 swap)
- k = (k + sj) & 255 # Swap 2 (non-standard third index)
- tmp = s[k]
- s[k] = si
- s[j] = tmp
- idx = (tmp + si + sj) & 255 # combined lookup index
- # LFSR step: Fibonacci-style, taps encoded in 0xD800A4
- if lfsr & 1:
- lfsr = ((lfsr >> 1) ^ 0xD800A4) & 0xffffffff
- else:
- lfsr >>= 1
- x = s[idx] ^ (lfsr & 0xff)
- ks = ((x << 3) | (x >> 5)) & 0xff # rotate-left 3
- ks ^= x >> 4 # final XOR mask
- out[p] ^= ks # keystream byte applied to ciphertext
- return bytes(out)
如同原始報告所指出的,這種「借用」自
jackskid
設計哲學的 KSA 強化方法的重複使用,暗示了兩個家族之間可能存在共享的工具鏈或共同的開發者血緣,而非獨立的趨同設計。
3. 區塊鏈網域 C2 解析與 IPv4 反混淆
Dysphoria 會查詢三個區塊鏈網域上的 TXT 等效記錄,這些網域對應到不同的語意金鑰,如下表摘要所示。
| 命名系統 | 網域 | 查詢金鑰 | 功能 |
|---|---|---|---|
| ENS | burrberry.eth | node | 中繼分發節點 IP |
| ENS | ukranianhorseriding.eth | network | 基礎網路基礎設施 |
| SNS | 24carnforth2merseyside.sol | deserialized | 基礎網路基礎設施 |
回傳的記錄文字中嵌入了由豎線字元分隔的誘餌 IPv6 格式字串,樣本會從中提取特定的 4 位元組區段,並應用一個特製的半位元組交換/旋轉/XOR 置換函數
F()
,以還原出真實的 IPv4 位址。由於該轉換是確定性但非標準的,因此從區塊鏈記錄中對「看似 IP」的字串進行單純的正規表示式提取,只會得到誘餌位址。
- def ror8(x, r):
- # 8-bit rotate-right, used to diffuse the nibble-swapped byte
- return ((x >> r) | (x << (8 - r))) & 0xFF
- def F(x, i, key=0x80408454):
- # Per-byte permutation applied to each of the 4 extracted octets.
- # i = byte position (0-3), key = 32-bit constant split into bytes.
- x = ((x >> 4) | (x << 4)) & 0xFF # swap high/low nibble
- x = ror8(x, i + 1) # position-dependent rotation
- k = (key >> (24 - 8 * i)) & 0xFF # select key byte for position i
- return ((x ^ k) + k) & 0xFF # XOR then modular add with key byte
- def decode_ip(ip_bytes):
- # ip_bytes: 4 raw bytes pulled from the decoy IPv6 text field
- k = 0x80408454
- return ".".join(str(F(ip_bytes[i], i, k)) for i in range(4))
- # Example: decoding the 4-byte segment "12e7:13d7" from the
- # fake IPv6 string yields the real relay-node IPv4 address.
- ip_bytes = bytearray.fromhex("12e713d7")
- print(f"Decoded IP: {decode_ip(ip_bytes)}")
兩階段的解析設計——首先從區塊鏈記錄中解碼出中繼分發 IP,然後透過普通的 HTTP 向該節點查詢「真正的」C2 位址列表——其機制如下圖所示。
圖 1 — 根據原始報告中描述的解析與封包流程重建的 C2 獲取序列。
至關重要的是,該報告自身的基礎設施追蹤發現,最終由分發節點交回的位址,本身也是被重新利用為中繼的其他受感染主機,而非真正的後端——這是一個雙層間接架構,在功能上類似於使用無伺服器邊緣平台作為隱藏來源的匿名化前端,此模式在 一場將 C2 隱藏在 Cloudflare Workers 背後的供應鏈攻擊活動 中也有記錄。在這兩種情況中,可觀測的網路端點都是為前端服務的可丟棄基礎設施,其後隱藏著由操作者控制的後端,這使得歸因與查緝變得更加複雜。
4. 固定長度二進位通訊協定
Login 與 Heartbeat 封包都填充為固定的 78 位元組,這種設計能扁平化流量大小的指紋特徵,並簡化在資源受限的目標上的解析過程。Login 封包含有一個 2 位元組的型別欄位、一個 12 位元組的 magic 常數、一個長度欄位,以及一個可變長度的值;Heartbeat 封包則重用了相同的標頭,但省略了 payload。
Login 封包 (78 位元組): [0..1] Type = 02 00 [2..13] Magic = 00 80 00 5a 00 57 00 c8 00 f0 00 1e [14..15] Length (Value 欄位的長度) [16..] Value (可變長度 payload) [..77] Padding (補零至 78 位元組) Heartbeat 封包 (78 位元組): [0..1] Type = 00 00 [2..13] Magic = 22 ba 15 24 1a 6f 04 d4 1f 9c 0d 06 [14..77] Padding (補零至 78 位元組)
在成功 handshake 之後發送的 DDoS 攻擊命令是一個巢狀可變長度結構,而非固定記錄,這允許將任意數量的目標和每個攻擊的選項旗標封裝到單一命令中:
攻擊命令: +----------+---------+-----------+----------------------+---------+------------------+ | Duration | AtkType | TargetCnt | targets[TargetCnt] | FlagCnt | flags[FlagCnt] | | (2B) | (1B) | (1B) | 巢狀結構陣列 | (1B) | 巢狀結構 | +----------+---------+-----------+----------------------+---------+------------------+ target 結構: flag 結構: +-----------+---------+ +--------+---------+------------+ | IP (4B) | Netmask | | Option | Length | Value[Len] | | | (1B) | | (1B) | (2B) | | +-----------+---------+ +--------+---------+------------+
flag 結構的 Type-Length-Value (TLV) 編碼表明操作者預期攻擊選項集會持續擴展,因為當引入新的 flag 選項時,TLV 解析器無需修改——只需在操作者端添加新的選項識別碼,而無需變更 bot 已解析的線路格式。
5. 中繼/Proxy 變體:UPnP 連接埠映射與使用 Epoll 的通道
六月下旬引入的功能性拆分——一個移除所有攻擊程式碼、僅用於中繼流量的變體——將一個普通的、受 NAT 約束的消費級裝置轉換為網際網路可存取的 Proxy。它透過在本地網路上廣播 UPnP 發現訊息,然後呼叫閘道器的 WAN 連線服務,在單次突發中請求約 155 個同時的連接埠映射。一旦映射完成,這些連接埠上的任何入站連線,都會使用 Linux 的
epoll
非阻塞 I/O 介面,橋接到對相同連接埠號碼上、朝向真正後端的匹配出站連線。這種雙向、保留連接埠的中繼,使得攻擊者面對的流量與真正的後端流量能夠在跳躍點上保持相同的連接埠號碼,從而最大程度地減少操作者必須追蹤的設定狀態。
圖 2 — 根據報告中對 UPnP 映射與 epoll 驅動橋接的描述重建的中繼節點通道序列。
每隔四秒或更長時間,中繼會將其自身的健康狀態回報給一個固定的收集網域,而非解析出的 C2 位址本身,從而將狀態遙測與攻擊命令的傳遞分離:
- {
- "status": "ONLINE",
- "connections": 42,
- "bandwidth_mbps": 12.5
- }
- // Sent via HTTP/JSON POST to login.trees4sale.net:9000
- // Purpose: lets the operator dashboard track relay capacity
- // and connection load without exposing this data through the
- // same channel used for DDoS command dispatch.
結合第 3 節與第 5 節的總體效果是一個雙層間接架構:區塊鏈記錄指向中繼分發節點,而這些節點本身是執行在受感染消費級硬體上的 Proxy,而非操作者擁有的基礎設施。這在網路拓撲層面上,與早期觀察到的 C2 系統日益偏好使用能將惡意流量混入防禦者無法簡單全面封鎖的基礎設施中的機制(無論是訊息平台、無伺服器運算,還是現在的家庭中繼群)的現象一致——此主題在對 IRC 為基礎的僵屍網路控制頻道被較不顯眼的傳輸方式取代 的分析中也有提及。UPnP 濫用的傳播表面本身也已被單獨描述為範圍廣泛,因為很大一部分曝露在網際網路的消費級閘道器會向未經認證的區域網路探索暴露使用 SOAP 的 UPnP 控制介面。 [3]
6. 傳播與漏洞涵蓋範圍
與其 Mirai 血統一致,Dysphoria 主要透過 Telnet/SSH 認證暴力破解,以及一系列針對路由器、閘道器和攝影機的 IoT 遠端程式碼執行漏洞進行傳播。其組合包含了長期被利用的經典漏洞,以及近期於 2025 年才揭露的漏洞,這顯示了其漏洞攻擊鏈是持續維護的,而非靜態的 payload。這種混合年份的漏洞利用策略——將傳統的預設認證攻擊與持續更新的 n-day 漏洞配對——與 另一個多階段僵屍網路攻擊鏈 中描述的多階段、雙語言的傳播邏輯相似,在該案例中,初始存取選項的廣度而非任何單一漏洞,才是其具備韌性的策略。
7. 討論與結論
Dysphoria 的技術軌跡說明了僵屍網路 C2 工程設計的更廣泛轉變:從依賴單一的 DNS 故障點,轉向由以下元素構成的分層間接架構:(a) 抗竄改的區塊鏈命名記錄,任何註冊商都無法單方面撤銷, [2] (b) 一個中間的中繼發現跳躍點,透過可丟棄的節點列表來掩蓋真正的後端,以及 (c) 一個專門的中繼 bot 類別,透過自動化的 UPnP 映射將住宅 IoT 裝置轉換為可丟棄的 Proxy 基礎設施。這些組件單獨來看都不新穎,但它們被組合成一個單一、且持續維護的家族——並與兩個先前的 Mirai 衍生品共享程式碼血緣——展現了商品化殭屍網路工具如何逐漸收斂到過去主要出現在資源更充足、目標更明確的攻擊活動中的防禦規避技術。對防禦者而言,實際的啟示是,以 IP 封鎖清單為中心的緩解措施,在結構上與一個 bot 所聯繫的 C2「位址」本質上是另一個受害者的架構並不相匹配。