# Linux 資訊安全檢測與漏洞分析 [![hackmd-github-sync-badge](https://hackmd.io/n57-QrYMQlutygt1StYxiw/badge)](https://hackmd.io/n57-QrYMQlutygt1StYxiw) ###### 【Linux Information Security Auditing And Exploitation Analysis】 :::info - **Study: Linux資訊安全檢測與漏洞分析** - **Author: [name=張呈顥(武田奈々)][$_{link}$](https://windware1203.github.io/takeda.github.io/html/about.html)** - **Advisor: [name=盧東華][$_{link}$](http://dhluserver.utaipei.edu.tw/)** - **[GitHub](https://github.com/windware1203/InfoSec_study)** ::: ## :star: 研究動機 - 伺服器長久失修 - 多項資安項目缺失 - 存有不少CVE漏洞 - 伺服器提供服務眾多 - 資安威脅只會增加不會減少 ## :star: 研究目的 - 資安健全 - 資安普及 - 安全意識 ## :star: 研究內容 - 資安弱點掃描與檢測 - 自動化腳本撰寫 - Linux kernel and applications 漏洞利用與原理(CVE-2021-4034) - Looney Tunables: Local Privilege Escalation in the glibc's `ld.so`(CVE-2023-4911) :::danger **All BASH commands are executed in the ==root== permission necessarily.** ::: ## :star: yum update ### $\rm I、$ auto-update Switch to the root user's crontab: ```bash sudo crontab -e ``` Add the following line to schedule monthly system updates, ensuring that it runs with sudo: ```bash 0 3 1 * * sudo /usr/bin/yum -y update ``` In this line: 0 3 1 * * specifies the timing to run the update on the 1st day of every month at 3:00 AM. sudo /usr/bin/yum -y update is the command to update all installed packages using yum. The -y flag answers "yes" to any prompts that may come up during the update process. ## :star: Lynis > ***Lynis, an introduction > Auditing, system hardening, compliance testing*** ### $\rm I、$ Install ```bash= # Github git clone https://github.com/CISOfy/lynis cd lynis chmod +x ./lynis ./lynis # yum yum install lynis -y ``` >executing screen >![](https://i.imgur.com/SFLTDKF.png) ### $\rm II、$ Updating Check ```shell= lynis update info ``` >It shows that is the least version. > >![](https://i.imgur.com/uaVIHUA.png) ### $\rm III、$ Start the audit ```shell= sudo lynis audit system ``` >Then, we got the scan details > >![](https://i.imgur.com/0xlKMHc.png) ### $\rm IV、$ Auto-scanning one a month Open your terminal and run the following command to edit your crontab file: ```bash= crontab -e ``` Add the following line to schedule a monthly Lynis scan with a dynamic log file name that includes the month: ```bash= 0 3 1 * * /usr/sbin/lynis audit system --cronjob > /var/log/lynis_$(date +\%Y\%m).log ``` In this modified cron job: - `0` : Specifies the minute (0-59). - `3` : Specifies the hour (0-23). - `1` : Specifies the day of the month (1-31). - `*` : Specifies the month (1-12). - `*` : Specifies the day of the week (0-6, where 0 is Sunday). `0 3 1 * *` specifies the timing to run the Lynis scan on the 1st day of every month at 3:00 AM. `/usr/sbin/lynis audit system --cronjob` runs Lynis with the `--cronjob` flag to prevent Lynis from prompting for user input. > /var/log/lynis_$(date +\%Y\%m).log redirects the output of the Lynis scan to a log file with a name that includes the current year and month (e.g., "lynis_202309.log" for September 2023). Save the file and exit the text editor. With this setup, each time the cron job runs, it will create a new log file with a name that includes the current year and month, ensuring that you have separate log files for each month. ## :star: chkrootkit > Chkrootkit – Linux Rootkit Scanner Chkrootkit亦是一個免費及開源的rootkit掃描工具,它能夠在Unix系統上進行檢查rootkit的跡象。它有助於檢測隱藏的安全漏洞。Chkrootkit包含一個shell腳本及一個程式,Shell腳本將會檢查系統二進位文件以進行rootkit修改,而程式將會檢查各種安全問題。 > *chkrootkit工具於Debian的系統下安裝得比較簡單* ```bash $ sudo apt install chkrootkit ``` 於CentOS系統中你需要透過以下指令去下載。 ```bash yum update yum install wget gcc-c++ glibc-static wget -c ftp://ftp.pangeia.com.br/pub/seg/pac/chkrootkit.tar.gz tar –xzf chkrootkit.tar.gz mkdir /usr/local/chkrootkit mv chkrootkit-0.53/* /usr/local/chkrootkit # (請檢查解壓後會是什麼版本,自行套落0.5X) cd /usr/local/chkrootkit make sense ``` 現在可以開始run Chkrootkit! ```bash sudo chkrootkit (Debian) #OR /usr/local/chkrootkit/chkrootkit (CentOS) ``` 跑完後您就能夠在報告中看到自己的伺服器有沒有惡意軟體及Rootkit。 如上,如果您想要每晚自動運行及收到電郵通知,可以透過以下cron job在晚上3點自動執行並將報告發送到您的電子郵件地址。 ```bash 0 3 * * * /usr/sbin/chkrootkit 2>&1 | mail -s "chkrootkit Reports of My Server" name@example.com ``` ## :star: ClamAV >ClamAV, short for "Clam AntiVirus," is an open-source antivirus software toolkit designed to detect and combat various forms of malware, including viruses, trojans, worms, and other malicious software. ClamAV is widely used in both personal and professional settings to provide an additional layer of security against malware threats. ```bash yum -y update yum -y install clamav 安裝後啟動是十分簡單的。 freshclam clamscan -r -i DIRECTORY ``` ## :star: Linux Malware Detect 先下載 LMD 及解壓: ```bash wget http://www.rfxn.com/downloads/maldetect-current.tar.gz tar -zxvf maldetect-current.tar.gz ``` 用 root 身份安裝,目前最新版本是 1.6,當安裝時需要按解壓的目錄名稱變更: ```bash cd maldetect-1.6 ./install.sh ``` 然後將 maldet 指令建立連結到 /bin 目錄: ```bash ln -s /usr/local/maldetect/maldet /bin/maldet hash -r ``` LMD 預設安裝目錄在 /usr/local/maldet/裡面的 conf.maldet 就是 LMD 的設定檔,開啟 LMD 的設定檔: ```bash vim /usr/local/maldetect/conf.maldet ``` 修改以下選項: - 設定成 1 開啟電郵通知 ``` email_alert=”1″ ``` - 設定收取電郵的電郵地址 ``` email_addr=”you@yourdomain.com” ``` - 使用 ClamAV 掃描 ``` scan_clamscan=”1″ ``` - [ref](https://www.ltsplus.com/linux/centos-7-install-lmd-clam-antivirus) ## Nessus掃描漏洞 Nessus偵測之漏洞: HTTP TRACE / TRACK Methods Allowed CVSS V3.0: 5.3 HTTP TRACE/TRAC 通常用於 Debug,如何驗證系統是否真的有開啟 TRACE/TRACK 之功能,可使用telnet 網頁所在 80 port: `telnet 127.0.0.1 80` 之後輸入 `TRACE / HTTP/1.1 Host: localhost.localdomain` 再連續按兩下 Enter 若系統有回應: `HTTP/1.1 200 OK` 表示確實HTTP TRACE / TRACK Methods Allowed。 那該如何關閉呢?可在 Apache 之設定檔 httpd.conf 加上 TraceEnable off 即可。 `vi /etc/httpd/conf/httpd.conf` 加上字串:`"TraceEnable off"` 接著重新啟動 Apache 即可 `service httpd restart` 使用 telnet 再測試一次,系統直接回應錯誤訊息: `HTTP/1.1 403 Forbidden` 表示HTTP TRACE / TRACK 確實已關閉 ## :star: PwnKit - polkit 之 **pkexec** 指令提權漏洞 > - **keyword: Local Privilege Escalation, out-of-bounds read/write, Memory corruption, SUID-root program** > - **CVSS v. 3.x: $\color{red}{7.8\,\,\,\, HIGH}$** > - [第一次業師報告](https://www.canva.com/design/DAFeAKyP_Ss/sYjOtouW9QDGm4vOc395GQ/view#1)(CVE-2021-4034) ### $\rm I、$ polkit簡介 `polkit` 是一個在 Unix like 作業系統中,用來控制系統 process 權限的一套工具。它提供非特權 processes 以一個有系統性的方式與特權 processes 進行溝通;也可以使用`polkit`裡面具有提升權限的指令`pkexec`,來取得 root 特權(與`sudo`不同,polkit 並沒有賦予完全的 root 權限)。 > "Polkit (formerly PolicyKit) is a component for controlling system-wide privileges in Unix-like operating systems. It provides an organized way for non-privileged processes to communicate with privileged ones. It is also possible to use polkit to execute commands with elevated privileges using the command pkexec followed by the command intended to be executed (with root permission)." (Wikipedia) [Qualys](https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt) 研究員形容此漏洞是攻擊者的美夢成真: - **pkexec 被預設安裝在 Linux 的各個發行版上** - **此漏洞自 2009 年 5 月就存在了** - (commit c8c3d83,"Add a `pkexec(1)` command") - **任何非特權使用者都可以取得完整的 root 權限** - **就算 polkit 本身沒有運作,此漏洞也可以被利用** 另外,pkexec是一個sudo-like, SUID(SetUID)-root 工具,它的相關宣告如下: ``` NAME pkexec - Execute a command as another user SYNOPSIS pkexec [--version] [--disable-internal-agent] [--help] pkexec [--user username] PROGRAM [ARGUMENTS...] DESCRIPTION pkexec allows an authorized user to execute PROGRAM as another user. If PROGRAM is not specified, the default shell will be run. If username is not specified, then the program will be executed as the administrative super user, root. ``` ### $\rm II、$ 漏洞分析 要分析此漏洞,我們要觀察 source code-- [pkexec.c (ver. 0.120)](https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c),先從`main()`函數著手: ```cpp 435 main (int argc, char *argv[]) 436 { ... 534 for (n = 1; n < (guint) argc; n++) 535 { ... 568 } ... 610 path = g_strdup (argv[n]); ... 629 if (path[0] != '/') 630 { ... 632 s = g_find_program_in_path (path); ... 639 argv[n] = path = s; 640 } ``` 名詞解釋: - `int argc`: argument count,意即引數的個數;根據`C99`規範 "The value of argc shall be nonnegative." argc 的值應為非負整數。 - `char *argv[]`: argument value,引數的值。 此程式乍看之下並無任何問題,但在些許非正常使用情況下將造成漏洞威脅。 正常情況下,`argc`的值至少為 $1$ ,因為 argument list 最少包括程式自身名稱;但不幸的是,若我們使用`execve()`進行系統呼叫的時候,傳遞給它的值為空的 i.e. `{NULL}`,此時的`argc`為 $0$,`argv[0]`為`NULL`,並且: - line 534, 整數 `n` 將永遠是 $1$ - line 610, 指標 `path` 將因`argv[1]`造成越界讀取 - line 639, 指標`s`將越界寫入到`argv[1]` 那我們實際越界存取到的這個`argv[1]`究竟是什麼位置呢?要解答這個問題,必須先了解我們呼叫`execve()`的時候,kernel做了甚麼動作。當我們使用它新增了一個程式時,kernel會將引數、環境變數字串以及指標們(argv, envp)複製到此程式的堆疊結尾,圖例如下: ``` |---------+---------+-----+------------|---------+---------+-----+------------| | argv[0] | argv[1] | ... | argv[argc] | envp[0] | envp[1] | ... | envp[envc] | |----|----+----|----+-----+-----|------|----|----+----|----+-----+-----|------| V V V V V V "program" "-option" NULL "value" "PATH=name" NULL ``` 因為`argv`及`envp`指標在記憶體中是連續的,我們可以輕易的觀察到,若`arcg`是 $0$,這個越界`argv[1]`其實是`envp[0]`,這個指標指向我們的第一個環境變數`value`。 - 在第 610 行,從`argv[1]`(即`envp[0]`)中讀取的程式路徑超出了範圍,並指向 "value"; - 在第 632 行,這個路徑 "value" 被傳遞給`g_find_program_in_path()`(因為 "value" 不以斜線開頭,所以在第 629 行); - `g_find_program_in_path()`在我們的 PATH 環境變數的目錄中搜尋名為 "value" 的可執行檔; 如果找到這樣的可執行檔,則將其完整路徑返回到 pkexec 的 `main()`函數(line 632); - 並且在第 639 行,將這個完整路徑寫出到`argv[1]`(即`envp[0]`),從而覆蓋我們的第一個環境變數。 更精確地說: - 如果我們的`PATH`環境變數是`"PATH=name"`,而且十分剛好的這個目錄`name`存在在當前工作目錄,又更剛好的裡面有個可執行檔案`value`,然而此指向這個字串`"name/value"`會被越界寫入到`envp[0]`。 > "PATH=name" 可替換成 "PATH=name=" 亦成立 - 換言之,此類的越界寫入允許我們重新引入這些不安全的環境變數到`pkexec`中(e.g., LD_PRELOAD);此類不安全的變數通常在呼叫`main()`會被`ld.so`從 SUID 程式中移除。 - 此外,值得注意的是 polkit 也支援非 Linux 作業系統,如:Solaris、BSD;然而在 OpenBSD中,因為其 kernel 不接受`argc`為 $\color{green}0$ 的`execve()`,此漏洞不可被利用。 ### 漏洞利用 至於要利用何種不安全的環境變數呢?這裡的選項是很有限的,因為在越界寫入後(at line 639)不久後,pkexec 會==完全地==清除了他的環境變數(at line 702)。 ```c 639 argv[n] = path = s; ... 657 for (n = 0; environment_variables_to_save[n] != NULL; n++) 658 { 659 const gchar *key = environment_variables_to_save[n]; ... 662 value = g_getenv (key); ... 670 if (!validate_environment_variable (key, value)) ... 675 } ... 702 if (clearenv () != 0) ``` [Qualys](https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt) 指出,為了印出錯誤訊息到 stderr, pkexec 呼叫了 GLib 的`g_printerr()`; >the GLib is a [GNOME](https://www.gnome.org/) library, not the GNU C Library, aka glibc e.g., `validate_environment_variable()` 和 `log_message()` 呼叫了 `g_printerr()` (at lines 126 and 408-409): ```c! 88 log_message (gint level, 89 gboolean print_to_stderr, 90 const gchar *format, 91 ...) 92 { ... 125 if (print_to_stderr) 126 g_printerr ("%s\n", s); ... 383 validate_environment_variable (const gchar *key, 384 const gchar *value) 385 { ... 406 log_message (LOG_CRIT, TRUE, 407 "The value for the SHELL variable was not found the /etc/shells file"); 408 g_printerr ("\n" 409 "This incident has been reported.\n"); ``` `g_printerr()`通常會輸出 UTF-8 的錯誤訊息,但是若他的環境變數`CHARSET`不是 UTF-8,他也可以輸出其他的字元集。 > 這裡的`CHARSET`與安全無關,他不是不安全的環境變數之一。 為了轉換錯誤訊息從 UTF-8 到其他字元集,pkexec 呼叫了 glibc 的`iconv_open()`。 要將一種字元集的訊息轉換為另一種字元集,會使用`iconv_open() `函數來執行相關的共享 library 來進行轉換操作。通常,這個函數會使用預設設定檔(通常在`/usr/lib/gconv/gconv-modules`)中的設定,包括來源字元集、目的字元集以及 library name。 不過,也可以透過設定環境變數`GCONV_PATH`來強制`iconv_open()` 使用其他設定檔案。需要注意的是,`GCONV_PATH`屬於一個「不安全」的環境變數,因為它有潛在的風險,可能會執行任意的 liarbry。因此,在 SUID 程式中,系統會自動刪除`GCONV_PATH`變數,以確保安全性。 不幸的是,CVE-2021-4034 允許我們重新引入(re-introduce)`GCONV_PATH`進入 pkexec 當中,並執行共享函式庫,就像 root 一樣。 但此漏洞利用技術會在 log file 當中留下`"The value for the SHELL variable was not found the /etc/shells file"`(line 406, 407)。 ### $\rm III、$ 漏洞復現 #### 環境 :::success - **Linux版本:** Linux localhost.localdomain 3.10.0-1160.21.1.el7.x86_64 #1 SMP Tue Mar 16 18:28:22 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux ![](https://i.imgur.com/QOoKszB.png) - **pkexec版本:** 0.112 ![](https://i.imgur.com/v3A9Ftv.png) ::: > 提權前狀況 ![](https://i.imgur.com/E3nJJ1K.png) > 執行破解程式後 ![](https://i.imgur.com/5uSU5QJ.png) ### $\rm IV、$ 漏洞修補 >[pkexec.c (ver. 0.121)](https://gitlab.freedesktop.org/polkit/polkit/-/blob/121/src/programs/pkexec.c?ref_type=tags) 由於此CVE風險分數高達 **7.8**,修補十分迅速,邏輯也十分簡單明瞭。 ```cpp=493 /* * If 'pkexec' is called THIS wrong, someone's probably evil-doing. Don't be nice, just bail out. */ if (argc<1) { exit(127); } ``` 你沒有看錯,就是加上一個`if`進行`argc`的判斷,並且拋出`exit(127)`,如此暴力、簡單就能化解高風險漏洞;因此程式設計師對於變數、指標的變化必須要精確的掌控,以免發生此種越界讀寫的安全性問題。 > 會直接觸發`if`,拋出`exit()` > ![](https://hackmd.io/_uploads/SJXNvjRy6.png) ### $\rm V、$ 文獻 - [1] [pkexec.c](https://gitlab.freedesktop.org/polkit/polkit/-/blob/0.120/src/programs/pkexec.c) - [2] [PwnKit: Local Privilege Escalation Vulnerability Discovered in polkit’s pkexec](https://blog.qualys.com/vulnerabilities-threat-research/2022/01/25/pwnkit-local-privilege-escalation-vulnerability-discovered-in-polkits-pkexec-cve-2021-4034) - [3] [CVE-2021-4034 polkit(pkexec)提权漏洞复现](https://cloud.tencent.com/developer/article/1945253) - [4] [CVE-2021-4034 pkexec 本地提权漏洞利用解析](https://www.anquanke.com/post/id/267774#h3-5) - [5] [Qualys Security Advisory](https://www.qualys.com/2022/01/25/cve-2021-4034/pwnkit.txt) - [6] [NIST](https://nvd.nist.gov/vuln/detail/CVE-2021-4034) - [7] [RedHat](https://access.redhat.com/security/vulnerabilities/RHSB-2022-001) - [8][深入分析](https://xz.aliyun.com/t/10870) ## :star: **Looney Tunables**: Local Privilege Escalation in the glibc's `ld.so` > - **keywords: glibc, buffer overflow, SUID permission,** > - **CVSS v. 3.x: $\color{red}{7.8\,\,\,\, HIGH}$** > - date: 2023/10/03 > - [CVE-2023-4911](https://www.ithome.com.tw/news/159146) > A buffer overflow was discovered in the GNU C Library’s dynamic loader ld.so while processing the GLIBC_TUNABLES environment variable. This issue could allow a local attacker to use maliciously crafted GLIBC_TUNABLES environment variables when launching binaries with SUID permission to execute code with elevated privileges. - [Red Hat](https://access.redhat.com/security/cve/cve-2023-4911) ### GNU C Library The GNU C Library's dynamic loader "find[s] and load[s] the shared objects (shared libraries) needed by a program, prepare[s] the program to run, and then run[s] it" (man `ld.so`). The dynamic loader is extremely security sensitive, because its code runs with elevated privileges when a local user executes a set-user-ID program, a set-group-ID program, or a program with capabilities. Historically, the processing of environment variables such as LD_PRELOAD, LD_AUDIT, and LD_LIBRARY_PATH has been a fertile source of vulnerabilities in the dynamic loader. ### Analysis 在執行的一開始,`ld.so`呼叫 `__tunables_init()`(位於第279行),走訪環境變數`envp`尋找 `GLIBC_TUNABLES` 變數(第282行)。對於每個找到的 `GLIBC_TUNABLES`,它會複製此變數(第284行),呼叫 `parse_tunables()` 來處理和清理此`new_env`(第286行),最後將原始的 `GLIBC_TUNABLES` 替換為這個清理過的`new_env`(第288行): ```c=269 269 void 270 __tunables_init (char **envp) 271 { 272 char *envname = NULL; 273 char *envval = NULL; 274 size_t len = 0; 275 char **prev_envp = envp; ... 279 while ((envp = get_next_env (envp, &envname, &len, &envval, 280 &prev_envp)) != NULL) 281 { 282 if (tunable_is_name ("GLIBC_TUNABLES", envname)) 283 { 284 char *new_env = tunables_strdup (envname); 285 if (new_env != NULL) 286 parse_tunables (new_env + len + 1, envval); 287 /* 放入更新後的 envval。 */ 288 *prev_envp = new_env; 289 continue; 290 } ``` `parse_tunables()` 的第一個參數(tunestr)指向 `GLIBC_TUNABLES` 的即將被清理複製值,而第二個參數(valstring)指向原始的 `GLIBC_TUNABLES` 環境變數(在堆疊中)。為了清理 `GLIBC_TUNABLES` 的複製(應該是形式為 `"tunable1=aaa:tunable2=bbb"`),`parse_tunables()` 會從 `tunestr` 中刪除所有危險的 tunables(`SXID_ERASE tunables`),但保留 `SXID_IGNORE` 和 `NONE` tunables(位於第221-235行): ```c=162 162 static void 163 parse_tunables (char *tunestr, char *valstring) 164 { ... 168 char *p = tunestr; 169 size_t off = 0; 170 171 while (true) 172 { 173 char *name = p; 174 size_t len = 0; 175 176 177 while (p[len] != '=' && p[len] != ':' && p[len] != '\0') 178 len++; 179 180 181 if (p[len] == '\0') 182 { 183 if (__libc_enable_secure) 184 tunestr[off] = '\0'; 185 return; 186 } 187 188 189 if (p[len]== ':') 190 { 191 p += len + 1; 192 continue; 193 } 194 195 p += len + 1; 196 197 198 char *value = &valstring[p - tunestr]; 199 len = 0; 200 201 while (p[len] != ':' && p[len] != '\0') 202 len++; 203 204 205 for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++) 206 { 207 tunable_t *cur = &tunable_list[i]; 208 209 if (tunable_is_name (cur->name, name)) 210 { ... 217 if (__libc_enable_secure) 218 { 219 if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE) 220 { 221 if (off > 0) 222 tunestr[off++] = ':'; 223 224 const char *n = cur->name; 225 226 while (*n != '\0') 227 tunestr[off++] = *n++; 228 229 tunestr[off++] = '='; 230 231 for (size_t j = 0; j < len; j++) 232 tunestr[off++] = value[j]; 233 } 234 235 if (cur->security_level != TUNABLE_SECLEVEL_NONE) 236 break; 237 } 238 239 value[len] = '\0'; 240 tunable_initialize (cur, value); 241 break; 242 } 243 } 244 245 if (p[len] != '\0') 246 p += len + 1; 247 } 248 } ``` 不幸的是,如果 `GLIBC_TUNABLES` 環境變數的形式是 `"tunable1=tunable2=AAA"`(其中 `"tunable1"` 和 `"tunable2"` 是 `SXID_IGNORE` tunables,例如 `"glibc.malloc.mxfast"`),則: 在`parse_tunables()`中的`while (true)`的第一次迭代中,整個`"tunable1=tunable2=AAA"`被原地複製到 `tunestr`(在第221-235行),從而填滿了`tunestr`; 在第247-248行,`p`沒有增加(因為在第203-204行沒有找到`':'`),因此`p`仍然指向`"tunable1"`的值,即`"tunable2=AAA"`; 在`parse_tunables()`中的`while (true)`的第二次迭代中,`"tunable2=AAA"`被附加(彷彿它是第二個 tunable)到`tunestr`(它已經滿了),從而溢出了 tunestr。 ### 漏洞利用 這個漏洞是一個直接的緩衝區溢位,但是為了實現任意代碼執行,我們應該覆寫什麼呢?我們溢出的緩衝區是由`tunables_strdup()`在第284行分配的,這是`strdup()`的重新實作,它使用了`ld.so`的`__minimal_malloc()`而不是glibc的`malloc()`,而glibc的`malloc()`沒有被初始化。這個`__minimal_malloc()`實現只是簡單地呼叫`mmap()`來從核心獲取更多記憶體。 那麼問題來了,在mmap區域中,我們應該覆寫哪些可寫分頁呢?據我們所知,我們只有兩個選擇,因為這個緩衝區溢出發生在ld.so的執行的最開始。 `ld.so`本身的可讀寫ELF段,實際上這個可讀寫段的前幾頁是ld.so的RELRO段,但它們還沒有使用`mprotect()`設為唯讀: ``` 7f209f367000-7f209f369000 r--p 00000000 fd:00 10943 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f209f369000-7f209f393000 r-xp 00002000 fd:00 10943 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f209f393000-7f209f39e000 r--p 0002c000 fd:00 10943 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7f209f39f000-7f209f3a3000 rw-p 00037000 fd:00 10943 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 `````` 然而,在我們檢查的所有Linux發行版中,`ld.so`的可讀寫段下方的未映射空間最多只有一頁,但是`ld.so`的`__minimal_malloc()`總是分配至少兩頁(為了減少mmap呼叫次數而多分配一頁)。換句話說,我們溢出的緩衝區不能立即分配到`ld.so`的可讀寫段下方,因此無法覆寫該段。 我們唯一的選擇是覆寫由`tunables_strdup()`本身分配的`mmap()`分頁:因為`__tunables_init()`可以處理多個`GLIBC_TUNABLES`環境變數,且由於Linux核心的`mmap()`是自頂向下分配的,我們可以`mmap()`第一個`GLIBC_TUNABLES`(而不溢出),然後`mmap()`第二個`GLIBC_TUNABLES`(直接在第一個下方),再溢出它,從而覆寫第一個`GLIBC_TUNABLES`。 用一個完全不同的環境變數(例如`LD_PRELOAD`或`LD_LIBRARY_PATH`)替換這第一個`GLIBC_TUNABLES`,但這些危險的變數在稍後被`ld.so`在`process_envvars()`中移除,因此這樣的替換將是無用的; 或者,用一個包含危險`SXID_ERASE`可調參數的`GLIBC_TUNABLES`替換第一個,這些參數在`parse_tunables()`中先前被刪除。雖然這起初似乎很有希望,但利用這樣的替換需要一個SUID-root程序,該程序以`setuid(0)`並以保留的環境;以root身份處理危險的`GLIBC_TUNABLES`,但不啟用`__libc_enable_secure`)執行另一個程序。 此時,情況看起來相當絕望,但是`ld.so`的`_dl_new_object()`中的一個註解引起了我們的注意(在第105行): ```c 56 struct link_map * 57 _dl_new_object (char *realname, const char *libname, int type, 58 struct link_map *loader, int mode, Lmid_t nsid) 59 { .. 84 struct link_map *new; 85 struct libname_list *newname; .. 92 new = (struct link_map *) calloc (sizeof (*new) + audit_space 93 + sizeof (struct link_map *) 94 + sizeof (*newname) + libname_len, 1); 95 if (new == NULL) 96 return NULL; 97 98 new->l_real = new; 99 new->l_symbolic_searchlist.r_list = (struct link_map **) ((char *) (new + 1) 100 + audit_space); 101 102 new->l_libname = newname 103 = (struct libname_list *) (new->l_symbolic_searchlist.r_list + 1); 104 newname->name = (char *) memcpy (newname + 1, libname, libname_len); 105 /* newname->next = NULL; We use calloc therefore not necessary. */ ``` ld.so使用`calloc()`為這個`link_map`結構分配記憶體,因此並未顯示初始化其各個成員為零;這是一個合理的優化。正如之前提到的,這裡的`calloc()`不是glibc的`calloc()`,而是ld.so的`__minimal_calloc()`,它呼叫`__minimal_malloc()`而不是將返回的記憶體顯示初始化為零;這也是一個合理的優化,因為就所有目的而言,`__minimal_malloc()`總是回傳一個由kernel初始化為零的乾淨的`mmap()`記憶體區塊。 不幸的是,`parse_tunables()`中的緩衝區溢出允許我們用非零字串覆寫乾淨的`mmap()`記憶體,從而覆寫即將分配的`link_map`結構的指標為非`NULL`值。這使我們完全打破了`ld.so`的邏輯,該邏輯假定這些指標是`NULL`。 我們首先嘗試通過覆寫`link_map`結構的`l_next`和`l_prev`指標(`link_map`結構的雙向鏈結map)來利用這個緩衝區溢出,但由於在`setup_vdso()`中出現了兩個`assert()`ion失敗,我們失敗了,這立即導致了`ld.so`的中止(我們檢查的所有發行版都編譯了帶有`assert()`ions的glibc,因此也是`ld.so`): ```c 96 assert (l->l_next == NULL); 97 assert (l->l_prev == main_map); ``` 我們後來意識到`link_map`結構中有更多指標沒有明確初始化為`NULL`;特別是在指標數組`l_info[]`中指向`Elf64_Dyn`結構的指標。其中,`l_info[DT_RPATH]`,即"Library search path"(庫搜索路徑),立即引起注意:如果我們覆寫此指標並控制它指向的位置和內容,那麼我們可以迫使`ld.so`信任我們擁有的目錄,因此從這個目錄加載我們自己的`libc.so.6`或`LD_PRELOAD`庫,並執行任意代碼(如果我們通過SUID-root程序運行`ld.so`)。 #### 覆寫的`l_info[DT_RPATH]`應該指向哪裡? 這個問題的簡單答案是:Stack,更確切地說是我們Stack中的環境字元串。在Linux上,Stack在一個16GB的區域內進行隨機化,而我們的環境字元串最多可以占用6MB(`_STK_LIM / 4 * 3`,在kernel's `bprm_stack_limits()`中):在16GB / 6MB = 2730 次嘗試後,我們有很大的機會猜測我們環境字元串的地址,在我們的 exploit 中,我們始終將 `l_info[DT_RPATH]` 覆寫為 `0x7ffdfffff010`,這是隨機化Stack區域的中心。在我們的測試中,這種暴力方法在 Debian 上大約需要30秒,在 Ubuntu 和 Fedora 上需要約5分鐘。 #### 覆寫的`l_info[DT_RPATH]`應該存放什麼? 換句話說,在我們6MB的環境字元串中,`l_info[DT_RPATH]` 是一個指向小型(16B)`Elf64_Dyn`結構的指標: - 一個 `int64_t d_tag`,應該是 `DT_RPATH (15)`,但實際上這個值在任何地方都沒有被檢查,因此我們可以在這裡存儲任何東西; - 一個 `uint64_t d_val`,它是正在執行的SUID-root程序的ELF字元串表的偏移量(此偏移量參考的字元串就是Library search path本身)。 在我們的 exploit 中,我們只是將我們6MB的環境字元串填充為 `0xfffffffffffffff8 (-8)`,因為在大多數SUID-root程序的字元串表的偏移 -8B 的地方,出現了字元串 `"\x08"`:這迫使`ld.so`信任一個名為 `"\x08"` 的相對目錄(在我們當前的工作目錄中),因此允許我們從這個目錄中加載和執行我們自己的`libc.so.6`或`LD_PRELOAD`庫,以 root 權限運行。 本文中描述的攻擊方法適用於幾乎所有Linux上預設安裝的SUID-root程序;其中一些例外情況包括: - 所有發行版上的 `sudo`,因為它指定了自己的ELF RUNPATH(`/usr/libexec/sudo`),這會覆蓋我們的 `l_info[DT_RPATH]`; - 在 Fedora 上的 `chage` 和 `passwd`,因為它們受到特殊的SELinux規則保護; - 在 Ubuntu 上的 `snap-confine`,因為它受到特殊的AppArmor規則保護。 雖然 glibc 2.34 受到這個緩衝區溢出的影響,但其 `tunables_strdup()` 使用 `__sbrk()`,而不是 `__minimal_malloc()`(它在 glibc 2.35 中由commit `b05fae` 引入,`"elf: Use the minimal malloc on tunables_strdup"`);我們尚未調查 glibc 2.34 是否是可利用的。 ### 漏洞修補 > elf: Ignore GLIBC_TUNABLES for setuid/setgid binaries > > The tunable privilege levels were a retrofit to try and keep the malloc tunable environment variables' behavior unchanged across securityboundaries. However, CVE-2023-4911 shows how tricky can be tunable parsing in a security-sensitive environment. > > Not only parsing, but the malloc tunable essentially changes some semantics on setuid/setgid processes. Although it is not a direct security issue, allowing users to change setuid/setgid semantics is not a good security practice, and requires extra code and analysis to check if each tunable is safe to use on all security boundaries. > > It also means that security opt-in features, like aarch64 MTE, would need to be explicit enabled by an administrator with a wrapper script or with a possible future system-wide tunable setting. > > Co-authored-by: Siddhesh Poyarekar <siddhesh@sourceware.org> > > Reviewed-by: DJ Delorie <dj@redhat.com> > > -[git](https://sourceware.org/git/?p=glibc.git;a=commit;h=9c96c87d60eafa4d78406e606e92b42bd4b570ad) - https://sourceware.org/git/?p=glibc.git;a=commit;h=a72a4eb10b2d9aef7a53f9d2facf166a685d85fb - https://sourceware.org/git/?p=glibc.git;a=commit;h=9c96c87d60eafa4d78406e606e92b42bd4b570ad ### reference - [cve.mitre.org](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-4911) - [Ubuntu](https://ubuntu.com/security/CVE-2023-4911) - [qualys](https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt)