Linux kexec 啟動遞迴 Quine ELF

這是一篇獨立的補充文章,接續先前四部分的系列。

閱讀先前文章的連結,如果您有興趣:

在先前的一篇文章中,我留下了一個神秘的指令。

這是什麼意思?執行它應該沒問題吧?我沒有在散佈惡意軟體吧?

好吧,好吧,我會打開它並向您展示裡面有什麼。

嗯,我猜我讓您將它透過管道傳送給 sh,所以這只是預期的,它是一個 shell 腳本。

糟糕,這堆 base64 雜訊太多了!考慮到它是一個 20MB 的 shell 腳本,這似乎是意料之中的。

好吧,如果它是一個 shell 腳本,它應該是可讀的。讓我們來看看它的開頭和結尾。

所以這個惡意軟體將一個作業系統寫入「k」和「r」,並用該作業系統取代目前的作業系統。明白了。

考慮到它將此檔案視為 ramdisk,它是一個有效的 cpio 也就不足為奇了。

一個 /bin,一個 /init,以及一個名為 k 的檔案。

k 是它先前解壓縮的 kernel 映像檔:

而 init 是一個 shell 腳本,這是可以預期的:

然後,它在 /r 中建立一個 cpio,包含除了 /proc 和 /r 之外的所有內容。

然後…嗯,它使用 /r 來 kexec /k。

一旦您到達那裡,kernel 就會被一個新的 kernel 取代,然後在 /r 中執行 /init,而這恰好是這個 /init 本身,它將用一個新的 kernel 取代目前的 kernel…

…換句話說,這是一個 Linux 發行版,它會遞迴地呼叫 kexec 自身!是不是很可愛?

在先前的文章系列中,我製作了一個 initramfs,它只是執行 curl > /dev/sda 並重新啟動。它在技術上是一個作業系統,但它也可能只是一個可執行檔。

但想想您如何執行這個可執行檔。您總是需要將它傳遞給 Linux,無論您是從引導載入程式、虛擬機還是 kexec 來執行。

這感覺很奇怪地讓人想起已經存在的事物。感覺就像您執行初始 payload 的方式是 curl | sh,或 sh myscript.sh,甚至是 python3 myscript.py。

在所有這些情況下,您都將程式傳遞給另一個程式,該程式實際上會直譯並執行其中的指令。

Initrds 是程式,而 Linux kernels 是 initrds 的直譯器程式。

這個惡意軟體中發生了一件奇怪的事情,那就是使用 kexec 進行遞迴是一種非常奇怪的遞迴形式。

CS 101 的遞迴 Fibonacci 函數範例是一種非常標準的遞迴形式。您被教導不要做這樣的事情,因為教授會輸入 1001,您就會達到 Python 的 1000 幀堆疊限制。

我給您的程式永遠不會達到這個堆疊限制。

您不是將 Linux kernels 嵌套在彼此裡面。您是用一個新的 kernel 取代每個 Linux kernel。但是您的新堆疊幀根本不會覆蓋舊的堆疊幀——它會在記憶體的不同部分建立一個新的 Linux 直譯器堆疊幀,並執行該堆疊幀,留下舊的堆疊幀。

這個 initrd 是一個尾遞迴最佳化的遞迴函數。堆疊幀替換是透過將程式資料複製到新的記憶體區塊並執行該新記憶體區塊來工作的。由於舊程式在建構新程式時仍在執行,因此這是寫入時複製的必要操作。

「現在,在這裡,您看到,您需要盡您所能地奔跑,才能留在同一個地方。如果您想去別的地方,您必須跑得比那快至少兩倍!」——《愛麗絲夢遊仙境》中的紅心皇后

有一個叫做 Quine 的概念,它是一個獨立的程式,可以列印出自身的副本。

例如,這是 Python 程式(取自 Wikipedia 頁面):

當透過管道傳送給 Python 時,它會列印出自身的副本。

好的,所以請記住上面提到的 init 進程?

如果我在結尾讓它做別的事情,也許像 cat /r 這樣的事情,它就會吐出它即將執行的 cpio。

這與它自身是完全相同的 cpio。

如果 /init 如下所示,我將會給您一個 Linux initrd 直譯器的 quine。

您可能會反駁說這個程式顯然透過讀取檔案來執行 I/O 以便執行它們。

一切皆檔案,但這些檔案是變數。實際上並沒有執行任何 I/O 來從磁碟讀取這些檔案。當腳本要求它們時,kernel 只是從 tmpfs 中讀取它們!

這就像一個 C++ 二進位檔掃描其所有記憶體並傾印其所有記憶體中的程式內容一樣,這是一個 quine。

讀者練習:我給您的惡意軟體有一個 15M 的 cpio。最小的 initrd,當在 Linux kernel 中執行時,能輸出自身?換句話說,最小的 initrd quine 是多少?

如果 Linux kernel 是一個直譯器,誰來直譯 Linux kernel 直譯器?一個程式語言被直譯意味著什麼?

讓我們首先思考傳統的直譯器是如何工作的。以 Python 和 Bash 為例,您給它們一個字串,它們解析該字串,然後執行字串中寫入的指令。

但那些腳本不是機器碼!當我執行 ./foo.sh 時,Linux 核心如何能夠執行我的 shell 腳本?

頂部的 shebang(例如 #!/bin/sh 或 #!/usr/bin/env python3)基本上告訴 Linux「透過將我傳遞給 #! 後面的東西來執行我」。

因此,這兩個指令做的事情完全相同:

並非所有可執行檔都有這個標頭。例如,/bin/sh 本身實際上有一個 ELF 標頭,\x7fELF,表示它是一個二進位可執行檔:

這很有道理。/bin/sh 是一個編譯好的二進位檔。即使是我 NixOS 安裝上的 file 命令也證實了這一點。

直譯器 /nix/store/vr7ds8vwbl2fz7pr221d5y0f8n9a5wda-glibc-2.40-218/lib/ld-linux-x86-64.so.2

直譯器?ELF 檔案也會被直譯嗎?

好吧,從 kernel 的角度來看,實際執行的程式是 ld-linux-x86-64.so.2!ELF 本身不執行動態函式庫管理,而是由 ld 這個程式來執行!

您知道嗎,我猜如果您能想像一個 Python 或 Bash 腳本,它會匯入東西然後執行原始機器碼指令,那麼 ELF 也是一種直譯語言!

如果您將 /bin/sh 傳遞給 ld-linux-x86-64.so.2,它會起作用嗎?

如果 /bin/sh 直譯 shell 腳本,而 ld.so 直譯 /bin/sh… 那麼誰來直譯 ld.so?

它不可能直譯它自己,對吧?

呼!原來它是靜態連結的,所以 Linux kernel 本身,它具有 ELF 直譯功能,可以直譯它!Linux 僅在有動態連結時才需要將 ELF 檔案委派給 ld.so。我們現在有一個基本情況!這裡沒有無限遞迴!

那麼,當您 chmod +x 一個沒有意義的檔案時會發生什麼?例如這個歸檔檔?

好的,這個「ASCII cpio archive (SVR4 with no CRC)」在其開頭有一個魔術字串 \x30\x37\x30\x37\x30\x31。因此,即使您 chmod +x 一個 cpio 檔案,您也無法執行它!

畢竟,這沒有任何意義!一個人究竟如何將一個 cpio 檔案作為程式來執行?

如果您曾經安裝過 Mono 或 Wine,您會突然發現自己能夠執行 EXE 檔案。這是因為它們配置了一個名為 binfmt_misc 的 kernel 模組,它允許您告訴 kernel「具有此魔術字串的檔案可以使用此直譯器來直譯」。

當然,如果 Linux 可以執行 EXE 檔案,那麼它絕對可以執行 cpio 檔案。

這個 QEMU 指令可以透過託管虛擬化的 Linux OS 來充當 initrd 直譯器:

將該腳本放在您系統上的某個位置,然後您可以像這樣在 binfmt 中註冊它:

這樣,您就可以執行具有可執行位元的 initrds。

CPIO 檔案的直譯器是虛擬 OS 的 kernel。

該 kernel 的直譯器是 QEMU,它虛擬化了您從中運行它的 OS 上的電腦。

我們的 QEMU 直譯器腳本可以被認為是建立了一個新的 Linux 環境堆疊幀。您讓您的 Linux 發行版在 VM 中呼叫另一個 Linux 發行版。這個堆疊可以無限高,並可能導致我們達到記憶體大小的堆疊幀限制。

讓我們應用一個尾遞迴最佳化。

然後讓我們將它放入我先前給您的惡意軟體中的 /bin/cpio-interpreter,並更新 initramfs 中的 /init 以將其註冊為 binfmt 處理器:

現在我們有一個 initrd,它衍生出真正的 init 進程,該進程是 cpio 格式的!然後它在結尾執行它,所有這些都以大多數發行版上的 initramfses 所採用的那種良好且方便的 POSIXLY_CORRECT 方式進行!

除了明顯的問題之外,我們所做的事情還有一些更嚴重的問題。

在大多數情況下,使用 binfmt 來執行腳本必須到底層。您的 #!/bin/sh 腳本必須由 /bin/sh 直譯,而 /bin/sh 又必須由 ld.so 直譯,而 ld.so 又必須由 kernel 直接直譯。

我製作了一個 binfmt 直譯器,它使用另一個 kernel 運行 CPIO,並且永遠不會到底層。

此系統上 CPIO 檔案的直譯器是其下次重新啟動的 kernel。

該 kernel 的直譯器是電腦本身。

我們在第一部分學到了 initrd 是什麼,並在第五部分用它來殺死上帝。

總之,這是原始碼。

對於您中的函數式程式設計師來說,quines 通常被稱為其運行時環境的固定點。因此,這是 Linux 直譯器作為執行 initrds 的程式的固定點。

這個陳述大致意思是「如果您被要求執行一個以字串位元組 \x30\x37\x30\x37\x30\x31 開頭的檔案,請使用 /path/to/my/script.sh 來處理它。對於其他欄位,請參閱文件 https://docs.kernel.org/admin-guide/binfmt-misc.html 以了解其含義。」

哦對了,它是一個 Nix 套件。原來 Nix 也能夠建構不包含 Nix store 的作業系統!